forge_runtime/dashboard/
pages.rs1use axum::{
2 extract::{Path, State},
3 response::Html,
4};
5
6use super::DashboardState;
7
8pub struct DashboardPages;
10
11fn 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
107pub 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
182pub 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
216pub 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
259pub 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
295pub 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
381pub 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
416pub 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()">×</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
484pub 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()">×</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
544pub 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
621pub 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}