Gateway: status route with per-route results, test push, reconnect and unpair
Gateway / check (push) Successful in 9s
Gateway / image (push) Successful in 24s

This commit is contained in:
gib committed 2026-11-01 15:04:30 -05:00
1 parent e6fd2b8f51
commit 9707daf1fe
9 files changed
+398 -55

No files matched your search

+54 -2
View File
@@ -11,19 +11,25 @@ import type {
ProjectsResponse,
PushRegistration,
RespondRequest,
PushResult,
SendMessageRequest,
StatusResponse,
} from "./contract.ts";
import type { Db, DeviceRow } from "./db.ts";
import type { Hub } from "./hub.ts";
import type { Pusher } from "./push.ts";
import { HostError, type T3Host } from "./t3/host.ts";
import { RpcError } from "./t3/rpc.ts";
import type { ApprovalDecision, ModelSelection, ThreadShell } from "./t3/types.ts";
import { buildFeed, buildSummary, buildThreadDetail, hostSummary, type SummaryCache } from "./views.ts";
import { buildFeed, buildSummary, buildThreadDetail, hostStatusDetail, hostSummary, type SummaryCache } from "./views.ts";
export interface ApiDeps {
db: Db;
hub: Hub;
cache: SummaryCache;
pusher: Pusher;
/** When the gateway started, for the status screen. */
startedAt: string;
log?: (message: string) => void;
}
@@ -50,7 +56,7 @@ class HttpError extends Error {
const json = (body: unknown, status = 200) => Response.json(body, { status });
const accepted = () => new Response(null, { status: 202 });
export function createRoutes({ db, hub, cache, log = () => {} }: ApiDeps) {
export function createRoutes({ db, hub, cache, pusher, startedAt, log = () => {} }: ApiDeps) {
let pairFailures: number[] = [];
/** Wraps a handler with device auth and error mapping. */
@@ -131,6 +137,52 @@ export function createRoutes({ db, hub, cache, log = () => {} }: ApiDeps) {
}),
},
"/v1/device": {
/** Unpairs the watch asking. */
DELETE: authed<"/v1/device">((_req, device) => {
db.devices.revoke(device.id);
log(`unpaired device ${device.name} (${device.id})`);
return new Response(null, { status: 204 });
}),
},
"/v1/device/test-push": {
POST: authed<"/v1/device/test-push">(async (_req, device) =>
json(
(await pusher.sendTo(device, {
title: "WristCode",
body: "Test notification. Pushes reach this watch.",
category: "test",
data: {},
collapseId: "test",
})) satisfies PushResult,
),
),
},
"/v1/status": {
GET: authed((_req, device) => {
const expiries = new Map(hub.rows().map((row) => [row.id, row.token_expires_at]));
return json({
hosts: hub.list().map((host) => hostStatusDetail(host, expiries.get(host.id) ?? null)),
gateway: { startedAt, pushConfigured: pusher.configured, lastPush: pusher.last },
device: {
name: device.name,
pairedAt: device.created_at,
pushRegistered: device.apns_token !== null,
apnsEnvironment: device.apns_env,
},
} satisfies StatusResponse);
}),
},
"/v1/hosts/:hostId/reconnect": {
POST: authed<"/v1/hosts/:hostId/reconnect">((req) => {
findHost(req.params.hostId).reconnect();
return accepted();
}),
},
"/v1/feed": {
GET: authed(async () => json(await buildFeed(hub.list(), cache))),
},