Files
directus/.planning/phase-1-slice-1-schema
julian 25a9731070 Task 1.3 — Initial migrations
Three SQL files under db-init/ create the schema processor writes
against. All three apply cleanly via apply-db-init.sh, are idempotent
on re-run, and end with assertion blocks that catch silent
schema drift.

001_extensions.sql — registers timescaledb on the directus database.
  PostGIS deferred to Phase 2 (per Plan A). The timescaledb-ha image
  pre-creates the extension at DB init, so the IF NOT EXISTS guard
  fires as a NOTICE — expected and harmless.

002_positions_hypertable.sql — positions hypertable, exact
  column-by-column match against processor/src/db/migrations/0001_positions.sql.

  Cross-checking against processor surfaced 8 divergences from the
  original task spec; processor wins in every case (it is the writer
  and is in production). The corrections:

    - added ingested_at timestamptz NOT NULL DEFAULT now()
    - added codec text NOT NULL
    - altitude/angle/speed: real NOT NULL (not DOUBLE PRECISION nullable)
    - satellites/priority: NOT NULL
    - removed attributes DEFAULT '{}'::jsonb (processor always writes)
    - replaced PRIMARY KEY with UNIQUE INDEX positions_device_ts
      (idiomatic for TimescaleDB hypertables)
    - chunk interval 1 day, not 7 days
    - two indexes (positions_device_ts + positions_ts), not one composite

  Without these corrections every processor INSERT would have failed
  with NOT NULL violations. Spec deliverables section updated to
  reflect the correct shape so future readers see the right schema.

003_faulty_column.sql — adds the operator-controlled faulty boolean
  flag plus the partial index positions_faulty_idx ON (device_id,
  ts DESC) WHERE faulty = FALSE. The column is set only via Directus
  admin (Phase 4 permissions); processor's writer never touches it.
  The partial index optimises the hot-path read pattern (every
  processor evaluator filters faulty = FALSE); operator queries that
  look at faulty rows specifically use the broader positions_device_ts
  index from 002.

Live-verified 2026-05-01:
  - First apply: 3 applied, 0 skipped, exit 0.
  - Re-run: 0 applied, 3 skipped, exit 0.
  - All 13 columns present with correct types/nullability/defaults.
  - Hypertable registered with 1-day chunk interval.
  - Three expected indexes present.

Non-blocking observation: TimescaleDB's create_hypertable()
auto-created a fourth index (positions_ts_idx) duplicating our
explicit positions_ts. Processor's migration has the same redundancy
so stage already lives with this. Cleanup path documented in the
task spec for Phase 3 hardening (create_default_indexes => FALSE
in the create_hypertable call).

ROADMAP marks 1.3 done; 1.4 next.
2026-05-01 22:52:06 +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.