diff --git a/CLAUDE.md b/CLAUDE.md index 9c41a24..cfdf453 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,16 @@ There is **no server-side email sending anywhere in the app** — no SMTP client - **Hard limitation, not a bug**: `mailto:` cannot carry attachments — this is a browser/OS security restriction, not something this app can work around. The email body includes a bracketed reminder ("Bitte die heruntergeladene Datei „…" anhängen, dann diese Zeile löschen") and the modal's info box explains the same thing before sending, so the manual-attach step is expected UX, not a silent gap. - If you add a third document type that needs "send by email", follow the same pattern: reuse the existing PDF-blob-download code for that document, then call the same `mailto:` construction — don't reintroduce a server-side send path. +## Update-Benachrichtigung / Changelog + +A per-account "what's new" system tells a user, once per account, what changed since they last looked — needed now that the app ships as versioned Docker images rather than one continuously-running dev process (see "Docker deployment" above). + +- `changelog.py` — a static data module (same pattern as `katalog.py`/`krankenkassen.py`): `CHANGELOG`, a list of `{version, datum, titel, punkte}` entries, always kept **newest-first**. `APP_VERSION` is derived automatically as `CHANGELOG[0]["version"]` — there is deliberately no separate version constant to remember to bump. **To ship a release note: add one new entry to the top of `CHANGELOG` and nothing else.** +- `nutzer.letzte_gesehene_version` (TEXT, nullable) tracks the last version each account has acknowledged. `_changelog_ungesehen(letzte_gesehene_version)` in `app.py` walks `CHANGELOG` from the top and collects entries until it hits the stored version (or returns everything if the value is `None`/not found — "show more rather than risk hiding something"). +- **New vs. existing accounts are handled differently on purpose**: every `INSERT INTO nutzer` (default admin bootstrap, demo bootstrap, and `POST /api/nutzer` for admin-created accounts) sets `letzte_gesehene_version = APP_VERSION` immediately, so a brand-new account starts "caught up" and is never shown historical entries it never lived through. Existing accounts, by contrast, get the column via the usual `ALTER TABLE ADD COLUMN` migration (see schema-evolution pattern above), which leaves it `NULL` — so on the release where this shipped, and on any future upgrade of a running deployment, already-existing accounts correctly see whatever's newer than what they've acknowledged. Don't backfill existing rows to "already seen" — that would silently suppress the exact notice this feature exists to show. +- `GET /api/auth/me` carries `app_version` and `changelog_ungesehen` (the actual unseen entries, not just a flag) on every call, so the frontend needs no extra round-trip to decide whether to show anything. `POST /api/changelog/gelesen` marks the current `APP_VERSION` as seen for the logged-in user. `GET /api/changelog` returns the full history (used by the standalone page, see below) and is intentionally *not* on the public allowlist — it's only reachable once logged in, same as the rest of the app shell. +- Frontend: `initApp()` shows a modal (`showChangelogModal()`, built on the existing `OM()` modal helper) with any unseen entries right after login/app-load, unless `aboBlockiert` is true (a payment-blocked account gets sent to `meinabo` instead — no point distracting it with release notes it can't act on yet; it'll see the notice on its next unblocked login). A sidebar nav item **"Was ist neu?"** (`pgChangelog()`, under the `Konto` section next to `Mein Abo`) shows the full history at any time and carries a small red `#cl-badge` dot (matching the existing `#abadge`/`#kbadge`/`#tb-reminder-badge` pattern) while there's anything unseen; visiting the page itself also calls `/api/changelog/gelesen`, so opening the history counts as acknowledging it too, not just clicking "Gelesen" in the modal. + ## Demo account A permanent `demo`/`demo` account (`nutzer.ist_demo=1`) exists for showing the app to prospects, created automatically in `init_db()` if missing. It's a completely ordinary `nutzer`-role account — no special-casing in the isolation model — with two exceptions layered on top: diff --git a/open.md b/open.md index 3340956..7a07dbf 100644 --- a/open.md +++ b/open.md @@ -16,11 +16,12 @@ Reihenfolge unten nach Abhängigkeit/Aufwand sortiert (nicht nach ursprüngliche - ~~Ports müssen über Variablen konfigurierbar sein, nicht hart codiert~~ - Umsetzung: `Dockerfile` + `docker-compose.yml` + `.env` (gitignored) + `requirements.txt` in `pflegepilot_alpha_v2/pflegepilot/`. Ein Service (SQLite ist eingebettet, kein eigener DB-Server nötig/sinnvoll ohne Postgres-Rewrite); Isolation stattdessen über zwei benannte Docker-Volumes (`pflegepilot_db`, `pflegepilot_static`), die App-Container-Rebuilds überleben. `.env` enthält `SECRET_KEY` (zufällig generiert), `APP_PORT` (Host-Port, Default 5050), `GUNICORN_WORKERS` — läuft produktiv über Gunicorn statt Flask-Dev-Server. Bewusst keine Domain im Code/`.env` hinterlegt, da die DynDNS-Domain erst nachträglich auf die Testbench gelegt wird — der Container kennt nur seinen Port. Nebenbei einen echten Bug in der DB-Migrationsreihenfolge gefunden und behoben (`nutzer`-Tabelle wurde zu spät angelegt, fiel nur auf einer wirklich leeren DB auf — z.B. frisches Docker-Volume). Details siehe CLAUDE.md, Abschnitt "Docker deployment". Getestet: Build, Start, Login (admin+demo), Neustart mit Datenpersistenz über die Volumes, sauberer Boot auf komplett leerer DB. -- [ ] **3. Update-Benachrichtigung / Changelog-System** - - Nach dem ersten Login nach einem Update: kurzer anklickbarer Hinweis, was sich geändert hat - - Ein Klick auf "Gelesen" reicht, um den Hinweis zu schließen - - Optional: dauerhaftes Icon in der Sidebar, um alle vergangenen Changelogs als Historie nachzulesen - - Setzt eine einfache Versionierung des Deployments voraus (siehe Punkt 4 unten), damit die App weiß, "was ist neu seit dem letzten Login" +- [x] ~~**3. Update-Benachrichtigung / Changelog-System**~~ *(erledigt)* + - ~~Nach dem ersten Login nach einem Update: kurzer anklickbarer Hinweis, was sich geändert hat~~ + - ~~Ein Klick auf "Gelesen" reicht, um den Hinweis zu schließen~~ + - ~~Optional: dauerhaftes Icon in der Sidebar, um alle vergangenen Changelogs als Historie nachzulesen~~ + - ~~Setzt eine einfache Versionierung des Deployments voraus, damit die App weiß, "was ist neu seit dem letzten Login"~~ + - Umsetzung: `changelog.py` (neues, statisches Datenmodul wie `katalog.py`) enthält die Liste aller Einträge, neueste zuerst; die App-Version wird automatisch aus dem obersten Eintrag abgeleitet – für ein Release reicht ein neuer Eintrag ganz oben, sonst nichts. Jedes Konto merkt sich (`nutzer.letzte_gesehene_version`), was es schon gesehen hat; nach Login erscheint einmalig ein Modal mit allem Neuen seit dem letzten Stand, mit "Gelesen"-Button. Neu angelegte Konten starten bewusst "auf aktuellem Stand" (kein Rückblick auf Änderungen, die sie nie erlebt haben), bestehende Konten sehen bei einem echten Update korrekt den neuen Hinweis. Zusätzlich ein dauerhafter Sidebar-Eintrag "Was ist neu?" mit Verlaufsansicht und rotem Punkt bei Ungelesenem. Details siehe CLAUDE.md, Abschnitt "Update-Benachrichtigung / Changelog". Getestet: Modal nach Login, Badge verschwindet nach "Gelesen", Verlaufsseite, neu angelegtes Konto sieht keinen Hinweis, funktioniert identisch lokal und im Docker-Container (inkl. Upgrade-Fall mit bestehendem Volume). - [ ] **4. ⚠️ Separates Grossvorhaben: Migration zu React + PostgreSQL** - Vollständiger Technologiewechsel weg von Flask/vanilla-JS/SQLite — deutlich grösserer Umfang als die Punkte oben, keine Nebenbei-Aufgabe diff --git a/pflegepilot_alpha_v2/pflegepilot/Dockerfile b/pflegepilot_alpha_v2/pflegepilot/Dockerfile index cdf052d..f3fe80e 100644 --- a/pflegepilot_alpha_v2/pflegepilot/Dockerfile +++ b/pflegepilot_alpha_v2/pflegepilot/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY app.py katalog.py krankenkassen.py start.py ./ +COPY app.py katalog.py krankenkassen.py changelog.py start.py ./ COPY db/schema.sql db/schema.sql COPY templates/ templates/ diff --git a/pflegepilot_alpha_v2/pflegepilot/app.py b/pflegepilot_alpha_v2/pflegepilot/app.py index d37e578..293e645 100644 --- a/pflegepilot_alpha_v2/pflegepilot/app.py +++ b/pflegepilot_alpha_v2/pflegepilot/app.py @@ -11,6 +11,7 @@ from katalog import (KATALOG, get_all, get_by_zielgruppe, get_by_pflegegrad, get_by_kategorie, get_kategorien, search, get_by_id, get_widerspruch_paragraphen) from krankenkassen import KRANKENKASSEN, get_by_name as kk_by_name, get_by_land as kk_by_land +from changelog import CHANGELOG, APP_VERSION BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "db", "pflegepilot.db") @@ -57,6 +58,7 @@ def init_db(): abo_bis TEXT, abo_typ TEXT, ist_demo INTEGER DEFAULT 0, + letzte_gesehene_version TEXT, erstellt_am TEXT DEFAULT (datetime('now','localtime')) )""", "ALTER TABLE patienten ADD COLUMN zielgruppe TEXT DEFAULT 'alterspflege'", @@ -75,6 +77,7 @@ def init_db(): "ALTER TABLE nutzer ADD COLUMN abo_bis TEXT", "ALTER TABLE nutzer ADD COLUMN abo_typ TEXT", "ALTER TABLE nutzer ADD COLUMN ist_demo INTEGER DEFAULT 0", + "ALTER TABLE nutzer ADD COLUMN letzte_gesehene_version TEXT", """CREATE TABLE IF NOT EXISTS admin_audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, admin_id INTEGER, @@ -113,18 +116,22 @@ def init_db(): except Exception: pass # Default-Admin anlegen wenn keine Nutzer vorhanden + # (letzte_gesehene_version = APP_VERSION: ein brandneues Konto startet + # "auf dem aktuellen Stand", statt sofort mit dem Update-Hinweis begruesst + # zu werden - der ist nur fuer bereits bestehende Konten gedacht, die ein + # echtes Update miterleben. Siehe _changelog_ungesehen().) if not db.execute("SELECT id FROM nutzer LIMIT 1").fetchone(): db.execute( - "INSERT INTO nutzer (benutzername, passwort_hash, name, rolle) VALUES (?,?,?,?)", - ("admin", generate_password_hash("admin"), "Administrator", "admin") + "INSERT INTO nutzer (benutzername, passwort_hash, name, rolle, letzte_gesehene_version) VALUES (?,?,?,?,?)", + ("admin", generate_password_hash("admin"), "Administrator", "admin", APP_VERSION) ) db.commit() # Demo-Konto anlegen, falls noch keins existiert (fuer Vorfuehrungen - # eigener Nutzer, ganz normal durch die bestehende Isolation gedeckt) if not db.execute("SELECT id FROM nutzer WHERE benutzername='demo'").fetchone(): db.execute( - "INSERT INTO nutzer (benutzername, passwort_hash, name, rolle, ist_demo) VALUES (?,?,?,?,1)", - ("demo", generate_password_hash("demo"), "Demo-Konto", "nutzer") + "INSERT INTO nutzer (benutzername, passwort_hash, name, rolle, ist_demo, letzte_gesehene_version) VALUES (?,?,?,?,1,?)", + ("demo", generate_password_hash("demo"), "Demo-Konto", "nutzer", APP_VERSION) ) db.commit() if _demo_reset_faellig(): @@ -145,11 +152,29 @@ def _get_current_user(): return None try: return get_db().execute( - "SELECT id, benutzername, name, rolle, abo_bis, abo_typ, ist_demo FROM nutzer WHERE id=? AND aktiv=1", (uid,) + "SELECT id, benutzername, name, rolle, abo_bis, abo_typ, ist_demo, letzte_gesehene_version FROM nutzer WHERE id=? AND aktiv=1", (uid,) ).fetchone() except Exception: return None +def _changelog_ungesehen(letzte_gesehene_version): + """Changelog-Einträge, die neuer sind als der zuletzt gesehene Stand eines + Nutzers. CHANGELOG ist immer neueste-zuerst sortiert (siehe changelog.py): + ist noch nichts gesehen (None), gilt der komplette aktuelle Verlauf als neu.""" + if not letzte_gesehene_version: + return CHANGELOG + ungesehen = [] + for eintrag in CHANGELOG: + if eintrag["version"] == letzte_gesehene_version: + break + ungesehen.append(eintrag) + else: + # zuletzt gesehene Version steht nicht mehr im Verlauf (sollte nicht + # vorkommen, da Einträge nie geloescht werden) - im Zweifel lieber + # alles zeigen als etwas verschlucken + return CHANGELOG + return ungesehen + def _check_admin(): u = _get_current_user() return u is not None and u["rolle"] == "admin" @@ -377,7 +402,9 @@ def api_auth_me(): if not u: return jsonify({"error": "Nicht angemeldet"}), 401 return jsonify({"id": u["id"], "benutzername": u["benutzername"], - "name": u["name"] or u["benutzername"], "rolle": u["rolle"], "ist_demo": bool(u["ist_demo"])}) + "name": u["name"] or u["benutzername"], "rolle": u["rolle"], "ist_demo": bool(u["ist_demo"]), + "app_version": APP_VERSION, + "changelog_ungesehen": _changelog_ungesehen(u["letzte_gesehene_version"])}) @app.route("/api/auth/login", methods=["POST"]) def api_auth_login(): @@ -394,6 +421,18 @@ def api_auth_login(): _demo_reset() return jsonify({"ok": True, "name": u["name"] or u["benutzername"], "rolle": u["rolle"], "ist_demo": bool(u["ist_demo"])}) +@app.route("/api/changelog", methods=["GET"]) +def api_changelog(): + return jsonify({"version": APP_VERSION, "eintraege": CHANGELOG}) + +@app.route("/api/changelog/gelesen", methods=["POST"]) +def api_changelog_gelesen(): + u = _get_current_user() + db = get_db() + db.execute("UPDATE nutzer SET letzte_gesehene_version=? WHERE id=?", (APP_VERSION, u["id"])) + db.commit() + return jsonify({"ok": True}) + @app.route("/api/auth/logout", methods=["POST"]) def api_auth_logout(): session.clear() @@ -472,9 +511,9 @@ def api_nutzer_create(): db = get_db() try: cur = db.execute( - "INSERT INTO nutzer (benutzername, passwort_hash, name, rolle) VALUES (?,?,?,?)", + "INSERT INTO nutzer (benutzername, passwort_hash, name, rolle, letzte_gesehene_version) VALUES (?,?,?,?,?)", (benutzername, generate_password_hash(passwort), - d.get("name", ""), d.get("rolle", "nutzer")) + d.get("name", ""), d.get("rolle", "nutzer"), APP_VERSION) ) db.commit() return jsonify({"ok": True, "id": cur.lastrowid}) diff --git a/pflegepilot_alpha_v2/pflegepilot/changelog.py b/pflegepilot_alpha_v2/pflegepilot/changelog.py new file mode 100644 index 0000000..69d1152 --- /dev/null +++ b/pflegepilot_alpha_v2/pflegepilot/changelog.py @@ -0,0 +1,24 @@ +""" +Pflegepilot – Update-Verlauf. + +Neuer Eintrag = ganz oben in CHANGELOG einfügen (Liste ist immer neueste-zuerst +sortiert). APP_VERSION wird automatisch aus dem ersten Eintrag abgeleitet - +sonst nichts weiter anzupassen. Jeder Nutzer sieht nach einem Update genau +einmal einen Hinweis mit allen Einträgen neuer als sein zuletzt gesehener +Stand (siehe nutzer.letzte_gesehene_version in app.py). +""" + +CHANGELOG = [ + { + "version": "1.0.0", + "datum": "2026-07-18", + "titel": "Docker-Betrieb, neuer E-Mail-Versand & Update-Hinweise", + "punkte": [ + "E-Mails werden jetzt über das eigene E-Mail-Programm verschickt (Mail auf dem iPhone, Gmail/Outlook auf Android, Standardprogramm am PC) statt über einen zentralen Versand.", + "Die App läuft jetzt containerisiert (Docker) und lässt sich unabhängig von der Datenbank aktualisieren.", + "Neu: dieser Update-Hinweis hier – informiert ab sofort nach jedem Update kurz über Neuerungen.", + ], + }, +] + +APP_VERSION = CHANGELOG[0]["version"] diff --git a/pflegepilot_alpha_v2/pflegepilot/templates/index.html b/pflegepilot_alpha_v2/pflegepilot/templates/index.html index cc0e899..2d64f24 100644 --- a/pflegepilot_alpha_v2/pflegepilot/templates/index.html +++ b/pflegepilot_alpha_v2/pflegepilot/templates/index.html @@ -416,6 +416,7 @@ textarea{resize:vertical;min-height:80px}
Fristen & Kalender
Konto
Mein Abo
+
Was ist neu?
BackendAdmin
@@ -482,7 +483,7 @@ function go(name,extra){ if(el)el.classList.add('on'); ({dashboard:pgDash,patienten:pgPatienten,neu:pgNeu,katalog:pgKatalog,rechner:pgRechner, antrag:pgAntrag,widerspruch:pgWiderspruch,musterbrief:pgMuster,aufgaben:pgAufgaben,tagebuch:pgTagebuch, - kalender:pgKalender,backend:pgBackend,admindash:pgAdminDash,meinabo:pgMeinAbo})[name]?.(extra); + kalender:pgKalender,backend:pgBackend,admindash:pgAdminDash,meinabo:pgMeinAbo,changelog:pgChangelog})[name]?.(extra); } async function api(path,opts={}){ const r=await fetch(API+path,{headers:{'Content-Type':'application/json'},...opts}); @@ -1159,6 +1160,13 @@ async function initApp(){ // Admin-nav nur für admin sichtbar const adm=document.querySelector('.sb-admin'); if(adm)adm.style.display=me.rolle==='admin'?'':'none'; + // Update-Hinweis: einmalig nach einem Update anzeigen, bis "Gelesen" geklickt wird. + // Waehrend eines Abo-Sperre keine Ablenkung - kommt beim naechsten Login dran. + const clBadge=document.getElementById('cl-badge'); + if(clBadge)clBadge.style.display=(me.changelog_ungesehen&&me.changelog_ungesehen.length)?'':'none'; + if(!aboBlockiert&&me.changelog_ungesehen&&me.changelog_ungesehen.length){ + showChangelogModal(me.changelog_ungesehen); + } // Öffentliche Einstellungen / Branding try{ const s=await api('/api/einstellungen/public'); @@ -1232,6 +1240,32 @@ async function aboJetztBezahlen(){ toast(r.error||'Zahlung folgt in Kürze',false); } +// ── Update-Hinweis / Changelog ──────────────────────────── +function clEntriesHtml(eintraege){ + return eintraege.map(e=> + '
v'+e.version+''+fdate(e.datum)+'
'+ + '
'+e.titel+'
'+ + '
' + ).join('
'); +} +function showChangelogModal(eintraege){ + OM(' Was ist neu?', + clEntriesHtml(eintraege)+ + ''); +} +async function changelogGelesen(){ + await api('/api/changelog/gelesen',{method:'POST'}); + document.getElementById('cl-badge')?.style.setProperty('display','none'); + CM(); +} +async function pgChangelog(){ + R('
Lade…
','Was ist neu?'); + const d=await api('/api/changelog'); + R('
'+clEntriesHtml(d.eintraege)+'
','Was ist neu?'); + await api('/api/changelog/gelesen',{method:'POST'}); + document.getElementById('cl-badge')?.style.setProperty('display','none'); +} + // ── Backend / Admin-Einstellungen ───────────────────────── async function pgBackend(){ if(!curUser||curUser.rolle!=='admin'){