Files
medassist-ng/.github/copilot-instructions.md
T
Daniel Volz d0a40bde88 feat: Nagging reminders with max limit + ENV defaults for settings (#18)
* ci: prevent duplicate test runs - tests only on PRs, inline tests for builds

* docs: add testing and CI/CD documentation

* security: fix CodeQL vulnerabilities (SSRF, XSS, rate limiting)

- Add URL validation to prevent SSRF attacks on notification endpoints
  - Block private IPs (10.x, 172.16-31.x, 192.168.x, 169.254.x)
  - Block localhost and internal hostnames
  - Only allow HTTP/HTTPS protocols
- Add HTML escaping for medication names in email templates (XSS)
- Add stricter rate limiting for auth routes (5 req/15min for login/register)
- Add SSRF protection tests (405 tests total)

* security: add rate limiting to remaining auth routes

* chore: add CodeQL config to suppress rate-limit false positives

Rate limiting IS implemented via @fastify/rate-limit plugin:
- Global: 100 req/min (index.ts)
- Auth routes: 5-10 req/min via config.rateLimit option

CodeQL doesn't recognize Fastify's plugin-based rate limiting pattern.

* ci: switch to CodeQL Advanced Setup

- Add custom codeql.yml workflow
- Configure to use codeql-config.yml
- Exclude js/missing-rate-limiting rule (false positive)
  Rate limiting is implemented via @fastify/rate-limit plugin

* ci: add explicit permissions to workflows

Fixes CodeQL 'Workflow does not contain permissions' warnings.
Sets minimal 'contents: read' at top level.

* ci: add manual trigger to CodeQL workflow

* ci: add explicit permissions to all workflow jobs

* build(deps): bump esbuild, @vitest/coverage-v8 and vitest in /backend

Bumps [esbuild](https://github.com/evanw/esbuild) to 0.27.2 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together.


Updates `esbuild` from 0.21.5 to 0.27.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.27.2)

Updates `@vitest/coverage-v8` from 2.1.9 to 4.0.16
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.16/packages/coverage-v8)

Updates `vitest` from 2.1.9 to 4.0.16
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.16/packages/vitest)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.27.2
  dependency-type: indirect
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.0.16
  dependency-type: direct:development
- dependency-name: vitest
  dependency-version: 4.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: add GitHub issue templates

- Bug report template with deployment type, browser info, logs
- Feature request template with affected area, priority
- Config with link to discussions and README
- Optimize test.yml to skip tests for non-code changes

* Initial plan

* Remove database schema duplication by creating shared schema-sql.ts module

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Refactor frontend date formatting to eliminate duplication

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* docs: Add branch protection warning and PR workflow to instructions

* ci: remove paths filter from test workflow to fix branch protection

* fix: add .js extension to schema-sql imports for ESM compatibility (#15)

* feat: add setting to skip reminders for taken doses

- Add skipRemindersForTakenDoses setting to database schema
- Extend settings API to save and load new setting
- Update intake reminder scheduler to filter taken doses
- Add frontend toggle in settings with i18n (EN/DE)
- Only check doses from today (timezone-aware)
- Update all test schemas with new field
- All 405 tests passing

* feat: add repeat reminders for missed doses

- Add repeatRemindersEnabled and reminderRepeatIntervalMinutes settings
- Refactor intake reminder state from array to object with sendCount tracking
- Update scheduler to send repeated reminders at configurable intervals
- Only remind for today's doses (timezone-aware filtering)
- Add frontend toggle and interval input (5-480 minutes) in settings
- Maintain backward compatibility for old state file format
- Update all test schemas and assertions
- All 406 tests passing

* feat: add nagging reminders with max limit and ENV defaults

- Add maxNaggingReminders setting to limit repeat reminders (1-20)
- Add ENV defaults for all user settings (DEFAULT_*)
- Add ALTER TABLE migrations for backward compatibility
- Add smtpConfigured/shoutrrrConfigured to health endpoint
- Fix Push toggle to allow enabling without existing URL
- Disable skip/repeat toggles when no notifications enabled
- Add Pocket ID button to registration page
- Add getTodaysIntakes() for repeat reminder logic
- Update translations (en/de) for new settings
- Add comprehensive tests for new features

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>
2026-01-10 21:05:44 +01:00

18 KiB

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

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

# Run tests
cd backend && npm test              # Run all tests
cd backend && npm run test:coverage # Run with coverage report

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

// Backend Test Beispiel (backend/src/test/example.test.ts)
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestApp, createTestUser } from './routes.test'; // Test-Utilities

