Files
directus/.planning/phase-1-slice-1-schema
julian e22d9d489a Tasks 1.6 + 1.7 — schema tooling + real entrypoint flow
Two parallel tasks landing together. The boot pipeline is now wired
end-to-end: db-init → schema apply → directus bootstrap → pm2-runtime.
Live-verified by booting a fresh compose stack to a serving Directus
admin UI on :8055.

Task 1.6 — snapshot tooling:
- scripts/schema-snapshot.sh — host-side, dev-time. Verifies docker
  is on PATH and the directus compose service is running, runs
  `node /directus/cli.js schema snapshot --yes` inside the container,
  copies the YAML out to ./snapshots/schema.yaml. Used after admin-UI
  schema changes to capture the new state for git commit.
- scripts/schema-apply.sh — image-side, boot-time. Reads
  /directus/snapshots/schema.yaml, runs a dry-run preview, then
  applies. Gracefully skips when the snapshot is absent or whitespace-
  only (Phase 1 first-boot path before tasks 1.4/1.5 produce
  collections). SNAPSHOT_PATH env var override for CI flexibility.
- snapshots/README.md — lifecycle doc; warns against hand-editing.

Task 1.7 — real entrypoint flow:
- entrypoint.sh rewritten from Phase 1.1's placeholder to the
  4-step boot per ROADMAP design rule #3:
    1/4 db-init          → /directus/scripts/apply-db-init.sh
    2/4 schema apply     → /directus/scripts/schema-apply.sh
    3/4 directus bootstrap → node /directus/cli.js bootstrap
    4/4 directus start   → exec pm2-runtime start ecosystem.config.cjs
  set -euo pipefail halts boot on any step's non-zero exit. Each step
  emits a [entrypoint] log marker so an operator reading container
  logs sees which step failed.

Bug found and fixed during live verification:
- Both 1.6 scripts initially called bare `directus schema ...` as if
  the CLI were on PATH. Upstream directus/directus:11.17.4 does NOT
  expose `directus` on PATH — invocation is via `node /directus/cli.js`,
  same pattern as the entrypoint's bootstrap step. Both scripts
  corrected. Also added -T to docker compose exec in schema-snapshot.sh
  so the script works in non-TTY contexts (CI).

Phase 5 follow-up (non-blocking) flagged in 07's Done section: Directus
warns "Collection 'positions' doesn't have a primary key column and
will be ignored". The positions table uses UNIQUE INDEX (device_id, ts)
matching processor's pattern, not a PK constraint. Means positions is
not auto-registered as a Directus collection — fine for Phase 1, but
the operator faulty-flag workflow will need a custom endpoint or
manual collection registration in Phase 5.

ROADMAP marks 1.6 + 1.7 done. Phase 1 progress: 5/9 tasks complete
(1.1, 1.2, 1.3, 1.6, 1.7); 1.4, 1.5, 1.8, 1.9 remain.
2026-05-02 09:40:53 +02:00
..

Phase 1 — Slice 1 schema + deploy pipeline

Stand up a Directus 11 instance with the minimum schema needed to register entries and tie them to devices, plus the schema-as-code pipeline (snapshots + db-init) and Gitea Actions CI. This is what Rally Albania 2026 needs to run as a test event.

Outcome statement

When Phase 1 is done:

  • Directus runs locally via docker compose -f compose.dev.yaml up, against a Postgres 16 + TimescaleDB + PostGIS container.
  • db-init/ contains three migrations applied at boot: TimescaleDB extension, positions hypertable creation, faulty boolean column on positions. All idempotent, all guarded by a migrations_applied table.
  • snapshots/schema.yaml contains 12 collections: organizations, users, organization_users, vehicles, organization_vehicles, devices, organization_devices, events, classes, entries, entry_crew, entry_devices. Relations and required fields per directus-schema-draft (the org-level catalog and event-participation sections).
  • The image entrypoint runs db-init, then directus schema apply --yes, then directus start. All three exit 0 against a fresh Postgres.
  • Gitea Actions builds the image on push to main (when snapshots/, db-init/, extensions/, Dockerfile, or workflow file changes), runs the apply pipeline against a throwaway Postgres in CI, and pushes the image to git.dev.microservices.al/trm/directus:main only if the dry-run passes.
  • "Motorsport Club Albania" exists as an organization, "Rally Albania 2026" exists as an event under it, and the Rally Albania class catalog is seeded (M-1..M-7, Q-1..Q-3, C-1/C-2/C-A/C-3, S-1/S-2/S-3 from wiki/sources/rally-albania-regulations-2025.md §2.2–§2.5). At least one test entry registered with vehicle + crew + devices, used to dogfood the registration workflow.

