From c59c3069c436eded758f4eac574fcd39457604d9 Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Wed, 17 Jun 2026 08:32:28 +0200 Subject: [PATCH] refactor: remove KI-Orakel from history page and backend Drop oracle UI, API endpoint, scheduled generation, and related docs. Co-authored-by: Cursor --- .env.example | 2 +- README.md | 6 +-- public/app.js | 2 - public/index.html | 4 -- public/lib/api.js | 1 - public/styles.css | 6 +-- server/ai.py | 83 ------------------------------------------ server/app.py | 25 +------------ server/scheduler.py | 20 +--------- tools/regenerate_ai.py | 18 ++------- 10 files changed, 12 insertions(+), 155 deletions(-) diff --git a/.env.example b/.env.example index 87b2cc6..c2d0192 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,6 @@ VAPID_PRIVATE_KEY= VAPID_PUBLIC_KEY= VAPID_CLAIMS_EMAIL=mailto:admin@schwenk.online -# OpenRouter (optional — Roast-of-the-Day + KI-Orakel) +# OpenRouter (optional — Roast-of-the-Day) OPENROUTER_API_KEY= OPENROUTER_MODEL=google/gemini-2.0-flash-001 diff --git a/README.md b/README.md index 294e804..cbe17dc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ Persönliche Medikamenten-PWA mit dezenten Push-Erinnerungen, Einnahme-Logging u ### Spaß - **Dopamin-Drop** — Animation + Erfolgsspruch beim Loggen - **Roast-of-the-Day** — täglicher sarkastischer Spruch (OpenRouter, gecacht) -- **KI-Orakel** — wöchentlicher Compliance-Report (OpenRouter, gecacht) - **Easter Eggs** — Meilenstein-Badges (50/100 Logs, Streak 7/30/100) - **~95 Humor-Texte** in `messages.yaml` (Reminder, Success, Missed, …) @@ -102,7 +101,7 @@ Volumes: | `VAPID_PRIVATE_KEY` | Web-Push Private Key (PEM) | | `VAPID_PUBLIC_KEY` | Web-Push Public Key | | `VAPID_CLAIMS_EMAIL` | mailto:-Adresse für VAPID | -| `OPENROUTER_API_KEY` | Optional — für Roast + Orakel | +| `OPENROUTER_API_KEY` | Optional — für Roast-of-the-Day | | `OPENROUTER_MODEL` | Default: `google/gemini-2.0-flash-001` | ### `meds.yaml` @@ -144,7 +143,6 @@ Alle Endpunkte unter `/api/*`. Auth via `Authorization: Bearer ` (außer | GET | `/api/history?days=90` | Heatmap-Daten + Stats | | GET | `/api/stats` | Streak, Compliance, Meilensteine | | GET | `/api/roast` | Roast-of-the-Day | -| GET | `/api/oracle` | Wöchentliches KI-Orakel | | POST | `/api/push/subscribe` | Web-Push Subscription speichern | --- @@ -176,7 +174,7 @@ Notification-Actions: **Genommen ✓**, **+15 Min**, **+30 Min** | **Vanilla JS** statt React/Vue | Kein Build-Step, schnelle Iteration, passt zu persönlicher App | | **SQLite** statt Postgres | Single-User, eine Datei, Backup = `data/medis.sqlite` kopieren | | **Web Push vom Server** statt Client-Timer | Zuverlässig auch bei geschlossener App; APScheduler im Container | -| **OpenRouter nur für Roast + Orakel** | Kosten/Latenz — Notifications nutzen statische `messages.yaml` | +| **OpenRouter nur für Roast** | Kosten/Latenz — Notifications nutzen statische `messages.yaml` | | **Model: gemini-2.0-flash** | Günstig, schnell, gut genug für kurze deutsche Roasts | | **PIN plain in .env** statt Hash | Single-User, unkritische Daten; `hmac.compare_digest` gegen Timing-Leaks | | **JWT 90 Tage** | Lange Session auf persönlichem Gerät, kein ständiges PIN-Eingeben | diff --git a/public/app.js b/public/app.js index 10b1027..a305067 100644 --- a/public/app.js +++ b/public/app.js @@ -129,8 +129,6 @@ async function loadHistory() { const data = await api.history(90); renderHeatmap(data.days); updateStats(data.stats); - const oracle = await api.oracle().catch(() => ({ text: "Das Orakel schweigt. Wahrscheinlich enttäuscht." })); - document.getElementById("oracleText").textContent = oracle.text; } async function handleDeepLink() { diff --git a/public/index.html b/public/index.html index a7638a2..9f81e31 100644 --- a/public/index.html +++ b/public/index.html @@ -67,10 +67,6 @@
Genommen
-
-
🔮 KI-Orakel
-

Lade Orakel…

-

90-Tage Heatmap

