# Proyecto OPI — Platform Documentation

**Version:** 1.2.0
**Last updated:** 2026-08-01
**Status:** Clinical demo / pre-production
**File:** `public/docs/proyecto-opi-platform.md` (downloadable from the footer of the landing page and the clinician portal)
**Owners:** LLAMALÍTICA · IISGS · Duponte · FPGMX
**Hosting:** European Union · TLS 1.3 · AES-256 at rest
**Compliance target:** GDPR / LOPD-GDD, EMA GCP, ISO 27001, ISO 13485 (software as medical device track)

---

## 1. Executive summary

Proyecto OPI is a multicentre pharmacogenomic platform for the management of **cancer pain treated with opioids**. It unifies four operational layers in a single web + mobile responsive application:

1. **eCRF** — structured clinical data capture during the in-clinic visit.
2. **ePRO with voice** — patient-reported outcomes captured at home through natural speech, transcribed and mapped to standard clinical ontologies.
3. **Clinical Decision Support (CDS)** — CPIC-aligned pharmacogenomic recommendations cross-referencing genotype (CYP2D6, OPRM1, COMT) with clinical phenotype.
4. **Interoperability** — HL7 v2 / FHIR R4 channels per centre through MIRTH Connect, REDCap bidirectional sync, and FPGMX genotyping ingestion.

The platform is bilingual (Spanish / English) with automatic locale detection and manual override, and is designed to be operated by four clinical roles: **investigator, monitor, data manager, pharmacist**, plus the **patient** on the ePRO surface.

---

## 2. Scope and clinical context

### 2.1 Study
Multicentre observational + interventional study on the pharmacogenomics of opioid response in oncologic pain. Primary variables:

- Pain intensity (VAS 0–10, BPI severity and interference).
- Adverse drug reactions graded per **CTCAE v5.0**.
- Quality of life (**EORTC QLQ-C30**).
- Concomitant medication and daily opioid dose (morphine equivalent).
- Genotypes: **CYP2D6** (metaboliser class), **OPRM1** (A118G), **COMT** (Val158Met).
- Patient-reported symptoms mapped to **HPO** and **SNOMED-CT**.

### 2.2 Actors
| Actor | Surface | Main tasks |
| --- | --- | --- |
| Investigator | `/app/*` | Enrol patients, fill eCRF, review alerts |
| Monitor | `/app/*` | Read-only audit, query resolution |
| Data manager | `/app/*` | Export, REDCap sync, quality control |
| Pharmacist | `/app/cds` | Trigger CDS, review recommendations |
| Patient | `/epro` | Voice diary, pain and sleep scoring |

---

## 3. Architecture

### 3.1 High-level

```text
┌────────────────────────────────────────────────────────────────┐
│                     Browser (responsive)                       │
│   Landing · /login · /app/*  (clinicians)                      │
│   /epro                       (patients, mobile-first)         │
└───────────────┬───────────────────────────────┬────────────────┘
                │ HTTPS                         │ HTTPS
                ▼                               ▼
        TanStack Start (SSR)             Static assets (Vite)
                │
                ▼
   Edge server routes (/api/*)
   ├── /api/stt              → Lovable AI Gateway (Whisper family)
   ├── /api/map-symptoms     → Lovable AI Gateway (Gemini flash)
   └── /api/cds              → Lovable AI Gateway (Gemini flash)
                │
                ▼
        Lovable AI Gateway
   (STT · LLM · rate limiting · auth)
```

### 3.2 Stack
- **Runtime:** TanStack Start v1 on Cloudflare-style edge Worker (nodejs_compat).
- **UI:** React 19, Tailwind v4 (semantic tokens in `src/styles.css`), shadcn primitives.
- **Routing:** File-based (`src/routes/`), auto-generated route tree.
- **State:** `useSyncExternalStore` on top of a `localStorage`-backed store (`src/lib/store.ts`) for the demo tier. Ready to be replaced by Lovable Cloud (Postgres + RLS) without touching UI components.
- **i18n:** `i18next` + `react-i18next`, ES default, EN alternative (`src/locales/{es,en}.ts`). Language is resolved from a cookie on **both** server and client so SSR HTML and the first client render always match.
- **AI:** Lovable AI Gateway called server-side; `LOVABLE_API_KEY` never reaches the browser.

