From 738513a3ba08c430039db8e8e13a08b93326ca10 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Wed, 24 Dec 2025 13:01:53 +0100 Subject: [PATCH] refactor: rename project to MedAssist-ng and update configurations - Updated environment variables in .env.example for production setup. - Changed project references from MedAssist to MedAssist-ng in documentation and code. - Adjusted Docker configurations for new image names and ports. - Removed deprecated push-images.sh script and added docker-compose.dev.yml for development. - Updated translation files to reflect new project name. - Ensured all email notifications and headers reflect the new branding. --- .env.example | 33 +++--- .github/copilot-instructions.md | 12 +- README.md | 117 +++++++++++++++----- backend/package-lock.json | 4 +- backend/package.json | 2 +- backend/src/db/client.ts | 2 +- backend/src/db/migrate.ts | 2 +- backend/src/i18n/translations.ts | 32 +++--- backend/src/plugins/env.ts | 3 +- backend/src/routes/planner.ts | 20 ++-- backend/src/routes/settings.ts | 12 +- docker-compose.dev.yml | 46 ++++++++ docker-compose.prod.yml | 55 ---------- docker-compose.yml | 65 ++++++----- frontend/index.html | 2 +- frontend/nginx.conf | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/src/App.tsx | 6 +- frontend/src/i18n/de.json | 10 +- frontend/src/i18n/en.json | 10 +- frontend/src/i18n/index.ts | 2 +- package.json | 2 +- scripts/push-images.sh | 183 ------------------------------- 24 files changed, 254 insertions(+), 374 deletions(-) create mode 100644 docker-compose.dev.yml delete mode 100644 docker-compose.prod.yml delete mode 100755 scripts/push-images.sh diff --git a/.env.example b/.env.example index 6c93632..d85bd88 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,26 @@ -NODE_ENV=development +# ============================================================================= +# MedAssist-ng Configuration +# ============================================================================= +# Copy this file to .env and adjust values for your setup +# ============================================================================= + +NODE_ENV=production PORT=3000 -DATABASE_URL=file:./data/medassist.db -CORS_ORIGINS=http://localhost:4173,http://localhost:5173 +DATABASE_URL=file:./data/medassist-ng.db +CORS_ORIGINS=http://localhost:4174 LOG_LEVEL=info # Timezone for scheduled reminders (e.g., Europe/Berlin, America/New_York) TZ=Europe/Berlin -# Auth (use strong secrets; min 10 chars required) -JWT_SECRET=change-me-now-with-stronger-secret -REFRESH_SECRET=change-me-refresh-strong-secret -COOKIE_SECRET=change-me-cookie-strong-secret -CSRF_SECRET=change-me-csrf-strong-secret +# Auth - CHANGE THESE! Generate with: openssl rand -hex 32 +JWT_SECRET=CHANGE_ME_generate_with_openssl_rand_hex_32 +REFRESH_SECRET=CHANGE_ME_generate_with_openssl_rand_hex_32 +COOKIE_SECRET=CHANGE_ME_generate_with_openssl_rand_hex_32 ACCESS_TOKEN_TTL_MIN=15 REFRESH_TOKEN_TTL_DAYS=14 -# SMTP (optional) +# SMTP (optional - for email notifications) SMTP_HOST= SMTP_PORT=587 SMTP_USER= @@ -23,12 +28,8 @@ SMTP_PASS= SMTP_FROM= SMTP_SECURE=false -# Planner limits +# Rate limits EMAILS_PER_DAY=3 -# Container registry (used by scripts/push-images.sh) -REGISTRY_HOST=git.danielvolz.org -REGISTRY_TOKEN= -REGISTRY_USER= # optional; defaults to token if empty -PROJECT_PATH=daniel/medassist -# IMAGE_TAG can stay empty; override via -v flag +# Default value only - frontend settings (stored in settings.json) take precedence +REMINDER_DAYS_BEFORE=7 \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 26ba2e1..3e9afd6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,8 +1,8 @@ -# MedAssist - AI Coding Instructions +# MedAssist-ng - AI Coding Instructions ## Architecture Overview -MedAssist is a **medication tracking and planning app** with a monorepo structure: +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/` @@ -21,12 +21,15 @@ The Vite proxy at `frontend/vite.config.ts` rewrites `/api/*` to `/` - so fronte ```bash # Start dev environment (preferred) -docker compose up +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 ``` @@ -90,5 +93,6 @@ Notifications: data/notification-settings.json (editable via UI) | Migrations | `backend/src/db/migrations/*.sql` | | Frontend app | `frontend/src/App.tsx` | | Styles | `frontend/src/styles.css` | -| Docker dev setup | `docker-compose.yml` | +| Docker prod | `docker-compose.yml` | +| Docker dev | `docker-compose.dev.yml` | | Env template | `.env.example` | diff --git a/README.md b/README.md index ef1f58c..c541ce0 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,94 @@ -# Medassist (Rebuild) +# MedAssist-ng -Sichere, schlanke Neuimplementierung mit Fastify + SQLite + React/Vite. Docker-first, Caddy ΓΌbernimmt TLS. +πŸ“Š Medication tracking and planning app with stock monitoring, intake reminders, and email notifications. -## Architektur -- Backend: Fastify, SQLite (Drizzle/Kysely/Prisma ready), Auth mit HttpOnly-Cookies (Browser) + Bearer (API). Helmet, CORS-Allowlist, Rate Limit, CSRF double-submit, Input-Validation (zod/ajv). -- Frontend: React + Vite (TS). GeschΓΌtzte Views, zentraler API-Client. -- Tokens: Access ~15m, Refresh rotierend (sliding) mit Max-Age ~14d, Reuse-Detection. -- Planner/Email: Server-escaped, Throttling, SMTP-Pass write-only. -- Deployment: Docker Compose (app + sqlite volume). Caddy als vorgelagerter Proxy/TLS. +## Quick Start (Production) -## Entwicklung -- Node-Version: siehe .nvmrc -- Env: .env.example kopieren β†’ .env -- Workspaces: root package.json mit backend/frontend Workspaces -- Scripts (nach npm install in beiden Paketen): - - Backend: `npm run dev` (backend), `npm run build`, `npm run start` - - Frontend: `npm run dev`, `npm run build`, `npm run preview` - - Compose: `docker-compose up --build` +```bash +# 1. Clone and configure +git clone https://github.com/your-username/medassist-ng.git +cd medassist-ng +cp .env.example .env -## Verzeichnisstruktur -- backend/ … Fastify-App, Migrations, Dockerfile -- frontend/ … React/Vite-App, Dockerfile -- docker-compose.yml … lokale Orchestrierung +# 2. Generate secure secrets (required!) +# Edit .env and replace CHANGE_ME values with output of: +openssl rand -hex 32 -## Security Defaults -- Keine Secrets in Logs/Responses -- CSRF nur fΓΌr Cookie-Clients -- CORS-Liste aus ENV -- Non-root Container, Healthcheck +# 3. Start +docker compose up -d -## NΓ€chste Schritte -- Dependencies installieren -- DB-Migrationen ausfΓΌhren -- Frontend-Routen/Views ausbauen +# App runs on http://localhost:4174 (frontend) and http://localhost:4000 (API) +``` + +## Development + +```bash +# Start dev environment with hot-reload +docker compose -f docker-compose.dev.yml up + +# Frontend: http://localhost:5173 +# Backend: http://localhost:3000 +``` + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Frontend β”‚ β”‚ Backend β”‚ β”‚ SQLite β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +- **Frontend**: React 18 + Vite + TypeScript, nginx-unprivileged (prod) +- **Backend**: Fastify 5 + TypeScript + SQLite (Drizzle ORM) +- **Security**: Non-root containers, read-only filesystem, no-new-privileges + +## Features + +- πŸ“¦ **Medication Inventory** - Track packs, blisters, loose pills +- πŸ“… **Intake Scheduling** - Multiple daily schedules with reminders +- πŸ“Š **Stock Monitoring** - Automatic low-stock detection +- πŸ“§ **Notifications** - Email (SMTP) and Push (ntfy, Discord, Telegram) +- 🌍 **i18n** - German and English +- πŸŒ™ **Dark/Light Mode** + +## Configuration + +Copy `.env.example` to `.env` and configure: + +| Variable | Required | Description | +|----------|----------|-------------| +| `JWT_SECRET` | βœ… | Access token signing (min 10 chars) | +| `REFRESH_SECRET` | βœ… | Refresh token signing | +| `COOKIE_SECional) | + +## File Structure + +``` +medassist-ng/ +β”œβ”€β”€ backend/ # Fastify API +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ db/ # Schema + migrations +β”‚ β”‚ β”œβ”€β”€ routes/ # API endpoints +β”‚ β”‚ └── services/ # Business logic +β”‚ └── Dockerfile +β”œβ”€β”€ frontend/ # React SPA +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ App.tsx # Main application +β”‚ β”‚ └── i18n/ # Translations +β”‚ └── Dockerfile +β”œβ”€β”€ docker-compose.yml # Production (default) +β”œβ”€β”€ docker-compose.dev.yml # Development +└── .env.example +``` + +## Reverse Proxy (Caddy example) + +``` +med.example.com { + reverse_proxy localhost:4174 +} +``` + +## License + +MIT diff --git a/backend/package-lock.json b/backend/package-lock.json index b08f939..e06698f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "medassist-backend", + "name": "medassist-ng-backend", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "medassist-backend", + "name": "medassist-ng-backend", "version": "0.1.0", "dependencies": { "@fastify/cookie": "^10.0.1", diff --git a/backend/package.json b/backend/package.json index ed06774..9d5c34a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,5 +1,5 @@ { - "name": "medassist-backend", + "name": "medassist-ng-backend", "version": "0.1.0", "private": true, "type": "module", diff --git a/backend/src/db/client.ts b/backend/src/db/client.ts index 2713de9..ad5c646 100644 --- a/backend/src/db/client.ts +++ b/backend/src/db/client.ts @@ -6,7 +6,7 @@ import dotenv from "dotenv"; dotenv.config({ path: process.env.DOTENV_PATH || ".env" }); -const url = process.env.DATABASE_URL || "file:./data/medassist.db"; +const url = process.env.DATABASE_URL || "file:./data/medassist-ng.db"; // Ensure data directory exists before creating database if (url.startsWith("file:")) { diff --git a/backend/src/db/migrate.ts b/backend/src/db/migrate.ts index 8874ccd..6a91391 100644 --- a/backend/src/db/migrate.ts +++ b/backend/src/db/migrate.ts @@ -3,7 +3,7 @@ import dotenv from "dotenv"; dotenv.config({ path: process.env.DOTENV_PATH || ".env" }); -const url = process.env.DATABASE_URL || "file:./data/medassist.db"; +const url = process.env.DATABASE_URL || "file:./data/medassist-ng.db"; async function main() { console.log("Starting database setup..."); diff --git a/backend/src/i18n/translations.ts b/backend/src/i18n/translations.ts index 91650e4..1260dc3 100644 --- a/backend/src/i18n/translations.ts +++ b/backend/src/i18n/translations.ts @@ -54,8 +54,8 @@ type TranslationKeys = { const translations: Record = { en: { stockReminder: { - subject: "MedAssist Auto-Reminder: {count} Medication{s} Running Low", - title: "⚠️ MedAssist - Automatic Reorder Reminder", + subject: "MedAssist-ng Auto-Reminder: {count} Medication{s} Running Low", + title: "⚠️ MedAssist-ng - Automatic Reorder Reminder", description: "The following medications are running low and need to be reordered:", alertSingle: "⚠️ 1 medication running low!", alertMultiple: "⚠️ {count} medications running low!", @@ -65,11 +65,11 @@ const translations: Record = { days: "Days", runsOut: "Runs Out", }, - footer: "πŸ€– Automatic reminder from MedAssist", + footer: "πŸ€– Automatic reminder from MedAssist-ng", }, intakeReminder: { - subject: "MedAssist: Medication Reminder - {medications}", - title: "πŸ’Š MedAssist - Intake Reminder", + subject: "MedAssist-ng: Medication Reminder - {medications}", + title: "πŸ’Š MedAssist-ng - Intake Reminder", description: "Time to take your medication in {minutes} minutes:", alertSingle: "πŸ’Š 1 medication scheduled", alertMultiple: "πŸ’Š {count} medications scheduled", @@ -79,11 +79,11 @@ const translations: Record = { time: "Time", }, pills: "pills", - footer: "MedAssist Medication Planner", + footer: "MedAssist-ng Medication Planner", }, push: { - stockTitle: "MedAssist: 1 Medication Running Low", - stockTitleMultiple: "MedAssist: {count} Medications Running Low", + stockTitle: "MedAssist-ng: 1 Medication Running Low", + stockTitleMultiple: "MedAssist-ng: {count} Medications Running Low", intakeTitle: "Medication Reminder in {minutes} min", pillsLeft: "{count} pills", daysLeft: "{count} days left", @@ -99,8 +99,8 @@ const translations: Record = { }, de: { stockReminder: { - subject: "MedAssist Auto-Erinnerung: {count} Medikament{e} wird knapp", - title: "⚠️ MedAssist - Automatische Nachbestell-Erinnerung", + subject: "MedAssist-ng Auto-Erinnerung: {count} Medikament{e} wird knapp", + title: "⚠️ MedAssist-ng - Automatische Nachbestell-Erinnerung", description: "Die folgenden Medikamente gehen zur Neige und sollten nachbestellt werden:", alertSingle: "⚠️ 1 Medikament wird knapp!", alertMultiple: "⚠️ {count} Medikamente werden knapp!", @@ -110,11 +110,11 @@ const translations: Record = { days: "Tage", runsOut: "Aufgebraucht", }, - footer: "πŸ€– Automatische Erinnerung von MedAssist", + footer: "πŸ€– Automatische Erinnerung von MedAssist-ng", }, intakeReminder: { - subject: "MedAssist: Einnahme-Erinnerung - {medications}", - title: "πŸ’Š MedAssist - Einnahme-Erinnerung", + subject: "MedAssist-ng: Einnahme-Erinnerung - {medications}", + title: "πŸ’Š MedAssist-ng - Einnahme-Erinnerung", description: "Zeit fΓΌr Ihre Medikamente in {minutes} Minuten:", alertSingle: "πŸ’Š 1 Medikament geplant", alertMultiple: "πŸ’Š {count} Medikamente geplant", @@ -124,11 +124,11 @@ const translations: Record = { time: "Uhrzeit", }, pills: "Tabletten", - footer: "MedAssist Medikamentenplaner", + footer: "MedAssist-ng Medikamentenplaner", }, push: { - stockTitle: "MedAssist: 1 Medikament wird knapp", - stockTitleMultiple: "MedAssist: {count} Medikamente werden knapp", + stockTitle: "MedAssist-ng: 1 Medikament wird knapp", + stockTitleMultiple: "MedAssist-ng: {count} Medikamente werden knapp", intakeTitle: "Einnahme-Erinnerung in {minutes} Min.", pillsLeft: "{count} Tabletten", daysLeft: "{count} Tage ΓΌbrig", diff --git a/backend/src/plugins/env.ts b/backend/src/plugins/env.ts index 7adb689..d37815b 100644 --- a/backend/src/plugins/env.ts +++ b/backend/src/plugins/env.ts @@ -6,13 +6,12 @@ dotenv.config({ path: process.env.DOTENV_PATH || ".env" }); const EnvSchema = z.object({ NODE_ENV: z.enum(["development", "production", "test"]).default("development"), PORT: z.string().transform((v) => parseInt(v, 10)).default("3000"), - DATABASE_URL: z.string().default("file:./data/medassist.db"), + DATABASE_URL: z.string().default("file:./data/medassist-ng.db"), CORS_ORIGINS: z.string().default("http://localhost:5173,http://localhost:4173"), LOG_LEVEL: z.string().default("info"), JWT_SECRET: z.string().min(10), REFRESH_SECRET: z.string().min(10), COOKIE_SECRET: z.string().min(10), - CSRF_SECRET: z.string().min(10), ACCESS_TOKEN_TTL_MIN: z.string().default("15"), REFRESH_TOKEN_TTL_DAYS: z.string().default("14"), }); diff --git a/backend/src/routes/planner.ts b/backend/src/routes/planner.ts index f63a905..3086328 100644 --- a/backend/src/routes/planner.ts +++ b/backend/src/routes/planner.ts @@ -97,7 +97,7 @@ export async function plannerRoutes(app: FastifyInstance) { const html = `
-

