0.1.3 ships the external package integration validated in #13 (Pass 2):
the canonical linux-x64-glibc Module Package installed and exercised
end to end by CI, with bounded external runtime image processing.
Only the version moves: the manifest, package.json, the server runtime,
both browser artifacts, the settings payload's moduleVersion, the test
fixtures, and the README status line. compatibility stays
platform ^0.2.0 and moduleSdk ^0.2.0; Core 0.2.4 already satisfies it,
so the range is not tightened. pnpm-lock.yaml carries no root version
and is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signage moves from the legacy module contract to Ekklesia 0.2.0's, with no behaviour or schema
change:
- manifest version 8, `database: { abi: 1 }`, compatibility ^0.2.0 for platform and module SDK;
the runtime declares the same ABI, as Core requires;
- every store reaches PostgreSQL through the platform's Module Database ABI v1 executor, with
Signage's own Drizzle tables and the module-side adapter (`moduleDrizzle`) from
@ekklesia/module-database. createSignageStore takes the executor, so no call site changes;
- the schema entrypoint exports only plain ABI v1 metadata (table names and foreign-key targets),
derived from tables.ts with moduleDrizzleSchemaMetadata; no Drizzle object reaches Core;
- duplicate-image detection reads SQLSTATE 23505 from the stable Ekklesia database error, found
with findEkklesiaDatabaseError through Drizzle's DrizzleQueryError wrapping. The old
`error.code === "23505"` check never matched a wrapped error, so a duplicate-upload race
surfaced as a 500 instead of the content-hash conflict.
tables.ts declarations, the migration, and its pinned checksum are unchanged.
Tests: an ABI v1 executor double that enforces Core's scope, parameter, and abort rules drives all
13 store transactions, their rollbacks, raw int8/timestamptz/jsonb decoding, and the 23505
regression, including generic and driver-shaped errors that must not count as duplicates.
CI, temporarily: the platform pin moves to v0.2.0 (1788d17) with a regenerated ci/pnpm-lock.yaml.
The integration job is disabled: from 0.2.0 the platform image is generic (ADR 0048) and installs
modules only as Module Packages, which Signage cannot be until its sharp dependency goes through
the native package flow. The job and ci/integration.sh stay, and the job returns in that change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
0.1.2 fixes what the first production Signage deployment found:
- #4: the display and server share one message schema; every heartbeat
from an image slide used to be refused with a 400;
- #5: every display request retries with backoff and never gives up, so
a display that failed one request no longer stays offline until
reloaded;
- #6: one object URL per cached slide, released when done; the leak
exhausted the browser within minutes;
- CI (#7, #8): every change runs inside a pinned Ekklesia, end to end
in a real browser, and cleans up even when killed.
compatibility.platform stays ^0.1.0: every fix is module-side, and CI
tested it against platform v0.1.1 plus test-only scripts. The README no
longer says Signage is unreleased or undeployed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The display controller and the viewer routes each described the
heartbeat their own way and disagreed about one thing: the display sent
null for "nothing to report", and the route accepted only a missing
field. Every heartbeat from a screen showing a native image was refused
with a 400. The roster therefore marked working screens Offline 90 s
after they opened, and never recorded offlineReady or cachedAssetCount.
src/shared/display-protocol.ts now defines every message between a
display and the server:
- viewer session request and response;
- heartbeat request and response;
- channel state;
- the realtime display payload.
Both halves import it. Its rules: a nullable field accepts null
explicitly, an absent field means "not reported", and an unknown field
is ignored, so a newer display is never refused by an older server.
- The routes parse with the shared parsers, and a refusal names the
field with Signage's sanitized code.
- The server's channel state extends the wire type, so a field the
display relies on cannot drift without a compile error.
- The display builds its heartbeat as the shared type, and reads its
registration with the shared parser instead of casting it.
- Test stubs that described server responses more loosely than the
server sends them (no viewerName, no stale, partial media) now match
the real responses and are typed as the protocol.
Co-Authored-By: Claude Opus 5.5 <[email protected]>