aoaoe 0.193.0 → 0.194.0
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/dist/config.d.ts +2 -0
- package/dist/config.js +15 -2
- package/dist/index.js +10 -1
- package/dist/web.d.ts +6 -0
- package/dist/web.js +268 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -333,7 +333,7 @@ export function parseCliArgs(argv) {
|
|
|
333
333
|
let initForce = false;
|
|
334
334
|
let runTaskCli = false;
|
|
335
335
|
let registerTitle;
|
|
336
|
-
const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, runHealth: false, healthJson: false, runSummary: false, runAdopt: false, adoptTemplate: undefined, showHistory: false, showStatus: false, runRunbook: false, runbookJson: false, runbookSection: undefined, runIncident: false, incidentSince: undefined, incidentLimit: undefined, incidentJson: false, incidentNdjson: false, incidentWatch: false, incidentChangesOnly: false, incidentHeartbeatSec: undefined, incidentIntervalMs: undefined, runSupervisor: false, supervisorAll: false, supervisorSince: undefined, supervisorLimit: undefined, supervisorJson: false, supervisorNdjson: false, supervisorWatch: false, supervisorChangesOnly: false, supervisorHeartbeatSec: undefined, supervisorIntervalMs: undefined, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runBackup: false, backupOutput: undefined, runRestore: false, restoreInput: undefined, runSync: false, syncAction: undefined, syncRemote: undefined, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, exportTasks: false, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
|
|
336
|
+
const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, runHealth: false, healthJson: false, runSummary: false, runAdopt: false, adoptTemplate: undefined, showHistory: false, showStatus: false, runRunbook: false, runbookJson: false, runbookSection: undefined, runIncident: false, incidentSince: undefined, incidentLimit: undefined, incidentJson: false, incidentNdjson: false, incidentWatch: false, incidentChangesOnly: false, incidentHeartbeatSec: undefined, incidentIntervalMs: undefined, runSupervisor: false, supervisorAll: false, supervisorSince: undefined, supervisorLimit: undefined, supervisorJson: false, supervisorNdjson: false, supervisorWatch: false, supervisorChangesOnly: false, supervisorHeartbeatSec: undefined, supervisorIntervalMs: undefined, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runBackup: false, backupOutput: undefined, runRestore: false, restoreInput: undefined, runSync: false, syncAction: undefined, syncRemote: undefined, runWeb: false, webPort: undefined, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, exportTasks: false, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
|
|
337
337
|
// check for subcommand as first non-flag arg
|
|
338
338
|
if (argv[2] === "test-context") {
|
|
339
339
|
return { ...defaults, testContext: true };
|
|
@@ -528,6 +528,17 @@ export function parseCliArgs(argv) {
|
|
|
528
528
|
if (argv[2] === "notify-test") {
|
|
529
529
|
return { ...defaults, notifyTest: true };
|
|
530
530
|
}
|
|
531
|
+
if (argv[2] === "web") {
|
|
532
|
+
let port;
|
|
533
|
+
for (let i = 3; i < argv.length; i++) {
|
|
534
|
+
if ((argv[i] === "--port" || argv[i] === "-p") && argv[i + 1]) {
|
|
535
|
+
const val = parseInt(argv[++i], 10);
|
|
536
|
+
if (!isNaN(val) && val > 0)
|
|
537
|
+
port = val;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return { ...defaults, runWeb: true, webPort: port };
|
|
541
|
+
}
|
|
531
542
|
if (argv[2] === "sync") {
|
|
532
543
|
const action = argv[3]; // init, push, pull, status
|
|
533
544
|
const remote = argv[4]; // only for init
|
|
@@ -733,7 +744,7 @@ export function parseCliArgs(argv) {
|
|
|
733
744
|
break;
|
|
734
745
|
}
|
|
735
746
|
}
|
|
736
|
-
return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, runHealth: false, healthJson: false, runSummary: false, runAdopt: false, adoptTemplate: undefined, showHistory: false, showStatus: false, runRunbook: false, runbookJson: false, runbookSection: undefined, runIncident: false, incidentSince: undefined, incidentLimit: undefined, incidentJson: false, incidentNdjson: false, incidentWatch: false, incidentChangesOnly: false, incidentHeartbeatSec: undefined, incidentIntervalMs: undefined, runSupervisor: false, supervisorAll: false, supervisorSince: undefined, supervisorLimit: undefined, supervisorJson: false, supervisorNdjson: false, supervisorWatch: false, supervisorChangesOnly: false, supervisorHeartbeatSec: undefined, supervisorIntervalMs: undefined, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runBackup: false, backupOutput: undefined, runRestore: false, restoreInput: undefined, runSync: false, syncAction: undefined, syncRemote: undefined, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, exportTasks: false, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
|
|
747
|
+
return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, runHealth: false, healthJson: false, runSummary: false, runAdopt: false, adoptTemplate: undefined, showHistory: false, showStatus: false, runRunbook: false, runbookJson: false, runbookSection: undefined, runIncident: false, incidentSince: undefined, incidentLimit: undefined, incidentJson: false, incidentNdjson: false, incidentWatch: false, incidentChangesOnly: false, incidentHeartbeatSec: undefined, incidentIntervalMs: undefined, runSupervisor: false, supervisorAll: false, supervisorSince: undefined, supervisorLimit: undefined, supervisorJson: false, supervisorNdjson: false, supervisorWatch: false, supervisorChangesOnly: false, supervisorHeartbeatSec: undefined, supervisorIntervalMs: undefined, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runBackup: false, backupOutput: undefined, runRestore: false, restoreInput: undefined, runSync: false, syncAction: undefined, syncRemote: undefined, runWeb: false, webPort: undefined, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, exportTasks: false, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
|
|
737
748
|
}
|
|
738
749
|
export function printHelp() {
|
|
739
750
|
console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
|
|
@@ -779,6 +790,8 @@ commands:
|
|
|
779
790
|
notify-test send a test notification to configured webhooks
|
|
780
791
|
backup [path] backup ~/.aoaoe/ state + config to tarball
|
|
781
792
|
restore <path> restore from backup tarball or directory
|
|
793
|
+
web start browser dashboard (default: http://127.0.0.1:4099)
|
|
794
|
+
web --port N custom port for dashboard
|
|
782
795
|
sync init <url> set up git-based state sharing with remote repo
|
|
783
796
|
sync push push local state to sync remote
|
|
784
797
|
sync pull pull remote state and restore locally
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
|
|
|
40
40
|
const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
|
|
41
41
|
const TASK_RECONCILE_EVERY_POLLS = 6;
|
|
42
42
|
async function main() {
|
|
43
|
-
const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showTasksJson, runProgress, progressSince, progressJson, runHealth, healthJson, runSummary, runAdopt, adoptTemplate, showHistory, showStatus, runRunbook, runbookJson, runbookSection, runIncident, incidentSince, incidentLimit, incidentJson, incidentNdjson, incidentWatch, incidentChangesOnly, incidentHeartbeatSec, incidentIntervalMs, runSupervisor, supervisorAll, supervisorSince, supervisorLimit, supervisorJson, supervisorNdjson, supervisorWatch, supervisorChangesOnly, supervisorHeartbeatSec, supervisorIntervalMs, showConfig, configValidate, configDiff, notifyTest, runDoctor, runBackup, backupOutput, runRestore, restoreInput, runSync, syncAction, syncRemote, runLogs, logsActions, logsGrep, logsCount, runExport, exportFormat, exportOutput, exportLast, runInit, initForce, runTaskCli: isTaskCli, runTail: isTail, tailFollow, tailCount, runStats: isStats, statsLast, runReplay: isReplay, replaySpeed, replayLast, registerTitle } = parseCliArgs(process.argv);
|
|
43
|
+
const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showTasksJson, runProgress, progressSince, progressJson, runHealth, healthJson, runSummary, runAdopt, adoptTemplate, showHistory, showStatus, runRunbook, runbookJson, runbookSection, runIncident, incidentSince, incidentLimit, incidentJson, incidentNdjson, incidentWatch, incidentChangesOnly, incidentHeartbeatSec, incidentIntervalMs, runSupervisor, supervisorAll, supervisorSince, supervisorLimit, supervisorJson, supervisorNdjson, supervisorWatch, supervisorChangesOnly, supervisorHeartbeatSec, supervisorIntervalMs, showConfig, configValidate, configDiff, notifyTest, runDoctor, runBackup, backupOutput, runRestore, restoreInput, runSync, syncAction, syncRemote, runWeb, webPort, runLogs, logsActions, logsGrep, logsCount, runExport, exportFormat, exportOutput, exportLast, runInit, initForce, runTaskCli: isTaskCli, runTail: isTail, tailFollow, tailCount, runStats: isStats, statsLast, runReplay: isReplay, replaySpeed, replayLast, registerTitle } = parseCliArgs(process.argv);
|
|
44
44
|
if (help) {
|
|
45
45
|
printHelp();
|
|
46
46
|
process.exit(0);
|
|
@@ -143,6 +143,14 @@ async function main() {
|
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
// `aoaoe doctor` -- comprehensive health check
|
|
146
|
+
if (runWeb) {
|
|
147
|
+
setWebResolveProfiles(resolveProfiles);
|
|
148
|
+
const port = webPort ?? 4099;
|
|
149
|
+
startWebServer(port);
|
|
150
|
+
// keep process alive until Ctrl+C
|
|
151
|
+
process.on("SIGINT", () => process.exit(0));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
146
154
|
if (runSync) {
|
|
147
155
|
process.env.AOAOE_QUIET = "1";
|
|
148
156
|
try {
|
|
@@ -4561,6 +4569,7 @@ async function showProgressDigest(since, asJson = false) {
|
|
|
4561
4569
|
import { resolveTemplate } from "./task-templates.js";
|
|
4562
4570
|
import { createBackup, restoreBackup, formatBackupResult, formatRestoreResult } from "./backup.js";
|
|
4563
4571
|
import { syncInit, syncPush, syncPull, syncDiff, syncStatus } from "./sync.js";
|
|
4572
|
+
import { startWebServer, setResolveProfiles as setWebResolveProfiles } from "./web.js";
|
|
4564
4573
|
// adopt untracked live AoE sessions as tasks with optional template goal.
|
|
4565
4574
|
async function adoptUntrackedSessions(templateName) {
|
|
4566
4575
|
const basePath = process.cwd();
|
package/dist/web.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
export declare function setResolveProfiles(fn: (config: ReturnType<typeof loadConfig>) => string[]): void;
|
|
3
|
+
export declare function startWebServer(port: number): {
|
|
4
|
+
close: () => void;
|
|
5
|
+
};
|
|
6
|
+
//# sourceMappingURL=web.d.ts.map
|
package/dist/web.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// web.ts — minimal browser dashboard served from aoaoe daemon.
|
|
2
|
+
// serves a single HTML page + JSON API endpoints using Node stdlib http.
|
|
3
|
+
// zero dependencies. auto-refreshes every 5 seconds.
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { loadTaskState, loadTaskDefinitions, formatAgo, TaskManager } from "./task-manager.js";
|
|
6
|
+
import { computeAllHealth } from "./health-score.js";
|
|
7
|
+
import { loadSupervisorEvents } from "./supervisor-history.js";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
let resolveProfilesFn = null;
|
|
10
|
+
export function setResolveProfiles(fn) {
|
|
11
|
+
resolveProfilesFn = fn;
|
|
12
|
+
}
|
|
13
|
+
function getTasks() {
|
|
14
|
+
const basePath = process.cwd();
|
|
15
|
+
const defs = loadTaskDefinitions(basePath);
|
|
16
|
+
if (defs.length === 0)
|
|
17
|
+
return [...loadTaskState().values()];
|
|
18
|
+
const config = loadConfig();
|
|
19
|
+
const profiles = resolveProfilesFn ? resolveProfilesFn(config) : ["default"];
|
|
20
|
+
return new TaskManager(basePath, defs, profiles).tasks;
|
|
21
|
+
}
|
|
22
|
+
// ── JSON API ────────────────────────────────────────────────────────────────
|
|
23
|
+
function apiTasks() {
|
|
24
|
+
const tasks = getTasks();
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
return tasks.map((t) => ({
|
|
27
|
+
session: t.sessionTitle,
|
|
28
|
+
repo: t.repo,
|
|
29
|
+
status: t.status,
|
|
30
|
+
goal: t.goal,
|
|
31
|
+
dependsOn: t.dependsOn ?? [],
|
|
32
|
+
lastProgressAt: t.lastProgressAt ?? null,
|
|
33
|
+
lastProgressAgo: t.lastProgressAt ? formatAgo(now - t.lastProgressAt) : null,
|
|
34
|
+
progressCount: t.progress.length,
|
|
35
|
+
lastProgress: t.progress.length > 0 ? t.progress[t.progress.length - 1].summary : null,
|
|
36
|
+
stuckNudgeCount: t.stuckNudgeCount ?? 0,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function apiHealth() {
|
|
40
|
+
return computeAllHealth(getTasks());
|
|
41
|
+
}
|
|
42
|
+
function apiProgress(sinceMs = 24 * 60 * 60 * 1000) {
|
|
43
|
+
const tasks = getTasks();
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const cutoff = now - sinceMs;
|
|
46
|
+
return tasks.map((t) => ({
|
|
47
|
+
session: t.sessionTitle,
|
|
48
|
+
status: t.status,
|
|
49
|
+
recentProgress: t.progress.filter((p) => p.at >= cutoff).map((p) => ({
|
|
50
|
+
at: p.at,
|
|
51
|
+
ago: formatAgo(now - p.at),
|
|
52
|
+
summary: p.summary,
|
|
53
|
+
})),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function apiSupervisor(limit = 20) {
|
|
57
|
+
const tasks = getTasks();
|
|
58
|
+
const events = loadSupervisorEvents(limit).reverse();
|
|
59
|
+
const active = tasks.filter((t) => t.status === "active").length;
|
|
60
|
+
const pending = tasks.filter((t) => t.status === "pending").length;
|
|
61
|
+
const paused = tasks.filter((t) => t.status === "paused").length;
|
|
62
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
63
|
+
return {
|
|
64
|
+
summary: { total: tasks.length, active, pending, paused, completed },
|
|
65
|
+
recentEvents: events.map((e) => ({ at: e.at, detail: e.detail })),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ── HTML dashboard ──────────────────────────────────────────────────────────
|
|
69
|
+
function dashboardHtml() {
|
|
70
|
+
return `<!DOCTYPE html>
|
|
71
|
+
<html lang="en">
|
|
72
|
+
<head>
|
|
73
|
+
<meta charset="utf-8">
|
|
74
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
75
|
+
<title>aoaoe dashboard</title>
|
|
76
|
+
<style>
|
|
77
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
78
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; background: #0d1117; color: #c9d1d9; padding: 20px; }
|
|
79
|
+
h1 { font-size: 1.4em; margin-bottom: 16px; color: #58a6ff; }
|
|
80
|
+
h2 { font-size: 1.1em; margin: 20px 0 8px; color: #8b949e; border-bottom: 1px solid #21262d; padding-bottom: 4px; }
|
|
81
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; }
|
|
82
|
+
.card { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 14px; }
|
|
83
|
+
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
84
|
+
.session-name { font-weight: 600; font-size: 1em; }
|
|
85
|
+
.status { font-size: 0.8em; padding: 2px 8px; border-radius: 12px; }
|
|
86
|
+
.status-active { background: #1f6f2b; color: #3fb950; }
|
|
87
|
+
.status-pending { background: #2d333b; color: #8b949e; }
|
|
88
|
+
.status-paused { background: #4a3200; color: #d29922; }
|
|
89
|
+
.status-completed { background: #1a3a4a; color: #58a6ff; }
|
|
90
|
+
.status-failed { background: #4a1a1a; color: #f85149; }
|
|
91
|
+
.health-bar { height: 6px; background: #21262d; border-radius: 3px; margin: 6px 0; overflow: hidden; }
|
|
92
|
+
.health-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
|
|
93
|
+
.health-healthy { background: #3fb950; }
|
|
94
|
+
.health-ok { background: #58a6ff; }
|
|
95
|
+
.health-degraded { background: #d29922; }
|
|
96
|
+
.health-critical { background: #f85149; }
|
|
97
|
+
.health-inactive { background: #484f58; }
|
|
98
|
+
.goal { font-size: 0.85em; color: #8b949e; margin: 4px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
99
|
+
.progress-entry { font-size: 0.8em; color: #8b949e; padding: 2px 0; border-top: 1px solid #21262d; }
|
|
100
|
+
.progress-time { color: #484f58; }
|
|
101
|
+
.meta { font-size: 0.75em; color: #484f58; margin-top: 6px; }
|
|
102
|
+
.summary-bar { display: flex; gap: 16px; margin-bottom: 12px; font-size: 0.9em; }
|
|
103
|
+
.summary-item { padding: 6px 12px; background: #161b22; border: 1px solid #21262d; border-radius: 6px; }
|
|
104
|
+
.events { max-height: 200px; overflow-y: auto; }
|
|
105
|
+
.event { font-size: 0.8em; padding: 3px 0; color: #8b949e; }
|
|
106
|
+
.event-time { color: #484f58; }
|
|
107
|
+
.refresh { font-size: 0.75em; color: #484f58; text-align: right; margin-top: 12px; }
|
|
108
|
+
#error { color: #f85149; font-size: 0.85em; display: none; margin-bottom: 8px; }
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
<body>
|
|
112
|
+
<h1>aoaoe dashboard</h1>
|
|
113
|
+
<div id="error"></div>
|
|
114
|
+
<div class="summary-bar" id="summary"></div>
|
|
115
|
+
<h2>sessions</h2>
|
|
116
|
+
<div class="grid" id="tasks"></div>
|
|
117
|
+
<h2>supervisor events</h2>
|
|
118
|
+
<div class="events" id="events"></div>
|
|
119
|
+
<div class="refresh" id="refresh"></div>
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
const API = window.location.origin;
|
|
123
|
+
const REFRESH_MS = 5000;
|
|
124
|
+
|
|
125
|
+
async function fetchJson(path) {
|
|
126
|
+
const res = await fetch(API + path);
|
|
127
|
+
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
|
128
|
+
return res.json();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function statusClass(status) {
|
|
132
|
+
return 'status status-' + (status || 'pending');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function healthClass(grade) {
|
|
136
|
+
return 'health-fill health-' + (grade || 'inactive');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function escHtml(s) {
|
|
140
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function refresh() {
|
|
144
|
+
try {
|
|
145
|
+
const [tasks, healths, progress, supervisor] = await Promise.all([
|
|
146
|
+
fetchJson('/api/tasks'),
|
|
147
|
+
fetchJson('/api/health'),
|
|
148
|
+
fetchJson('/api/progress?since=8h'),
|
|
149
|
+
fetchJson('/api/supervisor'),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const healthMap = {};
|
|
153
|
+
healths.forEach(h => healthMap[h.session] = h);
|
|
154
|
+
|
|
155
|
+
// summary
|
|
156
|
+
const s = supervisor.summary;
|
|
157
|
+
document.getElementById('summary').innerHTML =
|
|
158
|
+
'<div class="summary-item">' + s.total + ' tasks</div>' +
|
|
159
|
+
'<div class="summary-item" style="color:#3fb950">' + s.active + ' active</div>' +
|
|
160
|
+
(s.pending > 0 ? '<div class="summary-item">' + s.pending + ' pending</div>' : '') +
|
|
161
|
+
(s.paused > 0 ? '<div class="summary-item" style="color:#d29922">' + s.paused + ' paused</div>' : '') +
|
|
162
|
+
(s.completed > 0 ? '<div class="summary-item" style="color:#58a6ff">' + s.completed + ' done</div>' : '') +
|
|
163
|
+
'<div class="summary-item">avg health: ' + Math.round(healths.reduce((a,h) => a + h.score, 0) / (healths.length || 1)) + '</div>';
|
|
164
|
+
|
|
165
|
+
// task cards
|
|
166
|
+
const progressMap = {};
|
|
167
|
+
progress.forEach(p => progressMap[p.session] = p.recentProgress || []);
|
|
168
|
+
|
|
169
|
+
let html = '';
|
|
170
|
+
tasks.forEach(t => {
|
|
171
|
+
const h = healthMap[t.session] || { score: 0, grade: 'inactive', factors: [] };
|
|
172
|
+
const recent = (progressMap[t.session] || []).slice(-3);
|
|
173
|
+
html += '<div class="card">';
|
|
174
|
+
html += '<div class="card-header"><span class="session-name">' + escHtml(t.session) + '</span><span class="' + statusClass(t.status) + '">' + t.status + '</span></div>';
|
|
175
|
+
html += '<div class="health-bar"><div class="' + healthClass(h.grade) + '" style="width:' + h.score + '%"></div></div>';
|
|
176
|
+
html += '<div class="meta">health: ' + h.score + '/100 (' + h.grade + ') · ' + h.factors.join(' · ') + '</div>';
|
|
177
|
+
html += '<div class="goal" title="' + escHtml(t.goal) + '">' + escHtml(t.goal) + '</div>';
|
|
178
|
+
if (t.dependsOn && t.dependsOn.length) html += '<div class="meta">depends on: ' + t.dependsOn.join(', ') + '</div>';
|
|
179
|
+
if (recent.length > 0) {
|
|
180
|
+
recent.forEach(p => {
|
|
181
|
+
html += '<div class="progress-entry"><span class="progress-time">' + p.ago + '</span> ' + escHtml(p.summary) + '</div>';
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
html += '<div class="progress-entry">' + (t.lastProgressAgo ? 'last progress: ' + t.lastProgressAgo : 'no progress yet') + '</div>';
|
|
185
|
+
}
|
|
186
|
+
if (t.stuckNudgeCount > 0) html += '<div class="meta" style="color:#d29922">stuck nudges: ' + t.stuckNudgeCount + '</div>';
|
|
187
|
+
html += '</div>';
|
|
188
|
+
});
|
|
189
|
+
document.getElementById('tasks').innerHTML = html;
|
|
190
|
+
|
|
191
|
+
// events
|
|
192
|
+
let evHtml = '';
|
|
193
|
+
supervisor.recentEvents.forEach(e => {
|
|
194
|
+
const d = new Date(e.at);
|
|
195
|
+
evHtml += '<div class="event"><span class="event-time">' + d.toLocaleTimeString() + '</span> ' + escHtml(e.detail) + '</div>';
|
|
196
|
+
});
|
|
197
|
+
document.getElementById('events').innerHTML = evHtml || '<div class="event">no recent events</div>';
|
|
198
|
+
|
|
199
|
+
document.getElementById('error').style.display = 'none';
|
|
200
|
+
document.getElementById('refresh').textContent = 'last refresh: ' + new Date().toLocaleTimeString() + ' (every ' + (REFRESH_MS/1000) + 's)';
|
|
201
|
+
} catch (err) {
|
|
202
|
+
document.getElementById('error').textContent = 'fetch error: ' + err.message;
|
|
203
|
+
document.getElementById('error').style.display = 'block';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
refresh();
|
|
208
|
+
setInterval(refresh, REFRESH_MS);
|
|
209
|
+
</script>
|
|
210
|
+
</body>
|
|
211
|
+
</html>`;
|
|
212
|
+
}
|
|
213
|
+
// ── HTTP server ─────────────────────────────────────────────────────────────
|
|
214
|
+
function parseSince(url) {
|
|
215
|
+
const raw = url.searchParams.get("since");
|
|
216
|
+
if (!raw)
|
|
217
|
+
return 24 * 60 * 60 * 1000;
|
|
218
|
+
const match = raw.match(/^(\d+)(h|m|d)$/);
|
|
219
|
+
if (!match)
|
|
220
|
+
return 24 * 60 * 60 * 1000;
|
|
221
|
+
const [, n, unit] = match;
|
|
222
|
+
const ms = unit === "h" ? 3_600_000 : unit === "m" ? 60_000 : 86_400_000;
|
|
223
|
+
return parseInt(n, 10) * ms;
|
|
224
|
+
}
|
|
225
|
+
function handleRequest(req, res) {
|
|
226
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
227
|
+
const path = url.pathname;
|
|
228
|
+
// CORS for local dev
|
|
229
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
230
|
+
if (path === "/" || path === "/index.html") {
|
|
231
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
232
|
+
res.end(dashboardHtml());
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (path === "/api/tasks") {
|
|
236
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
237
|
+
res.end(JSON.stringify(apiTasks()));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (path === "/api/health") {
|
|
241
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
242
|
+
res.end(JSON.stringify(apiHealth()));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (path === "/api/progress") {
|
|
246
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
247
|
+
res.end(JSON.stringify(apiProgress(parseSince(url))));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (path === "/api/supervisor") {
|
|
251
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
|
|
252
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
253
|
+
res.end(JSON.stringify(apiSupervisor(limit)));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
257
|
+
res.end("not found");
|
|
258
|
+
}
|
|
259
|
+
export function startWebServer(port) {
|
|
260
|
+
// suppress noisy log lines when loading config/tasks for API responses
|
|
261
|
+
process.env.AOAOE_QUIET = "1";
|
|
262
|
+
const server = createServer(handleRequest);
|
|
263
|
+
server.listen(port, "127.0.0.1", () => {
|
|
264
|
+
console.log(`aoaoe dashboard: http://127.0.0.1:${port}`);
|
|
265
|
+
});
|
|
266
|
+
return { close: () => server.close() };
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=web.js.map
|