octoturge 24142d4603 Add the operator console's live telemetry socket
The only socket until now was the gateway-facing /ws/edge/v1, which
authenticates as a *farm* with a credential any of its gateways holds,
and which accepts telemetry. Pointing a browser at it would mean handing
the browser something that can drive printers. /ws/telemetry
authenticates as a *user* with a Supabase access token, and accepts
nothing at all: it is push-only, and a frame arriving from the browser is
ignored. Everything that changes a printer keeps going over HTTP, where
the role checks and the audit trail already are.

Auth is checked at the upgrade, as on the edge socket, so an
unauthenticated browser never gets a socket. The token rides in
`Sec-WebSocket-Protocol` - `new WebSocket(url, ["bearer", token])` -
because that field is the whole of what JavaScript controls about a
browser handshake, and the usual workaround of `?token=` would put a
bearer credential into every access and proxy log. "bearer" is first
because the runtime echoes the *first* offered value back in the
response header, so the token must not be in that position; asserted,
not assumed.

The payload is the printer resource itself, returned straight out of
`persistTelemetry`'s UPDATE through `printerProjection` - the same
projection the REST routes use. So a push is byte-for-byte what
`GET /printers/:id` would have answered, and a client applies it to the
row it already holds. Rebuilding it from the gateway's snapshot instead
would be a second translation of the same data, free to drift, and
missing the columns a gateway never reports at all like `name` and
`vendor`.

`maySee` is what keeps one organisation's telemetry out of another's
console, and it lives in `telemetry/broadcast.ts` with no imports but a
type so it can be tested without a socket or a database - the same reason
as `dispatch/job_status.ts`, `stream.ts` and `auth/edge_upgrade.ts`. It
refuses a printer with no farm rather than matching it against anything,
which no row can be today, because this is the one function where being
wrong leaks another org's machines.

**A latent bug this turned up:** the edge route's resolve was
`{ as: "scoped" }`, copied from `requireUser`. For `requireUser` that is
right - it is used inside each route file and must propagate `user` to
that file's routes. On a ws plugin it propagates the check to the parent
app, where it gates every route mounted *after* that plugin. Measured
with a two-plugin probe: the second plugin's own socket is refused
because the first one's check runs first and finds no edge token. Nothing
shipped broken only because /ws/telemetry is the first route mounted
after it - and it was refused outright until this was found. Both ws
resolves now use the default `local` scope.

Known limits, both written down rather than discovered later:

- **In-process fan-out.** A `Set` of callbacks in one process, so two
  backend instances would each push only the telemetry their own
  gateways are connected to and an operator would see half a fleet. The
  fix is Valkey pub/sub, already a dependency; doing it now would mean
  testing a message broker to deliver events between two objects in the
  same process.
- **Visibility is resolved once, when the socket opens.** Re-deriving it
  per message would put a database round trip on every push to every
  viewer. The direction that matters: someone removed from an
  organisation keeps receiving its telemetry until their socket drops.
  Closing that needs a signal on membership change that this backend does
  not have; a periodic re-resolve would narrow the window without closing
  it. Telemetry is also the least sensitive thing here, and every HTTP
  route re-checks on each request.

Verified against the real stack, not just mirrored in unit tests:
`scripts/e2e-edge.sh` now drives the actual route with a real
JWKS-verified token and asserts the handshake echoes "bearer", that a
`ready` frame arrives, and that a real push carries the printer resource
with live readings - "2 push(es); last was H2C state=FINISH nozzle=30
camera=h2c". A bogus token is refused before any socket exists.
`scripts/watch-telemetry.ts` is the client it uses, and is the quickest
way to see by hand whether a farm's telemetry reaches a console.

110 tests (was 95), `bunx tsc --noEmit` clean.
2026-10-18 19:45:35 +00:00
2026-10-03 10:10:28 +00:00

continuum-backend

Cloud control plane for Continuum. Bun + ElysiaJS, PostgreSQL (via pgCat/PgBouncer, with pgvector for frame embeddings), Valkey Streams, and Cloudflare R2 storage.

