6.0 KiB
Announcements v2
Announcements is a small Express, React, Socket.IO, MQTT, SQLite, and S3-compatible signage application. Version 2 is being built in phases and is intended to run beside the existing production version until cutover.
Development
The repository is a pnpm workspace with one lockfile. It requires Node.js 22.13 or newer and pnpm 10.29.3.
cp .env.example .env
pnpm install --frozen-lockfile
pnpm check
docker compose up --build
Only the Nginx web service publishes a host port (5175 by default). The API and SeaweedFS S3 endpoint remain private on Compose networks. Replace the example S3 credentials before starting the stack.
Library assets
POST /api/assets accepts multipart uploads in the repeated file or files
field. An optional display_name (or displayName) is accepted for a
single-file upload. Supported decoded formats are still JPEG, PNG, and WebP;
extensions and multipart content types are not trusted. Animated, multipage,
malformed, unsupported, oversized, and excessive-pixel inputs are rejected.
Default limits are:
- 25 MiB per file (
UPLOAD_MAX_FILE_BYTES) - 20 files per request (
UPLOAD_MAX_FILES) - 40 million decoded pixels per image (
IMAGE_MAX_INPUT_PIXELS)
Nginx accepts up to 512 MiB per request, enough for the default aggregate plus
multipart overhead. If the backend limits are raised, update
client_max_body_size in client/nginx.conf as well. Proxied API requests use
a ten-minute inactivity timeout so a valid large batch can finish processing.
Original bytes are archived unchanged. Content identity is the SHA-256 of
those bytes, so uploading the same content returns the existing asset even
when the filename differs. Display-name collisions with different content
return 409 ASSET_NAME_CONFLICT. Multi-file uploads validate every new image
locally before persisting any of them. If a later storage operation fails,
already completed items remain valid and retrying the request is idempotent.
Sharp produces two metadata-free, auto-oriented WebP renditions:
- display: inside-fit at most 1920×1080, quality 82
- thumbnail: inside-fit at most 480×270, quality 72
Neither rendition crops or upscales. Transparency is flattened against black. Presentation effects such as blur, shadows, darkening, and framing are not baked into stored media.
Asset and media API
GET /api/assetsPOST /api/assetsGET /api/assets/:assetIdPATCH /api/assets/:assetIdGET /api/assets/:assetId/referencesDELETE /api/assets/:assetIdGET /api/assets/:assetId/originalGET /media/assets/:assetId/display/:sha256.webpGET /media/assets/:assetId/thumbnail/:sha256.webp
PATCH accepts only display name and expiration. A null expiration is permanent. Manual deletion refuses playlist, current playback, and static references. It marks an asset as deleting before idempotently removing its three objects and removes SQLite metadata only after storage succeeds. A failed processing row can be inspected, safely deleted, and uploaded again; no automatic retry loop is run.
Media URLs are derived from SQLite metadata and never accept object keys. The display and thumbnail routes stream private S3 objects through the API with a one-year immutable cache policy. Original downloads use private, no-store caching.
Playlists
Playlist and entry metadata is authoritative in SQLite. The API surface is:
GET /api/playlistsPOST /api/playlistsGET /api/playlists/:playlistIdPATCH /api/playlists/:playlistIdDELETE /api/playlists/:playlistIdPOST /api/playlists/:playlistId/entriesPATCH /api/playlists/:playlistId/entries/:entryIdDELETE /api/playlists/:playlistId/entries/:entryIdPUT /api/playlists/:playlistId/entries/orderPOST /api/playlists/:playlistId/activate
Entry positions are zero-based and contiguous. Reordering requires the complete
ordered list as { "entryIds": [...] }. An asset can occur only once in a
playlist, but the same asset can be used independently by any number of other
playlists. Only ready, unexpired assets can be newly added.
Entry duration overrides may be null (inherit the global setting) or 1,000 to
3,600,000 milliseconds. Presentation overrides are nullable; darkening is an
inherit/off/on boolean and blur always comes from the global setting. Schedule
instants use epoch milliseconds or ISO strings with an explicit Z or numeric
UTC offset, and intervals are evaluated as [start, end).
Playlist detail responses include effective duration and presentation, server-evaluated eligibility, immutable display/thumbnail URLs, and the settings revision and evaluation time used. Playlist revisions increment once per semantic content change. Exact no-ops and reads do not increment them.
Activation is explicit. A request is held in the existing pending-activation state while a playable normal slide is mid-duration; otherwise the target playlist is prepared immediately at its first currently playable entry. The output-off flag remains only a visual gate, and static hold is preserved while the hidden normal playlist changes. Phase 3A does not include the advancing playback timer that will consume pending activation at a slide boundary.
Socket.IO publishes playlists:changed as a refetch invalidation after
committed playlist changes and after settings or referenced-asset edits that
affect resolved playlist data. Socket.IO is not a mutation API.
Storage and health
The API uses a small path-style S3 adapter configured by S3_ENDPOINT,
S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY.
Connection timeout, socket-inactivity timeout (S3_REQUEST_TIMEOUT_MS),
attempts, and background probe interval are configurable in .env.example.
Startup verifies or safely creates only the configured bucket.
Storage outages do not stop the API or fail process readiness. /health/ready
reports storage as degraded while remaining HTTP 200 when SQLite and the API
runtime are healthy. /api/health and /api/status expose cached, redacted
storage state and recover after a successful probe or storage operation.