diff --git a/README.md b/README.md index 796acdc..ee40282 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,13 @@ Detailed configuration references: Development setup and local commands are documented in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). +For cross-stack maintenance work and pre-PR validation, the repository root now exposes: + +```bash +npm run check +npm run build +``` + # Acknowledgements This project was inspired by [MedAssist](https://github.com/njic/medassist) by njic. diff --git a/backend/src/plugins/auth.ts b/backend/src/plugins/auth.ts index 44e8435..d6ce3ef 100644 --- a/backend/src/plugins/auth.ts +++ b/backend/src/plugins/auth.ts @@ -136,7 +136,7 @@ async function tryApiKeyAuth(request: FastifyRequest, reply: FastifyReply): Prom } const [user] = await db.select().from(users).where(eq(users.id, keyRow.userId)); - if (!user || !user.isActive) { + if (!user?.isActive) { reply.status(401).send({ error: "User not found", code: "USER_NOT_FOUND" }); throw new Error("USER_NOT_FOUND"); } diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 6b3a1e8..eb6f796 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -438,7 +438,7 @@ export async function authRoutes(app: FastifyInstance) { // Get user const [user] = await db.select().from(users).where(eq(users.id, decoded.sub)); - if (!user || !user.isActive) { + if (!user?.isActive) { return reply.status(401).send({ error: "User not found or disabled", code: "USER_INVALID" }); } diff --git a/backend/src/routes/doses.ts b/backend/src/routes/doses.ts index 7787c39..b18f34f 100644 --- a/backend/src/routes/doses.ts +++ b/backend/src/routes/doses.ts @@ -6,7 +6,7 @@ import { doseTracking, medications, shareTokens, userSettings } from "../db/sche import { getAnonymousUserId, requireAuth } from "../plugins/auth.js"; import { env } from "../plugins/env.js"; import { computeMedicationCurrentStock } from "../services/current-stock.js"; -import { dismissDosesForUser, markDoseTakenForUser } from "../services/dose-tracking-service.js"; +import { markDoseTakenForUser } from "../services/dose-tracking-service.js"; import type { AuthUser } from "../types/fastify.js"; import { applyOpenApiRouteStandards, diff --git a/backend/src/routes/export.ts b/backend/src/routes/export.ts index a716e79..53a8cc3 100644 --- a/backend/src/routes/export.ts +++ b/backend/src/routes/export.ts @@ -140,8 +140,6 @@ const settingsSchemaBase = z.object({ shareMedicationOverview: z.boolean().default(false), }); -const exportSettingsSchema = settingsSchemaBase.optional(); - const importSettingsSchema = settingsSchemaBase .extend({ // Accept the removed field from legacy exports so old backups still import, @@ -297,7 +295,7 @@ function imageToBase64(imageUrl: string | null): string | null { // Save base64 image to file and return filename function base64ToImage(base64: string, medicationId: number): string | null { - if (!base64 || !base64.startsWith("data:")) return null; + if (!base64.startsWith("data:")) return null; try { // Parse data URL: "data:image/jpeg;base64,/9j/4AAQ..." diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index a92f85c..997c527 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -11,7 +11,7 @@ Configure MedAssist with environment variables in `.env`. Start from `.env.examp | `PORT` | `3000` | Backend API port | | `CORS_ORIGINS` | `http://localhost:4174` | Allowed origins for CORS | | `TZ` | `Europe/Berlin` | Server default timezone for scheduled reminders | -| `PUBLIC_APP_URL` | — | Public base URL for notification action links | +| `PUBLIC_APP_URL` | — | Public base URL for notification action links. Must be reachable by phones, browsers, and notification providers; do not point this to `localhost` or an internal Docker hostname. | | `LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error`, or `silent` | | `RATE_LIMIT_MAX` | `100` | Maximum requests per minute per IP | | `OPENAPI_DOCS_ENABLED` | `auto` | Explicitly enable or disable `/docs` and `/docs/json` | @@ -108,6 +108,8 @@ Push notification setup, provider support, and URL examples are documented in [P Recommended provider: `ntfy`, especially for intake reminders with direct actions. +Notification action links use `PUBLIC_APP_URL` as their base URL. For self-hosted setups, this should normally be your externally reachable HTTPS address, for example `https://med.example.com`. + ## Default User Settings Default values for newly created users are documented in [DEFAULT_USER_SETTINGS.md](DEFAULT_USER_SETTINGS.md). diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 6b2f520..fb1a948 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -30,6 +30,17 @@ These development overrides are documented here intentionally and are not part o ```bash npm run lint +npm run check +npm run build cd backend && npm run test:run cd frontend && npm run test:run -``` \ No newline at end of file +``` + +Recommended local maintenance preflight before opening or updating a PR: + +```bash +npm run check +npm run build +``` + +Use the root-level commands for full-stack validation when a change spans backend and frontend. Keep using the package-local commands when you are validating only one slice. \ No newline at end of file diff --git a/docs/PUSH_NOTIFICATIONS.md b/docs/PUSH_NOTIFICATIONS.md index 1862eeb..e0977d6 100644 --- a/docs/PUSH_NOTIFICATIONS.md +++ b/docs/PUSH_NOTIFICATIONS.md @@ -25,6 +25,23 @@ When an ntfy intake action succeeds, MedAssist publishes the confirmation as the Configure push notifications in the app under `Settings -> Push`, or set defaults for new users with environment variables. +Notification action links such as `Take`, `Skip`, and `View` use `PUBLIC_APP_URL` as their base URL. Set this to the public MedAssist URL that the receiving device can actually reach. + +Good examples: + +```text +https://med.example.com +https://medtest.example.com +``` + +Bad examples for notification actions: + +```text +http://localhost:3000 +http://backend-dev:3000 +http://192.168.x.x:3000 +``` + Push-related default variables: | Variable | Default | Description | @@ -72,4 +89,22 @@ telegram://TOKEN@telegram?chats=CHAT_ID telegram://TOKEN@telegram?chats=@your_channel,-1001234567890 ``` -For all supported services and options, see the [Shoutrrr documentation](https://containrrr.dev/shoutrrr/v0.8/services/overview/). \ No newline at end of file +For all supported services and options, see the [Shoutrrr documentation](https://containrrr.dev/shoutrrr/v0.8/services/overview/). + +## Troubleshooting + +### ntfy `Take` / `Skip` fails with a connection timeout + +If the ntfy client shows an error such as `failed to connect to ... port 443`, the failure usually happens before MedAssist can process the action token. + +Check these points first: + +1. `PUBLIC_APP_URL` points to your real public MedAssist URL, not to `localhost`, a Docker service name, or another internal-only address. +2. The same URL opens from the same phone and network outside the notification flow. +3. If the failure only happens on your home Wi-Fi, retry once on mobile data. That strongly helps distinguish an app issue from missing NAT loopback / hairpin routing on the local network. + +### ntfy shows an old actionable entry after a successful action + +MedAssist updates the notification state after a successful ntfy action and removes the stale actionable entry using the original ntfy message ID when available. + +If an outdated actionable entry still remains visible, verify that the action actually reached MedAssist and that your ntfy server accepted both the confirmation publish and the follow-up delete of the original message. \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b50c10c..c261708 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { AboutModal, @@ -13,7 +14,17 @@ import { AppHeader } from "./components/AppHeader"; import { AuthPage, AuthProvider, useAuth } from "./components/Auth"; import { AppProvider, UnsavedChangesProvider, useAppContext, useShareContext } from "./context"; import { useScrollLock } from "./hooks/useScrollLock"; -import { DashboardPage, MedicationsPage, PlannerPage, SchedulePage, SettingsPage, SharedOverviewPage } from "./pages"; + +const DashboardPage = lazy(() => import("./pages/DashboardPage").then((module) => ({ default: module.DashboardPage }))); +const MedicationsPage = lazy(() => + import("./pages/MedicationsPage").then((module) => ({ default: module.MedicationsPage })) +); +const PlannerPage = lazy(() => import("./pages/PlannerPage").then((module) => ({ default: module.PlannerPage }))); +const SchedulePage = lazy(() => import("./pages/SchedulePage").then((module) => ({ default: module.SchedulePage }))); +const SettingsPage = lazy(() => import("./pages/SettingsPage").then((module) => ({ default: module.SettingsPage }))); +const SharedOverviewPage = lazy(() => + import("./pages/SharedOverviewPage").then((module) => ({ default: module.SharedOverviewPage })) +); // Vite injects this at build time from package.json declare const __APP_VERSION__: string; @@ -21,19 +32,27 @@ export const FRONTEND_VERSION = typeof __APP_VERSION__ !== "undefined" ? __APP_V const GITHUB_REPO = "DanielVolz/medassist-ng"; export const GITHUB_URL = `https://github.com/${GITHUB_REPO}`; +function RouteLoadingFallback() { + const { t } = useTranslation(); + + return