### 3.3 Directory map
```text
src/
├── routes/
│   ├── __root.tsx           # HTML shell, providers, error boundaries
│   ├── index.tsx            # Public landing
│   ├── login.tsx            # Role-based sign-in (demo)
│   ├── epro.tsx             # Patient voice diary
│   ├── app.tsx              # Clinician layout (sidebar + tabbar)
│   ├── app.dashboard.tsx    # KPIs and recruitment
│   ├── app.patients.tsx     # Patient list
│   ├── app.patients.$id.tsx # Patient detail (tabs)
│   ├── app.ecrf.tsx         # Structured visit form
│   ├── app.cds.tsx          # CDS console
│   ├── app.integrations.tsx # MIRTH / REDCap / FPGMX
│   └── api/
│       ├── stt.ts           # Speech-to-text edge endpoint
│       ├── map-symptoms.ts  # NLP → HPO / SNOMED-CT (+ terminology validation)
│       └── cds.ts           # CPIC recommendation engine
├── components/              # Sidebar, VoiceRecorder, LangToggle, shadcn UI
├── lib/
│   ├── ai-gateway.server.ts   # Lovable AI provider factory
│   ├── terminology.server.ts  # FHIR CodeSystem/$lookup SNOMED-CT validation
│   ├── integrations.server.ts # Real REDCap + MIRTH health probes
│   ├── lang.ts                # Isomorphic language resolution (cookie)
│   ├── lang.server.ts         # Reads the language cookie from the request
│   ├── store.ts               # Local demo store (stable snapshots)
│   └── utils.ts
├── locales/{es,en}.ts       # Full translation catalogues
├── i18n.ts                  # i18next bootstrap
└── styles.css               # Tailwind v4 + design tokens
```

---

## 4. Feature reference

### 4.1 Landing (`/`)
- Bilingual hero, features grid, dual CTA (clinician / patient), language toggle.
- Footer links legal, compliance and this documentation download.

### 4.2 Clinician portal (`/app/*`)

#### 4.2.1 Dashboard
- KPI cards: active centres, enrolled patients, voice entries (7 d), open CDS alerts.
- Recruitment by centre (aggregated from local store; ready to bind to a live SQL view).
- Recent visits and recent alerts lists.

#### 4.2.2 Patients
- Search, register (pseudonymised code), tabular list with age, sex, opioid, dose, genotype snapshot, last visit.
- Detail view with tabs: Overview · Visits · ePRO diary · Genomics · CDS alerts.

#### 4.2.3 eCRF (`/app/ecrf`)
Structured sections:
- **Visit:** patient, date, visit type.
- **Pain:** VAS (0–10), BPI severity, BPI interference.
- **Medication:** current opioid, daily dose (mg morphine-equivalent), concomitant meds.
- **ADR:** CTCAE grade, textual description.
- **QoL:** QLQ-C30 (0–100).

Every save appends an immutable audit-trail entry with actor, timestamp and payload hash (client-side hash in demo, moves to server signature in Cloud tier).

#### 4.2.4 CDS (`/app/cds`)
Triggers `/api/cds` with `{ patient, genotype, phenotype, currentOpioid, dose }`. The LLM returns:
- `recommendation` (free text, ES/EN following UI locale).
- `rationale` (CPIC guideline reference).
- `severity` in `{ info, caution, critical }`.

Recommendations are persisted in the store and surface in the dashboard alerts feed.