diff --git a/public/lib/api.js b/public/lib/api.js index 70c5d8b..b958730 100644 --- a/public/lib/api.js +++ b/public/lib/api.js @@ -26,7 +26,6 @@ export const api = { history: (days = 90) => request(`/api/history?days=${days}`), stats: () => request("/api/stats"), roast: () => request("/api/roast"), - oracle: () => request("/api/oracle"), snooze: (slot_id, minutes) => request("/api/snooze", { method: "POST", body: JSON.stringify({ slot_id, minutes }) }), vapidKey: () => request("/api/vapid-public-key"), pushSubscribe: (subscription) => diff --git a/public/styles.css b/public/styles.css index 232d7cf..3bcf6fe 100644 --- a/public/styles.css +++ b/public/styles.css @@ -97,18 +97,18 @@ body { @keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } /* Cards */ -.roastCard, .oracleCard, .motivationCard { +.roastCard, .motivationCard { background: linear-gradient(135deg, var(--bg-card) 0%, #3a1855 100%); border: 2px solid var(--accent-purple); border-radius: var(--radius); padding: 1rem 1.25rem; margin-bottom: 1.25rem; box-shadow: 0 4px 20px rgba(124,77,255,.2); } -.roastLabel, .oracleLabel, .motivationLabel { +.roastLabel, .motivationLabel { font-size: .65rem; text-transform: uppercase; letter-spacing: .06em; line-height: 1.35; color: var(--accent-purple); margin-bottom: .5rem; font-weight: 600; } -.roastCard p, .oracleCard p, .motivationCard p { line-height: 1.5; font-size: .95rem; } +.roastCard p, .motivationCard p { line-height: 1.5; font-size: .95rem; } .slotCards { display: flex; flex-direction: column; gap: 1rem; } diff --git a/server/ai.py b/server/ai.py index d555447..6244c98 100644 --- a/server/ai.py +++ b/server/ai.py @@ -21,12 +21,6 @@ MOTIVATION_SYSTEM = ( "Genau ein Satz auf Deutsch, max 25 Wörter. Keine Anführungszeichen um den Spruch." ) -ORACLE_SYSTEM = ( - "Du bist das tägliche KI-Orakel einer ADHS-Medikamenten-App. " - "Passiv-aggressiv, trocken, max 4 Sätze auf Deutsch. Keine medizinischen Ratschläge." -) - - async def _get_cache(db: aiosqlite.Connection, key: str) -> str | None: cur = await db.execute("SELECT content FROM ai_cache WHERE key = ?", (key,)) row = await cur.fetchone() @@ -93,18 +87,6 @@ def _motivation_key(day: str) -> str: return f"motivation:v2:{day}" -def _oracle_key(day: str) -> str: - return f"oracle:v3:{day}" - - -async def _cached_oracle(db: aiosqlite.Connection, day: str) -> str | None: - cached = await _get_cache(db, _oracle_key(day)) - if cached: - return cached - week = datetime.strptime(day, "%Y-%m-%d").strftime("%Y-W%W") - return await _get_cache(db, f"oracle:v2:{week}") - - async def _cached_motivation(db: aiosqlite.Connection, day: str) -> str | None: cached = await _get_cache(db, _motivation_key(day)) if cached: @@ -154,68 +136,3 @@ async def get_daily_motivation(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[s async def get_roast_of_the_day(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[str, Any]: return await get_daily_motivation(db, tz) - - -async def generate_oracle( - db: aiosqlite.Connection, - tz: ZoneInfo, - day: str | None = None, - *, - force: bool = False, -) -> dict[str, Any]: - day = day or datetime.now(tz).strftime("%Y-%m-%d") - key = _oracle_key(day) - if not force: - cached = await _cached_oracle(db, day) - if cached: - return {"text": cached, "source": "cache", "day": day} - - stats = await compute_stats(db) - days_active = stats["days_active"] - window = stats["compliance_window_days"] - young_app = days_active < 90 - context = ( - f"App aktiv seit {days_active} Tag(en), erste Einnahme am {stats['first_day']}. " - if young_app - else "" - ) - caveat = ( - "Die App ist noch jung — lange Streaks oder 90-Tage-Compliance sind noch nicht realistisch erreichbar. " - "Bewerte nur die verfügbaren Daten, sei fair aber sarkastisch. " - if young_app - else "" - ) - prompt = ( - f"Täglicher Orakel-Report für heute. {context}{caveat}" - f"Streak: {stats['streak']} Tage. " - f"Compliance über {window} Tag(e): {stats['compliance_percent']}%. " - f"Genommen gesamt: {stats['total_taken']}." - ) - text = await _call_openrouter(prompt, system=ORACLE_SYSTEM) - if not text: - if young_app: - text = ( - f"Tag {days_active} deiner Medis-Karriere. " - f"Streak: {stats['streak']}. Compliance ({window} Tage): {stats['compliance_percent']}%. " - "Das Orakel ist beeindruckt — oder gelangweilt. Grenzwertig." - ) - else: - text = pick("streak", streak=stats["streak"]) + f" Compliance: {stats['compliance_percent']}%." - source = "fallback" - else: - source = "ai" - await _set_cache(db, key, text) - logger.info("Generated daily oracle for %s (%s)", day, source) - return {"text": text, "source": source, "day": day} - - -async def get_oracle(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[str, Any]: - day = datetime.now(tz).strftime("%Y-%m-%d") - cached = await _cached_oracle(db, day) - if cached: - return {"text": cached, "source": "cache", "day": day} - return { - "text": "Das Orakel bereitet sich vor. Schau später nochmal rein.", - "source": "pending", - "day": day, - } diff --git a/server/app.py b/server/app.py index d5955e1..739f1fd 100644 --- a/server/app.py +++ b/server/app.py @@ -11,7 +11,7 @@ from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles -from server.ai import generate_daily_motivation, generate_oracle, get_oracle, get_roast_of_the_day +from server.ai import generate_daily_motivation, get_roast_of_the_day from server.auth import create_token, require_auth, verify_pin from server.config_loader import load_meds_config, slot_to_dict, today_str from server.db import ensure_schema, get_db, new_id, utc_now_iso @@ -51,17 +51,6 @@ async def _ensure_today_motivation() -> None: await db.close() -async def _ensure_today_oracle() -> None: - config = load_meds_config() - db = await get_db() - try: - await generate_oracle(db, config.timezone) - except Exception: - logger.exception("Startup oracle generation failed") - finally: - await db.close() - - @asynccontextmanager async def lifespan(app: FastAPI): db = await get_db() @@ -69,7 +58,6 @@ async def lifespan(app: FastAPI): await db.close() start_scheduler() asyncio.create_task(_ensure_today_motivation()) - asyncio.create_task(_ensure_today_oracle()) yield @@ -219,17 +207,6 @@ async def get_roast(_: dict = Depends(require_auth)) -> JSONResponse: return JSONResponse(data, headers=NO_STORE) -@app.get("/api/oracle") -async def get_oracle_route(_: dict = Depends(require_auth)) -> JSONResponse: - config = load_meds_config() - db = await get_db() - try: - data = await get_oracle(db, config.timezone) - finally: - await db.close() - return JSONResponse(data, headers=NO_STORE) - - # Static files — must be after API routes PUBLIC = pathlib.Path(settings.public_dir) if PUBLIC.exists(): diff --git a/server/scheduler.py b/server/scheduler.py index 0e73f5a..081254b 100644 --- a/server/scheduler.py +++ b/server/scheduler.py @@ -9,7 +9,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger -from server.ai import generate_daily_motivation, generate_oracle +from server.ai import generate_daily_motivation from server.config_loader import load_meds_config, slot_to_dict, today_str from server.db import get_db, utc_now_iso from server.messages import pick @@ -54,17 +54,6 @@ async def _generate_daily_motivation() -> None: await db.close() -async def _generate_daily_oracle() -> None: - config = load_meds_config() - db = await get_db() - try: - await generate_oracle(db, config.timezone) - except Exception: - logger.exception("Daily oracle generation failed") - finally: - await db.close() - - async def _evening_check() -> None: logger.info("Evening check triggered") db = await get_db() @@ -158,13 +147,6 @@ def start_scheduler() -> None: replace_existing=True, ) - scheduler.add_job( - _generate_daily_oracle, - trigger=CronTrigger(hour=3, minute=5, timezone=tz), - id="daily-oracle", - replace_existing=True, - ) - if not scheduler.running: scheduler.start() logger.info("Scheduler started for timezone %s", tz) diff --git a/tools/regenerate_ai.py b/tools/regenerate_ai.py index 3db6cda..1204ca4 100644 --- a/tools/regenerate_ai.py +++ b/tools/regenerate_ai.py @@ -1,36 +1,26 @@ #!/usr/bin/env python3 -"""Regenerate cached AI texts (motivation + oracle).""" +"""Regenerate cached AI texts (motivation).""" from __future__ import annotations import argparse import asyncio -from server.ai import generate_daily_motivation, generate_oracle +from server.ai import generate_daily_motivation from server.config_loader import load_meds_config from server.db import get_db async def main() -> None: parser = argparse.ArgumentParser(description="Regenerate cached AI texts") - parser.add_argument("--motivation", action="store_true", help="Regenerate today's motivation") - parser.add_argument("--oracle", action="store_true", help="Regenerate today's oracle") parser.add_argument("--force", action="store_true", help="Regenerate even if cache exists") args = parser.parse_args() - if not args.motivation and not args.oracle: - args.motivation = True - args.oracle = True - config = load_meds_config() db = await get_db() try: - if args.motivation: - result = await generate_daily_motivation(db, config.timezone, force=args.force) - print("motivation:", result) - if args.oracle: - result = await generate_oracle(db, config.timezone, force=args.force) - print("oracle:", result) + result = await generate_daily_motivation(db, config.timezone, force=args.force) + print("motivation:", result) finally: await db.close()