Raise alerts: failed or paused prints, and AI detections

Nothing subscribed to channel:print_alerts and nothing watched a printer
fail, so an operator away from the screen found out when they came back to
a ruined plate. Alerts are now rows in printer_alerts, listed at
GET /alerts (open=true for what still needs a person) and cleared with
POST /alerts/:id/acknowledge, which is operator-only and keeps the first
acknowledger on the record.

Two sources. Telemetry: a change into error is a failed print if the printer
was printing or paused, otherwise a printer error; a print the printer
paused itself (how an AMS fault looks) is announced too. Transitions, not
levels - a printer sitting in error is one alert, not one per frame - and a
connection's first sight of a printer raises nothing, so a gateway that
reconnects does not re-announce known faults. AI: the worker's Valkey
publications are stored when confidence is at least ALERT_MIN_CONFIDENCE
(default 0.6); the worker deliberately does not threshold, so the policy
lives here. A bad message costs one message, not the listener.

e2e-authz.sh gains the scoping/ack cases; new e2e-alerts.sh drives a stand-in
gateway over the real /ws/edge/v1 and the real Valkey channel (checked to
fail when the derivation is broken).
This commit is contained in:
2026-10-29 14:56:24 +00:00
parent 6a2457315e
commit 847f7cb686
17 changed files with 1526 additions and 2 deletions
+2
View File
@@ -2,10 +2,12 @@ export { requireUser, type AuthenticatedUser } from "./plugin";
export { roleSatisfies, ORG_ROLES, type OrgRole } from "./roles";
export {
listMemberships,
requireAlert,
requireFarm,
requireJob,
requireOrg,
requirePrinter,
visibleAlerts,
visibleFarms,
visibleJobs,
visiblePrinters,
+31 -1
View File
@@ -1,7 +1,7 @@
import { and, eq, inArray } from "drizzle-orm";
import { ForbiddenError, NotFoundError } from "@continuum/ts-core";
import { db } from "../db";
import { farms, orgMembers, printJobs, printers } from "../db/schema";
import { farms, orgMembers, printJobs, printerAlerts, printers } from "../db/schema";
import { roleSatisfies, type OrgRole } from "./roles";
/**
@@ -64,6 +64,14 @@ export function visibleJobs(userId: string) {
);
}
/** `where` condition limiting `printer_alerts` to those the user can see. */
export function visibleAlerts(userId: string) {
return inArray(
printerAlerts.farmId,
db.select({ id: farms.id }).from(farms).where(visibleFarms(userId)),
);
}
function check(role: OrgRole | null, minimum: OrgRole, missing: string, action: string): OrgRole {
if (role === null) throw new NotFoundError(missing);
if (!roleSatisfies(role, minimum)) {
@@ -156,3 +164,25 @@ export async function requireJob(
const role = check(row?.role ?? null, minimum, `Print job ${jobId} not found`, action);
return { farmId: row!.farmId, orgId: row!.orgId, role };
}
/** As `requireFarm`, resolved through the alert's farm. */
export async function requireAlert(
userId: string,
alertId: string,
minimum: OrgRole,
action = "this operation",
): Promise<{ farmId: string; orgId: string; role: OrgRole }> {
const [row] = await db
.select({ farmId: printerAlerts.farmId, orgId: farms.orgId, role: orgMembers.role })
.from(printerAlerts)
.innerJoin(farms, eq(farms.id, printerAlerts.farmId))
.leftJoin(
orgMembers,
and(eq(orgMembers.orgId, farms.orgId), eq(orgMembers.userId, userId)),
)
.where(eq(printerAlerts.id, alertId))
.limit(1);
const role = check(row?.role ?? null, minimum, `Alert ${alertId} not found`, action);
return { farmId: row!.farmId, orgId: row!.orgId, role };
}