Skip to content

Leo → RestartiX Platform: Consolidated Port Map (F1, F3, F4, F5, F6, F11)

Survey artefact, 2026-08-02. Produced by a 14-agent survey of the four live restartix-leo-* / restartix-intakes repos, mapped against the shipped platform substrate. Companion to platform-completion.md, which is the plan; this is the evidence behind it.

Status: §8.1, §8.3 and §8.4 were ratified 2026-08-02 and are recorded as settled in platform-completion.md → Known Phase-0 reconciliations and CLAUDE.md → Settled 2026-08-02. In short: the F2.1 offerings stand-in ships, named offerings from day one (catalog identity + offering_specialists + offering_forms; no pricing, no plans, no products, no purchase path); specialties are per-org (organization_id NOT NULL); CNP is opt-in per template, encrypted on patient_profiles, never in the generic value store. The §8 entries below keep the original analysis and carry the outcome inline. Still open and not to be invented: §8.2 override scoping, §8.5–§8.13, and appointment-package tracking.

Spot-verified against code before filing: the apps/docs/features/scheduling/go/ reference engine (6 files), adherence/cadence.go's AppointmentCounter hook, the absence of any forms.* / appointments.* / specialists.* permission seed, consents.source_form_id's reservation in 000008, the seeded max_specialists limit, migration 000039 being taken, and pii_contact not being a valid classification class.

Verification basis: every claim below was checked against the live repos. Corrections to the input port maps are marked ⚠️. Source trees inspected: restartix-platform (target), restartix-leo-dashboard, restartix-leo-platform, restartix-leo-api, restartix-intakes.


0. Five verified corrections that change the plan before you read further

These invalidate specific instructions in the per-dimension port maps. Fix them first or the first migration fails.

0.1 ⚠️ The Intakes service source is available. Three port maps say it isn't.

/Users/ntropyspace/Work/restartix/restartix-intakes is a complete Next.js + Drizzle + Postgres + Redis service at commit 624a73e, with:

AssetPathSize
Availability engine (TS original)core/services/availability.ts976 lines
Full schemacore/db/drizzle/schema.ts313 lines
Double-booking exclusion constraintcore/db/drizzle/migrations/0000_intakes_overlap.sqlhand-written
Hold protocolcore/services/business/holds/holds.ts
Timeslot cachecore/services/timeslots/timeslots-cache.service.ts257 lines
Client cooldowncore/services/rate-limiting/client-rate-limiting.service.ts153 lines
Min-lead-timecore/services/lead-time/lead-time.service.ts42 lines
Assignmentcore/services/business/opening-assignment.ts66 lines
Operational docsdocs/*.md23 files

Consequences:

  • The scheduling port map's "cooldown, min-lead-time and the overlap exclusion constraint are all re-implementation requirements" is wrong. They are ports with a reference implementation.
  • The appointments port map's "the ~20k users have live bookings … inside an Intakes Postgres whose code we do not have" is wrong. The schema is core/db/drizzle/schema.ts and the migration history is .drizzle/. The legacy extraction plan has a source of truth today.
  • The scheduling open question "does any real clinic use per-service overrides today?" is answered by the schema: scheduleOpeningOverrides.scheduleId is NOT NULL with onDelete: cascade. Overrides are per-schedule in production, with live rows. See §8.2.

0.2 ⚠️ Migration 000039 is taken. Every port map allocates it.

services/api/migrations/core/000039_restore_safe_principal_is_human.{up,down}.sql exists. New migrations start at 000040.

0.3 ⚠️ apps/docs/architecture/appointments-substrate.md will not apply as written

Four defects, verified:

LineDoc saysReality
41, 181, 205, 208patient_persons(id), current_user_patient_person_ids()patient_profiles / current_human_patient_profile_ids() (migration 000006)
385class pii_contactNot a class. internal/shared/classification/types.go:17-25 defines exactly nine: public, org_internal, pii_basic, pii_regulated, clinical, clinical_sensitive, auth_secret, audit_only, system_metadata. make check fails.
43, 182, 221specialist_principal_id → principals(id)Contradicts P9's specialists.human_id UNIQUE NULL — makes calendar-only specialists unbookable
73patient_service_plan_id BIGINTP26 is UUIDv7

0.4 ⚠️ The "copy-hours bulk-replace bug" does not exist

The specialists port map calls a per-day transactional replace "the concrete fix for the copy-hours bug the survey flagged." Verified at restartix-intakes/core/repositories/openings.repository.ts:200-234: bulkReplaceWeeklyHours already loops per dayOfWeek, deleting only that day's rows, inside db.transaction. The proposed fix is what leo already does. Port the semantics as-is; drop the phantom concern.

0.5 The reference Go engine is real but untested and uncompiled

apps/docs/features/scheduling/go/ is in no Go module (go.mod exists only at services/api, services/telemetry, services/media). I copied it to a scratch module and ran go vet:

  • availability.go + assignment.go + types.govet clean, type-checks
  • holds.go needs github.com/redis/go-redis/v9 (already a platform dep) ✅
  • types.go is not gofmt-clean ⚠️
  • Zero _test.go files ⚠️

So it is a faithful, type-correct transcription that has never executed. Treat it as a high-quality draft requiring a test suite, not as shipped code. The TS original is now available to differential-test against.


1. Executive summary

What this port actually is

Three different things are being conflated under the word "port," and they have wildly different economics:

(a) Genuine code reuse — ~15% of the effort, concentrated in one place. Only one substantial artefact ports as code: the availability/assignment engine. apps/docs/features/scheduling/go/{availability,assignment,types}.go (~1,400 lines, vet-clean) is a faithful Go port of restartix-intakes/core/services/availability.ts (976 lines), and both now sit on disk for differential testing. This is the single largest de-risked item in the September scope, because slot math is where correctness bugs are subtle, expensive, and invisible until a patient shows up at the wrong hour. DST spring-forward gap probing, overnight-rule midnight splitting, and override replace-not-merge semantics are all encoded and derived from a system that has been running against real bookings.

Beyond that: the exclusion-constraint SQL ports verbatim, the hold key/TTL/heartbeat protocol ports structurally onto internal/core/locks, and roughly a dozen packages/ui-shaped React interaction surfaces port as UX transcription (structure and behaviour preserved, every line rewritten).

(b) Knowledge transfer — ~70% of the value, and the reason this exercise is worth doing. The clinical operating rules in §4 are the real payload. They are not in any spec, in leo or here. Things like: analysis forms deliberately do not pre-create value rows (FORM_TYPE_TO_SLOT.analysis.createValues: false); the exclusion constraint is partial on status='created' so cancelled bookings do not block a slot; report is offering-bound and auto-created while prescription is free-choice; a UTF-8 BOM on CSV export because Excel mangles Ștefan without it. Each is a small thing someone learned from a clinic complaining. Collectively they are years of operational learning, and they cost nothing to carry and a lot to rediscover.

(c) Anti-pattern inoculation — the negative space, and larger than it looks. The leo Strapi DB is genuinely bad in ways that are instructive: exactly one NOT NULL in 94 tables (and it is id), zero FK constraints, zero indexes in the dump, 52 of 94 tables are _links join tables, patient passwords stored in plaintext in a non-private column that reaches the browser and gets interpolated into rendered consent text. The platform's foundation already forecloses every one of these. The value here is knowing which leo behaviours are load-bearing product decisions versus scar tissue — and §7 draws that line.

Honest scope assessment

Porting does not meaningfully reduce the build. F1/F3/F4/F5/F6 remain a greenfield build against the platform's own authoritative specs (data-model.md Areas 2/4/5/7/11, appointments-substrate.md, patterns.md P9/P13/P14/P18/P19/P23/P24/P30/P33/P40/P47/P54). What porting changes:

  1. It de-risks the hardest algorithm (availability/slots/DST) with a running reference on both sides of a language boundary.
  2. It supplies a requirements oracle. When a spec is silent, leo's live behaviour answers — and now, with the Intakes source available, the answer is readable rather than inferred.
  3. It converts several "open questions" into observations. Override scoping, hold TTLs, cooldown keying, exclusion-constraint predicate, form-slot cardinality: all now empirically settled (§8).
  4. It surfaces four platform-internal doc bugs that would otherwise have been discovered by a failing migration (§0.3, §0.2).

Current state, verified: F1–F6 across apps/docs/implementation-plan/features.md shows 14 checked / 156 unchecked boxes. services/api/internal/core/domain/ has 43 domains, none of which are specialists, forms, appointments, scheduling, documents, or offerings. apps/clinic/app/(dashboard)/ has six routes — analytics, integrations, legal-documents, library, patient-tiers, patients — and no team, staff, settings, calendar, or appointments surface. grep for forms.*/documents.*/appointments.* permission seeds across all migrations returns zero rows.