#### 4.2.5 Integrations (`/app/integrations`)
- **MIRTH channels per centre** with `live | testing | pending` status.
- **REDCap sync** timestamp + on-demand sync button (hook point for a scheduled job).
- **FPGMX genotyping** ingestion status.

### 4.3 Patient ePRO (`/epro`)
Mobile-first single screen:
1. Warm bilingual greeting keyed by pseudonymised code.
2. **Voice capture** via `MediaRecorder` → `POST /api/stt` (multipart audio) → transcript.
3. Transcript sent to `POST /api/map-symptoms` → HPO + SNOMED-CT chips.
4. Manual sliders for pain now (0–10) and sleep quality.
5. Submit persists to store and shows a recent-entry history.

Graceful fallback when microphone permission is denied (text area).

---

## 5. AI edge endpoints

All endpoints are TanStack server routes running in the edge Worker. The `LOVABLE_API_KEY` secret is read inside the handler.

### 5.1 `POST /api/stt`
| Field | Type | Notes |
| --- | --- | --- |
| `file` | `Blob` (multipart) | `audio/webm`, `mp4`, `mp3`, `wav` |
| `language` | `string` | ISO code, default `es` |

Response: `{ "text": string }`. Underlying model: `openai/gpt-4o-mini-transcribe`.

### 5.2 `POST /api/map-symptoms`
Request: `{ "text": string, "language": "es" | "en" }`.
Response:
```json
{
  "hpo":    [{ "term": "Neuropathic pain", "code": "HP:0100749" }],
  "snomed": [{ "term": "Cancer pain",      "code": "278860009" }]
}
```
Model: `google/gemini-3-flash-preview`. Output is guard-parsed (regex-extracted JSON) and capped to 6 items per list.

### 5.3 `POST /api/cds`
Request: patient snapshot including `genotype`, `phenotype`, `currentOpioid`, `dose`, `locale`.
Response: `{ recommendation, rationale, severity }`. Model: `google/gemini-3-flash-preview` with a CPIC-anchored system prompt.

### 5.4 Failure modes
- Missing key → `500 Missing LOVABLE_API_KEY`.
- Upstream failure → surfaces status + body from gateway.
- Malformed JSON from LLM → safe empty payload, never a 500 to the client.

---

## 6. Data model (demo tier)

Stored under key `opio-gen:v1` in `localStorage`:

```ts
type DB = {
  patients: Patient[];
  visits:   Visit[];
  eproEntries: EproEntry[];
  cdsAlerts: CdsAlert[];
  audit:    AuditEntry[];
};
```

Selectors return derived arrays; snapshots are memoised so `useSyncExternalStore` remains referentially stable (fix for React 19 update-depth loop).

### 6.1 Migration path to Lovable Cloud
1. Enable Lovable Cloud (Postgres + Auth).
2. Create tables mirroring the `DB` shape.
3. Add `GRANT` + RLS policies per role (see `<user-roles>` skeleton).
4. Replace `useStore()` selectors with `useSuspenseQuery(queryOptions)` inside route loaders.
5. Move `audit` writes into a `SECURITY DEFINER` function to prevent tampering.

---

## 7. Security and compliance

- **Pseudonymisation:** patients are keyed by a short opaque code; no direct identifiers stored.
- **Transport:** HTTPS enforced end-to-end (Lovable hosting + gateway).
- **At rest:** AES-256 (browser store is demo-only, not for PHI).
- **Secrets:** `LOVABLE_API_KEY` server-only, injected via environment; never bundled.
- **Roles:** UI role gate today; production tier must store roles in a dedicated `user_roles` table with a `has_role()` `SECURITY DEFINER` function — never on the profile row.
- **Audit trail:** append-only per visit, per CDS invocation, per REDCap sync.
- **Data residency:** EU-only hosting for both application and AI Gateway calls.

---

## 8. Internationalisation

