Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>NIFTY 50 Directional Forecaster</title> | |
| <!-- Google Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <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"> | |
| <!-- FontAwesome for Premium Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Chart.js --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| :root { | |
| --bg-color: #070913; | |
| --card-bg: rgba(18, 22, 45, 0.4); | |
| --card-border: rgba(255, 255, 255, 0.07); | |
| --card-border-hover: rgba(147, 51, 234, 0.3); | |
| --text-primary: #f3f4f6; | |
| --text-secondary: #9ca3af; | |
| /* Accent Colors */ | |
| --accent-purple: #a855f7; | |
| --accent-blue: #3b82f6; | |
| --accent-emerald: #10b981; | |
| --accent-rose: #f43f5e; | |
| --accent-amber: #f59e0b; | |
| /* Glows */ | |
| --glow-purple: rgba(168, 85, 247, 0.15); | |
| --glow-emerald: rgba(16, 185, 129, 0.15); | |
| --glow-rose: rgba(244, 63, 94, 0.15); | |
| --glow-blue: rgba(59, 130, 246, 0.15); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| scrollbar-width: thin; | |
| scrollbar-color: rgba(255, 255, 255, 0.1) transparent; | |
| } | |
| /* Custom Scrollbar */ | |
| *::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| *::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| *::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.12); | |
| border-radius: 4px; | |
| } | |
| *::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.25); | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-color); | |
| background-image: | |
| radial-gradient(at 10% 20%, rgba(59, 130, 246, 0.15) 0px, transparent 50%), | |
| radial-gradient(at 90% 80%, rgba(168, 85, 247, 0.15) 0px, transparent 50%), | |
| radial-gradient(at 50% 50%, rgba(18, 22, 45, 0.5) 0px, transparent 80%); | |
| background-attachment: fixed; | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| padding-bottom: 40px; | |
| overflow-x: hidden; | |
| } | |
| h1, h2, h3, h4, .title-font { | |
| font-family: 'Outfit', sans-serif; | |
| } | |
| .container { | |
| max-width: 1440px; | |
| margin: 0 auto; | |
| padding: 0 24px; | |
| } | |
| /* Glassmorphism utility */ | |
| .glass-panel { | |
| background: var(--card-bg); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border: 1px solid var(--card-border); | |
| border-radius: 16px; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .glass-panel:hover { | |
| border-color: rgba(255, 255, 255, 0.12); | |
| box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5); | |
| } | |
| /* Header design */ | |
| header { | |
| padding: 24px 0; | |
| margin-bottom: 24px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| background: rgba(7, 9, 19, 0.6); | |
| backdrop-filter: blur(12px); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .header-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 16px; | |
| } | |
| .logo-area { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo-icon { | |
| font-size: 28px; | |
| background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| filter: drop-shadow(0 2px 8px rgba(168, 85, 247, 0.4)); | |
| } | |
| .logo-title { | |
| font-size: 22px; | |
| font-weight: 800; | |
| letter-spacing: -0.5px; | |
| background: linear-gradient(to right, #ffffff, #d1d5db); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .logo-badge { | |
| font-size: 10px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| padding: 2px 6px; | |
| border-radius: 6px; | |
| background: rgba(168, 85, 247, 0.15); | |
| color: var(--accent-purple); | |
| border: 1px solid rgba(168, 85, 247, 0.3); | |
| letter-spacing: 0.5px; | |
| } | |
| /* System Info Statuses */ | |
| .system-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| flex-wrap: wrap; | |
| } | |
| .status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 13px; | |
| padding: 6px 12px; | |
| border-radius: 99px; | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| color: var(--text-secondary); | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| } | |
| .status-dot.active { | |
| background-color: var(--accent-emerald); | |
| box-shadow: 0 0 8px var(--accent-emerald); | |
| animation: pulse 2s infinite; | |
| } | |
| .status-dot.pending { | |
| background-color: var(--accent-amber); | |
| box-shadow: 0 0 8px var(--accent-amber); | |
| } | |
| .status-dot.inactive { | |
| background-color: var(--accent-rose); | |
| box-shadow: 0 0 8px var(--accent-rose); | |
| } | |
| /* Nav Tabs */ | |
| .nav-tabs { | |
| display: flex; | |
| gap: 8px; | |
| padding: 4px; | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| border-radius: 12px; | |
| margin-bottom: 28px; | |
| } | |
| .tab-btn { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-secondary); | |
| padding: 10px 20px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 15px; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.25s ease; | |
| } | |
| .tab-btn:hover { | |
| color: var(--text-primary); | |
| background: rgba(255, 255, 255, 0.04); | |
| } | |
| .tab-btn.active { | |
| color: #ffffff; | |
| background: linear-gradient(135deg, rgba(168, 85, 247, 0.25), rgba(59, 130, 246, 0.25)); | |
| border: 1px solid rgba(168, 85, 247, 0.3); | |
| box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1); | |
| } | |
| /* Main View Switcher */ | |
| .tab-content { | |
| display: none; | |
| animation: fadeIn 0.4s ease; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| /* Grid Layouts */ | |
| .dashboard-grid { | |
| display: grid; | |
| grid-template-columns: 2fr 1fr; | |
| gap: 24px; | |
| margin-bottom: 24px; | |
| } | |
| @media (max-width: 1024px) { | |
| .dashboard-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Prediction Card Section */ | |
| .predictions-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 24px; | |
| } | |
| .predict-card { | |
| padding: 24px; | |
| position: relative; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| min-height: 280px; | |
| } | |
| .predict-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 4px; | |
| } | |
| .predict-card.up::before { background: linear-gradient(to right, var(--accent-emerald), #34d399); } | |
| .predict-card.down::before { background: linear-gradient(to right, var(--accent-rose), #fb7185); } | |
| .predict-card.pending::before { background: linear-gradient(to right, var(--accent-amber), #fbbf24); } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin-bottom: 16px; | |
| } | |
| .card-title { | |
| font-size: 14px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-secondary); | |
| font-weight: 700; | |
| } | |
| .card-badge { | |
| font-size: 11px; | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| font-weight: 600; | |
| } | |
| .predict-card.up .card-badge { background: rgba(16, 185, 129, 0.1); color: var(--accent-emerald); } | |
| .predict-card.down .card-badge { background: rgba(244, 63, 94, 0.1); color: var(--accent-rose); } | |
| .predict-card.pending .card-badge { background: rgba(245, 158, 11, 0.1); color: var(--accent-amber); } | |
| .signal-display { | |
| text-align: center; | |
| margin: 20px 0; | |
| } | |
| .signal-text { | |
| font-size: 48px; | |
| font-weight: 900; | |
| letter-spacing: -1.5px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 12px; | |
| } | |
| .predict-card.up .signal-text { | |
| color: var(--accent-emerald); | |
| text-shadow: 0 0 20px var(--glow-emerald); | |
| } | |
| .predict-card.down .signal-text { | |
| color: var(--accent-rose); | |
| text-shadow: 0 0 20px var(--glow-rose); | |
| } | |
| .predict-card.pending .signal-text { | |
| color: var(--accent-amber); | |
| font-size: 32px; | |
| } | |
| .metric-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 8px 0; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.04); | |
| font-size: 13px; | |
| } | |
| .metric-row:last-child { | |
| border-bottom: none; | |
| } | |
| .metric-label { | |
| color: var(--text-secondary); | |
| } | |
| .metric-value { | |
| font-weight: 600; | |
| } | |
| /* Nifty Quote Card widget */ | |
| .quote-panel { | |
| padding: 24px; | |
| background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.08), transparent), var(--card-bg); | |
| } | |
| .quote-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 18px; | |
| } | |
| .quote-symbol { | |
| font-size: 20px; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| } | |
| .quote-price-area { | |
| margin-bottom: 20px; | |
| } | |
| .quote-price { | |
| font-size: 36px; | |
| font-weight: 800; | |
| letter-spacing: -1px; | |
| line-height: 1; | |
| } | |
| .quote-change { | |
| font-size: 14px; | |
| font-weight: 600; | |
| margin-top: 4px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .quote-change.up { color: var(--accent-emerald); } | |
| .quote-change.down { color: var(--accent-rose); } | |
| .quote-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| font-size: 13px; | |
| } | |
| .quote-item { | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid rgba(255, 255, 255, 0.04); | |
| border-radius: 8px; | |
| padding: 10px; | |
| } | |
| /* Action hub panel */ | |
| .action-panel { | |
| padding: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| } | |
| .action-title { | |
| font-size: 16px; | |
| font-weight: 700; | |
| margin-bottom: 6px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .btn { | |
| background: rgba(255, 255, 255, 0.04); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| color: var(--text-primary); | |
| padding: 12px 18px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-family: 'Outfit', sans-serif; | |
| font-weight: 600; | |
| font-size: 14px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| transition: all 0.2s ease; | |
| } | |
| .btn:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| border-color: rgba(255, 255, 255, 0.15); | |
| } | |
| .btn:active { | |
| transform: translateY(1px); | |
| } | |
| .btn.btn-primary { | |
| background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue)); | |
| border: none; | |
| box-shadow: 0 4px 15px rgba(168, 85, 247, 0.3); | |
| } | |
| .btn.btn-primary:hover { | |
| opacity: 0.9; | |
| box-shadow: 0 6px 20px rgba(168, 85, 247, 0.4); | |
| } | |
| .btn.btn-success { | |
| background: rgba(16, 185, 129, 0.15); | |
| border-color: rgba(16, 185, 129, 0.3); | |
| color: var(--accent-emerald); | |
| } | |
| .btn.btn-success:hover { | |
| background: rgba(16, 185, 129, 0.25); | |
| } | |
| .btn.btn-danger { | |
| background: rgba(244, 63, 94, 0.15); | |
| border-color: rgba(244, 63, 94, 0.3); | |
| color: var(--accent-rose); | |
| } | |
| .btn.btn-danger:hover { | |
| background: rgba(244, 63, 94, 0.25); | |
| } | |
| .btn i.spin { | |
| animation: spin 1s linear infinite; | |
| } | |
| /* MFE Panel specific */ | |
| .mfe-panel { | |
| padding: 24px; | |
| background: radial-gradient(circle at bottom left, rgba(168, 85, 247, 0.08), transparent), var(--card-bg); | |
| margin-bottom: 24px; | |
| } | |
| .mfe-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| } | |
| .mfe-body { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| @media (max-width: 600px) { | |
| .mfe-body { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .mfe-side { | |
| background: rgba(255, 255, 255, 0.01); | |
| border: 1px solid rgba(255, 255, 255, 0.03); | |
| border-radius: 12px; | |
| padding: 16px; | |
| } | |
| .mfe-side-title { | |
| font-size: 13px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 12px; | |
| color: var(--text-secondary); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .mfe-pts { | |
| font-size: 28px; | |
| font-weight: 800; | |
| letter-spacing: -0.5px; | |
| } | |
| .mfe-side.up .mfe-pts { color: var(--accent-emerald); } | |
| .mfe-side.down .mfe-pts { color: var(--accent-rose); } | |
| /* Chart container */ | |
| .chart-panel { | |
| padding: 24px; | |
| margin-bottom: 24px; | |
| } | |
| .chart-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| } | |
| .chart-wrapper { | |
| position: relative; | |
| width: 100%; | |
| height: 380px; | |
| } | |
| /* Accuracy Grid */ | |
| .accuracy-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 24px; | |
| } | |
| .accuracy-card { | |
| padding: 24px; | |
| } | |
| .accuracy-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .gauge-area { | |
| position: relative; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 120px; | |
| margin-bottom: 16px; | |
| } | |
| .gauge-number { | |
| position: absolute; | |
| font-size: 32px; | |
| font-weight: 800; | |
| font-family: 'Outfit', sans-serif; | |
| } | |
| .gauge-svg { | |
| transform: rotate(-90deg); | |
| width: 120px; | |
| height: 120px; | |
| } | |
| .gauge-svg circle { | |
| fill: none; | |
| stroke-width: 10; | |
| } | |
| .gauge-track { | |
| stroke: rgba(255, 255, 255, 0.05); | |
| } | |
| .gauge-fill { | |
| stroke: var(--accent-purple); | |
| stroke-linecap: round; | |
| stroke-dasharray: 314.16; | |
| stroke-dashoffset: 314.16; | |
| transition: stroke-dashoffset 1s ease-out; | |
| } | |
| /* Tables & Lists style */ | |
| .table-panel { | |
| padding: 24px; | |
| margin-bottom: 24px; | |
| } | |
| .table-title { | |
| font-size: 18px; | |
| font-weight: 700; | |
| margin-bottom: 16px; | |
| } | |
| .table-responsive { | |
| width: 100%; | |
| overflow-x: auto; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| text-align: left; | |
| font-size: 13px; | |
| } | |
| th { | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| padding: 12px 16px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.06); | |
| text-transform: uppercase; | |
| font-size: 11px; | |
| letter-spacing: 0.5px; | |
| } | |
| td { | |
| padding: 14px 16px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.04); | |
| color: var(--text-primary); | |
| } | |
| tr:hover td { | |
| background: rgba(255, 255, 255, 0.01); | |
| } | |
| .badge-pill { | |
| display: inline-block; | |
| padding: 3px 8px; | |
| border-radius: 99px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| } | |
| .badge-pill.success { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); } | |
| .badge-pill.danger { background: rgba(244, 63, 94, 0.15); color: var(--accent-rose); } | |
| .badge-pill.secondary { background: rgba(255, 255, 255, 0.06); color: var(--text-secondary); } | |
| /* Tools Page Tabs and Grids */ | |
| .tools-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 24px; | |
| } | |
| @media (max-width: 1024px) { | |
| .tools-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Kotak credentials and snap */ | |
| .kotak-panel { | |
| padding: 24px; | |
| } | |
| .kotak-auth-box { | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid rgba(255, 255, 255, 0.04); | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-top: 16px; | |
| } | |
| .form-group { | |
| margin-bottom: 16px; | |
| } | |
| .form-group label { | |
| display: block; | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| margin-bottom: 6px; | |
| text-transform: uppercase; | |
| } | |
| .form-input { | |
| width: 100%; | |
| background: rgba(0, 0, 0, 0.2); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| border-radius: 8px; | |
| padding: 12px; | |
| color: #fff; | |
| font-family: inherit; | |
| font-size: 14px; | |
| transition: all 0.2s ease; | |
| } | |
| .form-input:focus { | |
| outline: none; | |
| border-color: var(--accent-purple); | |
| box-shadow: 0 0 10px rgba(168, 85, 247, 0.25); | |
| } | |
| /* Balances & Grid counters */ | |
| .cash-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); | |
| gap: 12px; | |
| margin-top: 16px; | |
| } | |
| .cash-card { | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid rgba(255, 255, 255, 0.04); | |
| border-radius: 10px; | |
| padding: 14px; | |
| } | |
| .cash-title { | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| text-transform: uppercase; | |
| font-weight: 600; | |
| margin-bottom: 6px; | |
| } | |
| .cash-val { | |
| font-size: 18px; | |
| font-weight: 700; | |
| font-family: 'Outfit', sans-serif; | |
| } | |
| /* Screener style */ | |
| .screener-panel { | |
| padding: 24px; | |
| } | |
| .search-row { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .screener-results { | |
| margin-top: 16px; | |
| animation: fadeIn 0.4s ease; | |
| } | |
| .pro-con-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| margin: 16px 0; | |
| } | |
| @media (max-width: 600px) { | |
| .pro-con-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .pro-list, .con-list { | |
| padding: 16px; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| } | |
| .pro-list { background: rgba(16, 185, 129, 0.04); border: 1px solid rgba(16, 185, 129, 0.1); } | |
| .con-list { background: rgba(244, 63, 94, 0.04); border: 1px solid rgba(244, 63, 94, 0.1); } | |
| .pro-list h4 { color: var(--accent-emerald); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } | |
| .con-list h4 { color: var(--accent-rose); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } | |
| .bullet-list li { | |
| list-style: none; | |
| margin-bottom: 8px; | |
| position: relative; | |
| padding-left: 14px; | |
| line-height: 1.4; | |
| } | |
| .pro-list li::before { content: '•'; color: var(--accent-emerald); position: absolute; left: 0; } | |
| .con-list li::before { content: '•'; color: var(--accent-rose); position: absolute; left: 0; } | |
| .screener-sec-title { | |
| font-size: 14px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin: 24px 0 10px 0; | |
| color: var(--text-secondary); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| padding-bottom: 6px; | |
| } | |
| /* Settings cog & modal */ | |
| .settings-cog { | |
| background: rgba(255, 255, 255, 0.04); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| width: 38px; | |
| height: 38px; | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| transition: all 0.2s ease; | |
| } | |
| .settings-cog:hover { | |
| color: #fff; | |
| border-color: rgba(255, 255, 255, 0.15); | |
| transform: rotate(30deg); | |
| } | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| background: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(8px); | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .modal-overlay.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .modal { | |
| width: 450px; | |
| max-width: 90%; | |
| padding: 28px; | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .modal-title { | |
| font-size: 20px; | |
| font-weight: 700; | |
| } | |
| .modal-close { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-secondary); | |
| font-size: 20px; | |
| cursor: pointer; | |
| } | |
| .modal-close:hover { | |
| color: #fff; | |
| } | |
| /* Toast notifications */ | |
| .toast-container { | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| z-index: 10000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .toast { | |
| background: rgba(18, 22, 45, 0.9); | |
| border: 1px solid var(--card-border); | |
| backdrop-filter: blur(12px); | |
| padding: 14px 20px; | |
| border-radius: 10px; | |
| color: #fff; | |
| font-size: 13px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); | |
| animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; | |
| min-width: 280px; | |
| } | |
| .toast.success { border-left: 4px solid var(--accent-emerald); } | |
| .toast.error { border-left: 4px solid var(--accent-rose); } | |
| .toast.info { border-left: 4px solid var(--accent-blue); } | |
| /* Animations */ | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(6px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%) translateY(0); opacity: 0; } | |
| to { transform: translateX(0) translateY(0); opacity: 1; } | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.15); opacity: 0.7; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| @keyframes spin { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="container header-content"> | |
| <div class="logo-area"> | |
| <i class="fa-solid fa-chart-line logo-icon"></i> | |
| <div class="logo-title">NIFTY 50 Forecaster</div> | |
| <div class="logo-badge">Live</div> | |
| </div> | |
| <div class="system-status"> | |
| <div class="status-badge"> | |
| <span id="conn-dot" class="status-dot inactive"></span> | |
| <span id="conn-text">Connecting...</span> | |
| </div> | |
| <div class="status-badge"> | |
| <i class="fa-regular fa-clock"></i> | |
| <span id="server-time">--:--:-- IST</span> | |
| </div> | |
| <div class="status-badge"> | |
| <i class="fa-solid fa-calendar-day"></i> | |
| <span id="trading-day-text">Trading Day: --</span> | |
| </div> | |
| <button class="settings-cog" id="settings-btn" title="API Settings"> | |
| <i class="fa-solid fa-cog"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <main class="container"> | |
| <!-- Main Tabs --> | |
| <nav class="nav-tabs"> | |
| <button class="tab-btn active" data-tab="overview"> | |
| <i class="fa-solid fa-chart-pie"></i> Overview | |
| </button> | |
| <button class="tab-btn" data-tab="analytics"> | |
| <i class="fa-solid fa-chart-column"></i> Performance & Analytics | |
| </button> | |
| <button class="tab-btn" data-tab="tools"> | |
| <i class="fa-solid fa-toolbox"></i> Tools (Broker & Screener) | |
| </button> | |
| </nav> | |
| <!-- 1. OVERVIEW TAB CONTENT --> | |
| <div id="overview" class="tab-content active"> | |
| <div class="dashboard-grid"> | |
| <!-- Left Side: Prediction cards & MFE --> | |
| <div> | |
| <!-- Main Predictions Grid --> | |
| <div class="predictions-container"> | |
| <!-- T+5 Card --> | |
| <div class="glass-panel predict-card pending" id="card-t5"> | |
| <div class="card-header"> | |
| <span class="card-title">T+5 Prediction</span> | |
| <span class="card-badge" id="t5-badge">Pending</span> | |
| </div> | |
| <div class="signal-display"> | |
| <div class="signal-text" id="t5-signal"> | |
| <i class="fa-solid fa-hourglass-half"></i> PENDING | |
| </div> | |
| </div> | |
| <div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Confidence</span> | |
| <span class="metric-value" id="t5-conf">--%</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Probability (Up)</span> | |
| <span class="metric-value" id="t5-prob">--%</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Model</span> | |
| <span class="metric-value" id="t5-model">--</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Tomorrow Card --> | |
| <div class="glass-panel predict-card pending" id="card-tomorrow"> | |
| <div class="card-header"> | |
| <span class="card-title">Tomorrow Prediction</span> | |
| <span class="card-badge" id="tomorrow-badge">Pending</span> | |
| </div> | |
| <div class="signal-display"> | |
| <div class="signal-text" id="tomorrow-signal"> | |
| <i class="fa-solid fa-hourglass-half"></i> PENDING | |
| </div> | |
| </div> | |
| <div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Target Date</span> | |
| <span class="metric-value" id="tomorrow-target-date">--</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Probability (Up)</span> | |
| <span class="metric-value" id="tomorrow-prob">--%</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Model</span> | |
| <span class="metric-value" id="tomorrow-model">--</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- T+1 Card --> | |
| <div class="glass-panel predict-card pending" id="card-tplus1"> | |
| <div class="card-header"> | |
| <span class="card-title">T+1 Prediction</span> | |
| <span class="card-badge" id="tplus1-badge">Pending</span> | |
| </div> | |
| <div class="signal-display"> | |
| <div class="signal-text" id="tplus1-signal"> | |
| <i class="fa-solid fa-hourglass-half"></i> PENDING | |
| </div> | |
| </div> | |
| <div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Confidence</span> | |
| <span class="metric-value" id="tplus1-conf">--%</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Probability (Up)</span> | |
| <span class="metric-value" id="tplus1-prob">--%</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Model</span> | |
| <span class="metric-value" id="tplus1-model">--</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- MFE Regression Card --> | |
| <div class="glass-panel mfe-panel"> | |
| <div class="mfe-header"> | |
| <h3 class="title-font"><i class="fa-solid fa-expand"></i> Maximum Favorable Excursion (MFE) Bounds</h3> | |
| <span class="status-badge" id="mfe-date-badge">Session: --</span> | |
| </div> | |
| <div class="mfe-body"> | |
| <div class="mfe-side up"> | |
| <div class="mfe-side-title"><i class="fa-solid fa-circle-arrow-up"></i> Predicted Day High</div> | |
| <div class="mfe-pts" id="mfe-high-val">+-- pts</div> | |
| <div style="margin-top: 10px;"> | |
| <div class="metric-row"> | |
| <span class="metric-label">Target Level</span> | |
| <span class="metric-value" id="mfe-high-level">--</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Actual Day High</span> | |
| <span class="metric-value" id="mfe-high-actual">--</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mfe-side down"> | |
| <div class="mfe-side-title"><i class="fa-solid fa-circle-arrow-down"></i> Predicted Day Low</div> | |
| <div class="mfe-pts" id="mfe-low-val">--- pts</div> | |
| <div style="margin-top: 10px;"> | |
| <div class="metric-row"> | |
| <span class="metric-label">Target Level</span> | |
| <span class="metric-value" id="mfe-low-level">--</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Actual Day Low</span> | |
| <span class="metric-value" id="mfe-low-actual">--</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Side: Nifty Quote and Action Hub --> | |
| <div> | |
| <!-- Nifty Live Quote panel --> | |
| <div class="glass-panel quote-panel" style="margin-bottom: 24px;"> | |
| <div class="quote-header"> | |
| <span class="quote-symbol title-font">NIFTY 50 SPOT</span> | |
| <span class="logo-badge" id="quote-source">Source: --</span> | |
| </div> | |
| <div class="quote-price-area"> | |
| <div class="quote-price" id="quote-price">------</div> | |
| <div class="quote-change" id="quote-change">-- (--%)</div> | |
| </div> | |
| <div class="quote-grid"> | |
| <div class="quote-item"> | |
| <div style="color: var(--text-secondary); font-size: 11px;">Open</div> | |
| <div style="font-weight: 700; margin-top: 4px;" id="quote-open">--</div> | |
| </div> | |
| <div class="quote-item"> | |
| <div style="color: var(--text-secondary); font-size: 11px;">Prev Close</div> | |
| <div style="font-weight: 700; margin-top: 4px;" id="quote-prev">--</div> | |
| </div> | |
| <div class="quote-item"> | |
| <div style="color: var(--text-secondary); font-size: 11px;">Day High</div> | |
| <div style="font-weight: 700; margin-top: 4px;" id="quote-high">--</div> | |
| </div> | |
| <div class="quote-item"> | |
| <div style="color: var(--text-secondary); font-size: 11px;">Day Low</div> | |
| <div style="font-weight: 700; margin-top: 4px;" id="quote-low">--</div> | |
| </div> | |
| </div> | |
| <div style="font-size: 11px; color: var(--text-secondary); margin-top: 14px; text-align: right;" id="quote-time"> | |
| As of: -- | |
| </div> | |
| </div> | |
| <!-- Action Hub panel --> | |
| <div class="glass-panel action-panel"> | |
| <h4 class="action-title"><i class="fa-solid fa-gears"></i> Operations Control Hub</h4> | |
| <button class="btn btn-primary" id="btn-keepalive"> | |
| <i class="fa-solid fa-heartbeat"></i> Trigger Keepalive Ping | |
| </button> | |
| <button class="btn" id="btn-refresh-first5"> | |
| <i class="fa-solid fa-hourglass-start"></i> Refresh First 5-Mins (T+5) | |
| </button> | |
| <button class="btn" id="btn-refresh-daily"> | |
| <i class="fa-solid fa-sync"></i> Refresh Daily candles | |
| </button> | |
| <button class="btn" id="btn-refresh-close"> | |
| <i class="fa-solid fa-circle-check"></i> Refresh Market Close Data | |
| </button> | |
| <div id="data-staleness" style="font-size: 11px; margin-top: 6px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 8px;"> | |
| <div style="font-weight: 700; margin-bottom: 6px; color: var(--text-secondary); text-transform: uppercase;">Stale Checks:</div> | |
| <div style="display:flex; justify-content:space-between; margin-bottom: 4px;"> | |
| <span>Daily Data Stale:</span> | |
| <span id="stale-daily" class="badge-pill secondary">--</span> | |
| </div> | |
| <div style="display:flex; justify-content:space-between; margin-bottom: 4px;"> | |
| <span>Minutes Data Stale:</span> | |
| <span id="stale-minutes" class="badge-pill secondary">--</span> | |
| </div> | |
| <div style="display:flex; justify-content:space-between; margin-bottom: 4px;"> | |
| <span>T+5 Forecast Stale:</span> | |
| <span id="stale-t5" class="badge-pill secondary">--</span> | |
| </div> | |
| <div style="display:flex; justify-content:space-between;"> | |
| <span>T+1 Forecast Stale:</span> | |
| <span id="stale-tplus1" class="badge-pill secondary">--</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 2. ANALYTICS & BACKTEST TAB CONTENT --> | |
| <div id="analytics" class="tab-content"> | |
| <!-- Daily Closes chart --> | |
| <div class="glass-panel chart-panel"> | |
| <div class="chart-header"> | |
| <h3 class="title-font"><i class="fa-solid fa-chart-line"></i> NIFTY 50 Daily Closes History</h3> | |
| <span class="status-badge" id="chart-sessions-badge">Last 180 Days</span> | |
| </div> | |
| <div class="chart-wrapper"> | |
| <canvas id="closesChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- Accuracy Gauges --> | |
| <div class="accuracy-grid"> | |
| <!-- T+5 Accuracy --> | |
| <div class="glass-panel accuracy-card"> | |
| <div class="accuracy-header"> | |
| <h4 class="title-font">T+5 Model Accuracy</h4> | |
| <span class="badge-pill secondary" id="gauge-t5-count">0 Days</span> | |
| </div> | |
| <div class="gauge-area"> | |
| <span class="gauge-number" id="gauge-t5-pct">--%</span> | |
| <svg class="gauge-svg"> | |
| <circle class="gauge-track" cx="60" cy="60" r="50"></circle> | |
| <circle class="gauge-fill" id="gauge-t5-fill" cx="60" cy="60" r="50"></circle> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Live Correct</span> | |
| <span class="metric-value" id="gauge-t5-correct">0</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Validation Accuracy</span> | |
| <span class="metric-value" id="gauge-t5-val">--%</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Tomorrow Accuracy --> | |
| <div class="glass-panel accuracy-card"> | |
| <div class="accuracy-header"> | |
| <h4 class="title-font">Tomorrow Model Accuracy</h4> | |
| <span class="badge-pill secondary" id="gauge-tomorrow-count">0 Days</span> | |
| </div> | |
| <div class="gauge-area"> | |
| <span class="gauge-number" id="gauge-tomorrow-pct">--%</span> | |
| <svg class="gauge-svg"> | |
| <circle class="gauge-track" cx="60" cy="60" r="50"></circle> | |
| <circle class="gauge-fill" id="gauge-tomorrow-fill" cx="60" cy="60" r="50"></circle> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Live Correct</span> | |
| <span class="metric-value" id="gauge-tomorrow-correct">0</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Validation Accuracy</span> | |
| <span class="metric-value" id="gauge-tomorrow-val">--%</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- T+1 Accuracy --> | |
| <div class="glass-panel accuracy-card"> | |
| <div class="accuracy-header"> | |
| <h4 class="title-font">T+1 Model Accuracy</h4> | |
| <span class="badge-pill secondary" id="gauge-tplus1-count">0 Days</span> | |
| </div> | |
| <div class="gauge-area"> | |
| <span class="gauge-number" id="gauge-tplus1-pct">--%</span> | |
| <svg class="gauge-svg"> | |
| <circle class="gauge-track" cx="60" cy="60" r="50"></circle> | |
| <circle class="gauge-fill" id="gauge-tplus1-fill" cx="60" cy="60" r="50"></circle> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Live Correct</span> | |
| <span class="metric-value" id="gauge-tplus1-correct">0</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Validation Accuracy</span> | |
| <span class="metric-value" id="gauge-tplus1-val">--%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Tomorrow Predictions Live Track record --> | |
| <div class="glass-panel table-panel"> | |
| <div class="table-title">Tomorrow Forecast Live Track Record (Last 10 Days)</div> | |
| <div class="table-responsive"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Date</th> | |
| <th>Input Date</th> | |
| <th>Prediction</th> | |
| <th>Probability (Up)</th> | |
| <th>Actual Direction</th> | |
| <th>Move %</th> | |
| <th>Outcome</th> | |
| <th>Source</th> | |
| </tr> | |
| </thead> | |
| <tbody id="tomorrow-track-record-tbody"> | |
| <tr><td colspan="8" style="text-align: center; color: var(--text-secondary);">No historical records found.</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 3. TOOLS TAB CONTENT --> | |
| <div id="tools" class="tab-content"> | |
| <div class="tools-grid"> | |
| <!-- Left panel: Kotak Neo Broker --> | |
| <div class="glass-panel kotak-panel"> | |
| <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid rgba(255,255,255,0.05); padding-bottom:12px;"> | |
| <h3 class="title-font"><i class="fa-solid fa-key"></i> Kotak Securities Neo</h3> | |
| <span class="badge-pill secondary" id="kotak-status-badge">Checking...</span> | |
| </div> | |
| <!-- Auth Box if not authenticated --> | |
| <div id="kotak-auth-container" class="kotak-auth-box"> | |
| <h4 style="font-size: 13px; font-weight: 700; margin-bottom: 12px; color: var(--accent-purple);">Session Authentication Required</h4> | |
| <div class="form-group"> | |
| <label for="totp-input">Enter TOTP Token</label> | |
| <input type="text" id="totp-input" class="form-input" placeholder="6-digit auth code" maxlength="6"> | |
| </div> | |
| <button class="btn btn-primary" id="btn-kotak-login" style="width: 100%;"> | |
| <i class="fa-solid fa-right-to-bracket"></i> Validate Session Token | |
| </button> | |
| </div> | |
| <!-- Snapshot information if authenticated --> | |
| <div id="kotak-snapshot-container" style="display: none; margin-top: 16px;"> | |
| <h4 style="font-size: 13px; font-weight: 700; color: var(--accent-emerald);">Active Trading Portfolio Snap</h4> | |
| <div class="cash-grid"> | |
| <div class="cash-card"> | |
| <div class="cash-title">Net Value</div> | |
| <div class="cash-val" id="cash-net">--</div> | |
| </div> | |
| <div class="cash-card"> | |
| <div class="cash-title">Margin Used</div> | |
| <div class="cash-val" id="cash-margin">--</div> | |
| </div> | |
| <div class="cash-card"> | |
| <div class="cash-title">Available Cash</div> | |
| <div class="cash-val" id="cash-avail">--</div> | |
| </div> | |
| <div class="cash-card"> | |
| <div class="cash-title">Live P&L</div> | |
| <div class="cash-val" id="cash-pnl">--</div> | |
| </div> | |
| </div> | |
| <!-- Active holdings list --> | |
| <div class="screener-sec-title" style="margin-top: 20px;">Current Stock Holdings</div> | |
| <div class="table-responsive" style="max-height: 200px; overflow-y: auto;"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Symbol</th> | |
| <th>Qty</th> | |
| <th>Cost</th> | |
| <th>Market Val</th> | |
| <th>P&L</th> | |
| </tr> | |
| </thead> | |
| <tbody id="kotak-holdings-tbody"> | |
| <tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No stock holdings.</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right panel: Screener Scraper --> | |
| <div class="glass-panel screener-panel"> | |
| <h3 class="title-font" style="border-bottom:1px solid rgba(255,255,255,0.05); padding-bottom:12px; margin-bottom:16px;"> | |
| <i class="fa-solid fa-search"></i> Fundamental Stock Screener | |
| </h3> | |
| <div class="search-row"> | |
| <input type="text" id="screener-query" class="form-input" placeholder="Enter stock symbol (e.g., RELIANCE, TCS)"> | |
| <button class="btn btn-primary" id="btn-screener-search"> | |
| <i class="fa-solid fa-magnifying-glass"></i> Search | |
| </button> | |
| </div> | |
| <div id="screener-loader" style="display:none; text-align:center; padding: 40px 0;"> | |
| <i class="fa-solid fa-spinner fa-spin" style="font-size: 32px; color: var(--accent-purple);"></i> | |
| <div style="margin-top: 12px; font-size:13px; color: var(--text-secondary);">Fetching data from Screener.in...</div> | |
| </div> | |
| <!-- Search Result container --> | |
| <div id="screener-results" class="screener-results" style="display: none;"> | |
| <h4 class="title-font" id="screener-company-name" style="font-size: 20px; font-weight:700;">COMPANY NAME</h4> | |
| <!-- Ratios grid --> | |
| <div class="screener-sec-title">Key Ratios</div> | |
| <div class="quote-grid" id="screener-ratios-grid"></div> | |
| <!-- Pros & Cons --> | |
| <div class="pro-con-grid"> | |
| <div class="pro-list"> | |
| <h4><i class="fa-solid fa-thumbs-up"></i> Strengths (Pros)</h4> | |
| <ul class="bullet-list" id="screener-pros"></ul> | |
| </div> | |
| <div class="con-list"> | |
| <h4><i class="fa-solid fa-thumbs-down"></i> Weaknesses (Cons)</h4> | |
| <ul class="bullet-list" id="screener-cons"></ul> | |
| </div> | |
| </div> | |
| <!-- Growth metrics table --> | |
| <div class="screener-sec-title">Compound Growth Metrics</div> | |
| <div class="table-responsive"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Metric</th> | |
| <th>Period</th> | |
| <th>Growth % / Value</th> | |
| </tr> | |
| </thead> | |
| <tbody id="screener-growth-tbody"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Settings Modal --> | |
| <div class="modal-overlay" id="settings-modal"> | |
| <div class="glass-panel modal"> | |
| <div class="modal-header"> | |
| <h3 class="modal-title title-font"><i class="fa-solid fa-link"></i> API Endpoint Config</h3> | |
| <button class="modal-close" id="settings-close-btn">×</button> | |
| </div> | |
| <div class="form-group"> | |
| <label for="backend-url-input">Backend Base URL</label> | |
| <input type="text" id="backend-url-input" class="form-input" placeholder="e.g. https://domain.hf.space"> | |
| <small style="display:block; font-size:11px; color:var(--text-secondary); margin-top:6px;"> | |
| Leave blank to automatically connect to this webserver's origin. | |
| </small> | |
| </div> | |
| <button class="btn btn-primary" id="btn-save-settings" style="width: 100%;"> | |
| <i class="fa-solid fa-save"></i> Save Configuration | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Toast container --> | |
| <div class="toast-container" id="toast-container"></div> | |
| <script> | |
| // Global variables for Client State | |
| let apiBaseUrl = localStorage.getItem('forecast_api_url') || ''; | |
| let dashboardData = null; | |
| let chartInstance = null; | |
| // Elements | |
| const connDot = document.getElementById('conn-dot'); | |
| const connText = document.getElementById('conn-text'); | |
| const serverTimeEl = document.getElementById('server-time'); | |
| const tradingDayEl = document.getElementById('trading-day-text'); | |
| const settingsModal = document.getElementById('settings-modal'); | |
| const backendUrlInput = document.getElementById('backend-url-input'); | |
| // Toast notifications logic | |
| function showToast(message, type = 'info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| let iconClass = 'fa-circle-info'; | |
| if (type === 'success') iconClass = 'fa-circle-check'; | |
| if (type === 'error') iconClass = 'fa-circle-exclamation'; | |
| toast.innerHTML = ` | |
| <i class="fa-solid ${iconClass}"></i> | |
| <span>${message}</span> | |
| `; | |
| container.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.animation = 'slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) reverse forwards'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 4000); | |
| } | |
| // Active URL selector | |
| function getApiUrl(endpoint) { | |
| const base = apiBaseUrl.trim() || window.location.origin; | |
| return `${base.replace(/\/$/, '')}/${endpoint.replace(/^\//, '')}`; | |
| } | |
| // Settings Modal management | |
| document.getElementById('settings-btn').addEventListener('click', () => { | |
| backendUrlInput.value = apiBaseUrl; | |
| settingsModal.classList.add('active'); | |
| }); | |
| document.getElementById('settings-close-btn').addEventListener('click', () => { | |
| settingsModal.classList.remove('active'); | |
| }); | |
| document.getElementById('btn-save-settings').addEventListener('click', () => { | |
| const val = backendUrlInput.value.trim(); | |
| apiBaseUrl = val; | |
| localStorage.setItem('forecast_api_url', val); | |
| settingsModal.classList.remove('active'); | |
| showToast('API URL configuration updated!', 'success'); | |
| fetchDashboard(); | |
| }); | |
| // Tab Navigation management | |
| document.querySelectorAll('.tab-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| btn.classList.add('active'); | |
| const contentId = btn.getAttribute('data-tab'); | |
| document.getElementById(contentId).classList.add('active'); | |
| // Render chart when switching to analytics | |
| if (contentId === 'analytics' && dashboardData) { | |
| setTimeout(() => renderChart(dashboardData.charts.daily_closes), 100); | |
| } | |
| }); | |
| }); | |
| // Parse Float helper | |
| function formatVal(val, decimals = 2, suffix = '') { | |
| if (val === null || val === undefined || isNaN(val)) return '--'; | |
| return parseFloat(val).toFixed(decimals) + suffix; | |
| } | |
| // Main Dashboard API fetcher | |
| async function fetchDashboard() { | |
| try { | |
| connDot.className = 'status-dot pending'; | |
| connText.textContent = 'Syncing...'; | |
| const response = await fetch(getApiUrl('dashboard')); | |
| if (!response.ok) throw new Error(`HTTP Error status: ${response.status}`); | |
| const data = await response.json(); | |
| dashboardData = data; | |
| renderDashboard(data); | |
| connDot.className = 'status-dot active'; | |
| connText.textContent = 'Connected'; | |
| } catch (err) { | |
| console.error(err); | |
| connDot.className = 'status-dot inactive'; | |
| connText.textContent = 'Disconnected'; | |
| showToast('Failed to fetch dashboard data. Verify your API URL settings.', 'error'); | |
| } | |
| } | |
| // UI Renderer | |
| function renderDashboard(data) { | |
| // Header stats | |
| const status = data.data_status || {}; | |
| const dateStr = status.server_time_ist ? new Date(status.server_time_ist).toLocaleTimeString('en-US', {timeZone: 'Asia/Kolkata'}) + ' IST' : '--:--:-- IST'; | |
| serverTimeEl.textContent = dateStr; | |
| tradingDayEl.textContent = `Trading Day: ${status.is_trading_day ? 'Yes' : 'No'}`; | |
| // 1. T+5 Card | |
| const t5 = data.predictions.t5 || {}; | |
| const cardT5 = document.getElementById('card-t5'); | |
| const sigT5 = document.getElementById('t5-signal'); | |
| cardT5.className = 'glass-panel predict-card ' + (t5.available ? (t5.prediction === 'UP' ? 'up' : 'down') : 'pending'); | |
| document.getElementById('t5-badge').textContent = t5.status || 'Pending'; | |
| if (t5.available) { | |
| 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'; | |
| document.getElementById('t5-conf').textContent = formatVal(t5.confidence * 100, 1, '%'); | |
| document.getElementById('t5-prob').textContent = formatVal(t5.prob_up * 100, 1, '%'); | |
| document.getElementById('t5-model').textContent = t5.model_name || '--'; | |
| } else { | |
| sigT5.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING'; | |
| document.getElementById('t5-conf').textContent = '--%'; | |
| document.getElementById('t5-prob').textContent = '--%'; | |
| document.getElementById('t5-model').textContent = '--'; | |
| } | |
| // 2. Tomorrow Card | |
| const tom = data.predictions.tomorrow || {}; | |
| const cardTom = document.getElementById('card-tomorrow'); | |
| const sigTom = document.getElementById('tomorrow-signal'); | |
| cardTom.className = 'glass-panel predict-card ' + (tom.available ? (tom.prediction === 'UP' ? 'up' : 'down') : 'pending'); | |
| document.getElementById('tomorrow-badge').textContent = tom.status || 'Pending'; | |
| if (tom.available) { | |
| 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'; | |
| document.getElementById('tomorrow-target-date').textContent = tom.target_date || '--'; | |
| document.getElementById('tomorrow-prob').textContent = formatVal(tom.prob_up * 100, 1, '%'); | |
| document.getElementById('tomorrow-model').textContent = tom.source_model || tom.model_name || '--'; | |
| } else { | |
| sigTom.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING'; | |
| document.getElementById('tomorrow-target-date').textContent = '--'; | |
| document.getElementById('tomorrow-prob').textContent = '--%'; | |
| document.getElementById('tomorrow-model').textContent = '--'; | |
| } | |
| // 3. T+1 Card | |
| const tplus = data.predictions.tplus1 || {}; | |
| const cardTplus1 = document.getElementById('card-tplus1'); | |
| const sigTplus1 = document.getElementById('tplus1-signal'); | |
| cardTplus1.className = 'glass-panel predict-card ' + (tplus.available ? (tplus.prediction === 'UP' ? 'up' : 'down') : 'pending'); | |
| document.getElementById('tplus1-badge').textContent = tplus.status || 'Pending'; | |
| if (tplus.available) { | |
| 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'; | |
| document.getElementById('tplus1-conf').textContent = formatVal(tplus.confidence * 100, 1, '%'); | |
| document.getElementById('tplus1-prob').textContent = formatVal(tplus.prob_up * 100, 1, '%'); | |
| document.getElementById('tplus1-model').textContent = tplus.model_name || '--'; | |
| } else { | |
| sigTplus1.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING'; | |
| document.getElementById('tplus1-conf').textContent = '--%'; | |
| document.getElementById('tplus1-prob').textContent = '--%'; | |
| document.getElementById('tplus1-model').textContent = '--'; | |
| } | |
| // 4. MFE Regression Card | |
| const mfe = data.predictions.mfe || {}; | |
| const mfeLatest = mfe.latest || {}; | |
| const mfeSummary = mfe.summary || {}; | |
| document.getElementById('mfe-date-badge').textContent = `Session: ${mfeLatest.input_date || '--'}`; | |
| if (mfe.available && mfeLatest.predicted_up_points !== undefined) { | |
| document.getElementById('mfe-high-val').textContent = `+${formatVal(mfeLatest.predicted_up_points, 1)} pts`; | |
| document.getElementById('mfe-low-val').textContent = `-${formatVal(mfeLatest.predicted_down_points, 1)} pts`; | |
| const f5Close = parseFloat(mfeLatest.first5_close); | |
| document.getElementById('mfe-high-level').textContent = formatVal(f5Close + parseFloat(mfeLatest.predicted_up_points), 1); | |
| document.getElementById('mfe-low-level').textContent = formatVal(f5Close - parseFloat(mfeLatest.predicted_down_points), 1); | |
| // History metrics mapping | |
| const currentHistoryRow = (mfe.history || []).find(r => r.date === mfeLatest.input_date); | |
| if (currentHistoryRow) { | |
| document.getElementById('mfe-high-actual').textContent = formatVal(currentHistoryRow.actual_high, 1); | |
| document.getElementById('mfe-low-actual').textContent = formatVal(currentHistoryRow.actual_low, 1); | |
| } else { | |
| document.getElementById('mfe-high-actual').textContent = '--'; | |
| document.getElementById('mfe-low-actual').textContent = '--'; | |
| } | |
| } else { | |
| document.getElementById('mfe-high-val').textContent = '+-- pts'; | |
| document.getElementById('mfe-low-val').textContent = '--- pts'; | |
| document.getElementById('mfe-high-level').textContent = '--'; | |
| document.getElementById('mfe-low-level').textContent = '--'; | |
| document.getElementById('mfe-high-actual').textContent = '--'; | |
| document.getElementById('mfe-low-actual').textContent = '--'; | |
| } | |
| // 5. Nifty Spot Live Quote | |
| const quote = data.nifty_quote || {}; | |
| const quoteErr = data.nifty_quote_error; | |
| if (quoteErr) { | |
| document.getElementById('quote-price').textContent = 'UNAVAILABLE'; | |
| document.getElementById('quote-price').style.fontSize = '24px'; | |
| document.getElementById('quote-change').innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> ${quoteErr.message}`; | |
| document.getElementById('quote-change').className = 'quote-change down'; | |
| document.getElementById('quote-source').textContent = 'Error'; | |
| document.getElementById('quote-open').textContent = '--'; | |
| document.getElementById('quote-prev').textContent = '--'; | |
| document.getElementById('quote-high').textContent = '--'; | |
| document.getElementById('quote-low').textContent = '--'; | |
| document.getElementById('quote-time').textContent = 'Quote refresh failed.'; | |
| } else if (quote.last_traded_price !== undefined) { | |
| document.getElementById('quote-price').textContent = formatVal(quote.last_traded_price, 2); | |
| document.getElementById('quote-price').style.fontSize = '36px'; | |
| document.getElementById('quote-source').textContent = quote.exchange_segment || 'Index'; | |
| const changeVal = parseFloat(quote.change); | |
| const isUp = changeVal >= 0; | |
| document.getElementById('quote-change').innerHTML = ` | |
| <i class="fa-solid ${isUp ? 'fa-caret-up' : 'fa-caret-down'}"></i> | |
| ${isUp ? '+' : ''}${formatVal(changeVal, 2)} (${isUp ? '+' : ''}${formatVal(quote.change_pct, 2)}%) | |
| `; | |
| document.getElementById('quote-change').className = 'quote-change ' + (isUp ? 'up' : 'down'); | |
| document.getElementById('quote-open').textContent = formatVal(quote.open, 2); | |
| document.getElementById('quote-prev').textContent = formatVal(quote.close, 2); | |
| document.getElementById('quote-high').textContent = formatVal(quote.high, 2); | |
| document.getElementById('quote-low').textContent = formatVal(quote.low, 2); | |
| const timeStr = quote.as_of ? new Date(quote.as_of).toLocaleTimeString('en-US') : '--:--'; | |
| document.getElementById('quote-time').textContent = `As of: ${timeStr}`; | |
| } | |
| // 6. Action Hub - Stale details mapping | |
| const stale = data.data_status || {}; | |
| const formatStaleBadge = (elId, isStale) => { | |
| const el = document.getElementById(elId); | |
| el.textContent = isStale ? 'Stale' : 'Fresh'; | |
| el.className = `badge-pill ${isStale ? 'danger' : 'success'}`; | |
| }; | |
| formatStaleBadge('stale-daily', stale.daily_stale); | |
| formatStaleBadge('stale-minutes', stale.minutes_stale); | |
| formatStaleBadge('stale-t5', stale.t5_stale); | |
| formatStaleBadge('stale-tplus1', stale.tplus1_stale); | |
| // 7. Accuracy tab gauges update | |
| const liveAcc = data.live_accuracy || {}; | |
| // T+5 Gauge | |
| const t5Acc = liveAcc.t5 || {}; | |
| renderGauge('gauge-t5', t5Acc.accuracy, t5Acc.total, t5Acc.correct_count, t5.validation_accuracy); | |
| // Tomorrow Gauge | |
| const tomAcc = liveAcc.tomorrow || {}; | |
| renderGauge('gauge-tomorrow', tomAcc.accuracy, tomAcc.total, tomAcc.correct_count, tom.validation_accuracy); | |
| // T+1 Gauge | |
| const tplusAcc = liveAcc.tplus1 || {}; | |
| renderGauge('gauge-tplus1', tplusAcc.accuracy, tplusAcc.total, tplusAcc.correct_count, tplus.validation_accuracy); | |
| // 8. Tomorrow Live Track Record Table | |
| const trackRecord = data.charts.tomorrow_live_track_record || []; | |
| const trTbody = document.getElementById('tomorrow-track-record-tbody'); | |
| if (trackRecord.length > 0) { | |
| trTbody.innerHTML = ''; | |
| trackRecord.forEach(row => { | |
| const moveVal = parseFloat(row.actual_move) * 100; | |
| const moveClass = moveVal >= 0 ? 'success' : 'danger'; | |
| const correctClass = row.correct ? 'success' : 'danger'; | |
| trTbody.innerHTML += ` | |
| <tr> | |
| <td><strong>${row.date}</strong></td> | |
| <td>${row.input_date}</td> | |
| <td><span class="badge-pill ${row.prediction === 'UP' ? 'success' : 'danger'}">${row.prediction}</span></td> | |
| <td>${formatVal(row.prob_up * 100, 1, '%')}</td> | |
| <td><span class="badge-pill ${row.actual_direction === 'UP' ? 'success' : 'danger'}">${row.actual_direction}</span></td> | |
| <td><span class="badge-pill ${moveClass}">${moveVal >= 0 ? '+' : ''}${formatVal(moveVal, 2)}%</span></td> | |
| <td><span class="badge-pill ${correctClass}">${row.correct ? 'Correct' : 'Incorrect'}</span></td> | |
| <td><span style="color:var(--text-secondary);">${row.prediction_source || 'Rolling'}</span></td> | |
| </tr> | |
| `; | |
| }); | |
| } else { | |
| trTbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: var(--text-secondary);">No historical records found.</td></tr>`; | |
| } | |
| // 9. Kotak credentials/snapshots updates | |
| const kotakStatus = data.predictions.t5.kotak_status || data.kotak_status || {}; | |
| const kotakBadge = document.getElementById('kotak-status-badge'); | |
| if (kotakStatus.authenticated) { | |
| kotakBadge.textContent = 'Authenticated'; | |
| kotakBadge.className = 'badge-pill success'; | |
| document.getElementById('kotak-auth-container').style.display = 'none'; | |
| document.getElementById('kotak-snapshot-container').style.display = 'block'; | |
| // Display Limits & balances | |
| const limits = data.limits_summary || {}; | |
| document.getElementById('cash-net').textContent = formatVal(limits.net, 0); | |
| document.getElementById('cash-margin').textContent = formatVal(limits.margin_used, 0); | |
| let cashAvail = '--'; | |
| if (limits.net !== null && limits.margin_used !== null) { | |
| cashAvail = limits.net - limits.margin_used; | |
| } | |
| document.getElementById('cash-avail').textContent = formatVal(cashAvail, 0); | |
| const livePnl = (data.summary || {}).live_pnl; | |
| const pnlEl = document.getElementById('cash-pnl'); | |
| pnlEl.textContent = formatVal(livePnl, 0); | |
| pnlEl.style.color = livePnl >= 0 ? 'var(--accent-emerald)' : 'var(--accent-rose)'; | |
| // Holdings table mapping | |
| const holdings = data.holdings || []; | |
| const holdingsTbody = document.getElementById('kotak-holdings-tbody'); | |
| if (holdings.length > 0) { | |
| holdingsTbody.innerHTML = ''; | |
| holdings.forEach(row => { | |
| const pnlVal = parseFloat(row.pnl); | |
| holdingsTbody.innerHTML += ` | |
| <tr> | |
| <td><strong>${row.trading_symbol || row.symbol}</strong></td> | |
| <td>${row.quantity}</td> | |
| <td>${formatVal(row.average_price, 1)}</td> | |
| <td>${formatVal(row.market_value, 0)}</td> | |
| <td style="color: ${pnlVal >= 0 ? 'var(--accent-emerald)' : 'var(--accent-rose)'}; font-weight:700;"> | |
| ${pnlVal >= 0 ? '+' : ''}${formatVal(pnlVal, 0)} | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| } else { | |
| holdingsTbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No stock holdings.</td></tr>`; | |
| } | |
| } else { | |
| kotakBadge.textContent = 'Unauthorized'; | |
| kotakBadge.className = 'badge-pill danger'; | |
| document.getElementById('kotak-auth-container').style.display = 'block'; | |
| document.getElementById('kotak-snapshot-container').style.display = 'none'; | |
| } | |
| } | |
| // Render Gauge Helper | |
| function renderGauge(id, accuracy, total, correctCount, validationAcc) { | |
| const pct = accuracy !== null && accuracy !== undefined ? (accuracy * 100) : null; | |
| document.getElementById(`${id}-pct`).textContent = pct !== null ? `${pct.toFixed(0)}%` : '--%'; | |
| document.getElementById(`${id}-count`).textContent = `${total || 0} Sessions`; | |
| document.getElementById(`${id}-correct`).textContent = `${correctCount || 0} / ${total || 0}`; | |
| document.getElementById(`${id}-val`).textContent = formatVal(validationAcc * 100, 1, '%'); | |
| const strokeOffset = pct !== null ? (314.16 - (314.16 * pct) / 100) : 314.16; | |
| document.getElementById(`${id}-fill`).style.strokeDashoffset = strokeOffset; | |
| } | |
| // Render History Close line chart with Chart.js | |
| function renderChart(closes) { | |
| if (!closes || closes.length === 0) return; | |
| const ctx = document.getElementById('closesChart').getContext('2d'); | |
| const labels = closes.map(r => r.date.split(' ')[0]); | |
| const prices = closes.map(r => r.close); | |
| if (chartInstance) { | |
| chartInstance.destroy(); | |
| } | |
| chartInstance = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'NIFTY 50 Close', | |
| data: prices, | |
| borderColor: '#a855f7', | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| pointHoverRadius: 6, | |
| tension: 0.1, | |
| fill: true, | |
| backgroundColor: (context) => { | |
| const chart = context.chart; | |
| const {ctx, chartArea} = chart; | |
| if (!chartArea) return null; | |
| const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom); | |
| gradient.addColorStop(0, 'rgba(168, 85, 247, 0.25)'); | |
| gradient.addColorStop(1, 'rgba(59, 130, 246, 0.00)'); | |
| return gradient; | |
| } | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| backgroundColor: 'rgba(7, 9, 19, 0.9)', | |
| titleFont: { family: 'Outfit' }, | |
| bodyFont: { family: 'Inter' }, | |
| borderColor: 'rgba(255, 255, 255, 0.1)', | |
| borderWidth: 1 | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| grid: { color: 'rgba(255, 255, 255, 0.03)' }, | |
| ticks: { color: '#9ca3af', font: { family: 'Inter', size: 10 } } | |
| }, | |
| y: { | |
| grid: { color: 'rgba(255, 255, 255, 0.03)' }, | |
| ticks: { color: '#9ca3af', font: { family: 'Inter', size: 10 } } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Operation triggers logic helper | |
| async function runTrigger(btnId, endpoint, method = 'POST', successMsg) { | |
| const btn = document.getElementById(btnId); | |
| const originalHtml = btn.innerHTML; | |
| try { | |
| btn.disabled = true; | |
| btn.innerHTML = `<i class="fa-solid fa-spinner spin"></i> Processing...`; | |
| const response = await fetch(getApiUrl(endpoint), { method: method }); | |
| if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`); | |
| showToast(successMsg, 'success'); | |
| fetchDashboard(); | |
| } catch (err) { | |
| console.error(err); | |
| showToast(`Operation failed: ${err.message}`, 'error'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = originalHtml; | |
| } | |
| } | |
| // Action Hub triggers | |
| document.getElementById('btn-keepalive').addEventListener('click', () => { | |
| runTrigger('btn-keepalive', 'cron/keepalive', 'GET', 'Keepalive keep-warm routine triggered!'); | |
| }); | |
| document.getElementById('btn-refresh-first5').addEventListener('click', () => { | |
| runTrigger('btn-refresh-first5', 'prediction/refresh-first5', 'POST', 'First 5-minutes prediction refreshed!'); | |
| }); | |
| document.getElementById('btn-refresh-daily').addEventListener('click', () => { | |
| runTrigger('btn-refresh-daily', 'data/refresh-daily', 'POST', 'Daily parquet data refreshed!'); | |
| }); | |
| document.getElementById('btn-refresh-close').addEventListener('click', () => { | |
| runTrigger('btn-refresh-close', 'data/refresh-market-close', 'POST', 'Market close refresh sequence executed!'); | |
| }); | |
| // Kotak Securities login action | |
| document.getElementById('btn-kotak-login').addEventListener('click', async () => { | |
| const totpInput = document.getElementById('totp-input'); | |
| const totpVal = totpInput.value.trim(); | |
| if (!totpVal || totpVal.length < 6) { | |
| showToast('A valid 6-digit TOTP code is required.', 'error'); | |
| return; | |
| } | |
| const btn = document.getElementById('btn-kotak-login'); | |
| const originalHtml = btn.innerHTML; | |
| try { | |
| btn.disabled = true; | |
| btn.innerHTML = `<i class="fa-solid fa-spinner spin"></i> Authenticating...`; | |
| const response = await fetch(getApiUrl('kotak/auth/totp'), { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ totp: totpVal }) | |
| }); | |
| if (!response.ok) { | |
| const data = await response.json(); | |
| throw new Error(data.detail || `Authentication rejected: status ${response.status}`); | |
| } | |
| showToast('Kotak Neo session authenticated successfully!', 'success'); | |
| totpInput.value = ''; | |
| fetchDashboard(); | |
| } catch (err) { | |
| console.error(err); | |
| showToast(`Session auth failed: ${err.message}`, 'error'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = originalHtml; | |
| } | |
| }); | |
| // Fundamental Screener search search logic | |
| document.getElementById('btn-screener-search').addEventListener('click', async () => { | |
| const queryInput = document.getElementById('screener-query'); | |
| const query = queryInput.value.trim().toUpperCase(); | |
| if (!query) { | |
| showToast('Please specify a ticker symbol (e.g. INFYS, RELIANCE).', 'error'); | |
| return; | |
| } | |
| const resultsContainer = document.getElementById('screener-results'); | |
| const loader = document.getElementById('screener-loader'); | |
| const btn = document.getElementById('btn-screener-search'); | |
| try { | |
| btn.disabled = true; | |
| loader.style.display = 'block'; | |
| resultsContainer.style.display = 'none'; | |
| const response = await fetch(getApiUrl(`info/${query}`)); | |
| if (!response.ok) { | |
| const data = await response.json(); | |
| throw new Error(data.detail || `Server returned error status ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| renderScreenerResult(data); | |
| resultsContainer.style.display = 'block'; | |
| } catch (err) { | |
| console.error(err); | |
| showToast(`Screener search failed: ${err.message}`, 'error'); | |
| } finally { | |
| btn.disabled = false; | |
| loader.style.display = 'none'; | |
| } | |
| }); | |
| // Search renderer helper | |
| function renderScreenerResult(data) { | |
| document.getElementById('screener-company-name').textContent = data.ticker || 'STOCK ANALYSIS'; | |
| // Ratios | |
| const ratiosGrid = document.getElementById('screener-ratios-grid'); | |
| ratiosGrid.innerHTML = ''; | |
| const metrics = data.key_metrics || {}; | |
| for (const [name, val] of Object.entries(metrics)) { | |
| ratiosGrid.innerHTML += ` | |
| <div class="quote-item"> | |
| <div style="color: var(--text-secondary); font-size: 11px;">${name}</div> | |
| <div style="font-weight: 700; margin-top: 4px; font-size: 14px;">${val}</div> | |
| </div> | |
| `; | |
| } | |
| // Pros / Cons | |
| const prosUl = document.getElementById('screener-pros'); | |
| const consUl = document.getElementById('screener-cons'); | |
| prosUl.innerHTML = ''; | |
| consUl.innerHTML = ''; | |
| const pros = data.pros || []; | |
| if (pros.length > 0) { | |
| pros.forEach(item => prosUl.innerHTML += `<li>${item}</li>`); | |
| } else { | |
| prosUl.innerHTML = `<li>No data listed.</li>`; | |
| } | |
| const cons = data.cons || []; | |
| if (cons.length > 0) { | |
| cons.forEach(item => consUl.innerHTML += `<li>${item}</li>`); | |
| } else { | |
| consUl.innerHTML = `<li>No data listed.</li>`; | |
| } | |
| // Growth table | |
| const growthTbody = document.getElementById('screener-growth-tbody'); | |
| const growth = data.growth || []; | |
| if (growth.length > 0) { | |
| growthTbody.innerHTML = ''; | |
| growth.forEach(row => { | |
| growthTbody.innerHTML += ` | |
| <tr> | |
| <td><strong>${row.Metric}</strong></td> | |
| <td>${row.Period}</td> | |
| <td><span class="badge-pill secondary" style="font-size:12px; font-weight:700;">${row.Value}</span></td> | |
| </tr> | |
| `; | |
| }); | |
| } else { | |
| growthTbody.innerHTML = `<tr><td colspan="3" style="text-align: center; color: var(--text-secondary);">No growth metrics available.</td></tr>`; | |
| } | |
| } | |
| // Initialize fetching loop and setup timer ticks | |
| fetchDashboard(); | |
| setInterval(fetchDashboard, 60000); // dashboard reload once every minute | |
| // Incremental clock tick loop | |
| setInterval(() => { | |
| if (dashboardData && dashboardData.data_status) { | |
| const now = new Date(); | |
| serverTimeEl.textContent = now.toLocaleTimeString('en-US', {timeZone: 'Asia/Kolkata'}) + ' IST'; | |
| } | |
| }, 1000); | |
| </script> | |
| </body> | |
| </html> | |