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.
This commit is contained in:
Daniel Volz
2025-12-24 13:01:53 +01:00
parent c5e03a787d
commit 738513a3ba
24 changed files with 254 additions and 374 deletions
+17 -16
View File
@@ -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
+8 -4
View File
@@ -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` |
+88 -29
View File
@@ -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
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "medassist-backend",
"name": "medassist-ng-backend",
"version": "0.1.0",
"private": true,
"type": "module",
+1 -1
View File
@@ -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:")) {
+1 -1
View File
@@ -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...");
+16 -16
View File
@@ -54,8 +54,8 @@ type TranslationKeys = {
const translations: Record<Language, TranslationKeys> = {
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<Language, TranslationKeys> = {
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<Language, TranslationKeys> = {
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<Language, TranslationKeys> = {
},
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<Language, TranslationKeys> = {
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<Language, TranslationKeys> = {
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",
+1 -2
View File
@@ -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"),
});
+10 -10
View File
@@ -97,7 +97,7 @@ export async function plannerRoutes(app: FastifyInstance) {
const html = `
<div style="font-family: system-ui, -apple-system, sans-serif; max-width: 100%; margin: 0 auto; padding: 12px; background: #f9fafb;">
<div style="background: white; border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h2 style="color: #1f2937; margin: 0 0 8px; font-size: 18px;">MedAssist - Demand Calculator</h2>
<h2 style="color: #1f2937; margin: 0 0 8px; font-size: 18px;">MedAssist-ng - Demand Calculator</h2>
<p style="color: #6b7280; margin: 0 0 16px; font-size: 13px;">Supply overview from <strong>${fromDate}</strong> to <strong>${untilDate}</strong></p>
<div style="padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; ${
@@ -129,12 +129,12 @@ export async function plannerRoutes(app: FastifyInstance) {
</div>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 16px 0;" />
<p style="color: #9ca3af; font-size: 11px; margin: 0;">Sent from MedAssist Medication Planner</p>
<p style="color: #9ca3af; font-size: 11px; margin: 0;">Sent from MedAssist-ng Medication Planner</p>
</div>
</div>
`;
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 = `
<div style="font-family: system-ui, -apple-system, sans-serif; max-width: 100%; margin: 0 auto; padding: 12px; background: #f9fafb;">
<div style="background: white; border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h2 style="color: #1f2937; margin: 0 0 8px; font-size: 18px;">⚠️ MedAssist - Reorder Reminder</h2>
<h2 style="color: #1f2937; margin: 0 0 8px; font-size: 18px;">⚠️ MedAssist-ng - Reorder Reminder</h2>
<p style="color: #6b7280; margin: 0 0 16px; font-size: 13px;">The following medications are running low and need to be reordered:</p>
<div style="padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; background: #fef2f2; border: 1px solid #fecaca;">
@@ -234,19 +234,19 @@ Sent from MedAssist Medication Planner`;
</div>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 16px 0;" />
<p style="color: #9ca3af; font-size: 11px; margin: 0;">Sent from MedAssist Medication Planner</p>
<p style="color: #9ca3af; font-size: 11px; margin: 0;">Sent from MedAssist-ng Medication Planner</p>
</div>
</div>
`;
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,
});
+6 -6
View File
@@ -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: `
<div style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #2563eb;">MedAssist - Test Email</h2>
<p>This is a test email from MedAssist.</p>
<h2 style="color: #2563eb;">MedAssist-ng - Test Email</h2>
<p>This is a test email from MedAssist-ng.</p>
<p style="color: #10b981; font-weight: 600;">✓ If you received this, your email configuration is working correctly!</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
<p style="color: #6b7280; font-size: 14px;">Sent from MedAssist Medication Planner</p>
<p style="color: #6b7280; font-size: 14px;">Sent from MedAssist-ng Medication Planner</p>
</div>
`,
});
@@ -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" });
+46
View File
@@ -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:
-55
View File
@@ -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
+37 -28
View File
@@ -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
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MedAssist</title>
<title>MedAssist-ng</title>
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "medassist-frontend",
"name": "medassist-ng-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
+3 -3
View File
@@ -575,7 +575,7 @@ export default function App() {
<main className="page">
<header className="hero">
<div className="hero-title">
<img src="/favicon.svg" alt="MedAssist" className="hero-logo" />
<img src="/favicon.svg" alt="MedAssist-ng" className="hero-logo" />
<div>
<p className="eyebrow">{pageInfo.eyebrow}</p>
<h1>{pageInfo.title}</h1>
@@ -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
+5 -5
View File
@@ -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": {
+5 -5
View File
@@ -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": {
+1 -1
View File
@@ -23,7 +23,7 @@ i18n
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
lookupLocalStorage: 'medassist-language',
lookupLocalStorage: 'medassist-ng-language',
},
});
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "medassist-monorepo",
"name": "medassist-ng-monorepo",
"private": true,
"version": "0.1.0",
"workspaces": [
-183
View File
@@ -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 <tag> 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 <tag>]
Flow:
1) Tag wählen (per -v oder Auswahl/Prompt)
2) Optional bauen (Backend/Frontend)
3) Push bestätigen
Options:
-v <tag> 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 '<none>' | 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