feat(dashboard): Phase 3 — embedded web dashboard
- Single-file dark-theme HTML dashboard (569 lines, zero deps) - GET / serves dashboard HTML (no auth required, token handled in JS) - Auto-poll every 5s: health, senses, workflows - Trigger/Kill buttons with confirmation + toast notifications - Bearer token input persisted in localStorage - Connection status indicator (green/red dot) - Responsive layout for mobile - SenseInfo gains triggers[] field, WorkflowStatus gains activeRunIds[] - rslib copies dashboard.html to dist/ Refs #133
This commit is contained in:
@@ -11,6 +11,8 @@ import type { SenseInfo } from "./sense.js";
|
||||
export type WorkflowStatus = {
|
||||
name: string;
|
||||
activeThreads: number;
|
||||
/** Run IDs currently executing (same identifiers accepted by kill-workflow). */
|
||||
activeRunIds: string[];
|
||||
queuedThreads: number;
|
||||
config: { concurrency: number; overflow: string };
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ export function isSenseInfo(value: unknown): value is SenseInfo {
|
||||
typeof value.group === "string" &&
|
||||
(value.throttle === null || typeof value.throttle === "number") &&
|
||||
(value.timeout === null || typeof value.timeout === "number") &&
|
||||
Array.isArray(value.triggers) &&
|
||||
value.triggers.every((t: unknown) => typeof t === "string") &&
|
||||
(value.lastSignalTimestamp === null || typeof value.lastSignalTimestamp === "number")
|
||||
);
|
||||
}
|
||||
@@ -22,6 +24,8 @@ export function isWorkflowStatus(value: unknown): value is WorkflowStatus {
|
||||
return (
|
||||
typeof value.name === "string" &&
|
||||
typeof value.activeThreads === "number" &&
|
||||
Array.isArray(value.activeRunIds) &&
|
||||
value.activeRunIds.every((id: unknown) => typeof id === "string") &&
|
||||
typeof value.queuedThreads === "number" &&
|
||||
typeof cfg.concurrency === "number" &&
|
||||
typeof cfg.overflow === "string"
|
||||
|
||||
@@ -9,6 +9,7 @@ export type {
|
||||
NerveConfig,
|
||||
} from "./config.js";
|
||||
export type { Signal, SenseInfo, SenseResult } from "./sense.js";
|
||||
export { labelSenseReflexTrigger, senseTriggerLabels } from "./sense-trigger-labels.js";
|
||||
export type {
|
||||
WorkflowMessage,
|
||||
RoleResult,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ReflexConfig } from "./config.js";
|
||||
|
||||
function formatIntervalMs(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
if (totalSeconds < 60) return `${totalSeconds}s`;
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
/** Human-readable label for one sense reflex (interval and/or signal subscriptions). */
|
||||
export function labelSenseReflexTrigger(reflex: Extract<ReflexConfig, { kind: "sense" }>): string {
|
||||
const parts: string[] = [];
|
||||
if (reflex.interval !== null) {
|
||||
parts.push(`every ${formatIntervalMs(reflex.interval)}`);
|
||||
}
|
||||
if (reflex.on.length > 0) {
|
||||
parts.push(`on: ${reflex.on.join(", ")}`);
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
return "reflex (no interval or on)";
|
||||
}
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
/** All reflex trigger descriptions that target the given sense name. */
|
||||
export function senseTriggerLabels(senseName: string, reflexes: readonly ReflexConfig[]): string[] {
|
||||
const out: string[] = [];
|
||||
for (const ref of reflexes) {
|
||||
if (ref.kind !== "sense" || ref.sense !== senseName) continue;
|
||||
out.push(labelSenseReflexTrigger(ref));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -11,6 +11,8 @@ export type SenseInfo = {
|
||||
group: string;
|
||||
throttle: number | null;
|
||||
timeout: number | null;
|
||||
/** Declarative reflex lines that schedule this sense (derived from nerve.yaml). */
|
||||
triggers: string[];
|
||||
lastSignalTimestamp: number | null;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user