Skip to content

Commit 101f63d

Browse files
OgeonX-AiAitomates
andauthored
feat: add swarm dashboard tooling and package manifest (#26)
* feat(promptimprover): add swarm dashboard tooling and package manifest - dashboard/View-SwarmDashboard.ps1: console-based live tail of trace log (C:\PersonalRepo\.planning\traces.jsonl), refreshed every 2s - dashboard/server.js: minimal Node HTTP server serving dashboard/index.html and a /data endpoint that streams the last 20 trace log entries as JSON - dashboard/index.html: browser table view polling /data every 2s to render swarm run traces (time, span, persona, action, tokens, reasoning) - package.json: manifest exposing `dashboard` (PowerShell) and `dashboard-web` (Node server) npm scripts - run-dashboard.ps1: entry point that launches View-SwarmDashboard.ps1 * fix(dashboard): bind loopback, fallback JSON, prevent XSS --------- Co-authored-by: Kim Harjamäki <kim.harjamaki@prosimo.fi>
1 parent 8975f96 commit 101f63d

5 files changed

Lines changed: 178 additions & 0 deletions

File tree

dashboard/View-SwarmDashboard.ps1

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
$traceFile = "C:\\PersonalRepo\\.planning\\traces.jsonl"
2+
$clearString = [char]27 + "[2J" + [char]27 + "[H"
3+
4+
while ($true) {
5+
Write-Host -NoNewline $clearString
6+
Write-Host "================================================================================" -ForegroundColor Cyan
7+
Write-Host " LIVE SWARM DASHBOARD (2026 OTel Architecture) " -ForegroundColor White -BackgroundColor DarkBlue
8+
Write-Host "================================================================================`n" -ForegroundColor Cyan
9+
10+
if (Test-Path $traceFile) {
11+
# Read the last 15 lines so the dashboard doesn't overflow
12+
$rawLines = Get-Content -Path $traceFile -Tail 15 -ErrorAction SilentlyContinue
13+
14+
$logs = @()
15+
foreach ($line in $rawLines) {
16+
try {
17+
if (-not [string]::IsNullOrWhiteSpace($line)) {
18+
$logs += $line | ConvertFrom-Json -ErrorAction Stop
19+
}
20+
} catch {
21+
# Skip invalid JSON lines
22+
}
23+
}
24+
25+
if ($logs.Count -gt 0) {
26+
$formattedLogs = foreach ($log in $logs) {
27+
$reason = $log.reasoning_path
28+
if ($null -ne $reason -and $reason.Length -gt 60) {
29+
$reason = $reason.Substring(0,57) + "..."
30+
}
31+
32+
[PSCustomObject]@{
33+
Time = [datetime]::Parse($log.timestamp).ToString("HH:mm:ss")
34+
Span = if ($log.span_id) { $log.span_id } else { "N/A" }
35+
Persona = if ($log.persona) { $log.persona } else { "Unknown" }
36+
Action = if ($log.action_type) { $log.action_type } else { "Unknown" }
37+
Tokens = if ($log.metrics.total_tokens) { $log.metrics.total_tokens } else { 0 }
38+
Reasoning = $reason
39+
}
40+
}
41+
$formattedLogs | Format-Table -AutoSize
42+
} else {
43+
Write-Host "No valid traces found yet." -ForegroundColor Yellow
44+
}
45+
} else {
46+
Write-Host "Waiting for traces... ($traceFile not found)" -ForegroundColor Yellow
47+
}
48+
49+
Write-Host "`nPress Ctrl+C to exit. Refreshing every 2 seconds..." -ForegroundColor DarkGray
50+
Start-Sleep -Seconds 2
51+
}

dashboard/index.html

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Promptimprover Swarm Dashboard</title>
7+
<link rel="stylesheet" href="style.css" />
8+
</head>
9+
<body>
10+
<header>
11+
<h1>Promptimprover Swarm Dashboard</h1>
12+
</header>
13+
<main>
14+
<table id="dashboard">
15+
<thead>
16+
<tr>
17+
<th>Time</th>
18+
<th>Span</th>
19+
<th>Persona</th>
20+
<th>Action</th>
21+
<th>Tokens</th>
22+
<th>Reasoning</th>
23+
</tr>
24+
</thead>
25+
<tbody></tbody>
26+
</table>
27+
</main>
28+
<script>
29+
async function fetchData() {
30+
const resp = await fetch('/data');
31+
const logs = await resp.json();
32+
const tbody = document.querySelector('#dashboard tbody');
33+
tbody.innerHTML = '';
34+
logs.reverse().forEach(log => {
35+
const tr = document.createElement('tr');
36+
const fmtTime = new Date(log.timestamp).toLocaleTimeString();
37+
const reason = log.reasoning_path && log.reasoning_path.length > 60 ? log.reasoning_path.slice(0,57) + '...' : log.reasoning_path;
38+
['time', 'span', 'persona', 'action', 'tokens', 'reasoning'].forEach((k, i) => {
39+
const td = document.createElement('td');
40+
const values = [fmtTime, log.span_id || 'N/A', log.persona || 'Unknown', log.action_type || 'Unknown', log.metrics?.total_tokens || 0, reason || ''];
41+
td.textContent = values[i];
42+
tr.appendChild(td);
43+
});
44+
tbody.appendChild(tr);
45+
});
46+
}
47+
setInterval(fetchData, 2000);
48+
fetchData();
49+
</script>
50+
</body>
51+
</html>

dashboard/server.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// C:\PersonalRepo\portfolio\Promptimprover\dashboard\server.js
2+
const http = require('http');
3+
const fs = require('fs');
4+
const path = require('path');
5+
const url = require('url');
6+
const { once } = require('events');
7+
8+
const TRACE_FILE = path.resolve('C:/PersonalRepo/.planning/traces.jsonl');
9+
const STATIC_DIR = path.join(__dirname);
10+
const PORT = 3000;
11+
12+
// Helper: read last N lines from trace file
13+
async function readLastLines(filePath, maxLines = 20) {
14+
if (!fs.existsSync(filePath)) return [];
15+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
16+
let data = '';
17+
stream.on('data', chunk => { data += chunk; });
18+
await once(stream, 'end');
19+
const lines = data.trim().split(/\r?\n/);
20+
const recent = lines.slice(-maxLines);
21+
// parse each JSON line safely
22+
return recent.map(l => {
23+
try { return JSON.parse(l); } catch (_) { return null; }
24+
}).filter(Boolean);
25+
}
26+
27+
function serveStatic(res, filePath, contentType) {
28+
fs.readFile(filePath, (err, content) => {
29+
if (err) {
30+
res.writeHead(404);
31+
res.end('Not found');
32+
} else {
33+
res.writeHead(200, { 'Content-Type': contentType });
34+
res.end(content);
35+
}
36+
});
37+
}
38+
39+
const server = http.createServer(async (req, res) => {
40+
const parsed = url.parse(req.url, true);
41+
if (parsed.pathname === '/' || parsed.pathname === '/index.html') {
42+
serveStatic(res, path.join(STATIC_DIR, 'index.html'), 'text/html');
43+
} else if (parsed.pathname === '/style.css') {
44+
serveStatic(res, path.join(STATIC_DIR, 'style.css'), 'text/css');
45+
} else if (parsed.pathname === '/data') {
46+
try {
47+
const logs = await readLastLines(TRACE_FILE, 20);
48+
res.writeHead(200, { 'Content-Type': 'application/json' });
49+
res.end(JSON.stringify(logs));
50+
} catch (e) {
51+
res.writeHead(500);
52+
res.end('Error reading trace file');
53+
}
54+
} else {
55+
res.writeHead(404);
56+
res.end('Not found');
57+
}
58+
});
59+
60+
server.listen(PORT, '127.0.0.1', () => {
61+
console.log(`Promptimprover dashboard listening on http://127.0.0.1:${PORT}`);
62+
});

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "promptimprover",
3+
"version": "0.1.0",
4+
"scripts": {
5+
"dashboard": "powershell -NoProfile -ExecutionPolicy Bypass -File ./run-dashboard.ps1",
6+
"dashboard-web": "node dashboard/server.js"
7+
}
8+
}

run-dashboard.ps1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
$script = Join-Path $PSScriptRoot "dashboard\View-SwarmDashboard.ps1"
2+
if (Test-Path $script) {
3+
powershell -NoProfile -ExecutionPolicy Bypass -File $script
4+
} else {
5+
Write-Host "Dashboard script not found at $script" -ForegroundColor Red
6+
}

0 commit comments

Comments
 (0)