topsecrettraders commited on
Commit
8b904ae
·
verified ·
1 Parent(s): f1c5ddc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +546 -717
app.py CHANGED
@@ -207,7 +207,7 @@ def fetch_series(
207
 
208
 
209
  # ==========================================
210
- # 5. FRONTEND TEMPLATE (World-Class Refactor)
211
  # ==========================================
212
 
213
  HTML_TEMPLATE = """
@@ -215,376 +215,266 @@ HTML_TEMPLATE = """
215
  <html lang="en">
216
  <head>
217
  <meta charset="UTF-8">
218
- <title>DepthChain Live Pro</title>
219
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
220
  <link rel="preconnect" href="https://fonts.googleapis.com">
221
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
222
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
223
  <style>
224
- /* --- WORLD CLASS THEME --- */
225
  :root {
226
- --bg-body: #0d1117; /* GitHub Dimmed Background */
227
- --bg-header: rgba(22, 27, 34, 0.85); /* Translucent Header */
228
- --bg-card: #161b22; /* Card Background */
229
- --bg-input: #010409; /* Dark Input Background */
230
- --border: #30363d; /* Subtle Border */
231
- --border-hover: #6e7681;
232
 
233
- --text-primary: #e6edf3;
234
- --text-secondary: #8b949e;
 
 
 
 
 
 
 
 
 
 
 
235
 
236
- --accent: #2f81f7;
237
- --accent-glow: rgba(47, 129, 247, 0.2);
238
- --success: #238636;
239
- --danger: #f85149;
240
- --warning: #d29922;
241
-
242
- --shadow-card: 0 4px 12px rgba(0,0,0,0.5);
243
  --radius-md: 8px;
244
- --radius-sm: 6px;
 
 
 
245
  }
246
 
247
- * { box-sizing: border-box; margin: 0; padding: 0; outline: none; -webkit-tap-highlight-color: transparent; }
 
248
 
249
  body {
250
  background: var(--bg-body);
251
  color: var(--text-primary);
252
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
253
- display: flex;
254
- flex-direction: column;
255
  height: 100vh;
256
- overflow: hidden; /* Body doesn't scroll, columns do */
257
- font-size: 13px;
 
258
  }
259
 
260
- /* --- SCROLLBARS --- */
 
261
  ::-webkit-scrollbar { width: 8px; height: 8px; }
262
  ::-webkit-scrollbar-track { background: transparent; }
263
- ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
264
- ::-webkit-scrollbar-thumb:hover { background: #586069; }
265
-
266
- /* --- TOP BAR --- */
267
- .top-bar {
268
- height: 56px;
269
- background: var(--bg-header);
270
- backdrop-filter: blur(12px);
271
- -webkit-backdrop-filter: blur(12px);
272
- border-bottom: 1px solid var(--border);
273
- display: flex;
274
- align-items: center;
275
- justify-content: space-between;
276
- padding: 0 20px;
277
- z-index: 100;
278
- flex-shrink: 0;
279
  }
 
280
 
281
- .logo {
282
- font-weight: 700;
283
- font-size: 16px;
284
- letter-spacing: -0.5px;
285
- color: var(--text-primary);
286
- display: flex;
287
- align-items: center;
288
- gap: 8px;
 
 
 
 
289
  }
290
- .logo span {
291
- background: var(--accent);
292
- color: white;
293
- font-size: 10px;
294
- padding: 2px 6px;
295
- border-radius: 12px;
296
- font-weight: 600;
297
  }
298
-
299
- .controls { display: flex; gap: 12px; align-items: center; }
300
-
301
- /* Custom Select Styling */
302
- .select-wrapper { position: relative; }
303
- select {
304
- appearance: none;
305
- background: var(--bg-input);
306
- border: 1px solid var(--border);
307
- color: var(--text-primary);
308
- padding: 6px 30px 6px 12px;
309
- border-radius: var(--radius-sm);
310
- font-size: 12px;
311
- font-family: inherit;
312
- cursor: pointer;
313
- transition: all 0.2s;
314
- min-width: 120px;
315
  }
316
- select:hover { border-color: var(--border-hover); }
317
- select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
318
- .select-chevron {
319
- position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
320
- pointer-events: none; color: var(--text-secondary); font-size: 10px;
 
321
  }
 
322
 
323
- /* Buttons */
324
- button {
325
- background: var(--bg-card);
326
- border: 1px solid var(--border);
327
- color: var(--text-primary);
328
- padding: 6px 12px;
329
- border-radius: var(--radius-sm);
330
- font-size: 12px;
331
- font-weight: 500;
332
- cursor: pointer;
333
- transition: all 0.2s;
334
  }
335
- button:hover { border-color: var(--text-secondary); background: #21262d; }
336
-
337
- .btn-primary { background: var(--accent); color: #fff; border: 1px solid var(--accent); }
338
- .btn-primary:hover { background: #2c76e3; border-color: #2c76e3; }
339
-
340
- .btn-danger { color: var(--danger); border-color: var(--border); }
341
- .btn-danger:hover { background: rgba(248, 81, 73, 0.1); border-color: var(--danger); }
342
 
343
- .btn-ghost {
344
- background: transparent;
345
- border: 1px solid var(--text-secondary);
346
- color: var(--text-secondary);
347
- opacity: 0.7;
348
- }
349
- .btn-ghost:hover {
350
- opacity: 1;
351
- border-color: var(--accent);
352
- color: var(--accent);
353
- background: rgba(47, 129, 247, 0.05);
354
  }
 
 
355
 
356
- /* --- LAYOUT SELECTOR --- */
357
- .layout-group {
358
- display: flex;
359
- background: var(--bg-input);
360
- border: 1px solid var(--border);
361
- border-radius: var(--radius-sm);
362
- padding: 2px;
363
- }
364
- .layout-btn {
365
- background: transparent; border: none; padding: 4px 10px;
366
- color: var(--text-secondary); border-radius: 4px;
367
  }
368
- .layout-btn:hover { background: transparent; color: var(--text-primary); }
369
- .layout-btn.active { background: var(--border); color: var(--text-primary); }
370
 
371
- /* --- MAIN GRID --- */
372
- .main-content {
373
  flex: 1;
374
- display: flex; /* Flex rows for columns */
375
- overflow: hidden; /* Prevent body scroll */
376
- padding: 0;
377
- gap: 1px; /* Divider look */
378
- background: var(--border);
379
  }
380
 
381
  .column {
382
  flex: 1;
383
  display: flex;
384
  flex-direction: column;
385
- gap: 16px;
386
- overflow-y: auto; /* INDEPENDENT SCROLL */
387
- background: var(--bg-body);
388
- padding: 16px;
389
- min-width: 320px;
390
- }
391
-
392
- .column-add-btn {
393
- padding: 20px;
394
- border: 1px dashed var(--border);
395
- color: var(--text-secondary);
396
- text-align: center;
397
- border-radius: var(--radius-md);
398
- cursor: pointer;
399
- margin-top: auto;
400
- transition: 0.2s;
401
- font-size: 13px;
402
  }
403
- .column-add-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(47, 129, 247, 0.03); }
404
 
405
  /* --- CHART CARD --- */
406
  .chart-card {
407
- background: var(--bg-card);
408
- border: 1px solid var(--border);
409
- border-radius: var(--radius-md);
410
- height: 380px;
411
- min-height: 380px;
412
- display: flex;
413
- flex-direction: column;
414
- box-shadow: 0 1px 3px rgba(0,0,0,0.2);
415
- position: relative;
416
- transition: border-color 0.2s;
417
  }
418
- .chart-card:hover { border-color: var(--border-hover); }
419
 
420
  .card-header {
421
- height: 40px;
422
- border-bottom: 1px solid var(--border);
423
- display: flex;
424
- align-items: center;
425
- justify-content: space-between;
426
- padding: 0 12px;
427
- background: rgba(255,255,255,0.01);
428
- border-radius: var(--radius-md) var(--radius-md) 0 0;
429
  }
 
 
 
430
 
431
- .card-title {
432
- font-weight: 600;
433
- font-size: 12px;
434
- display: flex;
435
- align-items: center;
436
- gap: 8px;
437
- color: var(--text-primary);
438
- }
439
- .card-meta {
440
- color: var(--text-secondary);
441
- font-family: 'JetBrains Mono', monospace;
442
- font-size: 11px;
443
- opacity: 0.8;
444
- margin-left: 6px;
445
- }
446
-
447
- .icon-btn {
448
- padding: 4px;
449
- border: none;
450
- background: transparent;
451
- color: var(--text-secondary);
452
- font-size: 14px;
453
- border-radius: 4px;
454
- }
455
- .icon-btn:hover { background: var(--border); color: var(--text-primary); }
456
 
457
  .card-body {
458
- flex: 1;
459
- position: relative;
460
- padding: 8px 8px 4px 4px; /* Slight padding for chart labels */
461
  }
462
 
463
- .empty-state {
464
- display: flex;
465
- flex-direction: column;
466
- align-items: center;
467
- justify-content: center;
468
- height: 100%;
469
- color: var(--text-secondary);
470
- gap: 12px;
 
471
  }
472
-
473
- /* --- STATUS DOTS --- */
474
- .status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
475
- .status-dot.live { background: var(--success); box-shadow: 0 0 8px var(--success); animation: pulse 2s infinite; }
476
- .status-dot.hist { background: var(--accent); }
477
- .status-dot.retry { background: var(--warning); }
478
- .status-dot.offline { background: var(--border); }
479
- @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
480
-
481
- /* --- TOAST --- */
482
- .toast-container {
483
- position: fixed; bottom: 24px; right: 24px; z-index: 2000;
484
- display: flex; flex-direction: column; gap: 10px;
485
- }
486
- .toast {
487
- background: var(--bg-card); border: 1px solid var(--border);
488
- color: var(--text-primary); padding: 12px 16px;
489
- border-radius: var(--radius-md); font-weight: 500; font-size: 13px;
490
- box-shadow: 0 8px 24px rgba(0,0,0,0.5);
491
- display: flex; align-items: center; gap: 10px;
492
- animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
493
- }
494
- .toast-success { border-left: 3px solid var(--success); }
495
- .toast-info { border-left: 3px solid var(--accent); }
496
- @keyframes slideIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
497
 
498
  /* --- MODAL --- */
499
  .modal-overlay {
500
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
501
- background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); z-index: 1000;
502
- display: none; justify-content: center; align-items: center;
503
- opacity: 0; transition: opacity 0.2s;
504
  }
505
- .modal-overlay.active { opacity: 1; }
506
-
507
  .modal {
508
- background: #1c2128; border: 1px solid var(--border); border-radius: 12px;
509
  width: 480px; max-width: 90%; max-height: 85vh; display: flex; flex-direction: column;
510
- box-shadow: 0 24px 48px rgba(0,0,0,0.6);
511
- transform: scale(0.95); transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
512
  }
513
- .modal-overlay.active .modal { transform: scale(1); }
514
 
515
- .modal-head {
516
- padding: 16px 20px; border-bottom: 1px solid var(--border);
517
- font-weight: 600; font-size: 14px;
518
- display: flex; justify-content: space-between; align-items: center;
519
- }
520
  .modal-body { padding: 20px; overflow-y: auto; }
521
- .modal-foot { padding: 16px 20px; border-top: 1px solid var(--border); text-align: right; background: rgba(0,0,0,0.2); border-radius: 0 0 12px 12px; }
522
 
523
- .form-group { margin-bottom: 16px; }
524
- .form-group label { display: block; margin-bottom: 6px; color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
525
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
526
 
527
- input[type="text"], input[type="time"], input[type="number"] {
528
- width: 100%; background: var(--bg-input); border: 1px solid var(--border);
529
- color: var(--text-primary); padding: 8px 10px; border-radius: var(--radius-sm);
530
- }
531
- input:focus { border-color: var(--accent); }
532
 
533
- /* Series List Styling */
534
- .series-row {
535
- display: grid; grid-template-columns: 32px 1fr 1fr 60px 24px;
536
- gap: 8px; margin-bottom: 8px; align-items: center;
537
- }
538
- .color-picker {
539
- width: 100%; height: 32px; padding: 0; border: none;
540
- background: none; cursor: pointer;
541
  }
 
542
 
543
  </style>
544
  </head>
545
  <body>
546
 
547
- <!-- HEADER -->
548
- <div class="top-bar">
549
- <div class="logo">DepthChain <span>PRO</span></div>
 
 
550
 
551
- <div class="controls">
552
  <!-- Date Selector -->
553
  <div class="select-wrapper">
554
  <select id="globalDate" onchange="GlobalApp.onDateChange()"></select>
555
- <span class="select-chevron">▼</span>
556
  </div>
557
-
558
  <!-- Mode Toggle -->
559
- <div class="layout-group">
560
- <button class="layout-btn active" id="modeHist" onclick="GlobalApp.setMode('HIST')">History</button>
561
- <button class="layout-btn" id="modeLive" onclick="GlobalApp.setMode('LIVE')">Live</button>
 
 
 
562
  </div>
563
 
564
- <div style="width: 1px; height: 20px; background: var(--border); margin: 0 8px;"></div>
565
-
566
- <!-- Layout Selector -->
567
- <div class="layout-group">
568
- <button class="layout-btn" id="lay1" onclick="GlobalApp.setLayout(1)">1</button>
569
- <button class="layout-btn" id="lay2" onclick="GlobalApp.setLayout(2)">2</button>
570
- <button class="layout-btn active" id="lay3" onclick="GlobalApp.setLayout(3)">3</button>
571
- <button class="layout-btn" id="lay4" onclick="GlobalApp.setLayout(4)">4</button>
572
  </div>
573
 
574
- <button onclick="ConfigModal.openDefaults()">Global Defaults</button>
575
  <button class="btn-danger" onclick="GlobalApp.resetApp()">Reset</button>
576
  </div>
577
  </div>
578
 
579
- <!-- GRID CONTAINER -->
580
- <div class="main-content" id="gridContainer">
581
- <!-- Columns injected via JS -->
582
  </div>
583
 
584
- <!-- TOAST CONTAINER -->
585
- <div class="toast-container" id="toastContainer"></div>
586
-
587
- <!-- MODALS -->
588
  <div class="modal-overlay" id="configModalOverlay">
589
  <div class="modal">
590
  <div class="modal-head">
@@ -592,40 +482,35 @@ HTML_TEMPLATE = """
592
  <button class="icon-btn" onclick="ConfigModal.close()">✕</button>
593
  </div>
594
  <div class="modal-body">
595
- <!-- Loading State -->
596
- <div id="cfgLoader" style="display:none; text-align:center; padding:20px; color:var(--text-secondary);">Loading metadata...</div>
597
 
598
- <!-- Config Form -->
599
  <div id="cfgForm">
600
- <div class="form-row">
601
  <div class="form-group">
602
- <label>Symbol (Root)</label>
603
- <div class="select-wrapper">
604
  <select id="cfgRoot" onchange="ConfigModal.onRootChange()" style="width:100%"></select>
605
- <span class="select-chevron">▼</span>
606
  </div>
607
  </div>
608
  <div class="form-group">
609
  <label>Expiry</label>
610
- <div class="select-wrapper">
611
  <select id="cfgExp" onchange="ConfigModal.onExpChange()" style="width:100%"></select>
612
- <span class="select-chevron">▼</span>
613
  </div>
614
  </div>
615
  </div>
616
-
617
  <div class="form-group">
618
  <label>Instrument</label>
619
- <div class="select-wrapper">
620
  <select id="cfgInst" style="width:100%"></select>
621
- <span class="select-chevron">▼</span>
622
  </div>
623
  </div>
624
 
625
- <div class="form-row">
626
  <div class="form-group">
627
  <label>Timeframe</label>
628
- <div class="select-wrapper">
629
  <select id="cfgTF" style="width:100%">
630
  <option value="1min">1 Minute</option>
631
  <option value="3min">3 Minutes</option>
@@ -634,67 +519,54 @@ HTML_TEMPLATE = """
634
  <option value="30min">30 Minutes</option>
635
  <option value="1hour">1 Hour</option>
636
  </select>
637
- <span class="select-chevron">▼</span>
638
  </div>
639
  </div>
640
  <div class="form-group">
641
  <label>Range (HH:MM)</label>
642
  <div style="display:flex; gap:8px;">
643
- <input type="time" id="cfgStart" value="09:15">
644
- <input type="time" id="cfgEnd" value="15:30">
645
  </div>
646
  </div>
647
  </div>
648
 
649
- <hr style="border:0; border-top:1px solid var(--border); margin:15px 0;">
650
 
651
  <div class="form-group">
652
- <label>Plot Lines (Vars: $P, $B, $S, $V, $OI)</label>
 
 
 
653
  <div id="cfgSeriesList"></div>
654
- <button class="btn-ghost" style="width:100%; margin-top:8px; border-style:dashed;" onclick="ConfigModal.addSeriesRow()">+ Add Line</button>
655
  </div>
656
  </div>
657
  </div>
658
  <div class="modal-foot">
659
- <button onclick="ConfigModal.close()" style="background:transparent; border:none; margin-right:10px;">Cancel</button>
660
  <button class="btn-primary" onclick="ConfigModal.save()">Save Configuration</button>
661
  </div>
662
  </div>
663
  </div>
664
 
665
- <!-- RESTORE SESSION POPUP -->
666
  <div class="modal-overlay" id="restoreModal">
667
- <div class="modal" style="width: 360px;">
668
  <div class="modal-head">Restore Session?</div>
669
- <div class="modal-body" style="color: var(--text-secondary);">
670
- We found a saved layout from your previous visit. Would you like to restore it?
671
  </div>
672
  <div class="modal-foot">
673
- <button class="btn-danger" onclick="GlobalApp.initNew()" style="margin-right:8px;">Start Fresh</button>
674
  <button class="btn-primary" onclick="GlobalApp.loadFromStorage()">Restore</button>
675
  </div>
676
  </div>
677
  </div>
678
 
679
- <script>
680
- /**
681
- * --- UTILS ---
682
- */
683
- const Toast = {
684
- show(msg, type='info') {
685
- const container = document.getElementById('toastContainer');
686
- const el = document.createElement('div');
687
- el.className = `toast toast-${type}`;
688
- el.innerHTML = `<span>${msg}</span>`;
689
- container.appendChild(el);
690
- setTimeout(() => {
691
- el.style.opacity = '0';
692
- el.style.transform = 'translateY(10px)';
693
- setTimeout(() => el.remove(), 300);
694
- }, 3000);
695
- }
696
- };
697
 
 
698
  /**
699
  * --- GLOBAL STATE MANAGER ---
700
  */
@@ -702,26 +574,22 @@ const GlobalApp = {
702
  state: {
703
  date: '',
704
  mode: 'HIST',
705
- columns: 3, // Default to 3
706
- charts: {}, // ID -> { config: {}, instance: ChartJS, interval: null, lastData: null, col: int }
707
- layout: [] // Array of Arrays [[id1, id2], [id3], [id4]]
708
  },
709
-
710
  defaults: [
711
- { formula: '$P', color: '#58a6ff', label: 'Price', axis: 'left' },
712
- { formula: '$B', color: '#238636', label: 'Buy Qty', axis: 'right' },
713
- { formula: '$S', color: '#da3633', label: 'Sell Qty', axis: 'right' }
714
  ],
715
 
716
  async init() {
717
- // Load dates first
718
  await this.loadDates();
719
 
720
- // Check local storage
721
- if (localStorage.getItem('dc_state_v3')) {
722
- const modal = document.getElementById('restoreModal');
723
- modal.style.display = 'flex';
724
- setTimeout(() => modal.classList.add('active'), 10);
725
  } else {
726
  this.initNew();
727
  }
@@ -732,199 +600,204 @@ const GlobalApp = {
732
  const res = await fetch('/api/dates');
733
  const dates = await res.json();
734
  const sel = document.getElementById('globalDate');
735
-
736
- // Populating ALL dates as requested
737
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
738
 
739
- if(dates.length) {
 
740
  this.state.date = dates[0];
741
  sel.value = dates[0];
742
  }
743
- } catch(e) { console.error("Dates error", e); Toast.show("Failed to load dates", "error"); }
 
 
 
744
  },
745
 
746
  initNew() {
747
- this.closeRestoreModal();
748
- this.setLayout(3); // Default 3 cols
749
- // Add one chart per column initially
750
- this.addChart(0);
751
- this.addChart(1);
752
- this.addChart(2);
753
- },
754
-
755
- closeRestoreModal() {
756
- const modal = document.getElementById('restoreModal');
757
- modal.classList.remove('active');
758
- setTimeout(() => modal.style.display = 'none', 200);
759
  },
760
 
761
  loadFromStorage() {
762
  try {
763
- const raw = localStorage.getItem('dc_state_v3');
764
  const saved = JSON.parse(raw);
765
 
766
- this.closeRestoreModal();
767
 
768
- // Restore Global Settings
769
- this.state.date = saved.date;
770
- this.state.mode = saved.mode;
771
- this.state.columns = saved.columns;
772
-
773
- // Update UI Controls
774
- const dateSel = document.getElementById('globalDate');
775
- if(dateSel.querySelector(`option[value="${saved.date}"]`)) {
776
- dateSel.value = saved.date;
777
  }
 
 
 
778
  this.updateModeUI();
779
- this.updateLayoutUI();
780
 
781
- // Rebuild Layout
782
- this.setupGridDOM(); // Create columns
783
-
784
- // Add Charts
785
- saved.chartList.forEach(item => {
786
- this.addChart(item.col, item.id, item.config);
 
 
 
 
 
 
 
 
 
 
 
 
787
  });
788
 
789
- Toast.show("Session Restored", "success");
790
 
791
  } catch(e) {
792
  console.error("Load failed", e);
793
- Toast.show("Save file corrupted, starting new.", "error");
794
  this.initNew();
795
  }
796
  },
797
 
798
  saveToStorage() {
799
- // Flatten layout for storage
800
- const chartList = [];
801
- Object.keys(this.state.charts).forEach(id => {
802
- chartList.push({
803
  id: id,
804
- col: this.state.charts[id].col,
805
  config: this.state.charts[id].config
806
- });
807
  });
808
 
809
  const dump = {
810
  date: this.state.date,
811
  mode: this.state.mode,
812
  columns: this.state.columns,
813
- chartList: chartList
814
  };
815
- localStorage.setItem('dc_state_v3', JSON.stringify(dump));
816
  },
817
 
818
- resetApp() {
819
- if(!confirm("Are you sure you want to reset everything?")) return;
820
- localStorage.removeItem('dc_state_v3');
821
- location.reload();
822
- },
823
-
824
- // --- LAYOUT LOGIC (PERSISTENCE) ---
825
  setLayout(cols) {
826
- if (cols === this.state.columns && document.getElementById('col-0')) return; // No change
 
 
 
827
 
 
828
  this.state.columns = cols;
829
- this.updateLayoutUI();
830
-
831
- // 1. Get all existing chart elements
832
- const existingCards = [];
833
- Object.keys(this.state.charts).forEach(id => {
834
- const el = document.getElementById(`card-${id}`);
835
- if (el) existingCards.push({ id, el });
 
 
836
  });
837
 
838
- // 2. Setup New Grid Columns
839
- this.setupGridDOM();
 
 
840
 
841
- // 3. Re-distribute existing charts
842
- existingCards.forEach((item, index) => {
843
- const newColIdx = index % cols; // Round robin distribution
844
- const colDiv = document.getElementById(`col-${newColIdx}`);
845
- const addBtn = colDiv.querySelector('.column-add-btn');
846
-
847
- // Move DOM Element (Persists Canvas State if handled correctly, though resize might be needed)
848
- colDiv.insertBefore(item.el, addBtn);
849
-
850
- // Update Internal State
851
- this.state.charts[item.id].col = newColIdx;
852
-
853
- // Trigger Resize on ChartJS instance just in case
854
- const inst = this.state.charts[item.id].instance;
855
- if(inst) inst.resize();
856
  });
857
-
858
- this.saveToStorage();
859
  },
860
 
861
- setupGridDOM() {
862
  const container = document.getElementById('gridContainer');
863
- container.innerHTML = ''; // Clear container
 
 
 
 
 
 
 
 
 
 
864
 
865
  for (let i = 0; i < this.state.columns; i++) {
866
  const colDiv = document.createElement('div');
867
  colDiv.className = 'column';
868
  colDiv.id = `col-${i}`;
869
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
870
  const addBtn = document.createElement('div');
871
- addBtn.className = 'column-add-btn';
872
  addBtn.innerHTML = '+ Add Chart';
873
  addBtn.onclick = () => this.addChart(i);
874
-
875
  colDiv.appendChild(addBtn);
 
876
  container.appendChild(colDiv);
877
  }
878
  },
879
 
880
- updateLayoutUI() {
881
- document.querySelectorAll('.layout-btn').forEach(b => b.classList.remove('active'));
882
- const btn = document.getElementById(`lay${this.state.columns}`);
883
- if(btn) btn.classList.add('active');
884
- },
885
-
886
- // --- CHART MANAGEMENT ---
887
- addChart(colIdx, forceId = null, forceConfig = null) {
888
- const id = forceId || 'c_' + Date.now() + Math.random().toString(36).substr(2,5);
889
 
890
- // Init State
891
- if (!this.state.charts[id]) {
892
- this.state.charts[id] = {
893
- col: colIdx,
894
- config: forceConfig || {},
895
- instance: null,
896
- interval: null,
897
- lastData: null
898
- };
899
- }
900
 
901
- // Render UI
902
  const colDiv = document.getElementById(`col-${colIdx}`);
903
- const btn = colDiv.querySelector('.column-add-btn');
904
- const el = this.createChartElement(id);
905
- colDiv.insertBefore(el, btn);
906
-
907
- // If config exists (Restore or moved), plot from memory or fetch
908
- if (forceConfig && forceConfig.root) {
909
- // Delay slightly to ensure DOM is painted for Canvas
910
- setTimeout(() => ChartLogic.plot(id), 0);
911
- }
912
-
913
- if (!forceId) this.saveToStorage();
914
  },
915
 
916
  removeChart(id) {
917
- // Confirmation is annoying, maybe just do it? Or subtle check.
918
- if(!confirm("Remove this chart?")) return;
919
-
920
  ChartLogic.stopLive(id);
921
  if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
922
-
923
- delete this.state.charts[id];
924
 
925
- const el = document.getElementById(`card-${id}`);
926
- if(el) el.remove();
 
927
 
 
928
  this.saveToStorage();
929
  },
930
 
@@ -932,412 +805,368 @@ const GlobalApp = {
932
  const div = document.createElement('div');
933
  div.className = 'chart-card';
934
  div.id = `card-${id}`;
 
 
 
 
 
935
  div.innerHTML = `
936
  <div class="card-header">
937
- <div class="card-title" id="title-${id}">
938
- <span class="status-dot offline"></span>
939
- <span>New Chart</span>
940
  </div>
941
- <div class="controls" style="gap:4px">
942
  <button class="icon-btn" onclick="ConfigModal.open('${id}')">⚙</button>
943
  <button class="icon-btn" onclick="GlobalApp.removeChart('${id}')">✕</button>
944
  </div>
945
  </div>
946
  <div class="card-body" id="body-${id}">
947
- <div class="empty-state">
948
- <p style="font-weight:500;">Not Configured</p>
949
- <button class="btn-ghost" onclick="ConfigModal.open('${id}')">Configure Chart</button>
950
- </div>
 
 
951
  </div>
952
  `;
953
  return div;
954
  },
955
 
956
- // --- GLOBAL CONTROLS ---
957
  onDateChange() {
958
  this.state.date = document.getElementById('globalDate').value;
959
  this.saveToStorage();
960
- // Reload all charts
961
- Object.keys(this.state.charts).forEach(id => {
962
- this.state.charts[id].lastData = null; // Clear cache
963
- ChartLogic.plot(id);
964
- });
965
- Toast.show(`Date changed to ${this.state.date}`);
966
  },
967
 
968
- setMode(mode) {
969
- this.state.mode = mode;
970
  this.updateModeUI();
971
  this.saveToStorage();
972
 
973
  Object.keys(this.state.charts).forEach(id => {
974
- if(mode === 'LIVE') ChartLogic.startLive(id);
975
  else ChartLogic.stopLive(id);
976
  });
977
- Toast.show(`Switched to ${mode === 'LIVE' ? 'Live Auto-Refresh' : 'Historical Mode'}`);
978
  },
979
 
980
  updateModeUI() {
981
- document.getElementById('modeHist').classList.toggle('active', this.state.mode === 'HIST');
982
- document.getElementById('modeLive').classList.toggle('active', this.state.mode === 'LIVE');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
983
  }
984
  };
985
 
986
  /**
987
- * --- CHART LOGIC & DATA ---
988
  */
989
  const ChartLogic = {
990
-
991
  async plot(id, isUpdate=false) {
992
  const chartData = GlobalApp.state.charts[id];
993
- if (!chartData) return;
994
  const cfg = chartData.config;
995
-
996
- if (!cfg || !cfg.root) return; // Not configured
997
-
998
- // UI Header Update
999
- const titleEl = document.querySelector(`#card-${id} .card-title`);
1000
- const dotClass = GlobalApp.state.mode === 'LIVE' ? 'live' : 'hist';
1001
- if(titleEl) {
1002
- titleEl.innerHTML = `
1003
- <span class="status-dot ${dotClass}"></span>
1004
- ${cfg.root} <span style="color:var(--accent); margin-left:4px;">${cfg.expiry}</span>
1005
- <span class="card-meta">${cfg.instrument} · ${cfg.timeframe}</span>
1006
- `;
1007
- }
1008
-
1009
- // Ensure DOM exists
1010
- const body = document.getElementById(`body-${id}`);
1011
- if (!body) return;
1012
 
1013
- // Create Canvas if missing
1014
- if (!body.querySelector('canvas')) {
1015
- body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1016
- }
1017
 
1018
  try {
1019
- let data;
 
 
 
 
 
 
 
 
 
 
 
 
 
1020
 
1021
- // Use Cache if simple redraw and not live update
1022
- if (!isUpdate && chartData.lastData) {
1023
- data = chartData.lastData;
1024
- } else {
1025
- // Fetch
1026
- if(isUpdate && titleEl) titleEl.querySelector('.status-dot').classList.add('retry');
1027
-
1028
- const res = await fetch('/api/fetch_series', {
1029
- method: 'POST',
1030
- headers: {'Content-Type': 'application/json'},
1031
- body: JSON.stringify({
1032
- date: GlobalApp.state.date,
1033
- root: cfg.root,
1034
- expiry: cfg.expiry,
1035
- instrument: cfg.instrument,
1036
- timeframe: cfg.timeframe,
1037
- start_time: cfg.start,
1038
- end_time: cfg.end
1039
- })
1040
- });
1041
- data = await res.json();
1042
-
1043
- // Update Cache
1044
- if (data.labels && data.labels.length > 0) {
1045
- chartData.lastData = data;
1046
- }
1047
- }
1048
-
1049
- if(data.error || !data.labels || data.labels.length === 0) {
1050
- if(GlobalApp.state.mode !== 'LIVE') {
1051
- if(chartData.instance) chartData.instance.destroy();
1052
- chartData.instance = null;
1053
- body.innerHTML = '<div class="empty-state">No Data Available</div>';
1054
- }
1055
  return;
1056
  }
1057
 
1058
- // Success Visuals
1059
- if(titleEl) titleEl.querySelector('.status-dot').className = `status-dot ${dotClass}`;
1060
-
1061
- // Process Data
1062
- const datasets = cfg.series.map(s => {
1063
- const computed = data.labels.map((_, i) => {
1064
- const P = data.P[i] || 0, V = data.V[i] || 0, OI = data.OI[i] || 0;
1065
- const B = data.B[i] || 0, S = data.S[i] || 0;
1066
- let f = s.formula
1067
- .replace(/\$OI/g, OI).replace(/\$P/g, P).replace(/\$V/g, V)
1068
- .replace(/\$B/g, B).replace(/\$S/g, S);
1069
- try { return new Function('return ' + f)(); } catch { return null; }
1070
- });
1071
-
1072
- return {
1073
- label: s.label,
1074
- data: computed,
1075
- borderColor: s.color,
1076
- backgroundColor: s.color,
1077
- borderWidth: 1.5,
1078
- pointRadius: 0,
1079
- pointHoverRadius: 4,
1080
- yAxisID: s.axis === 'left' ? 'y' : 'y1',
1081
- tension: 0.1
1082
- };
1083
- });
1084
-
1085
- this.renderCanvas(id, data.labels, datasets);
1086
 
1087
  } catch (e) {
1088
- console.error("Plot Error", e);
 
1089
  }
1090
  },
1091
 
1092
- renderCanvas(id, labels, datasets) {
1093
- const ctx = document.getElementById(`canvas-${id}`);
1094
- if (!ctx) return;
1095
-
1096
  const chartData = GlobalApp.state.charts[id];
 
 
 
 
 
 
 
 
 
1097
 
1098
- // If chart exists, update data (prevent flicker)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1099
  if (chartData.instance) {
1100
- chartData.instance.data.labels = labels;
1101
  chartData.instance.data.datasets = datasets;
1102
- chartData.instance.update('none');
1103
  } else {
1104
- // Create New
1105
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
1106
- Chart.defaults.color = '#8b949e';
1107
  chartData.instance = new Chart(ctx.getContext('2d'), {
1108
  type: 'line',
1109
- data: { labels, datasets },
1110
  options: {
1111
  responsive: true,
1112
- maintainAspectRatio: false,
1113
  animation: false,
1114
  interaction: { mode: 'index', intersect: false },
1115
  plugins: {
1116
- legend: { labels: { usePointStyle: true, boxWidth: 6 } },
1117
  tooltip: {
1118
- backgroundColor: 'rgba(22, 27, 34, 0.95)',
1119
- titleColor:'#e6edf3',
1120
- bodyColor:'#e6edf3',
1121
- borderColor:'#30363d',
1122
- borderWidth:1,
1123
- padding: 10,
1124
- cornerRadius: 6
1125
  }
1126
  },
1127
  scales: {
1128
- x: {
1129
- grid: { color: '#30363d', tickLength: 0, drawBorder: false, lineWidth: 0.5 },
1130
- ticks: { maxTicksLimit: 8, maxRotation: 0 }
1131
- },
1132
- y: {
1133
- type: 'linear', display: true, position: 'left',
1134
- grid: { color: '#30363d', borderDash: [2, 2], drawBorder: false },
1135
- },
1136
- y1: {
1137
- type: 'linear', display: hasRight, position: 'right',
1138
- grid: { display: false, drawBorder: false }
1139
- }
1140
  }
1141
  }
1142
  });
1143
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1144
  },
1145
 
1146
  startLive(id) {
1147
  this.stopLive(id);
 
1148
  this.plot(id);
1149
- GlobalApp.state.charts[id].interval = setInterval(() => {
1150
- this.plot(id, true);
1151
- }, 15000);
1152
  },
1153
 
1154
  stopLive(id) {
1155
  const c = GlobalApp.state.charts[id];
1156
- if(c && c.interval) {
1157
- clearInterval(c.interval);
1158
- c.interval = null;
1159
- }
1160
  }
1161
  };
1162
 
1163
  /**
1164
- * --- CONFIGURATION MODAL ---
1165
  */
1166
  const ConfigModal = {
1167
- currentId: null,
1168
  isGlobal: false,
1169
 
1170
  async open(id) {
1171
- this.currentId = id;
1172
  this.isGlobal = false;
1173
-
1174
- const modal = document.querySelector('.modal');
1175
- document.getElementById('cfgModalTitle').textContent = "Configure Chart";
1176
  document.getElementById('configModalOverlay').style.display = 'flex';
1177
- // Trigger Animation
1178
- setTimeout(() => document.getElementById('configModalOverlay').classList.add('active'), 10);
1179
-
1180
  document.getElementById('cfgForm').style.display = 'block';
1181
- document.getElementById('cfgLoader').style.display = 'none';
1182
-
1183
- const c = GlobalApp.state.charts[id].config;
1184
 
 
1185
  await this.loadRoots(c.root);
1186
 
1187
  document.getElementById('cfgTF').value = c.timeframe || '1min';
1188
  document.getElementById('cfgStart').value = c.start || '09:15';
1189
  document.getElementById('cfgEnd').value = c.end || '15:30';
1190
-
1191
- const series = c.series && c.series.length ? c.series : GlobalApp.defaults;
1192
- this.renderSeries(series);
1193
-
1194
  if(c.root) {
1195
- document.getElementById('cfgRoot').value = c.root;
1196
- await this.onRootChange(c.expiry, c.instrument);
1197
  }
1198
  },
1199
 
1200
  openDefaults() {
1201
  this.isGlobal = true;
1202
- document.getElementById('cfgModalTitle').textContent = "Global Default Series";
1203
  document.getElementById('configModalOverlay').style.display = 'flex';
1204
- setTimeout(() => document.getElementById('configModalOverlay').classList.add('active'), 10);
1205
-
1206
- document.getElementById('cfgForm').style.display = 'block';
1207
- // Hacky: hide other fields via CSS or just ignore them. For now, we leave them but they won't save.
1208
  this.renderSeries(GlobalApp.defaults);
1209
  },
1210
 
1211
- close() {
1212
- const overlay = document.getElementById('configModalOverlay');
1213
- overlay.classList.remove('active');
1214
- setTimeout(() => overlay.style.display = 'none', 200);
1215
- },
1216
 
1217
- async loadRoots(preSelect) {
1218
- const rSel = document.getElementById('cfgRoot');
1219
- rSel.innerHTML = '<option>Loading...</option>';
1220
  try {
1221
- const date = GlobalApp.state.date;
1222
- const res = await fetch(`/api/roots?date=${date}`);
1223
- const roots = await res.json();
1224
- rSel.innerHTML = `<option value="">-- Select Symbol --</option>` + roots.map(r => `<option>${r}</option>`).join('');
1225
- if(preSelect && roots.includes(preSelect)) rSel.value = preSelect;
1226
- } catch(e) { rSel.innerHTML = '<option>Error</option>'; }
1227
  },
1228
 
1229
  async onRootChange(preExp, preInst) {
1230
  const root = document.getElementById('cfgRoot').value;
1231
- const date = GlobalApp.state.date;
1232
- const eSel = document.getElementById('cfgExp');
1233
- eSel.innerHTML = '<option>Loading...</option>';
1234
 
1235
- const res = await fetch(`/api/expiries?date=${date}&root=${root}`);
1236
- const exps = await res.json();
1237
- eSel.innerHTML = `<option value="">-- Select Expiry --</option>` + exps.map(e => `<option>${e}</option>`).join('');
1238
 
1239
- if(preExp && exps.includes(preExp)) {
1240
- eSel.value = preExp;
1241
- await this.onExpChange(preInst);
1242
- } else {
1243
- const auto = await fetch(`/api/auto_config?date=${date}&root=${root}`).then(r=>r.json());
1244
- if(auto.current) {
1245
- eSel.value = auto.current.expiry;
1246
- await this.onExpChange(auto.current.instrument);
1247
- }
1248
- }
1249
  },
1250
 
1251
  async onExpChange(preInst) {
1252
  const root = document.getElementById('cfgRoot').value;
1253
  const exp = document.getElementById('cfgExp').value;
1254
- const date = GlobalApp.state.date;
1255
- const iSel = document.getElementById('cfgInst');
1256
 
1257
- iSel.innerHTML = '<option>Loading...</option>';
1258
- const res = await fetch(`/api/instruments?date=${date}&root=${root}&expiry=${exp}`);
1259
- const insts = await res.json();
1260
- iSel.innerHTML = `<option value="">-- Select Instrument --</option>` + insts.map(i => `<option>${i}</option>`).join('');
1261
 
1262
- if(preInst && insts.includes(preInst)) iSel.value = preInst;
1263
- else if(insts.length > 0) iSel.value = insts[0];
1264
  },
1265
 
1266
  renderSeries(list) {
1267
- const container = document.getElementById('cfgSeriesList');
1268
- container.innerHTML = '';
1269
  list.forEach(s => {
1270
- const div = document.createElement('div');
1271
- div.className = 'series-row';
1272
- div.innerHTML = `
1273
- <input type="color" value="${s.color}" class="color-picker s-color">
1274
- <input type="text" value="${s.formula}" class="s-formula" placeholder="$P">
1275
- <input type="text" value="${s.label}" class="s-label" placeholder="Label">
1276
  <div class="select-wrapper">
1277
  <select class="s-axis" style="min-width:50px; padding-right:15px;">
1278
  <option value="left" ${s.axis==='left'?'selected':''}>L</option>
1279
  <option value="right" ${s.axis==='right'?'selected':''}>R</option>
1280
  </select>
1281
  </div>
1282
- <button class="icon-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1283
  `;
1284
- container.appendChild(div);
1285
  });
1286
  },
1287
 
1288
  addSeriesRow() {
1289
- const container = document.getElementById('cfgSeriesList');
1290
- const div = document.createElement('div');
1291
- div.className = 'series-row';
1292
- div.innerHTML = `
1293
- <input type="color" value="#ffffff" class="color-picker s-color">
1294
- <input type="text" value="$P" class="s-formula">
1295
- <input type="text" value="New" class="s-label">
1296
- <div class="select-wrapper">
1297
- <select class="s-axis" style="min-width:50px; padding-right:15px;">
1298
- <option value="left">L</option><option value="right">R</option>
1299
- </select>
1300
- </div>
1301
- <button class="icon-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1302
- `;
1303
- container.appendChild(div);
1304
- div.scrollIntoView({ behavior: 'smooth' });
1305
  },
1306
 
1307
- save() {
1308
- const rows = document.querySelectorAll('#cfgSeriesList .series-row');
1309
- const newSeries = Array.from(rows).map(r => ({
1310
- color: r.querySelector('.s-color').value,
1311
- formula: r.querySelector('.s-formula').value,
1312
- label: r.querySelector('.s-label').value,
1313
  axis: r.querySelector('.s-axis').value
1314
  }));
 
1315
 
 
 
1316
  if(this.isGlobal) {
1317
- GlobalApp.defaults = newSeries;
1318
- Toast.show("Global Defaults Updated", "success");
1319
  } else {
1320
  const root = document.getElementById('cfgRoot').value;
1321
- const exp = document.getElementById('cfgExp').value;
1322
- const inst = document.getElementById('cfgInst').value;
1323
 
1324
- if(!root || !exp || !inst) { Toast.show("Please select all symbol details", "warning"); return; }
1325
-
1326
- const config = {
1327
- root, expiry: exp, instrument: inst,
1328
  timeframe: document.getElementById('cfgTF').value,
1329
  start: document.getElementById('cfgStart').value,
1330
  end: document.getElementById('cfgEnd').value,
1331
- series: newSeries
1332
  };
1333
-
1334
- GlobalApp.state.charts[this.currentId].config = config;
1335
- // Clear cache to force redraw with new config
1336
- GlobalApp.state.charts[this.currentId].lastData = null;
 
 
1337
 
1338
  GlobalApp.saveToStorage();
1339
- ChartLogic.plot(this.currentId);
1340
- Toast.show("Chart Configured", "success");
1341
  }
1342
  this.close();
1343
  }
 
207
 
208
 
209
  # ==========================================
210
+ # 5. FRONTEND TEMPLATE (MODERN UI)
211
  # ==========================================
212
 
213
  HTML_TEMPLATE = """
 
215
  <html lang="en">
216
  <head>
217
  <meta charset="UTF-8">
218
+ <title>DepthChain Pro</title>
219
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
220
  <link rel="preconnect" href="https://fonts.googleapis.com">
221
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
222
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
223
  <style>
224
+ /* --- CORE VARIABLES --- */
225
  :root {
226
+ /* macOS / GitHub Dimmed Palette */
227
+ --bg-body: #1c2128;
228
+ --bg-nav: rgba(28, 33, 40, 0.85);
229
+ --bg-card: #22272e;
230
+ --bg-card-hover: #2d333b;
231
+ --bg-input: #373e47;
232
 
233
+ --border: #444c56;
234
+ --border-subtle: #373e47;
235
+
236
+ --text-primary: #adbac7;
237
+ --text-secondary: #768390;
238
+
239
+ --accent-blue: #539bf5;
240
+ --accent-green: #57ab5a;
241
+ --accent-red: #e5534b;
242
+ --accent-orange: #d19a66;
243
+
244
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
245
+ --shadow-lg: 0 12px 24px rgba(0,0,0,0.4);
246
 
 
 
 
 
 
 
 
247
  --radius-md: 8px;
248
+ --radius-lg: 12px;
249
+
250
+ --font-main: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
251
+ --font-mono: 'JetBrains Mono', monospace;
252
  }
253
 
254
+ /* --- RESET & LAYOUT --- */
255
+ * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
256
 
257
  body {
258
  background: var(--bg-body);
259
  color: var(--text-primary);
260
+ font-family: var(--font-main);
 
 
261
  height: 100vh;
262
+ overflow: hidden; /* Prevent body scroll */
263
+ display: flex;
264
+ flex-direction: column;
265
  }
266
 
267
+ /* --- UI COMPONENTS --- */
268
+ /* Custom Scrollbar */
269
  ::-webkit-scrollbar { width: 8px; height: 8px; }
270
  ::-webkit-scrollbar-track { background: transparent; }
271
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
272
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
273
+
274
+ /* Buttons */
275
+ button {
276
+ background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
277
+ padding: 6px 12px; border-radius: var(--radius-md); font-size: 13px; font-weight: 500;
278
+ cursor: pointer; transition: all 0.2s ease;
 
 
 
 
 
 
 
 
279
  }
280
+ button:hover { background: var(--border); border-color: var(--text-secondary); color: white; }
281
 
282
+ .btn-primary { background: var(--accent-blue); color: white; border: none; }
283
+ .btn-primary:hover { background: #4a8bda; border: none; }
284
+
285
+ .btn-ghost { background: transparent; border: 1px solid var(--border-subtle); color: var(--text-secondary); }
286
+ .btn-ghost:hover { border-color: var(--accent-blue); color: var(--accent-blue); background: rgba(83, 155, 245, 0.1); }
287
+
288
+ .btn-danger { color: var(--accent-red); border-color: var(--border-subtle); background: transparent; }
289
+ .btn-danger:hover { background: rgba(229, 83, 75, 0.15); border-color: var(--accent-red); }
290
+
291
+ /* Custom Select */
292
+ .select-wrapper {
293
+ position: relative; display: inline-block;
294
  }
295
+ .select-wrapper select {
296
+ appearance: none; background: var(--bg-input); border: 1px solid var(--border);
297
+ color: var(--text-primary); padding: 6px 30px 6px 12px; border-radius: var(--radius-md);
298
+ font-family: var(--font-main); font-size: 13px; cursor: pointer; min-width: 120px;
 
 
 
299
  }
300
+ .select-wrapper::after {
301
+ content: '▼'; position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
302
+ font-size: 10px; color: var(--text-secondary); pointer-events: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  }
304
+ .select-wrapper select:hover { border-color: var(--text-secondary); }
305
+
306
+ /* Inputs */
307
+ input[type="text"], input[type="time"] {
308
+ background: var(--bg-body); border: 1px solid var(--border); color: var(--text-primary);
309
+ padding: 8px; border-radius: var(--radius-md); font-family: var(--font-mono); font-size: 12px;
310
  }
311
+ input:focus { border-color: var(--accent-blue); }
312
 
313
+ /* --- HEADER --- */
314
+ .navbar {
315
+ height: 60px; background: var(--bg-nav); backdrop-filter: blur(12px);
316
+ border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
317
+ padding: 0 20px; z-index: 100; flex-shrink: 0;
 
 
 
 
 
 
318
  }
319
+ .brand { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: -0.5px; display: flex; align-items: center; gap: 8px; }
320
+ .brand span { color: var(--accent-blue); background: rgba(83, 155, 245, 0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
321
+
322
+ .nav-controls { display: flex; gap: 12px; align-items: center; }
 
 
 
323
 
324
+ /* Layout Toggle */
325
+ .layout-toggle { display: flex; background: var(--bg-input); padding: 2px; border-radius: var(--radius-md); border: 1px solid var(--border); }
326
+ .layout-btn {
327
+ background: transparent; border: none; padding: 4px 8px; border-radius: 4px; color: var(--text-secondary); font-size: 12px;
 
 
 
 
 
 
 
328
  }
329
+ .layout-btn.active { background: var(--bg-card); color: white; shadow: var(--shadow-sm); }
330
+ .layout-btn:hover:not(.active) { background: transparent; color: white; }
331
 
332
+ /* Mode Switch */
333
+ .mode-switch {
334
+ display: flex; align-items: center; gap: 8px; background: var(--bg-input);
335
+ padding: 4px 12px; border-radius: var(--radius-md); border: 1px solid var(--border); font-size: 12px;
 
 
 
 
 
 
 
336
  }
337
+ .status-led { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); transition: all 0.3s; }
338
+ .status-led.live { background: var(--accent-green); box-shadow: 0 0 8px var(--accent-green); }
339
 
340
+ /* --- MAIN GRID LAYOUT --- */
341
+ .grid-container {
342
  flex: 1;
343
+ display: flex;
344
+ padding: 10px;
345
+ gap: 10px;
346
+ overflow: hidden; /* Container doesn't scroll, columns do */
 
347
  }
348
 
349
  .column {
350
  flex: 1;
351
  display: flex;
352
  flex-direction: column;
353
+ gap: 12px;
354
+ overflow-y: auto; /* Independent scrolling */
355
+ padding-right: 4px; /* Space for scrollbar */
356
+ min-width: 320px; /* Prevent crushing on small screens */
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  }
 
358
 
359
  /* --- CHART CARD --- */
360
  .chart-card {
361
+ background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg);
362
+ display: flex; flex-direction: column;
363
+ position: relative; transition: border-color 0.2s;
364
+ flex-grow: 1; /* Expand to fill space */
365
+ min-height: 340px; /* Minimum useful height */
366
+ box-shadow: var(--shadow-sm);
 
 
 
 
367
  }
368
+ .chart-card:hover { border-color: var(--border); }
369
 
370
  .card-header {
371
+ padding: 10px 14px; display: flex; justify-content: space-between; align-items: center;
372
+ border-bottom: 1px solid var(--border-subtle); height: 44px;
 
 
 
 
 
 
373
  }
374
+ .card-info { display: flex; flex-direction: column; line-height: 1.1; }
375
+ .card-title { font-weight: 600; font-size: 13px; color: white; }
376
+ .card-meta { font-family: var(--font-mono); font-size: 10px; color: var(--text-secondary); margin-top: 2px; }
377
 
378
+ .card-actions { opacity: 0; transition: opacity 0.2s; display: flex; gap: 6px; }
379
+ .chart-card:hover .card-actions { opacity: 1; }
380
+ .icon-btn { padding: 4px 8px; font-size: 14px; line-height: 1; color: var(--text-secondary); background: transparent; border: none; }
381
+ .icon-btn:hover { color: white; background: var(--bg-input); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
  .card-body {
384
+ flex: 1; position: relative; padding: 10px;
385
+ display: flex; align-items: center; justify-content: center;
 
386
  }
387
 
388
+ .empty-state { text-align: center; color: var(--text-secondary); }
389
+ .empty-state p { margin-bottom: 12px; font-size: 13px; }
390
+
391
+ /* Add Button in Column */
392
+ .col-add-btn {
393
+ padding: 15px; border: 1px dashed var(--border); border-radius: var(--radius-lg);
394
+ color: var(--text-secondary); text-align: center; cursor: pointer; font-size: 13px;
395
+ margin-top: auto; /* Push to bottom if space allows */
396
+ transition: all 0.2s; min-height: 50px; display: flex; align-items: center; justify-content: center;
397
  }
398
+ .col-add-btn:hover { border-color: var(--accent-blue); color: var(--accent-blue); background: rgba(83, 155, 245, 0.05); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
  /* --- MODAL --- */
401
  .modal-overlay {
402
+ position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
403
+ z-index: 500; display: none; align-items: center; justify-content: center;
 
 
404
  }
 
 
405
  .modal {
406
+ background: #2d333b; border: 1px solid var(--border); border-radius: var(--radius-lg);
407
  width: 480px; max-width: 90%; max-height: 85vh; display: flex; flex-direction: column;
408
+ box-shadow: var(--shadow-lg); animation: modalIn 0.2s ease-out;
 
409
  }
410
+ @keyframes modalIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
411
 
412
+ .modal-head { padding: 16px; border-bottom: 1px solid var(--border); font-weight: 600; display: flex; justify-content: space-between; align-items: center; color: white; }
 
 
 
 
413
  .modal-body { padding: 20px; overflow-y: auto; }
414
+ .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
415
 
416
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
417
+ .form-group label { display: block; margin-bottom: 6px; color: var(--text-secondary); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
 
418
 
419
+ .series-row { display: grid; grid-template-columns: 32px 1fr 1fr 50px 30px; gap: 8px; margin-bottom: 8px; align-items: center; }
420
+ .color-picker { width: 100%; height: 30px; padding: 0; border: none; background: none; cursor: pointer; }
 
 
 
421
 
422
+ /* --- TOAST --- */
423
+ .toast {
424
+ position: fixed; bottom: 24px; right: 24px;
425
+ background: var(--bg-card); border: 1px solid var(--border); border-left: 4px solid var(--accent-blue);
426
+ padding: 12px 20px; border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
427
+ color: white; font-size: 13px; font-weight: 500;
428
+ transform: translateY(100px); opacity: 0; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
429
+ z-index: 1000;
430
  }
431
+ .toast.show { transform: translateY(0); opacity: 1; }
432
 
433
  </style>
434
  </head>
435
  <body>
436
 
437
+ <!-- TOP NAVIGATION -->
438
+ <div class="navbar">
439
+ <div class="brand">
440
+ DepthChain <span>PRO</span>
441
+ </div>
442
 
443
+ <div class="nav-controls">
444
  <!-- Date Selector -->
445
  <div class="select-wrapper">
446
  <select id="globalDate" onchange="GlobalApp.onDateChange()"></select>
 
447
  </div>
448
+
449
  <!-- Mode Toggle -->
450
+ <div class="mode-switch" title="Toggle Live Mode">
451
+ <span id="liveIndicator" class="status-led"></span>
452
+ <select id="globalMode" onchange="GlobalApp.onModeChange()" style="background:transparent; border:none; padding:0; width: auto; color: inherit; appearance: none; cursor:pointer;">
453
+ <option value="HIST">Historical</option>
454
+ <option value="LIVE">Live Monitor</option>
455
+ </select>
456
  </div>
457
 
458
+ <div style="width: 1px; height: 24px; background: var(--border); margin: 0 8px;"></div>
459
+
460
+ <!-- Layout Control -->
461
+ <div class="layout-toggle">
462
+ <button class="layout-btn" onclick="GlobalApp.setLayout(1)" id="btn-col-1">1 Col</button>
463
+ <button class="layout-btn" onclick="GlobalApp.setLayout(2)" id="btn-col-2">2 Cols</button>
464
+ <button class="layout-btn active" onclick="GlobalApp.setLayout(3)" id="btn-col-3">3 Cols</button>
 
465
  </div>
466
 
467
+ <button onclick="ConfigModal.openDefaults()">Defaults</button>
468
  <button class="btn-danger" onclick="GlobalApp.resetApp()">Reset</button>
469
  </div>
470
  </div>
471
 
472
+ <!-- MAIN CONTENT -->
473
+ <div class="grid-container" id="gridContainer">
474
+ <!-- Columns are injected here -->
475
  </div>
476
 
477
+ <!-- CONFIG MODAL -->
 
 
 
478
  <div class="modal-overlay" id="configModalOverlay">
479
  <div class="modal">
480
  <div class="modal-head">
 
482
  <button class="icon-btn" onclick="ConfigModal.close()">✕</button>
483
  </div>
484
  <div class="modal-body">
485
+ <div id="cfgLoader" style="display:none; text-align:center; padding:20px; color:var(--text-secondary);">Loading Metadata...</div>
 
486
 
 
487
  <div id="cfgForm">
488
+ <div class="form-grid">
489
  <div class="form-group">
490
+ <label>Symbol Root</label>
491
+ <div class="select-wrapper" style="width:100%">
492
  <select id="cfgRoot" onchange="ConfigModal.onRootChange()" style="width:100%"></select>
 
493
  </div>
494
  </div>
495
  <div class="form-group">
496
  <label>Expiry</label>
497
+ <div class="select-wrapper" style="width:100%">
498
  <select id="cfgExp" onchange="ConfigModal.onExpChange()" style="width:100%"></select>
 
499
  </div>
500
  </div>
501
  </div>
502
+
503
  <div class="form-group">
504
  <label>Instrument</label>
505
+ <div class="select-wrapper" style="width:100%">
506
  <select id="cfgInst" style="width:100%"></select>
 
507
  </div>
508
  </div>
509
 
510
+ <div class="form-grid" style="margin-top:12px;">
511
  <div class="form-group">
512
  <label>Timeframe</label>
513
+ <div class="select-wrapper" style="width:100%">
514
  <select id="cfgTF" style="width:100%">
515
  <option value="1min">1 Minute</option>
516
  <option value="3min">3 Minutes</option>
 
519
  <option value="30min">30 Minutes</option>
520
  <option value="1hour">1 Hour</option>
521
  </select>
 
522
  </div>
523
  </div>
524
  <div class="form-group">
525
  <label>Range (HH:MM)</label>
526
  <div style="display:flex; gap:8px;">
527
+ <input type="time" id="cfgStart" value="09:15" style="width:100%">
528
+ <input type="time" id="cfgEnd" value="15:30" style="width:100%">
529
  </div>
530
  </div>
531
  </div>
532
 
533
+ <div style="border-top: 1px solid var(--border); margin: 16px 0;"></div>
534
 
535
  <div class="form-group">
536
+ <div style="display:flex; justify-content:space-between; margin-bottom:8px;">
537
+ <label>Data Series</label>
538
+ <span style="font-size:10px; color:var(--text-secondary);">$P, $V, $OI, $B, $S</span>
539
+ </div>
540
  <div id="cfgSeriesList"></div>
541
+ <button class="btn-ghost" style="width:100%; margin-top:8px; font-size:11px;" onclick="ConfigModal.addSeriesRow()">+ Add Line</button>
542
  </div>
543
  </div>
544
  </div>
545
  <div class="modal-foot">
546
+ <button class="btn-ghost" onclick="ConfigModal.close()">Cancel</button>
547
  <button class="btn-primary" onclick="ConfigModal.save()">Save Configuration</button>
548
  </div>
549
  </div>
550
  </div>
551
 
552
+ <!-- RESTORE SESSION MODAL -->
553
  <div class="modal-overlay" id="restoreModal">
554
+ <div class="modal" style="width: 380px;">
555
  <div class="modal-head">Restore Session?</div>
556
+ <div class="modal-body">
557
+ We found a saved workspace layout from your previous session. Would you like to restore it?
558
  </div>
559
  <div class="modal-foot">
560
+ <button class="btn-danger" onclick="GlobalApp.initNew()">Start Fresh</button>
561
  <button class="btn-primary" onclick="GlobalApp.loadFromStorage()">Restore</button>
562
  </div>
563
  </div>
564
  </div>
565
 
566
+ <!-- TOAST -->
567
+ <div id="toast" class="toast">Settings Saved</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
 
569
+ <script>
570
  /**
571
  * --- GLOBAL STATE MANAGER ---
572
  */
 
574
  state: {
575
  date: '',
576
  mode: 'HIST',
577
+ columns: 3,
578
+ layout: [[], [], []], // Array of arrays of chartIDs
579
+ charts: {} // id -> { config, instance, interval, lastData }
580
  },
 
581
  defaults: [
582
+ { formula: '$P', color: '#539bf5', label: 'Price', axis: 'left' },
583
+ { formula: '$OI', color: '#57ab5a', label: 'OI', axis: 'right' }
 
584
  ],
585
 
586
  async init() {
587
+ // 1. Load Dates
588
  await this.loadDates();
589
 
590
+ // 2. Check Storage
591
+ if (localStorage.getItem('dc_pro_state')) {
592
+ document.getElementById('restoreModal').style.display = 'flex';
 
 
593
  } else {
594
  this.initNew();
595
  }
 
600
  const res = await fetch('/api/dates');
601
  const dates = await res.json();
602
  const sel = document.getElementById('globalDate');
 
 
603
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
604
 
605
+ if (dates.length > 0) {
606
+ // Determine which date to select (usually first in list from backend)
607
  this.state.date = dates[0];
608
  sel.value = dates[0];
609
  }
610
+ } catch(e) {
611
+ console.error("Dates error", e);
612
+ this.showToast("Failed to load dates", true);
613
+ }
614
  },
615
 
616
  initNew() {
617
+ document.getElementById('restoreModal').style.display = 'none';
618
+ this.setLayout(3); // Default 3 Cols
619
+ // Add one empty chart to first col
620
+ this.addChart(0);
 
 
 
 
 
 
 
 
621
  },
622
 
623
  loadFromStorage() {
624
  try {
625
+ const raw = localStorage.getItem('dc_pro_state');
626
  const saved = JSON.parse(raw);
627
 
628
+ document.getElementById('restoreModal').style.display = 'none';
629
 
630
+ // Validate Date
631
+ const sel = document.getElementById('globalDate');
632
+ if ([...sel.options].some(o => o.value === saved.date)) {
633
+ sel.value = saved.date;
634
+ this.state.date = saved.date;
635
+ } else {
636
+ // Saved date not in history anymore? Use default.
637
+ this.state.date = sel.value;
 
638
  }
639
+
640
+ this.state.mode = saved.mode;
641
+ document.getElementById('globalMode').value = saved.mode;
642
  this.updateModeUI();
 
643
 
644
+ // Rebuild Layout logic
645
+ // We need to set columns first
646
+ this.state.columns = saved.columns;
647
+ this.updateLayoutBtns();
648
+
649
+ // Populate charts map
650
+ saved.layout.forEach((colCharts, colIdx) => {
651
+ if(!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
652
+ colCharts.forEach(c => {
653
+ this.state.charts[c.id] = {
654
+ col: colIdx,
655
+ config: c.config,
656
+ instance: null,
657
+ interval: null,
658
+ lastData: null
659
+ };
660
+ this.state.layout[colIdx].push(c.id);
661
+ });
662
  });
663
 
664
+ this.renderGrid(true); // render grid and plot all
665
 
666
  } catch(e) {
667
  console.error("Load failed", e);
 
668
  this.initNew();
669
  }
670
  },
671
 
672
  saveToStorage() {
673
+ const minimalLayout = this.state.layout.map(col => {
674
+ return col.map(id => ({
 
 
675
  id: id,
 
676
  config: this.state.charts[id].config
677
+ }));
678
  });
679
 
680
  const dump = {
681
  date: this.state.date,
682
  mode: this.state.mode,
683
  columns: this.state.columns,
684
+ layout: minimalLayout
685
  };
686
+ localStorage.setItem('dc_pro_state', JSON.stringify(dump));
687
  },
688
 
689
+ // --- LAYOUT & COLUMNS ---
690
+
 
 
 
 
 
691
  setLayout(cols) {
692
+ if(cols === this.state.columns) return;
693
+
694
+ // Collect all existing chart IDs in a flat list
695
+ const allCharts = this.state.layout.flat();
696
 
697
+ // Reset layout structure
698
  this.state.columns = cols;
699
+ this.state.layout = Array.from({length: cols}, () => []);
700
+
701
+ // Round-Robin Redistribution
702
+ allCharts.forEach((id, index) => {
703
+ const targetCol = index % cols;
704
+ this.state.layout[targetCol].push(id);
705
+ if(this.state.charts[id]) {
706
+ this.state.charts[id].col = targetCol;
707
+ }
708
  });
709
 
710
+ this.updateLayoutBtns();
711
+ this.renderGrid(false); // Do not re-fetch, just move
712
+ this.saveToStorage();
713
+ },
714
 
715
+ updateLayoutBtns() {
716
+ [1, 2, 3].forEach(n => {
717
+ const btn = document.getElementById(`btn-col-${n}`);
718
+ if(n === this.state.columns) btn.classList.add('active');
719
+ else btn.classList.remove('active');
 
 
 
 
 
 
 
 
 
 
720
  });
 
 
721
  },
722
 
723
+ renderGrid(shouldFetch = false) {
724
  const container = document.getElementById('gridContainer');
725
+
726
+ // We need to detach existing charts to preserve their state/canvas if possible,
727
+ // OR simply re-create them. Chart.js canvases don't like moving in DOM.
728
+ // Best approach: Clear Container -> Create Columns -> Create Card Shells -> Re-init Chart.js
729
+
730
+ // Destroy old instances strictly to prevent memory leaks before rebuilding DOM
731
+ Object.values(this.state.charts).forEach(c => {
732
+ if(c.instance) { c.instance.destroy(); c.instance = null; }
733
+ });
734
+
735
+ container.innerHTML = '';
736
 
737
  for (let i = 0; i < this.state.columns; i++) {
738
  const colDiv = document.createElement('div');
739
  colDiv.className = 'column';
740
  colDiv.id = `col-${i}`;
741
+
742
+ // Add Charts
743
+ const chartIds = this.state.layout[i] || [];
744
+ chartIds.forEach(id => {
745
+ colDiv.appendChild(this.createChartElement(id));
746
+ // Defer render to allow DOM to calculate size
747
+ setTimeout(() => {
748
+ const c = this.state.charts[id];
749
+ if(c.config && c.config.root) {
750
+ // If we have cached data and shouldn't force fetch, use cache
751
+ if(!shouldFetch && c.lastData) {
752
+ ChartLogic.renderFromCache(id);
753
+ } else {
754
+ ChartLogic.plot(id);
755
+ }
756
+ }
757
+ }, 0);
758
+ });
759
+
760
+ // Add "Add Chart" Button at bottom of column
761
  const addBtn = document.createElement('div');
762
+ addBtn.className = 'col-add-btn';
763
  addBtn.innerHTML = '+ Add Chart';
764
  addBtn.onclick = () => this.addChart(i);
 
765
  colDiv.appendChild(addBtn);
766
+
767
  container.appendChild(colDiv);
768
  }
769
  },
770
 
771
+ addChart(colIdx) {
772
+ const id = 'c_' + Date.now() + Math.random().toString(36).substr(2,4);
773
+ this.state.charts[id] = {
774
+ col: colIdx,
775
+ config: {},
776
+ instance: null,
777
+ interval: null
778
+ };
 
779
 
780
+ if (!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
781
+ this.state.layout[colIdx].push(id);
 
 
 
 
 
 
 
 
782
 
783
+ // Append to DOM immediately
784
  const colDiv = document.getElementById(`col-${colIdx}`);
785
+ const btn = colDiv.querySelector('.col-add-btn');
786
+ colDiv.insertBefore(this.createChartElement(id), btn);
787
+
788
+ this.saveToStorage();
 
 
 
 
 
 
 
789
  },
790
 
791
  removeChart(id) {
792
+ if(!confirm("Delete this chart?")) return;
 
 
793
  ChartLogic.stopLive(id);
794
  if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
 
 
795
 
796
+ const c = this.state.charts[id];
797
+ this.state.layout[c.col] = this.state.layout[c.col].filter(x => x !== id);
798
+ delete this.state.charts[id];
799
 
800
+ document.getElementById(`card-${id}`).remove();
801
  this.saveToStorage();
802
  },
803
 
 
805
  const div = document.createElement('div');
806
  div.className = 'chart-card';
807
  div.id = `card-${id}`;
808
+
809
+ const cfg = this.state.charts[id].config || {};
810
+ const title = cfg.root ? `${cfg.root} <span style="color:var(--text-secondary)">${cfg.expiry}</span>` : 'Empty Chart';
811
+ const sub = cfg.root ? `${cfg.instrument} • ${cfg.timeframe}` : '';
812
+
813
  div.innerHTML = `
814
  <div class="card-header">
815
+ <div class="card-info" id="head-${id}">
816
+ <div class="card-title">${title}</div>
817
+ <div class="card-meta">${sub}</div>
818
  </div>
819
+ <div class="card-actions">
820
  <button class="icon-btn" onclick="ConfigModal.open('${id}')">⚙</button>
821
  <button class="icon-btn" onclick="GlobalApp.removeChart('${id}')">✕</button>
822
  </div>
823
  </div>
824
  <div class="card-body" id="body-${id}">
825
+ ${cfg.root ? `<canvas id="canvas-${id}"></canvas>` :
826
+ `<div class="empty-state">
827
+ <p>No Data Configuration</p>
828
+ <button class="btn-ghost" onclick="ConfigModal.open('${id}')">Configure</button>
829
+ </div>`
830
+ }
831
  </div>
832
  `;
833
  return div;
834
  },
835
 
 
836
  onDateChange() {
837
  this.state.date = document.getElementById('globalDate').value;
838
  this.saveToStorage();
839
+ // Refresh all charts
840
+ Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id));
 
 
 
 
841
  },
842
 
843
+ onModeChange() {
844
+ this.state.mode = document.getElementById('globalMode').value;
845
  this.updateModeUI();
846
  this.saveToStorage();
847
 
848
  Object.keys(this.state.charts).forEach(id => {
849
+ if(this.state.mode === 'LIVE') ChartLogic.startLive(id);
850
  else ChartLogic.stopLive(id);
851
  });
 
852
  },
853
 
854
  updateModeUI() {
855
+ const led = document.getElementById('liveIndicator');
856
+ if(this.state.mode === 'LIVE') led.classList.add('live');
857
+ else led.classList.remove('live');
858
+ },
859
+
860
+ resetApp() {
861
+ if(confirm("Factory Reset? This clears all layouts.")) {
862
+ localStorage.removeItem('dc_pro_state');
863
+ location.reload();
864
+ }
865
+ },
866
+
867
+ showToast(msg, isError=false) {
868
+ const t = document.getElementById('toast');
869
+ t.textContent = msg;
870
+ t.style.borderLeftColor = isError ? 'var(--accent-red)' : 'var(--accent-blue)';
871
+ t.classList.add('show');
872
+ setTimeout(() => t.classList.remove('show'), 3000);
873
  }
874
  };
875
 
876
  /**
877
+ * --- CHART LOGIC ---
878
  */
879
  const ChartLogic = {
 
880
  async plot(id, isUpdate=false) {
881
  const chartData = GlobalApp.state.charts[id];
 
882
  const cfg = chartData.config;
883
+ if (!cfg || !cfg.root) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
 
885
+ // UI Prep
886
+ this.updateCardHeader(id, true); // Loading state
 
 
887
 
888
  try {
889
+ const res = await fetch('/api/fetch_series', {
890
+ method: 'POST',
891
+ headers: {'Content-Type': 'application/json'},
892
+ body: JSON.stringify({
893
+ date: GlobalApp.state.date,
894
+ root: cfg.root,
895
+ expiry: cfg.expiry,
896
+ instrument: cfg.instrument,
897
+ timeframe: cfg.timeframe,
898
+ start_time: cfg.start,
899
+ end_time: cfg.end
900
+ })
901
+ });
902
+ const data = await res.json();
903
 
904
+ // Handle Empty
905
+ if(data.error || !data.labels || !data.labels.length) {
906
+ if(!isUpdate) this.renderError(id, "No Data Available");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  return;
908
  }
909
 
910
+ // Save for cache
911
+ chartData.lastData = data;
912
+
913
+ this.renderFromCache(id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
914
 
915
  } catch (e) {
916
+ console.error(e);
917
+ this.renderError(id, "Network Error");
918
  }
919
  },
920
 
921
+ renderFromCache(id) {
 
 
 
922
  const chartData = GlobalApp.state.charts[id];
923
+ const data = chartData.lastData;
924
+ const cfg = chartData.config;
925
+ if(!data) return;
926
+
927
+ // Ensure canvas exists
928
+ const body = document.getElementById(`body-${id}`);
929
+ if(!body.querySelector('canvas')) {
930
+ body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
931
+ }
932
 
933
+ // Re-construct Datasets
934
+ const datasets = cfg.series.map(s => {
935
+ const computed = data.labels.map((_, i) => {
936
+ const P = data.P[i]||0, V = data.V[i]||0, OI = data.OI[i]||0, B = data.B[i]||0, S = data.S[i]||0;
937
+ let val = 0;
938
+ // Simple formula eval
939
+ try {
940
+ let f = s.formula.replace(/\$P/g,P).replace(/\$V/g,V).replace(/\$OI/g,OI).replace(/\$B/g,B).replace(/\$S/g,S);
941
+ val = eval(f); // Safe enough for local constrained input
942
+ } catch(e){ val=null; }
943
+ return val;
944
+ });
945
+ return {
946
+ label: s.label,
947
+ data: computed,
948
+ borderColor: s.color,
949
+ backgroundColor: s.color,
950
+ borderWidth: 1.5,
951
+ pointRadius: 0,
952
+ pointHoverRadius: 4,
953
+ yAxisID: s.axis === 'left' ? 'y' : 'y1',
954
+ tension: 0.1
955
+ };
956
+ });
957
+
958
+ const ctx = document.getElementById(`canvas-${id}`);
959
+ if(!ctx) return;
960
+
961
  if (chartData.instance) {
962
+ chartData.instance.data.labels = data.labels;
963
  chartData.instance.data.datasets = datasets;
964
+ chartData.instance.update('none');
965
  } else {
 
966
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
 
967
  chartData.instance = new Chart(ctx.getContext('2d'), {
968
  type: 'line',
969
+ data: { labels: data.labels, datasets },
970
  options: {
971
  responsive: true,
972
+ maintainAspectRatio: false, // Fits parent flex
973
  animation: false,
974
  interaction: { mode: 'index', intersect: false },
975
  plugins: {
976
+ legend: { labels: { color: '#768390', font: {size:10, family:"'Inter'"} } },
977
  tooltip: {
978
+ backgroundColor: '#2d333b', titleColor:'#adbac7', bodyColor:'#adbac7',
979
+ borderColor:'#444c56', borderWidth:1, padding: 10, cornerRadius: 6
 
 
 
 
 
980
  }
981
  },
982
  scales: {
983
+ x: { grid: { color: '#373e47', tickLength:0 }, ticks: { color: '#768390', maxTicksLimit: 8 } },
984
+ y: { position: 'left', grid: { color: '#373e47' }, ticks: { color: '#768390' } },
985
+ y1: { display: hasRight, position: 'right', grid: { display: false }, ticks: { color: '#768390' } }
 
 
 
 
 
 
 
 
 
986
  }
987
  }
988
  });
989
  }
990
+
991
+ this.updateCardHeader(id, false);
992
+ },
993
+
994
+ updateCardHeader(id, isLoading) {
995
+ const h = document.getElementById(`head-${id}`);
996
+ const c = GlobalApp.state.charts[id].config;
997
+ const color = isLoading ? 'var(--accent-orange)' : 'white';
998
+ h.querySelector('.card-title').innerHTML = `
999
+ ${c.root} <span style="color:var(--text-secondary)">${c.expiry}</span>
1000
+ ${isLoading ? '<span style="font-size:10px; margin-left:5px;">●</span>' : ''}
1001
+ `;
1002
+ h.querySelector('.card-title').style.color = color;
1003
+ },
1004
+
1005
+ renderError(id, msg) {
1006
+ const body = document.getElementById(`body-${id}`);
1007
+ body.innerHTML = `<div class="empty-state"><p>${msg}</p><button class="btn-ghost" onclick="ChartLogic.plot('${id}')">Retry</button></div>`;
1008
  },
1009
 
1010
  startLive(id) {
1011
  this.stopLive(id);
1012
+ if(!GlobalApp.state.charts[id].config.root) return;
1013
  this.plot(id);
1014
+ GlobalApp.state.charts[id].interval = setInterval(() => this.plot(id, true), 15000);
 
 
1015
  },
1016
 
1017
  stopLive(id) {
1018
  const c = GlobalApp.state.charts[id];
1019
+ if(c && c.interval) { clearInterval(c.interval); c.interval = null; }
 
 
 
1020
  }
1021
  };
1022
 
1023
  /**
1024
+ * --- CONFIG MODAL ---
1025
  */
1026
  const ConfigModal = {
1027
+ currId: null,
1028
  isGlobal: false,
1029
 
1030
  async open(id) {
1031
+ this.currId = id;
1032
  this.isGlobal = false;
 
 
 
1033
  document.getElementById('configModalOverlay').style.display = 'flex';
1034
+ document.getElementById('cfgModalTitle').textContent = "Configure Instrument";
 
 
1035
  document.getElementById('cfgForm').style.display = 'block';
 
 
 
1036
 
1037
+ const c = GlobalApp.state.charts[id].config;
1038
  await this.loadRoots(c.root);
1039
 
1040
  document.getElementById('cfgTF').value = c.timeframe || '1min';
1041
  document.getElementById('cfgStart').value = c.start || '09:15';
1042
  document.getElementById('cfgEnd').value = c.end || '15:30';
1043
+
1044
+ this.renderSeries(c.series || GlobalApp.defaults);
1045
+
 
1046
  if(c.root) {
1047
+ document.getElementById('cfgRoot').value = c.root;
1048
+ await this.onRootChange(c.expiry, c.instrument);
1049
  }
1050
  },
1051
 
1052
  openDefaults() {
1053
  this.isGlobal = true;
 
1054
  document.getElementById('configModalOverlay').style.display = 'flex';
1055
+ document.getElementById('cfgModalTitle').textContent = "Global Default Lines";
1056
+ // Hide instrument parts, keep series
1057
+ // For simplicity, we use same modal but ignore top inputs in UI?
1058
+ // Better: just fill defaults in list and ignore other logic.
1059
  this.renderSeries(GlobalApp.defaults);
1060
  },
1061
 
1062
+ close() { document.getElementById('configModalOverlay').style.display = 'none'; },
 
 
 
 
1063
 
1064
+ async loadRoots(pre) {
1065
+ const el = document.getElementById('cfgRoot');
1066
+ el.innerHTML = '<option>Loading...</option>';
1067
  try {
1068
+ const r = await fetch(`/api/roots?date=${GlobalApp.state.date}`);
1069
+ const d = await r.json();
1070
+ el.innerHTML = '<option value="">Select Root</option>' + d.map(x=>`<option>${x}</option>`).join('');
1071
+ if(pre) el.value = pre;
1072
+ } catch(e){ el.innerHTML='<option>Error</option>'; }
 
1073
  },
1074
 
1075
  async onRootChange(preExp, preInst) {
1076
  const root = document.getElementById('cfgRoot').value;
1077
+ const exEl = document.getElementById('cfgExp');
1078
+ exEl.innerHTML = '<option>Loading...</option>';
 
1079
 
1080
+ const r = await fetch(`/api/expiries?date=${GlobalApp.state.date}&root=${root}`);
1081
+ const d = await r.json();
1082
+ exEl.innerHTML = d.map(x=>`<option>${x}</option>`).join('');
1083
 
1084
+ // Auto Config Logic
1085
+ const auto = await fetch(`/api/auto_config?date=${GlobalApp.state.date}&root=${root}`).then(x=>x.json());
1086
+
1087
+ if(preExp && d.includes(preExp)) exEl.value = preExp;
1088
+ else if(auto.current) exEl.value = auto.current.expiry;
1089
+
1090
+ await this.onExpChange(preInst || (auto.current ? auto.current.instrument : null));
 
 
 
1091
  },
1092
 
1093
  async onExpChange(preInst) {
1094
  const root = document.getElementById('cfgRoot').value;
1095
  const exp = document.getElementById('cfgExp').value;
1096
+ const inEl = document.getElementById('cfgInst');
1097
+ inEl.innerHTML = '<option>Loading...</option>';
1098
 
1099
+ const r = await fetch(`/api/instruments?date=${GlobalApp.state.date}&root=${root}&expiry=${exp}`);
1100
+ const d = await r.json();
1101
+ inEl.innerHTML = d.map(x=>`<option>${x}</option>`).join('');
 
1102
 
1103
+ if(preInst && d.includes(preInst)) inEl.value = preInst;
1104
+ else if(d.length) inEl.value = d[0];
1105
  },
1106
 
1107
  renderSeries(list) {
1108
+ const div = document.getElementById('cfgSeriesList');
1109
+ div.innerHTML = '';
1110
  list.forEach(s => {
1111
+ const row = document.createElement('div');
1112
+ row.className = 'series-row';
1113
+ row.innerHTML = `
1114
+ <input type="color" class="color-picker s-col" value="${s.color}">
1115
+ <input type="text" class="s-form" value="${s.formula}" placeholder="$P">
1116
+ <input type="text" class="s-lbl" value="${s.label}" placeholder="Name">
1117
  <div class="select-wrapper">
1118
  <select class="s-axis" style="min-width:50px; padding-right:15px;">
1119
  <option value="left" ${s.axis==='left'?'selected':''}>L</option>
1120
  <option value="right" ${s.axis==='right'?'selected':''}>R</option>
1121
  </select>
1122
  </div>
1123
+ <button class="btn-danger" onclick="this.parentElement.remove()">✕</button>
1124
  `;
1125
+ div.appendChild(row);
1126
  });
1127
  },
1128
 
1129
  addSeriesRow() {
1130
+ this.renderSeries([...this.scrapeSeries(), {formula:'$P', color:'#ffffff', label:'New', axis:'left'}]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1131
  },
1132
 
1133
+ scrapeSeries() {
1134
+ return Array.from(document.querySelectorAll('.series-row')).map(r => ({
1135
+ color: r.querySelector('.s-col').value,
1136
+ formula: r.querySelector('.s-form').value,
1137
+ label: r.querySelector('.s-lbl').value,
 
1138
  axis: r.querySelector('.s-axis').value
1139
  }));
1140
+ },
1141
 
1142
+ save() {
1143
+ const series = this.scrapeSeries();
1144
  if(this.isGlobal) {
1145
+ GlobalApp.defaults = series;
1146
+ GlobalApp.showToast("Defaults Updated");
1147
  } else {
1148
  const root = document.getElementById('cfgRoot').value;
1149
+ if(!root) { alert("Please select a symbol"); return; }
 
1150
 
1151
+ GlobalApp.state.charts[this.currId].config = {
1152
+ root,
1153
+ expiry: document.getElementById('cfgExp').value,
1154
+ instrument: document.getElementById('cfgInst').value,
1155
  timeframe: document.getElementById('cfgTF').value,
1156
  start: document.getElementById('cfgStart').value,
1157
  end: document.getElementById('cfgEnd').value,
1158
+ series
1159
  };
1160
+
1161
+ // Re-render whole chart shell to update titles, then plot
1162
+ const col = GlobalApp.state.charts[this.currId].col;
1163
+ const card = GlobalApp.createChartElement(this.currId);
1164
+ const oldCard = document.getElementById(`card-${this.currId}`);
1165
+ oldCard.replaceWith(card);
1166
 
1167
  GlobalApp.saveToStorage();
1168
+ ChartLogic.plot(this.currId);
1169
+ if(GlobalApp.state.mode === 'LIVE') ChartLogic.startLive(this.currId);
1170
  }
1171
  this.close();
1172
  }