- Enhanced the database migration process to ensure compatibility with existing production databases, including detailed steps for adding/modifying columns. - Updated the client-side logic to support tracking doses taken by multiple users, including changes to the data structure and UI components. - Added new styles for per-person dose tracking to improve user experience and visual clarity.
13 KiB
MedAssist-ng - AI Coding Instructions
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
Frontend (React) → /api/* proxy → Backend (Fastify) → SQLite
↓ (Vite rewrites /api to /)
The Vite proxy at frontend/vite.config.ts rewrites /api/* to / - so frontend calls /api/medications but backend route is just /medications.
Development Commands
# Start dev environment (preferred)
docker compose -f docker-compose.dev.yml up
# Or run services separately:
cd backend && npm run dev # tsx watch on port 3000
cd frontend && npm run dev # Vite on port 5173
# Production
docker compose up -d
# Database migrations
cd backend && npm run migrate
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 |
| SMTP | Email config (read-only from .env) |
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
{
name, genericName, takenBy, // Identity
packCount, stripsPerPack, tabsPerStrip, looseTablets, // Inventory
count, strips, stripSize, // Derived/legacy
pillWeightMg, // For mg display
usageJson, everyJson, startJson, // Intake schedules as JSON arrays
imageUrl, expiryDate, notes, // Optional metadata
intakeRemindersEnabled // Per-med reminder toggle
}
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 viadata-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 infrontend/src/i18n/*.json
⚠️⚠️⚠️ Database Migrations (ABSOLUTELY CRITICAL) ⚠️⚠️⚠️
THIS IS NON-NEGOTIABLE: ALL database changes MUST work for EXISTING production databases!
Users update their Docker containers and expect the app to work with their existing data. If migrations don't run automatically, the app crashes with SQLITE_ERROR: no such column errors.
The Migration System
The app uses auto-migrations at startup in backend/src/db/client.ts. This file:
- Creates tables if they don't exist (fresh install)
- Runs
ALTER TABLE ADD COLUMNfor each new column (existing databases) - Ignores "duplicate column" errors (migration already applied)
When adding/modifying database columns or tables, ALWAYS do ALL of the following:
1. Update schema: backend/src/db/schema.ts
// Add the new column to the Drizzle schema
stockCalculationMode: text("stock_calculation_mode").notNull().default("automatic"),
2. Update client.ts TABLE CREATION: backend/src/db/client.ts
Find the CREATE TABLE IF NOT EXISTS statement and add the new column:
CREATE TABLE IF NOT EXISTS user_settings (
...
stock_calculation_mode text NOT NULL DEFAULT 'automatic', -- ADD THIS LINE
...
)
This is for FRESH installs - new databases get all columns from the start.
3. Update client.ts MIGRATIONS ARRAY: backend/src/db/client.ts
Add an entry to the migrations array:
const migrations = [
...existing migrations...
{ name: "user_settings_stock_calculation_mode", sql: "ALTER TABLE user_settings ADD COLUMN stock_calculation_mode TEXT NOT NULL DEFAULT 'automatic'" },
];
This is for EXISTING databases - the ALTER TABLE adds the column to old databases.
4. Create migration SQL file (for documentation): backend/src/db/migrations/XXXX_description.sql
-- Add stock calculation mode setting
ALTER TABLE user_settings ADD COLUMN stock_calculation_mode TEXT NOT NULL DEFAULT 'automatic';
5. Update journal: backend/src/db/migrations/meta/_journal.json
{ "idx": X, "version": 1, "when": TIMESTAMP, "tag": "XXXX_description", "breakpoint": false }
6. Update migrate.ts: backend/src/db/migrate.ts
Add the column to the CREATE TABLE statement AND to the migrations array.
⚠️ CRITICAL CHECKLIST - DO NOT SKIP ANY STEP:
| Step | File | Purpose | If Missing |
|---|---|---|---|
| 1 | schema.ts |
Drizzle ORM knows about column | TypeScript errors |
| 2 | client.ts (CREATE TABLE) |
Fresh installs have column | Fresh installs crash |
| 3 | client.ts (migrations array) |
Existing DBs get column | PRODUCTION CRASHES |
| 4 | migrations/*.sql |
Documentation | None (but keep for history) |
| 5 | _journal.json |
Migration tracking | None (but keep for history) |
| 6 | migrate.ts |
CLI migration tool | CLI tool fails |
Step 3 is the most critical! Without it, users who update their Docker container will get SQLITE_ERROR: no such column and the app will not start.
Testing Migrations
Before pushing changes:
- Test with fresh database: Delete
backend/data/medassist-ng.dband restart - Test with existing database: Keep old DB and restart - new columns should be added automatically
File Locations
| Purpose | Location |
|---|---|
| Backend entry | backend/src/index.ts |
| Database schema | backend/src/db/schema.ts |
| Migrations | backend/src/db/migrations/*.sql |
| Migration journal | backend/src/db/migrations/meta/_journal.json |
| 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 |