Phase 1 deliberately stops short of:

  • Course definition (stages, segments, geofences, SLZs) — Phase 2.
  • Penalty system tables and timing tables — Phase 3.
  • Permission policies — Phase 4 (collections are admin-only by default).
  • Custom extension code — Phase 5.

Sequencing

1.1 Project scaffold
   └─→ 1.2 db-init runner script
          └─→ 1.3 Initial migrations
                 ├─→ 1.4 Org-level catalog collections (admin UI work)
                 │      └─→ 1.5 Event-participation collections (admin UI work)
                 │             └─→ 1.6 Schema snapshot/apply tooling
                 │                    └─→ 1.7 Image build & entrypoint
                 │                           └─→ 1.8 Gitea CI dry-run
                 │                                  └─→ 1.9 Rally Albania 2026 seed

Tasks 1.1 → 1.3 are pure infrastructure and can land before any Directus admin UI work begins. Tasks 1.4 + 1.5 happen against a locally running Directus instance. Tasks 1.6 → 1.8 wire the artifacts together. Task 1.9 is dogfood verification.

Files modified

Phase 1 produces this layout in directus/:

directus/
├── .gitea/workflows/build.yml
├── snapshots/
│   └── schema.yaml             # generated; edits via admin UI + pnpm run schema:snapshot
├── db-init/
│   ├── 001_extensions.sql      # CREATE EXTENSION timescaledb (postgis added in Phase 2)
│   ├── 002_positions_hypertable.sql
│   └── 003_faulty_column.sql
├── extensions/                 # empty — Phase 5 fills this
├── scripts/
│   ├── apply-db-init.sh        # numeric-order, guard-table-protected runner
│   ├── schema-snapshot.sh      # wraps `directus schema snapshot --yes`
│   └── schema-apply.sh         # wraps `directus schema apply --yes`
├── entrypoint.sh               # apply-db-init.sh && directus schema apply && directus start
├── Dockerfile                  # FROM directus/directus:11.x + bundled artifacts
├── compose.dev.yaml            # local dev: directus + timescaledb container
├── package.json                # only for the snapshot/apply npm scripts and tooling
├── pnpm-lock.yaml
├── .env.example
├── .dockerignore
├── .gitignore
└── README.md

Tech stack (decided)

  • Directus 11.x (latest stable on the 11.x line at time of build). Pinned in Dockerfile FROM line.
  • Postgres 16 + TimescaleDB + PostGIS as the database (PostGIS extension added in Phase 2; Phase 1 only uses TimescaleDB).
  • pnpm for any local dev scripts (snapshot wrappers, lint).
  • bash (POSIX-compatible) for apply-db-init.sh and entrypoint.sh. No Node dependency at runtime — only Directus needs Node, and that's the upstream image's responsibility.
  • psql (from postgresql-client package) inside the image for db-init application.
  • Gitea Actions for CI, matching the processor and tcp-ingestion workflow shape.

If an implementer wants to deviate, they must update the relevant task file first.

Key design decisions inherited from processor

  • Image is bundled, not assembled at runtime. snapshots/, db-init/, and extensions/ are baked into the image, not mounted as volumes. Reproducible across envs.
  • Slim Dockerfile. Multi-stage if extensions need a build step (Phase 5+); for Phase 1 a single stage is enough.
  • CI workflow — single-job pattern matching processor/.gitea/workflows/build.yml. Use services: for the throwaway Postgres in the dry-run step.
  • No .env in image. All env vars come from the deploy stack (Portainer / compose) at runtime.

Open questions blocking task-level detail

None. The schema draft pinned the org-level catalog and event-participation shape; Phase 1 implements exactly that subset.