MedAssist-ng - AI Coding Instructions
General Rules
- No temporary files: Delete temporary scripts/files immediately after use. Do not commit temporary debug scripts, test files, or one-off utilities to the repository.
- Clean workspace: Always clean up after yourself. If you create a file for a specific task, delete it once done.
Architecture Overview
MedAssist-ng is a medication tracking and planning app with a monorepo structure:
- Backend: Fastify 5 + TypeScript + SQLite (Drizzle ORM) at
backend/
- Frontend: React 18 + Vite + TypeScript at
frontend/
- Database: SQLite with migrations in
backend/src/db/migrations/
- Deployment: Docker Compose with separate dev containers
- i18n: English (en) and German (de) via react-i18next
Data Flow
The Vite proxy at frontend/vite.config.ts rewrites /api/* to / - so frontend calls /api/medications but backend route is just /medications.
Development Commands
Testing (MANDATORY)
⚠️ WICHTIG: Jede neue Funktionalität MUSS mit Tests abgedeckt werden!
Pull Requests ohne Tests für neue Features werden nicht akzeptiert.
Test-Framework
- Vitest 2.1 mit v8 Coverage
- Tests in
backend/src/test/*.test.ts
- Coverage-Ziel: Mindestens gleiche oder bessere Coverage nach Änderungen
Test-Struktur
| Datei |
Testet |
routes.test.ts |
API-Endpunkte (Auth, Medications, Doses, Settings, Share, Planner) |
services.test.ts |
Scheduler-Utilities (Timezone, Blisters, Usage-Berechnung) |
db.test.ts |
Datenbank-Schema und Operationen |
Tests schreiben
Test-Commands
⚠️ WICHTIG für AI-Agenten: Tests IMMER mit CI=true ausführen!
Ohne CI=true läuft Vitest im Watch-Mode und wartet auf Eingaben.
CI/CD Pipeline (GitHub Actions)
Workflow-Übersicht
Branch Protection
⚠️ WICHTIG: Der main Branch ist geschützt!
Direktes Pushen nach main ist nicht möglich - GitHub lehnt den Push ab.
Alle Änderungen müssen über Pull Requests erfolgen.
- main Branch ist geschützt (Repository Rules)
- Direktes Pushen wird von GitHub abgelehnt mit:
GH013: Repository rule violations
- PRs benötigen:
- ✅
backend-test Status Check bestanden
- ✅
frontend-build Status Check bestanden
- Nach erfolgreichem Merge wird der Feature-Branch automatisch gelöscht
Workflow für Änderungen:
Workflow-Dateien
| Datei |
Trigger |
Zweck |
.github/workflows/test.yml |
Pull Requests |
Tests ausführen, PR blockieren bei Fehlern |
.github/workflows/docker-build.yml |
Push to main, Tags |
Tests + Docker Images bauen und pushen |
Neuen Code hinzufügen - Checkliste
- ✅ Feature implementieren
- ✅ Tests für das Feature schreiben
- ✅ Lokal
npm run test:coverage ausführen
- ✅ Coverage darf nicht sinken
- ✅ Feature Branch erstellen und pushen
- ✅ Pull Request erstellen
- ✅ Warten bis CI grün ist
- ✅ PR mergen (Branch wird automatisch gelöscht)
Key Patterns
Backend Routes (backend/src/routes/)
| Route File |
Endpoints |
auth.ts |
/auth/login, /auth/register, /auth/logout, /auth/refresh, /auth/me |
medications.ts |
CRUD /medications, /medications/:id/image |
doses.ts |
/doses/taken - track dose intake |
planner.ts |
/medications/usage - calculate usage for date range |
settings.ts |
/settings - user settings CRUD |
share.ts |
/share - create share tokens, /share/:token - public access |
health.ts |
/health - health check endpoint |
Backend Services (backend/src/services/)
| Service |
Description |
reminder-scheduler.ts |
Stock reminder emails/push notifications |
intake-reminder-scheduler.ts |
Intake reminder notifications |
Frontend (frontend/src/App.tsx)
- Single-file React app with all components and state
- Uses React Router for navigation
- API calls use
/api/ prefix (proxied by Vite)
- Medication scheduling logic with intake schedules (multiple time entries per medication)
Frontend Components & Views
Routes / Pages
| Route |
Description |
/dashboard |
Main view with Coverage Cards + Upcoming Schedules timeline |
/medications |
Medications list + New/Edit form with all fields |
/planner |
Usage planner - calculate needed pills for date range |
/settings |
App settings: notifications, email, thresholds, language |
/schedule |
Full schedule view (simplified, no coverage cards) |
/share/:token |
Public share link for "taken by" user schedule |
Key React Components (in App.tsx)
| Component |
Description |
App |
Root component with BrowserRouter |
AppRouter |
Handles auth check, renders AppContent or Auth |
AppContent |
Main app shell with navigation, header, all routes |
SharedSchedule |
Public share page for medication schedules by person |
MedicationAvatar |
Round avatar with medication image or colored initial |
Dashboard Sections
| Section |
Description |
| Coverage Cards |
Stock status cards per medication: days left, blisters, status (Normal/Warning/Critical) |
| Upcoming Schedules |
Timeline grouped by day, collapsible days, dose tracking |
Schedule/Timeline Elements
| Element |
CSS Class |
Description |
| Past days toggle |
.past-days-toggle |
Click to show/hide past days |
| Day container |
.day-block |
Container for one day, collapsible |
| Today highlight |
.day-block.today |
Blue border/background for current day |
| Past day |
.day-block.past |
Dashed border, reduced opacity |
| All taken |
.day-block.all-taken |
Green styling when all doses taken |
| Day header |
.day-divider |
Date header with collapse toggle arrow |
| Collapse icon |
.day-collapse-icon |
▶/▼ arrow for expand/collapse |
| Day summary |
.day-summary |
Shows "X/Y" doses taken or "✓ All taken" |
| Medication row |
.time-row |
One medication's doses for that day |
| Dose item |
.dose-item |
Individual dose with time, amount, take/undo button |
| Dose taken |
.dose-item.taken |
Green background when dose is marked taken |
| Dose overdue |
.dose-item.overdue |
Styling for past untaken doses |
| Dose future |
.dose-item.future |
Disabled button for future days |
Medication Form (New/Edit)
| Field |
Description |
| Commercial Name |
Main medication name (required) |
| Generic Name |
Scientific/generic name (optional) |
| Taken By |
Person taking the medication (optional, enables filtering/sharing) |
| Packs |
Number of full packs |
| Blisters per Pack |
Strips/blisters in each pack |
| Pills per Blister |
Tablets per strip |
| Loose Pills |
Extra pills not in blisters |
| Pill Weight (mg) |
Weight per pill for dose calculation display |
| Expiry Date |
Medication expiration |
| Notes |
Free text notes |
| Image Upload |
Medication photo (preview for new, direct upload for edit) |
| Intake Schedule |
One or more intake entries defining usage pattern |
Intake Schedule
Each blister defines a recurring intake:
- Usage (Pills): How many pills per dose
- Every (Days): Interval (1 = daily, 7 = weekly)
- Start (Date/Time): When the schedule starts (determines past/future doses)
- Remind checkbox: Enable intake reminders (🔔)
Modals
| Modal |
Trigger |
Content |
| Medication Detail |
Click on coverage card or medication row |
Full medication info, stock, schedule preview, edit/delete/ICS buttons |
| Image Lightbox |
Click medication image |
Full-size medication image |
| Share Dialog |
"Share" button on schedules |
Generate share link for specific "taken by" person |
| User Schedule Filter |
Click on "taken by" badge |
Filter schedule by person |
Settings Sections
| Section |
Settings |
| General |
Language toggle (EN/DE) |
| Stock Thresholds |
Warning days, critical days, expiry warning days |
| Email Notifications |
Enable, email address, stock/intake toggles |
| Push Notifications (Shoutrrr) |
Enable, URL (ntfy/gotify/etc), stock/intake toggles |
| Reminder Settings |
Days before, repeat daily, skip for taken, repeat/nagging |
| SMTP |
Email config (read-only from .env) |
Settings ENV Defaults
All user settings can be pre-configured via ENV variables (see .env.example).
These are only used as defaults when a new user is created.
Once a user saves settings in the app, their saved values take precedence over ENV.
| ENV Variable |
Setting |
Default |
DEFAULT_EMAIL_ENABLED |
Email notifications |
false |
DEFAULT_SHOUTRRR_ENABLED |
Push notifications |
false |
DEFAULT_SHOUTRRR_URL |
ntfy/gotify URL |
(empty) |
DEFAULT_REPEAT_REMINDERS_ENABLED |
Nagging reminders |
false |
DEFAULT_REMINDER_REPEAT_INTERVAL_MINUTES |
Nag interval |
30 |
DEFAULT_MAX_NAGGING_REMINDERS |
Max nags |
5 |
DEFAULT_LOW_STOCK_DAYS |
Low stock threshold |
30 |
DEFAULT_LANGUAGE |
UI language |
en |
Database Schema (backend/src/db/schema.ts)
| Table |
Description |
users |
User accounts with password hash, auth provider, timestamps |
medications |
Per-user medications with inventory, schedules as JSON arrays |
userSettings |
Per-user settings: notifications, thresholds, language |
refreshTokens |
JWT refresh tokens for auth rotation |
shareTokens |
Public share links by takenBy person |
doseTracking |
Tracks when doses are marked as taken |
Key Medication Fields
Dose ID Format
Dose IDs follow the pattern: {medicationId}-{blisterIndex}-{timestampMs}
Example: 5-0-1735344000000 = Medication 5, Blister 0, timestamp
State Management (AppContent)
Key State Variables
| State |
Purpose |
meds |
Array of all user's medications |
form |
Current medication form data |
editingId |
ID of medication being edited (null for new) |
pendingImage / pendingImagePreview |
Image upload for new medications |
settings / savedSettings |
User settings current vs saved |
scheduleDays |
How many days to show (30/90/180) |
showPastDays |
Toggle for past days visibility |
takenDoses |
Set of dose IDs that are marked taken |
manuallyCollapsedDays / manuallyExpandedDays |
Day collapse state |
selectedMed |
Medication shown in detail modal |
selectedUser |
Filter schedule by "taken by" person |
Key Computed Values (useMemo)
| Value |
Purpose |
schedule |
All scheduled events from buildSchedulePreview() |
groupedSchedule |
Events grouped by day |
pastDays / futureDays |
Split groupedSchedule by today |
coverage |
Stock coverage calculations |
coverageByMed / depletionByMed |
Coverage lookups |
Conventions
- TypeScript: Strict mode, ESM modules (
"type": "module")
- Styling: CSS custom properties in
frontend/src/styles.css, dark/light theme via data-theme
- API responses: Return objects directly, Fastify serializes to JSON
- Environment: Copy
.env.example → .env, secrets must be 10+ chars
- i18n: All UI text via
t('key') function, translations in frontend/src/i18n/*.json
Database Schema Changes (WICHTIG: Abwärtskompatibilität!)
⚠️ KRITISCH: Die App MUSS abwärtskompatibel mit älteren Datenbanken bleiben!
Nutzer upgraden ihre Docker-Container, aber behalten ihre bestehende DB.
Die App darf NICHT abstürzen wenn alte Spalten fehlen.
Regeln für neue Spalten
- IMMER mit DEFAULT-Wert: Neue Spalten müssen
NOT NULL DEFAULT <wert> haben
- NULL-sicher im Code: Alle Abfragen müssen
?? defaultValue oder ?? false verwenden
- Schema-SQL aktualisieren: In diesen Dateien hinzufügen:
backend/src/db/schema.ts - Drizzle Schema
backend/src/db/schema-sql.ts - getTableCreationSQL() für neue DBs
backend/src/db/client.ts - ALTER TABLE ADD COLUMN IF NOT EXISTS Migration
- Test-Schemas updaten: Alle Test-Dateien mit eigenem Schema:
backend/src/test/e2e-routes.test.ts
backend/src/test/integration.test.ts
backend/src/test/planner.test.ts
Beispiel: Neue Spalte hinzufügen
Was NICHT erlaubt ist
- ❌ Spalten löschen oder umbenennen (bricht alte DBs)
- ❌
NOT NULL ohne DEFAULT (INSERT schlägt fehl)
- ❌ Spalten ohne Fallback im Code lesen
- ❌ DB löschen als "Lösung" dokumentieren
Wann Abwärtskompatibilität NICHT möglich ist
Wenn eine Breaking Change unvermeidbar ist:
- Explizit kommunizieren: In Release Notes dokumentieren
- Migration-Script: Automatisches Upgrade-Script bereitstellen
- Versionsprüfung: App sollte DB-Version prüfen und warnen
File Locations
| Purpose |
Location |
| Backend entry |
backend/src/index.ts |
| Database schema |
backend/src/db/schema.ts |
| Backend routes |
backend/src/routes/*.ts |
| Backend services |
backend/src/services/*.ts |
| Frontend app |
frontend/src/App.tsx |
| Frontend auth |
frontend/src/components/Auth.tsx |
| Styles |
frontend/src/styles.css |
| i18n English |
frontend/src/i18n/en.json |
| i18n German |
frontend/src/i18n/de.json |
| Docker prod |
docker-compose.yml |
| Docker dev |
docker-compose.dev.yml |
| Env template |
.env.example |