feat: add explicit native admin CLI consent with memory-only approval and persistent redacted node evidence plus separate validated broker scopes and localhost proof source

This commit is contained in:
Matthew Meszaros
2026-10-10 06:41:21 +00:00
parent 03ed09138b
commit 4db93bf292
19 changed files with 938 additions and 5 deletions
@@ -0,0 +1,96 @@
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { APIError } from "@/lib/api/client";
import type * as ClientModule from "@/lib/api/client";
import DeviceApprovalPage from "./DeviceApprovalPage";
const api = vi.hoisted(() => ({ describe: vi.fn(), decide: vi.fn(), reauth: vi.fn() }));
vi.mock("@/lib/api/client/admin/device", () => ({ describeAdminDevice: api.describe, decideAdminDevice: api.decide }));
vi.mock("@/lib/api/client", async (importOriginal) => ({ ...await importOriginal<typeof ClientModule>(), promptForReauth: api.reauth }));
vi.mock("@/lib/env", () => ({ API_URL: "http://localhost:8080" }));
vi.mock("@/hooks/useMe", () => ({ useMe: () => ({ data: { id: "fixture-admin", admin_permissions: 16 } }) }));
let root: Root;
let host: HTMLDivElement;
const consentToken = "c".repeat(43);
const description = () => ({ request: { client_name: "Fixture laptop", instance_url: "http://localhost:8080", status: "pending", expires_at: new Date(Date.now() + 60000).toISOString(), consent_token: consentToken }, requested_access: "Your live platform administrator permissions through a separate revocable session", admin_permissions: 16 });
beforeEach(async () => {
vi.clearAllMocks();
localStorage.clear(); sessionStorage.clear();
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
api.describe.mockResolvedValue(description());
api.decide.mockImplementation((_code, _token, decision) => Promise.resolve({ status: decision }));
api.reauth.mockResolvedValue(undefined);
host = document.createElement("div"); document.body.append(host); root = createRoot(host);
await act(async () => root.render(<MemoryRouter initialEntries={["/device?user_code=ABCD-EFGH&approve=true"]}><DeviceApprovalPage /></MemoryRouter>));
});
afterEach(async () => {
await act(async () => root.unmount()); host.remove(); vi.useRealTimers(); vi.unstubAllGlobals();
});
function button(name: string) {
const found = [...host.querySelectorAll("button")].find((b) => b.textContent === name);
if (!found) throw new Error(`Button missing: ${name}`);
return found;
}
async function review() {
await act(async () => {
const input = host.querySelector("input")!;
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, "ABCD-EFGH");
input.dispatchEvent(new Event("input", { bubbles: true }));
});
await act(async () => host.querySelector("form")!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })));
}
describe("explicit admin device consent", () => {
it("does not describe or approve URL codes on load", () => {
expect(api.describe).not.toHaveBeenCalled(); expect(api.decide).not.toHaveBeenCalled();
});
it("shows instance/client/capability/expiry without exposing or persisting consent", async () => {
await review();
expect(api.describe).toHaveBeenCalledWith("ABCD-EFGH");
expect(api.decide).not.toHaveBeenCalled();
expect(host.textContent).toContain("Fixture laptop"); expect(host.textContent).toContain("http://localhost:8080");
expect(host.textContent).toContain("live platform administrator permissions"); expect(host.textContent).toContain("Expires");
expect(host.innerHTML).not.toContain(consentToken); expect(localStorage.length).toBe(0); expect(sessionStorage.length).toBe(0);
await act(async () => button("Approve CLI sign-in").click());
expect(api.decide).toHaveBeenCalledWith("ABCD-EFGH", consentToken, "approved");
expect(host.textContent).toContain("Request approved");
});
it("supports an explicit denial instead of an approval", async () => {
await review(); await act(async () => button("Deny request").click());
expect(api.decide).toHaveBeenCalledWith("ABCD-EFGH", consentToken, "denied");
expect(host.textContent).toContain("Request denied");
});
it("requires a new explicit decision after native reauthentication", async () => {
api.decide.mockRejectedValueOnce(new APIError("Confirm identity", 403, { code: "reauth_required" }));
await review(); await act(async () => button("Approve CLI sign-in").click());
expect(api.decide).toHaveBeenCalledTimes(1); expect(api.reauth).not.toHaveBeenCalled();
await act(async () => button("Confirm identity").click());
expect(api.reauth).toHaveBeenCalledTimes(1); expect(api.decide).toHaveBeenCalledTimes(1);
await act(async () => button("Approve CLI sign-in").click());
expect(api.decide).toHaveBeenCalledTimes(2);
});
it("expires pending consent without an automatic decision", async () => {
vi.useFakeTimers(); await review();
await act(async () => { vi.advanceTimersByTime(60001); });
expect(host.textContent).toContain("Request expired"); expect(api.decide).not.toHaveBeenCalled();
});
it.each(["admin_device_expired", "admin_device_resolved"])("keeps %s fail-closed", async (code) => {
api.decide.mockRejectedValueOnce(new APIError("Cannot use code", code.endsWith("expired") ? 404 : 409, { code }));
await review(); await act(async () => button("Approve CLI sign-in").click());
expect(host.textContent).toContain(code.endsWith("expired") ? "Request expired" : "Request already resolved");
expect(host.textContent).not.toContain("Request approved");
});
it.each(["https://other.example.test", "http://user:password@localhost:8080", "http://localhost:8080?token=hidden"])("withholds consent for ambiguous/mismatched instance %s", async (instance_url) => {
api.describe.mockResolvedValue({ ...description(), request: { ...description().request, instance_url } });
await review(); expect(host.textContent).toContain("could not be verified"); expect(host.innerHTML).not.toContain(instance_url); expect(api.decide).not.toHaveBeenCalled();
});
it("does not silently retry an uncertain failed decision", async () => {
api.decide.mockRejectedValueOnce(new Error("Network unavailable"));
await review(); await act(async () => button("Approve CLI sign-in").click());
expect(api.decide).toHaveBeenCalledTimes(1); expect(host.textContent).toContain("Review request"); expect(host.textContent).not.toContain("Request approved");
});
});
+134
View File
@@ -0,0 +1,134 @@
import { useEffect, useRef, useState, type FormEvent } from "react";
import { ShieldCheck } from "lucide-react";
import { PageHeader } from "@/components/layout/PageHeader";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Callout, Panel, Property, PropertyList, StatusBadge } from "@/components/ui/kit";
import { ErrorState } from "@/components/ErrorState";
import { APIError, promptForReauth } from "@/lib/api/client";
import { describeAdminDevice, decideAdminDevice, type DeviceDescription } from "@/lib/api/client/admin/device";
import { API_URL } from "@/lib/env";
import { AdminPerm } from "@/lib/auth/permissions";
import { validDeviceDescription } from "@/lib/adminDevice";
type Phase = "entry" | "pending" | "approved" | "denied" | "expired" | "resolved";
type Review = Omit<DeviceDescription["request"], "consent_token"> & { permissions: number; requestedAccess: string };
export default function DeviceApprovalPage() {
const [code, setCode] = useState("");
const [review, setReview] = useState<Review | null>(null);
const [phase, setPhase] = useState<Phase>("entry");
const [busy, setBusy] = useState(false);
const [needsReauth, setNeedsReauth] = useState(false);
const [error, setError] = useState<unknown>(null);
const consent = useRef("");
const submittedCode = useRef("");
const inFlight = useRef(false);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
return () => { mounted.current = false; consent.current = ""; };
}, []);
useEffect(() => {
if (phase !== "pending" || !review) return;
const expire = () => { consent.current = ""; setPhase("expired"); setNeedsReauth(false); };
const remaining = Date.parse(review.expires_at) - Date.now();
const timer = window.setTimeout(expire, Math.max(0, remaining));
return () => window.clearTimeout(timer);
}, [phase, review]);
function failed(err: unknown) {
setError(err);
if (err instanceof APIError && err.code === "reauth_required") {
setNeedsReauth(true);
} else {
consent.current = "";
if (err instanceof APIError && err.code === "admin_device_expired") setPhase("expired");
else if (err instanceof APIError && err.code === "admin_device_resolved") setPhase("resolved");
else setPhase("entry");
}
}
async function describe(e: FormEvent) {
e.preventDefault();
if (inFlight.current) return;
inFlight.current = true;
setBusy(true); setError(null); setReview(null); setNeedsReauth(false); consent.current = "";
try {
const normalized = code.trim().toUpperCase();
const result = await describeAdminDevice(normalized);
if (!mounted.current) return;
if (!validDeviceDescription(result, API_URL)) throw new Error("The request identity or response could not be verified. Check this admin panel's configured backend before trying again.");
const { consent_token, ...details } = result.request;
consent.current = consent_token;
submittedCode.current = normalized;
setReview({ ...details, permissions: result.admin_permissions, requestedAccess: result.requested_access });
setPhase(Date.parse(details.expires_at) <= Date.now() ? "expired" : "pending");
if (Date.parse(details.expires_at) <= Date.now()) consent.current = "";
} catch (err) { if (mounted.current) failed(err); }
finally { inFlight.current = false; if (mounted.current) setBusy(false); }
}
async function decide(decision: "approved" | "denied") {
if (inFlight.current || phase !== "pending" || !review || !consent.current || needsReauth) return;
if (Date.parse(review.expires_at) <= Date.now()) { consent.current = ""; setPhase("expired"); return; }
inFlight.current = true; setBusy(true); setError(null);
try {
const result = await decideAdminDevice(submittedCode.current, consent.current, decision);
if (!mounted.current) return;
if (result.status !== decision) throw new Error("The decision was not confirmed. Review a new request before trying again.");
consent.current = ""; setPhase(result.status);
} catch (err) { if (mounted.current) failed(err); }
finally { inFlight.current = false; if (mounted.current) setBusy(false); }
}
async function reauthenticate() {
if (inFlight.current) return;
inFlight.current = true; setBusy(true); setError(null);
try {
await promptForReauth();
if (mounted.current) setNeedsReauth(false);
} catch (err) { if (mounted.current) setError(err); }
finally { inFlight.current = false; if (mounted.current) setBusy(false); }
}
return <div className="max-w-3xl">
<PageHeader title="Approve CLI sign-in" description="Authorize a separate platform administrator session only for a CLI request you started." />
<Callout tone="warning" icon={ShieldCheck} title="Only approve your own request" className="mb-6">
Never enter a code someone else sent you. Check the instance and requester against your terminal. Client names are requester-supplied, not verified device identities.
</Callout>
{error != null && <ErrorState error={error} title="Sign-in request could not be completed" className="mb-6" />}
{phase === "entry" && <Panel title="Enter the code from your CLI">
<form onSubmit={describe} className="space-y-4">
<Label htmlFor="cli-code">User code</Label>
<Input id="cli-code" value={code} onChange={(e) => setCode(e.target.value)} autoComplete="off" autoCapitalize="characters" maxLength={9} placeholder="ABCD-EFGH" data-ph-mask="" disabled={busy} />
<p className="text-sm text-muted-foreground">Reviewing a code does not approve it. No CLI credentials are displayed or stored here.</p>
<Button type="submit" disabled={busy || !/^[A-Z2-9]{4}-?[A-Z2-9]{4}$/i.test(code.trim())}>{busy ? "Looking up…" : "Review request"}</Button>
</form>
</Panel>}
{phase === "pending" && review && <Panel title="Review requested access">
<StatusBadge tone="warning">Pending explicit approval</StatusBadge>
<PropertyList className="mt-4">
<Property label="Instance"><span className="break-all font-mono text-xs">{review.instance_url}</span></Property>
<Property label="Requester client">{review.client_name}</Property>
<Property label="Capability">{review.requestedAccess}</Property>
<Property label="Permissions"><span>{Object.entries(AdminPerm).filter(([, bit]) => (review.permissions & bit) === bit).map(([name]) => name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase()).join(", ")} (mask {review.permissions})</span></Property>
<Property label="Expires"><time dateTime={review.expires_at}>{new Date(review.expires_at).toLocaleString()}</time></Property>
</PropertyList>
<p className="my-4 text-sm text-muted-foreground">The CLI receives a separate, normally revocable session. Approval is not confirmation that the CLI has claimed it. Its original login proof can expire while it waits.</p>
{needsReauth ? <div className="space-y-3">
<p role="status" className="text-sm">Confirm your identity, then explicitly choose approve or deny again. Confirmation alone never approves this request.</p>
<Button disabled={busy} onClick={() => void reauthenticate()}>Confirm identity</Button>
</div> : <div className="flex gap-2">
<Button disabled={busy} onClick={() => void decide("approved")}>{busy ? "Submitting…" : "Approve CLI sign-in"}</Button>
<Button variant="outline" disabled={busy} onClick={() => void decide("denied")}>Deny request</Button>
</div>}
</Panel>}
{["approved", "denied", "expired", "resolved"].includes(phase) && <Panel title={phase === "approved" ? "Request approved" : phase === "denied" ? "Request denied" : phase === "expired" ? "Request expired or unavailable" : "Request already resolved"}>
<p role="status" className="text-sm text-muted-foreground">{phase === "approved" ? "Return to your CLI to finish sign-in. No access or refresh credentials are returned to this page. If claiming fails or the original proof expires, start a new request." : phase === "denied" ? "This request cannot sign in. Start a new request in your CLI if needed." : "This code can no longer be used. Start a new request in your CLI, then review its new code here."}</p>
<Button variant="outline" className="mt-4" disabled={busy} onClick={() => { consent.current = ""; setReview(null); setCode(""); setError(null); setPhase("entry"); }}>Enter another code</Button>
</Panel>}
</div>;
}
+8 -3
View File
@@ -1,7 +1,7 @@
// One machine in the fleet.
//
// Everything shown here is reported BY the node or resolved FOR it. There is
// no install, restart, log or reboot button, because nothing reaches into a
// no install, restart or reboot button, because nothing reaches into a
// machine any more: a node enrols with a token, heartbeats, and pulls the
// version it should run. What an operator can actually do is rename it, hold
// it at a version, and forget it.
@@ -56,6 +56,8 @@ import { TONE_PANEL, TONE_TEXT } from "@/lib/tones";
import { cn } from "@/lib/utils";
import { NodeStatePill } from "./fleet/tones";
import { ResourceUsage } from "./fleet/ResourceUsage";
import { NodeDiagnostics } from "./fleet/NodeDiagnostics";
import { ErrorState } from "@/components/ErrorState";
const CRUMBS = [{ label: "Workers", to: "/workers" }];
@@ -134,6 +136,7 @@ export default function WorkerDetailPage() {
</div>
);
}
if (nodeQ.isError) return <div><PageHeader breadcrumbs={CRUMBS} title="Node unavailable" /><ErrorState error={nodeQ.error} onRetry={() => void nodeQ.refetch()} /></div>;
if (!node) {
return (
<div>
@@ -168,7 +171,7 @@ export default function WorkerDetailPage() {
</>
}
>
<Button size="sm" variant="ghost" onClick={() => nodeQ.refetch()}>
<Button size="sm" variant="ghost" onClick={() => { void nodeQ.refetch(); void qc.invalidateQueries({ queryKey: ["admin", "node-diagnostics", id] }); }}>
<RefreshCw className={cn("size-3.5", nodeQ.isFetching && "animate-spin")} />
Refresh
</Button>
@@ -178,7 +181,7 @@ export default function WorkerDetailPage() {
<div className="min-w-0">
{node.last_error && (
<Callout tone="danger" icon={AlertTriangle} title="Last error" className="mb-8">
<span className="break-words font-mono text-xs">{node.last_error}</span>
<span>The node reported an error on its last heartbeat. Inspect redacted evidence below; raw node error text is not displayed.</span>
</Callout>
)}
@@ -277,6 +280,8 @@ export default function WorkerDetailPage() {
</Section>
)}
<NodeDiagnostics node={node} mailboxes={mailboxes} />
<Section
title="Version"
description="The node pulls whatever the fleet is set to. Pin it to hold this one machine back, or to canary a release on it before the rest follow."
@@ -0,0 +1,154 @@
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { FileText, RefreshCw } from "lucide-react";
import { ErrorState } from "@/components/ErrorState";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { EmptyState, Panel, Property, PropertyList, Section, StatusBadge } from "@/components/ui/kit";
import { useMe } from "@/hooks/useMe";
import useBrowseState from "@/hooks/useBrowseState";
import { AdminPerm, hasAdminPerm } from "@/lib/auth/permissions";
import type { FleetNode } from "@/lib/api/client/admin/fleetNodes";
import type { AdminWorkerEmail } from "@/lib/api/models/admin";
import { getNodeLogs, getNodeBroker, type NodeBrokerSource, type NodeLogFilters } from "@/lib/api/client/admin/nodeDiagnostics";
import { diagnosticReason, emptyLogFilters, eventLabels, isUUID, logFilterError, parseLogFilters, safeCount, safeLogEvent, validTime, validatedBroker } from "@/lib/nodeDiagnostics";
function Timestamp({ value }: { value?: string | null }) {
return validTime(value) ? <time dateTime={value}>{new Date(value).toLocaleString()}</time> : <>Not observed</>;
}
const count = (value: unknown) => safeCount(value) ? value.toLocaleString() : "Unknown";
function SourceBadge({ availability }: { availability?: string }) {
return <StatusBadge tone={availability === "fresh" ? "neutral" : "warning"}>{availability === "fresh" ? "Fresh observation" : availability === "stale" ? "Stale observation" : "Unavailable observation"}</StatusBadge>;
}
function BrokerScope({ title, source, group, topic }: { title: string; source: NodeBrokerSource; group: string; topic: string }) {
const observation = validatedBroker(source, group, topic);
const coverage = ["complete", "partial", "unavailable"].includes(source.coverage) ? source.coverage : "unknown";
return <Panel title={title}>
<SourceBadge availability={source.availability} />
<PropertyList className="mt-3">
<Property label="Consumer group"><span className="font-mono text-xs break-all">{group}</span></Property>
<Property label="Topic"><span className="font-mono text-xs break-all">{topic}</span></Property>
<Property label="Coverage">{coverage} ({count(source.measured_scopes)} / {count(source.expected_scopes)} scopes)</Property>
<Property label="Checked"><Timestamp value={source.checked_at} /></Property>
<Property label="Observed"><Timestamp value={source.observed_at} /></Property>
<Property label="Committed lag"><span className="font-mono">{observation ? `${observation.lag.toLocaleString()} messages` : "Unknown / unavailable"}</span></Property>
<Property label="Group members">{observation ? count(observation.members) : "Unknown / unavailable"}</Property>
</PropertyList>
<p className="mt-3 text-xs text-muted-foreground">{observation ? "Validated retained offsets at the observation time. Broker health and delivery remain unknown." : diagnosticReason(source.metrics?.find((m) => m.reason)?.reason ?? source.reason)}</p>
{observation && <details className="mt-4 text-xs">
<summary className="cursor-pointer font-medium">Validated partition offsets</summary>
<div className="mt-2 overflow-x-auto">
<table className="w-full text-left tabular-nums" aria-label={`${title} partition offsets`}>
<thead><tr className="border-b text-muted-foreground"><th className="p-2">Partition</th><th className="p-2">Earliest</th><th className="p-2">Committed</th><th className="p-2">Latest</th><th className="p-2">Lag</th></tr></thead>
<tbody>{observation.detail.partitions.map((p) => <tr key={p.partition} className="border-b"><td className="p-2">{p.partition}</td><td className="p-2">{p.earliest}</td><td className="p-2">{p.committed}</td><td className="p-2">{p.latest}</td><td className="p-2">{p.committed_lag}</td></tr>)}</tbody>
</table>
</div>
</details>}
</Panel>;
}
export function NodeDiagnostics({ node, mailboxes }: { node: FleetNode; mailboxes: AdminWorkerEmail[] }) {
const { data: me } = useMe();
const canViewBroker = !!me && hasAdminPerm(me.admin_permissions, AdminPerm.ViewAnalytics);
const canViewMailboxes = !!me && hasAdminPerm(me.admin_permissions, AdminPerm.ViewUsers);
const [filters, setFilters] = useBrowseState(`node-logs:${node.id}`, emptyLogFilters, parseLogFilters);
const filterError = logFilterError(filters);
const logs = useQuery({
queryKey: ["admin", "node-diagnostics", node.id, "logs", filters],
queryFn: () => getNodeLogs(node.id, filters),
enabled: !filterError,
retry: false, refetchInterval: 30_000,
});
const broker = useQuery({
queryKey: ["admin", "node-diagnostics", node.id, "broker"],
queryFn: () => getNodeBroker(node.id),
enabled: node.role === "worker" && canViewBroker,
retry: false, refetchInterval: 30_000,
});
const history = logs.data;
const matchedHistory = history?.node_id === node.id ? history : undefined;
const readable = !!matchedHistory && ["fresh", "stale"].includes(matchedHistory.availability) && matchedHistory.capture?.protocol === 1;
const events = readable ? (matchedHistory.events ?? []).filter(safeLogEvent) : [];
const hidden = readable && (matchedHistory.events?.length ?? 0) > events.length;
const setFilter = (key: keyof NodeLogFilters, value: string) => setFilters((previous) => ({ ...previous, [key]: value }));
return <>
<Section title="Recent redacted node logs" description="Partial allowlisted evidence, not a full journal, mailbox health check or confirmation of delivery. Snapshots refresh every 30 seconds."
actions={<Button size="xs" variant="ghost" disabled={logs.isFetching || !!filterError} onClick={() => void logs.refetch()}><RefreshCw className="size-3" />{logs.isFetching ? "Refreshing logs…" : "Refresh logs"}</Button>}>
<Panel>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div className="space-y-1.5"><Label htmlFor="node-log-level">Log level</Label>
<Select value={filters.level || "all"} onValueChange={(v) => setFilter("level", v === "all" ? "" : v)}>
<SelectTrigger id="node-log-level"><SelectValue /></SelectTrigger>
<SelectContent><SelectItem value="all">All levels</SelectItem><SelectItem value="info">Info</SelectItem><SelectItem value="warn">Warn</SelectItem><SelectItem value="error">Error</SelectItem></SelectContent>
</Select>
</div>
<div className="space-y-1.5"><Label htmlFor="node-log-after">After (local time)</Label><Input id="node-log-after" type="datetime-local" value={filters.after} onChange={(e) => setFilter("after", e.target.value)} /></div>
<div className="space-y-1.5"><Label htmlFor="node-log-before">Before (local time)</Label><Input id="node-log-before" type="datetime-local" value={filters.before} onChange={(e) => setFilter("before", e.target.value)} /></div>
<div className="space-y-1.5"><Label htmlFor="node-log-mailbox">Mailbox UUID</Label><Input id="node-log-mailbox" placeholder="Any mailbox" maxLength={36} value={filters.mailbox} onChange={(e) => setFilter("mailbox", e.target.value)} /></div>
</div>
<div className="mt-3 flex flex-wrap items-center gap-3"><Button variant="ghost" size="xs" onClick={() => setFilters(emptyLogFilters)}>Clear log filters</Button><span className="text-xs text-muted-foreground">Inclusive time bounds; up to 200 newest matching events. Filters are saved for this admin and node in this browser tab.</span></div>
{filterError && <p role="alert" className="mt-3 text-sm text-destructive">{filterError}</p>}
{!filterError && logs.isLoading && <Skeleton className="mt-4 h-32 w-full" />}
{logs.isError && !filterError && <ErrorState className="mt-4" error={logs.error} title="Node evidence could not be refreshed" onRetry={() => void logs.refetch()} />}
{!filterError && history && !matchedHistory && <p role="alert" className="mt-4 text-sm">Evidence does not match this node. No evidence is displayed.</p>}
{!filterError && matchedHistory && <>
{logs.isError && <p className="mt-3 text-xs text-muted-foreground">Last successful snapshot below. The latest refresh failed; it is not a current observation.</p>}
<div className="mt-5 flex flex-wrap gap-2"><SourceBadge availability={logs.isError ? "stale" : matchedHistory.availability} /><StatusBadge>{matchedHistory.coverage === "partial" ? "Partial coverage" : "Unavailable coverage"}</StatusBadge></div>
<p className="mt-3 text-sm text-muted-foreground">{diagnosticReason(matchedHistory.reason)}</p>
<PropertyList className="mt-3">
<Property label="Source">Node-agent allowlisted events via bounded Redis history. No historical journal import.</Property>
<Property label="Capture protocol">{matchedHistory.capture?.protocol === 1 ? "Protocol 1" : "Not observed or unsupported"}</Property>
<Property label="Snapshot queried"><Timestamp value={matchedHistory.observed_at} /></Property>
<Property label="Capture observed"><Timestamp value={matchedHistory.capture?.observed_at} /></Property>
<Property label="Capture received"><Timestamp value={matchedHistory.capture?.received_at} /></Property>
<Property label="Capture run"><span className="font-mono text-xs">{isUUID(matchedHistory.capture?.run_id ?? "") ? matchedHistory.capture?.run_id : "Not observed"}</span></Property>
<Property label="Run started"><Timestamp value={matchedHistory.capture?.started_at} /></Property>
<Property label="Known drops">{matchedHistory.capture ? count(matchedHistory.capture.dropped) : "Unknown"} (per process run, not total historical loss)</Property>
<Property label="Retention">Up to {count(matchedHistory.retention_seconds)} seconds and {count(matchedHistory.max_events)} events. Metadata expires separately.</Property>
<Property label="Retained window">{readable ? count(matchedHistory.retained_count) : "Unknown"} events across all filters. <Timestamp value={matchedHistory.oldest_retained_at} /> to <Timestamp value={matchedHistory.newest_retained_at} /></Property>
</PropertyList>
<p className="mt-3 text-xs text-muted-foreground">Pre-installation events, abrupt crashes and lost acknowledgements may be uncounted. Empty evidence never means zero errors or healthy mailboxes.</p>
{matchedHistory.truncated && <p role="status" className="mt-3 text-sm">More matching events are retained than this response contains. Narrow the filters to inspect the window.</p>}
{hidden && <p role="status" className="mt-3 text-sm">Unsupported or invalid event records were withheld.</p>}
{!readable ? <EmptyState icon={FileText} title="Log evidence unavailable" hint="No observed event count can be inferred. Check capture availability above." className="py-8" /> : events.length === 0 ?
<EmptyState icon={FileText} title="No retained events match these filters" hint="No matching evidence is not evidence of health. Retention and capture coverage still apply." className="py-8" /> :
<div className="mt-4 overflow-x-auto"><table className="w-full text-left text-xs" aria-label="Redacted node events">
<thead><tr className="border-b text-muted-foreground"><th className="p-2">Observed</th><th className="p-2">Level</th><th className="p-2">Evidence</th><th className="p-2">Mailbox</th></tr></thead>
<tbody>{events.map((event) => {
const mailbox = mailboxes.find((m) => m.id === event.mailbox_id);
const mailboxID = isUUID(event.mailbox_id ?? "") ? event.mailbox_id : null;
return <tr key={event.id} className="border-b align-top">
<td className="whitespace-nowrap p-2"><Timestamp value={event.observed_at} /></td>
<td className="p-2"><StatusBadge tone={event.level === "error" ? "danger" : event.level === "warn" ? "warning" : "neutral"}>{event.level}</StatusBadge></td>
<td className="p-2"><div className="font-medium">{eventLabels[event.event].title}</div><div className="mt-1 text-muted-foreground">{event.category.replaceAll("_", " ")}
{event.event === "sync_control_plane_held" && safeCount(event.http_status) && event.http_status >= 100 && event.http_status <= 599 && ` · HTTP ${event.http_status}`}
{safeCount(event.count) && event.count <= 1000000 && ` · count ${event.count}`}</div></td>
<td className="p-2 font-mono">{mailboxID ? mailbox && canViewMailboxes ? <Link className="text-[var(--admin-accent-strong)] underline underline-offset-2" to={`/mailboxes?q=${encodeURIComponent(mailbox.email)}&worker=${node.id}`} title="Open assigned mailbox in the mailbox browser">{mailboxID}</Link> : <span title="Mailbox identity is not in the currently loaded assignment page, or mailbox access is not permitted.">{mailboxID}</span> : "Not provided"}</td>
</tr>;
})}</tbody>
</table></div>}
</>}
</Panel>
</Section>
<Section title="Broker committed-offset observations" description="Committed lag is not send rate, processing throughput, provider health, confirmation of delivery or evidence of an outage. Command and shared result scopes must never be added together."
actions={node.role === "worker" && canViewBroker ? <Button size="xs" variant="ghost" disabled={broker.isFetching} onClick={() => void broker.refetch()}><RefreshCw className="size-3" />{broker.isFetching ? "Refreshing broker…" : "Refresh broker"}</Button> : undefined}>
{node.role !== "worker" ? <Panel>Command-group broker observations require a worker node. Consumer nodes use generic log evidence above; no worker lag is inferred for this consumer.</Panel> : !canViewBroker ? <Panel>View analytics permission is also required to read broker observations. Log access is unchanged.</Panel> : <>
{broker.isLoading && <Skeleton className="h-48 w-full" />}
{broker.isError && <ErrorState error={broker.error} title="Broker observation could not be refreshed" onRetry={() => void broker.refetch()} />}
{!broker.isError && broker.data && (broker.data.node_id === node.id ? <>
<p className="mb-3 text-xs text-muted-foreground">Snapshot queried: <Timestamp value={broker.data.observed_at} />. Shared results belong to the result-consumer group, not this node's processing or delivery count.</p>
<div className="grid gap-4 xl:grid-cols-2">
<BrokerScope title="Worker command scope" source={broker.data.commands} group={`worker-${node.id}`} topic={`w.${node.id}`} />
<BrokerScope title="Shared result-consumer scope" source={broker.data.results} group="consumer-group" topic="jobs.worker-events" />
</div>
</> : <p role="alert">Broker observation does not match this node. No metrics are displayed.</p>)}
</>}
</Section>
</>;
}
+2
View File
@@ -19,6 +19,7 @@ import {
Network,
Send,
SlidersHorizontal,
Terminal,
TicketPercent,
UserCog,
Users,
@@ -136,6 +137,7 @@ export const NAV_GROUPS: NavGroup[] = [
{
label: "Instance",
items: [
{ label: "CLI sign-in", icon: Terminal, pages: [{ to: "/device", label: "CLI sign-in", title: "Approve CLI sign-in" }] },
{
label: "Setup and health",
icon: HeartPulse,
+48
View File
@@ -0,0 +1,48 @@
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import useBrowseState from "./useBrowseState";
import { ADMIN_BROWSE_PREFIX, clearAdminBrowse, resumeBrowse } from "@/lib/browseState";
import { emptyLogFilters, parseLogFilters } from "@/lib/nodeDiagnostics";
const identity = vi.hoisted(() => ({ id: "admin-a" }));
vi.mock("@/hooks/useMe", () => ({ useMe: () => ({ data: { id: identity.id } }) }));
let root: Root;
let host: HTMLDivElement;
let node = "worker-a";
function View() {
const [value, setValue] = useBrowseState(`node-logs:${node}`, emptyLogFilters, parseLogFilters);
return <><span>{value.level || "all"}</span><button onClick={() => setValue({ ...value, level: "warn" })}>Warn</button></>;
}
beforeEach(async () => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); identity.id = "admin-a"; node = "worker-a";
sessionStorage.clear(); resumeBrowse(); host = document.createElement("div"); root = createRoot(host);
});
afterEach(async () => { await act(async () => root.unmount()); sessionStorage.clear(); vi.unstubAllGlobals(); });
const render = () => act(async () => root.render(<View />));
const key = (owner: string, worker: string) => `${ADMIN_BROWSE_PREFIX}${owner}:node-logs:${worker}`;
describe("admin-scoped diagnostic browse state", () => {
it("restores validated filters on remount and keeps other users/nodes isolated", async () => {
sessionStorage.setItem(key("admin-a", "worker-a"), JSON.stringify({ value: { ...emptyLogFilters, level: "error" } }));
await render(); expect(host.textContent).toContain("error");
identity.id = "admin-b"; await render(); expect(host.textContent).toContain("all");
await act(async () => host.querySelector("button")!.click());
expect(JSON.parse(sessionStorage.getItem(key("admin-b", "worker-a"))!).value.level).toBe("warn");
node = "consumer-a"; await render(); expect(host.textContent).toContain("all");
identity.id = "admin-a"; node = "worker-a"; await render(); expect(host.textContent).toContain("error");
});
it("rejects malformed stored filters without overwriting another user's state", async () => {
sessionStorage.setItem(key("admin-a", "worker-a"), "{broken");
sessionStorage.setItem(key("admin-b", "worker-a"), "untouched");
await render(); expect(host.textContent).toContain("all");
expect(sessionStorage.getItem(key("admin-b", "worker-a"))).toBe("untouched");
});
it("clears on logout and prevents mounted old-session state from reappearing", async () => {
await render(); await act(async () => host.querySelector("button")!.click());
expect(sessionStorage.length).toBe(1);
clearAdminBrowse(); resumeBrowse();
await act(async () => host.querySelector("button")!.click());
expect(sessionStorage.length).toBe(0);
});
});
+36
View File
@@ -0,0 +1,36 @@
import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from "react";
import { useMe } from "@/hooks/useMe";
import { ADMIN_BROWSE_PREFIX, browseGeneration, canPersistBrowse } from "@/lib/browseState";
export default function useBrowseState<T>(name: string, initial: T, parse: (value: unknown) => T | null): [T, Dispatch<SetStateAction<T>>] {
const { data: me } = useMe();
const owner = me?.id;
const key = owner ? `${ADMIN_BROWSE_PREFIX}${owner}:${name}` : null;
const generation = useRef(browseGeneration());
const read = () => {
if (key) {
try {
const raw = sessionStorage.getItem(key);
if (raw) return parse(JSON.parse(raw)?.value) ?? initial;
} catch { /* Browsing works without storage. */ }
}
return initial;
};
const [memory, setMemory] = useState(() => ({ key, value: read() }));
let current = memory;
if (memory.key !== key) {
current = { key, value: read() };
setMemory(current);
}
const setValue = useCallback<Dispatch<SetStateAction<T>>>((next) => {
setMemory((previous) => previous.key !== key ? previous : ({
key, value: typeof next === "function" ? (next as (value: T) => T)(previous.value) : next,
}));
}, [key]);
useEffect(() => {
if (!key || key !== memory.key || !canPersistBrowse(generation.current)) return;
try { sessionStorage.setItem(key, JSON.stringify({ value: memory.value })); }
catch { /* Browsing works without storage. */ }
}, [key, memory]);
return [current.value, setValue];
}
+19
View File
@@ -0,0 +1,19 @@
import type { DeviceDescription } from "@/lib/api/client/admin/device";
export function instanceIdentity(value: string): string | null {
try {
const url = new URL(value);
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) return null;
return url.toString().replace(/\/+$/, "");
} catch { return null; }
}
export function validDeviceDescription(value: DeviceDescription, backend: string): boolean {
const request = value?.request;
const configured = instanceIdentity(backend);
return !!request && request.status === "pending" && !!configured && instanceIdentity(request.instance_url) === configured &&
typeof request.client_name === "string" && request.client_name.length > 0 && request.client_name.length <= 64 &&
/^[A-Za-z0-9_-]{43}$/.test(request.consent_token) && Number.isFinite(Date.parse(request.expires_at)) &&
value.requested_access === "Your live platform administrator permissions through a separate revocable session" &&
Number.isSafeInteger(value.admin_permissions) && value.admin_permissions > 0 && value.admin_permissions <= 0xffffffff;
}
+1 -1
View File
@@ -61,7 +61,7 @@ const REAUTH_PROMPT_TIMEOUT_MS = 2 * 60 * 1000;
// promptForReauth asks ReauthDialog (mounted in the app shell) to confirm the
// operator, resolving on success and rejecting on cancel.
function promptForReauth(): Promise<void> {
export function promptForReauth(): Promise<void> {
return new Promise<void>((resolve, reject) => {
let settled = false;
let timer = 0;
@@ -0,0 +1,16 @@
import { describe, expect, it, vi } from "vitest";
import { describeAdminDevice, decideAdminDevice } from "./device";
const request = vi.hoisted(() => vi.fn());
vi.mock("@/lib/api/client", () => ({ Request: request }));
describe("device API consent contract", () => {
it("uses authorized describe without any automatic grant or poll request", async () => {
request.mockResolvedValueOnce({ request: { status: "pending" } });
await describeAdminDevice("ABCD-EFGH");
expect(request).toHaveBeenLastCalledWith({ method: "POST", url: "/admin/auth/device/describe", authorization: true, data: { user_code: "ABCD-EFGH" } });
});
it("disables the usual automatic retry after fresh-auth so consent is explicit twice", async () => {
await decideAdminDevice("ABCD-EFGH", "fixture-consent", "denied");
expect(request).toHaveBeenLastCalledWith({ method: "POST", url: "/admin/auth/device/decide", authorization: true, skipReauthPrompt: true, data: { user_code: "ABCD-EFGH", consent_token: "fixture-consent", decision: "denied" } });
});
});
+24
View File
@@ -0,0 +1,24 @@
import { Request } from "@/lib/api/client";
export interface DeviceDescription {
request: {
client_name: string;
instance_url: string;
expires_at: string;
status: "pending";
consent_token: string;
};
requested_access: string;
admin_permissions: number;
}
export function describeAdminDevice(userCode: string): Promise<DeviceDescription> {
return Request({ method: "POST", url: "/admin/auth/device/describe", authorization: true, data: { user_code: userCode } });
}
export function decideAdminDevice(userCode: string, consentToken: string, decision: "approved" | "denied"): Promise<{ status: "approved" | "denied" }> {
return Request({
method: "POST", url: "/admin/auth/device/decide", authorization: true, skipReauthPrompt: true,
data: { user_code: userCode, consent_token: consentToken, decision },
});
}
+2 -1
View File
@@ -2,7 +2,8 @@
//
// Nodes are pull-based: they enrol with the join token, heartbeat, and ask what
// version they should be running. Nothing here reaches into a machine, so there
// is no install, restart, logs or reboot call to make.
// is no install, restart or reboot call to make. Redacted submitted evidence
// is read through the generic node diagnostics API.
import { Request } from "@/lib/api/client";
@@ -0,0 +1,50 @@
import { Request } from "@/lib/api/client";
import type { MonitoringAvailability, MonitoringSource, MonitoringMetric } from "./monitoring";
export interface NodeLogEvent {
id: string;
observed_at: string;
event: string;
level: "info" | "warn" | "error";
category: string;
mailbox_id?: string;
http_status?: number;
count?: number;
}
export interface NodeLogHistory {
node_id: string;
availability: MonitoringAvailability;
coverage: "partial" | "unavailable";
reason?: string;
observed_at: string;
capture?: { protocol: number; run_id: string; started_at: string; observed_at: string; received_at: string; dropped: number };
events: NodeLogEvent[];
oldest_retained_at?: string;
newest_retained_at?: string;
retained_count: number;
truncated: boolean;
retention_seconds: number;
max_events: number;
}
export interface NodeLogFilters { level: "" | "info" | "warn" | "error"; after: string; before: string; mailbox: string }
export interface BrokerPartition { topic: string; partition: number; earliest: number; committed: number; latest: number; committed_lag: number }
export interface BrokerDetail { consumer_group: string; topics: string[]; partitions: BrokerPartition[] }
export type NodeBrokerMetric = MonitoringMetric & { broker?: BrokerDetail };
export type NodeBrokerSource = Omit<MonitoringSource, "metrics"> & { metrics: NodeBrokerMetric[] };
export interface NodeBrokerObservation { node_id: string; observed_at: string; commands: NodeBrokerSource; results: NodeBrokerSource; note: string }
export function getNodeLogs(id: string, filters: NodeLogFilters): Promise<NodeLogHistory> {
const params = new URLSearchParams({ limit: "200" });
if (filters.level) params.set("level", filters.level);
if (filters.after) params.set("after", new Date(filters.after).toISOString());
if (filters.before) params.set("before", new Date(filters.before).toISOString());
if (filters.mailbox.trim()) params.set("mailbox_id", filters.mailbox.trim());
return Request({ method: "GET", url: `/admin/fleet/nodes/${encodeURIComponent(id)}/logs?${params}`, authorization: true });
}
export function getNodeBroker(id: string): Promise<NodeBrokerObservation> {
return Request({ method: "GET", url: `/admin/fleet/nodes/${encodeURIComponent(id)}/broker`, authorization: true, timeout: 12_000 });
}
+5
View File
@@ -12,6 +12,8 @@
// intentional — the admin surface should require a fresh, deliberate
// auth event, not silently inherit a dashboard session.
import { clearAdminBrowse, resumeBrowse } from "@/lib/browseState";
const TOKEN_KEY = "warmbly_admin_token";
export interface AdminToken {
@@ -33,13 +35,16 @@ export function getToken(): AdminToken | null {
export function setToken(t: AdminToken | null) {
if (!t) {
clearAdminBrowse();
localStorage.removeItem(TOKEN_KEY);
return;
}
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
resumeBrowse();
}
export function clearToken() {
clearAdminBrowse();
localStorage.removeItem(TOKEN_KEY);
}
+17
View File
@@ -0,0 +1,17 @@
export const ADMIN_BROWSE_PREFIX = "warmbly:admin:browse:v1:";
let generation = 0;
let active = true;
export const browseGeneration = () => generation;
export const canPersistBrowse = (value: number) => active && value === generation;
export const resumeBrowse = () => { active = true; };
export function clearAdminBrowse() {
active = false;
generation++;
try {
for (let index = sessionStorage.length - 1; index >= 0; index--) {
const key = sessionStorage.key(index);
if (key?.startsWith(ADMIN_BROWSE_PREFIX)) sessionStorage.removeItem(key);
}
} catch { /* Storage may be disabled. */ }
}
+78
View File
@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import type { NodeBrokerSource, NodeLogEvent } from "@/lib/api/client/admin/nodeDiagnostics";
import { emptyLogFilters, logFilterError, parseLogFilters, safeLogEvent, validatedBroker } from "./nodeDiagnostics";
const id = "11111111-1111-4111-8111-111111111111";
const group = `worker-${id}`;
const topic = `w.${id}`;
const at = "2026-10-10T08:00:00Z";
function source(): NodeBrokerSource {
return {
id, availability: "fresh", coverage: "complete", observed_at: at, checked_at: at, measured_scopes: 1, expected_scopes: 1,
metrics: [{ id: "committed_lag", title: "Broker committed_lag", unit: "messages", scope_id: id, availability: "fresh", condition: "unknown", severity: "info", note: "Observation only", count: 6,
observed_at: at, evidence_at: at, latest_evidence_at: at, affected_mailboxes: null, affected_organizations: null, unknown_organization_rows: null, evidence_age_seconds: null, next_eligible_at: null, window_start: null, window_end: null, threshold_seconds: null,
broker: { consumer_group: group, topics: [topic], partitions: [{ topic, partition: 0, earliest: 10, committed: 16, latest: 22, committed_lag: 6 }] },
}],
};
}
describe("read-only node broker evidence", () => {
it("accepts exact retained committed offsets, not send rates or invented member counts", () => {
const result = validatedBroker(source(), group, topic);
expect(result?.lag).toBe(6);
expect(result?.members).toBeNull();
});
it("keeps validated zero lag distinct from an unsupported observation", () => {
const zero = source();
zero.metrics[0].count = 0;
zero.metrics[0].broker!.partitions[0].committed = 22;
zero.metrics[0].broker!.partitions[0].committed_lag = 0;
expect(validatedBroker(zero, group, topic)?.lag).toBe(0);
expect(validatedBroker({ ...zero, availability: "unavailable", metrics: [], reason: "unsupported" }, group, topic)).toBeNull();
});
it.each(["stale", "unavailable"] as const)("does not render %s observations as fresh values", (availability) => {
expect(validatedBroker({ ...source(), availability }, group, topic)).toBeNull();
});
it("does not attribute the shared result group to worker commands", () => {
expect(validatedBroker(source(), "consumer-group", "jobs.worker-events")).toBeNull();
});
it.each(["missing", "duplicate", "negative", "out-of-retention", "future-commit", "mismatch", "wrong-topic", "wrong-group", "overflow", "unsafe-offset", "healthy-condition", "wrong-scope", "partial"])("withholds %s data", (invalid) => {
const value = source();
const metric = value.metrics[0];
const detail = metric.broker!;
const partition = detail.partitions[0];
switch (invalid) {
case "missing": metric.count = null; break;
case "duplicate": detail.partitions.push({ ...partition }); break;
case "negative": partition.partition = -1; break;
case "out-of-retention": partition.committed = 9; break;
case "future-commit": partition.committed = 23; break;
case "mismatch": metric.count = 7; break;
case "wrong-topic": partition.topic = "unexpected"; break;
case "wrong-group": detail.consumer_group = "consumer-group"; break;
case "overflow": detail.partitions = Array.from({ length: 129 }, (_, i) => ({ ...partition, partition: i })); break;
case "unsafe-offset": partition.latest = Number.MAX_SAFE_INTEGER + 1; break;
case "healthy-condition": metric.condition = "healthy"; break;
case "wrong-scope": metric.scope_id = "other"; break;
case "partial": value.coverage = "partial"; break;
}
expect(validatedBroker(value, group, topic)).toBeNull();
});
});
describe("redacted node evidence and filters", () => {
it("only accepts canonical allowlisted event records", () => {
const event: NodeLogEvent = { id, event: "sync_control_plane_held", observed_at: at, level: "warn", category: "control_plane", http_status: 503 };
expect(safeLogEvent(event)).toBe(true);
expect(safeLogEvent({ ...event, event: "raw_provider_message" })).toBe(false);
expect(safeLogEvent({ ...event, category: "provider" })).toBe(false);
expect(safeLogEvent({ ...event, observed_at: "invalid" })).toBe(false);
});
it("strips unrelated stored fields and rejects invalid UUID/time ranges", () => {
expect(parseLogFilters({ ...emptyLogFilters, level: "warn", secret: "not-stored" })).toEqual({ ...emptyLogFilters, level: "warn" });
expect(parseLogFilters({ ...emptyLogFilters, mailbox: "not-a-uuid" })).toBeNull();
expect(parseLogFilters({ ...emptyLogFilters, after: "invalid" })).toBeNull();
expect(logFilterError({ ...emptyLogFilters, after: "2026-10-10T10:00", before: "2026-10-10T09:00" })).toMatch(/Start time/);
expect(logFilterError({ ...emptyLogFilters, mailbox: id, after: "2026-10-10T08:00" })).toBeNull();
});
});
+82
View File
@@ -0,0 +1,82 @@
import type { NodeBrokerSource, NodeLogFilters, NodeLogEvent, BrokerDetail } from "@/lib/api/client/admin/nodeDiagnostics";
export const emptyLogFilters: NodeLogFilters = { level: "", after: "", before: "", mailbox: "" };
export const isUUID = (value: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) && value !== "00000000-0000-0000-0000-000000000000";
export const safeCount = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
export const validTime = (value: unknown): value is string => typeof value === "string" && Number.isFinite(Date.parse(value));
export function parseLogFilters(value: unknown): NodeLogFilters | null {
if (!value || typeof value !== "object") return null;
const v = value as NodeLogFilters;
if (!["", "info", "warn", "error"].includes(v.level) || typeof v.after !== "string" || typeof v.before !== "string" || v.after.length > 24 || v.before.length > 24 || typeof v.mailbox !== "string" || v.mailbox.length > 36) return null;
if (logFilterError(v)) return null;
return { level: v.level, after: v.after, before: v.before, mailbox: v.mailbox };
}
export function logFilterError(value: NodeLogFilters): string | null {
if (value.mailbox.trim() && !isUUID(value.mailbox.trim())) return "Enter a valid mailbox UUID.";
if ((value.after && !validTime(value.after)) || (value.before && !validTime(value.before))) return "Enter valid start and end times.";
if (value.after && value.before && Date.parse(value.after) > Date.parse(value.before)) return "Start time must not be after end time.";
return null;
}
export const eventLabels: Record<string, { title: string; level: string; category: string }> = {
sync_control_plane_held: { title: "Control-plane sync held", level: "warn", category: "control_plane" },
mailbox_provider_unreachable: { title: "Mailbox provider unreachable", level: "warn", category: "provider" },
mailbox_provider_auth_failed: { title: "Mailbox provider authentication failed", level: "warn", category: "provider" },
mailbox_provider_throttled: { title: "Mailbox provider throttled", level: "warn", category: "provider" },
mailbox_provider_sync_completed: { title: "Mailbox provider sync completed", level: "info", category: "provider" },
sync_folder_skipped: { title: "Sync folder skipped", level: "info", category: "sync_policy" },
sync_skipped_folder_search_failed: { title: "Folder search failed; sync skipped", level: "warn", category: "provider" },
sync_message_deferred: { title: "Sync message deferred", level: "warn", category: "control_plane" },
sync_message_deferred_by_policy: { title: "Sync message deferred by policy", level: "info", category: "sync_policy" },
sync_legacy_arrival: { title: "Legacy sync arrival", level: "warn", category: "compatibility" },
worker_error: { title: "Coarse worker runtime error", level: "error", category: "worker_runtime" },
};
export function safeLogEvent(event: NodeLogEvent): boolean {
const canonical = eventLabels[event.event];
return !!canonical && isUUID(event.id) && validTime(event.observed_at) && canonical.level === event.level && canonical.category === event.category;
}
export const diagnosticReason = (reason?: string) => ({
log_credential_unavailable: "Log credential unavailable. Existing or manually enrolled nodes need upgrade and re-enrollment.",
capture_not_observed: "No capture has been observed.",
cache_unavailable: "Evidence cache could not be read.",
no_recent_capture: "No recent capture. Older retained evidence is not current health.",
no_recent_evidence: "Recent capture contained no retained events. This does not establish health.",
allowlisted_evidence_only: "Only allowlisted, redacted events are captured, not the full journal.",
unsupported: "This broker does not support committed-offset observations.",
dependency_missing: "Broker diagnostics are not configured.",
resource_absent: "The requested group or topic could not be observed.",
permission_denied: "The broker denied read access.",
timeout: "The broker observation timed out.",
collection_failed: "The observation could not be validated or collected. Missing commits, mismatched scopes or offsets outside retention remain unknown.",
}[reason ?? ""] ?? "Observation unavailable or incomplete.");
export function validatedBroker(source: NodeBrokerSource, group: string, topic: string): { lag: number; members: number | null; detail: BrokerDetail } | null {
if (source.availability !== "fresh" || source.coverage !== "complete" || source.measured_scopes !== 1 || source.expected_scopes !== 1) return null;
const scope = group === "consumer-group" ? "worker_events" : group.replace(/^worker-/, "");
if (source.id !== scope || !validTime(source.observed_at) || !validTime(source.checked_at)) return null;
const metrics = source.metrics ?? [];
const lagMetrics = metrics.filter((m) => m.id === "committed_lag");
const metric = lagMetrics[0];
if (lagMetrics.length !== 1 || !metric || metric.scope_id !== scope || metric.availability !== "fresh" || metric.condition !== "unknown" || !safeCount(metric.count) || !validTime(metric.observed_at)) return null;
const detail = metric.broker;
if (!detail || detail.consumer_group !== group || detail.topics?.length !== 1 || detail.topics[0] !== topic || !Array.isArray(detail.partitions) || detail.partitions.length < 1 || detail.partitions.length > 128) return null;
let total = 0;
const seen = new Set<number>();
for (const partition of detail.partitions) {
if (partition.topic !== topic || !safeCount(partition.partition) || partition.partition > 0x7fffffff || seen.has(partition.partition) ||
!safeCount(partition.earliest) || !safeCount(partition.committed) || !safeCount(partition.latest) || !safeCount(partition.committed_lag) ||
partition.earliest > partition.committed || partition.committed > partition.latest || partition.latest - partition.committed !== partition.committed_lag) return null;
seen.add(partition.partition);
total += partition.committed_lag;
if (!safeCount(total)) return null;
}
if (total !== metric.count) return null;
const memberMetrics = metrics.filter((m) => m.id === "members");
const member = memberMetrics[0];
const members = memberMetrics.length === 1 && member?.scope_id === scope && member.availability === "fresh" && member.condition === "unknown" && validTime(member.observed_at) && safeCount(member.count) ? member.count : null;
return { lag: total, members, detail };
}
+2
View File
@@ -24,6 +24,7 @@ import { RequireAdmin } from "@/components/layout/RequireAdmin";
import { RouteError } from "@/components/layout/RouteError";
import LoginPage from "@/app/auth/LoginPage";
import DeviceApprovalPage from "@/app/auth/DeviceApprovalPage";
import OverviewPage from "@/app/dashboard/OverviewPage";
import WorkersPage from "@/app/dashboard/WorkersPage";
import WorkerDetailPage from "@/app/dashboard/WorkerDetailPage";
@@ -135,6 +136,7 @@ const router = createBrowserRouter([
errorElement: <RouteError />,
children: [
{ index: true, element: <OverviewPage /> },
{ path: "device", element: <DeviceApprovalPage /> },
// Operations
{ path: "workers", element: gated(AdminPerm.ViewWorkers, <WorkersPage />) },
+164
View File
@@ -0,0 +1,164 @@
import { expect, test } from "../lib/proof.ts";
import { env } from "../lib/env.ts";
test.use({ signedIn: false, timezoneId: "UTC" });
const worker = "11111111-1111-4111-8111-111111111111";
const consumer = "22222222-2222-4222-8222-222222222222";
const mailbox = "33333333-3333-4333-8333-333333333333";
const admin = "44444444-4444-4444-8444-444444444444";
// Parent-run only: the worktree's localhost admin Vite origin with VITE_API_URL=QA_API_URL.
test("admin CLI consent and redacted node diagnostics with disclosed local response fixtures", async ({ page, proof }) => {
test.skip(process.env.QA_ADMIN_OPERATIONS_FIXTURES !== "1", "Set QA_ADMIN_OPERATIONS_FIXTURES=1 on this worktree's localhost admin origin.");
for (const origin of [env.webURL, env.apiURL]) {
expect(["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname)).toBe(true);
}
const at = new Date().toISOString();
const started = new Date(Date.now() - 30000).toISOString();
const expires = new Date(Date.now() + 5 * 60000).toISOString();
const before = new Date(Date.now() + 60000).toISOString().slice(0, 16);
let mfa = false;
let confirmed = false;
let decisions = 0;
let descriptions = 0;
let unsupportedResults = false;
let consumerBrokerReads = 0;
const node = (id: string, role: string) => ({ id, role, name: role === "worker" ? "fixture-operations-worker" : "fixture-operations-consumer", notes: "Synthetic UI fixture", region: "local", address: "", capacity_target: 100, version: "fixture", active: true, last_seen_at: at, enrolled_at: started, usage: {}, mailbox_count: role === "worker" ? 1 : 0, created_at: started, updated_at: at });
const metric = (scope: string, id: string, value: number) => ({ id, title: `Broker ${id}`, unit: id === "members" ? "consumers" : "messages", scope_id: scope, availability: "fresh", condition: "unknown", severity: "info", note: "Synthetic committed-offset observation, not delivery evidence", count: value, affected_mailboxes: null, affected_organizations: null, unknown_organization_rows: null, observed_at: at, evidence_at: at, latest_evidence_at: at, evidence_age_seconds: null, next_eligible_at: null, window_start: null, window_end: null, threshold_seconds: null });
const source = (scope: string, group: string, topic: string, lag: number) => ({ id: scope, availability: "fresh", coverage: "complete", checked_at: at, observed_at: at, measured_scopes: 1, expected_scopes: 1, metrics: [{ ...metric(scope, "committed_lag", lag), broker: { consumer_group: group, topics: [topic], partitions: [{ topic, partition: 0, earliest: 10, committed: 16, latest: 16 + lag, committed_lag: lag }] } }, metric(scope, "members", 1)] });
const held = { id: "55555555-5555-4555-8555-555555555555", observed_at: at, event: "sync_control_plane_held", level: "warn", category: "control_plane", mailbox_id: mailbox, http_status: 503, count: 1 };
const runtime = { id: "66666666-6666-4666-8666-666666666666", observed_at: at, event: "worker_error", level: "error", category: "worker_runtime", count: 1 };
await page.route(`${env.apiURL}/**`, async (route) => {
const request = route.request();
const url = new URL(request.url());
const path = url.pathname;
const headers = { "Cache-Control": "no-store", "Access-Control-Allow-Origin": new URL(env.webURL).origin, "Access-Control-Allow-Headers": "Authorization, Content-Type", "Access-Control-Allow-Methods": "GET, POST, OPTIONS" };
const json = (body: unknown, status = 200) => route.fulfill({ status, contentType: "application/json", headers, body: JSON.stringify(body) });
if (request.method() === "OPTIONS") return route.fulfill({ status: 204, headers });
if (path === "/v1/auth/config") return json({ captcha: false });
if (path === "/v1/auth/me") return json({ id: admin, email: "fixture-admin@example.test", is_admin: true, admin_permissions: (1 << 0) | (1 << 4) | (1 << 11), session_mfa_verified: mfa });
if (path === "/v1/auth/reauth") { confirmed = true; return json({ valid_for_seconds: 300 }); }
if (path === "/admin/auth/device/describe") {
descriptions++;
const body = request.postDataJSON() as { user_code: string };
if (body.user_code === "JKLM-2345") return json({ error: "expired", message: "Request expired or already consumed", code: "admin_device_expired", request_id: "fixture-expired" }, 404);
return json({ request: { client_name: "Local fixture laptop (unverified name)", instance_url: env.apiURL, expires_at: expires, status: "pending", consent_token: "c".repeat(43) }, requested_access: "Your live platform administrator permissions through a separate revocable session", admin_permissions: (1 << 0) | (1 << 4) | (1 << 11) });
}
if (path === "/admin/auth/device/decide") {
decisions++;
const body = request.postDataJSON() as { user_code: string; consent_token: string; decision: string };
expect(body.user_code).toBe("ABCD-EFGH"); expect(body.consent_token).toBe("c".repeat(43));
if (!confirmed) return json({ error: "forbidden", message: "Fresh authentication required", code: "reauth_required", request_id: "fixture-reauth" }, 403);
return json({ status: body.decision });
}
if (path === "/admin/fleet/nodes") return json({ data: [node(worker, "worker"), node(consumer, "consumer")] });
if (path === `/admin/workers/${worker}/stats`) return json({ worker_id: worker, total_emails_sent: 0, emails_sent_today: 0, emails_sent_this_week: 0, average_delivery_time_ms: 0, success_rate: 0, queue_depth: 0 });
if (path === `/admin/workers/${worker}/emails`) return json({ data: [{ id: mailbox, email: "fixture-mailbox@example.test", provider: "gmail", user_id: admin }], pagination: { has_more: false, total: 1 } });
if (path === `/admin/fleet/nodes/${worker}/logs`) {
const events = [held, runtime].filter((event) => (!url.searchParams.get("level") || event.level === url.searchParams.get("level")) && (!url.searchParams.get("mailbox_id") || ("mailbox_id" in event && event.mailbox_id === url.searchParams.get("mailbox_id"))) && (!url.searchParams.get("after") || Date.parse(event.observed_at) >= Date.parse(url.searchParams.get("after")!)) && (!url.searchParams.get("before") || Date.parse(event.observed_at) <= Date.parse(url.searchParams.get("before")!)));
return json({ node_id: worker, availability: "fresh", coverage: "partial", reason: "allowlisted_evidence_only", observed_at: at, capture: { protocol: 1, run_id: "77777777-7777-4777-8777-777777777777", started_at: started, observed_at: at, received_at: at, dropped: 3 }, events, oldest_retained_at: at, newest_retained_at: at, retained_count: 2, truncated: false, retention_seconds: 3600, max_events: 1000 });
}
if (path === `/admin/fleet/nodes/${consumer}/logs`) return json({ node_id: consumer, availability: "unavailable", coverage: "unavailable", reason: "log_credential_unavailable", observed_at: at, events: [], retained_count: 0, truncated: false, retention_seconds: 3600, max_events: 1000 });
if (path === `/admin/fleet/nodes/${consumer}/broker`) { consumerBrokerReads++; return json({ code: "bad_request", message: "Worker required" }, 400); }
if (path === `/admin/fleet/nodes/${worker}/broker`) return json({ node_id: worker, observed_at: at, commands: source(worker, `worker-${worker}`, `w.${worker}`, 6), results: unsupportedResults ? { id: "worker_events", availability: "unavailable", coverage: "unavailable", reason: "unsupported", checked_at: at, observed_at: at, measured_scopes: null, expected_scopes: null, metrics: [] } : source("worker_events", "consumer-group", "jobs.worker-events", 3), note: "Shared results are not this worker's delivery count; lag is not outage evidence." });
if (path === "/admin/instance/health") return json({ checks: [], checked_at: at });
if (path === "/admin/instance/monitoring") return json({ version: "fixture", checked_at: at, refresh_after: expires, coverage: "unavailable", sources: [] });
return json({ code: "service_unavailable", message: "Not included in the local UI fixture", request_id: "fixture-only" }, 503);
});
await page.goto("/device?user_code=ABCD-EFGH&approve=true");
await expect(page).toHaveURL(/\/auth\/login$/);
await expect(page.getByRole("heading", { name: "Sign in to Warmbly", exact: true })).toBeVisible();
expect(decisions).toBe(0); expect(descriptions).toBe(0);
await proof.chapter("Native sign-in boundary", "Local response fixtures only. This flow does not prove actual MFA, a real CLI session, Redis retention or live Kafka observations.");
await proof.shot("admin-cli-sign-in-boundary", { caption: "Unauthenticated /device redirects to the unchanged login screen. Synthetic fixtures are used throughout; no production credentials or real device secrets." });
await page.addInitScript(() => localStorage.setItem("warmbly_admin_token", JSON.stringify({ access_token: "fixture-not-a-credential", refresh_token: "fixture-not-a-credential", access_token_expires_at: "2099-01-01T00:00:00Z", refresh_token_expires_at: "2099-01-01T00:00:00Z" })));
await page.goto("/device");
await expect(page.getByRole("heading", { name: "Two-factor authentication required", exact: true })).toBeVisible();
expect(descriptions).toBe(0);
mfa = true;
await page.reload();
await expect(page.getByRole("button", { name: "Review request", exact: true })).toBeVisible();
await page.getByRole("textbox", { name: "User code", exact: true }).fill("ABCD-EFGH");
await page.getByRole("button", { name: "Review request", exact: true }).click();
await expect(page.getByText("Pending explicit approval", { exact: true })).toBeVisible();
await expect(page.getByText(env.apiURL, { exact: true })).toBeVisible();
await expect(page.getByText("Local fixture laptop (unverified name)", { exact: true })).toBeVisible();
expect(decisions).toBe(0);
await proof.chapter("Review is not approval", "Synthetic client/instance/access/expiry response. Opening the link, passing the MFA guard fixture and reviewing do not authorize the CLI.");
await proof.shot("admin-cli-pending-consent", { caption: "Disclosed local fixture: explicit pending consent shows instance, requester-supplied client name, administrator capability and expiry. No automatic decision." });
await page.getByRole("button", { name: "Approve CLI sign-in", exact: true }).click();
await page.getByRole("button", { name: "Confirm identity", exact: true }).click();
const confirmation = page.getByRole("alertdialog");
await expect(confirmation).toBeVisible();
await confirmation.getByPlaceholder("Two-factor or recovery code", { exact: true }).fill("123456");
await confirmation.getByRole("button", { name: "Confirm", exact: true }).click();
await expect(confirmation).not.toBeVisible();
await expect(page.getByRole("button", { name: "Approve CLI sign-in", exact: true })).toBeEnabled();
expect(decisions).toBe(1);
await page.getByRole("button", { name: "Approve CLI sign-in", exact: true }).click();
await expect(page.getByText("Request approved", { exact: true })).toBeVisible();
expect(decisions).toBe(2);
await proof.shot("admin-cli-explicit-approved", { caption: "Synthetic decision and reauth responses, not real MFA or session issuance. Native identity confirmation alone never retries approval; a second explicit click is required." });
await page.getByRole("button", { name: "Enter another code", exact: true }).click();
await page.getByRole("textbox", { name: "User code", exact: true }).fill("ABCD-EFGH");
await page.getByRole("button", { name: "Review request", exact: true }).click();
await page.getByRole("button", { name: "Deny request", exact: true }).click();
await expect(page.getByText("Request denied", { exact: true })).toBeVisible();
await proof.shot("admin-cli-explicit-denied", { caption: "Local decision fixture: explicit denial resolves the request without returning credentials." });
await page.getByRole("button", { name: "Enter another code", exact: true }).click();
await page.getByRole("textbox", { name: "User code", exact: true }).fill("JKLM-2345");
await page.getByRole("button", { name: "Review request", exact: true }).click();
await expect(page.getByText("Request expired or unavailable", { exact: true })).toBeVisible();
await proof.shot("admin-cli-expired-request", { caption: "Local 404 admin_device_expired fixture keeps unknown/expired/consumed codes fail-closed." });
await page.goto(`/workers/${worker}`);
const logs = page.locator("section").filter({ has: page.getByRole("heading", { name: "Recent redacted node logs", exact: true }) });
await logs.getByRole("heading").scrollIntoViewIfNeeded();
await expect(logs.getByRole("table", { name: "Redacted node events", exact: true })).toBeVisible();
await expect(logs.getByText("Control-plane sync held", { exact: true })).toBeVisible();
await expect(logs).toContainText("HTTP 503"); await expect(logs).toContainText("Partial coverage"); await expect(logs).toContainText("per process run");
await expect(logs.getByRole("link", { name: mailbox, exact: true })).toHaveAttribute("href", new RegExp("/mailboxes\\?q=fixture-mailbox%40example.test"));
await proof.chapter("Redacted operational evidence", "All node capture/log/offset responses are synthetic localhost fixtures. They do not establish production sampling, provider health, delivery or the cause of an arrival 503.");
await logs.getByRole("table", { name: "Redacted node events", exact: true }).scrollIntoViewIfNeeded();
await proof.shot("admin-node-redacted-evidence", { caption: "Synthetic capture and allowlisted events: timestamps, partial coverage, retention bounds, three known per-run drops, numeric control-plane HTTP 503 and native mailbox link." });
await logs.getByRole("combobox", { name: "Log level", exact: true }).click();
await page.getByRole("option", { name: "Warn", exact: true }).click();
await logs.getByRole("textbox", { name: "Mailbox UUID", exact: true }).fill(mailbox);
await logs.getByLabel("Before (local time)", { exact: true }).fill(before);
await expect(logs.getByText("Coarse worker runtime error", { exact: true })).toHaveCount(0);
await page.reload();
await expect(logs.getByRole("textbox", { name: "Mailbox UUID", exact: true })).toHaveValue(mailbox);
await expect(logs.getByLabel("Before (local time)", { exact: true })).toHaveValue(before);
await expect(logs.getByRole("combobox", { name: "Log level", exact: true })).toContainText("Warn");
await logs.getByRole("heading").scrollIntoViewIfNeeded();
await proof.shot("admin-node-persistent-filters", { caption: "Local fixture filters survive refresh under the current admin and node. Window counts cover all retained events, not just the filtered subset." });
const broker = page.locator("section").filter({ has: page.getByRole("heading", { name: "Broker committed-offset observations", exact: true }) });
await broker.getByRole("heading").scrollIntoViewIfNeeded();
await expect(broker).toContainText("Worker command scope"); await expect(broker).toContainText("Shared result-consumer scope");
await expect(broker).toContainText("6 messages"); await expect(broker).toContainText("3 messages");
await expect(broker).toContainText("consumer-group"); await expect(broker).toContainText("jobs.worker-events");
await broker.getByText("Validated partition offsets", { exact: true }).first().click();
await expect(broker.getByRole("table", { name: "Worker command scope partition offsets", exact: true })).toBeVisible();
await proof.shot("admin-node-separate-broker-scopes", { caption: "Synthetic retained offsets earliest 10 / committed 16 / latest 22 yield command lag 6. Shared result lag 3 is separate, never added, attributed to this worker or labeled an outage/send rate." });
unsupportedResults = true;
await broker.getByRole("button", { name: "Refresh broker", exact: true }).click();
await expect(broker).toContainText("This broker does not support"); await expect(broker).toContainText("Unknown / unavailable");
await proof.shot("admin-node-unavailable-broker", { caption: "Synthetic unsupported shared-result observation remains unknown/unavailable while independent worker command evidence stays visible. No fabricated zero lag or healthy state." });
await page.goto(`/workers/${consumer}`);
await expect(logs.getByText("Log evidence unavailable", { exact: true })).toBeVisible();
await expect(logs).toContainText("upgrade and re-enrollment");
await expect(broker).toContainText("Command-group broker observations require a worker node");
expect(consumerBrokerReads).toBe(0);
await logs.getByRole("heading").scrollIntoViewIfNeeded();
await proof.shot("admin-consumer-generic-evidence", { caption: "Local older-enrollment consumer fixture: generic log capture unavailable, retained event count unknown, no worker-command query and no inferred consumer lag." });
await page.goto(`/workers/${worker}`);
await expect(logs.getByRole("textbox", { name: "Mailbox UUID", exact: true })).toHaveValue(mailbox);
await expect(logs.getByRole("combobox", { name: "Log level", exact: true })).toContainText("Warn");
});