From e85554ac1d8eed44787b722871b2b4ef4dcd7cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Harjam=C3=A4ki?= Date: Mon, 6 Jul 2026 12:30:11 +0300 Subject: [PATCH 1/2] 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 --- dashboard/View-SwarmDashboard.ps1 | 51 ++++++++++++++++++++++++++ dashboard/index.html | 53 +++++++++++++++++++++++++++ dashboard/server.js | 61 +++++++++++++++++++++++++++++++ package.json | 8 ++++ run-dashboard.ps1 | 6 +++ 5 files changed, 179 insertions(+) create mode 100644 dashboard/View-SwarmDashboard.ps1 create mode 100644 dashboard/index.html create mode 100644 dashboard/server.js create mode 100644 package.json create mode 100644 run-dashboard.ps1 diff --git a/dashboard/View-SwarmDashboard.ps1 b/dashboard/View-SwarmDashboard.ps1 new file mode 100644 index 0000000..d03173d --- /dev/null +++ b/dashboard/View-SwarmDashboard.ps1 @@ -0,0 +1,51 @@ +$traceFile = "C:\\PersonalRepo\\.planning\\traces.jsonl" +$clearString = [char]27 + "[2J" + [char]27 + "[H" + +while ($true) { + Write-Host -NoNewline $clearString + Write-Host "================================================================================" -ForegroundColor Cyan + Write-Host " LIVE SWARM DASHBOARD (2026 OTel Architecture) " -ForegroundColor White -BackgroundColor DarkBlue + Write-Host "================================================================================`n" -ForegroundColor Cyan + + if (Test-Path $traceFile) { + # Read the last 15 lines so the dashboard doesn't overflow + $rawLines = Get-Content -Path $traceFile -Tail 15 -ErrorAction SilentlyContinue + + $logs = @() + foreach ($line in $rawLines) { + try { + if (-not [string]::IsNullOrWhiteSpace($line)) { + $logs += $line | ConvertFrom-Json -ErrorAction Stop + } + } catch { + # Skip invalid JSON lines + } + } + + if ($logs.Count -gt 0) { + $formattedLogs = foreach ($log in $logs) { + $reason = $log.reasoning_path + if ($null -ne $reason -and $reason.Length -gt 60) { + $reason = $reason.Substring(0,57) + "..." + } + + [PSCustomObject]@{ + Time = [datetime]::Parse($log.timestamp).ToString("HH:mm:ss") + Span = if ($log.span_id) { $log.span_id } else { "N/A" } + Persona = if ($log.persona) { $log.persona } else { "Unknown" } + Action = if ($log.action_type) { $log.action_type } else { "Unknown" } + Tokens = if ($log.metrics.total_tokens) { $log.metrics.total_tokens } else { 0 } + Reasoning = $reason + } + } + $formattedLogs | Format-Table -AutoSize + } else { + Write-Host "No valid traces found yet." -ForegroundColor Yellow + } + } else { + Write-Host "Waiting for traces... ($traceFile not found)" -ForegroundColor Yellow + } + + Write-Host "`nPress Ctrl+C to exit. Refreshing every 2 seconds..." -ForegroundColor DarkGray + Start-Sleep -Seconds 2 +} diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..5d84af3 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,53 @@ + + + + + + Promptimprover Swarm Dashboard + + + +
+

Promptimprover Swarm Dashboard

+
+
+ + + + + + + + + + + + +
TimeSpanPersonaActionTokensReasoning
+
+ + + diff --git a/dashboard/server.js b/dashboard/server.js new file mode 100644 index 0000000..a99c7c6 --- /dev/null +++ b/dashboard/server.js @@ -0,0 +1,61 @@ +// C:\PersonalRepo\portfolio\Promptimprover\dashboard\server.js +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const url = require('url'); +const { once } = require('events'); + +const TRACE_FILE = path.resolve('C:/PersonalRepo/.planning/traces.jsonl'); +const STATIC_DIR = path.join(__dirname); +const PORT = 3000; + +// Helper: read last N lines from trace file +async function readLastLines(filePath, maxLines = 20) { + const stream = fs.createReadStream(filePath, { encoding: 'utf8' }); + let data = ''; + stream.on('data', chunk => { data += chunk; }); + await once(stream, 'end'); + const lines = data.trim().split(/\r?\n/); + const recent = lines.slice(-maxLines); + // parse each JSON line safely + return recent.map(l => { + try { return JSON.parse(l); } catch (_) { return null; } + }).filter(Boolean); +} + +function serveStatic(res, filePath, contentType) { + fs.readFile(filePath, (err, content) => { + if (err) { + res.writeHead(404); + res.end('Not found'); + } else { + res.writeHead(200, { 'Content-Type': contentType }); + res.end(content); + } + }); +} + +const server = http.createServer(async (req, res) => { + const parsed = url.parse(req.url, true); + if (parsed.pathname === '/' || parsed.pathname === '/index.html') { + serveStatic(res, path.join(STATIC_DIR, 'index.html'), 'text/html'); + } else if (parsed.pathname === '/style.css') { + serveStatic(res, path.join(STATIC_DIR, 'style.css'), 'text/css'); + } else if (parsed.pathname === '/data') { + try { + const logs = await readLastLines(TRACE_FILE, 20); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(logs)); + } catch (e) { + res.writeHead(500); + res.end('Error reading trace file'); + } + } else { + res.writeHead(404); + res.end('Not found'); + } +}); + +server.listen(PORT, () => { + console.log(`Promptimprover dashboard listening on http://localhost:${PORT}`); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..662fd35 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "promptimprover", + "version": "0.1.0", + "scripts": { + "dashboard": "powershell -NoProfile -ExecutionPolicy Bypass -File ./run-dashboard.ps1", + "dashboard-web": "node dashboard/server.js" + } +} diff --git a/run-dashboard.ps1 b/run-dashboard.ps1 new file mode 100644 index 0000000..e9501e8 --- /dev/null +++ b/run-dashboard.ps1 @@ -0,0 +1,6 @@ +$script = Join-Path $PSScriptRoot "dashboard\View-SwarmDashboard.ps1" +if (Test-Path $script) { + powershell -NoProfile -ExecutionPolicy Bypass -File $script +} else { + Write-Host "Dashboard script not found at $script" -ForegroundColor Red +} From 23aa35549b58b6284f79c9b6b1be2dea3c27c14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Harjam=C3=A4ki?= Date: Mon, 6 Jul 2026 18:30:31 +0300 Subject: [PATCH 2/2] fix(dashboard): bind loopback, fallback JSON, prevent XSS --- dashboard/index.html | 14 ++++++-------- dashboard/server.js | 5 +++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/dashboard/index.html b/dashboard/index.html index 5d84af3..f08ec95 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -35,14 +35,12 @@

Promptimprover Swarm Dashboard

const tr = document.createElement('tr'); const fmtTime = new Date(log.timestamp).toLocaleTimeString(); const reason = log.reasoning_path && log.reasoning_path.length > 60 ? log.reasoning_path.slice(0,57) + '...' : log.reasoning_path; - tr.innerHTML = ` - ${fmtTime} - ${log.span_id || 'N/A'} - ${log.persona || 'Unknown'} - ${log.action_type || 'Unknown'} - ${log.metrics?.total_tokens || 0} - ${reason || ''} - `; + ['time', 'span', 'persona', 'action', 'tokens', 'reasoning'].forEach((k, i) => { + const td = document.createElement('td'); + const values = [fmtTime, log.span_id || 'N/A', log.persona || 'Unknown', log.action_type || 'Unknown', log.metrics?.total_tokens || 0, reason || '']; + td.textContent = values[i]; + tr.appendChild(td); + }); tbody.appendChild(tr); }); } diff --git a/dashboard/server.js b/dashboard/server.js index a99c7c6..4e6d8c5 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -11,6 +11,7 @@ const PORT = 3000; // Helper: read last N lines from trace file async function readLastLines(filePath, maxLines = 20) { + if (!fs.existsSync(filePath)) return []; const stream = fs.createReadStream(filePath, { encoding: 'utf8' }); let data = ''; stream.on('data', chunk => { data += chunk; }); @@ -56,6 +57,6 @@ const server = http.createServer(async (req, res) => { } }); -server.listen(PORT, () => { - console.log(`Promptimprover dashboard listening on http://localhost:${PORT}`); +server.listen(PORT, '127.0.0.1', () => { + console.log(`Promptimprover dashboard listening on http://127.0.0.1:${PORT}`); });