argusqa-os 9.3.0 β 9.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/mcp-server.js +53 -9
- package/src/orchestration/watch-mode.js +161 -0
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ The `landing/` directory contains the product landing page (React + Vite + Tailw
|
|
|
79
79
|
|
|
80
80
|
| π΄ Critical / π‘ Warning / π΅ Info | βοΈ | π§ͺ | π |
|
|
81
81
|
| :---: | :---: | :---: | :---: |
|
|
82
|
-
| **114 distinct issue types detected** | **24 analysis engines** | **
|
|
82
|
+
| **114 distinct issue types detected** | **24 analysis engines** | **360 test assertions** | **83 test blocks** |
|
|
83
83
|
|
|
84
84
|
</div>
|
|
85
85
|
|
|
@@ -341,7 +341,7 @@ Argus watches your running application and automatically surfaces issues that te
|
|
|
341
341
|
| **GitHub PR Integration** | Posts a structured Markdown findings table as a PR comment (updates in-place β one comment per PR, no spam); sets an `argus-qa` commit status check (`failure` when new criticals exist, `success` otherwise) β blocks merge via branch protection when regressions are introduced. Requires `GITHUB_TOKEN` + `GITHUB_REPOSITORY` env vars |
|
|
342
342
|
| **Auto Route Discovery** | Augments manual `routes[]` with paths from three sources: fetches `/sitemap.xml` (follows one sitemap-index level, 10s timeout), scans Next.js `pages/` (Next 12) and `app/` (Next 13+) directories stripping route groups `(auth)`, and greps JS/TS source for React Router `<Route path>` declarations. Dynamic `[param]` segments are skipped β no concrete URL to crawl. Manual route config (`critical`, `waitFor`) always takes precedence. |
|
|
343
343
|
| **`argus init` Setup Wizard** | `npm run init` (or `npx argus init`) guides first-time setup: collects target URLs, detects the app framework (Next.js / React Router / unknown) from the source directory's `package.json`, runs C3 route discovery against the dev URL, prompts for optional Slack tokens and GitHub credentials, then writes a populated `.env` and a pre-filled `src/config/targets.js` β zero manual config editing required. |
|
|
344
|
-
| **Watch Mode** | `npm run watch` attaches to whatever Chrome tab is open and polls `list_console_messages` + `list_network_requests` every 1 s (configurable via `ARGUS_WATCH_INTERVAL_MS`). Reports new console errors, network failures (4xx/5xx), CORS blocks, and auth failures in real time β without navigating. On `Ctrl+C`, generates a final `reports/report.html`. No route config needed. |
|
|
344
|
+
| **Watch Mode** | `npm run watch` attaches to whatever Chrome tab is open and polls `list_console_messages` + `list_network_requests` every 1 s (configurable via `ARGUS_WATCH_INTERVAL_MS`). Reports new console errors, network failures (4xx/5xx), CORS blocks, and auth failures in real time β without navigating. Starts a live web dashboard at `http://localhost:3002` (configurable via `ARGUS_WATCH_UI_PORT`). On `Ctrl+C`, generates a final `reports/report.html`. No route config needed. |
|
|
345
345
|
| **Full Lighthouse Suite** | All 4 Lighthouse categories (performance, SEO, best-practices, accessibility) with per-audit items |
|
|
346
346
|
| **Performance Budgets** | Enforces LCP < 2500ms, CLS < 0.1, FID < 100ms, TTFB < 800ms per route |
|
|
347
347
|
| **Slack Notifications** | Rich Block Kit reports with inline screenshots routed to `#bugs-critical`, `#bugs-warnings`, `#bugs-digest` |
|
|
@@ -943,11 +943,11 @@ argus/
|
|
|
943
943
|
β βββ README.md # Setup guide, Supabase SQL schema, env vars, deployment
|
|
944
944
|
βββ scripts/
|
|
945
945
|
β βββ dispatch-report.js # Standalone Slack re-dispatch script (re-posts last report.json to Slack)
|
|
946
|
-
βββ test-harness/ # Fixture server + test runner (
|
|
946
|
+
βββ test-harness/ # Fixture server + test runner (83 blocks, 360 hard assertions, 54 fixture pages)
|
|
947
947
|
β βββ README.md
|
|
948
948
|
β βββ server.js # Express fixture server (ports 3100 dev / 3101 staging)
|
|
949
949
|
β βββ harness-config.js # Route definitions + expected findings
|
|
950
|
-
β βββ validate.js # Test runner β
|
|
950
|
+
β βββ validate.js # Test runner β 83 numbered blocks ([80] MCP server, [81] createFinding, [82] withRetry, [83] watch dashboard)
|
|
951
951
|
β βββ pages/ # 54 fixture pages (one per detection category)
|
|
952
952
|
β βββ nextjs-fixture/ # Next.js app structure for C3 discovery tests (10 files)
|
|
953
953
|
β βββ source-fixture/ # Minimal app.js for C1 codebase-analyzer tests (env var audit)
|
|
@@ -988,7 +988,7 @@ argus/
|
|
|
988
988
|
|
|
989
989
|
## Known MCP Tool Limitations
|
|
990
990
|
|
|
991
|
-
The Chrome DevTools MCP behavioral constraints below cause **3 permanent test failures** in the harness (`
|
|
991
|
+
The Chrome DevTools MCP behavioral constraints below cause **3 permanent test failures** in the harness (`357/360` pass). These are MCP-layer restrictions β they cannot be fixed in Argus code.
|
|
992
992
|
|
|
993
993
|
> **`type_text` clarification**: `type_text` does fire DOM `input` events when the element is properly focused first with `mcp.click({ uid })`. Always use uid-based focus β passing `{ selector }` to `mcp.click` silently does nothing.
|
|
994
994
|
|
package/package.json
CHANGED
package/src/mcp-server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Argus MCP Server (v9.
|
|
3
|
+
* Argus MCP Server (v9.3.1)
|
|
4
4
|
*
|
|
5
5
|
* Exposes Argus as an MCP server so Claude (or any MCP client) can call
|
|
6
|
-
* argus_audit, argus_audit_full, argus_compare, and
|
|
7
|
-
* directly from a conversation without using the CLI.
|
|
6
|
+
* argus_audit, argus_audit_full, argus_compare, argus_last_report, and
|
|
7
|
+
* argus_get_context (fix loop) directly from a conversation without using the CLI.
|
|
8
8
|
*
|
|
9
9
|
* Architecture: MCP-inside-MCP
|
|
10
10
|
* Claude (MCP client)
|
|
@@ -33,6 +33,18 @@ import { CdpBrowserAdapter } from './adapters/browser.js';
|
|
|
33
33
|
|
|
34
34
|
const REPORTS_DIR = path.resolve(process.cwd(), 'reports');
|
|
35
35
|
|
|
36
|
+
// Fix loop: stores up to 20 snapshots keyed by snapshot_id so argus_get_context
|
|
37
|
+
// can diff before/after a fix. Keys are evicted oldest-first when the limit is hit.
|
|
38
|
+
const snapshotStore = new Map();
|
|
39
|
+
const MAX_SNAPSHOTS = 20;
|
|
40
|
+
|
|
41
|
+
function storeSnapshot(id, findings) {
|
|
42
|
+
snapshotStore.set(id, findings);
|
|
43
|
+
if (snapshotStore.size > MAX_SNAPSHOTS) {
|
|
44
|
+
snapshotStore.delete(snapshotStore.keys().next().value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
// ββ Tool definitions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
37
49
|
|
|
38
50
|
const TOOLS = [
|
|
@@ -82,11 +94,12 @@ const TOOLS = [
|
|
|
82
94
|
},
|
|
83
95
|
{
|
|
84
96
|
name: 'argus_get_context',
|
|
85
|
-
description: 'Captures everything currently broken on the open Chrome tab and formats it as a diagnostic context for Claude to read and suggest fixes. Does NOT navigate β reads the live tab state after user interactions, in authenticated sessions, or mid-flow. Returns { summary, url, timestamp, critical_issues, warnings, js_errors, network_failures, console_errors, recent_requests }
|
|
97
|
+
description: 'Captures everything currently broken on the open Chrome tab and formats it as a diagnostic context for Claude to read and suggest fixes. Does NOT navigate β reads the live tab state after user interactions, in authenticated sessions, or mid-flow. Returns { snapshot_id, summary, url, timestamp, critical_issues, warnings, js_errors, network_failures, console_errors, recent_requests }. Fix loop: pass the snapshot_id from a previous call as snapshot_id to get a diff β the response will include resolved (cleared since last snapshot), new_issues (appeared since last snapshot), and persisting (unchanged). Workflow: call argus_get_context β Claude suggests fix β apply fix β call argus_get_context with snapshot_id β verify resolved array is non-empty. Requires Chrome on --remote-debugging-port=9222.',
|
|
86
98
|
inputSchema: {
|
|
87
99
|
type: 'object',
|
|
88
100
|
properties: {
|
|
89
101
|
url: { type: 'string', description: 'Optional base URL to attribute findings to (default: TARGET_DEV_URL env var). Does not navigate β inspects the currently open Chrome tab.' },
|
|
102
|
+
snapshot_id: { type: 'string', description: 'Optional snapshot_id from a previous argus_get_context call. When provided, the response includes resolved/new_issues/persisting arrays showing what changed since that snapshot.' },
|
|
90
103
|
},
|
|
91
104
|
},
|
|
92
105
|
},
|
|
@@ -143,26 +156,56 @@ async function handleWatchSnapshot({ url } = {}) {
|
|
|
143
156
|
});
|
|
144
157
|
}
|
|
145
158
|
|
|
146
|
-
async function handleGetContext({ url } = {}) {
|
|
159
|
+
async function handleGetContext({ url, snapshot_id: prevId } = {}) {
|
|
147
160
|
return withMcp(async (mcp) => {
|
|
148
161
|
const browser = new CdpBrowserAdapter(mcp);
|
|
149
162
|
const baseUrl = url ?? process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
150
163
|
const session = new WatchSession(browser, baseUrl);
|
|
151
164
|
const { findings, newConsole, newNetwork } = await session.poll();
|
|
152
165
|
|
|
166
|
+
const newId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
167
|
+
storeSnapshot(newId, findings);
|
|
168
|
+
|
|
153
169
|
const critical = findings.filter(f => f.severity === 'critical');
|
|
154
170
|
const warnings = findings.filter(f => f.severity === 'warning');
|
|
155
171
|
|
|
172
|
+
const findingKey = (f) => `${f.type}::${(f.message ?? '').slice(0, 120)}`;
|
|
173
|
+
|
|
174
|
+
let resolved = [];
|
|
175
|
+
let persisting = [];
|
|
176
|
+
let new_issues = findings;
|
|
177
|
+
const isDiff = prevId && snapshotStore.has(prevId);
|
|
178
|
+
|
|
179
|
+
if (isDiff) {
|
|
180
|
+
const prev = snapshotStore.get(prevId);
|
|
181
|
+
const prevKeys = new Set(prev.map(findingKey));
|
|
182
|
+
const curKeys = new Set(findings.map(findingKey));
|
|
183
|
+
resolved = prev.filter(f => !curKeys.has(findingKey(f)));
|
|
184
|
+
persisting = findings.filter(f => prevKeys.has(findingKey(f)));
|
|
185
|
+
new_issues = findings.filter(f => !prevKeys.has(findingKey(f)));
|
|
186
|
+
}
|
|
187
|
+
|
|
156
188
|
let summary;
|
|
157
|
-
if (
|
|
189
|
+
if (isDiff) {
|
|
190
|
+
if (resolved.length > 0 && critical.length === 0 && warnings.length === 0) {
|
|
191
|
+
summary = `All issues resolved on ${baseUrl}. ${resolved.length} finding${resolved.length > 1 ? 's' : ''} cleared since last snapshot.`;
|
|
192
|
+
} else if (resolved.length > 0) {
|
|
193
|
+
summary = `${resolved.length} issue${resolved.length > 1 ? 's' : ''} resolved on ${baseUrl}. ${new_issues.length} new + ${persisting.length} persisting (${critical.length} critical). Pass the new snapshot_id to continue the fix loop.`;
|
|
194
|
+
} else if (critical.length === 0 && warnings.length === 0) {
|
|
195
|
+
summary = `No issues on ${baseUrl} β console and network are clean.`;
|
|
196
|
+
} else {
|
|
197
|
+
summary = `No change on ${baseUrl}: ${critical.length} critical + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} still present. Check the persisting array for what hasn't been fixed.`;
|
|
198
|
+
}
|
|
199
|
+
} else if (critical.length === 0 && warnings.length === 0) {
|
|
158
200
|
summary = `No issues detected on ${baseUrl} β console and network are clean.`;
|
|
159
201
|
} else if (critical.length > 0) {
|
|
160
|
-
summary = `${critical.length} critical issue${critical.length > 1 ? 's' : ''} + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. Focus on critical issues first.`;
|
|
202
|
+
summary = `${critical.length} critical issue${critical.length > 1 ? 's' : ''} + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. Focus on critical issues first. Pass snapshot_id to the next argus_get_context call after applying a fix to see what changed.`;
|
|
161
203
|
} else {
|
|
162
|
-
summary = `${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. No critical errors.`;
|
|
204
|
+
summary = `${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. No critical errors. Pass snapshot_id to the next call to verify fixes.`;
|
|
163
205
|
}
|
|
164
206
|
|
|
165
207
|
const context = {
|
|
208
|
+
snapshot_id: newId,
|
|
166
209
|
summary,
|
|
167
210
|
url: baseUrl,
|
|
168
211
|
timestamp: new Date().toISOString(),
|
|
@@ -172,6 +215,7 @@ async function handleGetContext({ url } = {}) {
|
|
|
172
215
|
network_failures: findings.filter(f => f.type === 'network-error' || f.type === 'cors-error' || f.type === 'auth-error'),
|
|
173
216
|
console_errors: newConsole.filter(m => m.level === 'error' || m.level === 'warning'),
|
|
174
217
|
recent_requests: newNetwork.slice(-20),
|
|
218
|
+
...(isDiff ? { resolved, new_issues, persisting } : {}),
|
|
175
219
|
};
|
|
176
220
|
|
|
177
221
|
return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] };
|
|
@@ -196,7 +240,7 @@ async function handleLastReport() {
|
|
|
196
240
|
// ββ Server bootstrap ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
197
241
|
|
|
198
242
|
const server = new Server(
|
|
199
|
-
{ name: 'argus', version: '9.3.
|
|
243
|
+
{ name: 'argus', version: '9.3.1' },
|
|
200
244
|
{ capabilities: { tools: {} } },
|
|
201
245
|
);
|
|
202
246
|
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* Environment variables:
|
|
16
16
|
* ARGUS_WATCH_INTERVAL_MS β poll interval in ms (default: 1000)
|
|
17
17
|
* TARGET_DEV_URL β base URL to monitor (default: http://localhost:3000)
|
|
18
|
+
* ARGUS_WATCH_UI_PORT β port for the live web dashboard (default: 3002)
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import fs from 'fs';
|
|
21
22
|
import path from 'path';
|
|
23
|
+
import http from 'http';
|
|
22
24
|
import { fileURLToPath } from 'url';
|
|
23
25
|
import 'dotenv/config';
|
|
24
26
|
import { createMcpClient } from '../utils/mcp-client.js';
|
|
@@ -41,6 +43,161 @@ import {
|
|
|
41
43
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
44
|
const REPORTS_DIR = path.resolve(__dirname, '../../reports');
|
|
43
45
|
|
|
46
|
+
// ββ Live dashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
47
|
+
|
|
48
|
+
const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="UTF-8">
|
|
52
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
53
|
+
<title>Argus Watch Dashboard</title>
|
|
54
|
+
<style>
|
|
55
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
56
|
+
body { background: #0d0d0d; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; min-height: 100vh; }
|
|
57
|
+
header { background: #111; border-bottom: 1px solid #1e1e1e; padding: 14px 24px; display: flex; align-items: center; gap: 12px; }
|
|
58
|
+
.pulse-dot { width: 10px; height: 10px; border-radius: 50%; background: #5E0ED7; flex-shrink: 0;
|
|
59
|
+
animation: pulse 1.8s ease-in-out infinite; }
|
|
60
|
+
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(1.3)} }
|
|
61
|
+
.header-text { flex: 1; }
|
|
62
|
+
.header-text h1 { font-size: 15px; font-weight: 600; color: #fff; letter-spacing: .02em; }
|
|
63
|
+
.header-text small { font-size: 11px; color: #666; }
|
|
64
|
+
.pills { display: flex; gap: 8px; flex-wrap: wrap; padding: 16px 24px; }
|
|
65
|
+
.pill { font-size: 12px; font-weight: 600; padding: 4px 12px; border-radius: 999px; letter-spacing: .04em; }
|
|
66
|
+
.pill-critical { background: #3a0e0e; color: #f87171; border: 1px solid #7f1d1d; }
|
|
67
|
+
.pill-warning { background: #2e2200; color: #fbbf24; border: 1px solid #78350f; }
|
|
68
|
+
.pill-info { background: #1a0b33; color: #a78bfa; border: 1px solid #4c1d95; }
|
|
69
|
+
.pill-clear { background: #0b2718; color: #34d399; border: 1px solid #064e3b; }
|
|
70
|
+
.status-bar { font-size: 11px; padding: 4px 24px 12px; color: #555; }
|
|
71
|
+
.status-bar.error { color: #f87171; }
|
|
72
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
73
|
+
thead th { background: #161616; color: #888; font-weight: 600; text-align: left;
|
|
74
|
+
padding: 10px 16px; border-bottom: 1px solid #222; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
|
|
75
|
+
tbody tr { border-bottom: 1px solid #181818; transition: background .15s; }
|
|
76
|
+
tbody tr:hover { background: #151515; }
|
|
77
|
+
td { padding: 10px 16px; vertical-align: top; }
|
|
78
|
+
.sev { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; white-space: nowrap; }
|
|
79
|
+
.sev-critical { background: #3a0e0e; color: #f87171; }
|
|
80
|
+
.sev-warning { background: #2e2200; color: #fbbf24; }
|
|
81
|
+
.sev-info { background: #1a0b33; color: #a78bfa; }
|
|
82
|
+
.type-cell { color: #9ca3af; font-size: 12px; font-family: monospace; white-space: nowrap; }
|
|
83
|
+
.msg-cell { color: #d1d5db; word-break: break-word; max-width: 480px; }
|
|
84
|
+
.empty-row td { text-align: center; color: #444; padding: 40px; font-size: 13px; }
|
|
85
|
+
.table-wrap { padding: 0 24px 24px; overflow-x: auto; }
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<header>
|
|
90
|
+
<div class="pulse-dot" id="dot"></div>
|
|
91
|
+
<div class="header-text">
|
|
92
|
+
<h1 id="target">Argus Watch</h1>
|
|
93
|
+
<small id="lastPoll">Connectingβ¦</small>
|
|
94
|
+
</div>
|
|
95
|
+
</header>
|
|
96
|
+
<div class="pills" id="pills"></div>
|
|
97
|
+
<div class="status-bar" id="status"></div>
|
|
98
|
+
<div class="table-wrap">
|
|
99
|
+
<table>
|
|
100
|
+
<thead><tr><th>Severity</th><th>Type</th><th>Message</th></tr></thead>
|
|
101
|
+
<tbody id="tbody"></tbody>
|
|
102
|
+
</table>
|
|
103
|
+
</div>
|
|
104
|
+
<script>
|
|
105
|
+
const SEV_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
106
|
+
|
|
107
|
+
function renderPills(findings) {
|
|
108
|
+
const counts = { critical: 0, warning: 0, info: 0 };
|
|
109
|
+
for (const f of findings) counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
110
|
+
const pills = document.getElementById('pills');
|
|
111
|
+
if (findings.length === 0) {
|
|
112
|
+
pills.innerHTML = '<span class="pill pill-clear">All clear</span>';
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
let html = '';
|
|
116
|
+
if (counts.critical) html += \`<span class="pill pill-critical">\${counts.critical} Critical</span>\`;
|
|
117
|
+
if (counts.warning) html += \`<span class="pill pill-warning">\${counts.warning} Warning</span>\`;
|
|
118
|
+
if (counts.info) html += \`<span class="pill pill-info">\${counts.info} Info</span>\`;
|
|
119
|
+
pills.innerHTML = html;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderTable(findings) {
|
|
123
|
+
const sorted = [...findings].sort((a, b) =>
|
|
124
|
+
(SEV_ORDER[a.severity] ?? 3) - (SEV_ORDER[b.severity] ?? 3));
|
|
125
|
+
const tbody = document.getElementById('tbody');
|
|
126
|
+
if (sorted.length === 0) {
|
|
127
|
+
tbody.innerHTML = '<tr class="empty-row"><td colspan="3">No findings yet</td></tr>';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
tbody.innerHTML = sorted.map(f => {
|
|
131
|
+
const sc = 'sev-' + (f.severity || 'info');
|
|
132
|
+
const msg = (f.message || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
133
|
+
const typ = (f.type || '').replace(/&/g,'&');
|
|
134
|
+
return \`<tr>
|
|
135
|
+
<td><span class="sev \${sc}">\${f.severity ?? 'info'}</span></td>
|
|
136
|
+
<td class="type-cell">\${typ}</td>
|
|
137
|
+
<td class="msg-cell">\${msg}</td>
|
|
138
|
+
</tr>\`;
|
|
139
|
+
}).join('');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function poll() {
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch('/data');
|
|
145
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
document.getElementById('target').textContent = 'Argus Watch β ' + (data.target || '');
|
|
148
|
+
document.getElementById('lastPoll').textContent = 'Last poll: ' + new Date(data.lastPoll).toLocaleTimeString();
|
|
149
|
+
document.getElementById('dot').style.background = '#5E0ED7';
|
|
150
|
+
document.getElementById('status').textContent = '';
|
|
151
|
+
document.getElementById('status').className = 'status-bar';
|
|
152
|
+
renderPills(data.findings || []);
|
|
153
|
+
renderTable(data.findings || []);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
document.getElementById('status').textContent = 'Connection lost β ' + e.message;
|
|
156
|
+
document.getElementById('status').className = 'status-bar error';
|
|
157
|
+
document.getElementById('dot').style.background = '#555';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
poll();
|
|
162
|
+
setInterval(poll, 2000);
|
|
163
|
+
</script>
|
|
164
|
+
</body>
|
|
165
|
+
</html>`;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Start the live web dashboard HTTP server.
|
|
169
|
+
*
|
|
170
|
+
* @param {() => object[]} getFindings β callback that returns current findings array
|
|
171
|
+
* @param {string} target β the URL being monitored (shown in header)
|
|
172
|
+
* @param {number} port β TCP port to listen on (default 3002)
|
|
173
|
+
* @returns {http.Server}
|
|
174
|
+
*/
|
|
175
|
+
function startDashboard(getFindings, target, port) {
|
|
176
|
+
const server = http.createServer((req, res) => {
|
|
177
|
+
if (req.url === '/data' || req.url?.startsWith('/data?')) {
|
|
178
|
+
const payload = JSON.stringify({
|
|
179
|
+
target,
|
|
180
|
+
lastPoll: new Date().toISOString(),
|
|
181
|
+
findings: getFindings(),
|
|
182
|
+
});
|
|
183
|
+
res.writeHead(200, {
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
'Access-Control-Allow-Origin': '*',
|
|
186
|
+
});
|
|
187
|
+
res.end(payload);
|
|
188
|
+
} else {
|
|
189
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
190
|
+
res.end(DASHBOARD_HTML);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
server.listen(port, '127.0.0.1', () => {
|
|
195
|
+
logger.info(`[ARGUS WATCH] Dashboard β http://localhost:${port}`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return server;
|
|
199
|
+
}
|
|
200
|
+
|
|
44
201
|
// ββ Deduplication key generators βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
45
202
|
// Two messages/requests are considered "the same" if their keys match. This
|
|
46
203
|
// prevents re-reporting errors that were already captured in a previous poll.
|
|
@@ -228,6 +385,9 @@ export async function runWatchMode(baseUrl) {
|
|
|
228
385
|
logger.info(`[ARGUS WATCH] Polling every ${pollIntervalMs}ms. Press Ctrl+C to stop.`);
|
|
229
386
|
logger.info('[ARGUS WATCH] βββββββββββββββββββββββββββββββββββββββββββββββββ\n');
|
|
230
387
|
|
|
388
|
+
const uiPort = parseInt(process.env.ARGUS_WATCH_UI_PORT ?? '3002', 10);
|
|
389
|
+
const dashServer = startDashboard(() => session.getAllFindings(), target, uiPort);
|
|
390
|
+
|
|
231
391
|
const badge = (severity) =>
|
|
232
392
|
severity === 'critical' ? 'β CRIT' :
|
|
233
393
|
severity === 'warning' ? '! WARN' : 'i INFO';
|
|
@@ -270,6 +430,7 @@ export async function runWatchMode(baseUrl) {
|
|
|
270
430
|
|
|
271
431
|
process.on('SIGINT', async () => {
|
|
272
432
|
clearInterval(interval);
|
|
433
|
+
try { dashServer.close(); } catch { /* ignore */ }
|
|
273
434
|
const all = session.getAllFindings();
|
|
274
435
|
|
|
275
436
|
logger.info(`\n[ARGUS WATCH] Stopped. Total findings: ${all.length}`);
|