MedAssist - Demand Calculator

+

MedAssist-ng - Demand Calculator

Supply overview from ${fromDate} to ${untilDate}

-

Sent from MedAssist Medication Planner

+

Sent from MedAssist-ng Medication Planner

`; - const plainText = `MedAssist - Demand Calculator + const plainText = `MedAssist-ng - Demand Calculator Supply overview from ${fromDate} to ${untilDate} ${summaryText} @@ -142,7 +142,7 @@ ${summaryText} ${rows.map((r) => `${r.medicationName}: ${r.totalPills} pills in stock, ${r.plannerUsage} pills needed, ${r.stripsAvailable} blisters available (${r.stripsNeeded} needed) - ${r.enough ? "Enough" : "OUT OF STOCK"}`).join("\n")} --- -Sent from MedAssist Medication Planner`; +Sent from MedAssist-ng Medication Planner`; try { const transporter = nodemailer.createTransport({ @@ -158,7 +158,7 @@ Sent from MedAssist Medication Planner`; await transporter.sendMail({ from: smtpFrom, to: email, - subject: `MedAssist - Supply Overview (${fromDate} - ${untilDate})`, + subject: `MedAssist-ng - Supply Overview (${fromDate} - ${untilDate})`, text: plainText, html, }); @@ -208,7 +208,7 @@ Sent from MedAssist Medication Planner`; const html = `
-