This is a from-scratch build of the entire clinical-operations stack. The port makes it better-informed, not shorter.

What the substrate already gives you for free

This is genuinely substantial and the port maps get it right. Shipped and directly usable: RLS + current_app_has_permission + ConnFromContext + P47; audit_log (partitioned, append-only, cmd/check-audit-coverage-enforced); internal/integration/s3 with SurfaceSignatures/SurfaceDocuments/SurfaceFormsUpload/SurfaceAppointmentFiles already registered and unused — reserved for exactly these features; internal/core/locks (Redis, Lua-atomic, 120s/45s, P54); internal/core/sse (Redis pub/sub fan-out across ECS tasks); internal/core/ratelimit (Redis, IPKey/PrincipalKey); internal/core/events (registry + bus + scheduler); internal/core/notify (SES live); internal/core/impersonation; internal/shared/{apiquery,softdelete,classification}; immutable_unaccent + pg_trgm from 000001; consents ledger with source_form_id already reserved for F3; RequireCurrentConsents middleware already mounted; P18 versioning proven twice (patient_tier_versions, tier_versions); packages/ui with sortable-list, async-multi-select-filter, data-table, edit-lock-banner, calendar, require-permission.


2. Revised build order, and where F2 bites

2.1 The F2 constraint, stated precisely

Leo's appointment_template (restartix-leo-api/src/api/appointment-template/content-types/appointment-template/schema.json) is one entity doing three jobs:

  1. Catalog identitytitle, slug, description, cover, video, speciality, is_public, minicrm_title, attachments
  2. Booking configurationintakes_schedule_id → the Intakes schedules row carrying slotDurationMinutes, slotGapMinutes, slotsCooldownMinutes, minLeadTimeMinutes, slotsHorizonDays, slotsOpenAt/CloseAt
  3. Form-slot binding + specialist rosterdisclaimers[], surveys[], parameters, report, advice, analysis, prescription, specialists[], use_even_distribution

The platform splits (2) into calendars (F4, in scope) and leaves (1) and (3) in offerings (F2, out of scope).

Six hard dependencies on the catalog entity, all inside in-scope features:

#DependencyFeatureSeverity
1calendars.offering_id NOT NULL FKF4.2Blocking — no FK target
2appointments.offering_id NOT NULL FKF5.1Blocking — no FK target
3offering_forms junction — decides which forms auto-attach at appointment creationF3.4Blocking — this is the form-generation mechanism
4offering_specialists with priority — the roster the assignment engine walksF4.3Blocking — assignment has nothing to iterate
5Public booking browse (/francize/[slug] → bookable offerings)F5.4Blocking — nothing to list
6"Servicii efectuate" row on the generated report PDFF6.2Cosmetic — degrades to blank

Items 3 and 4 are the ones that matter most, and they are the ones easiest to overlook: F3 Forms is not independently shippable in its useful form without a catalog entity, because "which forms does this appointment get" is answered by the offering, not by the appointment.

2.2 Proposed minimum viable stand-in

Create offerings under the glossary-mandated name from day one, scoped to F2.1 only.

offerings
  id, organization_id NOT NULL, title, slug, description,
  specialty_id FK specialties(id) ON DELETE RESTRICT,
  default_duration_minutes, cover_url, video_url,
  minicrm_title, is_public, published, published_at,
  deleted_at, created_at, updated_at
  UNIQUE (slug, organization_id)
  GIN (immutable_unaccent(title) gin_trgm_ops)

offering_specialists (offering_id, specialist_id, organization_id, priority INT NOT NULL DEFAULT 0)
  PK (offering_id, specialist_id)

offering_forms (offering_id, form_template_id, organization_id, slot, sort_order)
  slot ∈ disclaimer|survey|parameters|analysis|advice|report|medical_prescription

Why this is a stand-in and not F2 creeping back in. The deferred half of F2 is the commerce half — F2.2 Service Plans (packages, sessions-included, validity) and F2.3 Products. Those are the parts with pricing, purchase, and entitlement semantics, and they are genuinely independent. What is proposed here is the catalog identity only: roughly 15 columns and two junctions, no pricing, no plans, no products, no purchase path, no enrollment semantics. It is the FK target three in-scope features require, and nothing more.

Naming it offerings now is load-bearing. The glossary's deferral condition for services → offerings is "until that area is built." Building the stand-in meets it. Naming the stand-in services would mean a rename of five tables and every FK later, on a table that by then holds production rows under the forward-only freeze.

⚠️ New finding — the glossary's other rename target is already occupied. apps/docs/architecture/glossary.md:372 mandates service_plans → enrollments. But protocols.kind IN ('prescription','enrollment') is shipped and live in production (000023_sessions.up.sql:894), where enrollment means "patient self-enrolled in a guided program." When F2.2 ships it cannot be called enrollments. The glossary row needs a new target (offering_packages? service_packages?) — decided now, in the docs PR, while it costs nothing.

Explicit boundary for the build: F2's deferred surface stays deferred. No pricing column, no offering_plans, no purchase flow, no entitlement binding on offerings. If a task starts reaching for those, it has left scope.

2.3 Build order

STEP 0   Docs reconciliation PR (no schema) — BLOCKING
STEP 1   000040  specialties + specialists + specialist_specialties         (F1)        ✅ SHIPPED
STEP 2   000041  offerings + offering_specialists                            (F2 stand-in) ✅ SHIPPED
STEP 3   000042  custom_fields + custom_field_versions + custom_field_values (F3.1)
STEP 4   000043  form_templates + form_template_versions + forms
                 + offering_forms                                            (F3.2/3.3/3.4)
STEP 5   000044  btree_gist + specialist_weekly_hours
                 + specialist_schedule_overrides                             (F4.1)
STEP 6   000045  calendars + calendar_specialists + calendar_forms
                 + specialist_assignment_tracking                            (F4.2/4.3)
STEP 7   000046  appointments + appointment_files + appointment_reviews      (F5.1/5.2/5.3)
STEP 8   000047  pdf_templates + versions + components                       (F6.1)
STEP 9   000048  appointment_documents + appointment_document_files          (F6.3)
STEP 10  F11 compliance hardening + legacy migration rehearsal

Each migration seeds its own permission rows, role-template grants, RLS policies, and data-classification.md entries in the same PR (make check enforces the last one).

Layer-discipline gate. F1–F6 sit above 1D admin surfaces, which CLAUDE.md records as "partially shipped; remainder in flight." Before Step 1, read the live checkboxes in apps/docs/implementation-plan/foundation.md. Notably, apps/clinic has no settings/team surface at all today — F1's roster UI is the first one. If a required 1D surface is open, surface it rather than proceeding.


3. Per-feature port plan

F1 — Specialists & Specialties

Schema (000040)

specialtiesid, organization_id NOT NULL, title, slug, created_at, updated_at; UNIQUE (slug, organization_id); GIN (immutable_unaccent(title) gin_trgm_ops).

specialists — per data-model.md Area 2: id, organization_id NOT NULL, human_id UUID UNIQUE NULL REFERENCES humans(principal_id), name, title, description, slug, minicrm_name, signature_url, avatar_url, scheduling_timezone VARCHAR(64), scheduling_active BOOLEAN, deleted_at. Partial index (organization_id) WHERE deleted_at IS NULL; GIN (immutable_unaccent(name) gin_trgm_ops); UNIQUE (slug, organization_id).

specialist_specialties — junction with denormalised organization_id NOT NULL.

RLS: staff SELECT via organization_memberships; patient SELECT via current_human_patient_profile_ids() joined to patients.organization_id (required for the portal booking picker — without it F4/F5 booking returns zero rows). Permissions: specialties.manage, specialists.view_org, specialists.manage.

