ymlin105 commited on
Commit
fc9ccdf
·
1 Parent(s): 69975d9

feat: Fix €0 prediction issue and overhaul dashboard to match premium design

Browse files
Files changed (2) hide show
  1. src/app.py +8 -1
  2. src/frontend.py +264 -543
src/app.py CHANGED
@@ -143,9 +143,16 @@ def predict(request: PredictionRequest):
143
  X = pd.DataFrame()
144
  for c in feature_cols:
145
  if c in processed_df.columns:
146
- X[c] = processed_df[c]
 
 
 
 
147
  else:
148
  X[c] = 0
 
 
 
149
 
150
  # 6. Predict & Explain
151
  # Standard Prediction
 
143
  X = pd.DataFrame()
144
  for c in feature_cols:
145
  if c in processed_df.columns:
146
+ val = processed_df[c]
147
+ # Robustness: Cap Year to training range (2013-2015)
148
+ if c == 'Year':
149
+ val = val.clip(upper=2015)
150
+ X[c] = val
151
  else:
152
  X[c] = 0
153
+
154
+ # Ensure numeric types
155
+ X = X.apply(pd.to_numeric, errors='coerce').fillna(0)
156
 
157
  # 6. Predict & Explain
158
  # Standard Prediction
src/frontend.py CHANGED
@@ -3,675 +3,396 @@ Frontend HTML template for the Rossmann Store Sales Predictor.
3
  A clean, modern professional dashboard for making predictions.
4
  """
5
 
6
- FRONTEND_HTML = """
7
  <!DOCTYPE html>
8
  <html lang="en">
9
  <head>
10
  <meta charset="UTF-8">
11
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
- <title>Rossmann Sales Intelligence</title>
13
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
14
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15
  <style>
16
  :root {
17
- /* Professional SaaS Theme */
18
- --primary: #2563eb; /* Royal Blue */
19
- --primary-dark: #1e40af;
20
- --secondary: #64748b; /* Slate Grey */
21
- --bg-page: #f8fafc; /* Very light blue-grey */
22
- --bg-card: #ffffff;
23
- --text-main: #0f172a; /* Dark Slate */
24
- --text-muted: #64748b;
25
  --border: #e2e8f0;
26
  --success: #10b981;
27
- --accent-gradient: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%);
 
28
  }
29
 
30
- * {
31
- margin: 0;
32
- padding: 0;
33
- box-sizing: border-box;
34
  font-family: 'Inter', sans-serif;
35
- }
36
-
37
- body {
38
- background-color: var(--bg-page);
39
- color: var(--text-main);
40
  min-height: 100vh;
41
  display: flex;
42
  flex-direction: column;
43
  }
44
 
45
- /* Navbar */
 
 
46
  .navbar {
47
- background: var(--bg-card);
48
- border-bottom: 1px solid var(--border);
49
- padding: 1rem 2rem;
50
  display: flex;
51
  justify-content: space-between;
52
  align-items: center;
 
 
 
 
53
  }
54
-
55
- .brand {
56
- font-weight: 700;
57
- font-size: 1.25rem;
58
- color: var(--text-main);
59
- display: flex;
60
- align-items: center;
61
- gap: 0.5rem;
62
- }
63
-
64
- .brand span { color: var(--primary); }
65
-
66
- .status-badge {
67
- font-size: 0.85rem;
68
- padding: 0.25rem 0.75rem;
69
- border-radius: 9999px;
70
- background: #e2e8f0;
71
- color: var(--secondary);
72
- font-weight: 500;
73
- display: flex;
74
- align-items: center;
75
- gap: 0.4rem;
76
- }
77
 
78
- .status-dot {
79
- width: 8px;
80
- height: 8px;
81
- border-radius: 50%;
82
- background-color: #94a3b8;
83
  }
 
 
84
 
85
- /* Main Layout */
86
- .container {
87
- flex: 1;
88
- max-width: 1200px;
 
89
  margin: 0 auto;
90
- padding: 2rem;
91
  width: 100%;
92
- display: grid;
93
- grid-template-columns: 350px 1fr;
94
  gap: 2rem;
95
  }
96
 
97
- @media (max-width: 900px) {
98
- .container {
99
- grid-template-columns: 1fr;
100
- }
101
- }
102
 
103
- /* Cards */
104
- .card {
105
- background: var(--bg-card);
106
- border: 1px solid var(--border);
107
- border-radius: 12px;
108
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
109
- padding: 1.5rem;
110
- height: 100%;
111
- }
112
-
113
- .card-header {
114
- margin-bottom: 1.5rem;
115
- }
116
-
117
- .card-title {
118
- font-size: 1.1rem;
119
- font-weight: 600;
120
- color: var(--text-main);
121
- margin-bottom: 0.25rem;
122
  }
 
123
 
