Skip to main content

Developing curator

Samsung Frame TV artwork management. The site, with a quick start and a tour of the web app, is at curator.maroonedsoftware.com.

Layout​

pnpm + turbo workspace.

apps/
api/ @curator/api: HTTP API over the Frame client, on ServerKit Fastify (see its README)
web/ @curator/web: browser UI over the API, Vite + React + Mantine (see its README)
site/ @curator/site: the public website, curator.maroonedsoftware.com, on Docusaurus
packages/
frame/ @curator/frame: client for the Frame's local art-mode protocol (see its README)
config-eslint/ @repo/config-eslint: shared ESLint flat config
config-typescript/ @repo/config-typescript: shared tsconfig base
unraid/curator.xml the Unraid template for the image (see Running in Docker)

Commands​

pnpm install
pnpm db:up # Postgres for development and tests (docker compose)
pnpm migrate:up # apply apps/api/data/migrations
pnpm build # tsup + declarations for every package
pnpm typecheck
pnpm lint
pnpm test
pnpm format # prettier --write .
pnpm --filter @curator/api dev # the API on :14100, restarting on change
pnpm --filter @curator/web dev # the web app on :14102, proxying /api to the API
pnpm --filter @curator/site start # the website on :14104, which `pnpm dev` does not start

Node 26+ (.nvmrc), pnpm 11, Docker for Postgres. pnpm db:reset empties the development database and re-migrates it.

Running in Docker​

One image, maroonedsoftware/curator, holds the API, the web app it serves, and the migrations it applies on every start. Bring a Postgres 17:

services:
curator:
image: maroonedsoftware/curator:latest
restart: unless-stopped
ports:
- '14100:14100'
environment:
DATABASE_HOST: db
DATABASE_USER: postgres
DATABASE_PASSWORD: change-me
DATABASE_NAME: curator
volumes:
- curator-data:/data
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: change-me
POSTGRES_DB: curator
volumes:
- curator-db:/var/lib/postgresql/data
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-d', 'curator']
interval: 5s
retries: 10
volumes:
curator-data:
curator-db:

The web app is then on port 14100, with the API under /api. DATABASE_PORT (default 5432) and PORT (default 14100) are also read. Uploaded originals and the log live on the /data volume. The config is apps/api/appconfig.production.yaml. The migrations wait up to a minute for Postgres to accept connections, so the container can start before the database. If DATABASE_NAME does not exist yet, the first start creates it, which needs a user allowed to create databases.

The server runs as PUID:PGID (default 1000:1000), and the container hands /data to that user when it starts. Bind-mounting a host directory owned by someone else, set them to its owner: on Unraid, whose appdata belongs to nobody:users, that is PUID=99 and PGID=100.

Every connection to a TV is outbound, so bridge networking works, but SSDP discovery (the TVs page's Search) needs multicast on the TVs' network: run with network_mode: host (and no ports) to use it, or add each TV by address.

Tags: latest follows main, X.Y.Z is a release and X.Y the newest release in that line, and every published commit is also its full sha. To build it locally: docker build -t curator ..

On Unraid​

unraid/curator.xml is a template for the image, on host networking so the TVs page can search. Until it is in Community Applications, add it from the Docker tab: Template Repositories, add https://github.com/MaroonedSoftware/curator, save, then Add Container and pick curator. (Or copy the file into /boot/config/plugins/dockerMan/templates-user/ on the server.) Fill in your Postgres server and start it; the first start creates the curator database if it is missing. The template runs the app as nobody:users (PUID=99, PGID=100), Unraid's owner for appdata.

Releasing​

Releases come from changesets, and all of it runs in .github/workflows/release.yml:

  1. A pull request a user would notice carries a changeset (pnpm changeset; see .changeset/README.md).
  2. Every push to main publishes the image under its commit sha and moves latest onto it once the tests pass. If changesets are pending, it also opens (or rebuilds) a Version packages pull request that bumps the version and writes apps/api/CHANGELOG.md.
  3. Merging that pull request is the release: its push finds a version with no v* tag, publishes the image as X.Y.Z and X.Y, then tags vX.Y.Z and creates the GitHub release from the changelog.

A failed release cuts no tag, so the next push to main retries it. Publishing needs the DOCKERHUB_USERNAME and DOCKERHUB_TOKEN Actions secrets, and the version pull request needs Settings, Actions, General, "Allow GitHub Actions to create and approve pull requests".

Ports​

Curator owns 14100-14199 locally; no other project under ~/projects uses that block.

PortWhat
14100API (apps/api, server.port); the Docker image
14102Web dev server (apps/web, proxies /api)
14103Web vite preview
14104Site dev server (apps/site, start)
14105Site build, served (apps/site, serve)
14132Postgres for development (curator-db)
14133Postgres for the API tests (curator-db-test, tmpfs)
14180Fake TV (pnpm fake:tv in packages/frame)

The real TV listens on 8002 (and 8001 for REST) on its own address.