Run

bun install
scripts/dev-env.sh up        # Valkey + Postgres/pgvector, and print the URLs
scripts/dev-env.sh schema    # apply drizzle/ - same as bun run db:migrate
cp .env.example .env         # fill in the URLs it printed, plus R2_*, SUPABASE_JWT_SECRET
bun run dev

scripts/dev-env.sh uses docker-compose.yml when a Docker daemon is answering, and falls back to the same two services from apt when there is no usable container runtime - some sandboxes cannot run Docker at all, because dockerd needs CAP_NET_ADMIN for its bridge and user namespaces to unpack an image, and neither can be granted from inside. scripts/dev-env.sh status says which backend is in use.

Schema changes go through migrations: edit src/db/schema/, run bun run db:generate, and commit the file it writes to drizzle/ along with the schema change. scripts/dev-env.sh push reshapes a local database straight from the schema with no migration file, which is faster while a table is still moving but records nothing and will drop a column to make things match - so generate before committing. drizzle/0000_*.sql creates the vector extension itself; that line is hand-added, since drizzle-kit does not emit extension statements and the migration would otherwise only apply to a database somebody had already prepared.

Test

bun test                 # unit + protocol contract tests, no services needed
scripts/e2e-edge.sh      # the whole edge path: real database, real printers

scripts/e2e-edge.sh seeds a farm, claims its printers, mints an edge token, runs the real continuum-proxy against this backend, and asserts that live telemetry reached Postgres and that a command reached a printer. It only ever sends a fan speed, and only to a printer reporting IDLE. scripts/capture-gateway-frames.js is the smaller tool: it stands in for this backend and records what a gateway sends, for refreshing the fixtures in tests/fixtures/.

Layout

  • src/db/ - Drizzle schema (printers, farms, print_jobs, frame_embeddings) and client, pointed at pgCat. src/db/validators.ts derives Elysia/TypeBox request validators straight from the Drizzle tables via drizzle-typebox, so the Postgres schema and the API validators come from one definition instead of two hand-synced ones.
  • src/s3/ - R2 presigned URL helpers (PUT for client uploads, GET for the edge proxy).
  • src/queue/ - Valkey client (ioredis) + XADD helper pushing onto stream:camera_frames for continuum-ai-worker.

Valkey, not Redis: Redis 7.4+ is RSALv2/SSPL and 8.0+ adds AGPLv3, none of which a commercial product can ship. Valkey is BSD-3-Clause and speaks the same RESP protocol, so ioredis is unchanged - only the server differs. See continuum-common/docs/LICENSES.md. docker-compose.yml here brings up Valkey and Postgres+pgvector for local development.

  • src/routes/ - /auth, /farms, /printers, /jobs, and the edge-gateway WebSocket at /ws/edge/v1. That route's wire format is not defined here: it is schemas/continuum/v1/edge/ in continuum-schemas, validated through its TypeBox mirror in @continuum/ts-types, because both ends of it are separate programs in different languages and they had already drifted apart once. printers.gateway_printer_id is what maps a gateway's own printer ids ("p1p") onto rows here; telemetry for an unclaimed id is logged and skipped, never inserted.

frame_embeddings.embedding requires the vector extension. The first migration creates it, and scripts/dev-env.sh up also does so for the push path, which does not run migrations.

Dependency pins

elysia, @sinclair/typebox and the drizzle-* packages are pinned tightly on purpose, not out of caution. Caret ranges here resolved into a combination that could not boot: elysia: ^1.1.26 installed 1.4.30, which calls t.Module and so needs TypeBox >=0.34, while drizzle-typebox: ^0.2.1 installed a version whose peers demand drizzle-orm >=0.36 against the ^0.33.0 pinned here. Typechecking passed throughout - only starting the server failed. tests/dependency_identity.test.ts guards the part of this that can silently come back.

S
Description
Continuum platform: continuum-backend
Readme
624 KiB
Languages
TypeScript 63.8%
Shell 35.4%
JavaScript 0.8%