124
- .card-subtitle {
125
- font-size: 0.875rem;
126
- color: var(--text-muted);
127
- }
128
-
129
- /* Form Styling */
130
- .form-grid {
131
- display: grid;
132
- gap: 1.25rem;
133
- }
134
-
135
- .form-group {
136
- display: flex;
137
- flex-direction: column;
138
- gap: 0.4rem;
139
- }
140
-
141
- .form-group label {
142
- font-size: 0.85rem;
143
- font-weight: 500;
144
- color: var(--text-main);
145
- }
146
-
147
- .form-control {
148
- padding: 0.6rem 0.8rem;
149
- border: 1px solid var(--border);
150
- border-radius: 6px;
151
- font-size: 0.95rem;
152
- color: var(--text-main);
153
- background: #fff;
154
- transition: border-color 0.15s, box-shadow 0.15s;
155
- }
156
-
157
- .form-control:focus {
158
- outline: none;
159
- border-color: var(--primary);
160
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
161
- }
162
-
163
- /* Button */
164
- .btn-primary {
165
- background: var(--accent-gradient);
166
- color: white;
167
- border: none;
168
- padding: 0.75rem;
169
- border-radius: 8px;
170
- font-weight: 500;
171
- font-size: 0.95rem;
172
- cursor: pointer;
173
- transition: opacity 0.2s, transform 0.1s;
174
  margin-top: 1rem;
175
- width: 100%;
176
- box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2);
177
  }
 
 
178
 
179
- .btn-primary:hover {
180
- opacity: 0.95;
181
- }
182
-
183
- .btn-primary:active {
184
- transform: translateY(1px);
185
- }
186
-
187
- .btn-primary:disabled {
188
- background: var(--secondary);
189
- cursor: not-allowed;
190
- transform: none;
191
- }
192
 