⚠️ MedAssist - Reorder Reminder

+

⚠️ MedAssist-ng - Reorder Reminder

The following medications are running low and need to be reordered:

@@ -234,19 +234,19 @@ Sent from MedAssist Medication Planner`;

-

Sent from MedAssist Medication Planner

+

Sent from MedAssist-ng Medication Planner

`; - const plainText = `MedAssist - Reorder Reminder + const plainText = `MedAssist-ng - Reorder Reminder The following medications are running low: ${lowStock.map((r) => `${r.name}: ${r.medsLeft} pills left, ${r.daysLeft ?? 0} days remaining, runs out ${r.depletionDate ?? "soon"}`).join("\n")} --- -Sent from MedAssist Medication Planner`; +Sent from MedAssist-ng Medication Planner`; try { const transporter = nodemailer.createTransport({ @@ -262,7 +262,7 @@ Sent from MedAssist Medication Planner`; await transporter.sendMail({ from: smtpFrom, to: email, - subject: `⚠️ MedAssist - ${lowStock.length} Medication${lowStock.length > 1 ? "s" : ""} Running Low`, + subject: `⚠️ MedAssist-ng - ${lowStock.length} Medication${lowStock.length > 1 ? "s" : ""} Running Low`, text: plainText, html, }); diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 7a25026..327aae4 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -199,15 +199,15 @@ export async function settingsRoutes(app: FastifyInstance) { await transporter.sendMail({ from: smtpFrom, to: email, - subject: "MedAssist - Test Email", - text: "This is a test email from MedAssist. If you received this, your email configuration is working correctly!", + subject: "MedAssist-ng - Test Email", + text: "This is a test email from MedAssist-ng. If you received this, your email configuration is working correctly!", html: `
-

MedAssist - Test Email

-

This is a test email from MedAssist.

+

MedAssist-ng - Test Email

+

This is a test email from MedAssist-ng.

βœ“ If you received this, your email configuration is working correctly!


-

Sent from MedAssist Medication Planner

+

Sent from MedAssist-ng Medication Planner

`, }); @@ -228,7 +228,7 @@ export async function settingsRoutes(app: FastifyInstance) { } try { - const result = await sendShoutrrrNotification(url, "MedAssist Test", "This is a test notification from MedAssist. If you received this, your notification configuration is working correctly!"); + const result = await sendShoutrrrNotification(url, "MedAssist-ng Test", "This is a test notification from MedAssist-ng. If you received this, your notification configuration is working correctly!"); if (result.success) { return reply.send({ success: true, message: "Test notification sent successfully" }); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..ce83fbc --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,46 @@ +# ============================================================================= +# DEVELOPMENT DOCKER COMPOSE - Security Hardened +# ============================================================================= +# Note: Dev containers need write access to volumes for hot-reload. +# Production containers run as non-root with read-only filesystem. +# ============================================================================= + +services: + backend-dev: + image: node:22-slim + working_dir: /app + command: sh -c "npm install && npm run dev" + volumes: + - ./backend:/app + - backend_node_modules:/app/node_modules + - ./backend/data:/app/data + env_file: + - .env + ports: + - "3000:3000" + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\""] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend-dev: + image: node:22-slim + working_dir: /app + command: sh -c "npm install && npm run dev -- --host --port 5173" + volumes: + - ./frontend:/app + - frontend_node_modules:/app/node_modules + ports: + - "5173:5173" + security_opt: + - no-new-privileges:true + depends_on: + - backend-dev + +volumes: + backend_node_modules: + frontend_node_modules: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 3dd1c46..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,55 +0,0 @@ -# ============================================================================= -# PRODUCTION DOCKER COMPOSE - Security Hardened -# ============================================================================= - -services: - backend: - image: git.danielvolz.org/daniel/medassist/backend:0.0.1 - container_name: medassist-backend - env_file: - - .env - volumes: - - ./data:/app/data - ports: - - "4000:3000" - networks: - - medassist-net - # Security options - security_opt: - - no-new-privileges:true - read_only: true - tmpfs: - - /tmp:noexec,nosuid,size=64m - cap_drop: - - ALL - healthcheck: - test: ["CMD", "/nodejs/bin/node", "-e", "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 10s - - frontend: - image: git.danielvolz.org/daniel/medassist/frontend:0.0.1 - container_name: medassist-frontend - ports: - - "4174:8080" - networks: - - medassist-net - depends_on: - backend: - condition: service_healthy - # Security options - security_opt: - - no-new-privileges:true - read_only: true - tmpfs: - - /tmp:noexec,nosuid,size=64m - - /var/cache/nginx:noexec,nosuid,size=64m - - /var/run:noexec,nosuid,size=64m - cap_drop: - - ALL - -networks: - medassist-net: - driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 4184926..0f4408f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,46 +1,55 @@ # ============================================================================= -# DEVELOPMENT DOCKER COMPOSE - Security Hardened -# ============================================================================= -# Note: Dev containers need write access to volumes for hot-reload. -# Production containers run as non-root with read-only filesystem. +# PRODUCTION DOCKER COMPOSE - Security Hardened # ============================================================================= services: - backend-dev: - image: node:22-slim - working_dir: /app - command: sh -c "npm install && npm run dev" - volumes: - - ./backend:/app - - backend_node_modules:/app/node_modules - - ./backend/data:/app/data + backend: + image: git.danielvolz.org/daniel/medassist-ng/backend:0.0.1 + container_name: medassist-ng-backend env_file: - .env + volumes: + - ./data:/app/data ports: - - "3000:3000" + - "4000:3000" + networks: + - medassist-ng-net + # Security options security_opt: - no-new-privileges:true + read_only: true + tmpfs: + - /tmp:noexec,nosuid,size=64m + cap_drop: + - ALL healthcheck: - test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\""] + test: ["CMD", "/nodejs/bin/node", "-e", "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] interval: 30s timeout: 10s retries: 3 - start_period: 40s + start_period: 30s - frontend-dev: - image: node:22-slim - working_dir: /app - command: sh -c "npm install && npm run dev -- --host --port 5173" - volumes: - - ./frontend:/app - - frontend_node_modules:/app/node_modules + frontend: + image: git.danielvolz.org/daniel/medassist-ng/frontend:0.0.1 + container_name: medassist-ng-frontend ports: - - "5173:5173" + - "4174:8080" + networks: + - medassist-ng-net + depends_on: + backend: + condition: service_healthy + # Security options security_opt: - no-new-privileges:true - depends_on: - - backend-dev + read_only: true + tmpfs: + - /tmp:noexec,nosuid,size=64m + - /var/cache/nginx:noexec,nosuid,size=64m + - /var/run:noexec,nosuid,size=64m + cap_drop: + - ALL -volumes: - backend_node_modules: - frontend_node_modules: +networks: + medassist-ng-net: + driver: bridge diff --git a/frontend/index.html b/frontend/index.html index 229a5aa..5d05918 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - MedAssist + MedAssist-ng diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 15a5d70..f143f00 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -20,7 +20,7 @@ server { } location /api/ { - proxy_pass http://medassist-backend:3000/; + proxy_pass http://medassist-ng-backend:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 79c313b..c9a367f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "medassist-frontend", + "name": "medassist-ng-frontend", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "medassist-frontend", + "name": "medassist-ng-frontend", "version": "0.1.0", "dependencies": { "i18next": "^24.2.2", diff --git a/frontend/package.json b/frontend/package.json index a526afb..846b729 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "medassist-frontend", + "name": "medassist-ng-frontend", "private": true, "version": "0.1.0", "type": "module", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aa9727d..e747d98 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -575,7 +575,7 @@ export default function App() {
- MedAssist + MedAssist-ng

{pageInfo.eyebrow}

{pageInfo.title}

@@ -1640,7 +1640,7 @@ function generateICS(med: Medication) { ].filter(Boolean).join('\\n'); return `BEGIN:VEVENT -UID:medassist-${med.id}-${idx}@medassist +UID:medassist-ng-${med.id}-${idx}@medassist-ng DTSTAMP:${formatICSDate(new Date())} DTSTART:${formatICSDate(start)} DTEND:${formatICSDate(end)} @@ -1657,7 +1657,7 @@ END:VEVENT`; const ics = `BEGIN:VCALENDAR VERSION:2.0 -PRODID:-//MedAssist//Medication Schedule//EN +PRODID:-//MedAssist-ng//Medication Schedule//EN CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:${med.name} Schedule diff --git a/frontend/src/i18n/de.json b/frontend/src/i18n/de.json index 1732422..d0d3a88 100644 --- a/frontend/src/i18n/de.json +++ b/frontend/src/i18n/de.json @@ -8,11 +8,11 @@ }, "header": { "eyebrow": { - "overview": "MedAssist · Übersicht", - "inventory": "MedAssist · Inventar", - "planner": "MedAssist · Planer", - "settings": "MedAssist · Einstellungen", - "schedule": "MedAssist · Zeitplan" + "overview": "MedAssist-ng · Übersicht", + "inventory": "MedAssist-ng · Inventar", + "planner": "MedAssist-ng · Planer", + "settings": "MedAssist-ng · Einstellungen", + "schedule": "MedAssist-ng · Zeitplan" } }, "dashboard": { diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 0fe1480..f5d4859 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -8,11 +8,11 @@ }, "header": { "eyebrow": { - "overview": "MedAssist · Overview", - "inventory": "MedAssist · Inventory", - "planner": "MedAssist · Planner", - "settings": "MedAssist · Configuration", - "schedule": "MedAssist · Schedule" + "overview": "MedAssist-ng · Overview", + "inventory": "MedAssist-ng · Inventory", + "planner": "MedAssist-ng · Planner", + "settings": "MedAssist-ng · Configuration", + "schedule": "MedAssist-ng · Schedule" } }, "dashboard": { diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index d6e19ee..4a9c6e5 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -23,7 +23,7 @@ i18n detection: { order: ['localStorage', 'navigator'], caches: ['localStorage'], - lookupLocalStorage: 'medassist-language', + lookupLocalStorage: 'medassist-ng-language', }, }); diff --git a/package.json b/package.json index 8babbcc..2f4383b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "medassist-monorepo", + "name": "medassist-ng-monorepo", "private": true, "version": "0.1.0", "workspaces": [ diff --git a/scripts/push-images.sh b/scripts/push-images.sh deleted file mode 100755 index c6ca8b7..0000000 --- a/scripts/push-images.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Builds (optional) and pushes images to the registry. -# Required env: REGISTRY_TOKEN (registry access token). -# Optional env: REGISTRY_USER (defaults to token), REGISTRY_HOST (default git.danielvolz.org), PROJECT_PATH (default daniel/medassist), IMAGE_TAG (set via -v or prompt). -# Flag: -v to set image tag (e.g. -v 1.0.0). - -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) - -usage() { - cat >&2 <<'EOF' -Usage: REGISTRY_TOKEN=... [REGISTRY_USER=...] ./scripts/push-images.sh [-v ] - -Flow: - 1) Tag wÀhlen (per -v oder Auswahl/Prompt) - 2) Optional bauen (Backend/Frontend) - 3) Push bestÀtigen - -Options: - -v Set image tag (default: prompt if unset) - -h Show this help - -Env (can be supplied via .env): - REGISTRY_TOKEN Required registry access token - REGISTRY_USER Optional; defaults to REGISTRY_TOKEN - REGISTRY_HOST Default git.danielvolz.org - PROJECT_PATH Default daniel/medassist - IMAGE_TAG If set, used as default tag -EOF -} - -prompt_yes_no() { - local prompt="$1" default="$2" answer - local suffix="[y/N]" - [[ "$default" == "y" ]] && suffix="[Y/n]" - while true; do - read -r -p "$prompt $suffix " answer - answer=${answer:-$default} - case "$answer" in - y|Y) return 0 ;; - n|N) return 1 ;; - *) echo "Please answer y or n." ;; - esac - done -} - -select_tag() { - if [[ -n "${IMAGE_TAG:-}" ]]; then - echo "Using tag: $IMAGE_TAG" - return - fi - - mapfile -t tags < <(docker images --format '{{.Tag}}' medassist-backend 2>/dev/null | grep -v '' | sort -u) - - if ((${#tags[@]} > 0)); then - echo "Select tag to use:" - local i=1 - for t in "${tags[@]}"; do - echo " [$i] $t" - ((i++)) - done - echo " [n] Enter new tag" - read -r -p "Choice: " choice - if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#tags[@]} )); then - IMAGE_TAG="${tags[choice-1]}" - echo "Using tag: $IMAGE_TAG" - return - fi - fi - - while [[ -z "${IMAGE_TAG:-}" ]]; do - read -r -p "Enter tag (e.g. 1.0.0): " IMAGE_TAG - done - echo "Using tag: $IMAGE_TAG" -} - -if [[ -f "$REPO_ROOT/.env" ]]; then - set -a - # shellcheck source=/dev/null - source "$REPO_ROOT/.env" - set +a -fi - -REGISTRY_HOST=${REGISTRY_HOST:-git.danielvolz.org} -PROJECT_PATH=${PROJECT_PATH:-daniel/medassist} -IMAGE_TAG=${IMAGE_TAG:-} - -while getopts ":v:h" opt; do - case "$opt" in - v) - IMAGE_TAG="$OPTARG" - ;; - h) - usage - exit 0 - ;; - \?) - echo "Unknown option -$OPTARG" >&2 - usage - exit 1 - ;; - :) - echo "Option -$OPTARG requires an argument" >&2 - usage - exit 1 - ;; - esac -done - -select_tag - -REGISTRY_TOKEN=${REGISTRY_TOKEN:-} -REGISTRY_USER=${REGISTRY_USER:-$REGISTRY_TOKEN} - -if [[ -z "$REGISTRY_TOKEN" ]]; then - echo "Missing REGISTRY_TOKEN. Set it in your env or in .env." >&2 - usage - exit 1 -fi - -build_images() { - echo "Building medassist-backend:${IMAGE_TAG}..." - docker build -t "medassist-backend:${IMAGE_TAG}" "$REPO_ROOT/backend" - echo "Building medassist-frontend:${IMAGE_TAG}..." - docker build -t "medassist-frontend:${IMAGE_TAG}" "$REPO_ROOT/frontend" -} - -BACKEND_LOCAL="medassist-backend:${IMAGE_TAG}" -FRONTEND_LOCAL="medassist-frontend:${IMAGE_TAG}" -BACKEND_REMOTE="${REGISTRY_HOST}/${PROJECT_PATH}/backend:${IMAGE_TAG}" -FRONTEND_REMOTE="${REGISTRY_HOST}/${PROJECT_PATH}/frontend:${IMAGE_TAG}" - -update_compose_prod() { - local compose_file="$REPO_ROOT/docker-compose.prod.yml" - local sed_inplace - - case "$(uname -s)" in - Darwin*) sed_inplace=("-i" "") ;; - *) sed_inplace=("-i") ;; - esac - - if [[ -f "$compose_file" ]]; then - # Replace image tags in prod compose to the selected tag - sed "${sed_inplace[@]}" \ - -e "s|^\s*image: ${REGISTRY_HOST}/${PROJECT_PATH}/backend:.*| image: ${REGISTRY_HOST}/${PROJECT_PATH}/backend:${IMAGE_TAG}|" \ - -e "s|^\s*image: ${REGISTRY_HOST}/${PROJECT_PATH}/frontend:.*| image: ${REGISTRY_HOST}/${PROJECT_PATH}/frontend:${IMAGE_TAG}|" \ - "$compose_file" - echo "Updated docker-compose.prod.yml with tag ${IMAGE_TAG}." - else - echo "Warning: docker-compose.prod.yml not found; skipped updating tag." >&2 - fi -} - -built=0 -if prompt_yes_no "Build images for tag ${IMAGE_TAG}?" "y"; then - build_images - built=1 -else - echo "Skipping build. Using existing local images for tag ${IMAGE_TAG}." -fi - -push_default="n" -[[ $built -eq 1 ]] && push_default="y" - -if ! prompt_yes_no "Push images for tag ${IMAGE_TAG} to ${REGISTRY_HOST}/${PROJECT_PATH}?" "$push_default"; then - echo "Push cancelled." - exit 0 -fi - -printf 'Logging in to %s...\n' "$REGISTRY_HOST" -echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USER" --password-stdin - -docker tag "$BACKEND_LOCAL" "$BACKEND_REMOTE" -docker tag "$FRONTEND_LOCAL" "$FRONTEND_REMOTE" - -docker push "$BACKEND_REMOTE" -docker push "$FRONTEND_REMOTE" - -printf 'Pushed:\n %s\n %s\n' "$BACKEND_REMOTE" "$FRONTEND_REMOTE" - -update_compose_prod