a8e808e71c
Initial commit. Establishes the .planning/ tree mirroring processor's shape (ROADMAP.md as nav hub + per-phase folders with READMEs and granular task files). Six phases: 1. Slice 1 schema + deploy pipeline — what Rally Albania 2026 needs. Org catalog (orgs, users, vehicles, devices) + event participation (events, classes, entries, entry_crew, entry_devices). db-init/ for the positions hypertable + faulty column. snapshot/apply tooling. Gitea CI dry-run. Dogfood seed of Rally Albania 2026. Nine task files with full Goal / Deliverables / Specification / Acceptance criteria / Risks / Done sections. 2. Course definition — stages, segments, geofences, waypoints, SLZs. PostGIS extension introduced here. 3. Timing & penalty tables — co-developed with processor Phase 2. entry_segment_starts, entry_crossings, entry_penalties, stage_results, penalty_formulas. 4. Permissions & policies — Directus 11 dynamic-filter Policies per logical role. Deployment-time work, deferred to keep early phases focused on the data model. 5. Custom extensions — TypeScript hooks/endpoints implementing the cross-plane workflows the schema implies (faulty-flag → Redis stream emit, stage-open materializer, etc.). 6. Future / optional — retroactivity preview UI, command-routing Flows, audit trails, federation rule import. Not committed. Non-negotiable design rules captured in ROADMAP.md: schema authority in Directus + snapshot-as-code + db-init for non-Directus DDL + sequential idempotent migrations + entrypoint apply order + no application logic in Flows + permissions deferred to Phase 4. Architectural anchors point at the wiki at ../docs/wiki/ — the schema draft, the Rally Albania 2025 source page, plus the existing processor/postgres-timescaledb/live-channel pages. Each task file calls out the wiki refs an implementing agent should read first. README.md mirrors the processor service README structure: quick start, local Docker test, prod/stage deployment notes, env vars, CI behavior.
Phase 4 — Permissions & policies
Status: ⬜ Not started — depends on Phases 1–3 (all collections must exist before policies are drafted) Outcome: Every collection × action combination has an explicit Directus 11 Policy attached. Multi-tenant isolation is enforced by Directus's dynamic-filter mechanism, not by application code. Operators see only data scoped to orgs they belong to, with the actions allowed by their role. Non-admin users can register entries, view live tracking, review their own results — all without ever needing admin role.
Why this is a separate phase
- Premature policy commitment is expensive. Defining policies before the data model has shaken out leads to filters that break when a collection's shape changes. Phases 1–3 get one to two iterations on the schema; Phase 4 lands when the model is stable.
- Policy filters are tedious but not architectural. This is admin-UI configuration work, not design. Roughly 5 roles × ~20 collections × 4 CRUD actions = ~400 (role × collection × action) cells, most of which are templated repeats of "user is in this org via
organization_users". - Testable as a unit. End-state: a non-admin test user with
participantrole can perform exactly the operations they should, and zero others. Phase 4's CI / dogfood verification is a permission-boundary test suite.
Roles to support (per directus-schema-draft)
| Role | Power |
|---|---|
org-admin |
Full CRUD within their org. Can manage organization_users, classes, events. |
race-director |
Manage entries, segments, geofences, penalties for events in their org. Approve / publish stage results. Cannot create new orgs. |
marshal |
Read-only on most collections; can flag faulty positions and write notes on entries during the event. Time-limited (only during active event). |
timekeeper |
Edit entry_segment_starts.target_at (late-arrival reseeding); read all entries; cannot modify penalties. |
participant |
Read-only on entries they appear in (via entry_crew); read on the events they're registered for; no writes. |
viewer |
Read-only on public-facing event data (live map, published results). Lowest privilege; default for any user not otherwise scoped. |
Tasks (sketched, not detailed)
| # | Task | Notes |
|---|---|---|
| 4.1 | Draft the canonical "user is in this org with this role" filter expression | One JSON filter that gets reused. Lives in a template / snippet for copy-paste. |
| 4.2 | org-admin policy |
All CRUD on org-scoped collections, scoped via the canonical filter. |
| 4.3 | race-director policy |
CRUD on events / entries / classes / penalties for events in their org. |
| 4.4 | marshal policy |
Field-level write on positions.faulty; entry notes; otherwise read-only. |
| 4.5 | timekeeper policy |
Field-level write on entry_segment_starts.target_at and manual_override; otherwise read-only. |
| 4.6 | participant policy |
Filter on entries via entry_crew.user_id = $CURRENT_USER. |
| 4.7 | viewer policy |
Public read on a curated subset (live positions for active events, published stage_results). |
| 4.8 | Snapshot regeneration + CI verification | All policies round-trip via directus schema snapshot (verify the format is faithful — Directus's policy serialization has historically been finicky). |
| 4.9 | Permission-boundary test suite | Custom test that creates a user per role, attempts a series of CRUD operations, asserts allowed/denied per a fixture. Runs in CI alongside the dry-run. |
Open questions blocking task-level detail
- Marshal time-limiting. Marshal access tied to "during active event" — does Directus's dynamic filter support time-bounded conditions natively, or does this need a custom hook (Phase 5)?
- Field-level vs row-level restrictions. Some collections (
positions,entry_segment_starts) need field-level write restrictions (only one column writable). Verify Directus 11 supports field-level policies in the dynamic-filter mechanism, or fall back to a hook that rejects writes to other fields. - Snapshot fidelity. Does
directus schema snapshotfaithfully capture all policy filter JSON? If not, policies might need to live in a separate seed script applied alongside the snapshot.