datamk commited on
Commit
d385534
·
verified ·
1 Parent(s): f500c11

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +10 -3
  2. static/index.html +2091 -0
app.py CHANGED
@@ -555,9 +555,16 @@ def health() -> dict[str, str]:
555
  return {"status": "ok"}
556
 
557
 
558
- @app.get("/")
559
- def root() -> dict[str, str]:
560
- return {"service": "NIFTY 50 Forecaster Backend", "status": "ok"}
 
 
 
 
 
 
 
561
 
562
 
563
  @app.get("/dashboard")
 
555
  return {"status": "ok"}
556
 
557
 
558
+ from fastapi.responses import HTMLResponse
559
+
560
+ @app.get("/", response_class=HTMLResponse)
561
+ def root() -> HTMLResponse:
562
+ from pathlib import Path
563
+ static_html_path = Path(__file__).resolve().parent / "static" / "index.html"
564
+ if static_html_path.exists():
565
+ return HTMLResponse(content=static_html_path.read_text(encoding="utf-8"))
566
+ return HTMLResponse(content="<h1>NIFTY 50 Forecaster Backend is running. Frontend static/index.html not found.</h1>", status_code=404)
567
+
568
 
569
 
570
  @app.get("/dashboard")
static/index.html ADDED
@@ -0,0 +1,2091 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NIFTY 50 Directional Forecaster</title>
7
+ <!-- Google Fonts -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
11
+ <!-- FontAwesome for Premium Icons -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
13
+ <!-- Chart.js -->
14
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15
+
16
+ <style>
17
+ :root {
18
+ --bg-color: #070913;
19
+ --card-bg: rgba(18, 22, 45, 0.4);
20
+ --card-border: rgba(255, 255, 255, 0.07);
21
+ --card-border-hover: rgba(147, 51, 234, 0.3);
22
+ --text-primary: #f3f4f6;
23
+ --text-secondary: #9ca3af;
24
+
25
+ /* Accent Colors */
26
+ --accent-purple: #a855f7;
27
+ --accent-blue: #3b82f6;
28
+ --accent-emerald: #10b981;
29
+ --accent-rose: #f43f5e;
30
+ --accent-amber: #f59e0b;
31
+
32
+ /* Glows */
33
+ --glow-purple: rgba(168, 85, 247, 0.15);
34
+ --glow-emerald: rgba(16, 185, 129, 0.15);
35
+ --glow-rose: rgba(244, 63, 94, 0.15);
36
+ --glow-blue: rgba(59, 130, 246, 0.15);
37
+ }
38
+
39
+ * {
40
+ box-sizing: border-box;
41
+ margin: 0;
42
+ padding: 0;
43
+ scrollbar-width: thin;
44
+ scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
45
+ }
46
+
47
+ /* Custom Scrollbar */
48
+ *::-webkit-scrollbar {
49
+ width: 6px;
50
+ height: 6px;
51
+ }
52
+ *::-webkit-scrollbar-track {
53
+ background: transparent;
54
+ }
55
+ *::-webkit-scrollbar-thumb {
56
+ background: rgba(255, 255, 255, 0.12);
57
+ border-radius: 4px;
58
+ }
59
+ *::-webkit-scrollbar-thumb:hover {
60
+ background: rgba(255, 255, 255, 0.25);
61
+ }
62
+
63
+ body {
64
+ font-family: 'Inter', sans-serif;
65
+ background-color: var(--bg-color);
66
+ background-image:
67
+ radial-gradient(at 10% 20%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
68
+ radial-gradient(at 90% 80%, rgba(168, 85, 247, 0.15) 0px, transparent 50%),
69
+ radial-gradient(at 50% 50%, rgba(18, 22, 45, 0.5) 0px, transparent 80%);
70
+ background-attachment: fixed;
71
+ color: var(--text-primary);
72
+ min-height: 100vh;
73
+ padding-bottom: 40px;
74
+ overflow-x: hidden;
75
+ }
76
+
77
+ h1, h2, h3, h4, .title-font {
78
+ font-family: 'Outfit', sans-serif;
79
+ }
80
+
81
+ .container {
82
+ max-width: 1440px;
83
+ margin: 0 auto;
84
+ padding: 0 24px;
85
+ }
86
+
87
+ /* Glassmorphism utility */
88
+ .glass-panel {
89
+ background: var(--card-bg);
90
+ backdrop-filter: blur(16px);
91
+ -webkit-backdrop-filter: blur(16px);
92
+ border: 1px solid var(--card-border);
93
+ border-radius: 16px;
94
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
95
+ }
96
+
97
+ .glass-panel:hover {
98
+ border-color: rgba(255, 255, 255, 0.12);
99
+ box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
100
+ }
101
+
102
+ /* Header design */
103
+ header {
104
+ padding: 24px 0;
105
+ margin-bottom: 24px;
106
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
107
+ background: rgba(7, 9, 19, 0.6);
108
+ backdrop-filter: blur(12px);
109
+ position: sticky;
110
+ top: 0;
111
+ z-index: 100;
112
+ }
113
+
114
+ .header-content {
115
+ display: flex;
116
+ justify-content: space-between;
117
+ align-items: center;
118
+ flex-wrap: wrap;
119
+ gap: 16px;
120
+ }
121
+
122
+ .logo-area {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 12px;
126
+ }
127
+
128
+ .logo-icon {
129
+ font-size: 28px;
130
+ background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue));
131
+ -webkit-background-clip: text;
132
+ -webkit-text-fill-color: transparent;
133
+ filter: drop-shadow(0 2px 8px rgba(168, 85, 247, 0.4));
134
+ }
135
+
136
+ .logo-title {
137
+ font-size: 22px;
138
+ font-weight: 800;
139
+ letter-spacing: -0.5px;
140
+ background: linear-gradient(to right, #ffffff, #d1d5db);
141
+ -webkit-background-clip: text;
142
+ -webkit-text-fill-color: transparent;
143
+ }
144
+
145
+ .logo-badge {
146
+ font-size: 10px;
147
+ font-weight: 700;
148
+ text-transform: uppercase;
149
+ padding: 2px 6px;
150
+ border-radius: 6px;
151
+ background: rgba(168, 85, 247, 0.15);
152
+ color: var(--accent-purple);
153
+ border: 1px solid rgba(168, 85, 247, 0.3);
154
+ letter-spacing: 0.5px;
155
+ }
156
+
157
+ /* System Info Statuses */
158
+ .system-status {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 16px;
162
+ flex-wrap: wrap;
163
+ }
164
+
165
+ .status-badge {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 8px;
169
+ font-size: 13px;
170
+ padding: 6px 12px;
171
+ border-radius: 99px;
172
+ background: rgba(255, 255, 255, 0.03);
173
+ border: 1px solid rgba(255, 255, 255, 0.05);
174
+ color: var(--text-secondary);
175
+ }
176
+
177
+ .status-dot {
178
+ width: 8px;
179
+ height: 8px;
180
+ border-radius: 50%;
181
+ display: inline-block;
182
+ }
183
+
184
+ .status-dot.active {
185
+ background-color: var(--accent-emerald);
186
+ box-shadow: 0 0 8px var(--accent-emerald);
187
+ animation: pulse 2s infinite;
188
+ }
189
+
190
+ .status-dot.pending {
191
+ background-color: var(--accent-amber);
192
+ box-shadow: 0 0 8px var(--accent-amber);
193
+ }
194
+
195
+ .status-dot.inactive {
196
+ background-color: var(--accent-rose);
197
+ box-shadow: 0 0 8px var(--accent-rose);
198
+ }
199
+
200
+ /* Nav Tabs */
201
+ .nav-tabs {
202
+ display: flex;
203
+ gap: 8px;
204
+ padding: 4px;
205
+ background: rgba(255, 255, 255, 0.02);
206
+ border: 1px solid rgba(255, 255, 255, 0.05);
207
+ border-radius: 12px;
208
+ margin-bottom: 28px;
209
+ }
210
+
211
+ .tab-btn {
212
+ background: transparent;
213
+ border: none;
214
+ color: var(--text-secondary);
215
+ padding: 10px 20px;
216
+ border-radius: 10px;
217
+ cursor: pointer;
218
+ font-family: 'Outfit', sans-serif;
219
+ font-size: 15px;
220
+ font-weight: 600;
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 8px;
224
+ transition: all 0.25s ease;
225
+ }
226
+
227
+ .tab-btn:hover {
228
+ color: var(--text-primary);
229
+ background: rgba(255, 255, 255, 0.04);
230
+ }
231
+
232
+ .tab-btn.active {
233
+ color: #ffffff;
234
+ background: linear-gradient(135deg, rgba(168, 85, 247, 0.25), rgba(59, 130, 246, 0.25));
235
+ border: 1px solid rgba(168, 85, 247, 0.3);
236
+ box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
237
+ }
238
+
239
+ /* Main View Switcher */
240
+ .tab-content {
241
+ display: none;
242
+ animation: fadeIn 0.4s ease;
243
+ }
244
+
245
+ .tab-content.active {
246
+ display: block;
247
+ }
248
+
249
+ /* Grid Layouts */
250
+ .dashboard-grid {
251
+ display: grid;
252
+ grid-template-columns: 2fr 1fr;
253
+ gap: 24px;
254
+ margin-bottom: 24px;
255
+ }
256
+
257
+ @media (max-width: 1024px) {
258
+ .dashboard-grid {
259
+ grid-template-columns: 1fr;
260
+ }
261
+ }
262
+
263
+ /* Prediction Card Section */
264
+ .predictions-container {
265
+ display: grid;
266
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
267
+ gap: 20px;
268
+ margin-bottom: 24px;
269
+ }
270
+
271
+ .predict-card {
272
+ padding: 24px;
273
+ position: relative;
274
+ overflow: hidden;
275
+ display: flex;
276
+ flex-direction: column;
277
+ justify-content: space-between;
278
+ min-height: 280px;
279
+ }
280
+
281
+ .predict-card::before {
282
+ content: '';
283
+ position: absolute;
284
+ top: 0;
285
+ left: 0;
286
+ width: 100%;
287
+ height: 4px;
288
+ }
289
+
290
+ .predict-card.up::before { background: linear-gradient(to right, var(--accent-emerald), #34d399); }
291
+ .predict-card.down::before { background: linear-gradient(to right, var(--accent-rose), #fb7185); }
292
+ .predict-card.pending::before { background: linear-gradient(to right, var(--accent-amber), #fbbf24); }
293
+
294
+ .card-header {
295
+ display: flex;
296
+ justify-content: space-between;
297
+ align-items: flex-start;
298
+ margin-bottom: 16px;
299
+ }
300
+
301
+ .card-title {
302
+ font-size: 14px;
303
+ text-transform: uppercase;
304
+ letter-spacing: 1px;
305
+ color: var(--text-secondary);
306
+ font-weight: 700;
307
+ }
308
+
309
+ .card-badge {
310
+ font-size: 11px;
311
+ padding: 4px 8px;
312
+ border-radius: 6px;
313
+ font-weight: 600;
314
+ }
315
+
316
+ .predict-card.up .card-badge { background: rgba(16, 185, 129, 0.1); color: var(--accent-emerald); }
317
+ .predict-card.down .card-badge { background: rgba(244, 63, 94, 0.1); color: var(--accent-rose); }
318
+ .predict-card.pending .card-badge { background: rgba(245, 158, 11, 0.1); color: var(--accent-amber); }
319
+
320
+ .signal-display {
321
+ text-align: center;
322
+ margin: 20px 0;
323
+ }
324
+
325
+ .signal-text {
326
+ font-size: 48px;
327
+ font-weight: 900;
328
+ letter-spacing: -1.5px;
329
+ display: flex;
330
+ align-items: center;
331
+ justify-content: center;
332
+ gap: 12px;
333
+ }
334
+
335
+ .predict-card.up .signal-text {
336
+ color: var(--accent-emerald);
337
+ text-shadow: 0 0 20px var(--glow-emerald);
338
+ }
339
+
340
+ .predict-card.down .signal-text {
341
+ color: var(--accent-rose);
342
+ text-shadow: 0 0 20px var(--glow-rose);
343
+ }
344
+
345
+ .predict-card.pending .signal-text {
346
+ color: var(--accent-amber);
347
+ font-size: 32px;
348
+ }
349
+
350
+ .metric-row {
351
+ display: flex;
352
+ justify-content: space-between;
353
+ align-items: center;
354
+ padding: 8px 0;
355
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
356
+ font-size: 13px;
357
+ }
358
+
359
+ .metric-row:last-child {
360
+ border-bottom: none;
361
+ }
362
+
363
+ .metric-label {
364
+ color: var(--text-secondary);
365
+ }
366
+
367
+ .metric-value {
368
+ font-weight: 600;
369
+ }
370
+
371
+ /* Nifty Quote Card widget */
372
+ .quote-panel {
373
+ padding: 24px;
374
+ background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.08), transparent), var(--card-bg);
375
+ }
376
+
377
+ .quote-header {
378
+ display: flex;
379
+ justify-content: space-between;
380
+ align-items: center;
381
+ margin-bottom: 18px;
382
+ }
383
+
384
+ .quote-symbol {
385
+ font-size: 20px;
386
+ font-weight: 700;
387
+ letter-spacing: -0.5px;
388
+ }
389
+
390
+ .quote-price-area {
391
+ margin-bottom: 20px;
392
+ }
393
+
394
+ .quote-price {
395
+ font-size: 36px;
396
+ font-weight: 800;
397
+ letter-spacing: -1px;
398
+ line-height: 1;
399
+ }
400
+
401
+ .quote-change {
402
+ font-size: 14px;
403
+ font-weight: 600;
404
+ margin-top: 4px;
405
+ display: flex;
406
+ align-items: center;
407
+ gap: 6px;
408
+ }
409
+
410
+ .quote-change.up { color: var(--accent-emerald); }
411
+ .quote-change.down { color: var(--accent-rose); }
412
+
413
+ .quote-grid {
414
+ display: grid;
415
+ grid-template-columns: 1fr 1fr;
416
+ gap: 12px;
417
+ font-size: 13px;
418
+ }
419
+
420
+ .quote-item {
421
+ background: rgba(255, 255, 255, 0.02);
422
+ border: 1px solid rgba(255, 255, 255, 0.04);
423
+ border-radius: 8px;
424
+ padding: 10px;
425
+ }
426
+
427
+ /* Action hub panel */
428
+ .action-panel {
429
+ padding: 24px;
430
+ display: flex;
431
+ flex-direction: column;
432
+ gap: 14px;
433
+ }
434
+
435
+ .action-title {
436
+ font-size: 16px;
437
+ font-weight: 700;
438
+ margin-bottom: 6px;
439
+ display: flex;
440
+ align-items: center;
441
+ gap: 8px;
442
+ }
443
+
444
+ .btn {
445
+ background: rgba(255, 255, 255, 0.04);
446
+ border: 1px solid rgba(255, 255, 255, 0.08);
447
+ color: var(--text-primary);
448
+ padding: 12px 18px;
449
+ border-radius: 10px;
450
+ cursor: pointer;
451
+ font-family: 'Outfit', sans-serif;
452
+ font-weight: 600;
453
+ font-size: 14px;
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: center;
457
+ gap: 10px;
458
+ transition: all 0.2s ease;
459
+ }
460
+
461
+ .btn:hover {
462
+ background: rgba(255, 255, 255, 0.08);
463
+ border-color: rgba(255, 255, 255, 0.15);
464
+ }
465
+
466
+ .btn:active {
467
+ transform: translateY(1px);
468
+ }
469
+
470
+ .btn.btn-primary {
471
+ background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue));
472
+ border: none;
473
+ box-shadow: 0 4px 15px rgba(168, 85, 247, 0.3);
474
+ }
475
+
476
+ .btn.btn-primary:hover {
477
+ opacity: 0.9;
478
+ box-shadow: 0 6px 20px rgba(168, 85, 247, 0.4);
479
+ }
480
+
481
+ .btn.btn-success {
482
+ background: rgba(16, 185, 129, 0.15);
483
+ border-color: rgba(16, 185, 129, 0.3);
484
+ color: var(--accent-emerald);
485
+ }
486
+
487
+ .btn.btn-success:hover {
488
+ background: rgba(16, 185, 129, 0.25);
489
+ }
490
+
491
+ .btn.btn-danger {
492
+ background: rgba(244, 63, 94, 0.15);
493
+ border-color: rgba(244, 63, 94, 0.3);
494
+ color: var(--accent-rose);
495
+ }
496
+
497
+ .btn.btn-danger:hover {
498
+ background: rgba(244, 63, 94, 0.25);
499
+ }
500
+
501
+ .btn i.spin {
502
+ animation: spin 1s linear infinite;
503
+ }
504
+
505
+ /* MFE Panel specific */
506
+ .mfe-panel {
507
+ padding: 24px;
508
+ background: radial-gradient(circle at bottom left, rgba(168, 85, 247, 0.08), transparent), var(--card-bg);
509
+ margin-bottom: 24px;
510
+ }
511
+
512
+ .mfe-header {
513
+ display: flex;
514
+ justify-content: space-between;
515
+ align-items: center;
516
+ margin-bottom: 16px;
517
+ }
518
+
519
+ .mfe-body {
520
+ display: grid;
521
+ grid-template-columns: 1fr 1fr;
522
+ gap: 20px;
523
+ }
524
+
525
+ @media (max-width: 600px) {
526
+ .mfe-body {
527
+ grid-template-columns: 1fr;
528
+ }
529
+ }
530
+
531
+ .mfe-side {
532
+ background: rgba(255, 255, 255, 0.01);
533
+ border: 1px solid rgba(255, 255, 255, 0.03);
534
+ border-radius: 12px;
535
+ padding: 16px;
536
+ }
537
+
538
+ .mfe-side-title {
539
+ font-size: 13px;
540
+ font-weight: 700;
541
+ text-transform: uppercase;
542
+ letter-spacing: 0.5px;
543
+ margin-bottom: 12px;
544
+ color: var(--text-secondary);
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 8px;
548
+ }
549
+
550
+ .mfe-pts {
551
+ font-size: 28px;
552
+ font-weight: 800;
553
+ letter-spacing: -0.5px;
554
+ }
555
+
556
+ .mfe-side.up .mfe-pts { color: var(--accent-emerald); }
557
+ .mfe-side.down .mfe-pts { color: var(--accent-rose); }
558
+
559
+ /* Chart container */
560
+ .chart-panel {
561
+ padding: 24px;
562
+ margin-bottom: 24px;
563
+ }
564
+
565
+ .chart-header {
566
+ display: flex;
567
+ justify-content: space-between;
568
+ align-items: center;
569
+ margin-bottom: 16px;
570
+ flex-wrap: wrap;
571
+ gap: 12px;
572
+ }
573
+
574
+ .chart-wrapper {
575
+ position: relative;
576
+ width: 100%;
577
+ height: 380px;
578
+ }
579
+
580
+ /* Accuracy Grid */
581
+ .accuracy-grid {
582
+ display: grid;
583
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
584
+ gap: 20px;
585
+ margin-bottom: 24px;
586
+ }
587
+
588
+ .accuracy-card {
589
+ padding: 24px;
590
+ }
591
+
592
+ .accuracy-header {
593
+ display: flex;
594
+ justify-content: space-between;
595
+ align-items: center;
596
+ margin-bottom: 20px;
597
+ }
598
+
599
+ .gauge-area {
600
+ position: relative;
601
+ display: flex;
602
+ justify-content: center;
603
+ align-items: center;
604
+ height: 120px;
605
+ margin-bottom: 16px;
606
+ }
607
+
608
+ .gauge-number {
609
+ position: absolute;
610
+ font-size: 32px;
611
+ font-weight: 800;
612
+ font-family: 'Outfit', sans-serif;
613
+ }
614
+
615
+ .gauge-svg {
616
+ transform: rotate(-90deg);
617
+ width: 120px;
618
+ height: 120px;
619
+ }
620
+
621
+ .gauge-svg circle {
622
+ fill: none;
623
+ stroke-width: 10;
624
+ }
625
+
626
+ .gauge-track {
627
+ stroke: rgba(255, 255, 255, 0.05);
628
+ }
629
+
630
+ .gauge-fill {
631
+ stroke: var(--accent-purple);
632
+ stroke-linecap: round;
633
+ stroke-dasharray: 314.16;
634
+ stroke-dashoffset: 314.16;
635
+ transition: stroke-dashoffset 1s ease-out;
636
+ }
637
+
638
+ /* Tables & Lists style */
639
+ .table-panel {
640
+ padding: 24px;
641
+ margin-bottom: 24px;
642
+ }
643
+
644
+ .table-title {
645
+ font-size: 18px;
646
+ font-weight: 700;
647
+ margin-bottom: 16px;
648
+ }
649
+
650
+ .table-responsive {
651
+ width: 100%;
652
+ overflow-x: auto;
653
+ }
654
+
655
+ table {
656
+ width: 100%;
657
+ border-collapse: collapse;
658
+ text-align: left;
659
+ font-size: 13px;
660
+ }
661
+
662
+ th {
663
+ color: var(--text-secondary);
664
+ font-weight: 600;
665
+ padding: 12px 16px;
666
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
667
+ text-transform: uppercase;
668
+ font-size: 11px;
669
+ letter-spacing: 0.5px;
670
+ }
671
+
672
+ td {
673
+ padding: 14px 16px;
674
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
675
+ color: var(--text-primary);
676
+ }
677
+
678
+ tr:hover td {
679
+ background: rgba(255, 255, 255, 0.01);
680
+ }
681
+
682
+ .badge-pill {
683
+ display: inline-block;
684
+ padding: 3px 8px;
685
+ border-radius: 99px;
686
+ font-size: 11px;
687
+ font-weight: 600;
688
+ }
689
+
690
+ .badge-pill.success { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); }
691
+ .badge-pill.danger { background: rgba(244, 63, 94, 0.15); color: var(--accent-rose); }
692
+ .badge-pill.secondary { background: rgba(255, 255, 255, 0.06); color: var(--text-secondary); }
693
+
694
+ /* Tools Page Tabs and Grids */
695
+ .tools-grid {
696
+ display: grid;
697
+ grid-template-columns: 1fr 1fr;
698
+ gap: 24px;
699
+ }
700
+
701
+ @media (max-width: 1024px) {
702
+ .tools-grid {
703
+ grid-template-columns: 1fr;
704
+ }
705
+ }
706
+
707
+ /* Kotak credentials and snap */
708
+ .kotak-panel {
709
+ padding: 24px;
710
+ }
711
+
712
+ .kotak-auth-box {
713
+ background: rgba(255, 255, 255, 0.02);
714
+ border: 1px solid rgba(255, 255, 255, 0.04);
715
+ border-radius: 12px;
716
+ padding: 20px;
717
+ margin-top: 16px;
718
+ }
719
+
720
+ .form-group {
721
+ margin-bottom: 16px;
722
+ }
723
+
724
+ .form-group label {
725
+ display: block;
726
+ font-size: 12px;
727
+ color: var(--text-secondary);
728
+ font-weight: 600;
729
+ margin-bottom: 6px;
730
+ text-transform: uppercase;
731
+ }
732
+
733
+ .form-input {
734
+ width: 100%;
735
+ background: rgba(0, 0, 0, 0.2);
736
+ border: 1px solid rgba(255, 255, 255, 0.08);
737
+ border-radius: 8px;
738
+ padding: 12px;
739
+ color: #fff;
740
+ font-family: inherit;
741
+ font-size: 14px;
742
+ transition: all 0.2s ease;
743
+ }
744
+
745
+ .form-input:focus {
746
+ outline: none;
747
+ border-color: var(--accent-purple);
748
+ box-shadow: 0 0 10px rgba(168, 85, 247, 0.25);
749
+ }
750
+
751
+ /* Balances & Grid counters */
752
+ .cash-grid {
753
+ display: grid;
754
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
755
+ gap: 12px;
756
+ margin-top: 16px;
757
+ }
758
+
759
+ .cash-card {
760
+ background: rgba(255, 255, 255, 0.02);
761
+ border: 1px solid rgba(255, 255, 255, 0.04);
762
+ border-radius: 10px;
763
+ padding: 14px;
764
+ }
765
+
766
+ .cash-title {
767
+ font-size: 11px;
768
+ color: var(--text-secondary);
769
+ text-transform: uppercase;
770
+ font-weight: 600;
771
+ margin-bottom: 6px;
772
+ }
773
+
774
+ .cash-val {
775
+ font-size: 18px;
776
+ font-weight: 700;
777
+ font-family: 'Outfit', sans-serif;
778
+ }
779
+
780
+ /* Screener style */
781
+ .screener-panel {
782
+ padding: 24px;
783
+ }
784
+
785
+ .search-row {
786
+ display: flex;
787
+ gap: 10px;
788
+ margin-bottom: 20px;
789
+ }
790
+
791
+ .screener-results {
792
+ margin-top: 16px;
793
+ animation: fadeIn 0.4s ease;
794
+ }
795
+
796
+ .pro-con-grid {
797
+ display: grid;
798
+ grid-template-columns: 1fr 1fr;
799
+ gap: 16px;
800
+ margin: 16px 0;
801
+ }
802
+
803
+ @media (max-width: 600px) {
804
+ .pro-con-grid {
805
+ grid-template-columns: 1fr;
806
+ }
807
+ }
808
+
809
+ .pro-list, .con-list {
810
+ padding: 16px;
811
+ border-radius: 12px;
812
+ font-size: 12px;
813
+ }
814
+
815
+ .pro-list { background: rgba(16, 185, 129, 0.04); border: 1px solid rgba(16, 185, 129, 0.1); }
816
+ .con-list { background: rgba(244, 63, 94, 0.04); border: 1px solid rgba(244, 63, 94, 0.1); }
817
+
818
+ .pro-list h4 { color: var(--accent-emerald); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
819
+ .con-list h4 { color: var(--accent-rose); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
820
+
821
+ .bullet-list li {
822
+ list-style: none;
823
+ margin-bottom: 8px;
824
+ position: relative;
825
+ padding-left: 14px;
826
+ line-height: 1.4;
827
+ }
828
+
829
+ .pro-list li::before { content: '•'; color: var(--accent-emerald); position: absolute; left: 0; }
830
+ .con-list li::before { content: '•'; color: var(--accent-rose); position: absolute; left: 0; }
831
+
832
+ .screener-sec-title {
833
+ font-size: 14px;
834
+ font-weight: 700;
835
+ text-transform: uppercase;
836
+ letter-spacing: 0.5px;
837
+ margin: 24px 0 10px 0;
838
+ color: var(--text-secondary);
839
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
840
+ padding-bottom: 6px;
841
+ }
842
+
843
+ /* Settings cog & modal */
844
+ .settings-cog {
845
+ background: rgba(255, 255, 255, 0.04);
846
+ border: 1px solid rgba(255, 255, 255, 0.08);
847
+ width: 38px;
848
+ height: 38px;
849
+ border-radius: 10px;
850
+ display: flex;
851
+ align-items: center;
852
+ justify-content: center;
853
+ cursor: pointer;
854
+ color: var(--text-secondary);
855
+ transition: all 0.2s ease;
856
+ }
857
+
858
+ .settings-cog:hover {
859
+ color: #fff;
860
+ border-color: rgba(255, 255, 255, 0.15);
861
+ transform: rotate(30deg);
862
+ }
863
+
864
+ .modal-overlay {
865
+ position: fixed;
866
+ top: 0;
867
+ left: 0;
868
+ width: 100vw;
869
+ height: 100vh;
870
+ background: rgba(0, 0, 0, 0.7);
871
+ backdrop-filter: blur(8px);
872
+ z-index: 1000;
873
+ display: flex;
874
+ align-items: center;
875
+ justify-content: center;
876
+ opacity: 0;
877
+ pointer-events: none;
878
+ transition: opacity 0.3s ease;
879
+ }
880
+
881
+ .modal-overlay.active {
882
+ opacity: 1;
883
+ pointer-events: all;
884
+ }
885
+
886
+ .modal {
887
+ width: 450px;
888
+ max-width: 90%;
889
+ padding: 28px;
890
+ }
891
+
892
+ .modal-header {
893
+ display: flex;
894
+ justify-content: space-between;
895
+ align-items: center;
896
+ margin-bottom: 20px;
897
+ }
898
+
899
+ .modal-title {
900
+ font-size: 20px;
901
+ font-weight: 700;
902
+ }
903
+
904
+ .modal-close {
905
+ background: transparent;
906
+ border: none;
907
+ color: var(--text-secondary);
908
+ font-size: 20px;
909
+ cursor: pointer;
910
+ }
911
+
912
+ .modal-close:hover {
913
+ color: #fff;
914
+ }
915
+
916
+ /* Toast notifications */
917
+ .toast-container {
918
+ position: fixed;
919
+ bottom: 24px;
920
+ right: 24px;
921
+ z-index: 10000;
922
+ display: flex;
923
+ flex-direction: column;
924
+ gap: 10px;
925
+ }
926
+
927
+ .toast {
928
+ background: rgba(18, 22, 45, 0.9);
929
+ border: 1px solid var(--card-border);
930
+ backdrop-filter: blur(12px);
931
+ padding: 14px 20px;
932
+ border-radius: 10px;
933
+ color: #fff;
934
+ font-size: 13px;
935
+ display: flex;
936
+ align-items: center;
937
+ gap: 12px;
938
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
939
+ animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
940
+ min-width: 280px;
941
+ }
942
+
943
+ .toast.success { border-left: 4px solid var(--accent-emerald); }
944
+ .toast.error { border-left: 4px solid var(--accent-rose); }
945
+ .toast.info { border-left: 4px solid var(--accent-blue); }
946
+
947
+ /* Animations */
948
+ @keyframes fadeIn {
949
+ from { opacity: 0; transform: translateY(6px); }
950
+ to { opacity: 1; transform: translateY(0); }
951
+ }
952
+
953
+ @keyframes slideIn {
954
+ from { transform: translateX(100%) translateY(0); opacity: 0; }
955
+ to { transform: translateX(0) translateY(0); opacity: 1; }
956
+ }
957
+
958
+ @keyframes pulse {
959
+ 0% { transform: scale(1); opacity: 1; }
960
+ 50% { transform: scale(1.15); opacity: 0.7; }
961
+ 100% { transform: scale(1); opacity: 1; }
962
+ }
963
+
964
+ @keyframes spin {
965
+ from { transform: rotate(0deg); }
966
+ to { transform: rotate(360deg); }
967
+ }
968
+ </style>
969
+ </head>
970
+ <body>
971
+
972
+ <header>
973
+ <div class="container header-content">
974
+ <div class="logo-area">
975
+ <i class="fa-solid fa-chart-line logo-icon"></i>
976
+ <div class="logo-title">NIFTY 50 Forecaster</div>
977
+ <div class="logo-badge">Live</div>
978
+ </div>
979
+
980
+ <div class="system-status">
981
+ <div class="status-badge">
982
+ <span id="conn-dot" class="status-dot inactive"></span>
983
+ <span id="conn-text">Connecting...</span>
984
+ </div>
985
+ <div class="status-badge">
986
+ <i class="fa-regular fa-clock"></i>
987
+ <span id="server-time">--:--:-- IST</span>
988
+ </div>
989
+ <div class="status-badge">
990
+ <i class="fa-solid fa-calendar-day"></i>
991
+ <span id="trading-day-text">Trading Day: --</span>
992
+ </div>
993
+ <button class="settings-cog" id="settings-btn" title="API Settings">
994
+ <i class="fa-solid fa-cog"></i>
995
+ </button>
996
+ </div>
997
+ </div>
998
+ </header>
999
+
1000
+ <main class="container">
1001
+ <!-- Main Tabs -->
1002
+ <nav class="nav-tabs">
1003
+ <button class="tab-btn active" data-tab="overview">
1004
+ <i class="fa-solid fa-chart-pie"></i> Overview
1005
+ </button>
1006
+ <button class="tab-btn" data-tab="analytics">
1007
+ <i class="fa-solid fa-chart-column"></i> Performance & Analytics
1008
+ </button>
1009
+ <button class="tab-btn" data-tab="tools">
1010
+ <i class="fa-solid fa-toolbox"></i> Tools (Broker & Screener)
1011
+ </button>
1012
+ </nav>
1013
+
1014
+ <!-- 1. OVERVIEW TAB CONTENT -->
1015
+ <div id="overview" class="tab-content active">
1016
+ <div class="dashboard-grid">
1017
+ <!-- Left Side: Prediction cards & MFE -->
1018
+ <div>
1019
+ <!-- Main Predictions Grid -->
1020
+ <div class="predictions-container">
1021
+ <!-- T+5 Card -->
1022
+ <div class="glass-panel predict-card pending" id="card-t5">
1023
+ <div class="card-header">
1024
+ <span class="card-title">T+5 Prediction</span>
1025
+ <span class="card-badge" id="t5-badge">Pending</span>
1026
+ </div>
1027
+ <div class="signal-display">
1028
+ <div class="signal-text" id="t5-signal">
1029
+ <i class="fa-solid fa-hourglass-half"></i> PENDING
1030
+ </div>
1031
+ </div>
1032
+ <div>
1033
+ <div class="metric-row">
1034
+ <span class="metric-label">Confidence</span>
1035
+ <span class="metric-value" id="t5-conf">--%</span>
1036
+ </div>
1037
+ <div class="metric-row">
1038
+ <span class="metric-label">Probability (Up)</span>
1039
+ <span class="metric-value" id="t5-prob">--%</span>
1040
+ </div>
1041
+ <div class="metric-row">
1042
+ <span class="metric-label">Model</span>
1043
+ <span class="metric-value" id="t5-model">--</span>
1044
+ </div>
1045
+ </div>
1046
+ </div>
1047
+
1048
+ <!-- Tomorrow Card -->
1049
+ <div class="glass-panel predict-card pending" id="card-tomorrow">
1050
+ <div class="card-header">
1051
+ <span class="card-title">Tomorrow Prediction</span>
1052
+ <span class="card-badge" id="tomorrow-badge">Pending</span>
1053
+ </div>
1054
+ <div class="signal-display">
1055
+ <div class="signal-text" id="tomorrow-signal">
1056
+ <i class="fa-solid fa-hourglass-half"></i> PENDING
1057
+ </div>
1058
+ </div>
1059
+ <div>
1060
+ <div class="metric-row">
1061
+ <span class="metric-label">Target Date</span>
1062
+ <span class="metric-value" id="tomorrow-target-date">--</span>
1063
+ </div>
1064
+ <div class="metric-row">
1065
+ <span class="metric-label">Probability (Up)</span>
1066
+ <span class="metric-value" id="tomorrow-prob">--%</span>
1067
+ </div>
1068
+ <div class="metric-row">
1069
+ <span class="metric-label">Model</span>
1070
+ <span class="metric-value" id="tomorrow-model">--</span>
1071
+ </div>
1072
+ </div>
1073
+ </div>
1074
+
1075
+ <!-- T+1 Card -->
1076
+ <div class="glass-panel predict-card pending" id="card-tplus1">
1077
+ <div class="card-header">
1078
+ <span class="card-title">T+1 Prediction</span>
1079
+ <span class="card-badge" id="tplus1-badge">Pending</span>
1080
+ </div>
1081
+ <div class="signal-display">
1082
+ <div class="signal-text" id="tplus1-signal">
1083
+ <i class="fa-solid fa-hourglass-half"></i> PENDING
1084
+ </div>
1085
+ </div>
1086
+ <div>
1087
+ <div class="metric-row">
1088
+ <span class="metric-label">Confidence</span>
1089
+ <span class="metric-value" id="tplus1-conf">--%</span>
1090
+ </div>
1091
+ <div class="metric-row">
1092
+ <span class="metric-label">Probability (Up)</span>
1093
+ <span class="metric-value" id="tplus1-prob">--%</span>
1094
+ </div>
1095
+ <div class="metric-row">
1096
+ <span class="metric-label">Model</span>
1097
+ <span class="metric-value" id="tplus1-model">--</span>
1098
+ </div>
1099
+ </div>
1100
+ </div>
1101
+ </div>
1102
+
1103
+ <!-- MFE Regression Card -->
1104
+ <div class="glass-panel mfe-panel">
1105
+ <div class="mfe-header">
1106
+ <h3 class="title-font"><i class="fa-solid fa-expand"></i> Maximum Favorable Excursion (MFE) Bounds</h3>
1107
+ <span class="status-badge" id="mfe-date-badge">Session: --</span>
1108
+ </div>
1109
+ <div class="mfe-body">
1110
+ <div class="mfe-side up">
1111
+ <div class="mfe-side-title"><i class="fa-solid fa-circle-arrow-up"></i> Predicted Day High</div>
1112
+ <div class="mfe-pts" id="mfe-high-val">+-- pts</div>
1113
+ <div style="margin-top: 10px;">
1114
+ <div class="metric-row">
1115
+ <span class="metric-label">Target Level</span>
1116
+ <span class="metric-value" id="mfe-high-level">--</span>
1117
+ </div>
1118
+ <div class="metric-row">
1119
+ <span class="metric-label">Actual Day High</span>
1120
+ <span class="metric-value" id="mfe-high-actual">--</span>
1121
+ </div>
1122
+ </div>
1123
+ </div>
1124
+ <div class="mfe-side down">
1125
+ <div class="mfe-side-title"><i class="fa-solid fa-circle-arrow-down"></i> Predicted Day Low</div>
1126
+ <div class="mfe-pts" id="mfe-low-val">--- pts</div>
1127
+ <div style="margin-top: 10px;">
1128
+ <div class="metric-row">
1129
+ <span class="metric-label">Target Level</span>
1130
+ <span class="metric-value" id="mfe-low-level">--</span>
1131
+ </div>
1132
+ <div class="metric-row">
1133
+ <span class="metric-label">Actual Day Low</span>
1134
+ <span class="metric-value" id="mfe-low-actual">--</span>
1135
+ </div>
1136
+ </div>
1137
+ </div>
1138
+ </div>
1139
+ </div>
1140
+ </div>
1141
+
1142
+ <!-- Right Side: Nifty Quote and Action Hub -->
1143
+ <div>
1144
+ <!-- Nifty Live Quote panel -->
1145
+ <div class="glass-panel quote-panel" style="margin-bottom: 24px;">
1146
+ <div class="quote-header">
1147
+ <span class="quote-symbol title-font">NIFTY 50 SPOT</span>
1148
+ <span class="logo-badge" id="quote-source">Source: --</span>
1149
+ </div>
1150
+ <div class="quote-price-area">
1151
+ <div class="quote-price" id="quote-price">------</div>
1152
+ <div class="quote-change" id="quote-change">-- (--%)</div>
1153
+ </div>
1154
+ <div class="quote-grid">
1155
+ <div class="quote-item">
1156
+ <div style="color: var(--text-secondary); font-size: 11px;">Open</div>
1157
+ <div style="font-weight: 700; margin-top: 4px;" id="quote-open">--</div>
1158
+ </div>
1159
+ <div class="quote-item">
1160
+ <div style="color: var(--text-secondary); font-size: 11px;">Prev Close</div>
1161
+ <div style="font-weight: 700; margin-top: 4px;" id="quote-prev">--</div>
1162
+ </div>
1163
+ <div class="quote-item">
1164
+ <div style="color: var(--text-secondary); font-size: 11px;">Day High</div>
1165
+ <div style="font-weight: 700; margin-top: 4px;" id="quote-high">--</div>
1166
+ </div>
1167
+ <div class="quote-item">
1168
+ <div style="color: var(--text-secondary); font-size: 11px;">Day Low</div>
1169
+ <div style="font-weight: 700; margin-top: 4px;" id="quote-low">--</div>
1170
+ </div>
1171
+ </div>
1172
+ <div style="font-size: 11px; color: var(--text-secondary); margin-top: 14px; text-align: right;" id="quote-time">
1173
+ As of: --
1174
+ </div>
1175
+ </div>
1176
+
1177
+ <!-- Action Hub panel -->
1178
+ <div class="glass-panel action-panel">
1179
+ <h4 class="action-title"><i class="fa-solid fa-gears"></i> Operations Control Hub</h4>
1180
+ <button class="btn btn-primary" id="btn-keepalive">
1181
+ <i class="fa-solid fa-heartbeat"></i> Trigger Keepalive Ping
1182
+ </button>
1183
+ <button class="btn" id="btn-refresh-first5">
1184
+ <i class="fa-solid fa-hourglass-start"></i> Refresh First 5-Mins (T+5)
1185
+ </button>
1186
+ <button class="btn" id="btn-refresh-daily">
1187
+ <i class="fa-solid fa-sync"></i> Refresh Daily candles
1188
+ </button>
1189
+ <button class="btn" id="btn-refresh-close">
1190
+ <i class="fa-solid fa-circle-check"></i> Refresh Market Close Data
1191
+ </button>
1192
+ <div id="data-staleness" style="font-size: 11px; margin-top: 6px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 8px;">
1193
+ <div style="font-weight: 700; margin-bottom: 6px; color: var(--text-secondary); text-transform: uppercase;">Stale Checks:</div>
1194
+ <div style="display:flex; justify-content:space-between; margin-bottom: 4px;">
1195
+ <span>Daily Data Stale:</span>
1196
+ <span id="stale-daily" class="badge-pill secondary">--</span>
1197
+ </div>
1198
+ <div style="display:flex; justify-content:space-between; margin-bottom: 4px;">
1199
+ <span>Minutes Data Stale:</span>
1200
+ <span id="stale-minutes" class="badge-pill secondary">--</span>
1201
+ </div>
1202
+ <div style="display:flex; justify-content:space-between; margin-bottom: 4px;">
1203
+ <span>T+5 Forecast Stale:</span>
1204
+ <span id="stale-t5" class="badge-pill secondary">--</span>
1205
+ </div>
1206
+ <div style="display:flex; justify-content:space-between;">
1207
+ <span>T+1 Forecast Stale:</span>
1208
+ <span id="stale-tplus1" class="badge-pill secondary">--</span>
1209
+ </div>
1210
+ </div>
1211
+ </div>
1212
+ </div>
1213
+ </div>
1214
+ </div>
1215
+
1216
+ <!-- 2. ANALYTICS & BACKTEST TAB CONTENT -->
1217
+ <div id="analytics" class="tab-content">
1218
+ <!-- Daily Closes chart -->
1219
+ <div class="glass-panel chart-panel">
1220
+ <div class="chart-header">
1221
+ <h3 class="title-font"><i class="fa-solid fa-chart-line"></i> NIFTY 50 Daily Closes History</h3>
1222
+ <span class="status-badge" id="chart-sessions-badge">Last 180 Days</span>
1223
+ </div>
1224
+ <div class="chart-wrapper">
1225
+ <canvas id="closesChart"></canvas>
1226
+ </div>
1227
+ </div>
1228
+
1229
+ <!-- Accuracy Gauges -->
1230
+ <div class="accuracy-grid">
1231
+ <!-- T+5 Accuracy -->
1232
+ <div class="glass-panel accuracy-card">
1233
+ <div class="accuracy-header">
1234
+ <h4 class="title-font">T+5 Model Accuracy</h4>
1235
+ <span class="badge-pill secondary" id="gauge-t5-count">0 Days</span>
1236
+ </div>
1237
+ <div class="gauge-area">
1238
+ <span class="gauge-number" id="gauge-t5-pct">--%</span>
1239
+ <svg class="gauge-svg">
1240
+ <circle class="gauge-track" cx="60" cy="60" r="50"></circle>
1241
+ <circle class="gauge-fill" id="gauge-t5-fill" cx="60" cy="60" r="50"></circle>
1242
+ </svg>
1243
+ </div>
1244
+ <div>
1245
+ <div class="metric-row">
1246
+ <span class="metric-label">Live Correct</span>
1247
+ <span class="metric-value" id="gauge-t5-correct">0</span>
1248
+ </div>
1249
+ <div class="metric-row">
1250
+ <span class="metric-label">Validation Accuracy</span>
1251
+ <span class="metric-value" id="gauge-t5-val">--%</span>
1252
+ </div>
1253
+ </div>
1254
+ </div>
1255
+
1256
+ <!-- Tomorrow Accuracy -->
1257
+ <div class="glass-panel accuracy-card">
1258
+ <div class="accuracy-header">
1259
+ <h4 class="title-font">Tomorrow Model Accuracy</h4>
1260
+ <span class="badge-pill secondary" id="gauge-tomorrow-count">0 Days</span>
1261
+ </div>
1262
+ <div class="gauge-area">
1263
+ <span class="gauge-number" id="gauge-tomorrow-pct">--%</span>
1264
+ <svg class="gauge-svg">
1265
+ <circle class="gauge-track" cx="60" cy="60" r="50"></circle>
1266
+ <circle class="gauge-fill" id="gauge-tomorrow-fill" cx="60" cy="60" r="50"></circle>
1267
+ </svg>
1268
+ </div>
1269
+ <div>
1270
+ <div class="metric-row">
1271
+ <span class="metric-label">Live Correct</span>
1272
+ <span class="metric-value" id="gauge-tomorrow-correct">0</span>
1273
+ </div>
1274
+ <div class="metric-row">
1275
+ <span class="metric-label">Validation Accuracy</span>
1276
+ <span class="metric-value" id="gauge-tomorrow-val">--%</span>
1277
+ </div>
1278
+ </div>
1279
+ </div>
1280
+
1281
+ <!-- T+1 Accuracy -->
1282
+ <div class="glass-panel accuracy-card">
1283
+ <div class="accuracy-header">
1284
+ <h4 class="title-font">T+1 Model Accuracy</h4>
1285
+ <span class="badge-pill secondary" id="gauge-tplus1-count">0 Days</span>
1286
+ </div>
1287
+ <div class="gauge-area">
1288
+ <span class="gauge-number" id="gauge-tplus1-pct">--%</span>
1289
+ <svg class="gauge-svg">
1290
+ <circle class="gauge-track" cx="60" cy="60" r="50"></circle>
1291
+ <circle class="gauge-fill" id="gauge-tplus1-fill" cx="60" cy="60" r="50"></circle>
1292
+ </svg>
1293
+ </div>
1294
+ <div>
1295
+ <div class="metric-row">
1296
+ <span class="metric-label">Live Correct</span>
1297
+ <span class="metric-value" id="gauge-tplus1-correct">0</span>
1298
+ </div>
1299
+ <div class="metric-row">
1300
+ <span class="metric-label">Validation Accuracy</span>
1301
+ <span class="metric-value" id="gauge-tplus1-val">--%</span>
1302
+ </div>
1303
+ </div>
1304
+ </div>
1305
+ </div>
1306
+
1307
+ <!-- Tomorrow Predictions Live Track record -->
1308
+ <div class="glass-panel table-panel">
1309
+ <div class="table-title">Tomorrow Forecast Live Track Record (Last 10 Days)</div>
1310
+ <div class="table-responsive">
1311
+ <table>
1312
+ <thead>
1313
+ <tr>
1314
+ <th>Date</th>
1315
+ <th>Input Date</th>
1316
+ <th>Prediction</th>
1317
+ <th>Probability (Up)</th>
1318
+ <th>Actual Direction</th>
1319
+ <th>Move %</th>
1320
+ <th>Outcome</th>
1321
+ <th>Source</th>
1322
+ </tr>
1323
+ </thead>
1324
+ <tbody id="tomorrow-track-record-tbody">
1325
+ <tr><td colspan="8" style="text-align: center; color: var(--text-secondary);">No historical records found.</td></tr>
1326
+ </tbody>
1327
+ </table>
1328
+ </div>
1329
+ </div>
1330
+ </div>
1331
+
1332
+ <!-- 3. TOOLS TAB CONTENT -->
1333
+ <div id="tools" class="tab-content">
1334
+ <div class="tools-grid">
1335
+ <!-- Left panel: Kotak Neo Broker -->
1336
+ <div class="glass-panel kotak-panel">
1337
+ <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid rgba(255,255,255,0.05); padding-bottom:12px;">
1338
+ <h3 class="title-font"><i class="fa-solid fa-key"></i> Kotak Securities Neo</h3>
1339
+ <span class="badge-pill secondary" id="kotak-status-badge">Checking...</span>
1340
+ </div>
1341
+
1342
+ <!-- Auth Box if not authenticated -->
1343
+ <div id="kotak-auth-container" class="kotak-auth-box">
1344
+ <h4 style="font-size: 13px; font-weight: 700; margin-bottom: 12px; color: var(--accent-purple);">Session Authentication Required</h4>
1345
+ <div class="form-group">
1346
+ <label for="totp-input">Enter TOTP Token</label>
1347
+ <input type="text" id="totp-input" class="form-input" placeholder="6-digit auth code" maxlength="6">
1348
+ </div>
1349
+ <button class="btn btn-primary" id="btn-kotak-login" style="width: 100%;">
1350
+ <i class="fa-solid fa-right-to-bracket"></i> Validate Session Token
1351
+ </button>
1352
+ </div>
1353
+
1354
+ <!-- Snapshot information if authenticated -->
1355
+ <div id="kotak-snapshot-container" style="display: none; margin-top: 16px;">
1356
+ <h4 style="font-size: 13px; font-weight: 700; color: var(--accent-emerald);">Active Trading Portfolio Snap</h4>
1357
+ <div class="cash-grid">
1358
+ <div class="cash-card">
1359
+ <div class="cash-title">Net Value</div>
1360
+ <div class="cash-val" id="cash-net">--</div>
1361
+ </div>
1362
+ <div class="cash-card">
1363
+ <div class="cash-title">Margin Used</div>
1364
+ <div class="cash-val" id="cash-margin">--</div>
1365
+ </div>
1366
+ <div class="cash-card">
1367
+ <div class="cash-title">Available Cash</div>
1368
+ <div class="cash-val" id="cash-avail">--</div>
1369
+ </div>
1370
+ <div class="cash-card">
1371
+ <div class="cash-title">Live P&L</div>
1372
+ <div class="cash-val" id="cash-pnl">--</div>
1373
+ </div>
1374
+ </div>
1375
+
1376
+ <!-- Active holdings list -->
1377
+ <div class="screener-sec-title" style="margin-top: 20px;">Current Stock Holdings</div>
1378
+ <div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
1379
+ <table>
1380
+ <thead>
1381
+ <tr>
1382
+ <th>Symbol</th>
1383
+ <th>Qty</th>
1384
+ <th>Cost</th>
1385
+ <th>Market Val</th>
1386
+ <th>P&L</th>
1387
+ </tr>
1388
+ </thead>
1389
+ <tbody id="kotak-holdings-tbody">
1390
+ <tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No stock holdings.</td></tr>
1391
+ </tbody>
1392
+ </table>
1393
+ </div>
1394
+ </div>
1395
+ </div>
1396
+
1397
+ <!-- Right panel: Screener Scraper -->
1398
+ <div class="glass-panel screener-panel">
1399
+ <h3 class="title-font" style="border-bottom:1px solid rgba(255,255,255,0.05); padding-bottom:12px; margin-bottom:16px;">
1400
+ <i class="fa-solid fa-search"></i> Fundamental Stock Screener
1401
+ </h3>
1402
+
1403
+ <div class="search-row">
1404
+ <input type="text" id="screener-query" class="form-input" placeholder="Enter stock symbol (e.g., RELIANCE, TCS)">
1405
+ <button class="btn btn-primary" id="btn-screener-search">
1406
+ <i class="fa-solid fa-magnifying-glass"></i> Search
1407
+ </button>
1408
+ </div>
1409
+
1410
+ <div id="screener-loader" style="display:none; text-align:center; padding: 40px 0;">
1411
+ <i class="fa-solid fa-spinner fa-spin" style="font-size: 32px; color: var(--accent-purple);"></i>
1412
+ <div style="margin-top: 12px; font-size:13px; color: var(--text-secondary);">Fetching data from Screener.in...</div>
1413
+ </div>
1414
+
1415
+ <!-- Search Result container -->
1416
+ <div id="screener-results" class="screener-results" style="display: none;">
1417
+ <h4 class="title-font" id="screener-company-name" style="font-size: 20px; font-weight:700;">COMPANY NAME</h4>
1418
+
1419
+ <!-- Ratios grid -->
1420
+ <div class="screener-sec-title">Key Ratios</div>
1421
+ <div class="quote-grid" id="screener-ratios-grid"></div>
1422
+
1423
+ <!-- Pros & Cons -->
1424
+ <div class="pro-con-grid">
1425
+ <div class="pro-list">
1426
+ <h4><i class="fa-solid fa-thumbs-up"></i> Strengths (Pros)</h4>
1427
+ <ul class="bullet-list" id="screener-pros"></ul>
1428
+ </div>
1429
+ <div class="con-list">
1430
+ <h4><i class="fa-solid fa-thumbs-down"></i> Weaknesses (Cons)</h4>
1431
+ <ul class="bullet-list" id="screener-cons"></ul>
1432
+ </div>
1433
+ </div>
1434
+
1435
+ <!-- Growth metrics table -->
1436
+ <div class="screener-sec-title">Compound Growth Metrics</div>
1437
+ <div class="table-responsive">
1438
+ <table>
1439
+ <thead>
1440
+ <tr>
1441
+ <th>Metric</th>
1442
+ <th>Period</th>
1443
+ <th>Growth % / Value</th>
1444
+ </tr>
1445
+ </thead>
1446
+ <tbody id="screener-growth-tbody"></tbody>
1447
+ </table>
1448
+ </div>
1449
+ </div>
1450
+ </div>
1451
+ </div>
1452
+ </div>
1453
+ </main>
1454
+
1455
+ <!-- Settings Modal -->
1456
+ <div class="modal-overlay" id="settings-modal">
1457
+ <div class="glass-panel modal">
1458
+ <div class="modal-header">
1459
+ <h3 class="modal-title title-font"><i class="fa-solid fa-link"></i> API Endpoint Config</h3>
1460
+ <button class="modal-close" id="settings-close-btn">&times;</button>
1461
+ </div>
1462
+ <div class="form-group">
1463
+ <label for="backend-url-input">Backend Base URL</label>
1464
+ <input type="text" id="backend-url-input" class="form-input" placeholder="e.g. https://domain.hf.space">
1465
+ <small style="display:block; font-size:11px; color:var(--text-secondary); margin-top:6px;">
1466
+ Leave blank to automatically connect to this webserver's origin.
1467
+ </small>
1468
+ </div>
1469
+ <button class="btn btn-primary" id="btn-save-settings" style="width: 100%;">
1470
+ <i class="fa-solid fa-save"></i> Save Configuration
1471
+ </button>
1472
+ </div>
1473
+ </div>
1474
+
1475
+ <!-- Toast container -->
1476
+ <div class="toast-container" id="toast-container"></div>
1477
+
1478
+ <script>
1479
+ // Global variables for Client State
1480
+ let apiBaseUrl = localStorage.getItem('forecast_api_url') || '';
1481
+ let dashboardData = null;
1482
+ let chartInstance = null;
1483
+
1484
+ // Elements
1485
+ const connDot = document.getElementById('conn-dot');
1486
+ const connText = document.getElementById('conn-text');
1487
+ const serverTimeEl = document.getElementById('server-time');
1488
+ const tradingDayEl = document.getElementById('trading-day-text');
1489
+ const settingsModal = document.getElementById('settings-modal');
1490
+ const backendUrlInput = document.getElementById('backend-url-input');
1491
+
1492
+ // Toast notifications logic
1493
+ function showToast(message, type = 'info') {
1494
+ const container = document.getElementById('toast-container');
1495
+ const toast = document.createElement('div');
1496
+ toast.className = `toast ${type}`;
1497
+
1498
+ let iconClass = 'fa-circle-info';
1499
+ if (type === 'success') iconClass = 'fa-circle-check';
1500
+ if (type === 'error') iconClass = 'fa-circle-exclamation';
1501
+
1502
+ toast.innerHTML = `
1503
+ <i class="fa-solid ${iconClass}"></i>
1504
+ <span>${message}</span>
1505
+ `;
1506
+ container.appendChild(toast);
1507
+
1508
+ setTimeout(() => {
1509
+ toast.style.animation = 'slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) reverse forwards';
1510
+ setTimeout(() => toast.remove(), 300);
1511
+ }, 4000);
1512
+ }
1513
+
1514
+ // Active URL selector
1515
+ function getApiUrl(endpoint) {
1516
+ const base = apiBaseUrl.trim() || window.location.origin;
1517
+ return `${base.replace(/\/$/, '')}/${endpoint.replace(/^\//, '')}`;
1518
+ }
1519
+
1520
+ // Settings Modal management
1521
+ document.getElementById('settings-btn').addEventListener('click', () => {
1522
+ backendUrlInput.value = apiBaseUrl;
1523
+ settingsModal.classList.add('active');
1524
+ });
1525
+ document.getElementById('settings-close-btn').addEventListener('click', () => {
1526
+ settingsModal.classList.remove('active');
1527
+ });
1528
+ document.getElementById('btn-save-settings').addEventListener('click', () => {
1529
+ const val = backendUrlInput.value.trim();
1530
+ apiBaseUrl = val;
1531
+ localStorage.setItem('forecast_api_url', val);
1532
+ settingsModal.classList.remove('active');
1533
+ showToast('API URL configuration updated!', 'success');
1534
+ fetchDashboard();
1535
+ });
1536
+
1537
+ // Tab Navigation management
1538
+ document.querySelectorAll('.tab-btn').forEach(btn => {
1539
+ btn.addEventListener('click', () => {
1540
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
1541
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
1542
+
1543
+ btn.classList.add('active');
1544
+ const contentId = btn.getAttribute('data-tab');
1545
+ document.getElementById(contentId).classList.add('active');
1546
+
1547
+ // Render chart when switching to analytics
1548
+ if (contentId === 'analytics' && dashboardData) {
1549
+ setTimeout(() => renderChart(dashboardData.charts.daily_closes), 100);
1550
+ }
1551
+ });
1552
+ });
1553
+
1554
+ // Parse Float helper
1555
+ function formatVal(val, decimals = 2, suffix = '') {
1556
+ if (val === null || val === undefined || isNaN(val)) return '--';
1557
+ return parseFloat(val).toFixed(decimals) + suffix;
1558
+ }
1559
+
1560
+ // Main Dashboard API fetcher
1561
+ async function fetchDashboard() {
1562
+ try {
1563
+ connDot.className = 'status-dot pending';
1564
+ connText.textContent = 'Syncing...';
1565
+
1566
+ const response = await fetch(getApiUrl('dashboard'));
1567
+ if (!response.ok) throw new Error(`HTTP Error status: ${response.status}`);
1568
+ const data = await response.json();
1569
+
1570
+ dashboardData = data;
1571
+ renderDashboard(data);
1572
+
1573
+ connDot.className = 'status-dot active';
1574
+ connText.textContent = 'Connected';
1575
+ } catch (err) {
1576
+ console.error(err);
1577
+ connDot.className = 'status-dot inactive';
1578
+ connText.textContent = 'Disconnected';
1579
+ showToast('Failed to fetch dashboard data. Verify your API URL settings.', 'error');
1580
+ }
1581
+ }
1582
+
1583
+ // UI Renderer
1584
+ function renderDashboard(data) {
1585
+ // Header stats
1586
+ const status = data.data_status || {};
1587
+ const dateStr = status.server_time_ist ? new Date(status.server_time_ist).toLocaleTimeString('en-US', {timeZone: 'Asia/Kolkata'}) + ' IST' : '--:--:-- IST';
1588
+ serverTimeEl.textContent = dateStr;
1589
+ tradingDayEl.textContent = `Trading Day: ${status.is_trading_day ? 'Yes' : 'No'}`;
1590
+
1591
+ // 1. T+5 Card
1592
+ const t5 = data.predictions.t5 || {};
1593
+ const cardT5 = document.getElementById('card-t5');
1594
+ const sigT5 = document.getElementById('t5-signal');
1595
+ cardT5.className = 'glass-panel predict-card ' + (t5.available ? (t5.prediction === 'UP' ? 'up' : 'down') : 'pending');
1596
+ document.getElementById('t5-badge').textContent = t5.status || 'Pending';
1597
+
1598
+ if (t5.available) {
1599
+ sigT5.innerHTML = t5.prediction === 'UP' ? '<i class="fa-solid fa-circle-chevron-up"></i> UP' : '<i class="fa-solid fa-circle-chevron-down"></i> DOWN';
1600
+ document.getElementById('t5-conf').textContent = formatVal(t5.confidence * 100, 1, '%');
1601
+ document.getElementById('t5-prob').textContent = formatVal(t5.prob_up * 100, 1, '%');
1602
+ document.getElementById('t5-model').textContent = t5.model_name || '--';
1603
+ } else {
1604
+ sigT5.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING';
1605
+ document.getElementById('t5-conf').textContent = '--%';
1606
+ document.getElementById('t5-prob').textContent = '--%';
1607
+ document.getElementById('t5-model').textContent = '--';
1608
+ }
1609
+
1610
+ // 2. Tomorrow Card
1611
+ const tom = data.predictions.tomorrow || {};
1612
+ const cardTom = document.getElementById('card-tomorrow');
1613
+ const sigTom = document.getElementById('tomorrow-signal');
1614
+ cardTom.className = 'glass-panel predict-card ' + (tom.available ? (tom.prediction === 'UP' ? 'up' : 'down') : 'pending');
1615
+ document.getElementById('tomorrow-badge').textContent = tom.status || 'Pending';
1616
+
1617
+ if (tom.available) {
1618
+ sigTom.innerHTML = tom.prediction === 'UP' ? '<i class="fa-solid fa-circle-chevron-up"></i> UP' : '<i class="fa-solid fa-circle-chevron-down"></i> DOWN';
1619
+ document.getElementById('tomorrow-target-date').textContent = tom.target_date || '--';
1620
+ document.getElementById('tomorrow-prob').textContent = formatVal(tom.prob_up * 100, 1, '%');
1621
+ document.getElementById('tomorrow-model').textContent = tom.source_model || tom.model_name || '--';
1622
+ } else {
1623
+ sigTom.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING';
1624
+ document.getElementById('tomorrow-target-date').textContent = '--';
1625
+ document.getElementById('tomorrow-prob').textContent = '--%';
1626
+ document.getElementById('tomorrow-model').textContent = '--';
1627
+ }
1628
+
1629
+ // 3. T+1 Card
1630
+ const tplus = data.predictions.tplus1 || {};
1631
+ const cardTplus1 = document.getElementById('card-tplus1');
1632
+ const sigTplus1 = document.getElementById('tplus1-signal');
1633
+ cardTplus1.className = 'glass-panel predict-card ' + (tplus.available ? (tplus.prediction === 'UP' ? 'up' : 'down') : 'pending');
1634
+ document.getElementById('tplus1-badge').textContent = tplus.status || 'Pending';
1635
+
1636
+ if (tplus.available) {
1637
+ sigTplus1.innerHTML = tplus.prediction === 'UP' ? '<i class="fa-solid fa-circle-chevron-up"></i> UP' : '<i class="fa-solid fa-circle-chevron-down"></i> DOWN';
1638
+ document.getElementById('tplus1-conf').textContent = formatVal(tplus.confidence * 100, 1, '%');
1639
+ document.getElementById('tplus1-prob').textContent = formatVal(tplus.prob_up * 100, 1, '%');
1640
+ document.getElementById('tplus1-model').textContent = tplus.model_name || '--';
1641
+ } else {
1642
+ sigTplus1.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING';
1643
+ document.getElementById('tplus1-conf').textContent = '--%';
1644
+ document.getElementById('tplus1-prob').textContent = '--%';
1645
+ document.getElementById('tplus1-model').textContent = '--';
1646
+ }
1647
+
1648
+ // 4. MFE Regression Card
1649
+ const mfe = data.predictions.mfe || {};
1650
+ const mfeLatest = mfe.latest || {};
1651
+ const mfeSummary = mfe.summary || {};
1652
+ document.getElementById('mfe-date-badge').textContent = `Session: ${mfeLatest.input_date || '--'}`;
1653
+
1654
+ if (mfe.available && mfeLatest.predicted_up_points !== undefined) {
1655
+ document.getElementById('mfe-high-val').textContent = `+${formatVal(mfeLatest.predicted_up_points, 1)} pts`;
1656
+ document.getElementById('mfe-low-val').textContent = `-${formatVal(mfeLatest.predicted_down_points, 1)} pts`;
1657
+
1658
+ const f5Close = parseFloat(mfeLatest.first5_close);
1659
+ document.getElementById('mfe-high-level').textContent = formatVal(f5Close + parseFloat(mfeLatest.predicted_up_points), 1);
1660
+ document.getElementById('mfe-low-level').textContent = formatVal(f5Close - parseFloat(mfeLatest.predicted_down_points), 1);
1661
+
1662
+ // History metrics mapping
1663
+ const currentHistoryRow = (mfe.history || []).find(r => r.date === mfeLatest.input_date);
1664
+ if (currentHistoryRow) {
1665
+ document.getElementById('mfe-high-actual').textContent = formatVal(currentHistoryRow.actual_high, 1);
1666
+ document.getElementById('mfe-low-actual').textContent = formatVal(currentHistoryRow.actual_low, 1);
1667
+ } else {
1668
+ document.getElementById('mfe-high-actual').textContent = '--';
1669
+ document.getElementById('mfe-low-actual').textContent = '--';
1670
+ }
1671
+ } else {
1672
+ document.getElementById('mfe-high-val').textContent = '+-- pts';
1673
+ document.getElementById('mfe-low-val').textContent = '--- pts';
1674
+ document.getElementById('mfe-high-level').textContent = '--';
1675
+ document.getElementById('mfe-low-level').textContent = '--';
1676
+ document.getElementById('mfe-high-actual').textContent = '--';
1677
+ document.getElementById('mfe-low-actual').textContent = '--';
1678
+ }
1679
+
1680
+ // 5. Nifty Spot Live Quote
1681
+ const quote = data.nifty_quote || {};
1682
+ const quoteErr = data.nifty_quote_error;
1683
+
1684
+ if (quoteErr) {
1685
+ document.getElementById('quote-price').textContent = 'UNAVAILABLE';
1686
+ document.getElementById('quote-price').style.fontSize = '24px';
1687
+ document.getElementById('quote-change').innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> ${quoteErr.message}`;
1688
+ document.getElementById('quote-change').className = 'quote-change down';
1689
+ document.getElementById('quote-source').textContent = 'Error';
1690
+
1691
+ document.getElementById('quote-open').textContent = '--';
1692
+ document.getElementById('quote-prev').textContent = '--';
1693
+ document.getElementById('quote-high').textContent = '--';
1694
+ document.getElementById('quote-low').textContent = '--';
1695
+ document.getElementById('quote-time').textContent = 'Quote refresh failed.';
1696
+ } else if (quote.last_traded_price !== undefined) {
1697
+ document.getElementById('quote-price').textContent = formatVal(quote.last_traded_price, 2);
1698
+ document.getElementById('quote-price').style.fontSize = '36px';
1699
+ document.getElementById('quote-source').textContent = quote.exchange_segment || 'Index';
1700
+
1701
+ const changeVal = parseFloat(quote.change);
1702
+ const isUp = changeVal >= 0;
1703
+ document.getElementById('quote-change').innerHTML = `
1704
+ <i class="fa-solid ${isUp ? 'fa-caret-up' : 'fa-caret-down'}"></i>
1705
+ ${isUp ? '+' : ''}${formatVal(changeVal, 2)} (${isUp ? '+' : ''}${formatVal(quote.change_pct, 2)}%)
1706
+ `;
1707
+ document.getElementById('quote-change').className = 'quote-change ' + (isUp ? 'up' : 'down');
1708
+
1709
+ document.getElementById('quote-open').textContent = formatVal(quote.open, 2);
1710
+ document.getElementById('quote-prev').textContent = formatVal(quote.close, 2);
1711
+ document.getElementById('quote-high').textContent = formatVal(quote.high, 2);
1712
+ document.getElementById('quote-low').textContent = formatVal(quote.low, 2);
1713
+
1714
+ const timeStr = quote.as_of ? new Date(quote.as_of).toLocaleTimeString('en-US') : '--:--';
1715
+ document.getElementById('quote-time').textContent = `As of: ${timeStr}`;
1716
+ }
1717
+
1718
+ // 6. Action Hub - Stale details mapping
1719
+ const stale = data.data_status || {};
1720
+ const formatStaleBadge = (elId, isStale) => {
1721
+ const el = document.getElementById(elId);
1722
+ el.textContent = isStale ? 'Stale' : 'Fresh';
1723
+ el.className = `badge-pill ${isStale ? 'danger' : 'success'}`;
1724
+ };
1725
+ formatStaleBadge('stale-daily', stale.daily_stale);
1726
+ formatStaleBadge('stale-minutes', stale.minutes_stale);
1727
+ formatStaleBadge('stale-t5', stale.t5_stale);
1728
+ formatStaleBadge('stale-tplus1', stale.tplus1_stale);
1729
+
1730
+ // 7. Accuracy tab gauges update
1731
+ const liveAcc = data.live_accuracy || {};
1732
+
1733
+ // T+5 Gauge
1734
+ const t5Acc = liveAcc.t5 || {};
1735
+ renderGauge('gauge-t5', t5Acc.accuracy, t5Acc.total, t5Acc.correct_count, t5.validation_accuracy);
1736
+
1737
+ // Tomorrow Gauge
1738
+ const tomAcc = liveAcc.tomorrow || {};
1739
+ renderGauge('gauge-tomorrow', tomAcc.accuracy, tomAcc.total, tomAcc.correct_count, tom.validation_accuracy);
1740
+
1741
+ // T+1 Gauge
1742
+ const tplusAcc = liveAcc.tplus1 || {};
1743
+ renderGauge('gauge-tplus1', tplusAcc.accuracy, tplusAcc.total, tplusAcc.correct_count, tplus.validation_accuracy);
1744
+
1745
+ // 8. Tomorrow Live Track Record Table
1746
+ const trackRecord = data.charts.tomorrow_live_track_record || [];
1747
+ const trTbody = document.getElementById('tomorrow-track-record-tbody');
1748
+
1749
+ if (trackRecord.length > 0) {
1750
+ trTbody.innerHTML = '';
1751
+ trackRecord.forEach(row => {
1752
+ const moveVal = parseFloat(row.actual_move) * 100;
1753
+ const moveClass = moveVal >= 0 ? 'success' : 'danger';
1754
+ const correctClass = row.correct ? 'success' : 'danger';
1755
+
1756
+ trTbody.innerHTML += `
1757
+ <tr>
1758
+ <td><strong>${row.date}</strong></td>
1759
+ <td>${row.input_date}</td>
1760
+ <td><span class="badge-pill ${row.prediction === 'UP' ? 'success' : 'danger'}">${row.prediction}</span></td>
1761
+ <td>${formatVal(row.prob_up * 100, 1, '%')}</td>
1762
+ <td><span class="badge-pill ${row.actual_direction === 'UP' ? 'success' : 'danger'}">${row.actual_direction}</span></td>
1763
+ <td><span class="badge-pill ${moveClass}">${moveVal >= 0 ? '+' : ''}${formatVal(moveVal, 2)}%</span></td>
1764
+ <td><span class="badge-pill ${correctClass}">${row.correct ? 'Correct' : 'Incorrect'}</span></td>
1765
+ <td><span style="color:var(--text-secondary);">${row.prediction_source || 'Rolling'}</span></td>
1766
+ </tr>
1767
+ `;
1768
+ });
1769
+ } else {
1770
+ trTbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: var(--text-secondary);">No historical records found.</td></tr>`;
1771
+ }
1772
+
1773
+ // 9. Kotak credentials/snapshots updates
1774
+ const kotakStatus = data.predictions.t5.kotak_status || data.kotak_status || {};
1775
+ const kotakBadge = document.getElementById('kotak-status-badge');
1776
+
1777
+ if (kotakStatus.authenticated) {
1778
+ kotakBadge.textContent = 'Authenticated';
1779
+ kotakBadge.className = 'badge-pill success';
1780
+ document.getElementById('kotak-auth-container').style.display = 'none';
1781
+ document.getElementById('kotak-snapshot-container').style.display = 'block';
1782
+
1783
+ // Display Limits & balances
1784
+ const limits = data.limits_summary || {};
1785
+ document.getElementById('cash-net').textContent = formatVal(limits.net, 0);
1786
+ document.getElementById('cash-margin').textContent = formatVal(limits.margin_used, 0);
1787
+
1788
+ let cashAvail = '--';
1789
+ if (limits.net !== null && limits.margin_used !== null) {
1790
+ cashAvail = limits.net - limits.margin_used;
1791
+ }
1792
+ document.getElementById('cash-avail').textContent = formatVal(cashAvail, 0);
1793
+
1794
+ const livePnl = (data.summary || {}).live_pnl;
1795
+ const pnlEl = document.getElementById('cash-pnl');
1796
+ pnlEl.textContent = formatVal(livePnl, 0);
1797
+ pnlEl.style.color = livePnl >= 0 ? 'var(--accent-emerald)' : 'var(--accent-rose)';
1798
+
1799
+ // Holdings table mapping
1800
+ const holdings = data.holdings || [];
1801
+ const holdingsTbody = document.getElementById('kotak-holdings-tbody');
1802
+ if (holdings.length > 0) {
1803
+ holdingsTbody.innerHTML = '';
1804
+ holdings.forEach(row => {
1805
+ const pnlVal = parseFloat(row.pnl);
1806
+ holdingsTbody.innerHTML += `
1807
+ <tr>
1808
+ <td><strong>${row.trading_symbol || row.symbol}</strong></td>
1809
+ <td>${row.quantity}</td>
1810
+ <td>${formatVal(row.average_price, 1)}</td>
1811
+ <td>${formatVal(row.market_value, 0)}</td>
1812
+ <td style="color: ${pnlVal >= 0 ? 'var(--accent-emerald)' : 'var(--accent-rose)'}; font-weight:700;">
1813
+ ${pnlVal >= 0 ? '+' : ''}${formatVal(pnlVal, 0)}
1814
+ </td>
1815
+ </tr>
1816
+ `;
1817
+ });
1818
+ } else {
1819
+ holdingsTbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No stock holdings.</td></tr>`;
1820
+ }
1821
+ } else {
1822
+ kotakBadge.textContent = 'Unauthorized';
1823
+ kotakBadge.className = 'badge-pill danger';
1824
+ document.getElementById('kotak-auth-container').style.display = 'block';
1825
+ document.getElementById('kotak-snapshot-container').style.display = 'none';
1826
+ }
1827
+ }
1828
+
1829
+ // Render Gauge Helper
1830
+ function renderGauge(id, accuracy, total, correctCount, validationAcc) {
1831
+ const pct = accuracy !== null && accuracy !== undefined ? (accuracy * 100) : null;
1832
+ document.getElementById(`${id}-pct`).textContent = pct !== null ? `${pct.toFixed(0)}%` : '--%';
1833
+ document.getElementById(`${id}-count`).textContent = `${total || 0} Sessions`;
1834
+ document.getElementById(`${id}-correct`).textContent = `${correctCount || 0} / ${total || 0}`;
1835
+ document.getElementById(`${id}-val`).textContent = formatVal(validationAcc * 100, 1, '%');
1836
+
1837
+ const strokeOffset = pct !== null ? (314.16 - (314.16 * pct) / 100) : 314.16;
1838
+ document.getElementById(`${id}-fill`).style.strokeDashoffset = strokeOffset;
1839
+ }
1840
+
1841
+ // Render History Close line chart with Chart.js
1842
+ function renderChart(closes) {
1843
+ if (!closes || closes.length === 0) return;
1844
+
1845
+ const ctx = document.getElementById('closesChart').getContext('2d');
1846
+ const labels = closes.map(r => r.date.split(' ')[0]);
1847
+ const prices = closes.map(r => r.close);
1848
+
1849
+ if (chartInstance) {
1850
+ chartInstance.destroy();
1851
+ }
1852
+
1853
+ chartInstance = new Chart(ctx, {
1854
+ type: 'line',
1855
+ data: {
1856
+ labels: labels,
1857
+ datasets: [{
1858
+ label: 'NIFTY 50 Close',
1859
+ data: prices,
1860
+ borderColor: '#a855f7',
1861
+ borderWidth: 2,
1862
+ pointRadius: 0,
1863
+ pointHoverRadius: 6,
1864
+ tension: 0.1,
1865
+ fill: true,
1866
+ backgroundColor: (context) => {
1867
+ const chart = context.chart;
1868
+ const {ctx, chartArea} = chart;
1869
+ if (!chartArea) return null;
1870
+ const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
1871
+ gradient.addColorStop(0, 'rgba(168, 85, 247, 0.25)');
1872
+ gradient.addColorStop(1, 'rgba(59, 130, 246, 0.00)');
1873
+ return gradient;
1874
+ }
1875
+ }]
1876
+ },
1877
+ options: {
1878
+ responsive: true,
1879
+ maintainAspectRatio: false,
1880
+ plugins: {
1881
+ legend: { display: false },
1882
+ tooltip: {
1883
+ mode: 'index',
1884
+ intersect: false,
1885
+ backgroundColor: 'rgba(7, 9, 19, 0.9)',
1886
+ titleFont: { family: 'Outfit' },
1887
+ bodyFont: { family: 'Inter' },
1888
+ borderColor: 'rgba(255, 255, 255, 0.1)',
1889
+ borderWidth: 1
1890
+ }
1891
+ },
1892
+ scales: {
1893
+ x: {
1894
+ grid: { color: 'rgba(255, 255, 255, 0.03)' },
1895
+ ticks: { color: '#9ca3af', font: { family: 'Inter', size: 10 } }
1896
+ },
1897
+ y: {
1898
+ grid: { color: 'rgba(255, 255, 255, 0.03)' },
1899
+ ticks: { color: '#9ca3af', font: { family: 'Inter', size: 10 } }
1900
+ }
1901
+ }
1902
+ }
1903
+ });
1904
+ }
1905
+
1906
+ // Operation triggers logic helper
1907
+ async function runTrigger(btnId, endpoint, method = 'POST', successMsg) {
1908
+ const btn = document.getElementById(btnId);
1909
+ const originalHtml = btn.innerHTML;
1910
+
1911
+ try {
1912
+ btn.disabled = true;
1913
+ btn.innerHTML = `<i class="fa-solid fa-spinner spin"></i> Processing...`;
1914
+
1915
+ const response = await fetch(getApiUrl(endpoint), { method: method });
1916
+ if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
1917
+
1918
+ showToast(successMsg, 'success');
1919
+ fetchDashboard();
1920
+ } catch (err) {
1921
+ console.error(err);
1922
+ showToast(`Operation failed: ${err.message}`, 'error');
1923
+ } finally {
1924
+ btn.disabled = false;
1925
+ btn.innerHTML = originalHtml;
1926
+ }
1927
+ }
1928
+
1929
+ // Action Hub triggers
1930
+ document.getElementById('btn-keepalive').addEventListener('click', () => {
1931
+ runTrigger('btn-keepalive', 'cron/keepalive', 'GET', 'Keepalive keep-warm routine triggered!');
1932
+ });
1933
+ document.getElementById('btn-refresh-first5').addEventListener('click', () => {
1934
+ runTrigger('btn-refresh-first5', 'prediction/refresh-first5', 'POST', 'First 5-minutes prediction refreshed!');
1935
+ });
1936
+ document.getElementById('btn-refresh-daily').addEventListener('click', () => {
1937
+ runTrigger('btn-refresh-daily', 'data/refresh-daily', 'POST', 'Daily parquet data refreshed!');
1938
+ });
1939
+ document.getElementById('btn-refresh-close').addEventListener('click', () => {
1940
+ runTrigger('btn-refresh-close', 'data/refresh-market-close', 'POST', 'Market close refresh sequence executed!');
1941
+ });
1942
+
1943
+ // Kotak Securities login action
1944
+ document.getElementById('btn-kotak-login').addEventListener('click', async () => {
1945
+ const totpInput = document.getElementById('totp-input');
1946
+ const totpVal = totpInput.value.trim();
1947
+ if (!totpVal || totpVal.length < 6) {
1948
+ showToast('A valid 6-digit TOTP code is required.', 'error');
1949
+ return;
1950
+ }
1951
+
1952
+ const btn = document.getElementById('btn-kotak-login');
1953
+ const originalHtml = btn.innerHTML;
1954
+
1955
+ try {
1956
+ btn.disabled = true;
1957
+ btn.innerHTML = `<i class="fa-solid fa-spinner spin"></i> Authenticating...`;
1958
+
1959
+ const response = await fetch(getApiUrl('kotak/auth/totp'), {
1960
+ method: 'POST',
1961
+ headers: { 'Content-Type': 'application/json' },
1962
+ body: JSON.stringify({ totp: totpVal })
1963
+ });
1964
+
1965
+ if (!response.ok) {
1966
+ const data = await response.json();
1967
+ throw new Error(data.detail || `Authentication rejected: status ${response.status}`);
1968
+ }
1969
+
1970
+ showToast('Kotak Neo session authenticated successfully!', 'success');
1971
+ totpInput.value = '';
1972
+ fetchDashboard();
1973
+ } catch (err) {
1974
+ console.error(err);
1975
+ showToast(`Session auth failed: ${err.message}`, 'error');
1976
+ } finally {
1977
+ btn.disabled = false;
1978
+ btn.innerHTML = originalHtml;
1979
+ }
1980
+ });
1981
+
1982
+ // Fundamental Screener search search logic
1983
+ document.getElementById('btn-screener-search').addEventListener('click', async () => {
1984
+ const queryInput = document.getElementById('screener-query');
1985
+ const query = queryInput.value.trim().toUpperCase();
1986
+
1987
+ if (!query) {
1988
+ showToast('Please specify a ticker symbol (e.g. INFYS, RELIANCE).', 'error');
1989
+ return;
1990
+ }
1991
+
1992
+ const resultsContainer = document.getElementById('screener-results');
1993
+ const loader = document.getElementById('screener-loader');
1994
+ const btn = document.getElementById('btn-screener-search');
1995
+
1996
+ try {
1997
+ btn.disabled = true;
1998
+ loader.style.display = 'block';
1999
+ resultsContainer.style.display = 'none';
2000
+
2001
+ const response = await fetch(getApiUrl(`info/${query}`));
2002
+ if (!response.ok) {
2003
+ const data = await response.json();
2004
+ throw new Error(data.detail || `Server returned error status ${response.status}`);
2005
+ }
2006
+
2007
+ const data = await response.json();
2008
+ renderScreenerResult(data);
2009
+
2010
+ resultsContainer.style.display = 'block';
2011
+ } catch (err) {
2012
+ console.error(err);
2013
+ showToast(`Screener search failed: ${err.message}`, 'error');
2014
+ } finally {
2015
+ btn.disabled = false;
2016
+ loader.style.display = 'none';
2017
+ }
2018
+ });
2019
+
2020
+ // Search renderer helper
2021
+ function renderScreenerResult(data) {
2022
+ document.getElementById('screener-company-name').textContent = data.ticker || 'STOCK ANALYSIS';
2023
+
2024
+ // Ratios
2025
+ const ratiosGrid = document.getElementById('screener-ratios-grid');
2026
+ ratiosGrid.innerHTML = '';
2027
+
2028
+ const metrics = data.key_metrics || {};
2029
+ for (const [name, val] of Object.entries(metrics)) {
2030
+ ratiosGrid.innerHTML += `
2031
+ <div class="quote-item">
2032
+ <div style="color: var(--text-secondary); font-size: 11px;">${name}</div>
2033
+ <div style="font-weight: 700; margin-top: 4px; font-size: 14px;">${val}</div>
2034
+ </div>
2035
+ `;
2036
+ }
2037
+
2038
+ // Pros / Cons
2039
+ const prosUl = document.getElementById('screener-pros');
2040
+ const consUl = document.getElementById('screener-cons');
2041
+ prosUl.innerHTML = '';
2042
+ consUl.innerHTML = '';
2043
+
2044
+ const pros = data.pros || [];
2045
+ if (pros.length > 0) {
2046
+ pros.forEach(item => prosUl.innerHTML += `<li>${item}</li>`);
2047
+ } else {
2048
+ prosUl.innerHTML = `<li>No data listed.</li>`;
2049
+ }
2050
+
2051
+ const cons = data.cons || [];
2052
+ if (cons.length > 0) {
2053
+ cons.forEach(item => consUl.innerHTML += `<li>${item}</li>`);
2054
+ } else {
2055
+ consUl.innerHTML = `<li>No data listed.</li>`;
2056
+ }
2057
+
2058
+ // Growth table
2059
+ const growthTbody = document.getElementById('screener-growth-tbody');
2060
+ const growth = data.growth || [];
2061
+
2062
+ if (growth.length > 0) {
2063
+ growthTbody.innerHTML = '';
2064
+ growth.forEach(row => {
2065
+ growthTbody.innerHTML += `
2066
+ <tr>
2067
+ <td><strong>${row.Metric}</strong></td>
2068
+ <td>${row.Period}</td>
2069
+ <td><span class="badge-pill secondary" style="font-size:12px; font-weight:700;">${row.Value}</span></td>
2070
+ </tr>
2071
+ `;
2072
+ });
2073
+ } else {
2074
+ growthTbody.innerHTML = `<tr><td colspan="3" style="text-align: center; color: var(--text-secondary);">No growth metrics available.</td></tr>`;
2075
+ }
2076
+ }
2077
+
2078
+ // Initialize fetching loop and setup timer ticks
2079
+ fetchDashboard();
2080
+ setInterval(fetchDashboard, 60000); // dashboard reload once every minute
2081
+
2082
+ // Incremental clock tick loop
2083
+ setInterval(() => {
2084
+ if (dashboardData && dashboardData.data_status) {
2085
+ const now = new Date();
2086
+ serverTimeEl.textContent = now.toLocaleTimeString('en-US', {timeZone: 'Asia/Kolkata'}) + ' IST';
2087
+ }
2088
+ }, 1000);
2089
+ </script>
2090
+ </body>
2091
+ </html>