Files
Signage/ekklesia.module.json
T
ekklesiaandClaude Opus 5.5 a6d553efbb
CI / lint (pull_request) Successful in 14s
CI / typecheck (pull_request) Successful in 16s
CI / test (pull_request) Successful in 21s
CI / build (pull_request) Successful in 20s
CI / integration (pull_request) Skipped
feat: move Signage to the Ekklesia 0.2 module contract (manifest v8, database ABI v1)
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]>
2026-10-08 20:36:11 -05:00

162 lines
4.5 KiB
JSON

{
"manifestVersion": 8,
"id": "signage",
"name": "Signage",
"description": "Visual programming, timelines, synchronized channels, and display delivery for church screens.",
"version": "0.1.2",
"compatibility": {
"platform": "^0.2.0",
"moduleSdk": "^0.2.0"
},
"database": {
"abi": 1
},
"capabilities": [
{
"id": "signage.content-providers",
"version": "1.0.0"
},
{
"id": "signage.slide-renderer",
"version": "1.0.0"
},
{
"id": "signage.playback",
"version": "1.0.0"
},
{
"id": "signage.displays",
"version": "1.0.0"
}
],
"permissions": [
{
"id": "signage.view",
"baseline": "restricted",
"description": "View Signage timelines, images, channels, provider health, and display health."
},
{
"id": "signage.assets.manage",
"baseline": "restricted",
"description": "Upload, rename, expire, and delete Signage's own native images."
},
{
"id": "signage.timelines.manage",
"baseline": "restricted",
"description": "Create, edit, reorder, duplicate, schedule, and delete saved timelines."
},
{
"id": "signage.playback.control",
"baseline": "restricted",
"description": "Activate timelines and operate live playback transport for a channel group."
},
{
"id": "signage.displays.manage",
"baseline": "restricted",
"description": "Create channel groups and audience channels, and assign or rename viewers."
},
{
"id": "signage.display.view",
"baseline": "restricted",
"description": "Render the Signage full-viewport display and report its own health."
}
],
"configuration": [
{
"key": "timezone",
"type": "string",
"default": "America/Chicago",
"minLength": 1,
"maxLength": 64,
"description": "IANA time zone this installation's Signage schedules are written and displayed in."
},
{
"key": "nativeImageDurationSeconds",
"type": "integer",
"default": 7,
"minimum": 1,
"maximum": 3600,
"description": "How long a native Signage image is shown when no slot override applies."
},
{
"key": "crossfadeMilliseconds",
"type": "integer",
"default": 1000,
"minimum": 0,
"maximum": 10000,
"description": "How long a display fades an incoming slide over the outgoing one."
},
{
"key": "defaultBackgroundMode",
"type": "enum",
"values": ["blurred", "solid", "black"],
"default": "blurred",
"description": "How a native image that is not 16:9 is backed when a slot says nothing."
},
{
"key": "defaultBackgroundColor",
"type": "string",
"default": "#000000",
"minLength": 7,
"maxLength": 7,
"description": "The solid background colour used when the background mode is solid."
},
{
"key": "defaultBackgroundDarkened",
"type": "boolean",
"default": false,
"description": "Whether a native image's background is darkened behind the foreground by default."
},
{
"key": "defaultBlurPixels",
"type": "integer",
"default": 20,
"minimum": 0,
"maximum": 120,
"description": "Blur radius applied to a blurred native-image background."
},
{
"key": "providerTimeoutMilliseconds",
"type": "integer",
"default": 2000,
"minimum": 100,
"maximum": 15000,
"description": "How long Signage waits for one content-provider call before treating it as failed."
},
{
"key": "playbackHistoryLimit",
"type": "integer",
"default": 50,
"minimum": 2,
"maximum": 500,
"description": "How many resolved selections per channel group are retained as playback history."
},
{
"key": "viewerHeartbeatWriteSeconds",
"type": "integer",
"default": 30,
"minimum": 5,
"maximum": 600,
"description": "Shortest interval between two persisted heartbeat writes for one steady viewer."
}
],
"storage": {
"buckets": [
{
"id": "native-images",
"description": "Archived original uploads and generated renditions for Signage's native images."
}
]
},
"entrypoints": {
"server": "./dist/server/index.js",
"schema": "./dist/schema/index.js",
"migrations": "./drizzle",
"ui": "./dist/ui/index.js",
"display": {
"artifact": "./dist/display/index.js",
"permission": "signage.display.view"
}
}
}