Prajwal782007 commited on
Commit
e3fbc9c
·
1 Parent(s): 2e0c292

docs: add reward structure and deployment guide, update baseline scores, and implement dashboard UI

Browse files
README.md CHANGED
@@ -113,6 +113,24 @@ Episode **grade** is returned by `GET /grade` after the episode completes (or af
113
 
114
  ---
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  ## HTTP API
117
 
118
  Base URL: `http://<host>:7860` (default in container: port **7860**).
@@ -231,6 +249,75 @@ The image runs **supervisord** as a non-root user with two programs: Go server (
231
 
232
  ---
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  ## License
235
 
236
  See `LICENSE` in the repository.
 
113
 
114
  ---
115
 
116
+ ## Reward Structure
117
+
118
+ The environment uses a **dense, multi-component reward** signal. Each step returns a scalar `reward` (the sum) and a detailed `reward_components` breakdown in `info`:
119
+
120
+ | Component | Key | Active | Description |
121
+ |-----------|-----|--------|-------------|
122
+ | **Cost Savings** | `cost_savings` | All tasks | Positive baseline (1.5) minus relative step cost. Smart agents that reduce energy use earn higher rewards. |
123
+ | **Temperature Constraint** | `temp_constraint` | Task 2, 3 | Gaussian bonus for staying near setpoint (21°C). Max +1.5 at setpoint, degrades toward bounds, penalty outside [19°C, 23°C]. |
124
+ | **Grid Demand Response** | `grid_response` | Task 3 | Bonus for shedding load during high grid stress (>0.7), readiness bonus during moderate stress, penalty for unnecessary shedding. |
125
+ | **Efficiency Bonus** | `efficiency_bonus` | All tasks | Rewards thermal storage arbitrage (charge during cheap prices, discharge during expensive) and maintaining useful storage levels. |
126
+ | **Stability Reward** | `stability_penalty` | All tasks | Positive reward for smooth, stable control; penalty for rapid HVAC/storage oscillation. |
127
+ | **Deadline Penalty** | `deadline_penalty` | Task 2, 3 | Penalty per missed batch job deadline (-1.5 each). Positive bonus for keeping jobs on track. |
128
+ | **Carbon Reward** | `carbon_reward` | Task 3 | Baseline bonus for low-carbon operation, reduced by carbon-heavy consumption. Extra bonus during clean grid periods. |
129
+
130
+ **Grading weights (Task 3):** cost 28%, temperature 20%, grid_response 20%, batch_deadline 12%, carbon 20%.
131
+
132
+ ---
133
+
134
  ## HTTP API
135
 
136
  Base URL: `http://<host>:7860` (default in container: port **7860**).
 
249
 
250
  ---
251
 
252
+ ## Example API Calls
253
+
254
+ With the container running (`docker run -p 7860:7860 gridmind-rl`):
255
+
256
+ ```bash
257
+ # Health check
258
+ curl http://localhost:7860/health
259
+ # → {"status":"ok","version":"1.0.0"}
260
+
261
+ # Reset to Task 3 (hard) with seed 42
262
+ curl -X POST http://localhost:7860/reset \
263
+ -H "Content-Type: application/json" \
264
+ -d '{"task_id": 3, "seed": 42, "num_buildings": 1}'
265
+ # → {"observations":[{"indoor_temperature":21.3,...}],"episode":1,"task_id":3,"seed":42}
266
+
267
+ # Take one step
268
+ curl -X POST http://localhost:7860/step \
269
+ -H "Content-Type: application/json" \
270
+ -d '{"hvac_power_level": 0.6, "thermal_charge_rate": 0.3, "batch_job_slot": 1, "load_shed_fraction": 0.1}'
271
+ # → {"observation":{...},"reward":2.15,"done":false,"info":{"reward_components":{...},...}}
272
+
273
+ # Get current state
274
+ curl http://localhost:7860/state
275
+ # → {"buildings":[...],"price_curve_episode":[...],"step":1,"task_id":3,...}
276
+
277
+ # Grade after episode ends (run 96 steps first)
278
+ curl http://localhost:7860/grade
279
+ # → {"task_id":3,"score":0.3115,"sub_scores":{"cost":0.25,"temperature":0.14,...},...}
280
+
281
+ # List all tasks
282
+ curl http://localhost:7860/tasks
283
+ # → [{"id":1,"name":"Cost Minimization","difficulty":"easy",...},...]
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Hugging Face Space Deployment
289
+
290
+ ### 1. Create a new HF Space
291
+
292
+ Go to [huggingface.co/new-space](https://huggingface.co/new-space) and create a **Docker** space. Select:
293
+ - **SDK:** Docker
294
+ - **Hardware:** CPU Basic (2 vCPU, 16GB)
295
+
296
+ ### 2. Push code to HF
297
+
298
+ ```bash
299
+ # Clone and push
300
+ git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/gridmind-rl
301
+ git push hf main
302
+ ```
303
+
304
+ ### 3. Verify deployment
305
+
306
+ Once the Space builds, verify at your Space URL:
307
+
308
+ ```bash
309
+ curl https://YOUR_USERNAME-gridmind-rl.hf.space/health
310
+ # → {"status":"ok","version":"1.0.0"}
311
+
312
+ curl -X POST https://YOUR_USERNAME-gridmind-rl.hf.space/reset \
313
+ -H "Content-Type: application/json" \
314
+ -d '{"task_id":1,"seed":42}'
315
+ ```
316
+
317
+ > **Note:** HF Spaces only exposes **one port** publicly. Port **7860** (the OpenEnv API) is the primary port and will be the public endpoint. The dashboard on port 7861 is for local development only.
318
+
319
+ ---
320
+
321
  ## License
322
 
323
  See `LICENSE` in the repository.
baseline_scores.json CHANGED
@@ -16,9 +16,9 @@
16
  {
17
  "task_id": 1,
18
  "seed": 1100,
19
- "total_reward": -54.91106240679752,
20
  "total_steps": 96,
21
- "elapsed_sec": 0.8684265613555908,
22
  "score": 0.2776,
23
  "sub_scores": {
24
  "cost": 0.277555958007489
@@ -28,9 +28,9 @@
28
  {
29
  "task_id": 2,
30
  "seed": 1200,
31
- "total_reward": -573.2793620498348,
32
  "total_steps": 96,
33
- "elapsed_sec": 0.9907081127166748,
34
  "score": 0.2182,
35
  "sub_scores": {
36
  "cost": 0.2595566056450961,
@@ -41,9 +41,9 @@
41
  {
42
  "task_id": 3,
43
  "seed": 1300,
44
- "total_reward": -670.8653705366278,
45
  "total_steps": 96,
46
- "elapsed_sec": 0.8988945484161377,
47
  "score": 0.3115,
48
  "sub_scores": {
49
  "batch_deadline": 1,
 
16
  {
17
  "task_id": 1,
18
  "seed": 1100,
19
+ "total_reward": 114.28893759320243,
20
  "total_steps": 96,
21
+ "elapsed_sec": 1.3370721340179443,
22
  "score": 0.2776,
23
  "sub_scores": {
24
  "cost": 0.277555958007489
 
28
  {
29
  "task_id": 2,
30
  "seed": 1200,
31
+ "total_reward": -625.6665397814021,
32
  "total_steps": 96,
33
+ "elapsed_sec": 1.229602336883545,
34
  "score": 0.2182,
35
  "sub_scores": {
36
  "cost": 0.2595566056450961,
 
41
  {
42
  "task_id": 3,
43
  "seed": 1300,
44
+ "total_reward": -639.8462871515986,
45
  "total_steps": 96,
46
+ "elapsed_sec": 1.1910581588745117,
47
  "score": 0.3115,
48
  "sub_scores": {
49
  "batch_deadline": 1,
dashboard/static/dashboard.js CHANGED
@@ -1,6 +1,6 @@
1
  /**
2
- * GridMind-RL Dashboard — Chart.js real-time visualization
3
- * Polls /api/state every 500ms and updates all charts.
4
  */
5
 
6
  'use strict';
@@ -21,23 +21,30 @@ let currentBuilding = 0;
21
  let pollTimer = null;
22
  let connected = false;
23
 
24
- // ── Chart.js global defaults ─────────────────────────────────────────────────
25
- Chart.defaults.color = '#8899b4';
26
- Chart.defaults.borderColor = 'rgba(56,139,253,0.1)';
27
- Chart.defaults.font.family = "'Inter', sans-serif";
28
  Chart.defaults.font.size = 11;
 
29
  Chart.defaults.plugins.legend.display = false;
30
- Chart.defaults.animation.duration = 300;
31
-
32
- const COLORS = {
33
- blue: '#388bfd',
34
- green: '#3fb950',
35
- amber: '#d29922',
36
- red: '#f85149',
37
- purple: '#bc8cff',
38
- cyan: '#39d0d8',
39
- orange: '#ff7c39',
40
- dimBlue: 'rgba(56,139,253,0.15)',
 
 
 
 
 
 
41
  };
42
 
43
  function rgba(hex, alpha) {
@@ -47,55 +54,129 @@ function rgba(hex, alpha) {
47
  return `rgba(${r},${g},${b},${alpha})`;
48
  }
49
 
50
- // ── Chart factory helpers ────────────────────────────────────────────────────
51
  function makeLineChart(id, labels, datasets, opts = {}) {
52
- const ctx = document.getElementById(id).getContext('2d');
53
- return new Chart(ctx, {
 
54
  type: 'line',
55
  data: { labels, datasets },
56
  options: {
57
  responsive: true,
58
  maintainAspectRatio: false,
59
  interaction: { mode: 'index', intersect: false },
 
60
  scales: {
61
- x: { grid: { color: 'rgba(56,139,253,0.06)' }, ticks: { maxTicksLimit: 8 } },
62
- y: { grid: { color: 'rgba(56,139,253,0.06)' }, ...opts.yAxis },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  },
64
  plugins: {
65
- legend: { display: opts.legend || false },
66
- tooltip: { backgroundColor: '#0f1829', borderColor: 'rgba(56,139,253,0.3)', borderWidth: 1 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  },
68
  ...opts.extra,
69
  },
70
  });
71
  }
72
 
73
- function makeAreaChart(id, labels, datasets) {
74
- return makeLineChart(id, labels, datasets, {
75
- extra: { fill: true },
76
- });
77
- }
78
-
79
  function makeBarChart(id, labels, datasets) {
80
- const ctx = document.getElementById(id).getContext('2d');
81
- return new Chart(ctx, {
 
82
  type: 'bar',
83
  data: { labels, datasets },
84
  options: {
85
  responsive: true,
86
  maintainAspectRatio: false,
 
87
  scales: {
88
- x: { stacked: true, grid: { color: 'rgba(56,139,253,0.06)' }, ticks: { maxTicksLimit: 8 } },
89
- y: { stacked: true, grid: { color: 'rgba(56,139,253,0.06)' } },
 
 
 
 
 
 
 
 
 
 
90
  },
91
  plugins: {
92
- legend: { display: true, position: 'bottom', labels: { usePointStyle: true, padding: 10 } },
93
- tooltip: { backgroundColor: '#0f1829', borderColor: 'rgba(56,139,253,0.3)', borderWidth: 1 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  },
95
  },
96
  });
97
  }
98
 
 
 
 
 
 
 
 
 
99
  // ── Initialise all charts ─────────────────────────────────────────────────────
100
  const emptyLabels = Array.from({ length: CURVE_POINTS }, (_, i) => `${i}h`);
101
  const emptyData = Array(CURVE_POINTS).fill(null);
@@ -107,8 +188,8 @@ const priceChart = makeLineChart('chart-price',
107
  {
108
  label: 'Price ($/kWh)',
109
  data: [...emptyData],
110
- borderColor: COLORS.amber,
111
- backgroundColor: rgba(COLORS.amber, 0.15),
112
  borderWidth: 2,
113
  fill: true,
114
  tension: 0.4,
@@ -117,14 +198,16 @@ const priceChart = makeLineChart('chart-price',
117
  {
118
  label: 'Current',
119
  data: [...emptyData],
120
- borderColor: COLORS.red,
121
  backgroundColor: 'transparent',
122
  borderWidth: 0,
123
- pointRadius: 6,
124
- pointBackgroundColor: COLORS.red,
 
 
125
  },
126
  ],
127
- { legend: true, yAxis: { title: { display: true, text: '$/kWh' } } }
128
  );
129
 
130
  // 2. Temperature
@@ -134,8 +217,8 @@ const tempChart = makeLineChart('chart-temp',
134
  {
135
  label: 'Indoor Temp (°C)',
136
  data: [],
137
- borderColor: COLORS.cyan,
138
- backgroundColor: rgba(COLORS.cyan, 0.1),
139
  borderWidth: 2,
140
  fill: true,
141
  tension: 0.4,
@@ -144,33 +227,33 @@ const tempChart = makeLineChart('chart-temp',
144
  {
145
  label: 'T_max (23°C)',
146
  data: [],
147
- borderColor: rgba(COLORS.red, 0.5),
148
  borderWidth: 1,
149
- borderDash: [5, 5],
150
  pointRadius: 0,
151
  fill: false,
152
  },
153
  {
154
  label: 'T_min (19°C)',
155
  data: [],
156
- borderColor: rgba(COLORS.blue, 0.5),
157
  borderWidth: 1,
158
- borderDash: [5, 5],
159
  pointRadius: 0,
160
  fill: false,
161
  },
162
  ],
163
- { legend: true, yAxis: { suggestedMin: 15, suggestedMax: 30, title: { display: true, text: '°C' } } }
164
  );
165
 
166
- // 3. Storage history (mini)
167
  const storageChart = makeLineChart('chart-storage',
168
  [],
169
  [{
170
  label: 'Storage Level',
171
  data: [],
172
- borderColor: COLORS.cyan,
173
- backgroundColor: rgba(COLORS.cyan, 0.2),
174
  borderWidth: 2,
175
  fill: true,
176
  tension: 0.4,
@@ -179,23 +262,25 @@ const storageChart = makeLineChart('chart-storage',
179
  { yAxis: { min: 0, max: 1 } }
180
  );
181
 
182
- // 4. HVAC + Load Shed stacked area
183
  const hvacChart = makeBarChart('chart-hvac',
184
  [],
185
  [
186
  {
187
  label: 'HVAC Power',
188
  data: [],
189
- backgroundColor: rgba(COLORS.blue, 0.7),
190
- borderColor: COLORS.blue,
191
  borderWidth: 1,
 
192
  },
193
  {
194
  label: 'Load Shed',
195
  data: [],
196
- backgroundColor: rgba(COLORS.red, 0.7),
197
- borderColor: COLORS.red,
198
  borderWidth: 1,
 
199
  },
200
  ]
201
  );
@@ -207,8 +292,8 @@ const costChart = makeLineChart('chart-cost',
207
  {
208
  label: 'Agent Cost ($)',
209
  data: [],
210
- borderColor: COLORS.green,
211
- backgroundColor: rgba(COLORS.green, 0.1),
212
  borderWidth: 2,
213
  fill: true,
214
  tension: 0.4,
@@ -217,25 +302,25 @@ const costChart = makeLineChart('chart-cost',
217
  {
218
  label: 'Baseline ($)',
219
  data: [],
220
- borderColor: rgba(COLORS.amber, 0.7),
221
- borderDash: [6, 3],
222
  borderWidth: 2,
223
  fill: false,
224
  tension: 0.4,
225
  pointRadius: 0,
226
  },
227
  ],
228
- { legend: true, yAxis: { title: { display: true, text: '$' } } }
229
  );
230
 
231
- // 6. Grid stress history (mini)
232
  const stressChart = makeLineChart('chart-stress',
233
  [],
234
  [{
235
  label: 'Grid Stress',
236
  data: [],
237
- borderColor: COLORS.red,
238
- backgroundColor: rgba(COLORS.red, 0.2),
239
  borderWidth: 2,
240
  fill: true,
241
  tension: 0.4,
@@ -250,28 +335,36 @@ const carbonChart = makeLineChart('chart-carbon',
250
  [{
251
  label: 'Carbon Intensity (gCO₂/kWh)',
252
  data: [...emptyData],
253
- borderColor: COLORS.orange,
254
- backgroundColor: rgba(COLORS.orange, 0.15),
255
  borderWidth: 2,
256
  fill: true,
257
  tension: 0.4,
258
  pointRadius: 0,
259
  }],
260
- { yAxis: { title: { display: true, text: 'gCO₂/kWh' } } }
261
  );
262
 
263
- // 8. Reward timeline curve
264
  const rewardChart = makeLineChart('chart-reward',
265
  [],
266
- [
267
- { label: 'Step Reward', data: [], borderColor: COLORS.green, backgroundColor: rgba(COLORS.green, 0.1), borderWidth: 2, fill: true, tension: 0.4, pointRadius: 0 },
268
- ],
269
- { yAxis: { title: { display: true, text: 'Reward' } } }
 
 
 
 
 
 
 
270
  );
271
 
272
  // ── Stress meter bars ────────────────────────────────────────────────────────
273
  function buildStressMeter() {
274
  const el = document.getElementById('stress-meter');
 
275
  el.innerHTML = '';
276
  for (let i = 0; i < 20; i++) {
277
  const bar = document.createElement('div');
@@ -291,12 +384,12 @@ function updateStressMeter(stress) {
291
  const pct = (i / bars) * 100;
292
  bar.style.height = `${20 + pct * 0.8}%`;
293
  if (i < active) {
294
- const color = stress > 0.7 ? COLORS.red : stress > 0.4 ? COLORS.amber : COLORS.green;
295
  bar.style.background = color;
296
- bar.style.opacity = '1';
297
  } else {
298
- bar.style.background = 'rgba(255,255,255,0.05)';
299
- bar.style.opacity = '1';
300
  }
301
  }
302
  }
@@ -304,8 +397,9 @@ function updateStressMeter(stress) {
304
  // ── Batch Gantt renderer ─────────────────────────────────────────────────────
305
  function renderGantt(jobs, currentStep) {
306
  const wrap = document.getElementById('gantt-wrap');
 
307
  if (!jobs || jobs.length === 0) {
308
- wrap.innerHTML = '<div style="color:var(--text-dim);font-size:0.8rem">No batch jobs in this episode.</div>';
309
  return;
310
  }
311
  const totalSlots = EPISODE_STEPS;
@@ -344,7 +438,7 @@ function renderGantt(jobs, currentStep) {
344
  // Current step marker
345
  const curPct = (currentStep / totalSlots) * 100;
346
  const curMarker = document.createElement('div');
347
- curMarker.style.cssText = `position:absolute;top:0;bottom:0;width:1px;background:rgba(56,139,253,0.6);left:${curPct}%`;
348
  track.appendChild(curMarker);
349
 
350
  row.appendChild(track);
@@ -367,27 +461,28 @@ function renderGantt(jobs, currentStep) {
367
  function renderRewardRows(rc) {
368
  if (!rc) return;
369
  const container = document.getElementById('reward-rows');
 
370
  const components = [
371
- { key: 'cost_savings', label: 'Cost Savings', color: COLORS.green, sign: 1 },
372
- { key: 'temp_constraint', label: 'Temp Constr.', color: COLORS.cyan, sign: 1 },
373
- { key: 'grid_response', label: 'Grid DR', color: COLORS.blue, sign: 1 },
374
- { key: 'efficiency_bonus', label: 'Efficiency', color: COLORS.purple, sign: 1 },
375
- { key: 'stability_penalty', label: 'Stability', color: COLORS.amber, sign: -1 },
376
- { key: 'deadline_penalty', label: 'Deadlines', color: COLORS.red, sign: -1 },
377
- { key: 'carbon_reward', label: 'Carbon', color: COLORS.orange, sign: 1 },
378
  ];
379
  container.innerHTML = '';
380
  components.forEach(c => {
381
  const val = rc[c.key] || 0;
382
  const absVal = Math.abs(val);
383
- const pct = Math.min(100, absVal * 30); // scale 0–~3 reward to 0–100%
384
  container.innerHTML += `
385
  <div class="reward-row">
386
  <div class="reward-label">${c.label}</div>
387
- <div class="reward-bar-wrap">
388
- <div class="reward-bar" style="width:${pct}%;background:${c.color};opacity:0.8"></div>
389
  </div>
390
- <div class="reward-val" style="color:${val >= 0 ? COLORS.green : COLORS.red}">${val.toFixed(3)}</div>
391
  </div>`;
392
  });
393
  }
@@ -410,12 +505,12 @@ async function fetchAndUpdate() {
410
  connected = true;
411
  document.getElementById('conn-banner').classList.remove('show');
412
  document.getElementById('status-dot').style.background = 'var(--accent-green)';
 
413
 
414
  const b = state.buildings && state.buildings[currentBuilding];
415
  if (!b) return;
416
 
417
  const step = state.step;
418
- const hourOfDay = b.hour_of_day || 0;
419
 
420
  // ── Header ──
421
  document.getElementById('ep-step').textContent = `ep:${state.episode} step:${step}/${EPISODE_STEPS - 1}`;
@@ -464,8 +559,12 @@ async function fetchAndUpdate() {
464
  }
465
 
466
  // ── Grid stress ──
467
- document.getElementById('stress-big').textContent = b.grid_stress_signal.toFixed(3);
 
 
 
468
  updateStressMeter(b.grid_stress_signal);
 
469
  const cardStress = document.getElementById('card-stress');
470
  if (b.grid_stress_signal > 0.7) {
471
  cardStress.classList.add('alert-active');
@@ -478,7 +577,7 @@ async function fetchAndUpdate() {
478
  document.getElementById('storage-pct').textContent = storagePct;
479
  document.getElementById('storage-fill').style.width = `${storagePct}%`;
480
 
481
- // ── History-based charts (only update when step changes) ──
482
  if (step !== lastStep) {
483
  lastStep = step;
484
  const stepLabels = Array.from({ length: b.temp_history.length }, (_, i) => i);
@@ -495,8 +594,8 @@ async function fetchAndUpdate() {
495
  // Storage history
496
  if (b.hvac_history && b.hvac_history.length > 0) {
497
  storageChart.data.labels = stepLabels;
498
- storageChart.data.datasets[0].data = Array.from({ length: b.hvac_history.length }, (_, i) =>
499
- b.thermal_storage_level // simplify: use current level as placeholder
500
  );
501
  storageChart.update('none');
502
  }
@@ -515,25 +614,22 @@ async function fetchAndUpdate() {
515
  const n = b.cost_history.length;
516
  costChart.data.labels = Array.from({ length: n }, (_, i) => i);
517
  costChart.data.datasets[0].data = b.cost_history;
518
- // Generate approximate baseline curve (linear ramp to b.baseline_cost)
519
  const baselineStep = b.baseline_cost / Math.max(step, 1);
520
  costChart.data.datasets[1].data = b.cost_history.map((_, i) => baselineStep * (i + 1));
521
  costChart.update('none');
522
  }
523
 
524
- // Grid stress history
525
  if (b.reward_history && b.reward_history.length > 0) {
526
  const n = b.reward_history.length;
527
  stressChart.data.labels = Array.from({ length: n }, (_, i) => i);
528
  stressChart.data.datasets[0].data = b.reward_history.map(r => Math.max(0, r.grid_response || 0));
529
  stressChart.update('none');
530
 
531
- // Total reward timeline chart (full episode)
532
  rewardChart.data.labels = Array.from({ length: n }, (_, i) => i);
533
  rewardChart.data.datasets[0].data = b.reward_history.map(r => r.total || 0);
534
  rewardChart.update('none');
535
 
536
- // Reward rows (last step)
537
  renderRewardRows(b.reward_history[b.reward_history.length - 1]);
538
  }
539
 
@@ -545,7 +641,7 @@ async function fetchAndUpdate() {
545
  connected = false;
546
  document.getElementById('conn-banner').classList.add('show');
547
  document.getElementById('status-dot').style.background = 'var(--accent-red)';
548
- // console.error('Poll error:', err);
549
  }
550
  }
551
 
@@ -554,8 +650,9 @@ async function fetchAndUpdate() {
554
  async function doReset() {
555
  const taskId = parseInt(document.getElementById('task-select').value, 10);
556
  const btn = document.getElementById('btn-reset');
557
- btn.textContent = 'Resetting...';
558
  btn.disabled = true;
 
559
  lastStep = -1;
560
  try {
561
  await fetch(`${API_BASE}/reset`, {
@@ -566,48 +663,125 @@ async function doReset() {
566
  } catch (e) {
567
  console.error(e);
568
  }
569
- btn.textContent = ' New Episode';
570
  btn.disabled = false;
 
571
  document.getElementById('grade-result').textContent = '';
 
572
  }
573
 
574
  let liveSimTimer = null;
575
  let isLiveSimulating = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
  function toggleLiveSim() {
578
  const btn = document.getElementById('btn-live');
579
  if (isLiveSimulating) {
580
- // Stop live sim
581
  clearInterval(liveSimTimer);
582
  isLiveSimulating = false;
583
- btn.textContent = ' Start Live Simulation';
584
- btn.style.background = 'var(--accent-green)';
585
  } else {
586
- // Start live sim
587
  isLiveSimulating = true;
588
- btn.textContent = ' Pause Live Simulation';
589
- btn.style.background = 'var(--accent-amber)';
590
-
591
  liveSimTimer = setInterval(async () => {
592
- // Step the environment automatically with a simple heuristic policy
593
- const taskId = parseInt(document.getElementById('task-select').value, 10);
594
  try {
 
 
 
 
 
 
 
 
 
 
595
  await fetch(`${API_BASE}/step`, {
596
  method: 'POST',
597
  headers: { 'Content-Type': 'application/json' },
598
- body: JSON.stringify({
599
- hvac_power_level: 0.5,
600
- thermal_charge_rate: 0.0,
601
- batch_job_slot: 0,
602
- load_shed_fraction: 0.0,
603
- building_id: currentBuilding
604
- }),
605
  });
606
- // fetchAndUpdate() will catch the change via polling
607
  } catch (e) {
608
  console.error(e);
609
  }
610
- }, 400); // 400ms per step
611
  }
612
  }
613
 
@@ -619,14 +793,16 @@ async function doGrade() {
619
  const el = document.getElementById('grade-result');
620
  el.textContent = `Score: ${score}% ${grade.exploit_detected ? '⚠ exploit!' : ''}`;
621
  el.style.color = grade.score > 0.6 ? 'var(--accent-green)' : grade.score > 0.3 ? 'var(--accent-amber)' : 'var(--accent-red)';
 
 
622
  } catch (e) {
623
  console.error(e);
624
  }
625
  }
626
 
627
  function onTaskChange() {
628
- // Reset chart histories on task change
629
  [tempChart, storageChart, hvacChart, costChart, stressChart, rewardChart].forEach(c => {
 
630
  c.data.labels = [];
631
  c.data.datasets.forEach(d => d.data = []);
632
  c.update('none');
@@ -641,7 +817,7 @@ function onBuildingChange() {
641
  // ── Start polling ────────────────────────────────────────────────────────────
642
  function startPolling() {
643
  if (pollTimer) clearInterval(pollTimer);
644
- fetchAndUpdate(); // immediate first fetch
645
  pollTimer = setInterval(fetchAndUpdate, POLL_MS);
646
  }
647
 
 
1
  /**
2
+ * GridMind-RL Dashboard — Premium Chart.js real-time visualization
3
+ * Polls /api/state every 500ms and updates all charts + KPIs.
4
  */
5
 
6
  'use strict';
 
21
  let pollTimer = null;
22
  let connected = false;
23
 
24
+ // ── Chart.js Premium Theme ─────────────────────────────────────────────────
25
+ Chart.defaults.color = '#5a6478';
26
+ Chart.defaults.borderColor = 'rgba(255,255,255,0.03)';
27
+ Chart.defaults.font.family = "'Inter', -apple-system, system-ui, sans-serif";
28
  Chart.defaults.font.size = 11;
29
+ Chart.defaults.font.weight = 400;
30
  Chart.defaults.plugins.legend.display = false;
31
+ Chart.defaults.animation.duration = 350;
32
+ Chart.defaults.animation.easing = 'easeOutQuart';
33
+ Chart.defaults.elements.line.borderCapStyle = 'round';
34
+ Chart.defaults.elements.line.borderJoinStyle = 'round';
35
+
36
+ const C = {
37
+ blue: '#5b9cf6',
38
+ green: '#4ade80',
39
+ amber: '#f5a623',
40
+ red: '#f06e6e',
41
+ purple: '#a78bfa',
42
+ cyan: '#34d4e4',
43
+ orange: '#fb923c',
44
+ teal: '#2dd4bf',
45
+ rose: '#fb7185',
46
+ grid: 'rgba(255,255,255,0.025)',
47
+ surface: '#1a1f2e',
48
  };
49
 
50
  function rgba(hex, alpha) {
 
54
  return `rgba(${r},${g},${b},${alpha})`;
55
  }
56
 
57
+ // ── Chart factory refined styling ──────────────────────────────────────────
58
  function makeLineChart(id, labels, datasets, opts = {}) {
59
+ const ctx = document.getElementById(id);
60
+ if (!ctx) return null;
61
+ return new Chart(ctx.getContext('2d'), {
62
  type: 'line',
63
  data: { labels, datasets },
64
  options: {
65
  responsive: true,
66
  maintainAspectRatio: false,
67
  interaction: { mode: 'index', intersect: false },
68
+ layout: { padding: { top: 4, right: 4, bottom: 0, left: 4 } },
69
  scales: {
70
+ x: {
71
+ grid: { color: C.grid, drawBorder: false },
72
+ ticks: {
73
+ maxTicksLimit: 8,
74
+ font: { size: 10, family: "'JetBrains Mono', monospace" },
75
+ color: '#3d4558',
76
+ padding: 4,
77
+ },
78
+ border: { display: false },
79
+ },
80
+ y: {
81
+ grid: { color: C.grid, drawBorder: false },
82
+ ticks: {
83
+ font: { size: 10, family: "'JetBrains Mono', monospace" },
84
+ color: '#3d4558',
85
+ padding: 8,
86
+ },
87
+ border: { display: false },
88
+ ...opts.yAxis,
89
+ },
90
  },
91
  plugins: {
92
+ legend: {
93
+ display: opts.legend || false,
94
+ position: 'bottom',
95
+ labels: {
96
+ usePointStyle: true,
97
+ pointStyle: 'circle',
98
+ padding: 16,
99
+ font: { size: 10, weight: 500 },
100
+ color: '#8a94a8',
101
+ },
102
+ },
103
+ tooltip: {
104
+ backgroundColor: '#12151e',
105
+ titleColor: '#e8ecf4',
106
+ bodyColor: '#8a94a8',
107
+ borderColor: 'rgba(255,255,255,0.08)',
108
+ borderWidth: 1,
109
+ cornerRadius: 8,
110
+ padding: 10,
111
+ displayColors: true,
112
+ boxPadding: 4,
113
+ titleFont: { size: 11, weight: 600 },
114
+ bodyFont: { size: 11 },
115
+ },
116
  },
117
  ...opts.extra,
118
  },
119
  });
120
  }
121
 
 
 
 
 
 
 
122
  function makeBarChart(id, labels, datasets) {
123
+ const ctx = document.getElementById(id);
124
+ if (!ctx) return null;
125
+ return new Chart(ctx.getContext('2d'), {
126
  type: 'bar',
127
  data: { labels, datasets },
128
  options: {
129
  responsive: true,
130
  maintainAspectRatio: false,
131
+ layout: { padding: { top: 4, right: 4, bottom: 0, left: 4 } },
132
  scales: {
133
+ x: {
134
+ stacked: true,
135
+ grid: { color: C.grid, drawBorder: false },
136
+ ticks: { maxTicksLimit: 8, font: { size: 10, family: "'JetBrains Mono', monospace" }, color: '#3d4558' },
137
+ border: { display: false },
138
+ },
139
+ y: {
140
+ stacked: true,
141
+ grid: { color: C.grid, drawBorder: false },
142
+ ticks: { font: { size: 10, family: "'JetBrains Mono', monospace" }, color: '#3d4558' },
143
+ border: { display: false },
144
+ },
145
  },
146
  plugins: {
147
+ legend: {
148
+ display: true,
149
+ position: 'bottom',
150
+ labels: {
151
+ usePointStyle: true,
152
+ pointStyle: 'circle',
153
+ padding: 16,
154
+ font: { size: 10, weight: 500 },
155
+ color: '#8a94a8',
156
+ },
157
+ },
158
+ tooltip: {
159
+ backgroundColor: '#12151e',
160
+ titleColor: '#e8ecf4',
161
+ bodyColor: '#8a94a8',
162
+ borderColor: 'rgba(255,255,255,0.08)',
163
+ borderWidth: 1,
164
+ cornerRadius: 8,
165
+ padding: 10,
166
+ },
167
  },
168
  },
169
  });
170
  }
171
 
172
+ // ── Gradient helper ──────────────────────────────────────────────────────────
173
+ function createGradient(ctx, hex, startAlpha, endAlpha) {
174
+ const gradient = ctx.createLinearGradient(0, 0, 0, ctx.canvas.clientHeight);
175
+ gradient.addColorStop(0, rgba(hex, startAlpha));
176
+ gradient.addColorStop(1, rgba(hex, endAlpha));
177
+ return gradient;
178
+ }
179
+
180
  // ── Initialise all charts ─────────────────────────────────────────────────────
181
  const emptyLabels = Array.from({ length: CURVE_POINTS }, (_, i) => `${i}h`);
182
  const emptyData = Array(CURVE_POINTS).fill(null);
 
188
  {
189
  label: 'Price ($/kWh)',
190
  data: [...emptyData],
191
+ borderColor: C.amber,
192
+ backgroundColor: rgba(C.amber, 0.08),
193
  borderWidth: 2,
194
  fill: true,
195
  tension: 0.4,
 
198
  {
199
  label: 'Current',
200
  data: [...emptyData],
201
+ borderColor: C.red,
202
  backgroundColor: 'transparent',
203
  borderWidth: 0,
204
+ pointRadius: 5,
205
+ pointBackgroundColor: C.red,
206
+ pointBorderColor: rgba(C.red, 0.3),
207
+ pointBorderWidth: 6,
208
  },
209
  ],
210
+ { legend: true, yAxis: { title: { display: true, text: '$/kWh', color: '#3d4558', font: { size: 10 } } } }
211
  );
212
 
213
  // 2. Temperature
 
217
  {
218
  label: 'Indoor Temp (°C)',
219
  data: [],
220
+ borderColor: C.cyan,
221
+ backgroundColor: rgba(C.cyan, 0.06),
222
  borderWidth: 2,
223
  fill: true,
224
  tension: 0.4,
 
227
  {
228
  label: 'T_max (23°C)',
229
  data: [],
230
+ borderColor: rgba(C.red, 0.35),
231
  borderWidth: 1,
232
+ borderDash: [4, 4],
233
  pointRadius: 0,
234
  fill: false,
235
  },
236
  {
237
  label: 'T_min (19°C)',
238
  data: [],
239
+ borderColor: rgba(C.blue, 0.35),
240
  borderWidth: 1,
241
+ borderDash: [4, 4],
242
  pointRadius: 0,
243
  fill: false,
244
  },
245
  ],
246
+ { legend: true, yAxis: { suggestedMin: 15, suggestedMax: 30, title: { display: true, text: '°C', color: '#3d4558', font: { size: 10 } } } }
247
  );
248
 
249
+ // 3. Storage history
250
  const storageChart = makeLineChart('chart-storage',
251
  [],
252
  [{
253
  label: 'Storage Level',
254
  data: [],
255
+ borderColor: C.teal,
256
+ backgroundColor: rgba(C.teal, 0.1),
257
  borderWidth: 2,
258
  fill: true,
259
  tension: 0.4,
 
262
  { yAxis: { min: 0, max: 1 } }
263
  );
264
 
265
+ // 4. HVAC + Load Shed
266
  const hvacChart = makeBarChart('chart-hvac',
267
  [],
268
  [
269
  {
270
  label: 'HVAC Power',
271
  data: [],
272
+ backgroundColor: rgba(C.blue, 0.6),
273
+ borderColor: C.blue,
274
  borderWidth: 1,
275
+ borderRadius: 3,
276
  },
277
  {
278
  label: 'Load Shed',
279
  data: [],
280
+ backgroundColor: rgba(C.red, 0.6),
281
+ borderColor: C.red,
282
  borderWidth: 1,
283
+ borderRadius: 3,
284
  },
285
  ]
286
  );
 
292
  {
293
  label: 'Agent Cost ($)',
294
  data: [],
295
+ borderColor: C.green,
296
+ backgroundColor: rgba(C.green, 0.06),
297
  borderWidth: 2,
298
  fill: true,
299
  tension: 0.4,
 
302
  {
303
  label: 'Baseline ($)',
304
  data: [],
305
+ borderColor: rgba(C.amber, 0.6),
306
+ borderDash: [5, 3],
307
  borderWidth: 2,
308
  fill: false,
309
  tension: 0.4,
310
  pointRadius: 0,
311
  },
312
  ],
313
+ { legend: true, yAxis: { title: { display: true, text: '$', color: '#3d4558', font: { size: 10 } } } }
314
  );
315
 
316
+ // 6. Grid stress history
317
  const stressChart = makeLineChart('chart-stress',
318
  [],
319
  [{
320
  label: 'Grid Stress',
321
  data: [],
322
+ borderColor: C.red,
323
+ backgroundColor: rgba(C.red, 0.1),
324
  borderWidth: 2,
325
  fill: true,
326
  tension: 0.4,
 
335
  [{
336
  label: 'Carbon Intensity (gCO₂/kWh)',
337
  data: [...emptyData],
338
+ borderColor: C.orange,
339
+ backgroundColor: rgba(C.orange, 0.08),
340
  borderWidth: 2,
341
  fill: true,
342
  tension: 0.4,
343
  pointRadius: 0,
344
  }],
345
+ { yAxis: { title: { display: true, text: 'gCO₂/kWh', color: '#3d4558', font: { size: 10 } } } }
346
  );
347
 
348
+ // 8. Reward timeline
349
  const rewardChart = makeLineChart('chart-reward',
350
  [],
351
+ [{
352
+ label: 'Step Reward',
353
+ data: [],
354
+ borderColor: C.green,
355
+ backgroundColor: rgba(C.green, 0.06),
356
+ borderWidth: 2,
357
+ fill: true,
358
+ tension: 0.4,
359
+ pointRadius: 0,
360
+ }],
361
+ { yAxis: { title: { display: true, text: 'Reward', color: '#3d4558', font: { size: 10 } } } }
362
  );
363
 
364
  // ── Stress meter bars ────────────────────────────────────────────────────────
365
  function buildStressMeter() {
366
  const el = document.getElementById('stress-meter');
367
+ if (!el) return;
368
  el.innerHTML = '';
369
  for (let i = 0; i < 20; i++) {
370
  const bar = document.createElement('div');
 
384
  const pct = (i / bars) * 100;
385
  bar.style.height = `${20 + pct * 0.8}%`;
386
  if (i < active) {
387
+ const color = stress > 0.7 ? C.red : stress > 0.4 ? C.amber : C.green;
388
  bar.style.background = color;
389
+ bar.style.boxShadow = `0 0 6px ${rgba(color === C.red ? C.red : color === C.amber ? C.amber : C.green, 0.3)}`;
390
  } else {
391
+ bar.style.background = 'rgba(255,255,255,0.03)';
392
+ bar.style.boxShadow = 'none';
393
  }
394
  }
395
  }
 
397
  // ── Batch Gantt renderer ─────────────────────────────────────────────────────
398
  function renderGantt(jobs, currentStep) {
399
  const wrap = document.getElementById('gantt-wrap');
400
+ if (!wrap) return;
401
  if (!jobs || jobs.length === 0) {
402
+ wrap.innerHTML = '<div class="gantt-empty">No batch jobs in this episode</div>';
403
  return;
404
  }
405
  const totalSlots = EPISODE_STEPS;
 
438
  // Current step marker
439
  const curPct = (currentStep / totalSlots) * 100;
440
  const curMarker = document.createElement('div');
441
+ curMarker.style.cssText = `position:absolute;top:2px;bottom:2px;width:2px;background:rgba(91,156,246,0.5);left:${curPct}%;border-radius:1px;box-shadow:0 0 4px rgba(91,156,246,0.3)`;
442
  track.appendChild(curMarker);
443
 
444
  row.appendChild(track);
 
461
  function renderRewardRows(rc) {
462
  if (!rc) return;
463
  const container = document.getElementById('reward-rows');
464
+ if (!container) return;
465
  const components = [
466
+ { key: 'cost_savings', label: 'Cost Savings', color: C.green },
467
+ { key: 'temp_constraint', label: 'Temp Constr.', color: C.cyan },
468
+ { key: 'grid_response', label: 'Grid DR', color: C.blue },
469
+ { key: 'efficiency_bonus', label: 'Efficiency', color: C.purple },
470
+ { key: 'stability_penalty', label: 'Stability', color: C.amber },
471
+ { key: 'deadline_penalty', label: 'Deadlines', color: C.red },
472
+ { key: 'carbon_reward', label: 'Carbon', color: C.orange },
473
  ];
474
  container.innerHTML = '';
475
  components.forEach(c => {
476
  const val = rc[c.key] || 0;
477
  const absVal = Math.abs(val);
478
+ const pct = Math.min(100, absVal * 30);
479
  container.innerHTML += `
480
  <div class="reward-row">
481
  <div class="reward-label">${c.label}</div>
482
+ <div class="reward-bar-track">
483
+ <div class="reward-bar" style="width:${pct}%;background:${c.color};opacity:0.7"></div>
484
  </div>
485
+ <div class="reward-val" style="color:${val >= 0 ? C.green : C.red}">${val.toFixed(3)}</div>
486
  </div>`;
487
  });
488
  }
 
505
  connected = true;
506
  document.getElementById('conn-banner').classList.remove('show');
507
  document.getElementById('status-dot').style.background = 'var(--accent-green)';
508
+ document.getElementById('status-label').textContent = 'Live';
509
 
510
  const b = state.buildings && state.buildings[currentBuilding];
511
  if (!b) return;
512
 
513
  const step = state.step;
 
514
 
515
  // ── Header ──
516
  document.getElementById('ep-step').textContent = `ep:${state.episode} step:${step}/${EPISODE_STEPS - 1}`;
 
559
  }
560
 
561
  // ── Grid stress ──
562
+ const stressBig = document.getElementById('stress-big');
563
+ stressBig.textContent = b.grid_stress_signal.toFixed(3);
564
+ stressBig.className = 'stress-value ' +
565
+ (b.grid_stress_signal > 0.7 ? 'high' : b.grid_stress_signal > 0.4 ? 'mid' : 'low');
566
  updateStressMeter(b.grid_stress_signal);
567
+
568
  const cardStress = document.getElementById('card-stress');
569
  if (b.grid_stress_signal > 0.7) {
570
  cardStress.classList.add('alert-active');
 
577
  document.getElementById('storage-pct').textContent = storagePct;
578
  document.getElementById('storage-fill').style.width = `${storagePct}%`;
579
 
580
+ // ── History-based charts (only when step changes) ──
581
  if (step !== lastStep) {
582
  lastStep = step;
583
  const stepLabels = Array.from({ length: b.temp_history.length }, (_, i) => i);
 
594
  // Storage history
595
  if (b.hvac_history && b.hvac_history.length > 0) {
596
  storageChart.data.labels = stepLabels;
597
+ storageChart.data.datasets[0].data = Array.from({ length: b.hvac_history.length }, () =>
598
+ b.thermal_storage_level
599
  );
600
  storageChart.update('none');
601
  }
 
614
  const n = b.cost_history.length;
615
  costChart.data.labels = Array.from({ length: n }, (_, i) => i);
616
  costChart.data.datasets[0].data = b.cost_history;
 
617
  const baselineStep = b.baseline_cost / Math.max(step, 1);
618
  costChart.data.datasets[1].data = b.cost_history.map((_, i) => baselineStep * (i + 1));
619
  costChart.update('none');
620
  }
621
 
622
+ // Grid stress + reward history
623
  if (b.reward_history && b.reward_history.length > 0) {
624
  const n = b.reward_history.length;
625
  stressChart.data.labels = Array.from({ length: n }, (_, i) => i);
626
  stressChart.data.datasets[0].data = b.reward_history.map(r => Math.max(0, r.grid_response || 0));
627
  stressChart.update('none');
628
 
 
629
  rewardChart.data.labels = Array.from({ length: n }, (_, i) => i);
630
  rewardChart.data.datasets[0].data = b.reward_history.map(r => r.total || 0);
631
  rewardChart.update('none');
632
 
 
633
  renderRewardRows(b.reward_history[b.reward_history.length - 1]);
634
  }
635
 
 
641
  connected = false;
642
  document.getElementById('conn-banner').classList.add('show');
643
  document.getElementById('status-dot').style.background = 'var(--accent-red)';
644
+ document.getElementById('status-label').textContent = 'Offline';
645
  }
646
  }
647
 
 
650
  async function doReset() {
651
  const taskId = parseInt(document.getElementById('task-select').value, 10);
652
  const btn = document.getElementById('btn-reset');
653
+ btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="spin"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> Resetting...';
654
  btn.disabled = true;
655
+ btn.style.opacity = '0.6';
656
  lastStep = -1;
657
  try {
658
  await fetch(`${API_BASE}/reset`, {
 
663
  } catch (e) {
664
  console.error(e);
665
  }
666
+ btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> New Episode';
667
  btn.disabled = false;
668
+ btn.style.opacity = '1';
669
  document.getElementById('grade-result').textContent = '';
670
+ document.getElementById('grade-result').classList.remove('show');
671
  }
672
 
673
  let liveSimTimer = null;
674
  let isLiveSimulating = false;
675
+ let lastLiveState = null;
676
+
677
+ // ── Smart Heuristic Agent ───────────────────────────────────────────────────
678
+ // Mirrors the Python _heuristic_action() from inference.py.
679
+ // Reads the latest fetched state and generates intelligent actions that
680
+ // exercise ALL reward components: cost, temperature, grid DR, efficiency,
681
+ // stability, deadlines, and carbon.
682
+ function heuristicAction(b) {
683
+ if (!b) {
684
+ return { hvac_power_level: 0.5, thermal_charge_rate: 0.0, batch_job_slot: 0, load_shed_fraction: 0.0, building_id: currentBuilding };
685
+ }
686
+
687
+ const price = b.current_price || 0.10;
688
+ const stress = b.grid_stress_signal || 0.0;
689
+ const temp = b.indoor_temperature || 21.0;
690
+ const storage = b.thermal_storage_level || 0.5;
691
+ const queue = b.batch_queue || [];
692
+ const carbon = b.carbon_intensity || 300;
693
+ const step = b.step || 0;
694
+
695
+ // ── HVAC: price-aware + temperature-reactive ──
696
+ let hvac = 0.5;
697
+ if (price < 0.07) hvac = 0.7; // cheap → run more
698
+ else if (price > 0.15) hvac = 0.3; // expensive → reduce
699
+ else hvac = 0.5;
700
+
701
+ // Temperature override: keep within 19–23°C
702
+ if (temp > 23.0) hvac = Math.max(hvac, 0.8);
703
+ else if (temp > 22.0) hvac = Math.max(hvac, 0.6);
704
+ else if (temp < 19.0) hvac = Math.min(hvac, 0.2);
705
+ else if (temp < 20.0) hvac = Math.min(hvac, 0.35);
706
+
707
+ // ── Thermal storage: arbitrage ──
708
+ let charge = 0.0;
709
+ if (price < 0.07 && storage < 0.8) {
710
+ charge = 0.6; // charge during cheap periods
711
+ } else if (price > 0.15 && storage > 0.3) {
712
+ charge = -0.5; // discharge during expensive periods
713
+ } else if (price < 0.10 && storage < 0.5) {
714
+ charge = 0.3; // moderate charge at mid-low prices
715
+ } else if (price > 0.12 && storage > 0.6) {
716
+ charge = -0.3; // moderate discharge at mid-high prices
717
+ }
718
+
719
+ // Carbon-aware: prefer charging when carbon is low
720
+ if (carbon < 250 && storage < 0.7) {
721
+ charge = Math.max(charge, 0.4);
722
+ }
723
+
724
+ // ── Load shedding: grid stress response ──
725
+ let shed = 0.0;
726
+ if (stress > 0.8) shed = 0.45;
727
+ else if (stress > 0.7) shed = 0.35;
728
+ else if (stress > 0.5) shed = 0.15;
729
+ else if (stress > 0.3) shed = 0.05;
730
+
731
+ // ── Batch scheduling: urgency-aware ──
732
+ let slot = 2; // default: moderate defer
733
+ if (queue.length > 0) {
734
+ const minDeadline = Math.min(...queue);
735
+ const stepsLeft = minDeadline - step;
736
+ if (stepsLeft < 4) slot = 0; // urgent: run now
737
+ else if (stepsLeft < 8) slot = 1; // soon: start soon
738
+ else if (stepsLeft < 16) slot = 2; // moderate
739
+ else if (price < 0.08) slot = 0; // cheap: might as well run now
740
+ else slot = 3; // defer
741
+ }
742
+
743
+ return {
744
+ hvac_power_level: Math.max(0, Math.min(1, hvac)),
745
+ thermal_charge_rate: Math.max(-1, Math.min(1, charge)),
746
+ batch_job_slot: Math.max(0, Math.min(4, slot)),
747
+ load_shed_fraction: Math.max(0, Math.min(0.5, shed)),
748
+ building_id: currentBuilding,
749
+ };
750
+ }
751
 
752
  function toggleLiveSim() {
753
  const btn = document.getElementById('btn-live');
754
  if (isLiveSimulating) {
 
755
  clearInterval(liveSimTimer);
756
  isLiveSimulating = false;
757
+ btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg> Start Live Simulation';
758
+ btn.classList.remove('active');
759
  } else {
 
760
  isLiveSimulating = true;
761
+ btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Pause Simulation';
762
+ btn.classList.add('active');
763
+
764
  liveSimTimer = setInterval(async () => {
 
 
765
  try {
766
+ // Fetch current state to make informed actions
767
+ const stateRes = await fetch(`${API_BASE}/state`);
768
+ if (stateRes.ok) {
769
+ const state = await stateRes.json();
770
+ lastLiveState = state.buildings && state.buildings[currentBuilding];
771
+ }
772
+
773
+ // Use smart heuristic agent based on current state
774
+ const action = heuristicAction(lastLiveState);
775
+
776
  await fetch(`${API_BASE}/step`, {
777
  method: 'POST',
778
  headers: { 'Content-Type': 'application/json' },
779
+ body: JSON.stringify(action),
 
 
 
 
 
 
780
  });
 
781
  } catch (e) {
782
  console.error(e);
783
  }
784
+ }, 400);
785
  }
786
  }
787
 
 
793
  const el = document.getElementById('grade-result');
794
  el.textContent = `Score: ${score}% ${grade.exploit_detected ? '⚠ exploit!' : ''}`;
795
  el.style.color = grade.score > 0.6 ? 'var(--accent-green)' : grade.score > 0.3 ? 'var(--accent-amber)' : 'var(--accent-red)';
796
+ el.style.background = grade.score > 0.6 ? 'rgba(74,222,128,0.08)' : grade.score > 0.3 ? 'rgba(245,166,35,0.08)' : 'rgba(240,110,110,0.08)';
797
+ el.classList.add('show');
798
  } catch (e) {
799
  console.error(e);
800
  }
801
  }
802
 
803
  function onTaskChange() {
 
804
  [tempChart, storageChart, hvacChart, costChart, stressChart, rewardChart].forEach(c => {
805
+ if (!c) return;
806
  c.data.labels = [];
807
  c.data.datasets.forEach(d => d.data = []);
808
  c.update('none');
 
817
  // ── Start polling ────────────────────────────────────────────────────────────
818
  function startPolling() {
819
  if (pollTimer) clearInterval(pollTimer);
820
+ fetchAndUpdate();
821
  pollTimer = setInterval(fetchAndUpdate, POLL_MS);
822
  }
823
 
dashboard/static/index.html CHANGED
@@ -8,69 +8,141 @@
8
  <!-- Chart.js CDN -->
9
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
10
  <!-- Google Fonts -->
11
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
 
 
12
  <style>
13
- /* ── Design System ─────────────────────────────────────────────── */
 
 
14
  :root {
15
- --bg-base: #0a0f1e;
16
- --bg-surface: #0f1829;
17
- --bg-card: #141f35;
18
- --bg-card-h: #1a2840;
19
- --border: rgba(56, 139, 253, 0.15);
20
- --border-glow: rgba(56, 139, 253, 0.4);
21
- --text-primary: #e2e8f4;
22
- --text-secondary: #8899b4;
23
- --text-dim: #4d6080;
24
- --accent-blue: #388bfd;
25
- --accent-green: #3fb950;
26
- --accent-amber: #d29922;
27
- --accent-red: #f85149;
28
- --accent-purple: #bc8cff;
29
- --accent-cyan: #39d0d8;
30
- --accent-orange: #ff7c39;
31
- --gradient-hero: linear-gradient(135deg, #0d1b33 0%, #0a0f1e 100%);
32
- --glow-blue: 0 0 20px rgba(56,139,253,0.25), 0 0 40px rgba(56,139,253,0.1);
33
- --glow-green: 0 0 20px rgba(63,185,80,0.25);
34
- --font-mono: 'JetBrains Mono', monospace;
35
- --radius: 12px;
36
- --radius-lg: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  }
38
 
 
39
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
40
-
41
- html { scroll-behavior: smooth; }
42
 
43
  body {
44
- font-family: 'Inter', sans-serif;
45
  background: var(--bg-base);
46
  color: var(--text-primary);
47
  min-height: 100vh;
48
  overflow-x: hidden;
 
49
  }
50
 
51
- /* ── Animated background grid ── */
52
  body::before {
53
  content: '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  position: fixed;
55
  inset: 0;
56
- background-image:
57
- linear-gradient(rgba(56,139,253,0.03) 1px, transparent 1px),
58
- linear-gradient(90deg, rgba(56,139,253,0.03) 1px, transparent 1px);
59
- background-size: 40px 40px;
60
  pointer-events: none;
61
  z-index: 0;
62
  }
63
 
64
- /* ── Header ─────────────────────────────────────────────────────── */
 
 
65
  header {
66
  position: sticky;
67
  top: 0;
68
  z-index: 100;
69
- background: rgba(10,15,30,0.85);
70
- backdrop-filter: blur(16px);
71
- border-bottom: 1px solid var(--border);
72
- padding: 0 2rem;
73
- height: 64px;
 
74
  display: flex;
75
  align-items: center;
76
  justify-content: space-between;
@@ -79,132 +151,266 @@
79
  .logo {
80
  display: flex;
81
  align-items: center;
82
- gap: 10px;
83
  }
84
  .logo-icon {
85
- width: 32px; height: 32px;
86
- background: linear-gradient(135deg, var(--accent-blue), var(--accent-cyan));
87
- border-radius: 8px;
88
- display: flex; align-items: center; justify-content: center;
 
 
89
  font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
- .logo-text { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.3px; }
92
- .logo-text span { color: var(--accent-blue); }
93
 
94
- .header-status {
 
 
 
 
 
 
 
 
 
 
 
95
  display: flex;
96
  align-items: center;
97
- gap: 1.5rem;
 
 
 
 
98
  }
99
  .status-dot {
100
- width: 8px; height: 8px;
101
  border-radius: 50%;
102
  background: var(--accent-green);
103
- box-shadow: 0 0 8px var(--accent-green);
104
- animation: pulse 2s infinite;
105
  }
106
- @keyframes pulse {
107
- 0%, 100% { opacity: 1; transform: scale(1); }
108
- 50% { opacity: 0.6; transform: scale(0.9); }
109
  }
110
- .status-label { font-size: 0.8rem; color: var(--text-secondary); }
111
-
112
- .task-badge {
113
- padding: 4px 12px;
114
- border-radius: 20px;
115
- font-size: 0.75rem;
116
  font-weight: 600;
117
- background: rgba(56,139,253,0.15);
118
- border: 1px solid var(--border);
119
- color: var(--accent-blue);
 
 
 
 
 
 
 
120
  }
121
 
122
- /* ── KPI Bar ────────────────────────────────────────────────────── */
123
- .kpi-bar {
124
- position: relative; z-index: 1;
 
 
 
125
  display: grid;
126
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
127
- gap: 1px;
128
- background: var(--border);
129
- border-bottom: 1px solid var(--border);
 
 
 
 
 
130
  }
 
131
  .kpi {
132
- background: var(--bg-surface);
133
- padding: 1rem 1.5rem;
134
- display: flex; flex-direction: column; gap: 2px;
135
- transition: background 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
- .kpi:hover { background: var(--bg-card); }
138
- .kpi-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.8px; color: var(--text-dim); }
139
  .kpi-value {
140
  font-family: var(--font-mono);
141
- font-size: 1.5rem;
142
  font-weight: 600;
143
  color: var(--text-primary);
144
- transition: color 0.3s;
 
145
  }
146
  .kpi-value.good { color: var(--accent-green); }
147
  .kpi-value.warn { color: var(--accent-amber); }
148
  .kpi-value.bad { color: var(--accent-red); }
149
- .kpi-delta { font-size: 0.72rem; color: var(--text-secondary); font-family: var(--font-mono); }
150
 
151
- /* ── Main Grid ──────────────────────────────────────────────────── */
 
 
 
 
 
 
 
 
 
152
  main {
153
- position: relative; z-index: 1;
154
- max-width: 1600px;
 
155
  margin: 0 auto;
156
- padding: 1.5rem;
157
  display: grid;
158
  grid-template-columns: repeat(12, 1fr);
159
  gap: 1rem;
160
  }
161
 
162
- /* ── Card ───────────────────────────────────────────────────────── */
 
 
163
  .card {
164
  background: var(--bg-card);
165
- border: 1px solid var(--border);
166
  border-radius: var(--radius-lg);
167
  padding: 1.25rem;
168
- transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s;
169
  position: relative;
170
  overflow: hidden;
171
  }
172
  .card::before {
173
  content: '';
174
  position: absolute;
175
- inset: 0;
176
- background: linear-gradient(135deg, rgba(56,139,253,0.03) 0%, transparent 60%);
177
- pointer-events: none;
178
  }
179
  .card:hover {
180
- border-color: var(--border-glow);
181
- box-shadow: var(--glow-blue);
 
182
  transform: translateY(-1px);
183
  }
 
184
  .card.alert-active {
185
- border-color: rgba(248,81,73,0.5);
186
- box-shadow: 0 0 20px rgba(248,81,73,0.2);
187
- animation: alertPulse 1.5s infinite;
188
  }
189
- @keyframes alertPulse {
190
- 0%, 100% { box-shadow: 0 0 20px rgba(248,81,73,0.2); }
191
- 50% { box-shadow: 0 0 35px rgba(248,81,73,0.4); }
192
  }
193
 
 
 
 
 
 
 
194
  .card-title {
195
- font-size: 0.78rem;
 
 
 
196
  font-weight: 600;
197
  text-transform: uppercase;
198
  letter-spacing: 0.8px;
199
  color: var(--text-secondary);
200
- margin-bottom: 0.75rem;
 
 
 
201
  display: flex;
202
  align-items: center;
203
- gap: 0.5rem;
 
204
  }
205
- .card-title .icon { font-size: 0.9rem; }
 
 
 
 
 
 
 
 
206
 
207
- /* ── Grid layout spans ─ */
 
 
 
 
 
 
 
 
 
 
 
 
208
  .col-12 { grid-column: span 12; }
209
  .col-8 { grid-column: span 8; }
210
  .col-6 { grid-column: span 6; }
@@ -212,366 +418,559 @@
212
  .col-3 { grid-column: span 3; }
213
 
214
  @media (max-width: 1200px) {
215
- .col-8 { grid-column: span 12; }
216
- .col-4 { grid-column: span 12; }
217
- .col-6 { grid-column: span 12; }
218
- .col-3 { grid-column: span 6; }
219
  }
220
  @media (max-width: 768px) {
221
- .col-3 { grid-column: span 12; }
222
  main { padding: 0.75rem; gap: 0.75rem; }
223
  }
224
 
225
- /* ── Charts ─────────────────────────────────────────────────────── */
226
  .chart-wrap { position: relative; height: 200px; }
227
  .chart-wrap.tall { height: 260px; }
228
  .chart-wrap.short { height: 150px; }
229
 
230
- /* ── Thermal Storage Bar ─────────────────────────────────────────── */
231
- .storage-bar-wrap {
232
- height: 28px;
233
- background: rgba(255,255,255,0.05);
234
- border-radius: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  overflow: hidden;
236
- margin-top: 0.5rem;
237
  position: relative;
238
  }
239
  .storage-bar-fill {
240
  height: 100%;
241
- border-radius: 14px;
242
- background: linear-gradient(90deg, var(--accent-cyan), var(--accent-blue));
243
- transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
244
  position: relative;
245
  }
246
  .storage-bar-fill::after {
247
  content: '';
248
  position: absolute;
249
- inset: 0;
250
- background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.15) 50%, transparent 100%);
251
- animation: shimmer 2s infinite;
252
  }
253
- @keyframes shimmer {
254
  0% { transform: translateX(-100%); }
255
- 100% { transform: translateX(100%); }
 
 
 
 
 
 
 
 
256
  }
257
- .storage-label {
258
  font-family: var(--font-mono);
259
- font-size: 1.8rem;
260
  font-weight: 700;
261
- color: var(--accent-cyan);
262
- margin-top: 0.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  }
264
- .storage-label span { font-size: 1rem; color: var(--text-secondary); }
265
 
266
- /* ── Batch Gantt ─────────────────────────────────────────────────── */
267
- .gantt-wrap {
 
 
268
  display: flex;
269
  flex-direction: column;
270
- gap: 6px;
271
- margin-top: 0.25rem;
272
  }
273
  .gantt-row {
274
  display: flex;
275
  align-items: center;
276
- gap: 8px;
277
  font-size: 0.75rem;
278
  }
279
  .gantt-label {
280
- width: 40px;
281
- color: var(--text-secondary);
282
  font-family: var(--font-mono);
 
 
283
  flex-shrink: 0;
 
284
  }
285
  .gantt-track {
286
  flex: 1;
287
- height: 18px;
288
- background: rgba(255,255,255,0.05);
289
- border-radius: 4px;
290
  position: relative;
291
  overflow: hidden;
292
  }
293
  .gantt-block {
294
  position: absolute;
295
- top: 0; bottom: 0;
296
  border-radius: 4px;
297
- transition: width 0.3s, left 0.3s;
298
  }
299
- .gantt-block.scheduled { background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple)); }
300
- .gantt-block.completed { background: var(--accent-green); opacity: 0.7; }
301
- .gantt-block.missed { background: var(--accent-red); opacity: 0.8; }
302
  .gantt-deadline {
303
  position: absolute;
304
- top: 0; bottom: 0;
305
  width: 2px;
306
  background: var(--accent-amber);
307
  border-radius: 1px;
 
308
  }
309
  .gantt-status {
310
- width: 60px;
311
  text-align: right;
312
  flex-shrink: 0;
313
  }
314
  .badge {
 
315
  padding: 2px 8px;
316
- border-radius: 10px;
317
- font-size: 0.7rem;
318
  font-weight: 600;
 
 
319
  }
320
- .badge.ok { background: rgba(63,185,80,0.2); color: var(--accent-green); }
321
- .badge.pending { background: rgba(56,139,253,0.2); color: var(--accent-blue); }
322
- .badge.missed { background: rgba(248,81,73,0.2); color: var(--accent-red); }
323
- .badge.running { background: rgba(188,140,255,0.2); color: var(--accent-purple); }
324
 
325
- /* ── Reward breakdown mini-bars ──────────────────────────────── */
326
- .reward-row {
327
- display: flex; align-items: center; gap: 8px;
328
- font-size: 0.75rem; margin-bottom: 4px;
 
329
  }
330
- .reward-label { width: 100px; color: var(--text-secondary); }
331
- .reward-bar-wrap { flex: 1; height: 10px; background: rgba(255,255,255,0.05); border-radius: 5px; overflow: hidden; }
332
- .reward-bar { height: 100%; border-radius: 5px; transition: width 0.5s; }
333
- .reward-val { width: 55px; text-align: right; font-family: var(--font-mono); color: var(--text-primary); }
334
 
335
- /* ── Grid stress indicator ──────────────────────────────────── */
336
- .stress-meter {
337
- display: flex; align-items: flex-end; gap: 3px;
338
- height: 40px;
339
- margin-top: 0.5rem;
 
 
 
340
  }
341
- .stress-bar {
 
 
 
 
 
 
342
  flex: 1;
343
- background: rgba(255,255,255,0.05);
344
- border-radius: 2px 2px 0 0;
345
- transition: height 0.4s, background 0.4s;
 
346
  }
347
-
348
- /* ── Big number display ─────────────────────────────────────── */
349
- .big-num {
350
- font-family: var(--font-mono);
351
- font-weight: 700;
352
  }
353
- .big-num.xl { font-size: 2.8rem; }
354
- .big-num.lg { font-size: 2rem; }
355
- .big-num.md { font-size: 1.4rem; }
356
- .big-num.green { color: var(--accent-green); }
357
- .big-num.blue { color: var(--accent-blue); }
358
- .big-num.amber { color: var(--accent-amber); }
359
- .big-num.red { color: var(--accent-red); }
360
- .big-num.purple { color: var(--accent-purple); }
361
-
362
- .sub-label { font-size: 0.75rem; color: var(--text-secondary); margin-top: 2px; }
363
-
364
- /* ── Price ticker ──────────────────────────────────────────── */
365
- .price-row {
366
- display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
367
- }
368
- .price-tier {
369
- display: flex; align-items: center; gap: 6px;
370
- padding: 6px 10px;
371
- border-radius: 8px;
372
- background: rgba(255,255,255,0.04);
373
- border: 1px solid rgba(255,255,255,0.06);
374
- font-size: 0.8rem;
375
  }
376
- .price-tier .dot { width: 8px; height: 8px; border-radius: 50%; }
377
 
378
- /* ── Control panel ──────────────────────────────────────────── */
379
- .ctrl-row {
380
- display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
381
- margin-top: 0.5rem;
 
 
 
 
382
  }
 
383
  .btn {
384
- padding: 8px 16px;
385
- border-radius: 8px;
386
- border: 1px solid var(--border);
387
- background: rgba(56,139,253,0.1);
388
- color: var(--accent-blue);
389
- font-size: 0.82rem;
390
  font-weight: 600;
 
391
  cursor: pointer;
392
- transition: all 0.2s;
393
- font-family: 'Inter', sans-serif;
 
 
 
394
  }
 
395
  .btn:hover {
396
- background: rgba(56,139,253,0.2);
397
- border-color: var(--accent-blue);
398
- box-shadow: 0 0 12px rgba(56,139,253,0.3);
399
  transform: translateY(-1px);
 
400
  }
 
 
401
  .btn.primary {
402
- background: var(--accent-blue);
403
- color: #fff;
 
 
404
  }
405
- .btn.primary:hover { background: #4da3ff; }
406
- .btn.danger {
407
- background: rgba(248,81,73,0.15);
408
- color: var(--accent-red);
409
- border-color: rgba(248,81,73,0.3);
410
  }
411
 
412
- select {
413
- padding: 8px 12px;
414
- background: var(--bg-card);
415
- border: 1px solid var(--border);
416
- border-radius: 8px;
417
- color: var(--text-primary);
418
- font-size: 0.82rem;
419
- font-family: 'Inter', sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  }
422
- select:focus { outline: none; border-color: var(--accent-blue); }
423
 
424
- /* ── Connection banner ──────────────────────────────────────── */
 
 
425
  #conn-banner {
426
  display: none;
427
  position: fixed;
428
- top: 64px; left: 0; right: 0;
429
  z-index: 200;
430
- background: rgba(248,81,73,0.15);
431
- border-bottom: 1px solid rgba(248,81,73,0.3);
 
432
  text-align: center;
433
  padding: 10px;
434
- font-size: 0.85rem;
 
435
  color: var(--accent-red);
436
  }
437
  #conn-banner.show { display: block; }
438
 
439
- /* ── Footer ─────────────────────────────────────────────────── */
 
 
440
  footer {
441
- position: relative; z-index: 1;
 
442
  text-align: center;
443
  padding: 1.5rem;
444
- color: var(--text-dim);
445
- font-size: 0.75rem;
446
- border-top: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  }
448
  </style>
449
  </head>
450
  <body>
451
 
452
- <!-- Connection error banner -->
 
 
453
  <div id="conn-banner">
454
- Environment server unreachable retrying...
 
455
  </div>
456
 
457
- <!-- ── Header ─── -->
458
  <header>
459
  <div class="logo">
460
- <div class="logo-icon">⚡</div>
 
 
461
  <div class="logo-text">Grid<span>Mind</span>-RL</div>
 
462
  </div>
463
- <div class="header-status">
464
  <span id="task-badge" class="task-badge">Task 1 — Cost Minimization</span>
465
- <div style="display:flex;align-items:center;gap:6px">
466
  <div class="status-dot" id="status-dot"></div>
467
  <span class="status-label" id="status-label">Live</span>
468
  </div>
469
- <span id="ep-step" style="font-family:var(--font-mono);font-size:0.8rem;color:var(--text-secondary)">ep:— step:—</span>
470
  </div>
471
  </header>
472
 
473
- <!-- ── KPI Bar ─── -->
474
- <div class="kpi-bar">
475
  <div class="kpi">
476
- <span class="kpi-label">Current Price</span>
477
- <span class="kpi-value" id="kpi-price">—</span>
478
- <span class="kpi-delta">$/kWh</span>
479
  </div>
480
  <div class="kpi">
481
- <span class="kpi-label">Indoor Temp</span>
482
- <span class="kpi-value" id="kpi-temp">—</span>
483
- <span class="kpi-delta">°C (target 21°C)</span>
484
  </div>
485
  <div class="kpi">
486
- <span class="kpi-label">Grid Stress</span>
487
- <span class="kpi-value" id="kpi-stress">—</span>
488
- <span class="kpi-delta">0=normal 1=critical</span>
489
  </div>
490
  <div class="kpi">
491
- <span class="kpi-label">Cumulative Cost</span>
492
- <span class="kpi-value" id="kpi-cost">—</span>
493
- <span class="kpi-delta">vs baseline: <span id="kpi-baseline">—</span></span>
494
  </div>
495
  <div class="kpi">
496
- <span class="kpi-label">Carbon Intensity</span>
497
- <span class="kpi-value" id="kpi-carbon">—</span>
498
- <span class="kpi-delta">gCO₂/kWh</span>
499
  </div>
500
  <div class="kpi">
501
- <span class="kpi-label">Process Demand</span>
502
- <span class="kpi-value" id="kpi-demand">—</span>
503
- <span class="kpi-delta">kW</span>
504
  </div>
505
  <div class="kpi">
506
- <span class="kpi-label">Thermal Storage</span>
507
- <span class="kpi-value" id="kpi-storage">—</span>
508
- <span class="kpi-delta">% capacity</span>
509
  </div>
510
  </div>
511
 
512
- <!-- ── Main Content ─── -->
513
  <main>
514
 
515
- <!-- Row 1: Price curve + Temperature + Controls -->
516
  <div class="card col-8">
517
- <div class="card-title"><span class="icon">💰</span> Electricity Price Curve (24h)</div>
 
 
 
 
 
 
518
  <div class="chart-wrap">
519
  <canvas id="chart-price"></canvas>
520
  </div>
521
  </div>
522
 
523
  <div class="card col-4" id="card-stress">
524
- <div class="card-title"><span class="icon">⚠️</span> Grid Stress Signal</div>
525
- <div class="big-num xl" id="stress-big">0.00</div>
526
- <div class="sub-label">Demand-response urgency</div>
527
- <div class="stress-meter" id="stress-meter"></div>
528
- <div style="margin-top:0.75rem;">
529
- <div class="chart-wrap short">
530
- <canvas id="chart-stress"></canvas>
531
  </div>
 
 
 
 
 
 
 
 
 
532
  </div>
533
  </div>
534
 
535
  <!-- Row 2: Temperature + Storage + HVAC -->
536
  <div class="card col-6">
537
- <div class="card-title"><span class="icon">🌡️</span> Temperature Timeline</div>
 
 
 
 
 
 
538
  <div class="chart-wrap tall">
539
  <canvas id="chart-temp"></canvas>
540
  </div>
541
  </div>
542
 
543
  <div class="card col-3">
544
- <div class="card-title"><span class="icon">🔋</span> Thermal Storage Level</div>
545
- <div class="storage-label"><span id="storage-pct">—</span><span>%</span></div>
546
- <div class="storage-bar-wrap">
547
- <div class="storage-bar-fill" id="storage-fill" style="width:0%"></div>
548
- </div>
549
- <div style="margin-top:1rem">
550
- <div class="chart-wrap short">
551
- <canvas id="chart-storage"></canvas>
552
  </div>
553
  </div>
 
 
 
 
 
 
 
 
 
554
  </div>
555
 
556
  <div class="card col-3">
557
- <div class="card-title"><span class="icon">❄️</span> HVAC + Load Shed</div>
 
 
 
 
 
558
  <div class="chart-wrap tall">
559
  <canvas id="chart-hvac"></canvas>
560
  </div>
561
  </div>
562
 
563
- <!-- Row 3: Cost comparison + Reward breakdown -->
564
  <div class="card col-8">
565
- <div class="card-title"><span class="icon">📊</span> Cumulative Cost vs Baseline</div>
 
 
 
 
 
 
566
  <div class="chart-wrap tall">
567
  <canvas id="chart-cost"></canvas>
568
  </div>
569
  </div>
570
 
571
  <div class="card col-4">
572
- <div class="card-title"><span class="icon">🏆</span> Reward Breakdown</div>
573
- <div id="reward-rows" style="margin-top:0.5rem"></div>
574
- <div style="margin-top:1rem">
 
 
 
 
 
 
575
  <div class="chart-wrap short">
576
  <canvas id="chart-reward"></canvas>
577
  </div>
@@ -580,14 +979,26 @@
580
 
581
  <!-- Row 4: Batch Gantt + Carbon -->
582
  <div class="card col-6">
583
- <div class="card-title"><span class="icon">⚙️</span> Batch Job Timeline</div>
584
- <div class="gantt-wrap" id="gantt-wrap">
585
- <div style="color:var(--text-dim);font-size:0.8rem">No batch jobs queued.</div>
 
 
 
 
 
 
586
  </div>
587
  </div>
588
 
589
  <div class="card col-6">
590
- <div class="card-title"><span class="icon">🌍</span> Carbon Intensity Curve (24h)</div>
 
 
 
 
 
 
591
  <div class="chart-wrap">
592
  <canvas id="chart-carbon"></canvas>
593
  </div>
@@ -595,32 +1006,49 @@
595
 
596
  <!-- Row 5: Controls -->
597
  <div class="card col-12">
598
- <div class="card-title"><span class="icon">🎮</span> Episode Controls</div>
599
- <div class="ctrl-row">
600
- <select id="task-select" onchange="onTaskChange()">
 
 
 
 
 
601
  <option value="1">Task 1 — Cost Minimization (Easy)</option>
602
  <option value="2">Task 2 — Temperature Management (Medium)</option>
603
  <option value="3">Task 3 — Full Demand Response (Hard)</option>
604
  </select>
605
- <select id="building-select" onchange="onBuildingChange()">
606
  <option value="0">Building 1 (Primary)</option>
607
  <option value="1">Building 2</option>
608
  <option value="2">Building 3</option>
609
  </select>
610
- <button id="btn-reset" class="btn primary" onclick="doReset()">↺ New Episode</button>
611
- <button id="btn-live" class="btn" style="background:var(--accent-green);color:#fff;border:none;" onclick="toggleLiveSim()"> Start Live Simulation</button>
612
- <button class="btn" onclick="doGrade()">📋 Grade Episode</button>
613
- <button class="btn" onclick="window.open('/api/replay')">📥 Export Replay</button>
614
- <span id="grade-result" style="font-family:var(--font-mono);font-size:0.9rem;color:var(--accent-green)"></span>
 
 
 
 
 
 
 
 
 
 
 
 
615
  </div>
616
  </div>
617
 
618
  </main>
619
 
620
  <footer>
621
- GridMind-RL &nbsp;·&nbsp; OpenEnv-compliant RL environment for industrial demand response &nbsp;·&nbsp;
622
- <a href="/api/health" target="_blank" style="color:var(--accent-blue);text-decoration:none">API Health</a> &nbsp;·&nbsp;
623
- <a href="/api/metrics" target="_blank" style="color:var(--accent-blue);text-decoration:none">Metrics</a>
624
  </footer>
625
 
626
  <script src="/static/dashboard.js"></script>
 
8
  <!-- Chart.js CDN -->
9
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
10
  <!-- Google Fonts -->
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
12
+ <!-- Lucide Icons -->
13
+ <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
14
  <style>
15
+ /* ══════════════════════════════════════════════════════════════════
16
+ GridMind-RL — Premium Matte Dashboard Design System
17
+ ══════════════════════════════════════════════════════════════════ */
18
  :root {
19
+ /* Matte dark palette — deep, soft, no harsh contrast */
20
+ --bg-base: #0c0e14;
21
+ --bg-elevated: #12151e;
22
+ --bg-surface: #171b27;
23
+ --bg-card: #1a1f2e;
24
+ --bg-card-hover: #1f2538;
25
+ --bg-inset: #141824;
26
+ --bg-overlay: rgba(12, 14, 20, 0.92);
27
+
28
+ /* Borders — ultra subtle */
29
+ --border-subtle: rgba(255, 255, 255, 0.04);
30
+ --border-default: rgba(255, 255, 255, 0.06);
31
+ --border-hover: rgba(255, 255, 255, 0.10);
32
+ --border-accent: rgba(99, 179, 237, 0.20);
33
+
34
+ /* Text — muted and refined */
35
+ --text-primary: #e8ecf4;
36
+ --text-secondary: #8a94a8;
37
+ --text-tertiary: #5a6478;
38
+ --text-muted: #3d4558;
39
+
40
+ /* Accent palette — muted, sophisticated */
41
+ --accent-blue: #5b9cf6;
42
+ --accent-green: #4ade80;
43
+ --accent-amber: #f5a623;
44
+ --accent-red: #f06e6e;
45
+ --accent-purple: #a78bfa;
46
+ --accent-cyan: #34d4e4;
47
+ --accent-orange: #fb923c;
48
+ --accent-teal: #2dd4bf;
49
+ --accent-rose: #fb7185;
50
+
51
+ /* Gradients */
52
+ --grad-blue: linear-gradient(135deg, #5b9cf6 0%, #818cf8 100%);
53
+ --grad-green: linear-gradient(135deg, #4ade80 0%, #34d4e4 100%);
54
+ --grad-amber: linear-gradient(135deg, #f5a623 0%, #fb923c 100%);
55
+ --grad-red: linear-gradient(135deg, #f06e6e 0%, #fb7185 100%);
56
+ --grad-purple: linear-gradient(135deg, #a78bfa 0%, #818cf8 100%);
57
+ --grad-hero: linear-gradient(160deg, #0c0e14 0%, #12151e 30%, #171b27 100%);
58
+
59
+ /* Glass */
60
+ --glass-bg: rgba(26, 31, 46, 0.60);
61
+ --glass-border: rgba(255, 255, 255, 0.06);
62
+ --glass-blur: 20px;
63
+
64
+ /* Shadows */
65
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
66
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
67
+ --shadow-lg: 0 8px 32px rgba(0,0,0,0.5);
68
+ --shadow-glow-blue: 0 0 24px rgba(91,156,246,0.12);
69
+ --shadow-glow-green: 0 0 24px rgba(74,222,128,0.12);
70
+
71
+ /* Typography */
72
+ --font-sans: 'Inter', -apple-system, system-ui, sans-serif;
73
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
74
+
75
+ /* Spacing & Radius */
76
+ --radius-sm: 8px;
77
+ --radius-md: 12px;
78
+ --radius-lg: 16px;
79
+ --radius-xl: 20px;
80
+ --radius-pill: 9999px;
81
+
82
+ /* Transitions */
83
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
84
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
85
+ --duration-fast: 150ms;
86
+ --duration-base: 250ms;
87
+ --duration-slow: 400ms;
88
  }
89
 
90
+ /* ── Reset ────────────────────────────────────────────────────────── */
91
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
92
+ html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
 
93
 
94
  body {
95
+ font-family: var(--font-sans);
96
  background: var(--bg-base);
97
  color: var(--text-primary);
98
  min-height: 100vh;
99
  overflow-x: hidden;
100
+ line-height: 1.5;
101
  }
102
 
103
+ /* ── Ambient background ───────────────────────────────────────────── */
104
  body::before {
105
  content: '';
106
+ position: fixed;
107
+ top: -30%; left: -10%;
108
+ width: 60%; height: 60%;
109
+ background: radial-gradient(circle, rgba(91,156,246,0.04) 0%, transparent 65%);
110
+ pointer-events: none;
111
+ z-index: 0;
112
+ }
113
+ body::after {
114
+ content: '';
115
+ position: fixed;
116
+ bottom: -20%; right: -10%;
117
+ width: 50%; height: 50%;
118
+ background: radial-gradient(circle, rgba(168,85,247,0.03) 0%, transparent 65%);
119
+ pointer-events: none;
120
+ z-index: 0;
121
+ }
122
+
123
+ /* Subtle dot pattern */
124
+ .bg-pattern {
125
  position: fixed;
126
  inset: 0;
127
+ background-image: radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
128
+ background-size: 24px 24px;
 
 
129
  pointer-events: none;
130
  z-index: 0;
131
  }
132
 
133
+ /* ══════════════════════════════════════════════════════════════════
134
+ HEADER — Glass morphism nav bar
135
+ ══════════════════════════════════════════════════════════════════ */
136
  header {
137
  position: sticky;
138
  top: 0;
139
  z-index: 100;
140
+ background: var(--bg-overlay);
141
+ backdrop-filter: blur(24px) saturate(1.2);
142
+ -webkit-backdrop-filter: blur(24px) saturate(1.2);
143
+ border-bottom: 1px solid var(--border-subtle);
144
+ padding: 0 1.75rem;
145
+ height: 60px;
146
  display: flex;
147
  align-items: center;
148
  justify-content: space-between;
 
151
  .logo {
152
  display: flex;
153
  align-items: center;
154
+ gap: 12px;
155
  }
156
  .logo-icon {
157
+ width: 36px; height: 36px;
158
+ background: var(--grad-blue);
159
+ border-radius: 10px;
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: center;
163
  font-size: 16px;
164
+ box-shadow: 0 2px 12px rgba(91,156,246,0.25);
165
+ position: relative;
166
+ overflow: hidden;
167
+ }
168
+ .logo-icon::after {
169
+ content: '';
170
+ position: absolute;
171
+ inset: 0;
172
+ background: linear-gradient(135deg, rgba(255,255,255,0.15) 0%, transparent 50%);
173
+ }
174
+ .logo-icon svg { width: 18px; height: 18px; color: white; position: relative; z-index: 1; }
175
+ .logo-text {
176
+ font-size: 1.05rem;
177
+ font-weight: 700;
178
+ letter-spacing: -0.5px;
179
+ color: var(--text-primary);
180
+ }
181
+ .logo-text span {
182
+ background: var(--grad-blue);
183
+ -webkit-background-clip: text;
184
+ -webkit-text-fill-color: transparent;
185
+ background-clip: text;
186
+ }
187
+ .logo-tag {
188
+ font-size: 0.6rem;
189
+ font-weight: 600;
190
+ text-transform: uppercase;
191
+ letter-spacing: 1.5px;
192
+ color: var(--text-tertiary);
193
+ padding: 2px 8px;
194
+ border: 1px solid var(--border-default);
195
+ border-radius: var(--radius-pill);
196
+ margin-left: 4px;
197
+ }
198
+
199
+ .header-right {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 1rem;
203
  }
 
 
204
 
205
+ .task-badge {
206
+ padding: 5px 14px;
207
+ border-radius: var(--radius-pill);
208
+ font-size: 0.72rem;
209
+ font-weight: 600;
210
+ background: rgba(91,156,246,0.10);
211
+ border: 1px solid rgba(91,156,246,0.15);
212
+ color: var(--accent-blue);
213
+ letter-spacing: 0.2px;
214
+ }
215
+
216
+ .status-indicator {
217
  display: flex;
218
  align-items: center;
219
+ gap: 8px;
220
+ padding: 5px 14px;
221
+ border-radius: var(--radius-pill);
222
+ background: rgba(74,222,128,0.08);
223
+ border: 1px solid rgba(74,222,128,0.12);
224
  }
225
  .status-dot {
226
+ width: 7px; height: 7px;
227
  border-radius: 50%;
228
  background: var(--accent-green);
229
+ box-shadow: 0 0 8px rgba(74,222,128,0.5);
230
+ animation: statusPulse 2.5s ease-in-out infinite;
231
  }
232
+ @keyframes statusPulse {
233
+ 0%, 100% { opacity: 1; box-shadow: 0 0 8px rgba(74,222,128,0.5); }
234
+ 50% { opacity: 0.5; box-shadow: 0 0 16px rgba(74,222,128,0.3); }
235
  }
236
+ .status-label {
237
+ font-size: 0.72rem;
 
 
 
 
238
  font-weight: 600;
239
+ color: var(--accent-green);
240
+ letter-spacing: 0.5px;
241
+ text-transform: uppercase;
242
+ }
243
+
244
+ .header-meta {
245
+ font-family: var(--font-mono);
246
+ font-size: 0.72rem;
247
+ color: var(--text-tertiary);
248
+ letter-spacing: 0.5px;
249
  }
250
 
251
+ /* ══════════════════════════════════════════════════════════════════
252
+ KPI BAR — Elevated metrics strip
253
+ ══════════════════════════════════════════════════════════════════ */
254
+ .kpi-strip {
255
+ position: relative;
256
+ z-index: 1;
257
  display: grid;
258
+ grid-template-columns: repeat(7, 1fr);
259
+ background: var(--bg-elevated);
260
+ border-bottom: 1px solid var(--border-subtle);
261
+ }
262
+ @media (max-width: 1100px) {
263
+ .kpi-strip { grid-template-columns: repeat(4, 1fr); }
264
+ }
265
+ @media (max-width: 640px) {
266
+ .kpi-strip { grid-template-columns: repeat(2, 1fr); }
267
  }
268
+
269
  .kpi {
270
+ padding: 1rem 1.25rem;
271
+ border-right: 1px solid var(--border-subtle);
272
+ transition: background var(--duration-base) var(--ease-out);
273
+ cursor: default;
274
+ position: relative;
275
+ }
276
+ .kpi:last-child { border-right: none; }
277
+ .kpi:hover { background: var(--bg-surface); }
278
+ .kpi::after {
279
+ content: '';
280
+ position: absolute;
281
+ bottom: 0; left: 50%;
282
+ width: 0; height: 2px;
283
+ background: var(--grad-blue);
284
+ transition: all var(--duration-base) var(--ease-out);
285
+ transform: translateX(-50%);
286
+ border-radius: 1px;
287
+ }
288
+ .kpi:hover::after { width: 60%; }
289
+
290
+ .kpi-label {
291
+ font-size: 0.65rem;
292
+ font-weight: 600;
293
+ text-transform: uppercase;
294
+ letter-spacing: 1px;
295
+ color: var(--text-tertiary);
296
+ margin-bottom: 4px;
297
  }
 
 
298
  .kpi-value {
299
  font-family: var(--font-mono);
300
+ font-size: 1.35rem;
301
  font-weight: 600;
302
  color: var(--text-primary);
303
+ transition: color var(--duration-base);
304
+ line-height: 1.2;
305
  }
306
  .kpi-value.good { color: var(--accent-green); }
307
  .kpi-value.warn { color: var(--accent-amber); }
308
  .kpi-value.bad { color: var(--accent-red); }
 
309
 
310
+ .kpi-unit {
311
+ font-size: 0.65rem;
312
+ color: var(--text-muted);
313
+ font-family: var(--font-mono);
314
+ margin-top: 2px;
315
+ }
316
+
317
+ /* ══════════════════════════════════════════════════════════════════
318
+ MAIN LAYOUT — 12-column grid
319
+ ═════════════════════════════��════════════════════════════════════ */
320
  main {
321
+ position: relative;
322
+ z-index: 1;
323
+ max-width: 1560px;
324
  margin: 0 auto;
325
+ padding: 1.25rem;
326
  display: grid;
327
  grid-template-columns: repeat(12, 1fr);
328
  gap: 1rem;
329
  }
330
 
331
+ /* ══════════════════════════════════════════════════════════════════
332
+ CARD — Matte glass panels
333
+ ══════════════════════════════════════════════════════════════════ */
334
  .card {
335
  background: var(--bg-card);
336
+ border: 1px solid var(--border-default);
337
  border-radius: var(--radius-lg);
338
  padding: 1.25rem;
339
+ transition: all var(--duration-base) var(--ease-out);
340
  position: relative;
341
  overflow: hidden;
342
  }
343
  .card::before {
344
  content: '';
345
  position: absolute;
346
+ top: 0; left: 0; right: 0;
347
+ height: 1px;
348
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.06) 50%, transparent 100%);
349
  }
350
  .card:hover {
351
+ border-color: var(--border-hover);
352
+ background: var(--bg-card-hover);
353
+ box-shadow: var(--shadow-md);
354
  transform: translateY(-1px);
355
  }
356
+
357
  .card.alert-active {
358
+ border-color: rgba(240,110,110,0.35);
359
+ box-shadow: 0 0 30px rgba(240,110,110,0.08);
360
+ animation: alertGlow 2s ease-in-out infinite;
361
  }
362
+ @keyframes alertGlow {
363
+ 0%, 100% { box-shadow: 0 0 20px rgba(240,110,110,0.06); }
364
+ 50% { box-shadow: 0 0 40px rgba(240,110,110,0.12); }
365
  }
366
 
367
+ .card-header {
368
+ display: flex;
369
+ align-items: center;
370
+ justify-content: space-between;
371
+ margin-bottom: 1rem;
372
+ }
373
  .card-title {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 8px;
377
+ font-size: 0.75rem;
378
  font-weight: 600;
379
  text-transform: uppercase;
380
  letter-spacing: 0.8px;
381
  color: var(--text-secondary);
382
+ }
383
+ .card-title .icon-wrap {
384
+ width: 28px; height: 28px;
385
+ border-radius: var(--radius-sm);
386
  display: flex;
387
  align-items: center;
388
+ justify-content: center;
389
+ flex-shrink: 0;
390
  }
391
+ .card-title .icon-wrap svg { width: 14px; height: 14px; }
392
+ .card-title .icon-wrap.blue { background: rgba(91,156,246,0.12); color: var(--accent-blue); }
393
+ .card-title .icon-wrap.green { background: rgba(74,222,128,0.12); color: var(--accent-green); }
394
+ .card-title .icon-wrap.amber { background: rgba(245,166,35,0.12); color: var(--accent-amber); }
395
+ .card-title .icon-wrap.red { background: rgba(240,110,110,0.12); color: var(--accent-red); }
396
+ .card-title .icon-wrap.purple { background: rgba(167,139,250,0.12); color: var(--accent-purple); }
397
+ .card-title .icon-wrap.cyan { background: rgba(52,212,228,0.12); color: var(--accent-cyan); }
398
+ .card-title .icon-wrap.orange { background: rgba(251,146,60,0.12); color: var(--accent-orange); }
399
+ .card-title .icon-wrap.teal { background: rgba(45,212,191,0.12); color: var(--accent-teal); }
400
 
401
+ .card-badge {
402
+ font-size: 0.62rem;
403
+ font-weight: 600;
404
+ padding: 3px 8px;
405
+ border-radius: var(--radius-pill);
406
+ background: rgba(255,255,255,0.04);
407
+ border: 1px solid var(--border-subtle);
408
+ color: var(--text-tertiary);
409
+ text-transform: uppercase;
410
+ letter-spacing: 0.5px;
411
+ }
412
+
413
+ /* Column spans */
414
  .col-12 { grid-column: span 12; }
415
  .col-8 { grid-column: span 8; }
416
  .col-6 { grid-column: span 6; }
 
418
  .col-3 { grid-column: span 3; }
419
 
420
  @media (max-width: 1200px) {
421
+ .col-8, .col-6 { grid-column: span 12; }
422
+ .col-4, .col-3 { grid-column: span 6; }
 
 
423
  }
424
  @media (max-width: 768px) {
425
+ .col-3, .col-4 { grid-column: span 12; }
426
  main { padding: 0.75rem; gap: 0.75rem; }
427
  }
428
 
429
+ /* ── Chart containers ─────────────────────────────────────────────── */
430
  .chart-wrap { position: relative; height: 200px; }
431
  .chart-wrap.tall { height: 260px; }
432
  .chart-wrap.short { height: 150px; }
433
 
434
+ /* ══════════════════════════════════════════════════════════════════
435
+ THERMAL STORAGE — Premium gauge
436
+ ══════════════════════════════════════════════════════════════════ */
437
+ .storage-display {
438
+ text-align: center;
439
+ padding: 0.5rem 0;
440
+ }
441
+ .storage-value {
442
+ font-family: var(--font-mono);
443
+ font-size: 2.5rem;
444
+ font-weight: 700;
445
+ line-height: 1;
446
+ background: var(--grad-green);
447
+ -webkit-background-clip: text;
448
+ -webkit-text-fill-color: transparent;
449
+ background-clip: text;
450
+ }
451
+ .storage-value span {
452
+ font-size: 1.2rem;
453
+ -webkit-text-fill-color: var(--text-tertiary);
454
+ }
455
+ .storage-bar-track {
456
+ height: 8px;
457
+ background: var(--bg-inset);
458
+ border-radius: var(--radius-pill);
459
  overflow: hidden;
460
+ margin: 0.75rem 0;
461
  position: relative;
462
  }
463
  .storage-bar-fill {
464
  height: 100%;
465
+ border-radius: var(--radius-pill);
466
+ background: var(--grad-green);
467
+ transition: width 0.6s var(--ease-out);
468
  position: relative;
469
  }
470
  .storage-bar-fill::after {
471
  content: '';
472
  position: absolute;
473
+ top: 0; left: 0; right: 0; bottom: 0;
474
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
475
+ animation: barShimmer 2.5s ease-in-out infinite;
476
  }
477
+ @keyframes barShimmer {
478
  0% { transform: translateX(-100%); }
479
+ 100% { transform: translateX(200%); }
480
+ }
481
+
482
+ /* ══════════════════════════════════════════════════════════════════
483
+ STRESS DISPLAY
484
+ ══════════════════════════════════════════════════════════════════ */
485
+ .stress-display {
486
+ text-align: center;
487
+ padding: 0.25rem 0;
488
  }
489
+ .stress-value {
490
  font-family: var(--font-mono);
491
+ font-size: 2.8rem;
492
  font-weight: 700;
493
+ line-height: 1;
494
+ color: var(--text-primary);
495
+ transition: color var(--duration-base);
496
+ }
497
+ .stress-value.low { color: var(--accent-green); }
498
+ .stress-value.mid { color: var(--accent-amber); }
499
+ .stress-value.high { color: var(--accent-red); }
500
+ .stress-sub {
501
+ font-size: 0.72rem;
502
+ color: var(--text-tertiary);
503
+ margin-top: 4px;
504
+ }
505
+
506
+ .stress-meter {
507
+ display: flex;
508
+ align-items: flex-end;
509
+ gap: 3px;
510
+ height: 36px;
511
+ margin: 0.75rem 0;
512
+ padding: 0 0.5rem;
513
+ }
514
+ .stress-bar {
515
+ flex: 1;
516
+ background: rgba(255,255,255,0.04);
517
+ border-radius: 2px 2px 0 0;
518
+ transition: height var(--duration-slow) var(--ease-out),
519
+ background var(--duration-base);
520
+ min-height: 3px;
521
  }
 
522
 
523
+ /* ══════════════════════════════════════════════════════════════════
524
+ BATCH GANTT
525
+ ══════════════════════════════════════════════════════════════════ */
526
+ .gantt-container {
527
  display: flex;
528
  flex-direction: column;
529
+ gap: 8px;
 
530
  }
531
  .gantt-row {
532
  display: flex;
533
  align-items: center;
534
+ gap: 10px;
535
  font-size: 0.75rem;
536
  }
537
  .gantt-label {
538
+ width: 36px;
 
539
  font-family: var(--font-mono);
540
+ font-weight: 600;
541
+ color: var(--text-tertiary);
542
  flex-shrink: 0;
543
+ font-size: 0.7rem;
544
  }
545
  .gantt-track {
546
  flex: 1;
547
+ height: 22px;
548
+ background: var(--bg-inset);
549
+ border-radius: 6px;
550
  position: relative;
551
  overflow: hidden;
552
  }
553
  .gantt-block {
554
  position: absolute;
555
+ top: 3px; bottom: 3px;
556
  border-radius: 4px;
557
+ transition: width var(--duration-base), left var(--duration-base);
558
  }
559
+ .gantt-block.scheduled { background: var(--grad-blue); opacity: 0.85; }
560
+ .gantt-block.completed { background: var(--grad-green); opacity: 0.7; }
561
+ .gantt-block.missed { background: var(--grad-red); opacity: 0.75; }
562
  .gantt-deadline {
563
  position: absolute;
564
+ top: 2px; bottom: 2px;
565
  width: 2px;
566
  background: var(--accent-amber);
567
  border-radius: 1px;
568
+ box-shadow: 0 0 6px rgba(245,166,35,0.3);
569
  }
570
  .gantt-status {
571
+ width: 56px;
572
  text-align: right;
573
  flex-shrink: 0;
574
  }
575
  .badge {
576
+ display: inline-block;
577
  padding: 2px 8px;
578
+ border-radius: var(--radius-pill);
579
+ font-size: 0.62rem;
580
  font-weight: 600;
581
+ letter-spacing: 0.3px;
582
+ text-transform: uppercase;
583
  }
584
+ .badge.ok { background: rgba(74,222,128,0.12); color: var(--accent-green); }
585
+ .badge.pending { background: rgba(91,156,246,0.12); color: var(--accent-blue); }
586
+ .badge.missed { background: rgba(240,110,110,0.12); color: var(--accent-red); }
587
+ .badge.running { background: rgba(167,139,250,0.12); color: var(--accent-purple); }
588
 
589
+ .gantt-empty {
590
+ text-align: center;
591
+ padding: 2rem 0;
592
+ color: var(--text-muted);
593
+ font-size: 0.8rem;
594
  }
 
 
 
 
595
 
596
+ /* ══════════════════════════════════════════════════════════════════
597
+ REWARD ROWS
598
+ ══════════════════════════════════════════════════════════════════ */
599
+ .reward-row {
600
+ display: flex;
601
+ align-items: center;
602
+ gap: 10px;
603
+ padding: 4px 0;
604
  }
605
+ .reward-label {
606
+ width: 90px;
607
+ font-size: 0.7rem;
608
+ color: var(--text-tertiary);
609
+ flex-shrink: 0;
610
+ }
611
+ .reward-bar-track {
612
  flex: 1;
613
+ height: 6px;
614
+ background: var(--bg-inset);
615
+ border-radius: var(--radius-pill);
616
+ overflow: hidden;
617
  }
618
+ .reward-bar {
619
+ height: 100%;
620
+ border-radius: var(--radius-pill);
621
+ transition: width 0.5s var(--ease-out);
 
622
  }
623
+ .reward-val {
624
+ width: 52px;
625
+ text-align: right;
626
+ font-family: var(--font-mono);
627
+ font-size: 0.68rem;
628
+ font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
  }
 
630
 
631
+ /* ══════════════════════════════════════════════════════════════════
632
+ CONTROLS — Premium buttons
633
+ ══════════════════════════════════════════════════════════════════ */
634
+ .controls-grid {
635
+ display: flex;
636
+ gap: 10px;
637
+ align-items: center;
638
+ flex-wrap: wrap;
639
  }
640
+
641
  .btn {
642
+ padding: 9px 18px;
643
+ border-radius: var(--radius-md);
644
+ border: 1px solid var(--border-default);
645
+ background: var(--bg-surface);
646
+ color: var(--text-secondary);
647
+ font-size: 0.78rem;
648
  font-weight: 600;
649
+ font-family: var(--font-sans);
650
  cursor: pointer;
651
+ transition: all var(--duration-base) var(--ease-out);
652
+ display: inline-flex;
653
+ align-items: center;
654
+ gap: 6px;
655
+ white-space: nowrap;
656
  }
657
+ .btn svg { width: 14px; height: 14px; }
658
  .btn:hover {
659
+ background: var(--bg-card-hover);
660
+ border-color: var(--border-hover);
661
+ color: var(--text-primary);
662
  transform: translateY(-1px);
663
+ box-shadow: var(--shadow-sm);
664
  }
665
+ .btn:active { transform: translateY(0); }
666
+
667
  .btn.primary {
668
+ background: var(--grad-blue);
669
+ border: none;
670
+ color: white;
671
+ box-shadow: 0 2px 12px rgba(91,156,246,0.20);
672
  }
673
+ .btn.primary:hover {
674
+ box-shadow: 0 4px 20px rgba(91,156,246,0.30);
675
+ transform: translateY(-2px);
 
 
676
  }
677
 
678
+ .btn.success {
679
+ background: rgba(74,222,128,0.12);
680
+ border-color: rgba(74,222,128,0.20);
681
+ color: var(--accent-green);
682
+ }
683
+ .btn.success:hover {
684
+ background: rgba(74,222,128,0.18);
685
+ box-shadow: 0 2px 12px rgba(74,222,128,0.15);
686
+ }
687
+ .btn.success.active {
688
+ background: rgba(245,166,35,0.12);
689
+ border-color: rgba(245,166,35,0.20);
690
+ color: var(--accent-amber);
691
+ }
692
+
693
+ select.form-select {
694
+ padding: 9px 14px;
695
+ background: var(--bg-surface);
696
+ border: 1px solid var(--border-default);
697
+ border-radius: var(--radius-md);
698
+ color: var(--text-secondary);
699
+ font-size: 0.78rem;
700
+ font-weight: 500;
701
+ font-family: var(--font-sans);
702
  cursor: pointer;
703
+ transition: all var(--duration-base) var(--ease-out);
704
+ appearance: none;
705
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%235a6478' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E");
706
+ background-repeat: no-repeat;
707
+ background-position: right 10px center;
708
+ padding-right: 30px;
709
+ }
710
+ select.form-select:focus {
711
+ outline: none;
712
+ border-color: var(--accent-blue);
713
+ box-shadow: 0 0 0 3px rgba(91,156,246,0.10);
714
+ }
715
+ select.form-select:hover {
716
+ border-color: var(--border-hover);
717
+ background-color: var(--bg-card);
718
+ }
719
+
720
+ .grade-result {
721
+ font-family: var(--font-mono);
722
+ font-size: 0.82rem;
723
+ font-weight: 600;
724
+ padding: 5px 14px;
725
+ border-radius: var(--radius-pill);
726
+ background: rgba(74,222,128,0.08);
727
+ color: var(--accent-green);
728
+ display: none;
729
  }
730
+ .grade-result.show { display: inline-flex; }
731
 
732
+ /* ══════════════════════════════════════════════════════════════════
733
+ CONNECTION BANNER
734
+ ══════════════════════════════════════════════════════════════════ */
735
  #conn-banner {
736
  display: none;
737
  position: fixed;
738
+ top: 60px; left: 0; right: 0;
739
  z-index: 200;
740
+ background: rgba(240,110,110,0.08);
741
+ backdrop-filter: blur(12px);
742
+ border-bottom: 1px solid rgba(240,110,110,0.15);
743
  text-align: center;
744
  padding: 10px;
745
+ font-size: 0.8rem;
746
+ font-weight: 500;
747
  color: var(--accent-red);
748
  }
749
  #conn-banner.show { display: block; }
750
 
751
+ /* ══════════════════════════════════════════════════════════════════
752
+ FOOTER
753
+ ══════════════════════════════════════════════════════════════════ */
754
  footer {
755
+ position: relative;
756
+ z-index: 1;
757
  text-align: center;
758
  padding: 1.5rem;
759
+ color: var(--text-muted);
760
+ font-size: 0.7rem;
761
+ border-top: 1px solid var(--border-subtle);
762
+ background: var(--bg-elevated);
763
+ }
764
+ footer a {
765
+ color: var(--text-tertiary);
766
+ text-decoration: none;
767
+ transition: color var(--duration-fast);
768
+ }
769
+ footer a:hover { color: var(--accent-blue); }
770
+
771
+ /* ══════════════════════════════════════════════════════════════════
772
+ ANIMATIONS
773
+ ══════════════════════════════════════════════════════════════════ */
774
+ @keyframes fadeInUp {
775
+ from { opacity: 0; transform: translateY(12px); }
776
+ to { opacity: 1; transform: translateY(0); }
777
+ }
778
+ .card {
779
+ animation: fadeInUp 0.5s var(--ease-out) both;
780
+ }
781
+ .card:nth-child(1) { animation-delay: 0.05s; }
782
+ .card:nth-child(2) { animation-delay: 0.10s; }
783
+ .card:nth-child(3) { animation-delay: 0.15s; }
784
+ .card:nth-child(4) { animation-delay: 0.20s; }
785
+ .card:nth-child(5) { animation-delay: 0.25s; }
786
+ .card:nth-child(6) { animation-delay: 0.30s; }
787
+ .card:nth-child(7) { animation-delay: 0.35s; }
788
+ .card:nth-child(8) { animation-delay: 0.40s; }
789
+ .card:nth-child(9) { animation-delay: 0.45s; }
790
+
791
+ /* Scrollbar styling */
792
+ ::-webkit-scrollbar { width: 6px; }
793
+ ::-webkit-scrollbar-track { background: transparent; }
794
+ ::-webkit-scrollbar-thumb {
795
+ background: rgba(255,255,255,0.08);
796
+ border-radius: 3px;
797
+ }
798
+ ::-webkit-scrollbar-thumb:hover {
799
+ background: rgba(255,255,255,0.12);
800
  }
801
  </style>
802
  </head>
803
  <body>
804
 
805
+ <div class="bg-pattern"></div>
806
+
807
+ <!-- Connection banner -->
808
  <div id="conn-banner">
809
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: -2px; margin-right: 6px;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
810
+ Environment server unreachable — retrying connection...
811
  </div>
812
 
813
+ <!-- ═══ HEADER ═══ -->
814
  <header>
815
  <div class="logo">
816
+ <div class="logo-icon">
817
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
818
+ </div>
819
  <div class="logo-text">Grid<span>Mind</span>-RL</div>
820
+ <div class="logo-tag">v1.0</div>
821
  </div>
822
+ <div class="header-right">
823
  <span id="task-badge" class="task-badge">Task 1 — Cost Minimization</span>
824
+ <div class="status-indicator">
825
  <div class="status-dot" id="status-dot"></div>
826
  <span class="status-label" id="status-label">Live</span>
827
  </div>
828
+ <span class="header-meta" id="ep-step">ep:— step:—</span>
829
  </div>
830
  </header>
831
 
832
+ <!-- ═══ KPI STRIP ═══ -->
833
+ <div class="kpi-strip">
834
  <div class="kpi">
835
+ <div class="kpi-label">Current Price</div>
836
+ <div class="kpi-value" id="kpi-price">—</div>
837
+ <div class="kpi-unit">$/kWh</div>
838
  </div>
839
  <div class="kpi">
840
+ <div class="kpi-label">Indoor Temp</div>
841
+ <div class="kpi-value" id="kpi-temp">—</div>
842
+ <div class="kpi-unit">°C · target 21°C</div>
843
  </div>
844
  <div class="kpi">
845
+ <div class="kpi-label">Grid Stress</div>
846
+ <div class="kpi-value" id="kpi-stress">—</div>
847
+ <div class="kpi-unit">0 normal · 1 critical</div>
848
  </div>
849
  <div class="kpi">
850
+ <div class="kpi-label">Cumulative Cost</div>
851
+ <div class="kpi-value" id="kpi-cost">—</div>
852
+ <div class="kpi-unit">vs baseline: <span id="kpi-baseline">—</span></div>
853
  </div>
854
  <div class="kpi">
855
+ <div class="kpi-label">Carbon Intensity</div>
856
+ <div class="kpi-value" id="kpi-carbon">—</div>
857
+ <div class="kpi-unit">gCO₂/kWh</div>
858
  </div>
859
  <div class="kpi">
860
+ <div class="kpi-label">Process Demand</div>
861
+ <div class="kpi-value" id="kpi-demand">—</div>
862
+ <div class="kpi-unit">kW</div>
863
  </div>
864
  <div class="kpi">
865
+ <div class="kpi-label">Thermal Storage</div>
866
+ <div class="kpi-value" id="kpi-storage">—</div>
867
+ <div class="kpi-unit">% capacity</div>
868
  </div>
869
  </div>
870
 
871
+ <!-- ═══ MAIN CONTENT ═══ -->
872
  <main>
873
 
874
+ <!-- Row 1: Price Curve + Grid Stress -->
875
  <div class="card col-8">
876
+ <div class="card-header">
877
+ <div class="card-title">
878
+ <div class="icon-wrap amber"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></div>
879
+ Electricity Price Curve
880
+ </div>
881
+ <span class="card-badge">24H</span>
882
+ </div>
883
  <div class="chart-wrap">
884
  <canvas id="chart-price"></canvas>
885
  </div>
886
  </div>
887
 
888
  <div class="card col-4" id="card-stress">
889
+ <div class="card-header">
890
+ <div class="card-title">
891
+ <div class="icon-wrap red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
892
+ Grid Stress Signal
 
 
 
893
  </div>
894
+ <span class="card-badge">REAL-TIME</span>
895
+ </div>
896
+ <div class="stress-display">
897
+ <div class="stress-value" id="stress-big">0.000</div>
898
+ <div class="stress-sub">Demand-response urgency index</div>
899
+ </div>
900
+ <div class="stress-meter" id="stress-meter"></div>
901
+ <div class="chart-wrap short">
902
+ <canvas id="chart-stress"></canvas>
903
  </div>
904
  </div>
905
 
906
  <!-- Row 2: Temperature + Storage + HVAC -->
907
  <div class="card col-6">
908
+ <div class="card-header">
909
+ <div class="card-title">
910
+ <div class="icon-wrap cyan"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"/></svg></div>
911
+ Temperature Timeline
912
+ </div>
913
+ <span class="card-badge">INDOOR</span>
914
+ </div>
915
  <div class="chart-wrap tall">
916
  <canvas id="chart-temp"></canvas>
917
  </div>
918
  </div>
919
 
920
  <div class="card col-3">
921
+ <div class="card-header">
922
+ <div class="card-title">
923
+ <div class="icon-wrap teal"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="6" width="18" height="12" rx="2" ry="2"/><line x1="23" y1="13" x2="23" y2="11"/></svg></div>
924
+ Thermal Storage
 
 
 
 
925
  </div>
926
  </div>
927
+ <div class="storage-display">
928
+ <div class="storage-value"><span id="storage-pct">—</span><span>%</span></div>
929
+ </div>
930
+ <div class="storage-bar-track">
931
+ <div class="storage-bar-fill" id="storage-fill" style="width: 0%"></div>
932
+ </div>
933
+ <div class="chart-wrap short">
934
+ <canvas id="chart-storage"></canvas>
935
+ </div>
936
  </div>
937
 
938
  <div class="card col-3">
939
+ <div class="card-header">
940
+ <div class="card-title">
941
+ <div class="icon-wrap blue"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div>
942
+ HVAC + Load Shed
943
+ </div>
944
+ </div>
945
  <div class="chart-wrap tall">
946
  <canvas id="chart-hvac"></canvas>
947
  </div>
948
  </div>
949
 
950
+ <!-- Row 3: Cost vs Baseline + Reward Breakdown -->
951
  <div class="card col-8">
952
+ <div class="card-header">
953
+ <div class="card-title">
954
+ <div class="icon-wrap green"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>
955
+ Cumulative Cost vs Baseline
956
+ </div>
957
+ <span class="card-badge">COMPARISON</span>
958
+ </div>
959
  <div class="chart-wrap tall">
960
  <canvas id="chart-cost"></canvas>
961
  </div>
962
  </div>
963
 
964
  <div class="card col-4">
965
+ <div class="card-header">
966
+ <div class="card-title">
967
+ <div class="icon-wrap purple"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/></svg></div>
968
+ Reward Breakdown
969
+ </div>
970
+ <span class="card-badge">STEP</span>
971
+ </div>
972
+ <div id="reward-rows" style="margin-top: 0.25rem"></div>
973
+ <div style="margin-top: 0.75rem">
974
  <div class="chart-wrap short">
975
  <canvas id="chart-reward"></canvas>
976
  </div>
 
979
 
980
  <!-- Row 4: Batch Gantt + Carbon -->
981
  <div class="card col-6">
982
+ <div class="card-header">
983
+ <div class="card-title">
984
+ <div class="icon-wrap orange"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></div>
985
+ Batch Job Timeline
986
+ </div>
987
+ <span class="card-badge">SCHEDULER</span>
988
+ </div>
989
+ <div class="gantt-container" id="gantt-wrap">
990
+ <div class="gantt-empty">No batch jobs queued</div>
991
  </div>
992
  </div>
993
 
994
  <div class="card col-6">
995
+ <div class="card-header">
996
+ <div class="card-title">
997
+ <div class="icon-wrap green"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></div>
998
+ Carbon Intensity Curve
999
+ </div>
1000
+ <span class="card-badge">24H</span>
1001
+ </div>
1002
  <div class="chart-wrap">
1003
  <canvas id="chart-carbon"></canvas>
1004
  </div>
 
1006
 
1007
  <!-- Row 5: Controls -->
1008
  <div class="card col-12">
1009
+ <div class="card-header">
1010
+ <div class="card-title">
1011
+ <div class="icon-wrap purple"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg></div>
1012
+ Episode Controls
1013
+ </div>
1014
+ </div>
1015
+ <div class="controls-grid">
1016
+ <select id="task-select" class="form-select" onchange="onTaskChange()">
1017
  <option value="1">Task 1 — Cost Minimization (Easy)</option>
1018
  <option value="2">Task 2 — Temperature Management (Medium)</option>
1019
  <option value="3">Task 3 — Full Demand Response (Hard)</option>
1020
  </select>
1021
+ <select id="building-select" class="form-select" onchange="onBuildingChange()">
1022
  <option value="0">Building 1 (Primary)</option>
1023
  <option value="1">Building 2</option>
1024
  <option value="2">Building 3</option>
1025
  </select>
1026
+ <button id="btn-reset" class="btn primary" onclick="doReset()">
1027
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
1028
+ New Episode
1029
+ </button>
1030
+ <button id="btn-live" class="btn success" onclick="toggleLiveSim()">
1031
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
1032
+ Start Live Simulation
1033
+ </button>
1034
+ <button class="btn" onclick="doGrade()">
1035
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
1036
+ Grade Episode
1037
+ </button>
1038
+ <button class="btn" onclick="window.open('/api/replay')">
1039
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1040
+ Export Replay
1041
+ </button>
1042
+ <span id="grade-result" class="grade-result"></span>
1043
  </div>
1044
  </div>
1045
 
1046
  </main>
1047
 
1048
  <footer>
1049
+ GridMind-RL &nbsp;·&nbsp; OpenEnv-compliant RL environment for industrial demand response &nbsp;·&nbsp;
1050
+ <a href="/api/health" target="_blank">API Health</a> &nbsp;·&nbsp;
1051
+ <a href="/api/metrics" target="_blank">Metrics</a>
1052
  </footer>
1053
 
1054
  <script src="/static/dashboard.js"></script>
env/rewards.go CHANGED
@@ -31,32 +31,46 @@ func ComputeReward(inp ComputeRewardInput) RewardComponents {
31
  rc := RewardComponents{}
32
 
33
  // ── 1. Cost Savings ─────────────────────────────────────────────────────
34
- // Shift from pure penalty to a positive baseline: standardizing operations gives positive reward.
35
- // Baseline reward of 1.5, minus the relative cost.
36
  typicalCost := 4.0
37
- rc.CostSavings = 1.5 - (inp.StepCost / typicalCost) * 2.0
38
 
39
  // ── 2. Temperature Constraint ────────────────────────────────────────────
40
- // Only active for task 2 and 3.
41
  if inp.TaskID >= 2 {
42
  temp := inp.B.IndoorTemperature
43
  rc.TempConstraint = computeTempReward(temp, inp.B.SetpointTemperature, inp.TMin, inp.TMax)
44
  }
45
 
46
  // ── 3. Grid Stress Response ──────────────────────────────────────────────
47
- // Only active for task 3.
48
  if inp.TaskID >= 3 {
49
  rc.GridResponse = computeGridResponse(inp.GridStress, inp.ShedFraction)
50
  }
51
 
52
- // ── 4. Deadline Penalty ──────────────────────────────────────────────────
53
- // Task 1 is cost-only; batch jobs are not part of the objective.
54
- if inp.BatchMissed > 0 && inp.TaskID >= 2 {
55
- rc.DeadlinePenalty = -float64(inp.BatchMissed) * 1.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
 
58
- // ── 5. Efficiency Bonus (thermal storage arbitrage) ───────────────────────
59
- // Reward for charging storage during cheap periods and discharging during expensive ones.
60
  if len(inp.PriceCurve) > inp.CurrentStep {
61
  rc.EfficiencyBonus = computeArbitrageBonus(
62
  inp.ChargeRate,
@@ -65,23 +79,36 @@ func ComputeReward(inp ComputeRewardInput) RewardComponents {
65
  inp.CurrentStep,
66
  )
67
  }
 
 
 
 
 
 
 
68
 
69
- // ── 6. Stability Penalty ─────────────────────────────────────────────────
70
- // Penalise rapid oscillation in HVAC setpoint and thermal charge rate.
71
  hvacDelta := math.Abs(inp.Act.HVACPowerLevel - inp.PrevHVACLevel)
72
  chargeDelta := math.Abs(inp.ChargeRate - inp.PrevChargeRate)
73
  oscillation := hvacDelta*0.5 + chargeDelta*0.3
74
  if oscillation > 0.3 {
75
  rc.StabilityPenalty = -(oscillation - 0.3) * 0.8
 
 
 
76
  }
77
 
78
  // ── 7. Carbon Reward ─────────────────────────────────────────────────────
79
- // Low-carbon bonus: active for task 3.
80
  if inp.TaskID >= 3 {
81
- // Normalise carbon: iso-ne range roughly 100–700 gCO2/kWh
82
- carbonNorm := (inp.B.CarbonIntensity - 100.0) / 600.0
83
- // Provide a baseline positive score, reduced by carbon footprint
84
- rc.CarbonReward = 0.5 - (inp.EnergyKWh * carbonNorm * 0.3)
 
 
 
85
  }
86
 
87
  // ── Aggregate ────────────────────────────────────────────────────────────
@@ -105,15 +132,29 @@ func computeTempReward(temp, setpoint, tMin, tMax float64) float64 {
105
  return -excess * 0.6
106
  }
107
 
108
- // computeGridResponse returns a bonus for shedding load during high grid stress,
109
- // and a mild penalty for shedding when the grid is fine.
110
  func computeGridResponse(stress, shedFraction float64) float64 {
111
  if stress > 0.7 {
112
- // Bonus proportional to shed fraction
113
- return shedFraction * stress * 1.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
- // Mild penalty for unnecessary shedding (reduces productivity without benefit)
116
- return -shedFraction * (0.7 - stress) * 0.3
117
  }
118
 
119
  // computeArbitrageBonus rewards storage use when current price is low vs recent history
 
31
  rc := RewardComponents{}
32
 
33
  // ── 1. Cost Savings ─────────────────────────────────────────────────────
34
+ // Positive baseline minus relative cost: smart agents save money.
 
35
  typicalCost := 4.0
36
+ rc.CostSavings = 1.5 - (inp.StepCost/typicalCost)*2.0
37
 
38
  // ── 2. Temperature Constraint ────────────────────────────────────────────
39
+ // Active for task 2 and 3. Gaussian bonus for being near setpoint.
40
  if inp.TaskID >= 2 {
41
  temp := inp.B.IndoorTemperature
42
  rc.TempConstraint = computeTempReward(temp, inp.B.SetpointTemperature, inp.TMin, inp.TMax)
43
  }
44
 
45
  // ── 3. Grid Stress Response ──────────────────────────────────────────────
46
+ // Active for task 3. Rewards proactive grid awareness, not just reactive shedding.
47
  if inp.TaskID >= 3 {
48
  rc.GridResponse = computeGridResponse(inp.GridStress, inp.ShedFraction)
49
  }
50
 
51
+ // ── 4. Deadline Penalty / Bonus ──────────────────────────────────────────
52
+ // Task 2+: penalise missed jobs, reward on-track pending jobs.
53
+ if inp.TaskID >= 2 {
54
+ if inp.BatchMissed > 0 {
55
+ rc.DeadlinePenalty = -float64(inp.BatchMissed) * 1.5
56
+ }
57
+ // Positive signal: reward for jobs still on track (not missed yet)
58
+ onTrackJobs := 0
59
+ for _, job := range inp.B.Jobs {
60
+ if !job.Completed && !job.MissedDeadline {
61
+ onTrackJobs++
62
+ }
63
+ if job.Completed && !job.MissedDeadline {
64
+ onTrackJobs++ // completed on time is even better
65
+ }
66
+ }
67
+ if onTrackJobs > 0 && inp.BatchMissed == 0 {
68
+ rc.DeadlinePenalty += float64(onTrackJobs) * 0.08
69
+ }
70
  }
71
 
72
+ // ── 5. Efficiency Bonus (thermal storage utilization) ─────────────────────
73
+ // Rewards smart storage use: arbitrage + maintaining useful storage levels.
74
  if len(inp.PriceCurve) > inp.CurrentStep {
75
  rc.EfficiencyBonus = computeArbitrageBonus(
76
  inp.ChargeRate,
 
79
  inp.CurrentStep,
80
  )
81
  }
82
+ // Baseline: reward maintaining a balanced storage level (not empty, not always full)
83
+ storageLevel := inp.B.ThermalStorageLevel
84
+ if storageLevel > 0.2 && storageLevel < 0.85 {
85
+ rc.EfficiencyBonus += 0.15 // good operating range
86
+ } else if storageLevel <= 0.05 || storageLevel >= 0.98 {
87
+ rc.EfficiencyBonus -= 0.1 // extremes are wasteful
88
+ }
89
 
90
+ // ── 6. Stability Reward/Penalty ──────────────────────────────────────────
91
+ // Smooth operation earns a bonus; rapid oscillation earns a penalty.
92
  hvacDelta := math.Abs(inp.Act.HVACPowerLevel - inp.PrevHVACLevel)
93
  chargeDelta := math.Abs(inp.ChargeRate - inp.PrevChargeRate)
94
  oscillation := hvacDelta*0.5 + chargeDelta*0.3
95
  if oscillation > 0.3 {
96
  rc.StabilityPenalty = -(oscillation - 0.3) * 0.8
97
+ } else {
98
+ // Positive reward for smooth, stable control
99
+ rc.StabilityPenalty = (0.3 - oscillation) * 0.4
100
  }
101
 
102
  // ── 7. Carbon Reward ─────────────────────────────────────────────────────
103
+ // Active for task 3. Rewards low-carbon operation.
104
  if inp.TaskID >= 3 {
105
+ carbonNorm := math.Max(0, (inp.B.CarbonIntensity-100.0)/600.0)
106
+ // Baseline bonus, reduced by carbon-heavy consumption
107
+ rc.CarbonReward = 0.6 - (inp.EnergyKWh * carbonNorm * 0.25)
108
+ // Extra bonus for operating during genuinely clean grid periods
109
+ if carbonNorm < 0.3 {
110
+ rc.CarbonReward += 0.15
111
+ }
112
  }
113
 
114
  // ── Aggregate ────────────────────────────────────────────────────────────
 
132
  return -excess * 0.6
133
  }
134
 
135
+ // computeGridResponse returns a reward for grid-aware behavior:
136
+ // bonus for shedding during stress, baseline for readiness, penalty for waste.
137
  func computeGridResponse(stress, shedFraction float64) float64 {
138
  if stress > 0.7 {
139
+ // High stress: large bonus proportional to shed fraction
140
+ if shedFraction > 0.1 {
141
+ return shedFraction * stress * 1.5
142
+ }
143
+ // High stress but not shedding: penalty
144
+ return -0.2 * stress
145
+ }
146
+ if stress > 0.3 {
147
+ // Moderate stress: small bonus for readiness, small bonus for proactive shedding
148
+ if shedFraction > 0.05 {
149
+ return shedFraction * 0.5 // proactive shedding during moderate stress
150
+ }
151
+ return 0.08 // grid-aware readiness bonus
152
+ }
153
+ // Low stress: mild penalty for unnecessary shedding, baseline for normal operation
154
+ if shedFraction > 0.1 {
155
+ return -shedFraction * 0.3
156
  }
157
+ return 0.1 // small positive signal for operating normally under low stress
 
158
  }
159
 
160
  // computeArbitrageBonus rewards storage use when current price is low vs recent history