- Default: Spanish (`es`). Alternative: English (`en`).
- **Resolution (SSR-safe):** the active language is stored in the `opi_lang` cookie and read isomorphically — on the server from the request headers (`src/lib/lang.server.ts`) and on the client from `document.cookie` (`src/lib/lang.ts`). The root route loads it (`loader` in `src/routes/__root.tsx`) and applies it before the first render, so the server-rendered HTML and the hydrated tree are identical.
- **Why this matters:** the previous `localStorage`-only detection produced React hydration mismatches ("server rendered text didn't match") and, in the worst case, raw translation keys (`brand.name`) flashing on screen. Any new language-dependent value must come from this loader, never from a browser-only read during render.
- **Toggle:** `LangToggle` writes the cookie (plus a `localStorage` mirror), calls `i18n.changeLanguage` and invalidates the router so subsequent SSR responses use the new language. `<html lang>` is kept in sync.
- **Missing keys:** `parseMissingKeyHandler` humanises any key that fails to resolve, so a missing string degrades to readable text instead of a technical identifier.
- All strings live in `src/locales/{es,en}.ts` under a nested namespace; add a language by cloning the file, registering it in `src/i18n.ts` and extending `normalizeLang` in `src/lib/lang.ts`.

---

## 9. Design system

- Tailwind v4 with `@theme` tokens in `src/styles.css` (OKLCH palette, semantic tokens: `background`, `foreground`, `primary`, `accent`, `sidebar-*`, `destructive`).
- Utility classes: `.gradient-hero`, `.card-clinical`.
- Dark-mode ready via `@custom-variant`.
- Components composed from shadcn primitives — never hardcode colours, always tokens.

---

## 10. Operational runbook

### 10.1 Local run
```bash
bun install
bun run dev            # Vite + TanStack Start
```

### 10.2 Environment
| Variable | Scope | Purpose |
| --- | --- | --- |
| `LOVABLE_API_KEY` | server | AI Gateway access (STT, symptom mapping, CDS) |
| `TERMINOLOGY_SERVER_URL` | server | FHIR terminology base URL for SNOMED-CT `$lookup` |
| `TERMINOLOGY_SERVER_TOKEN` | server | Bearer token for the terminology server (optional if public) |
| `REDCAP_API_URL` | server | REDCap API endpoint (`https://…/api/`) |
| `REDCAP_API_TOKEN` | server | REDCap service-account token |
| `MIRTH_CHANNELS` | server | JSON array `[{"center":"…","hl7Url":"…","fhirUrl":"…"}]` probed for health |
| `VITE_*` | client | Any public config (none required today) |

Without these variables the platform does **not** fake success: the integrations dashboard reports `not configured` and SNOMED-CT codes stay tagged `unvalidated`.

### 10.3 Deployment
- Publish via Lovable → project served at `halo-voz-cerebro.lovable.app`.
- Preview endpoint: `id-preview--<uuid>.lovable.app`.
- Stable external URLs for webhooks / cron: `project--<id>.lovable.app` and `project--<id>-dev.lovable.app`.

### 10.4 Observability
- Client errors: `reportLovableError` in `src/lib/lovable-error-reporting.ts` (root error boundary).
- Server errors: `console.error` inside each handler (visible in server function logs).

---

## 11. Roadmap

### 11.1 Core platform sprints

| Sprint | Goal | Acceptance criteria |
| --- | --- | --- |
| S1 | Lovable Cloud migration | Postgres tables, Supabase Auth, RLS per role, audit trail in `SECURITY DEFINER` functions |
| S2 | Real MIRTH bridge | HL7 v2 MLLP listener + FHIR R4 REST endpoint per centre; message persistence and retry queue |
| S3 | REDCap production sync | OAuth 2.0 project connection, scheduled bidirectional sync, conflict resolution log |
| S4 | CPIC guideline pack | Versioned JSON dataset with CYP2D6 / OPRM1 / COMT rules; traceability per alert |
| S5 | SMART on FHIR launch | EHR launch context, patient picker, scoped token exchange |
| S6 | Compliance hardening | WCAG 2.2 AA audit, formal DPIA, ISO 27001 gap closure |

