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:
- A pull request a user would notice carries a changeset (
pnpm changeset; see.changeset/README.md). - Every push to
mainpublishes the image under its commit sha and moveslatestonto it once the tests pass. If changesets are pending, it also opens (or rebuilds) a Version packages pull request that bumps the version and writesapps/api/CHANGELOG.md. - Merging that pull request is the release: its push finds a version with no
v*tag, publishes the image asX.Y.ZandX.Y, then tagsvX.Y.Zand 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.
| Port | What |
|---|---|
| 14100 | API (apps/api, server.port); the Docker image |
| 14102 | Web dev server (apps/web, proxies /api) |
| 14103 | Web vite preview |
| 14104 | Site dev server (apps/site, start) |
| 14105 | Site build, served (apps/site, serve) |
| 14132 | Postgres for development (curator-db) |
| 14133 | Postgres for the API tests (curator-db-test, tmpfs) |
| 14180 | Fake TV (pnpm fake:tv in packages/frame) |
The real TV listens on 8002 (and 8001 for REST) on its own address.