1+ <!-- src/dashboard/dashboard.html -->
2+ <!DOCTYPE html>
3+ < html lang ="en ">
4+ < head >
5+ < meta charset ="UTF-8 ">
6+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
7+ < title > TimeFusion Dashboard</ title >
8+ < link href ="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap " rel ="stylesheet ">
9+ < script src ="https://cdn.jsdelivr.net/npm/chart.js "> </ script >
10+ < script src ="https://cdn.jsdelivr.net/npm/tippy.js@6/dist/tippy-bundle.umd.min.js "> </ script >
11+ < link rel ="stylesheet " href ="https://cdn.jsdelivr.net/npm/tippy.js@6/themes/light.css ">
12+ </ head >
13+ < body >
14+ < div class ="sidebar ">
15+ < h2 > TimeFusion</ h2 >
16+ < nav >
17+ < a href ="# " class ="active "> Dashboard</ a >
18+ < a href ="/ingest "> Ingest</ a >
19+ < a href ="/data "> Data</ a >
20+ < button id ="theme-toggle "> Toggle Theme</ button >
21+ </ nav >
22+ </ div >
23+ < div class ="main-content ">
24+ < header >
25+ < h1 > Dashboard</ h1 >
26+ < p > Real-time application insights</ p >
27+ < button id ="refresh-btn "> Refresh</ button >
28+ </ header >
29+ < main >
30+ < section class ="stats-grid ">
31+ < div class ="stat-card " data-tippy-content ="Application uptime in seconds ">
32+ < h2 > Uptime (s)</ h2 >
33+ < p id ="uptime "> {{uptime}}</ p >
34+ </ div >
35+ < div class ="stat-card " data-tippy-content ="Current ingestion queue size ">
36+ < h2 > Queue Size</ h2 >
37+ < p id ="queue_size "> {{queue_size}}</ p >
38+ </ div >
39+ < div class ="stat-card " data-tippy-content ="Database health status ">
40+ < h2 > DB Status</ h2 >
41+ < p id ="db_status " class ="{{db_status}} "> {{db_status}}</ p >
42+ </ div >
43+ < div class ="stat-card " data-tippy-content ="Ingestion rate (records/sec) ">
44+ < h2 > Ingestion Rate</ h2 >
45+ < p id ="ingestion_rate "> {{ingestion_rate}}</ p >
46+ </ div >
47+ < div class ="stat-card " data-tippy-content ="Error rate (errors/sec) ">
48+ < h2 > Error Rate</ h2 >
49+ < p id ="error_rate "> {{error_rate}}</ p >
50+ </ div >
51+ < div class ="stat-card " data-tippy-content ="Total records in database ">
52+ < h2 > Total Records</ h2 >
53+ < p id ="total_records "> {{total_records}}</ p >
54+ </ div >
55+ < div class ="stat-card " data-tippy-content ="Average latency in milliseconds ">
56+ < h2 > Avg Latency (ms)</ h2 >
57+ < p id ="avg_latency "> {{avg_latency}}</ p >
58+ </ div >
59+ </ section >
60+ < section class ="charts-grid ">
61+ < div class ="chart-container ">
62+ < h2 > Request Trends (Last Hour)</ h2 >
63+ < canvas id ="trendsChart "> </ canvas >
64+ </ div >
65+ </ section >
66+ < section class ="tables-grid ">
67+ < div class ="table-container ">
68+ < h2 > Recent Ingestion Statuses</ h2 >
69+ < input type ="text " id ="status-filter " placeholder ="Filter by ID or Status ">
70+ < table >
71+ < thead >
72+ < tr >
73+ < th > ID</ th >
74+ < th > Status</ th >
75+ </ tr >
76+ </ thead >
77+ < tbody id ="status-table "> </ tbody >
78+ </ table >
79+ < div class ="pagination " id ="status-pagination "> </ div >
80+ </ div >
81+ < div class ="table-container ">
82+ < h2 > Recent Records</ h2 >
83+ < input type ="text " id ="records-filter " placeholder ="Filter by Project ID or Timestamp ">
84+ < table >
85+ < thead >
86+ < tr >
87+ < th > Project ID</ th >
88+ < th > Record ID</ th >
89+ < th > Timestamp</ th >
90+ < th > Latency (ms)</ th >
91+ </ tr >
92+ </ thead >
93+ < tbody id ="records-table "> </ tbody >
94+ </ table >
95+ < div class ="pagination " id ="records-pagination "> </ div >
96+ </ div >
97+ </ section >
98+ < section class ="logs-container ">
99+ < h2 > Real-Time Logs</ h2 >
100+ < pre id ="logs "> Connecting to log stream...</ pre >
101+ </ section >
102+ </ main >
103+ < footer >
104+ < p > © 2025 TimeFusion</ p >
105+ </ footer >
106+ </ div >
107+
108+ < style >
109+ : root {
110+ --primary : # 3498db ;
111+ --secondary : # 2980b9 ;
112+ --text : # 2c3e50 ;
113+ --bg : # f5f7fa ;
114+ --card-bg : # ffffff ;
115+ --shadow : rgba (0 , 0 , 0 , 0.1 );
116+ --success : # 27ae60 ;
117+ --error : # e74c3c ;
118+ }
119+ [data-theme = "dark" ] {
120+ --primary : # 5dade2 ;
121+ --secondary : # 4e91c6 ;
122+ --text : # ecf0f1 ;
123+ --bg : # 2c3e50 ;
124+ --card-bg : # 34495e ;
125+ --shadow : rgba (0 , 0 , 0 , 0.3 );
126+ }
127+ * { margin : 0 ; padding : 0 ; box-sizing : border-box; font-family : 'Inter' , sans-serif; }
128+ body { background : var (--bg ); color : var (--text ); display : flex; transition : all 0.3s ; }
129+ .sidebar { width : 250px ; background : var (--secondary ); color : white; padding : 2rem 1rem ; height : 100vh ; position : fixed; }
130+ .sidebar h2 { font-size : 1.8rem ; font-weight : 700 ; margin-bottom : 2rem ; }
131+ .sidebar nav a , .sidebar nav button { display : block; color : white; padding : 0.75rem 1rem ; margin : 0.5rem 0 ; border-radius : 8px ; text-decoration : none; background : none; border : none; cursor : pointer; transition : background 0.3s ; }
132+ .sidebar nav a .active , .sidebar nav a : hover , .sidebar nav button : hover { background : var (--primary ); }
133+ .main-content { margin-left : 250px ; flex-grow : 1 ; padding : 2rem ; }
134+ header { background : linear-gradient (135deg , var (--primary ), var (--secondary )); color : white; padding : 2rem ; border-radius : 15px ; text-align : center; margin-bottom : 2rem ; box-shadow : 0 4px 12px var (--shadow ); }
135+ header h1 { font-size : 2.5rem ; font-weight : 700 ; margin-bottom : 0.5rem ; }
136+ # refresh-btn { position : absolute; right : 2rem ; top : 50% ; transform : translateY (-50% ); background : # ffffff33 ; color : white; border : none; padding : 0.5rem 1rem ; border-radius : 5px ; cursor : pointer; }
137+ # refresh-btn : hover { background : # ffffff66 ; }
138+ .stats-grid { display : grid; grid-template-columns : repeat (auto-fit, minmax (150px , 1fr )); gap : 1.5rem ; margin-bottom : 2rem ; }
139+ .stat-card { background : var (--card-bg ); border-radius : 15px ; padding : 1.5rem ; box-shadow : 0 6px 20px var (--shadow ); transition : transform 0.3s ; }
140+ .stat-card : hover { transform : translateY (-5px ); }
141+ .stat-card h2 { font-size : 1.1rem ; color : var (--primary ); margin-bottom : 0.75rem ; }
142+ .stat-card p { font-size : 1.6rem ; font-weight : 500 ; }
143+ .stat-card p .healthy { color : var (--success ); }
144+ .stat-card p .unhealthy { color : var (--error ); }
145+ .charts-grid , .tables-grid { display : grid; grid-template-columns : repeat (auto-fit, minmax (300px , 1fr )); gap : 1.5rem ; margin-bottom : 2rem ; }
146+ .chart-container , .table-container , .logs-container { background : var (--card-bg ); border-radius : 15px ; padding : 2rem ; box-shadow : 0 6px 20px var (--shadow ); }
147+ .chart-container h2 , .table-container h2 , .logs-container h2 { font-size : 1.4rem ; margin-bottom : 1rem ; }
148+ input [type = "text" ] { width : 100% ; padding : 0.75rem ; margin-bottom : 1rem ; border : 1px solid # ddd ; border-radius : 8px ; }
149+ table { width : 100% ; border-collapse : collapse; }
150+ th , td { padding : 1rem ; text-align : left; border-bottom : 1px solid # eee ; }
151+ th { background : var (--primary ); color : white; }
152+ .pagination { margin-top : 1rem ; text-align : center; }
153+ .pagination button { background : var (--primary ); color : white; border : none; padding : 0.5rem 1rem ; margin : 0 0.25rem ; border-radius : 5px ; cursor : pointer; }
154+ .pagination button : disabled { background : # ccc ; cursor : not-allowed; }
155+ .logs-container pre { font-size : 0.9rem ; white-space : pre-wrap; max-height : 200px ; overflow-y : auto; }
156+ footer { text-align : center; padding : 1rem ; font-size : 0.9rem ; }
157+ @keyframes fadeIn { from { opacity : 0 ; transform : translateY (10px ); } to { opacity : 1 ; transform : translateY (0 ); } }
158+ .fade-in { animation : fadeIn 0.5s ease-in; }
159+ </ style >
160+
161+ < script >
162+ const recentStatuses = JSON . parse ( '{{ recent_statuses }}' ) ;
163+ const recentRecords = JSON . parse ( '{{ recent_records | safe }}' ) ;
164+ const requestTrends = JSON . parse ( '{{ request_trends | safe }}' ) ;
165+
166+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
167+ // Theme Toggle
168+ const themeToggle = document . getElementById ( 'theme-toggle' ) ;
169+ themeToggle . addEventListener ( 'click' , ( ) => {
170+ document . documentElement . dataset . theme = document . documentElement . dataset . theme === 'dark' ? 'light' : 'dark' ;
171+ localStorage . setItem ( 'theme' , document . documentElement . dataset . theme ) ;
172+ tippy ( '[data-tippy-content]' ) . forEach ( t => t . setProps ( { theme : document . documentElement . dataset . theme } ) ) ;
173+ } ) ;
174+ if ( localStorage . getItem ( 'theme' ) === 'dark' ) document . documentElement . dataset . theme = 'dark' ;
175+
176+ // Tooltips
177+ tippy ( '[data-tippy-content]' , { theme : document . documentElement . dataset . theme || 'light' } ) ;
178+
179+ // Request Trends Chart
180+ const trendsChart = new Chart ( document . getElementById ( 'trendsChart' ) . getContext ( '2d' ) , {
181+ type : 'line' ,
182+ data : {
183+ labels : requestTrends . map ( t => t . timestamp ) ,
184+ datasets : [ { label : 'Requests' , data : requestTrends . map ( t => parseInt ( t . requests ) ) , borderColor : '#3498db' , fill : false } ]
185+ } ,
186+ options : { responsive : true , scales : { y : { beginAtZero : true } , x : { title : { display : true , text : 'Time' } } } }
187+ } ) ;
188+
189+ // Table Population with Pagination and Filtering
190+ function populateTable ( tableId , data , filterId , rowFn , paginationId , itemsPerPage = 5 ) {
191+ const table = document . getElementById ( tableId ) ;
192+ const filter = document . getElementById ( filterId ) ;
193+ const pagination = document . getElementById ( paginationId ) ;
194+ let filteredData = [ ...data ] ;
195+ let currentPage = 1 ;
196+
197+ const render = ( ) => {
198+ const start = ( currentPage - 1 ) * itemsPerPage ;
199+ const end = start + itemsPerPage ;
200+ table . innerHTML = '' ;
201+ filteredData . slice ( start , end ) . forEach ( item => {
202+ const row = document . createElement ( 'tr' ) ;
203+ row . classList . add ( 'fade-in' ) ;
204+ row . innerHTML = rowFn ( item ) ;
205+ table . appendChild ( row ) ;
206+ } ) ;
207+ pagination . innerHTML = '' ;
208+ for ( let i = 1 ; i <= Math . ceil ( filteredData . length / itemsPerPage ) ; i ++ ) {
209+ const btn = document . createElement ( 'button' ) ;
210+ btn . textContent = i ;
211+ btn . disabled = i === currentPage ;
212+ btn . addEventListener ( 'click' , ( ) => { currentPage = i ; render ( ) ; } ) ;
213+ pagination . appendChild ( btn ) ;
214+ }
215+ } ;
216+
217+ filter . addEventListener ( 'input' , ( ) => {
218+ filteredData = data . filter ( item => Object . values ( item ) . some ( v => v . toString ( ) . toLowerCase ( ) . includes ( filter . value . toLowerCase ( ) ) ) ) ;
219+ currentPage = 1 ;
220+ render ( ) ;
221+ } ) ;
222+ render ( ) ;
223+ }
224+
225+ populateTable ( 'status-table' , recentStatuses , 'status-filter' , s => `<td>${ s . id . substring ( 0 , 8 ) } ...</td><td>${ s . status } </td>` , 'status-pagination' ) ;
226+ populateTable ( 'records-table' , recentRecords , 'records-filter' , r => `
227+ <td>${ r . project_id } </td>
228+ <td>${ r . id . substring ( 0 , 8 ) } ...</td>
229+ <td>${ r . timestamp } </td>
230+ <td>${ ( parseInt ( r . duration_ns ) / 1000000 ) . toFixed ( 2 ) } </td>
231+ ` , 'records-pagination' ) ;
232+
233+ // WebSocket for Real-Time Logs
234+ const logs = document . getElementById ( 'logs' ) ;
235+ const ws = new WebSocket ( `ws://${ location . host } /ws/logs` ) ;
236+ ws . onmessage = ( event ) => logs . textContent += `\n${ event . data } ` ;
237+ ws . onerror = ( ) => logs . textContent = 'Log stream error' ;
238+ ws . onclose = ( ) => logs . textContent += '\nLog stream closed' ;
239+
240+ // Refresh Button
241+ document . getElementById ( 'refresh-btn' ) . addEventListener ( 'click' , ( ) => {
242+ fetch ( '/dashboard' )
243+ . then ( res => res . text ( ) )
244+ . then ( html => {
245+ const doc = new DOMParser ( ) . parseFromString ( html , 'text/html' ) ;
246+ document . body . innerHTML = doc . body . innerHTML ;
247+
248+ // Reinitialize
249+ [ 'uptime' , 'queue_size' , 'db_status' , 'ingestion_rate' , 'error_rate' , 'total_records' , 'avg_latency' ] . forEach ( id =>
250+ document . getElementById ( id ) . textContent = doc . getElementById ( id ) . textContent ) ;
251+ const newTrends = JSON . parse ( doc . querySelector ( 'script' ) . textContent . match ( / c o n s t r e q u e s t T r e n d s = ( .+ ?) ; / ) [ 1 ] ) ;
252+ trendsChart . data . labels = newTrends . map ( t => t . timestamp ) ;
253+ trendsChart . data . datasets [ 0 ] . data = newTrends . map ( t => parseInt ( t . requests ) ) ;
254+ trendsChart . update ( ) ;
255+ populateTable ( 'status-table' , JSON . parse ( doc . querySelector ( 'script' ) . textContent . match ( / c o n s t r e c e n t S t a t u s e s = ( .+ ?) ; / ) [ 1 ] ) , 'status-filter' , s => `<td>${ s . id . substring ( 0 , 8 ) } ...</td><td>${ s . status } </td>` , 'status-pagination' ) ;
256+ populateTable ( 'records-table' , JSON . parse ( doc . querySelector ( 'script' ) . textContent . match ( / c o n s t r e c e n t R e c o r d s = ( .+ ?) ; / ) [ 1 ] ) , 'records-filter' , r => `
257+ <td>${ r . project_id } </td>
258+ <td>${ r . id . substring ( 0 , 8 ) } ...</td>
259+ <td>${ r . timestamp } </td>
260+ <td>${ ( parseInt ( r . duration_ns ) / 1000000 ) . toFixed ( 2 ) } </td>
261+ ` , 'records-pagination' ) ;
262+ tippy ( '[data-tippy-content]' , { theme : document . documentElement . dataset . theme || 'light' } ) ;
263+ } ) ;
264+ } ) ;
265+ } ) ;
266+ </ script >
267+ </ body >
268+ </ html >
0 commit comments