⚠️ humans has no name column. Verified at 000002_tenancy_rbac.up.sql:292-322provider_subject_id, provider_org_id, email, confirmed, blocked, portal_credential_generation, last_activity, preferred_language, timezone. specialists.name covers specialists; admin/customer_support staff have nowhere to store a display name. See §8.5.

API

  • internal/core/domain/specialties/ and internal/core/domain/specialists/ via /new-domain
  • Create wrapped in middleware.EnforceLimit("max_specialists", 1) — the limit is already seeded (000004_tiers_subscriptions.up.sql:568,603-613: free 2 / pro 20 / dedicated unlimited)
  • Delete = softdelete.SoftDelete; specialty delete = hard, but pre-checked → 409 with in-use counts
  • ?q= typeahead + ?ids= resolve from the first commit
  • Register SurfaceAvatars in internal/integration/s3/surfaces.go — verified absent (registry has Signatures, Documents, FormsUpload, Logos, AppointmentFiles, ExerciseAssets). Do not overload SurfaceLogos (org branding)
  • Signature/avatar upload copies organization.UploadBrandingAsset (branding.go:131-201) verbatim: sniff → content-addressed key → upload → DB write → rollback-delete new on DB failure → orphan-delete previous only on success
  • internal/core/scheduling.ResolveSchedulingTimezone(ctx, locationID, specialistID, orgID) — P23's single chain; verified absent today
  • GET /v1/reference/timezones + IANA validation on write

UI to port