describe('Feature Name', () => {
  let app: FastifyInstance;
  let authToken: string;

  beforeAll(async () => {
    app = await createTestApp();
    const user = await createTestUser(app);
    authToken = user.token;
  });

  afterAll(async () => {
    await app.close();
  });

  it('should do something specific', async () => {
    const response = await app.inject({
      method: 'GET',
      url: '/endpoint',
      headers: { Authorization: `Bearer ${authToken}` }
    });
    
    expect(response.statusCode).toBe(200);
    expect(response.json()).toHaveProperty('expectedField');
  });
});

Test-Commands

cd backend
CI=true npm test            # Tests einmal ausführen (IMMER so ausführen!)
CI=true npm run test:coverage  # Mit Coverage-Report
npm test -- --watch         # Watch-Mode für manuelle Entwicklung
npm test -- -t "test name"  # Einzelnen Test ausführen

⚠️ 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

Pull Request erstellt
        ↓
┌─────────────────────────────────────┐
│  test.yml                           │
│  ├─ backend-test (parallel)         │
│  │   ├─ npm ci                      │
│  │   ├─ tsc --noEmit (Type-Check)   │
│  │   └─ npm run test:coverage       │
│  └─ frontend-build (parallel)       │
│      ├─ npm ci                      │
│      └─ npm run build               │
└─────────────────────────────────────┘
        ↓ Tests müssen bestehen
    PR kann gemerged werden
        ↓
Push to main / Tag erstellt
        ↓
┌─────────────────────────────────────┐
│  docker-build.yml                   │
│  ├─ backend-test (parallel)         │
│  ├─ frontend-build (parallel)       │
│  └─ build-and-push (nach Tests)     │
│      ├─ Docker Images bauen         │
│      └─ Push zu GHCR                │
└─────────────────────────────────────┘

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:

# 1. Feature Branch erstellen
git checkout -b feat/mein-feature

# 2. Änderungen committen und pushen
git add . && git commit -m "feat: Beschreibung"
git push -u origin feat/mein-feature

# 3. PR erstellen (via GitHub CLI oder Web)
gh pr create --title "Mein Feature" --body "Beschreibung"

# 4. Warten bis CI grün ist, dann mergen
gh pr merge --squash --delete-branch

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

  1. Feature implementieren
  2. Tests für das Feature schreiben
  3. Lokal npm run test:coverage ausführen
  4. Coverage darf nicht sinken
  5. Feature Branch erstellen und pushen
  6. Pull Request erstellen
  7. Warten bis CI grün ist
  8. 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

{
  name, genericName, takenByJson,        // Identity (takenByJson is JSON array)
  packCount, blistersPerPack, pillsPerBlister, looseTablets,  // Inventory
  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 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

  1. IMMER mit DEFAULT-Wert: Neue Spalten müssen NOT NULL DEFAULT <wert> haben
  2. NULL-sicher im Code: Alle Abfragen müssen ?? defaultValue oder ?? false verwenden
  3. 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
  4. 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

// 1. schema.ts - Drizzle Definition
maxNaggingReminders: integer("max_nagging_reminders").notNull().default(5),

// 2. schema-sql.ts - Für neue Datenbanken
"max_nagging_reminders integer NOT NULL DEFAULT 5,"

// 3. client.ts - Migration für bestehende DBs (IN ensureTablesExist())
await client.execute(`ALTER TABLE user_settings ADD COLUMN max_nagging_reminders integer NOT NULL DEFAULT 5`).catch(() => {});

// 4. Routes - NULL-sicher lesen
maxNaggingReminders: settings.maxNaggingReminders ?? 5,

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:

  1. Explizit kommunizieren: In Release Notes dokumentieren
  2. Migration-Script: Automatisches Upgrade-Script bereitstellen
  3. 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