Files
app/development.md
T
stroblmeandClaude Opus 5 08ccbbac5b Give the work queue its own Redis root, and pin what the suite runs as
The queue shared the `pipeline:` prefix with flow state, so `RedisState.clear()`
could DEL the queue stream and `keys()` enumerated queue entries — only callers
filtering `__`-prefixed names kept it safe. It moves to `queue:` without a
migration: whatever is in flight at the upgrade is dropped once, documented in
DEPLOY.md rather than papered over.

Alongside it: `pool_pre_ping`, so a connection idle across a Postgres restart
costs a round trip instead of a failed request; the test suite pins
ENVIRONMENT=local and DOMAIN=localhost itself rather than inheriting a
deployment's .env; and `depth` leaves the queue stats, where it reported the
capped journal length as if it were a backlog.

ALERTS_FILE and PANELS_FILE now point at /data. They defaulted to a path on no
volume, so alert routing and every wall-panel pairing were living in the
container's writable layer and vanishing on each rebuild. Carrying the existing
files across is a manual step; DEPLOY.md has it.

development.md was still the upstream template — compose.override.yml,
localhost.tiangolo.com, `docker compose watch` as the dev flow — and said
nothing about the Playwright suite. Rewritten against what the Makefiles
actually do. deployment.md was template text too, duplicating the root
DEPLOY.md, and is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
2026-08-21 10:10:57 +02:00

9.2 KiB

Development

How to run, test and lint the app stack. The workspace root owns the rest: README.md for the quick start, DEPLOY.md for production, DESIGN-GUIDELINES.md before any UI work. ROADMAP.md is what is planned, NOTEPAD.md what is deferred.

Running it

Integrated — the normal way. From the workspace root, once: make init (submodules, secrets, the per-stack .env files, the shared external proxy docker network). Then make dev brings both stacks up detached behind a single Traefik — which is what lets the SPA, the API and the marketing site answer on one port. make status lists the containers, make down stops everything.

The hostname comes from DOMAIN in app/.env, which scripts/setup.sh copies out of the root .env — change it there and re-run make init. make dev DOMAIN=… at the root does not reach this stack: app/Makefile assigns DOMAIN from app/.env, and a makefile assignment overrides an inherited environment variable.

App only. cd app && make dev starts this stack on its own Traefik with all host ports published, in the foreground. make dev-utils starts just db, adminer, proxy, mailcatcher and prestart — the useful half when the backend runs on the host.

No Docker. make install once, then make dev-backend (FastAPI on :8000) and make dev-frontend (Vite on :5173) in two terminals. A local stack's CORS list already contains http://localhost:5173, so a host Vite server can talk to a containerised API.

Nothing invokes docker compose bare, and neither should you: both stacks keep their compose files in a docker/ directory, so without -p fluksio-app they collide in a project named after that directory, and --env-file is required because compose interpolates .env before it reads compose.yml. The COMPOSE variable in Makefile has both. The files layer as compose.yml (production) → compose.dev.yml (local Traefik, published ports, hot reload, test helpers) → compose.local.yml (integrated). CI layers compose.ci.yml instead of the last one; compose.traefik.yml is the production edge proxy, deployed on its own.

URLs

Integrated stack (root make dev). Traefik's port 80 is the only published port: compose.local.yml drops the rest, since behind the proxy they add nothing and one of them already being taken would stop the whole stack from starting.

URL Service
http://app.localhost dashboard SPA
http://api.localhost backend API
http://api.localhost/docs OpenAPI / Swagger UI
http://localhost marketing site (the index stack)

Adminer and mailcatcher have no route here, so use the containers directly: docker exec -it fluksio-db psql -U postgres -d app for the database, docker logs -f fluksio-app-mailcatcher-1 for captured mail.

Standalone (cd app && make dev) publishes the full set:

URL Service
http://app.localhost, http://api.localhost this stack's own Traefik on :80
http://localhost:5173 SPA (the nginx image, not the Vite dev server)
http://localhost:8000 backend API
http://localhost:8090 Traefik dashboard
http://127.0.0.1:8080 adminer
http://localhost:1080 mailcatcher web UI (SMTP on 1025)
localhost:5432 Postgres

Hot reload, and what it misses

compose.dev.yml bind-mounts backend/app into the api container and runs uvicorn with --reload, so a backend edit is live. The develop.watch block next to it syncs the same directory, but only under docker compose watch, which no target runs — the detached up -d flow never triggers it. The mount is what does the work, so an api container created before the mount existed keeps serving the source baked into its image and has to be recreated once.

The frontend is built into an nginx image, so a UI change needs an explicit rebuild, from app/:

docker compose -p fluksio-app --env-file "$PWD/.env" \
  -f docker/compose.yml -f docker/compose.dev.yml -f docker/compose.local.yml \
  up --build -d frontend

Services the dev stack adds

compose.dev.yml brings up a broker and a time-series database of the stack's own, so the mqtt and influx node types are testable without external hardware. Neither publishes a host port, on purpose — nothing outside the stack needs them.

  • mosquitto — anonymous MQTT. Point a node at broker_host: mosquitto, port 1883.
  • influxdb 2.7http://influxdb:8086, org and bucket fluksio, token fluksio-dev-token. No volume, so down -v starts it over.
  • mailcatcher — SMTP on 1025, web UI on 1080; the backend is pointed at it, so no development mail leaves the machine.

adminer and redis come from compose.yml and exist in production too; dev only differs in that adminer gets a host port. Without REDIS_HOST the flow engine keeps state in memory instead of Redis.

Configuration and secrets

The root .env is the source of truth. scripts/setup.sh creates app/.env from app/.env.example and keeps the shared keys — domain, secret key, database password, superuser — in step with it. Only the .env.example files are tracked; make secrets reprints the generated values. A release that adds a key needs scripts/setup.sh --secrets before make update, so the key exists before the stack is rebuilt.

The backend settings load ../.env relative to backend/ through pydantic-settings, which is why an exported environment variable always outranks the file — how CI points the suite at a Docker-assigned Postgres port.

Tests

make test is test-backend plus test-frontend.

Backend. cd backend && uv run bash scripts/tests-start.sh waits for Postgres, then runs pytest under coverage; the HTML report lands in backend/htmlcov. Postgres is the only service needed — mail is patched out in tests/, and the flow engine falls back to MemoryState while REDIS_HOST is empty. tests/__init__.py pins the suite's own environment before anything imports the settings: database app_test (created and dropped per session, so a run never touches development data), plus ENVIRONMENT=local and DOMAIN=localhost, so a checkout configured for a deployment cannot drag that deployment's configuration into the run. make test-backend resolves POSTGRES_SERVER from the running fluksio-db container's address, because the integrated stack publishes no 5432.

Frontend (e2e). make test-frontend runs the suite inside the pinned mcr.microsoft.com/playwright:v<version>-noble image (version read from frontend/package.json) on the proxy network, against a stack that must already be up. It maps app.$(DOMAIN) and api.$(DOMAIN) onto the Traefik container's address twice, with --add-host and with HOST_RESOLVER_RULES, because Chromium pins *.localhost to loopback whatever /etc/hosts says. Narrow a run with make test-frontend PLAYWRIGHT_ARGS="--grep flows".

The specs are frontend/tests/*.spec.ts; playwright.config.ts defines setupchromium and mobile. auth.setup.ts logs in once into playwright/.auth/user.json, which both browser projects reuse; mobile runs mobile.spec.ts only, on a Pixel 5 at 393px.

Both origins come from PLAYWRIGHT_BASE_URL and PLAYWRIGHT_API_URL, deliberately never from VITE_API_URL — that one belongs to the app build and in a deployment checkout it names the deployment, which would mean driving a browser at the local stack while sending teardown DELETEs to the live instance. tests/guard.ts runs first and refuses the whole run when either origin resolves outside loopback or the private ranges, because the suite creates and deletes flows, dashboards and users. PLAYWRIGHT_ALLOW_PUBLIC=1 overrides it.

Visual check. From the root, make verify logs in and screenshots both themes into app/frontend/screenshots/{light,dark}/; make verify-docker does the same from the Playwright container when the host lacks the browser libraries. Both default to localhost rather than reading .env, so on a checkout configured for a deployment name the domain the stack actually serves: make verify DOMAIN=example.com.

Lint, hooks and the generated client

make lint runs ruff, mypy and biome. make hooks at the root installs the pre-commit hooks in both stacks; .pre-commit-config.yaml holds them. One is worth knowing about: generate-frontend-sdk regenerates the SPA's API client from the backend's OpenAPI schema whenever anything under backend/ changes, so a route change reaches the frontend without being asked. make generate-client forces it.

Run the root's make design-check before pushing anything that touches the design tokens, lib/motion.ts, components.json or the font asset — the design system is a duplication contract between the two frontends, and that target is the only thing checking it.

CI

.gitea/workflows/ runs four jobs, each writing .env from .env.example first: test-backend.yml (pytest against a compose Postgres), playwright.yml (the e2e suite in two shards, one compose project each, the browser loading the SPA from the nginx frontend service), pre-commit.yml (the hooks above, plus strict mypy, which they do not cover) and test-compose.yml (a smoke test that the production images actually come up).