SourceTarget
restartix-leo-dashboard/app/(dashboard)/echipa/listing.tsxClinic /team roster
.../echipa/[id]/{layout,sidebar,summary,summary-specialist}.tsx/team/{id} detail shell
.../echipa/[id]/specialitati/content.tsxSpecialties tab (leo's is a stub — build it)
.../francize/[id]/specialitati/edit.tsxSpecialties admin
.../echipa/[id]/disponibilitate/content.tsxAvailability page
app/_shared/specialist-availability/* (7 files: form-weekly, weekly-day-item, availability-calendar, override-dialog, copy-hours-dialog, timezone-selector, form-availability)The whole availability editor
app/_shared/specialist-availability-{dialog,tabs}.tsxThree-instantiation pattern (page / dialog+calendar / dialog−weekly)

Do not port leo's echipa/add/form-user.tsx — it sets the new member's initial password directly (confirmed: true, no invite loop). The platform's invites domain + POST /v1/organizations/{id}/staff-invitations (Clerk magic-link) is shipped and strictly better.

New packages/ui primitives: TimeSelect (15-min increments, searchable), TimezoneSelect (IANA, sorted by live UTC offset). Both verified absent.


F3 — Forms, Custom Fields & Signatures

Schema (000042 + 000043)

custom_fields / custom_field_versions / custom_field_values — P19 + P24. UNIQUE (organization_id, entity_type, key) and UNIQUE (organization_id, system_key) — never global. (leo's meta_field.key is globally unique, which is why its cross-franchise template copy leaves templates pointing at another tenant's field definitions.)

form_templates / form_template_versions / forms. forms carries fields JSONB snapshotted at instance creation + template_version INT, values JSONB (GIN), files JSONB, status enum pending|in_progress|completed|signed, completed_at, signed_at, deleted_at, patient_profile_id, organization_id, appointment_id NULL, created_by_principal_id, signed_by_principal_id.

The snapshot is the single most important structural change. leo has none — editing a template retroactively rewrites how every historical form renders, which its own migration docs call the #1 reason to redesign.

offering_forms junction (see §2.2).

ALTER consents to light up source_form_id → forms(id)already reserved in 000008 with the comment "FK lights up when F3 ships the forms table," plus CHECK ((source='form') = (source_form_id IS NOT NULL)).

API

  • Template CRUD + publish/version/rollback modelled on patienttiers.Repository.PublishVersion (repository.go:287,378)
  • Instance create-from-template: one INSERT, one transaction. leo does N+1 POSTs in a client-side for await loop with a standing in-code TODO that navigating away mid-loop corrupts the form
  • Batched PATCH /forms/{id}/values — one txn, one coalesced audit row with the changed-key diff
  • P14b: 409 Conflict on any mutation once status='signed', at handler and service layers
  • Auto-fill resolver: at creation, copy from patient_profiles (via profile_field_key) and custom_field_values (via custom_field_id) into the snapshot; on save, explicit audited write-back. This replaces leo's invisible meta-value redirect middleware
  • Server-side audience projection (patient | staff | admin) driving what is sent, what is validated, and what renders into the patient PDF
  • File answers on s3.SurfaceFormsUpload (already registered: pdf/png/jpeg/webp, 10 MB)
  • Cat E events: form.created, form.completed, form.signed, form_template.published

UI to port

SourceTarget
.../sabloane/[id]/campuri/{content,field,dialog-add}.tsxForm builder
.../francize/[id]/{dialog-meta,content-patients}.tsxField-library admin
.../consultatii/[id]/{chestionare,acorduri,recomandari,evaluare-mobilitate}Fill surfaces
restartix-leo-platform/app/consultatii/[uid]/layout.tsxPortal sequential form wall
.../consultatii/[uid]/chestionare (SurveyFields)Portal renderer

Drop the five patient-identity meta bindings (patient_meta_birthdate/_residence/_occupation/_sex) — patient_profiles has date_of_birth, sex, occupation, residence, phone as native columns (verified 000006:34-62). Only CNP has no home (§8.3).


F4 — Scheduling, Availability & Calendars

Schema (000044 + 000045)

CREATE EXTENSION btree_gist — verified absent (000001 enables uuid-ossp, pgcrypto, unaccent, pg_trgm, vector, pg_stat_statements only). Must run on DATABASE_DIRECT_URL.

specialist_weekly_hoursorganization_id NOT NULL (⚠️ data-model.md Area 4 omits it, violating a CLAUDE.md hard rule — fix the doc), specialist_id, day_of_week enum, start_time/end_time TIME (local wall-clock), location_id NULL (P40). UNIQUE (specialist_id, day_of_week, start_time, end_time) — direct port of leo's uq_opening_dow_start_end.

specialist_schedule_overridesorganization_id NOT NULL, start_date/end_date TIMESTAMPTZ (absolute UTC), availability BOOLEAN, location_id NULL, calendar_id UUID NULL (NULL = all calendars). Scope decision in §8.2.

calendars + calendar_specialists (priority INT) + calendar_forms + specialist_assignment_tracking. calendars carries offering_id NOT NULL, slot_duration_minutes, slot_gap_minutes, cooldown_minutes, min_lead_time_minutes, horizon_days, slots_open_at/close_at, assignment_strategy, is_public, published, slug, deleted_at — all ported from restartix-intakes/core/db/drizzle/schema.ts:116-147, plus a CHECK enforcing window-XOR-horizon (leo enforces it only in a client-side save handler).

Both availability tables are STATE — flat, never partitioned.

API

  • Move apps/docs/features/scheduling/go/{availability,assignment,types}.go into internal/core/domain/scheduling/, apply the appointment_type → calendar rename, add ResolveSchedulingTimezone
  • Write the test suite that does not exist. Differential-test against restartix-intakes/core/services/availability.ts. Minimum cases: spring-forward gap (safeLocalToUTC probing), fall-back ambiguity, overnight rule split at local midnight (BuildWeeklyWindowsUTC), override replaces-not-merges for a whole local day, 90-day window cap
  • gofmt types.go before it lands
  • Hold store: port go/holds.go onto platform Redis with cache.OrgResource(orgID, ...) namespacing (leo's keys hold:{scheduleId}:{slot}:{openingId} carry no org dimension), Lua-atomic heartbeat from internal/core/locks/store.go, publication onto internal/core/sse
  • Server-derived booking client identity — signed HttpOnly cookie. leo's clientId is caller-supplied (body > query > cookie > generated), never validated, never persisted, and the dashboard regenerates it after every successful booking, defeating the 24h cooldown
  • Weekly-hours PUT as per-day transactional replace (port openings.repository.ts:200-234 semantics as-is)
  • Availability read capped at 90 days, returning leo's AvailabilityDay envelope + dateRange {requestedStart, requestedEnd, effectiveStart, effectiveEnd} for out-of-window greying

UI to port

SourceTarget
.../servicii/[id]/calendar/ (info / specialisti / tab-settings)Calendar configuration
.../evenimente/ + hooks/use-availability.tsScheduling cockpit
restartix-leo-platform/app/programare/[slug]/components/* (25 UI-state components)Portal booking page
restartix-intakes hold-stream consumeruseHoldsStream + useHolds in packages/uibuild once; leo duplicates the reducer three times

F5 — Appointments

Schema (000046) — per the corrected appointments-substrate.md (§0.3). 9-status enum with cancelled split three ways (cancelled_by_patient / cancelled_by_clinic / cancelled_late) because the adherence denominator must exclude clinic-attributable cancellations. Two-phase identity: patient_profile_id set at booked with patient_id NULL; patient_id linked at onboarding. Both pair CHECKs, all five indexes (two partial), all three RLS policies. No deleted_at — the status enum covers every did-not-happen case.

Plus appointment_files and appointment_reviews.

API

  • State-machine validator (P33). This is new construction, not a port — leo has no enforced machine at all; Strapi accepts any status → any status and done can be dragged back to upcoming from a free Select
  • Cancel endpoint with attribution + cancelled_late derived from an org threshold; reason ≤ 500 chars
  • Wire AppointmentCounter into adherence. internal/core/domain/adherence/cadence.go already defines the interface and returns ErrSupervisedNotImplemented. Supervised protocols are shipped and waiting on exactly this
  • Bounded calendar-view endpoint bucketed by scheduling timezone, not toISOString() (leo's near-midnight wrong-day bug), with month heat-map from SQL COUNT GROUP BY
  • Public booking under /v1/public/ on AdminPool per P5, projection through classification.Filter
  • Auto-noshow sweep cron on internal/core/events/scheduler.goleo has none; nothing ever writes noshow. Apply the silence-timeout lesson: only flip genuinely-unstarted appointments
  • Daily.co video.Provider capability; extend platform_service_providers' chk_psp_capability_provider whitelist with ('video','daily')

UI to port

SourceTarget
.../consultatii/listare/{listing,listing.context}.tsx + components/{dialog-content-filters,filter-popover}.tsx/appointments list
.../consultatii/[id]/{layout,tabs,sidebar}.tsxDetail shell (clone apps/clinic/app/(dashboard)/patients/[id]/layout.tsx)
.../consultatii/creare/components/*Booking wizard
.../consultatii/_cards/*KPI tiles → new tab on existing /analytics
restartix-leo-platform/app/consultatii/*Portal appointment list + detail

F6 — Documents, Reports & Prescriptions

Schema (000047 + 000048)pdf_templates + pdf_template_versions + pdf_template_components (block-based). appointment_documents with UNIQUE(appointment_id, type), type ∈ report|medical_prescription (never bare prescription — settled in glossary.md), pdf_template_version frozen at generation, generated_by_principal_id, published, document_url storing the S3 key, not a URL. Plus appointment_document_files.

⚠️ Reject leo's document_templates design. restartix-leo-api/docs/go-migration/08-document-generation.md specifies per-org HTML/CSS templates with margins, and the go-migration survey calls it "best starting point for the new F6 Documents spec." apps/docs/architecture/data-model.md:1402 records that both designs were evaluated, the block-based design won, and document_templates was deleted. features.md F6 says "Use pdf_templates (block-based) — drop the legacy document_templates design." Architecture docs beat foreign migration docs. Mine leo's doc for the funcmap, DocumentData struct, and error→HTTP mapping only.

API

  • pdf.Renderer capability in internal/core/pdf/, registered via capabilities.WrapInternal (the glossary's own worked example of a capability)
  • Generation: signed form + frozen template version → DocumentData → HTML → PDF → s3.SurfaceDocuments
  • Port prepareReportPDFData as ONE type-parameterised builder (restartix-leo-dashboard/core/domain/report/report.service.ts) — leo duplicates it across report and prescription. Algorithm: group flattening → is_private pruning (a private group hides all children) → empty-value pruning → empty-group elision → age-at-appointment via differenceInYears(started_at, date_of_birth)
  • Signature embedded base64 (compliance rule: PDFs self-contained, no external URLs). Prescription generation refuses without a signature → typed 422 with a test
  • Presigned reads (15 min) + document.pdf_accessed audit row

Non-code assets to extract now (independent of build order — these are the irreplaceable part):

  • restartix-leo-dashboard/core/utils/report-templates/default.tsx (361 lines): the exact Romanian label set — Nume pacient, Vârsta la consult, Reședința, Ocupația, CNP, Telefon, Număr registru, Nume specialist, La data de, Servicii efectuate, Pagina n / total — and the A4 geometry (paddingTop 140 / paddingBottom 65 to clear the fixed header/footer)
  • nutritional.tsx (550 lines) + nutritia-durerii.tsx (633 lines): clinician-authored Romanian annex prose. This is domain content, not code. It must not live in TSX again — park it as seed content for platform-tier pdf_templates, with strings through next-intl

4. Preserved-knowledge appendix — business rules learned from running a clinic

This is the highest-value section. Every rule below is extracted from live code or schema and is absent from any spec on either side.

4.1 Scheduling & availability

#RuleSource
S1Overrides fully REPLACE a day's weekly hours — they never merge. If any override exists for a local date, that date's weekly rules are skipped entirely, then the override's availability=true intervals are added.availability.ts:268 applyOverridesAndIntakes
S2An override with zero intervals blocks the whole day. Encoded server-side as a single 00:00–23:59 availability=false row. This is how "block Tuesday" is expressed.override dialog + availability.ts
S3Overnight weekly rules split at local midnight. Fri 20:00–Sat 02:00 becomes two windows. Without this, DST and day-grouping both break.buildWeeklyWindowsUTC
S4Spring-forward gaps are handled by probing forward, not by erroring. A 02:30 slot on a day where 02:00–03:00 does not exist resolves to the next real instant.safeLocalToUtc:178
S5Weekly hours are per-specialist GLOBAL; overrides are per-schedule SCOPED. Deliberate: a specialist works 9–5 as a person, but blocks Tuesday afternoons for one offering only. The UI legend says "Afișare ore săptămânale globale".schema.ts:91 vs :171
S6The double-booking constraint is on the SPECIALIST, not the calendar, and works across calendars. Partial: WHERE (status='created' AND start_date IS NOT NULL AND end_date IS NOT NULL) — cancelled bookings do not block, and no-slot intakes are exempt.0000_intakes_overlap.sql:28-31
S7chk_intakes_time_order: both dates NULL (no-slot booking) OR both set with end > start. Non-timeslot bookings are a first-class state.same file
S8Slot lattice step = duration + gap, generated on a local-midnight grid — not a UTC grid. Off-grid slot starts are rejected by isAlignedToGrid.availability.ts:503
S9Booking window is horizon XOR explicit range. useHorizon=true nulls slotsOpenAt/CloseAt; useHorizon=false sets horizon to 0. Enforced only client-side in leo — needs a DB CHECK.schedules + save handler
S10Defaults that encode real clinic behaviour: slotsCooldownMinutes = 1440 (24h anti-spam), minLeadTimeMinutes = 1440 (24h notice), slotsHorizonDays = 0.schema.ts:130-140
S11Min-lead-time returns a structured error, not a boolean: {message, minLeadTimeMinutes, slotStart, earliestBookableAt}, with human formatting ("2h 30m" / "3 hours"). The UI needs earliestBookableAt to say when booking opens.lead-time.service.ts:27-42
S12Cooldown is keyed client_limit:{clientId}:{scheduleId} — per-schedule, not global. A patient blocked from rebooking Physio can still book Nutrition.client-rate-limiting.service.ts:8-12
S13Rate limiting fails OPEN on Redis error — logged, then booking proceeds. Deliberate: a Redis outage must not stop a clinic taking bookings.same, :51-56
S14Hold client index shares the hold's TTL exactly, "to avoid blocking after expiry" — a stale index would lock a client out of rebooking.holds.ts:90-92
S15Staff and patient heartbeat budgets differ deliberately — staff ~16 min (they need time to find or create a patient mid-booking), patients ~100s.dashboard vs platform hold hooks
S16Assignment tiebreaker is FNV-1a(calendarId:slotStart) — deterministic, so two concurrent callers computing candidates independently agree without shared state.availability.ts:809 / assignment.go
S17evenDistribution=true is expressed as all priorities set to 0, not a separate mode.orderCandidatesByPriorityThenDeterministic
S18Public projection attaches specialist identity and timezone only when the schedule has exactly one opening — pooled services do not leak which provider you will get.schedules/[id]/details/route.ts
S19Timeslot responses carry a generated-at freshness stamp so a stale cached grid is detectable client-side.timeslots-cache.service.ts

4.2 Appointments

#RuleSource
A1Reschedule preserves duration exactly — it is not re-derived from the offering, which may have changed since booking.11-appointment-lifecycle.md
A2Cannot reschedule into the past.same
A3Auto-noshow grace is 30 min after scheduled start; the sweep job runs every 15 min.same
A4Late arrival after auto-noshow must be recoverable. leo permits noshow → inprogress. Under a strict terminal-status rule a new row orphans the generated forms, the video room, and the protocol_id/session_id pair the adherence engine reads — and leaves the auto-noshow permanently in the adherence denominator, penalising a patient who actually attended. See §8.7.same
A5Cancellation captures a free-text reason, capped at 500 chars, behind a promise-based confirm dialog.appointment-management.js
A6Late cancellation is flagged at a 24h threshold.11-appointment-lifecycle.md
A7Unscheduled appointments are a first-class bucketstarted_at IS NULL renders as "Consultație fără programare" with its own tab and summary card, and "Programare inițială" on the portal. Real rows exist.consultatii/listare
A8Specialist overlap on the staff-created path is a WARNING, not a block — deliberately, because appointments can be created manually and because different calendars may share a specialist.11-appointment-lifecycle.md
A9Converted bookings are de-duplicated on the staff calendar so one booking does not render twice.calendar endpoint
A10The actions dropdown shows the exact WhatsApp number and email in the confirmation modal before dispatch — staff verify the destination.consultatii/[id] actions

4.3 Forms & custom fields

#RuleSource
F1Form-slot cardinality: disclaimer multiple, survey multiple, parameters single, analysis single, advice single. report and prescription are NOT in the table — report is generated via a separate skipReport path, prescription has no server-side attach route at all.appointment-forms.js:18-24
F2analysis sets createValues: false — analysis forms deliberately do not pre-create value rows. The mobility-evaluation flow writes them from measurements instead.same, :22
F3Detaching a form must never delete shared profile-level values. leo gets this right; it is easy to get wrong on the platform where auto-fill copies values in.deleteAppointmentForm
F4Required + private is a dead zone. Private fields are omitted from the patient DOM, from FormData, and from the zod required-check — so a required private field can never block a patient submit. Catch it at publish time as a template-authoring error.patient renderer
F5Autosave contract: 1s debounce + flush on blur + dirty ref (blur with no change is a no-op) + per-field spinner + per-field error + "+ add missing entry" affordance.edit-in-place.tsx
F6Field keys are generated {type}_{4 alnum} and are immutable once assigned — PDFs and exports reference them.generateFieldKey
F7Patient date input is three selects, not a date picker — deliberate, and correct for older patients on mobile. But the year range is hardcoded currentYear-100 … currentYear-10, making under-10s unrepresentable. Keep the control, drive the range from config.patient renderer
F8Consent bodies interpolate {{patient.name}} / {{fields.<key>}}; unresolved paths render a neutral placeholder rather than erroring.disclaimer.tsx
F9Global (appointment-null) forms gate every appointment: $or: [appointment.id = X, appointment.id IS NULL]. This is how clinic-wide disclaimers work.getRequiredDisclaimers
F10The blocking gate renders one form at a time, disclaimers before surveys, and the subtree never renders until the queue empties.consultatii/[uid]/layout.tsx

4.4 Documents

#RuleSource
D1Projection algorithm, in order: flatten groups → prune is_private (a private group hides all children) → prune empty values → elide now-empty groups.report.service.ts
D2Age is computed at the appointment date, not today — differenceInYears(started_at, date_of_birth).same
D3Prescriptions show ALL fields to the patient (transparency); reports filter private fields. NOT CARRIED — retired 2026-08-07. A field marked private is staff-only on every document type. leo's per-type audience split is an observation of what leo did, not a rule the platform owes it: is_private exists so a clinician has somewhere to note what the patient is not meant to read, and a promise one document type breaks is no promise.08-document-generation.md
D4A4 geometry: paddingTop 140 / paddingBottom 65 to clear the fixed header and footer. Footer reads "Pagina n / total".default.tsx
D5The prescription layout is the report layout minus "Servicii efectuate" and minus the support box.template diff
D6Postural-analysis images live on the report entity, which forces a hard block: "Este necesar să existe întâi un raport de evaluare." Attach to the appointment instead and remove the dead-end.analiza-posturala
D7Goniometer measurements are written as form values keyed <key>_left / <key>_right / <key>_file.analysis flow
D8Generate/Regenerate wording differs, and the produced artifact downloads immediately.report-actions.tsx
D9"Rapoarte în așteptare" = appointments where ended_at < now() AND no published report, self-scoped when the caller is a specialist. This is what actually drives report completion.ReportsCard

4.5 Team, exports, ops

#RuleSource
T1minicrm_name overrides the display name in outbound CRM payloads: `specialist_name = minicrm_name
T2Presence is throttled to 1 write / 5 min; it is a presence signal, not an access log.user-last-activity.js
T3CSV export prepends a UTF-8 BOM so Excel renders Romanian diacritics — without it Ștefan shows as Å¢tefan.03-api-contracts.md
T4Export columns are caller-supplied with caller-supplied Romanian labels and a per-column type (date/status/text) so enums and timestamps render human-readably. Nested paths (specialist.name, user.patient.name) are inherent — exports are join-shaped.same
T5Export caps: 10,000 rows, 1,000-row batches, 5 requests / 5 min.export.js
T6Upload UX: full-surface click target + "N / M documente încărcate" progress + per-file failure surfacing. leo's completes the bar silently for failed files — fix that.upload-files.tsx
T7Patient document policy copy: upload 12h before the appointment, "nu garantează analiza acestora".portal documents
T8Cross-system propagation runs in setImmediate with per-operation try/catch that only logs. Strapi and the scheduler diverge silently; the repair is "save it again." This whole class disappears on a single-database platform.Strapi lifecycles

5. Foundation conflicts — consolidated

Deduplicated across all five dimensions. Sixteen distinct classes.

#leo assumptionPlatform ruleResolution
C1Everything hangs off a users rowNo users table; users/user_id forbidden (glossary:369-370). principals + humans(principal_id)Subject → patient_profile_id; actor → created_by_principal_id/signed_by_principal_id/generated_by_principal_id; signer → specialists.human_id. Three columns, not one
C2franchise is the tenant; scoping is app-layer middleware; 5 entities exempt; list-GETs onlyorganization_id NOT NULL + RLS on every tenant table + ConnFromContext + P47Rename; RLS on all new tables including junctions and both availability tables. ⚠️ data-model.md Area 4 omits organization_id on specialist_weekly_hours/specialist_schedule_overrides — a doc bug against a hard rule; fix the doc
C3Authorization compares role strings (role.type === 'admin'), role IDs from env vars, nav-only visibilityPer-org permission codes; RLS calls current_app_has_permissionSeed permissions per migration. ⚠️ Verified: zero forms.*/documents.*/appointments.*/specialists.* rows exist in any migration today — they live only in rbac-permissions.md
C4publishedAt is the lifecycle for templates, forms, documents, and consent signatures simultaneouslyP33 explicit machines; P14b immutability; consent needs an auditable signature eventFour mechanisms: form_templates.published+version; forms.status enum; appointment_documents.published; consents ledger row. Default pending — fail-closed
C5Form instances carry no snapshot; template edits rewrite historyP18 + P14bforms.fields JSONB + template_version at creation; appointment_documents.pdf_template_version at generation. The single most important structural fix in the port
C6A form-value middleware silently redirects answers into a shared user-scoped meta_value; answering in one form rewrites every otherAudit with field-level changes; P18 no retroactive mutationExplicit two-step: auto-fill COPIES at creation; save performs a separately audited write-back. Historical instances never mutate
C7meta_field.key globally UNIQUE; template copy clones by id across tenantsRLS; clinical records never cross clinic boundariesUNIQUE (organization_id, entity_type, key). Copy remaps by system_keykey within the target org; dangling cross-org reference fails the copy
C8Hard DELETE of forms, values, reports, patient uploads; no tombstoneSoft delete only; erasure = anonymisation; cmd/check-softdeletedeleted_at + no DELETE RLS policy on clinical tables. Config tables (specialties, calendars, overrides) may hard-delete but must emit audit
C9Nothing immutable — signed consents still PUT-able; sent reports regenerable in placeP14a/P14b; signed forms reject edits with 409409 guard at handler and service; _versions tables get no UPDATE/DELETE policies; regeneration produces a new row, never an in-place file swap
C10Availability lives in a second system with fire-and-forget one-way syncSingle database; no silent-failure pathsDelete the sync concern entirely. specialist_weekly_hours + specialist_schedule_overrides are the source of truth. External calendar sync, if wanted later, is Cat B via organization_integrations
C11Bookability gated on a lazily-provisioned intakes_opening_id; specialists without one are silently undroppable from every rosterNo silent-failure states; P23 scheduling_timezone NULL = unbookableDerived + explicit: scheduling_timezone IS NOT NULL AND scheduling_active AND EXISTS(weekly_hours). Expose a computed bookable flag with a machine-readable reason, surfaced in the roster UI
C12Deactivation is a non-atomic two-write toggle (user.blocked + publishedAt=null) leaving the scheduler liveOne column, one transactionscheduling_active = false removes the specialist from availability computation by construction. Keep strictly separate from humans.blocked — revoking at Clinic A must not lock them out of Clinic B
C13Hold/cooldown/SSE keys carry no org dimension; clientId caller-supplied and regenerated after each bookingCache scope must match data visibility (P42/P45); rate limits key off IPKey/PrincipalKeycache.OrgResource(orgID, ...) on every hold/stream/cooldown key. Booking client id = server-signed HttpOnly cookie, persisted to appointments.booking_client_id. Cooldown keys on (org, calendar, patient_profile_id) or (org, calendar, hashed IP+email)
C14PDFs rendered client-side by @react-pdf/renderer in the staff browser, images by remote URLpdf.Renderer capability; PDFs self-contained with base64-embedded signatures; P39 egressServer-side capability registered via capabilities.WrapInternal; images fetched server-side and base64-inlined; SOUP row in the same PR
C15Lists cap at 50 with no pagination control; pickers load everything and filter client-side; pageSize:-1 on the calendar endpoint; sort on unindexed createdAtProduction Scale: server-side pagination (default ≤50, cap 500), COUNT(*)-derived counts, async typeahead, indexed filter columnsapiquery from the first commit; AsyncMultiSelectFilter; GIN trigram + immutable_unaccent on every searchable column. Non-negotiable at 20k users
C16Vocabulary: franchise, Opening, Schedule, Intake, appointment_template, speciality, meta_field, prescriptionGlossary wins every naming dispute; PRs without glossary entries are incompleteFull mapping in the docs PR (§2.3 Step 0). ⚠️ prescription genuinely collidesprotocols.kind='prescription' is live in production meaning a specialist-assigned exercise program; leo's is a rețetă-medicală PDF. ⚠️ service_plans → enrollments is also blockedenrollment is taken by protocols.kind='enrollment'

6. New dependencies requiring SOUP rows

apps/docs/reference/soup.md currently has 97 rows; cmd/check-soup fails the build on any manifest dep without one.

Required

DepWhyRisk tierNotes
PDF engine — chromedp/headless-shell, Gotenberg, or Go-nativeF6.2 pdf.RendererHighDrives ECS task shape, image size, memory profile, MDR posture. See §8.8
@dnd-kit/modifiersOnly if cross-container drag is needed (grouped form-builder fields)MediumWould be a second dnd-kit import site; packages/ui/src/components/sortable-list.tsx documents a one-import-site rule keeping the SOUP surface at 3 rows. Port into packages/ui to preserve it

Conditional

DepConditionNotes
libphonenumber-jsIf E.164 validation is needed for the 20k migrationReal need, not convenience. restartix-leo-dashboard/core/utils/phone.ts
Calendar grid library (FullCalendar or similar)If the cockpit is not hand-rolled5 packages in leo + a licensing question on some views. Alternative: react-day-picker (already SOUP'd) + CSS grid. Decides how much cockpit code is reusable at all — see §8.9
cmdkOnly if leo's combobox.tsx is ported rather than extending AsyncMultiSelectFilterPrefer extending the existing component

Already inventoried — no new rows: swr, @dnd-kit/{core,sortable,utilities}, react-day-picker, date-fns, recharts, zod, radix-ui, go-redis/v9.

Explicitly NOT to add (each would be a regression): axios (bypasses the P43 undici Agent), ioredis (Go API owns Redis per P45), qs (Strapi filter syntax only), jotai, jose (Clerk owns JWT), slate ×4 + is-hotkey, @react-pdf/renderer, @strapi/blocks-react-renderer, lodash.merge/lodash.debounce, date-fns-tz, nanoid (use crypto.randomUUID()), react-confirm, vaul, filesize, short-unique-id, @sindresorhus/slugify.

Latent hazard: leo's package.json is not a reliable statement of its dependency surface. nanoid and is-hotkey are imported but undeclared (yarn-hoisted phantoms); drizzle-orm and @tanstack/react-table are declared but never imported. Verify every import before assuming a dep exists.


7. Anti-pattern guardrails — what the port must not carry

Grouped by severity. The DB catalogue is drawn from strapi_db.sql (94 tables, pre-data section) and verified by direct grep.

Class 1 — Structural defects the platform already forecloses

#leoGuardrail
G1Exactly one NOT NULL in 94 tables, and it is id. required: true in schema.json is app-layer only, bypassed by strapi.db.query, admin import, and direct DB accessEvery semantically-required column is NOT NULL in DDL; enums get CHECK or a Postgres enum
G2Zero FK constraints. Zero indexes. Zero uniques. Verified: grep -c "REFERENCES|CONSTRAINT|CREATE INDEX|pkey" = 0FKs with explicit ON DELETE; indexes on every filtered/sorted column
G352 of 94 tables are _links join tables. appointments alone has 11 satellites. Every one is a 1:1 or N:1 that should be an FK columnFK column for 1:1/N:1; join table only for true M:N, with composite PK
G4files_related_morphs (related_id, related_type) — untyped polymorphic attachment carrying patient medical documents. Orphans structurally undetectable; ordering is double precisionTyped FK + organization_id on a real table
G5deleteAppointmentForm deletes N value rows in a for loop, then the report, then the form — no transactionOne transaction per multi-row mutation
G6Zero deleted_at anywhere; hard-deleted clinical records and patient uploadsdeleted_at + no DELETE RLS policy
G7timestamp without time zone everywhere except two columns; timezone carried as free-text varcharTIMESTAMPTZ + IANA validated against tzdata
G8EAV clinical data across three competing field stores (form_values, meta_values, components_form_fields); reading one questionnaire is a 10-table join; multi-select packed as `a
G9franchises.patient_meta_* varchar columns whose values are string keys into the EAV store — unvalidated per-tenant pointersNative patient_profiles columns + the platform custom-fields design

Class 2 — Security defects

#leoGuardrail
G10patients.password stores the plaintext generated password, has no private flag, serialises in /api/users/me?populate=*, reaches the browser, and is interpolated into rendered consent text as {{patient.password}}No recoverable credential column ever. The importer discards it — not into a column, a log, or an export. The variable namespace becomes a server-side allow-list from the classification registry
G11Custom routes ship policies: [] — cancel, reschedule, attach-forms, from-template, and POST /patients/:id/impersonate are all unguarded. Exactly one route in the API has a policyRequirePermission + RLS + P47 on every route. There is no exempt list
G12Tenant filter applies to list GETs only; findOne and all writes are unscoped. Five entities — meta_value, form_value, patient, report, specialist — explicitly exempted "filtered by other means." With sequential integer PKs, any authenticated patient can walk /api/forms/1..N across every clinicRLS at the database layer. Exemption is not expressible
G13/api/video-proxy?url=<anything> — open SSRF proxy with Access-Control-Allow-Origin: *, buffering whole videos via arrayBuffer()Never. Signed, scoped, expiring media access
G14/videocall/[uid] is unauthenticated; createToken(roomName, "0", "Guest") mints a Daily token for whatever room name is in the URL. The appointment uid is a non-expiring capability URL to a live medical consultationRoom names derived from a server-side secret, never URL-visible. Authenticated principal binding
G15Raw unsigned, never-expiring media URLs for uploaded medical documentss3.PresignRead (15 min) + ValidateOrgScope; Block Public Access
G16Cross-tenant template copy/move authorised by role.type !== 'superadmin'; move re-parents historical clinical documents across a tenant boundaryP49 platform-tier + clone. Never move. Break-glass for genuine cross-org reads
G17ENCRYPTION_KEY in an env var; encryption via a single ORM lifecycle hook on a varchar, which double-encrypts on echo-backKMS-held keys; BYTEA via internal/core/crypto; cmd/check-classification enforced

Class 3 — Audit & compliance

#leoGuardrail
G18There is no audit log. audit-service.js (400+ lines) is never registered in config/middlewares.js — and could not work if it were: it queries audit_organization_id/audit_organization_api_key on franchise, neither of which exists in the schema, so it silently skipsaudit_log in-transaction, append-only, monthly-partitioned, cmd/check-audit-coverage-enforced
G19The one live path is fire-and-forget HTTP to an external service, 3 retries then give up. A comment reads // Could write to dead letter queue here — it doesn'tLocal synchronous first. An audit path that can silently no-op is not an audit path
G20user_activities has updated_at/updated_by_id — mutable, not append-only; data is unstructured text; no actor type, no diff, no status codeaudit_log is the transition history. No bespoke event table
G21A patient's consent signature is publishedAt = now() set by an ordinary PUT — no signer, no IP, no UA, no version pin, no hash — and the same PUT can unset itconsents ledger: purpose_version, granted_by_principal_id, granted_via_ip, source, withdrawal columns, partial-unique active grant
G22CNP stored as plaintext meta_value.value and printed on every report and prescriptionCLOSED 2026-08-07. pii_regulated → encrypted BYTEA on patient_profiles + a blind index for search; both plaintext routes refused by trigger; egress target patient_document. No per-template opt-in — the field's presence is the declaration
G23Report prints full demographics unconditionally for any patient at any franchiseResolve through the renderer's own org-scoped query — the portable profile is readable only through a patients row at the printing org — not as a template-authoring convention
G24No rule that production data never reaches staging. A 20k-patient prod snapshot restored into staging to debug the migration turns a processor into a breach reporterAdd a hard rule to production-launch-readiness.md + a seeded synthetic generator (@example.com per RFC 2606, +40700000XXX Romanian test range)

Class 4 — Frontend

#leoGuardrail
G25refreshInterval: 60000 as a global SWR default — 6 cards = 6 req/min/tab foreverViolates the platform's no-polling rule. revalidateOnFocus/onReconnect + the SSE hub
G26cache: "no-cache" hardcoded on every server fetchP42 tagged unstable_cache + P43 dispatcher + P45 cache-aside
G2752 bare mutate() calls; zero optimistic updates; loading derived as !error && !data (wrong with keepPreviousData)useOptimistic with auto-revert; validate before optimistic apply; SWR's own isLoading
G28useState(field.value) seeded from props throughoutP48 — use the prop, useOptimistic, or useServerSyncedState
G29269 of 642 files carry hardcoded Romanian; error copy embedded in the data layer (patient.repository.ts error maps) and in Error subclass defaults; support codes baked into message bodies ("(PR-0006)")next-intl catalogs; packages/ui stays locale-free (strings as props); codes as a separate prop. pnpm check-i18n enforces
G30The hold reducer is duplicated three timesBuild useHoldsStream + useHolds once in packages/ui
G31Filters transported as base64(uriencoded(JSON)) and evaluated client-sidePlain readable query params; apiquery allow-lists; server-side evaluation

8. Open decisions, ranked by downstream blocking

8.1 — Does the F2 stand-in ship, under what name, and with what boundary? — SETTLED 2026-08-02: option (a)

Three in-scope features have NOT NULL FKs into a catalog entity that is out of scope (§2.1). Options: (a) ship the F2.1-only offerings stand-in as proposed; (b) make calendars.offering_id/appointments.offering_id nullable with tracked debt; (c) pull F2.1 formally into scope.

Settled 2026-08-02 — option (a), named offerings from day one: catalog identity + offering_specialists + offering_forms, with no pricing, no plans, no products, no purchase path. F2.2 / F2.3 stay deferred. This unblocks migrations 000041000048 (F3.4, F4.2, F5.1, F5.4).

Sub-decision, also settled: service_plans → enrollments was blocked — enrollment is taken by the shipped protocols.kind='enrollment' and the protocols.enroll permission. The reserved name is now offering_packages (glossary.md → Forbidden terms). The name is reserved; the concept stays deferred with F2.2.

8.2 — Override scoping — SETTLED 2026-08-05: calendar_id UUID NULL

The three platform sources disagreed (Area 4 has no scope column and sketches a JSONB on the junction; the specialists feature spec says appointment_type_id, a table in no architecture doc). The live system settles what clinics actually do: scheduleOpeningOverrides.scheduleId is NOT NULL (restartix-intakes/core/db/drizzle/schema.ts:171-196), and the predecessor openingOverrides table was explicitly removed in favour of it — a deliberate migration toward per-schedule scoping, with production rows.

Settled 2026-08-05 — specialist_schedule_overrides.calendar_id UUID NULL, NULL = all calendars. Preserves both capabilities: "I'm on vacation" is one row, and "block Physio next Tuesday but keep Nutrition open" is still expressible. leo's NOT NULL was the alternative and would have made a vacation N rows that can drift apart. The override_weekly_hours JSONB is struck. Requires correcting data-model.md Area 4 and the specialists feature spec.

8.3 — Is CNP required on generated documents? — SETTLED 2026-08-02, BUILT 2026-08-07

The single most expensive field in the port. "Yes" forces an encrypted patient_profiles.national_id_encrypted BYTEA, a new pii_regulated egress target for the renderer, per-template opt-in, and a regulated-PII surface the platform does not have today. "No" removes that surface entirely.

leo prints it on every report and prescription, so someone presumably relies on it — but rehabilitation reports are not obviously a CNP context.

Settled 2026-08-02 — yes. BUILT 2026-08-07, and the per-template opt-in flags were dropped along the way: a national_id question's presence, or the cnp field's selection on a block, already declares the intent. One home: pii_regulated → encrypted patient_profiles.national_id_encrypted BYTEA via internal/core/crypto, stored once on the patient-owned profile, never duplicated per-org. Never in the generic value store — a custom_field_values.value TEXT column can never legally hold a CNP, so field_type='national_id' routes to the dedicated encrypted column or is rejected outright (this is what 000042 needed settled). Egress is explicit: a data-classification.md entry with an egress target for the PDF renderer, which calls classification.AllowedFor rather than hand-building the field list (P39). Reads are permissioned and audited on the reveal endpoint.

8.4 — Are specialties per-org or platform-global? — SETTLED 2026-08-02: per-org

features.md F1.1 said verbatim "per-org or global — decide before migration." leo, data-model.md Area 2, and the specialties feature spec all say per-org. Global would have enabled cross-org anonymised analytics and a platform-seeded starter catalog.

Settled 2026-08-02 — per-org. specialties.organization_id UUID NOT NULL, UNIQUE (slug, organization_id). Not worth diverging from the live system the data migrates out of. 000040 is unblocked.

8.5 — humans has no name column. BLOCKS the F1 roster UI

Verified (000002:292-322). specialists.name covers specialists; admin and customer_support staff have no display name. Options: add humans.name TEXT NULL in 000040 with a classification entry, or accept email-as-display-name on the roster. A genuine foundation gap surfaced by the port, not a feature preference.

8.6 — Route topology: /team/{principalId} or /team + /specialists/{id}?

Calendar-only specialists (human_id NULL — an explicitly designed state) have no principal to route on, which argues for the split. leo's single-page shape argues for fewer surfaces. Blocks: the F1 UI wave and every deep link into it.

8.7 — Terminal-status policy vs. late arrival. Silent adherence corruption if unresolved

The substrate makes noshow terminal; leo permits noshow → inprogress. The cron marks noshow at +30 min; the patient walks in at +35. Under a strict terminal rule the specialist must create a new row, orphaning the generated forms, the video room, and the protocol_id/session_id pair the adherence engine reads — and leaving the auto-noshow permanently in the adherence denominator.

Recommendation: permit noshow → inprogress only when the noshow was set by the system actor and within a bounded window, audited as a correction. This undoes a machine's guess, not a human's decision. Related: make the grace period organization_settings.noshow_grace_minutes (leo already parameterises it).

8.8 — PDF engine + sync-vs-queued. BLOCKS F6.2, SOUP, and ECS sizing

chromedp/headless-shell sidecar (~150 MB image, full CSS fidelity, matches the browser preview) vs Gotenberg (cleaner isolation) vs Go-native (no Chrome, but the block editor's fidelity promise collapses). Separately: sync matches leo's UX; queued reuses the proven exercise_renders claim/attempts/backoff/dead-letter shape.

Profile CPU on Fargate explicitly — the media service was already burned once by ffmpeg reading host cores instead of the cgroup.

8.9 — Calendar grid — SETTLED 2026-08-05: hand-rolled, no drag in v1

Decides how much of leo's cockpit is reusable at all — its capacity-lane CSS only works against FullCalendar's DOM.

Three things measured before deciding, two of which changed the shape of the question:

  1. The licensing objection is void. All five plugins leo uses — core, daygrid, timegrid, interaction, react — are MIT. The licence question applies only to the premium resource/timeline plugins, which leo does not use.
  2. It is two surfaces, not one. Only three files in leo touch FullCalendar. availability-calendar.tsx (351 lines) is a month grid with click and drag-select — and packages/ui already ships react-day-picker@9, which does mode="range" and day modifiers natively, so hand-rolling it is less code. evenimente/calendar.tsx (431 lines) is the real question: a time-grid with slotDuration, businessHours, nowIndicator and editable drag-to-move.
  3. FullCalendar cannot render a named timezone unaided. namedTimeZonedImpl defaults to null in core — out of the box it does 'local' or 'UTC' only, and IANA zones need @fullcalendar/luxon3 plus luxon. The platform computes in a resolved scheduling timezone (P23) that is deliberately not the browser's, so this is not optional: it is 7 SOUP rows, not 5.

Settled — hand-roll both. The availability editor rides the already-shipped react-day-picker; the cockpit time-grid is hand-rolled at roughly 400 lines (layout ~200, overlap packing ~100, availability shading ~50, now indicator ~20).

The decisive point is the shading. businessHours takes a recurring weekly shape and has no concept of a date override that replaces a day (rule S1) — which is precisely why leo carries capacity-lane CSS hacking FullCalendar's DOM. Adopting the library would mean fighting it on the single most important visual in the cockpit while inheriting a workaround this document already flags as non-portable.

Drag-to-move is deliberately NOT in v1, and the reason is not effort (~250 lines). §8.13's reschedule semantics are undecided — mutate scheduled_at in place versus cancel + new row — and building drag-to-reschedule against an unsettled contract means building it twice. leo's own drag has no confirmation step. v1 is click-to-select and click-to-open; drag lands once the semantics do.

8.10 — Patient self-booking at launch? — SETTLED 2026-08-05: not in F4

The substrate treats it as an org capability with UI gating, but shipping the public path exposes unauthenticated hold + book endpoints and their abuse surface at launch. leo's real creation surface is staff-side; patient bookings arrive from the external Intakes public UI.

Settled 2026-08-05 — F4 is org-scoped only. Availability and holds ship behind auth, driving the clinic cockpit. The unauthenticated /v1/public/ path lands with F5.4, where appointments exist to be created and the booking-client cookie + cooldown machinery has something to attach to. Holds are still built in F4 — staff booking needs them.

8.11 — Legacy migration: source of truth and fidelity

Now tractable given §0.1. Concretely: (a) Do future bookings and specialist availability migrate from the Intakes Postgres (schema readable at core/db/drizzle/schema.ts), or do clinics re-enter availability? (b) Preserve leo's random-nanoid slugs verbatim to keep live public URLs ({speciality.slug}/{template.slug}) working? (c) Historical forms have no snapshots — import with the template's current state (accepting history was already rewritten) or as read-only "legacy answers" with a synthesized snapshot and a visible provenance marker? The GDPR/medical-record posture differs sharply. (d) Who signs off that a repaired pipe-delimited answer is still the patient's answer?

8.12 — Cross-org double-booking

specialists.organization_id NOT NULL gives a specialist at two clinics two independent profiles and schedules, so P40's "cannot be in two places at once" holds only within an org. leo's constraint is on opening_id, which is org-scoped too — so leo has the same limitation. Accept as documented, or add a cross-org conflict check keyed on specialists.human_id (a cross-tenant read needing anonymised/break-glass treatment).

8.13 — Lower-blocking, but decide before the relevant migration

  • Reschedule semantics — mutate scheduled_at in place with audit (leo, preserves duration) vs cancel + new row (forensically cleaner, consistent with the terminal-cancel rule)
  • Late-cancellation threshold — org-configurable organization_settings.late_cancellation_hours (default 24h) or a platform constant
  • Legacy cancelled → which of three buckets? leo carries no attribution signal beyond free text. Mapping all to cancelled_by_clinic avoids retroactively penalising 5k+ migrated patients but overstates clinic-caused cancellations in fill-rate reporting
  • Does holding a specialty GATE roster assignment? leo built the enforcing component (select-specialist.tsx) and abandoned it unimported. Either enforce with a 422, or declare specialty membership taxonomic-only in the glossary
  • Timezone-change policy when weekly hours exist — block-with-migrate (the spec) or allow-with-preview. leo silently shifts real availability, which is the one behaviour that is definitely wrong
  • prescription naming collision — rename the document type or qualify it in the glossary. Must be settled before the F6 migration
  • Audit granularity for autosave — one coalesced row per batched flush (with the changed-key diff) vs one per field. At 20 fields × 20k patients this is the difference between a usable and an unusable audit table
  • is_required enforcement point — per-field on save (breaks autosave) or at the pending→completed transition (allows indefinite in_progress with gaps, which matters for the patient wall's "is this done" query)
  • Break-glass review artifactapps/docs/features/audit/compliance.md:47 asserts "review within 24 hours, two approvers" but 000011_break_glass.up.sql has no reviewed_at/review_notes/opened_from_ip. An asserted control with no mechanism. Note: 000011 is applied to prod, so this needs the catch-up-DDL pattern (infra/scripts/000023-skip-note-prod.sql)
  • CSV export as a first-class surfacegrep -n "export" apps/docs/openapi.yaml returns nothing. It is in daily clinic use in leo. Treat as a scope question, not a slip-in; if in, columns come from classification.AllowedFor(table, "bulk_export"), never a hand-rolled list
  • Locations at launch — build the location_id NULL columns per P40 (yes), but does the September UI expose a location picker?
  • Is F8 segments in September scope? The filter-by-form-answer dialog is in daily staff use; removing it is a visible regression at migration. But segments is a whole subsystem

9. Immediate next actions

  1. Docs reconciliation PR, no code. Fix appointments-substrate.md (§0.3, four defects — one of which fails make check). Add organization_id NOT NULL to both availability tables in data-model.md Area 4. Rename appointment_typescalendars across apps/docs/features/scheduling/api.md and all six go/*.go reference files before anyone copies the losing name into a migration. Add glossary entries: Specialist, Specialty, Offering, Calendar, Hold, Weekly Hours, Schedule Override, Booking Client ID, two-phase booking identity, Form Template, Form Instance, Custom Field, System Field, Form Slot. Add Opening→Specialist, Intake→Appointment, Schedule→Calendar, franchise→organization to the Forbidden-terms table. Resolve the two naming collisions (prescription, enrollments).

  2. Answer §8.1 and §8.4 — nothing can be written before these.

  3. Extract the non-code assets now — independent of build order and the genuinely irreplaceable part: the Romanian label set and A4 geometry from default.tsx, and the two clinician-authored annex documents (nutritional.tsx, nutritia-durerii.tsx, 1,183 lines of clinical prose).

  4. Snapshot the Intakes production schema and data shape while the service is live. §0.1 makes this tractable today; it will not stay that way. This is the only part of the dimension with irreplaceable production data.

  5. Write the availability-engine test suite — differential against restartix-intakes/core/services/availability.ts. This can start immediately, in parallel with the docs PR, and is the highest-leverage de-risking available.