### 11.2 Integration reality-check sprints

**Sprint status (implemented in this build):** the simulated indicators are gone. The platform now performs real probes and shows an honest `not_configured` state until credentials exist.

| Sprint | Code | Server configuration |
| --- | --- | --- |
| 11.2.1 SNOMED-CT validation | `src/lib/terminology.server.ts`, `/api/map-symptoms` | `TERMINOLOGY_SERVER_URL`, `TERMINOLOGY_SERVER_TOKEN` |
| 11.2.2 REDCap connection | `src/lib/integrations.server.ts` (`checkRedcap`) | `REDCAP_API_URL`, `REDCAP_API_TOKEN` |
| 11.2.3 MIRTH channel health | `src/lib/integrations.server.ts` (`checkMirthChannels`) | `MIRTH_CHANNELS` (JSON array of `{center, hl7Url, fhirUrl}`) |

Every AI-proposed SNOMED-CT code is now sent to a FHIR `CodeSystem/$lookup`; codes come back tagged `validated`, `invalid` or `unvalidated`, and the ePRO screen labels unvalidated codes as requiring manual coding. REDCap health is read from the real API (`content=version`, plus a record count when the token allows it) — the "Sync now" button no longer touches a local clock. MIRTH channel status is an HTTP probe per configured endpoint.

The remaining work below is organisational (licences, tokens, VPNs) rather than code.

#### 11.2.1 SNOMED-CT licensing and terminology server

**Status today (code):** implemented — `/api/map-symptoms` sends every LLM-proposed code to a FHIR `CodeSystem/$lookup` via `src/lib/terminology.server.ts` and returns `validated` / `invalid` / `unvalidated`. The ePRO screen badges each code accordingly. **Blocked on:** licence + terminology server credentials.

**What is missing:**
- A **SNOMED-CT licence** for each country/jurisdiction where the platform is used. In Spain this is managed through the **Ministry of Health / SNS Terminology Service**; in other markets it may require a direct IHTSDO affiliate licence.
- A **terminology server** (e.g. Ontoserver, Snowstorm, or the public NHS SNOMED CT API) to validate, expand and keep codes current.
- A **fallback policy** for terms that do not map cleanly (map to HPO only, flag for manual coding, or use a local extension).

**Sprint deliverables:**
1. Confirm licence path and sign agreement before go-live.
2. Deploy or subscribe to a SNOMED-CT terminology server in the EU.
3. Replace LLM-only SNOMED output with validate-then-cache flow: LLM proposes → terminology server validates → cache approved mappings.
4. Add a "terminology confidence" field and manual-coding UI for low-confidence terms.

#### 11.2.2 REDCap real connection and licence

**Status today (code):** implemented — `checkRedcap` in `src/lib/integrations.server.ts` calls the real REDCap API (`content=version`, plus record count when the token allows) and the dashboard shows the live result. **Blocked on:** a REDCap project, service-account token and field mapping.

**What is missing:**
- A **REDCap project** with the matching data dictionary (or a project created per centre).
- A **REDCap API token** with appropriate privileges (ideally a service account, not a personal token).
- If the institution uses **REDCap Enterprise / Vanderbilt-hosted**, verify the licence and allowed use for a multi-centre pharmacogenomic study.
- Mapping between the local eCRF schema and REDCap data-entry forms / events.

**Sprint deliverables:**
1. Obtain REDCap project(s) and API tokens for each centre.
2. Implement OAuth 2.0 or token-based server-to-server authentication (token stored in Lovable Cloud secrets, never in the browser).
3. Build a bidirectional sync worker:
   - Push new patients, visits and ePRO entries to REDCap.
   - Pull genotyping results and centre-level metadata from REDCap.
   - Log every sync attempt, conflict and resolution.
4. Add a "last successful sync" verification that actually checks REDCap API health, not just the local clock.