193
- /* Results Area */
194
- .results-container {
195
- display: flex;
196
- flex-direction: column;
197
- gap: 1.5rem;
198
- height: 100%;
199
- }
200
-
201
- .metric-card {
202
- background: linear-gradient(to right, #ffffff, #f8fafc);
203
- border: 1px solid var(--border);
204
- border-radius: 12px;
205
- padding: 2rem;
206
- text-align: center;
207
- position: relative;
208
  overflow: hidden;
209
  }
210
-
211
- .metric-card::before {
212
- content: '';
213
- position: absolute;
214
- top: 0;
215
- left: 0;
216
- height: 100%;
217
- width: 4px;
218
- background: var(--primary);
219
- }
220
-
221
- .metric-label {
222
- font-size: 0.875rem;
223
- text-transform: uppercase;
224
- letter-spacing: 0.05em;
225
- color: var(--text-muted);
226
- font-weight: 600;
227
- margin-bottom: 0.5rem;
228
- }
229
-
230
- .metric-value {
231
- font-size: 3rem;
232
- font-weight: 700;
233
- color: var(--text-main);
234
- letter-spacing: -1px;
235
- line-height: 1;
236
- }
237
-
238
- .ci-range {
239
- font-size: 0.95rem;
240
- color: var(--text-muted);
241
- margin-top: 0.5rem;
242
- font-weight: 500;
243
- }
244
-
245
- /* Explanation Waterfall */
246
- .explanation-grid {
247
- display: grid;
248
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
249
- gap: 1.5rem;
250
- }
251
-
252
- .explanation-item {
253
- display: flex;
254
- justify-content: space-between;
255
- align-items: center;
256
- padding: 0.75rem 0;
257
- border-bottom: 1px solid #f1f5f9;
258
- }
259
-
260
- .explanation-item:last-child {
261
- border-bottom: none;
262
- }
263
-
264
- .impact-tag {
265
- font-size: 0.8rem;
266
- font-weight: 700;
267
- padding: 0.2rem 0.6rem;
268
- border-radius: 4px;
269
- }
270
-
271
- .impact-pos { background: #dcfce7; color: #166534; }
272
- .impact-neg { background: #fee2e2; color: #991b1b; }
273
-
274
- .feature-name {
275
- font-size: 0.9rem;
276
- font-weight: 500;
277
- color: var(--text-main);
278
  }
 
279
 
280
- .chart-wrapper {
281
- flex: 1;
282
- min-height: 400px;
283
- background: #fff;
284
- border-radius: 12px;
285
- position: relative;
286
  }
 
 
 
 
287
 
288
- /* Empty State */
289
- .empty-state {
290
- height: 100%;
291
- display: flex;
292
- flex-direction: column;
293
- align-items: center;
294
- justify-content: center;
295
- color: var(--text-muted);
296
- text-align: center;
297
- padding: 2rem;
298
- border: 2px dashed var(--border);
299
- border-radius: 12px;
300
- background: #fff;
301
- }
302
-
303
- .empty-icon {
304
- font-size: 3rem;
305
- margin-bottom: 1rem;
306
- opacity: 0.5;
307
  }
 
308
 
309
- /* Footer */
310
- .footer {
311
- text-align: center;
312
- padding: 2rem;
313
- color: var(--text-muted);
314
- font-size: 0.85rem;
315
- border-top: 1px solid var(--border);
316
- background: #fff;
317
- margin-top: auto;
318
- }
319
-
320
- .footer a {
321
- color: var(--primary);
322
- text-decoration: none;
323
- }
324
 
325
- .footer a:hover { text-decoration: underline; }
326
-
327
- /* Smooth reveal */
328
- .reveal {
329
- animation: fadeIn 0.4s ease-out;
330
- }
331
-
332
- @keyframes fadeIn {
333
- from { opacity: 0; transform: translateY(10px); }
334
- to { opacity: 1; transform: translateY(0); }
335
  }
336
  </style>
337
  </head>
338
  <body>
339
  <nav class="navbar">
340
- <div class="brand">
341
- <span>🔹</span> Rossmann Sales Intelligence
342
- </div>
343
- <div class="status-badge" id="mode-badge">
344
- <div class="status-dot" id="status-dot"></div>
345
- <span id="status-text">Connecting...</span>
346
  </div>
347
  </nav>
348
 
349
- <div class="container">
350
- <!-- Sidebar / Configuration -->
351
  <aside class="sidebar">
352
- <div class="card">
353
- <div class="card-header">
354
- <h2 class="card-title">Prediction Parameters</h2>
355
- <p class="card-subtitle">Configure store & scenario details.</p>
356
- </div>
357
-
358
- <form id="predict-form">
359
- <div class="form-grid">
360
- <div class="form-group">
361
- <label for="store">Store ID</label>
362
- <input type="number" id="store" class="form-control"
363
- value="1" min="1" max="1115" required placeholder="e.g. 1">
364
- </div>
365
-
366
- <div class="form-group">
367
- <label for="date">Start Date</label>
368
- <input type="date" id="date" class="form-control" required>
369
- </div>
370
-
371
- <div class="form-group">
372
- <label for="horizon">Forecast Horizon</label>
373
- <select id="horizon" class="form-control">
374
- <option value="1">1 Day</option>
375
- <option value="7" selected>7 Days</option>
376
- <option value="14">14 Days</option>
377
- <option value="30">30 Days</option>
378
- </select>
379
- </div>
380
-
381
- <div class="form-group">
382
- <label for="promo">Promotion Status</label>
383
- <select id="promo" class="form-control">
384
- <option value="1" selected>Active Promo</option>
385
- <option value="0">No Promo</option>
386
- </select>
387
- </div>
388
-
389
- <!-- Advanced grouped in details for simplicity? No, keep expanding -->
390
- <div class="form-group">
391
- <label for="state_holiday">State Holiday</label>
392
- <select id="state_holiday" class="form-control">
393
- <option value="0">None</option>
394
- <option value="a">Public Holiday</option>
395
- <option value="b">Easter Holiday</option>
396
- <option value="c">Christmas</option>
397
- </select>
398
- </div>
399
-
400
- <div class="form-group">
401
- <label for="school_holiday">School Holiday</label>
402
- <select id="school_holiday" class="form-control">
403
- <option value="0">No</option>
404
- <option value="1">Yes</option>
405
- </select>
406
- </div>
407
-
408
- <div class="form-group">
409
- <label for="store_type">Store Type</label>
410
- <select id="store_type" class="form-control">
411
- <option value="a">Type A</option>
412
- <option value="b">Type B</option>
413
- <option value="c">Type C</option>
414
- <option value="d">Type D</option>
415
- </select>
416
- </div>
417
-
418
- <div class="form-group">
419
- <label for="assortment">Assortment</label>
420
- <select id="assortment" class="form-control">
421
- <option value="a">Basic</option>
422
- <option value="b">Extra</option>
423
- <option value="c">Extended</option>
424
- </select>
425
- </div>
426
-
427
- <div class="form-group">
428
- <label for="competition_distance">Competition Dist. (m)</label>
429
- <input type="number" id="competition_distance" class="form-control" value="1270">
430
- </div>
431
- </div>
432
-
433
- <button type="submit" class="btn-primary" id="submit-btn">
434
- Result Generation
435
- </button>
436
  </form>
437
  </div>
438
  </aside>
439
 
440
- <!-- Main Content / Results -->
441
- <main class="results-area">
442
- <div id="result-placeholder" class="empty-state">
443
- <div class="empty-icon">📊</div>
444
- <h3>Ready to Forecast</h3>
445
- <p>Enter parameters on the left to generate a sales prediction.</p>
446
  </div>
447
 
448
- <div id="results-content" class="results-container" style="display: none;">
449
- <div class="explanation-grid">
450
- <!-- Key Metric -->
451
- <div class="metric-card reveal">
452
- <div class="metric-label">Projected Sales (95% CI)</div>
453
- <div class="metric-value" id="sales_val">--</div>
454
- <div class="ci-range" id="ci_val">Range: --</div>
455
- <div style="font-size: 0.9rem; color: var(--success); margin-top: 1rem;" id="trend-indicator"></div>
456
  </div>
457
 
458
- <!-- Top Drivers Card -->
459
- <div class="card reveal">
460
- <div class="card-header">
461
- <h3 class="card-title">Model Decision Insights</h3>
462
- <p class="card-subtitle">Top drivers influencing this specific forecast.</p>
463
- </div>
464
- <div id="explanation-list">
465
- <!-- Populated by JS -->
466
  </div>
467
  </div>
468
  </div>
469
 
470
- <!-- Chart -->
471
- <div class="card reveal" style="flex: 1; display: flex; flex-direction: column;">
472
- <div class="card-header">
473
- <h3 class="card-title">Forecast Trend</h3>
474
- <p class="card-subtitle">Expected turnover over the selected horizon.</p>
475
- </div>
476
- <div class="chart-wrapper">
477
- <canvas id="salesChart"></canvas>
478
  </div>
479
  </div>
480
  </div>
481
  </main>
482
  </div>
483
 
484
- <footer class="footer">
485
- <p>© 2026 Rossmann Intelligence System. Powered by XGBoost, FastAPI & Docker.</p>
486
- <div style="margin-top: 0.5rem;">
487
- <a href="/docs">API Docs</a> • <a href="https://github.com/sylvia-ymlin/Rossmann-Store-Sales">GitHub</a>
488
- </div>
489
- </footer>
490
-
491
  <script>
492
- // Set default date to today
493
  document.getElementById('date').valueAsDate = new Date();
494
- let chartInstance = null;
495
-
496
- // Health Check
497
- fetch('/health')
498
- .then(res => res.json())
499
- .then(data => {
500
- const badge = document.getElementById('mode-badge');
501
- const dot = document.getElementById('status-dot');
502
- const text = document.getElementById('status-text');
503
-
504
- if (data.status === 'healthy') {
505
- badge.style.background = '#dcfce7'; /* Green-100 */
506
- badge.style.color = '#166534'; /* Green-800 */
507
- dot.style.backgroundColor = '#166534';
508
- text.textContent = 'System Online';
509
- } else {
510
- text.textContent = 'System Issues';
511
- dot.style.backgroundColor = '#dc2626';
512
- }
513
- })
514
- .catch(() => {
515
- document.getElementById('status-text').textContent = 'Offline';
516
- });
517
 
518
- // Form Logic
519
- document.getElementById('predict-form').addEventListener('submit', async (e) => {
520
  e.preventDefault();
521
-
522
- const btn = document.getElementById('submit-btn');
523
- const originalText = btn.textContent;
524
  btn.disabled = true;
525
- btn.textContent = 'Processing...';
526
-
527
- // Gather Data
528
- const features = {
529
- "Store": parseInt(document.getElementById('store').value),
530
- "Date": document.getElementById('date').value,
531
- "Promo": parseInt(document.getElementById('promo').value),
532
- "StateHoliday": document.getElementById('state_holiday').value,
533
- "SchoolHoliday": parseInt(document.getElementById('school_holiday').value),
534
- "Assortment": document.getElementById('assortment').value,
535
- "StoreType": document.getElementById('store_type').value,
536
- "CompetitionDistance": parseInt(document.getElementById('competition_distance').value) || 0,
537
- "ForecastDays": parseInt(document.getElementById('horizon').value)
538
  };
539
 
540
  try {
541
- const response = await fetch('/predict', {
542
  method: 'POST',
543
  headers: { 'Content-Type': 'application/json' },
544
- body: JSON.stringify(features)
545
  });
 
 
 
 
 
 
 
 
 
546
 
547
- const data = await response.json();
548
-
549
- // Switch View
550
- document.getElementById('result-placeholder').style.display = 'none';
551
- document.getElementById('results-content').style.display = 'flex';
552
-
553
- // Update Metric
554
- document.getElementById('sales_val').textContent = '€' + Math.round(data.PredictedSales).toLocaleString();
555
-
556
- // Update CI
557
- const [low, high] = data.ConfidenceInterval;
558
- document.getElementById('ci_val').textContent = `Est. Range: €${Math.round(low).toLocaleString()} - €${Math.round(high).toLocaleString()}`;
559
-
560
- // Update Explanations
561
- const explList = document.getElementById('explanation-list');
562
- explList.innerHTML = '';
563
- data.Explanation.forEach(item => {
564
- const div = document.createElement('div');
565
- div.className = 'explanation-item';
566
  const isPos = item.impact > 0;
567
- div.innerHTML = `
568
- <span class="feature-name">${item.feature}</span>
569
- <span class="impact-tag ${isPos ? 'impact-pos' : 'impact-neg'}">
570
- ${isPos ? '↑' : '↓'} ${Math.abs(item.impact).toFixed(1)}%
571
- </span>
 
 
 
 
 
 
 
 
 
572
  `;
573
- explList.appendChild(div);
574
  });
575
 
576
- // Simple Trend Indicator Logic (Compare first vs avg)
577
- const avg = data.Forecast.reduce((acc, curr) => acc + curr.sales, 0) / data.Forecast.length;
578
- const trendEl = document.getElementById('trend-indicator');
579
- if (data.PredictedSales < avg) {
580
- trendEl.innerHTML = '<strong>Increasing</strong> demand projected over horizon';
581
- trendEl.style.color = '#10b981'; // Green
582
- } else {
583
- trendEl.innerHTML = 'Shop expects <strong>stable/decreasing</strong> demand';
584
- trendEl.style.color = '#64748b'; // Grey
585
- }
586
-
587
- // Render Chart
588
  renderChart(data.Forecast);
589
 
590
- } catch (error) {
591
- alert('Prediction Failed: ' + error.message);
592
- console.error(error);
593
  } finally {
 
594
  btn.disabled = false;
595
- btn.textContent = originalText;
596
  }
597
  });
598
 
599
- // Professional Chart
600
- function renderChart(forecastData) {
601
- const ctx = document.getElementById('salesChart').getContext('2d');
602
- const labels = forecastData.map(d => {
603
- const date = new Date(d.date);
604
- return date.toLocaleDateString(undefined, {month: 'short', day: 'numeric'});
605
- });
606
- const dataPoints = forecastData.map(d => d.sales);
607
-
608
- if (chartInstance) chartInstance.destroy();
609
 
610
- // Gradient Fill
 
611
  const gradient = ctx.createLinearGradient(0, 0, 0, 400);
612
- gradient.addColorStop(0, 'rgba(37, 99, 235, 0.2)'); // Brand Blue
613
  gradient.addColorStop(1, 'rgba(37, 99, 235, 0)');
614
 
615
- chartInstance = new Chart(ctx, {
616
  type: 'line',
617
  data: {
618
  labels: labels,
619
  datasets: [{
620
- label: 'Projected Revenue (€)',
621
- data: dataPoints,
622
  borderColor: '#2563eb',
623
  backgroundColor: gradient,
624
- borderWidth: 2,
 
 
 
625
  pointBackgroundColor: '#fff',
626
- pointBorderColor: '#2563eb',
627
  pointBorderWidth: 2,
628
- pointRadius: 4,
629
- pointHoverRadius: 6,
630
- tension: 0.4, // Smooth Spline
631
- fill: true
632
  }]
633
  },
634
  options: {
635
  responsive: true,
636
  maintainAspectRatio: false,
637
- plugins: {
638
  legend: { display: false },
639
  tooltip: {
640
- backgroundColor: '#1e293b',
641
- padding: 12,
642
- titleFont: { family: 'Inter', size: 13 },
643
- bodyFont: { family: 'Inter', size: 14, weight: 'bold' },
644
- cornerRadius: 8,
645
  displayColors: false,
646
- callbacks: {
647
- label: (ctx) => '€' + Math.round(ctx.raw).toLocaleString()
648
- }
649
  }
650
  },
651
  scales: {
652
- x: {
653
- grid: { display: false },
654
- ticks: { font: { family: 'Inter' }, color: '#64748b' }
655
- },
656
- y: {
657
- border: { display: false },
658
  grid: { color: '#f1f5f9' },
659
  ticks: {
660
- font: { family: 'Inter' },
661
- color: '#64748b',
662
- callback: (value) => '€' + value.toLocaleString()
663
- },
664
- beginAtZero: true
665
  }
666
- },
667
- interaction: {
668
- intersect: false,
669
- mode: 'index',
670
- },
671
  }
672
  });
673
  }
674
  </script>
675
  </body>
676
  </html>
677
- """
 
3
  A clean, modern professional dashboard for making predictions.
4
  """
5
 
6
+ FRONTEND_HTML = \"\"\"
7
  <!DOCTYPE html>
8
  <html lang="en">
9
  <head>
10
  <meta charset="UTF-8">
11
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <title>Rossmann Sales Intelligence | Dashboard</title>
13
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
14
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15
  <style>
16
  :root {
17
+ --primary: #2563eb;
18
+ --primary-light: #eff6ff;
19
+ --secondary: #64748b;
20
+ --bg-page: #f1f5f9;
21
+ --text-main: #0f172a;
22
+ --text-muted: #475569;
 
 
23
  --border: #e2e8f0;
24
  --success: #10b981;
25
+ --danger: #ef4444;
26
+ --card-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
27
  }
28
 
29
+ * { margin: 0; padding: 0; box-sizing: border-box; }
30
+ body {
31
+ background-color: var(--bg-page);
32
+ color: var(--text-main);
33
  font-family: 'Inter', sans-serif;
 
 
 
 
 
34
  min-height: 100vh;
35
  display: flex;
36
  flex-direction: column;
37
  }
38
 
39
+ h1, h2, h3 { font-family: 'Outfit', sans-serif; }
40
+
41
+ /* Navigation */
42
  .navbar {
43
+ background: #fff;
44
+ padding: 1rem 2.5rem;
 
45
  display: flex;
46
  justify-content: space-between;
47
  align-items: center;
48
+ border-bottom: 1px solid var(--border);
49
+ position: sticky;
50
+ top: 0;
51
+ z-index: 100;
52
  }
53
+ .brand { font-weight: 700; font-size: 1.4rem; color: #1e293b; display: flex; align-items: center; gap: 0.75rem; }
54
+ .brand-icon { color: var(--primary); font-size: 1.6rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ .status-pill {
57
+ display: flex; align-items: center; gap: 0.5rem;
58
+ background: #fff; padding: 0.4rem 1rem; border-radius: 99px;
59
+ border: 1px solid var(--border); font-size: 0.85rem; font-weight: 600;
 
60
  }
61
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; }
62
+ .dot.online { background: var(--success); box-shadow: 0 0 8px var(--success); }
63
 
64
+ /* Layout */
65
+ .dashboard-container {
66
+ display: grid;
67
+ grid-template-columns: 320px 1fr;
68
+ max-width: 1440px;
69
  margin: 0 auto;
 
70
  width: 100%;
71
+ flex: 1;
72
+ padding: 2rem;
73
  gap: 2rem;
74
  }
75
 
76
+ @media (max-width: 1024px) { .dashboard-container { grid-template-columns: 1fr; } }
 
 
 
 
77
 
78
+ /* Sidebar Controls */
79
+ .sidebar { display: flex; flex-direction: column; gap: 1.5rem; }
80
+ .side-card {
81
+ background: #fff; border-radius: 16px; padding: 1.5rem;
82
+ box-shadow: var(--card-shadow); border: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
+ .group-title { font-size: 0.85rem; font-weight: 700; color: var(--secondary); text-transform: uppercase; margin-bottom: 1rem; border-bottom: 1px solid #f1f5f9; padding-bottom: 0.5rem; letter-spacing: 0.05em; }
85
 
86
+ .form-control { width: 100%; padding: 0.7rem 0.9rem; border: 1px solid var(--border); border-radius: 10px; font-size: 0.9rem; margin-bottom: 1rem; transition: 0.2s; background: #fafafa; }
87
+ .form-control:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px var(--primary-light); background: #fff; }
88
+ label { display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.4rem; color: var(--text-muted); }
89
+
90
+ .btn-predict {
91
+ width: 100%; background: var(--primary); color: #fff; border: none;
92
+ padding: 1rem; border-radius: 12px; font-weight: 700; font-size: 1rem;
93
+ cursor: pointer; transition: 0.2s; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  margin-top: 1rem;
 
 
95
  }
96
+ .btn-predict:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(37, 99, 235, 0.3); background: #1d4ed8; }
97
+ .btn-predict:active { transform: translateY(0); }
98
 
99
+ /* Result Area */
100
+ .main-display { display: flex; flex-direction: column; gap: 1.5rem; }
101
+ .top-row { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 1.5rem; }
102
+ @media (max-width: 768px) { .top-row { grid-template-columns: 1fr; } }
 
 
 
 
 
 
 
 
 
103
 
104
+ /* KPI Card */
105
+ .kpi-card {
106
+ background: #fff; border-radius: 20px; padding: 2.5rem; position: relative;
107
+ box-shadow: var(--card-shadow); border: 1px solid var(--border);
108
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
 
 
 
 
 
 
 
 
 
 
109
  overflow: hidden;
110
  }
111
+ .kpi-card::before { content: ''; position: absolute; top:0; left:0; width: 6px; height: 100%; background: var(--primary); }
112
+ .kpi-label { font-size: 0.9rem; font-weight: 700; color: var(--secondary); margin-bottom: 1.5rem; letter-spacing: 0.05em; }
113
+ .kpi-value { font-size: 5rem; font-weight: 800; color: var(--text-main); font-family: 'Outfit'; line-height: 1; margin-bottom: 1rem; }
114
+ .ci-badge {
115
+ background: var(--primary-light); color: var(--primary);
116
+ padding: 0.5rem 1.25rem; border-radius: 99px; font-size: 0.9rem; font-weight: 700;
117
+ border: 1px solid rgba(37, 99, 235, 0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
+ .trend-text { margin-top: 2rem; font-size: 1rem; display: flex; align-items: center; gap: 0.6rem; color: var(--text-muted); }
120
 
121
+ /* Drivers Card */
122
+ .drivers-card {
123
+ background: #fff; border-radius: 20px; padding: 2rem;
124
+ box-shadow: var(--card-shadow); border: 1px solid var(--border);
 
 
125
  }
126
+ .driver-row { margin-bottom: 1.25rem; }
127
+ .driver-info { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.9rem; font-weight: 600; }
128
+ .bar-container { height: 10px; background: #f1f5f9; border-radius: 5px; overflow: hidden; position: relative; }
129
+ .bar-fill { height: 100%; border-radius: 5px; transition: 1s cubic-bezier(0.34, 1.56, 0.64, 1); }
130
 
131
+ /* Chart Card */
132
+ .chart-card {
133
+ background: #fff; border-radius: 20px; padding: 2.5rem;
134
+ box-shadow: var(--card-shadow); border: 1px solid var(--border);
135
+ flex: 1; min-height: 500px; display: flex; flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
+ .chart-container { flex: 1; width: 100%; margin-top: 2rem; position: relative; }
138
 
139
+ /* Transitions */
140
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ .empty-state {
143
+ height: calc(100vh - 200px); display: flex; flex-direction: column; align-items: center; justify-content: center;
144
+ opacity: 0.6; text-align: center; color: var(--secondary);
 
 
 
 
 
 
 
145
  }
146
  </style>
147
  </head>
148
  <body>
149
  <nav class="navbar">
150
+ <div class="brand"><span class="brand-icon">🔹</span> Rossmann <strong>Sales Intelligence</strong></div>
151
+ <div class="status-pill">
152
+ <div class="dot" id="dot"></div>
153
+ <span id="status">Syncing Engines...</span>
 
 
154
  </div>
155
  </nav>
156
 
157
+ <div class="dashboard-container">
158
+ <!-- Sidebar -->
159
  <aside class="sidebar">
160
+ <div class="side-card">
161
+ <h3 class="group-title">Scenario Context</h3>
162
+ <form id="form">
163
+ <label>Target Store</label>
164
+ <input type="number" id="store" class="form-control" value="1" min="1" max="1115">
165
+
166
+ <label>Launch Date</label>
167
+ <input type="date" id="date" class="form-control">
168
+
169
+ <label>Forecast Horizon</label>
170
+ <select id="horizon" class="form-control">
171
+ <option value="1">Next 24 Hours</option>
172
+ <option value="7" selected>7-Day Outlook</option>
173
+ <option value="14">14-Day Outlook</option>
174
+ <option value="30">30-Day Outlook</option>
175
+ </select>
176
+
177
+ <h3 class="group-title">Operational Shifts</h3>
178
+ <label>Promotional Strategy</label>
179
+ <select id="promo" class="form-control">
180
+ <option value="1" selected>Active Sales Promo</option>
181
+ <option value="0">Standard Operations</option>
182
+ </select>
183
+
184
+ <label>Public Holidays</label>
185
+ <select id="state_holiday" class="form-control">
186
+ <option value="0">Normal Business Day</option>
187
+ <option value="a">State Holiday</option>
188
+ <option value="b">Easter Period</option>
189
+ <option value="c">Christmas Context</option>
190
+ </select>
191
+
192
+ <label>Academic Holidays</label>
193
+ <select id="school_holiday" class="form-control">
194
+ <option value="0">Schools in Session</option>
195
+ <option value="1">School Holidays Active</option>
196
+ </select>
197
+
198
+ <h3 class="group-title">Local Demographics</h3>
199
+ <label>Competitor Proximity (m)</label>
200
+ <input type="number" id="dist" class="form-control" value="1270">
201
+
202
+ <button type="submit" class="btn-predict" id="btn">Generate Intelligence</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  </form>
204
  </div>
205
  </aside>
206
 
207
+ <!-- Main Workspace -->
208
+ <main class="main-display">
209
+ <div id="init-view" class="empty-state">
210
+ <span style="font-size: 5rem; margin-bottom: 1.5rem;">📊</span>
211
+ <h2 style="font-size: 1.8rem;">Decision Intelligence Workspace</h2>
212
+ <p style="max-width: 400px; line-height: 1.6;">Configure the store parameters on the left to run our AI-driven revenue projection engine.</p>
213
  </div>
214
 
215
+ <div id="result-view" style="display: none; animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1);">
216
+ <div class="top-row">
217
+ <!-- Projection KPI -->
218
+ <div class="kpi-card">
219
+ <span class="kpi-label">ESTIMATED REVENUE (95% CI)</span>
220
+ <h2 class="kpi-value" id="val">--</h2>
221
+ <div class="ci-badge" id="ci">Confidence Interval: --</div>
222
+ <div class="trend-text" id="trend"></div>
223
  </div>
224
 
225
+ <!-- Explainability Drivers -->
226
+ <div class="drivers-card">
227
+ <h3 class="group-title">Decision Drivers</h3>
228
+ <div id="drivers-list">
229
+ <!-- Populated dynamically -->
 
 
 
230
  </div>
231
  </div>
232
  </div>
233
 
234
+ <!-- Analysis Chart -->
235
+ <div class="chart-card">
236
+ <h3 class="group-title">Forecast Velocity & Variance</h3>
237
+ <div class="chart-container">
238
+ <canvas id="chart"></canvas>
 
 
 
239
  </div>
240
  </div>
241
  </div>
242
  </main>
243
  </div>
244
 
 
 
 
 
 
 
 
245
  <script>
 
246
  document.getElementById('date').valueAsDate = new Date();
247
+ let chart = null;
248
+
249
+ // System Pulse
250
+ fetch('/health').then(r => r.json()).then(d => {
251
+ const dot = document.getElementById('dot');
252
+ const txt = document.getElementById('status');
253
+ if(d.status === 'healthy') {
254
+ dot.classList.add('online');
255
+ txt.textContent = 'Intelligence Engine Online';
256
+ }
257
+ });
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ document.getElementById('form').addEventListener('submit', async (e) => {
 
260
  e.preventDefault();
261
+ const btn = document.getElementById('btn');
262
+ btn.textContent = 'Processing Data...';
 
263
  btn.disabled = true;
264
+
265
+ const payload = {
266
+ Store: parseInt(document.getElementById('store').value),
267
+ Date: document.getElementById('date').value,
268
+ Promo: parseInt(document.getElementById('promo').value),
269
+ StateHoliday: document.getElementById('state_holiday').value,
270
+ SchoolHoliday: parseInt(document.getElementById('school_holiday').value),
271
+ Assortment: "a",
272
+ StoreType: "a",
273
+ CompetitionDistance: parseInt(document.getElementById('dist').value) || 0,
274
+ ForecastDays: parseInt(document.getElementById('horizon').value)
 
 
275
  };
276
 
277
  try {
278
+ const res = await fetch('/predict', {
279
  method: 'POST',
280
  headers: { 'Content-Type': 'application/json' },
281
+ body: JSON.stringify(payload)
282
  });
283
+ const data = await res.json();
284
+
285
+ document.getElementById('init-view').style.display = 'none';
286
+ document.getElementById('result-view').style.display = 'flex';
287
+
288
+ // Display KPI
289
+ const pred = Math.round(data.PredictedSales);
290
+ document.getElementById('val').textContent = '€' + pred.toLocaleString();
291
+ document.getElementById('ci').textContent = `95% Range: €${Math.round(data.ConfidenceInterval[0]).toLocaleString()} - €${Math.round(data.ConfidenceInterval[1]).toLocaleString()}`;
292
 
293
+ // Narrative Trend
294
+ const forecastAvg = data.Forecast.reduce((s,f) => s + f.sales, 0) / data.Forecast.length;
295
+ const trendEl = document.getElementById('trend');
296
+ if (data.PredictedSales < forecastAvg) {
297
+ trendEl.innerHTML = '<span style="color:var(--success); font-size:1.2rem;">↑</span> <strong>Upward momentum</strong> detected over the forecast horizon.';
298
+ } else {
299
+ trendEl.innerHTML = '<span style="color:var(--secondary); font-size:1.2rem;">→</span> <strong>Stable operations</strong> projected with minor seasonal dip.';
300
+ }
301
+
302
+ // AI Explanation Bars
303
+ const list = document.getElementById('drivers-list');
304
+ list.innerHTML = '';
305
+ data.Explanation.slice(0, 5).forEach(item => {
 
 
 
 
 
 
306
  const isPos = item.impact > 0;
307
+ const color = isPos ? 'var(--success)' : 'var(--danger)';
308
+ // Scale bar width (log-based contributions are usually -1.0 to 1.0, so abs(impact)% is roughly 0-50%)
309
+ const barWidth = Math.min(Math.max(Math.abs(item.impact) * 2, 5), 98);
310
+
311
+ const row = document.createElement('div');
312
+ row.className = 'driver-row';
313
+ row.innerHTML = `
314
+ <div class="driver-info">
315
+ <span>${item.feature}</span>
316
+ <span style="color:${color}">${item.formatted_val}</span>
317
+ </div>
318
+ <div class="bar-container">
319
+ <div class="bar-fill" style="width:${barWidth}%; background:${color}; ${isPos ? '' : 'margin-left:auto;'}"></div>
320
+ </div>
321
  `;
322
+ list.appendChild(row);
323
  });
324
 
325
+ // Render High-Fidelity Chart
 
 
 
 
 
 
 
 
 
 
 
326
  renderChart(data.Forecast);
327
 
328
+ } catch(e) {
329
+ console.error(e);
330
+ alert("Core engine returned error. Please check parameters.");
331
  } finally {
332
+ btn.textContent = 'Generate Intelligence';
333
  btn.disabled = false;
 
334
  }
335
  });
336
 
337
+ function renderChart(forecast) {
338
+ const ctx = document.getElementById('chart').getContext('2d');
339
+ const labels = forecast.map(f => new Date(f.date).toLocaleDateString(undefined, {weekday: 'short', month:'short', day:'numeric'}));
340
+ const vals = forecast.map(f => f.sales);
 
 
 
 
 
 
341
 
342
+ if(chart) chart.destroy();
343
+
344
  const gradient = ctx.createLinearGradient(0, 0, 0, 400);
345
+ gradient.addColorStop(0, 'rgba(37, 99, 235, 0.15)');
346
  gradient.addColorStop(1, 'rgba(37, 99, 235, 0)');
347
 
348
+ chart = new Chart(ctx, {
349
  type: 'line',
350
  data: {
351
  labels: labels,
352
  datasets: [{
353
+ label: 'Projected Daily Revenue',
354
+ data: vals,
355
  borderColor: '#2563eb',
356
  backgroundColor: gradient,
357
+ fill: true,
358
+ tension: 0.4,
359
+ borderWidth: 4,
360
+ pointRadius: 6,
361
  pointBackgroundColor: '#fff',
 
362
  pointBorderWidth: 2,
363
+ pointHoverRadius: 8
 
 
 
364
  }]
365
  },
366
  options: {
367
  responsive: true,
368
  maintainAspectRatio: false,
369
+ plugins: {
370
  legend: { display: false },
371
  tooltip: {
372
+ backgroundColor: '#0f172a',
373
+ padding: 16,
374
+ cornerRadius: 12,
375
+ titleFont: { size: 14, weight: 'bold' },
376
+ bodyFont: { size: 16 },
377
  displayColors: false,
378
+ callbacks: { label: c => '€' + Math.round(c.raw).toLocaleString() }
 
 
379
  }
380
  },
381
  scales: {
382
+ x: { grid: { display: false }, ticks: { font: { size: 12 } } },
383
+ y: {
384
+ beginAtZero: false,
 
 
 
385
  grid: { color: '#f1f5f9' },
386
  ticks: {
387
+ callback: v => '' + v.toLocaleString(),
388
+ font: { size: 12 }
389
+ }
 
 
390
  }
391
+ }
 
 
 
 
392
  }
393
  });
394
  }
395
  </script>
396
  </body>
397
  </html>
398
+ \"\"\"