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:
+17
-16
@@ -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
|
||||
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"name": "medassist-backend",
|
||||
"name": "medassist-ng-backend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -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:")) {
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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:
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Generated
+2
-2
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"name": "medassist-frontend",
|
||||
"name": "medassist-ng-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -23,7 +23,7 @@ i18n
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
lookupLocalStorage: 'medassist-language',
|
||||
lookupLocalStorage: 'medassist-ng-language',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "medassist-monorepo",
|
||||
"name": "medassist-ng-monorepo",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"workspaces": [
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user