#### 11.2.3 MIRTH Connect production channels

**Status today (code):** implemented — `checkMirthChannels` performs a real HTTP probe against each endpoint declared in `MIRTH_CHANNELS`; unconfigured centres are shown as `not configured` rather than green. **Blocked on:** MIRTH instances, VPN connectivity and channel definitions.

**What is missing:**
- MIRTH Connect instances deployed at each hospital or a central instance with VPN/site-to-site connectivity.
- HL7 v2/FHIR channel definitions, endpoint certificates and message queueing.

**Sprint deliverables:**
1. Deploy MIRTH Connect 4.x in each centre (or a central instance with secure tunnels).
2. Define channel templates for ADT^A08 (demographics), ORU^R01 (results) and FHIR R4 Patient/Observation/MedicationRequest.
3. Implement message persistence, dead-letter queue and idempotency checks.
4. Surface real channel health (HTTP/S or MLLP probe) in the integrations dashboard.

### 11.3 Risk register

| Risk | Impact | Mitigation |
| --- | --- | --- |
| SNOMED-CT licence delayed | Cannot show validated clinical codes in production | Start licence paperwork now; keep HPO-only fallback |
| REDCap API token scope too narrow | Sync fails for genotyping or multi-centre data | Define required privileges in the data-management plan before requesting tokens |
| MIRTH VPN not ready at a centre | That centre remains manual until channel is live | Build CSV upload fallback and audit trail |
| LLM terminology drift | Incorrect SNOMED/HPO codes | Terminology-server validation is live; add a manual review queue for `unvalidated` codes |
| Hydration/i18n regression | Raw translation keys or mismatched text on first paint | Language must always come from the `opi_lang` cookie via the root loader; never read `localStorage` during render |

---

## 12. Change log

| Version | Date | Changes |
| --- | --- | --- |
| 1.2.0 | 2026-08-01 | Cookie-based isomorphic language resolution (no hydration mismatches); `<html lang>` sync; documentation renamed to `proyecto-opi-platform.md`; full-route audit in ES and EN with zero console/hydration errors |
| 1.1.0 | 2026-07 | Integration reality-check sprints: real SNOMED-CT validation, real REDCap probe, real MIRTH channel probes, honest `not configured` states |
| 1.0.0 | 2026-07 | Initial platform: landing, ePRO with voice capture, clinician portal (dashboard, patients, eCRF, CDS, integrations), AI edge endpoints |

---

## 13. Verification checklist

Run before every release:

1. `bunx tsgo --noEmit` — clean typecheck.
2. Load `/`, `/login`, `/epro`, `/app/dashboard`, `/app/patients`, `/app/ecrf`, `/app/cds`, `/app/integrations` in **both** languages: no console errors, no hydration warnings, no raw `something.key` text.
3. Toggle language on one page, reload another: the new language must survive the reload (cookie), server-side.
4. With no integration secrets set, `/app/integrations` must show `not configured` — never a false green.
5. The footer documentation link must return HTTP 200.

---

## 14. Glossary

| Term | Definition |
| --- | --- |
| **eCRF** | Electronic Case Report Form |
| **ePRO** | Electronic Patient-Reported Outcome |
| **CPIC** | Clinical Pharmacogenetics Implementation Consortium |
| **HPO** | Human Phenotype Ontology |
| **SNOMED-CT** | Systematized Nomenclature of Medicine — Clinical Terms |
| **CTCAE** | Common Terminology Criteria for Adverse Events |
| **BPI** | Brief Pain Inventory |
| **QLQ-C30** | EORTC Quality of Life core questionnaire |
| **MIRTH** | Open-source healthcare integration engine |
| **FPGMX** | Fundación Pública Galega de Medicina Xenómica |

---

© LLAMALÍTICA · IISGS · Duponte · FPGMX. Pseudonymised data · EU hosting · GDPR · TLS 1.3 · AES-256.
