forge_runtime/dashboard/
pages.rs

1use axum::{
2    extract::{Path, State},
3    response::Html,
4};
5
6use super::DashboardState;
7
8/// Dashboard page handlers.
9pub struct DashboardPages;
10
11/// Base HTML template.
12fn base_template(title: &str, content: &str, active_page: &str) -> String {
13    format!(
14        r#"<!DOCTYPE html>
15<html lang="en">
16<head>
17    <meta charset="UTF-8">
18    <meta name="viewport" content="width=device-width, initial-scale=1.0">
19    <title>{title} - FORGE Dashboard</title>
20    <link rel="stylesheet" href="/_dashboard/assets/styles.css">
21    <script src="/_dashboard/assets/chart.js" defer></script>
22    <script src="/_dashboard/assets/main.js" defer></script>
23</head>
24<body>
25    <div class="dashboard">
26        <nav class="sidebar">
27            <div class="sidebar-header">
28                <h1>⚒️ FORGE</h1>
29                <span class="version">v{version}</span>
30            </div>
31            <ul class="nav-links">
32                <li><a href="/_dashboard" class="{overview_active}">📊 Overview</a></li>
33                <li><a href="/_dashboard/metrics" class="{metrics_active}">📈 Metrics</a></li>
34                <li><a href="/_dashboard/logs" class="{logs_active}">📝 Logs</a></li>
35                <li><a href="/_dashboard/traces" class="{traces_active}">🔍 Traces</a></li>
36                <li><a href="/_dashboard/alerts" class="{alerts_active}">🚨 Alerts</a></li>
37                <li><a href="/_dashboard/jobs" class="{jobs_active}">⚙️ Jobs</a></li>
38                <li><a href="/_dashboard/workflows" class="{workflows_active}">🔄 Workflows</a></li>
39                <li><a href="/_dashboard/crons" class="{crons_active}">⏰ Crons</a></li>
40                <li><a href="/_dashboard/cluster" class="{cluster_active}">🖥️ Cluster</a></li>
41            </ul>
42            <div class="sidebar-footer">
43                <a href="https://github.com/example/forge" target="_blank">Documentation</a>
44            </div>
45        </nav>
46        <main class="content">
47            <header class="content-header">
48                <h2>{title}</h2>
49                <div class="header-actions">
50                    <select id="time-range" class="time-range-select">
51                        <option value="5m">Last 5 minutes</option>
52                        <option value="15m">Last 15 minutes</option>
53                        <option value="1h" selected>Last hour</option>
54                        <option value="6h">Last 6 hours</option>
55                        <option value="24h">Last 24 hours</option>
56                        <option value="7d">Last 7 days</option>
57                    </select>
58                    <button id="refresh-btn" class="btn btn-secondary">↻ Refresh</button>
59                </div>
60            </header>
61            <div class="content-body">
62                {content}
63            </div>
64        </main>
65    </div>
66</body>
67</html>"#,
68        title = title,
69        content = content,
70        version = env!("CARGO_PKG_VERSION"),
71        overview_active = if active_page == "overview" {
72            "active"
73        } else {
74            ""
75        },
76        metrics_active = if active_page == "metrics" {
77            "active"
78        } else {
79            ""
80        },
81        logs_active = if active_page == "logs" { "active" } else { "" },
82        traces_active = if active_page == "traces" {
83            "active"
84        } else {
85            ""
86        },
87        alerts_active = if active_page == "alerts" {
88            "active"
89        } else {
90            ""
91        },
92        jobs_active = if active_page == "jobs" { "active" } else { "" },
93        workflows_active = if active_page == "workflows" {
94            "active"
95        } else {
96            ""
97        },
98        crons_active = if active_page == "crons" { "active" } else { "" },
99        cluster_active = if active_page == "cluster" {
100            "active"
101        } else {
102            ""
103        },
104    )
105}
106
107/// Overview/index page.
108pub async fn index(State(_state): State<DashboardState>) -> Html<String> {
109    let content = r#"
110        <div class="stats-grid">
111            <div class="stat-card">
112                <div class="stat-icon">📊</div>
113                <div class="stat-content">
114                    <h3>Requests</h3>
115                    <p class="stat-value" id="stat-requests">-</p>
116                    <p class="stat-label">per second</p>
117                </div>
118            </div>
119            <div class="stat-card">
120                <div class="stat-icon">⚡</div>
121                <div class="stat-content">
122                    <h3>Latency (p99)</h3>
123                    <p class="stat-value" id="stat-latency">-</p>
124                    <p class="stat-label">milliseconds</p>
125                </div>
126            </div>
127            <div class="stat-card">
128                <div class="stat-icon">❌</div>
129                <div class="stat-content">
130                    <h3>Error Rate</h3>
131                    <p class="stat-value" id="stat-errors">-</p>
132                    <p class="stat-label">percent</p>
133                </div>
134            </div>
135            <div class="stat-card">
136                <div class="stat-icon">🔌</div>
137                <div class="stat-content">
138                    <h3>Connections</h3>
139                    <p class="stat-value" id="stat-connections">-</p>
140                    <p class="stat-label">active</p>
141                </div>
142            </div>
143        </div>
144
145        <div class="charts-row">
146            <div class="chart-container">
147                <h3>Request Rate</h3>
148                <canvas id="requests-chart"></canvas>
149            </div>
150            <div class="chart-container">
151                <h3>Response Times</h3>
152                <canvas id="latency-chart"></canvas>
153            </div>
154        </div>
155
156        <div class="panels-row">
157            <div class="panel">
158                <h3>🚨 Active Alerts</h3>
159                <div id="active-alerts" class="alert-list">
160                    <p class="empty-state">No active alerts</p>
161                </div>
162            </div>
163            <div class="panel">
164                <h3>📝 Recent Logs</h3>
165                <div id="recent-logs" class="log-list">
166                    <p class="empty-state">Loading...</p>
167                </div>
168            </div>
169        </div>
170
171        <div class="panel">
172            <h3>🖥️ Cluster Nodes</h3>
173            <div id="cluster-nodes" class="nodes-grid">
174                <p class="empty-state">Loading...</p>
175            </div>
176        </div>
177    "#;
178
179    Html(base_template("Overview", content, "overview"))
180}
181
182/// Metrics page.
183pub async fn metrics(State(_state): State<DashboardState>) -> Html<String> {
184    let content = r#"
185        <div class="metrics-controls">
186            <input type="text" id="metric-search" placeholder="Search metrics..." class="search-input">
187            <select id="metric-type" class="select-input">
188                <option value="">All Types</option>
189                <option value="counter">Counters</option>
190                <option value="gauge">Gauges</option>
191                <option value="histogram">Histograms</option>
192            </select>
193        </div>
194
195        <div class="metrics-grid" id="metrics-list">
196            <p class="empty-state">Loading metrics...</p>
197        </div>
198
199        <!-- Metric Detail Modal -->
200        <div id="metric-modal" class="modal" style="display: none;">
201            <div class="modal-content">
202                <div class="modal-header">
203                    <h3 id="metric-modal-title">Metric Detail</h3>
204                    <button class="modal-close" onclick="closeMetricModal()">×</button>
205                </div>
206                <div class="modal-body" id="metric-modal-body">
207                    <p>Loading...</p>
208                </div>
209            </div>
210        </div>
211    "#;
212
213    Html(base_template("Metrics", content, "metrics"))
214}
215
216/// Logs page.
217pub async fn logs(State(_state): State<DashboardState>) -> Html<String> {
218    let content = r##"
219        <div class="logs-controls">
220            <input type="text" id="log-search" placeholder="Search logs..." class="search-input">
221            <select id="log-level" class="select-input">
222                <option value="">All Levels</option>
223                <option value="error">Error</option>
224                <option value="warn">Warning</option>
225                <option value="info">Info</option>
226                <option value="debug">Debug</option>
227            </select>
228            <button id="log-stream-toggle" class="btn btn-primary">▶ Live Stream</button>
229        </div>
230
231        <div class="logs-table-container">
232            <table class="logs-table" id="logs-table">
233                <thead>
234                    <tr>
235                        <th>Time</th>
236                        <th>Level</th>
237                        <th>Message</th>
238                        <th>Trace</th>
239                    </tr>
240                </thead>
241                <tbody id="logs-tbody">
242                    <tr class="empty-row">
243                        <td colspan="4">Loading logs...</td>
244                    </tr>
245                </tbody>
246            </table>
247        </div>
248
249        <div class="logs-pagination">
250            <button class="btn btn-secondary" id="logs-prev">← Previous</button>
251            <span id="logs-page-info">Page 1</span>
252            <button class="btn btn-secondary" id="logs-next">Next →</button>
253        </div>
254    "##;
255
256    Html(base_template("Logs", content, "logs"))
257}
258
259/// Traces page.
260pub async fn traces(State(_state): State<DashboardState>) -> Html<String> {
261    let content = r#"
262        <div class="traces-controls">
263            <input type="text" id="trace-search" placeholder="Search by trace ID, service, or operation..." class="search-input">
264            <input type="number" id="min-duration" placeholder="Min duration (ms)" class="number-input">
265            <label class="checkbox-label">
266                <input type="checkbox" id="errors-only"> Errors only
267            </label>
268        </div>
269
270        <div class="traces-table-container">
271            <table class="traces-table" id="traces-table">
272                <thead>
273                    <tr>
274                        <th>Trace ID</th>
275                        <th>Root Span</th>
276                        <th>Service</th>
277                        <th>Duration</th>
278                        <th>Spans</th>
279                        <th>Status</th>
280                        <th>Started</th>
281                    </tr>
282                </thead>
283                <tbody id="traces-tbody">
284                    <tr class="empty-row">
285                        <td colspan="7">Loading traces...</td>
286                    </tr>
287                </tbody>
288            </table>
289        </div>
290    "#;
291
292    Html(base_template("Traces", content, "traces"))
293}
294
295/// Trace detail page with waterfall visualization.
296pub async fn trace_detail(
297    State(_state): State<DashboardState>,
298    Path(trace_id): Path<String>,
299) -> Html<String> {
300    let content = format!(
301        r##"
302        <div class="trace-header">
303            <div class="trace-info">
304                <h3>Trace: <code id="trace-id-display">{trace_id}</code></h3>
305                <div id="trace-summary" class="trace-summary">
306                    <span class="summary-item">Loading...</span>
307                </div>
308            </div>
309            <div class="trace-actions">
310                <button class="btn btn-secondary" onclick="copyTraceId()">📋 Copy ID</button>
311                <button class="btn btn-secondary" onclick="history.back()">← Back</button>
312            </div>
313        </div>
314
315        <div class="trace-waterfall-container">
316            <div class="waterfall-header">
317                <div class="waterfall-labels">
318                    <span class="label-service">Service / Operation</span>
319                </div>
320                <div class="waterfall-timeline">
321                    <div class="timeline-ruler" id="timeline-ruler"></div>
322                </div>
323            </div>
324            <div class="waterfall-body" id="waterfall-body">
325                <p class="empty-state">Loading spans...</p>
326            </div>
327        </div>
328
329        <div class="trace-details-panel">
330            <div class="panel span-list-panel">
331                <h4>Span Tree</h4>
332                <div id="span-tree" class="span-tree">
333                    <p class="empty-state">Loading...</p>
334                </div>
335            </div>
336            <div class="panel span-details-panel" id="span-details">
337                <h4>Span Details</h4>
338                <p class="empty-state">Select a span to view details</p>
339            </div>
340        </div>
341
342        <div class="panel">
343            <h4>Span Attributes</h4>
344            <div class="tabs">
345                <button class="tab active" data-tab="attributes">Attributes</button>
346                <button class="tab" data-tab="events">Events</button>
347                <button class="tab" data-tab="logs">Logs</button>
348            </div>
349            <div class="tab-content" id="span-attributes-content">
350                <p class="empty-state">Select a span to view attributes</p>
351            </div>
352        </div>
353
354        <script>
355            const traceId = '{trace_id}';
356
357            function copyTraceId() {{
358                navigator.clipboard.writeText(traceId);
359                showToast('Trace ID copied!');
360            }}
361
362            function showToast(message) {{
363                const toast = document.createElement('div');
364                toast.className = 'toast';
365                toast.textContent = message;
366                document.body.appendChild(toast);
367                setTimeout(() => toast.remove(), 2000);
368            }}
369
370            document.addEventListener('DOMContentLoaded', function() {{
371                loadTraceDetail(traceId);
372            }});
373        </script>
374    "##,
375        trace_id = trace_id
376    );
377
378    Html(base_template("Trace Detail", &content, "traces"))
379}
380
381/// Alerts page.
382pub async fn alerts(State(_state): State<DashboardState>) -> Html<String> {
383    let content = r#"
384        <div class="alerts-summary">
385            <div class="alert-stat critical">
386                <span class="count" id="alerts-critical">-</span>
387                <span class="label">Critical</span>
388            </div>
389            <div class="alert-stat warning">
390                <span class="count" id="alerts-warning">-</span>
391                <span class="label">Warning</span>
392            </div>
393            <div class="alert-stat info">
394                <span class="count" id="alerts-info">-</span>
395                <span class="label">Info</span>
396            </div>
397        </div>
398
399        <div class="tabs">
400            <button class="tab active" data-tab="active">Active Alerts</button>
401            <button class="tab" data-tab="history">Alert History</button>
402            <button class="tab" data-tab="rules">Alert Rules</button>
403        </div>
404
405        <div class="tab-content" id="alerts-content">
406            <div class="empty-state">
407                <p>Alerts not yet configured</p>
408                <p class="subtitle">Configure alert rules in forge.toml to enable alerting</p>
409            </div>
410        </div>
411    "#;
412
413    Html(base_template("Alerts", content, "alerts"))
414}
415
416/// Jobs page.
417pub async fn jobs(State(_state): State<DashboardState>) -> Html<String> {
418    let content = r#"
419        <div class="jobs-stats">
420            <div class="job-stat">
421                <span class="count" id="jobs-pending">-</span>
422                <span class="label">Pending</span>
423            </div>
424            <div class="job-stat">
425                <span class="count" id="jobs-running">-</span>
426                <span class="label">Running</span>
427            </div>
428            <div class="job-stat">
429                <span class="count" id="jobs-completed">-</span>
430                <span class="label">Completed</span>
431            </div>
432            <div class="job-stat error">
433                <span class="count" id="jobs-failed">-</span>
434                <span class="label">Failed</span>
435            </div>
436        </div>
437
438        <div class="tabs">
439            <button class="tab active" data-tab="queue">Queue</button>
440            <button class="tab" data-tab="running">Running</button>
441            <button class="tab" data-tab="history">History</button>
442            <button class="tab" data-tab="dead-letter">Dead Letter</button>
443        </div>
444
445        <div class="jobs-table-container" id="jobs-content">
446            <table class="jobs-table">
447                <thead>
448                    <tr>
449                        <th>Job ID</th>
450                        <th>Type</th>
451                        <th>Priority</th>
452                        <th>Status</th>
453                        <th>Progress</th>
454                        <th>Attempts</th>
455                        <th>Created</th>
456                        <th>Error</th>
457                    </tr>
458                </thead>
459                <tbody id="jobs-tbody">
460                    <tr class="empty-row">
461                        <td colspan="8">Loading jobs...</td>
462                    </tr>
463                </tbody>
464            </table>
465        </div>
466
467        <!-- Job Detail Modal -->
468        <div id="job-modal" class="modal" style="display:none;">
469            <div class="modal-content">
470                <div class="modal-header">
471                    <h3>Job Details</h3>
472                    <button class="modal-close" onclick="closeJobModal()">&times;</button>
473                </div>
474                <div class="modal-body" id="job-modal-body">
475                    Loading...
476                </div>
477            </div>
478        </div>
479    "#;
480
481    Html(base_template("Jobs", content, "jobs"))
482}
483
484/// Workflows page.
485pub async fn workflows(State(_state): State<DashboardState>) -> Html<String> {
486    let content = r#"
487        <div class="workflows-stats">
488            <div class="workflow-stat">
489                <span class="count" id="workflows-running">-</span>
490                <span class="label">Running</span>
491            </div>
492            <div class="workflow-stat">
493                <span class="count" id="workflows-completed">-</span>
494                <span class="label">Completed</span>
495            </div>
496            <div class="workflow-stat">
497                <span class="count" id="workflows-waiting">-</span>
498                <span class="label">Waiting</span>
499            </div>
500            <div class="workflow-stat error">
501                <span class="count" id="workflows-failed">-</span>
502                <span class="label">Failed</span>
503            </div>
504        </div>
505
506        <div class="workflows-table-container">
507            <table class="workflows-table">
508                <thead>
509                    <tr>
510                        <th>Run ID</th>
511                        <th>Workflow</th>
512                        <th>Version</th>
513                        <th>Status</th>
514                        <th>Current Step</th>
515                        <th>Started</th>
516                        <th>Error</th>
517                    </tr>
518                </thead>
519                <tbody id="workflows-tbody">
520                    <tr class="empty-row">
521                        <td colspan="7">Loading workflows...</td>
522                    </tr>
523                </tbody>
524            </table>
525        </div>
526
527        <!-- Workflow Detail Modal -->
528        <div id="workflow-modal" class="modal" style="display:none;">
529            <div class="modal-content">
530                <div class="modal-header">
531                    <h3>Workflow Details</h3>
532                    <button class="modal-close" onclick="closeWorkflowModal()">&times;</button>
533                </div>
534                <div class="modal-body" id="workflow-modal-body">
535                    Loading...
536                </div>
537            </div>
538        </div>
539    "#;
540
541    Html(base_template("Workflows", content, "workflows"))
542}
543
544/// Crons page.
545pub async fn crons(State(_state): State<DashboardState>) -> Html<String> {
546    let content = r#"
547        <div class="crons-stats">
548            <div class="cron-stat">
549                <span class="count" id="crons-active">-</span>
550                <span class="label">Active</span>
551            </div>
552            <div class="cron-stat">
553                <span class="count" id="crons-paused">-</span>
554                <span class="label">Paused</span>
555            </div>
556            <div class="cron-stat success">
557                <span class="count" id="crons-success-rate">-</span>
558                <span class="label">Success Rate</span>
559            </div>
560            <div class="cron-stat">
561                <span class="count" id="crons-next-run">-</span>
562                <span class="label">Next Run</span>
563            </div>
564        </div>
565
566        <div class="crons-table-container">
567            <table class="crons-table">
568                <thead>
569                    <tr>
570                        <th>Name</th>
571                        <th>Schedule</th>
572                        <th>Status</th>
573                        <th>Last Run</th>
574                        <th>Last Result</th>
575                        <th>Next Run</th>
576                        <th>Avg Duration</th>
577                        <th>Actions</th>
578                    </tr>
579                </thead>
580                <tbody id="crons-tbody">
581                    <tr class="empty-row">
582                        <td colspan="8">Loading cron jobs...</td>
583                    </tr>
584                </tbody>
585            </table>
586        </div>
587
588        <div class="panel">
589            <h3>📊 Recent Executions</h3>
590            <div class="chart-container">
591                <canvas id="cron-executions-chart"></canvas>
592            </div>
593        </div>
594
595        <div class="panel">
596            <h3>📜 Execution History</h3>
597            <div class="cron-history-table-container">
598                <table class="cron-history-table">
599                    <thead>
600                        <tr>
601                            <th>Cron</th>
602                            <th>Started</th>
603                            <th>Duration</th>
604                            <th>Status</th>
605                            <th>Error</th>
606                        </tr>
607                    </thead>
608                    <tbody id="cron-history-tbody">
609                        <tr class="empty-row">
610                            <td colspan="5">Loading history...</td>
611                        </tr>
612                    </tbody>
613                </table>
614            </div>
615        </div>
616    "#;
617
618    Html(base_template("Crons", content, "crons"))
619}
620
621/// Cluster page.
622pub async fn cluster(State(_state): State<DashboardState>) -> Html<String> {
623    let content = r#"
624        <div class="cluster-health" id="cluster-health-panel">
625            <div class="health-indicator" id="health-indicator">
626                <span class="health-icon" id="health-icon">...</span>
627                <span class="health-text" id="health-text">Loading...</span>
628            </div>
629            <div class="cluster-info">
630                <span id="node-count">- Nodes</span>
631                <span>|</span>
632                <span id="leader-info">Leader: -</span>
633            </div>
634        </div>
635
636        <div class="nodes-grid" id="nodes-grid">
637            <p class="empty-state">Loading nodes...</p>
638        </div>
639
640        <div class="panel">
641            <h3>Leadership</h3>
642            <table class="leadership-table">
643                <thead>
644                    <tr>
645                        <th>Role</th>
646                        <th>Leader Node</th>
647                    </tr>
648                </thead>
649                <tbody id="leadership-tbody">
650                    <tr class="empty-row">
651                        <td colspan="2">Loading leaders...</td>
652                    </tr>
653                </tbody>
654            </table>
655        </div>
656    "#;
657
658    Html(base_template("Cluster", content, "cluster"))
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn test_base_template() {
667        let html = base_template("Test", "<p>Content</p>", "overview");
668        assert!(html.contains("Test - FORGE Dashboard"));
669        assert!(html.contains("<p>Content</p>"));
670        assert!(html.contains("class=\"active\""));
671    }
672}