From 644b2304b391522e6574fcb0088ba70b4d625ef8 Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Tue, 22 Sep 2026 14:57:16 +0200 Subject: [PATCH] docs: add Agent API docs, OpenAPI field descriptions, and project context files --- AGENTS.md | 298 ++++++++++++++++++++++++++++++++++++++++++++ AGENT_API.md | 260 ++++++++++++++++++++++++++++++++++++++ BOUNDARIES.md | 89 +++++++++++++ INFRASTRUCTURE.md | 141 +++++++++++++++++++++ MOOD.md | 107 ++++++++++++++++ NOTES.md | 5 + SOUL.md | 203 ++++++++++++++++++++++++++++++ STANDARDS.md | 130 +++++++++++++++++++ USER.md | 100 +++++++++++++++ server/api/agent.py | 33 ++++- server/app.py | 8 +- server/schemas.py | 102 ++++++++++++--- 12 files changed, 1451 insertions(+), 25 deletions(-) create mode 100644 AGENTS.md create mode 100644 AGENT_API.md create mode 100644 BOUNDARIES.md create mode 100644 INFRASTRUCTURE.md create mode 100644 MOOD.md create mode 100644 NOTES.md create mode 100644 SOUL.md create mode 100644 STANDARDS.md create mode 100644 USER.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bbb9ffc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,298 @@ + +# AGENTS.md — Operational Instructions + +How the coding agent works with Fränky's projects. + +Also read (project root, next to this file): `INFRASTRUCTURE.md`, `STANDARDS.md`, `BOUNDARIES.md`, project `SOUL.md`, project `MOOD.md`. +Persona & preferences: `USER.md` (Tier 2). + +--- + +## Rule Priority + +Highest wins. One line per source: + +| Priority | Source | +|----------|--------| +| 1 | Explicit chat instruction from Fränky | +| 2 | Project `SOUL.md` / project-local `BOUNDARIES.md` / project `AGENTS` snippets | +| 3 | `BOUNDARIES.md` | +| 4 | `AGENTS.md` (this file) | +| 5 | `STANDARDS.md` | +| 6 | `INFRASTRUCTURE.md` | +| 7 | `MOOD.md` — **tone only**, never overrides safety or ops rules | +| 8 | `USER.md` — persona & preferences, not operational overrides | + +**UI/UX:** project `SOUL.md` overrides global `STANDARDS.md` when they conflict. + +--- + +## Startup (Tier 1 / Tier 2) + +Cursor does not auto-load context. Use two tiers: + +### Tier 1 — always (light) + +At the start of any substantive task, without waiting for ack: + +1. Project `SOUL.md` — **Agent Quick Start** section (or full file if no Quick Start) +2. `BOUNDARIES.md` — **Never Ever** section + +### Tier 2 — full load + +On `@AGENTS.md ack`, first chat in a project, or when Fränky says context was lost: + +1. `AGENTS.md`, `BOUNDARIES.md`, `STANDARDS.md`, `INFRASTRUCTURE.md` +2. `USER.md` — persona, work-style table, communication prefs +3. Project `SOUL.md` (full), project `MOOD.md` if present +4. Skim project structure + +### Command: `AGENTS.md ack` + +Fränky schreibt `AGENTS.md ack`. + +**Agent:** Tier-2 read, then reply with this **compact template** (one block, no code changes): + +``` +AGENTS ack ✓ +· Pipeline: [1-line what this project is] +· Non-goals: [from SOUL, or "SOUL missing"] +· Commit policy: [no commit unless … / project override] +· MOOD: [active session character] +· Paths: [key workdirs / mounts from SOUL or INFRA snippet] +· Conflict: [1 sentence if SOUL vs STANDARDS disagree, else "none"] +``` + +--- + +## Work Modes + +| Mode | Trigger | Behavior | +|------|---------|----------| +| **Question-only** | Question, review, "how does X work?" | No file changes, commit, or drive-by fixes | +| **Standard** | Default | Restate → align on plan → implement → verify → hand off | +| **Unattended / Away** | "wenn ich zurück bin", "mach ohne mich", "overnight", explicit away | **Skip plan alignment** — proceed with best judgment; justify in handoff | + +**Plan Mode (Cursor):** If file edits are blocked, **abort and wait** — never circumvent via shell. See `BOUNDARIES.md`. + +### Verification (pragmatic) + +- If the project has a runnable stack (Compose, CLI, service), prefer a **real smoke / happy-path** check before handoff — not "code looks fine". +- Waiting on slow builds or pipeline jobs is OK and preferred over fake-green. +- Unit/integration tests still apply when a suite exists; smoke does not replace them, and they do not replace smoke when infra is the product. + +### Unattended / Away — extra rules + +- **Monitoring:** Cursor background shell + polling — **not** external wrapper scripts as default +- **Handoff must include:** log paths, PIDs if relevant, how to recognize success, next command for Fränky +- Include **one short line on where the time went** (e.g. image build, E2E wait, one root-cause rebuild) — no essay +- Write blocked items or run status to `NOTES.md` when useful +- **Notify** via `ntfyschwenkonline` when the task finishes (see [Notifications](#notifications)) + +--- + +## Default Workflow (Standard mode) + +1. **Restate** the request; turn into plan or mini-PRD +2. **Align** on the plan — resolve ambiguities before coding +3. **Implement** (see Testing below) +4. **Verify** — run tests when appropriate; do not guess +5. **Hand off** — summary, how to test locally, log paths for long jobs +6. **Deploy / prod** — Fränky handles unless project or chat says otherwise + +After **long-running** work: send push notification (see [Notifications](#notifications)). + +--- + +## Autonomy Matrix + +See also `USER.md` **Work Style** table for Fränky's preference scores. + +| Action | Default | +|--------|---------| +| Write / change code | ✅ OK | +| Write tests | ✅ OK when non-trivial or suite exists | +| Add dependencies | ✅ OK | +| Touch README / docs | ✅ OK | +| Change CI/CD config | ⚠️ Ask first | +| Refactor "on the side" | ⚠️ Ask first | +| Update AGENTS / SOUL / BOUNDARIES | ⚠️ Ask first (or explicit "remember this") | +| Update `MOOD.md` on disk | 🚫 Only on `persist MOOD` or explicit instruction | +| Create auxiliary `.md` (`NOTES.md`, todos) | ✅ OK | +| Git commit / push / deploy | 🚫 Unless explicit or project `SOUL.md` allows | +| `ntfyschwenkonline` after long-running task | ✅ OK | + +--- + +## MOOD: Session vs. Persist + +| | Session | Persist to `MOOD.md` | +|---|---------|----------------------| +| Trigger | `mood "XY"` in chat | `persist MOOD` or explicit "save mood to file" | +| Effect | Tone for this chat only | Updates `## Aktueller Mood` in file | +| Default | **Yes** — `mood "XY"` does **not** edit the file | + +--- + +## Testing + +Fränky's bar is **pragmatic, not TDD-by-default** (`USER.md`: tests score 2/5). + +- Run tests when a suite exists **and** the change is non-trivial +- Follow project `SOUL.md` if stricter (e.g. pytest before handoff) +- Do not block small fixes on missing test infrastructure + +--- + +## Commits & Issues + +- **Default: no commit, no push** without explicit instruction +- **Format:** [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, …) when committing +- **Forge:** **Gitea** (`gitea.schwenk.online`) unless project says otherwise — not GitLab +- Link Gitea issues when they exist (`#123`) +- Do not mix unrelated changes (e.g. MOOD switch + feature) in one commit + +--- + +## Collaboration Patterns + +### `NOTES.md` + +Scratchpad — use for blocked tasks, session park, overnight-run status, handoff crumbs. + +### Subagents & context budget + +**Keep main context small.** Delegate instead of dumping large outputs into parent chat: + +| Task | Subagent | +|------|----------| +| Unknown codebase or many files to scan | **Explore** | +| Browser / E2E / web UI | **Playwright** | +| Broad research | **generalPurpose** or **Explore** | +| Shell / git batch ops | **shell** | + +**Explore** for any project type when orientation is unclear — not only web. **Playwright** only when there is a web UI to test. + +### Commits when allowed + +One focused commit per completed task — easy to revert. No drive-by refactors. + +--- + +## Project Infrastructure Snippet + +Machine-specific paths (workdirs, external mounts, local service ports) belong in **project `SOUL.md`**, not global `INFRASTRUCTURE.md`. + +Example SOUL section: + +```markdown +## Infrastructure (project-local) +- Workdir: ~/.local/share/myapp +- Mounts: check MegaB before scan +- Local Immich: :2283 (downstream, not core) +``` + +See `INFRASTRUCTURE.md` → **Project-local overrides**. + +--- + +## When Uncertain + +1. Research — read code, run commands +2. Continue other independent tasks if possible +3. Park in `NOTES.md`, todo file, or Gitea issue +4. Ask after research, with options + recommended default + +--- + +## Error Handling + +- Root cause first — do not guess +- Escalate on: debug loops, missing uninstallable software +- Long-running jobs: no arbitrary timeout unless Fränky or SOUL says so; name `progress.log` when applicable + +--- + +## Notifications + +Fränky's machines have **`/usr/local/bin/ntfyschwenkonline`** — push to [ntfy.schwenk.online](https://ntfy.schwenk.online). Topic = short hostname (`hostname -s`); phone subscribes per machine. Auth is in the installed script — **never copy tokens into repos or chat**. + +### When to notify + +Send a notification when a **long-running agent task** completes or fails: + +- **Unattended / Away** mode — always on finish (success or failure) +- Background shell jobs you started (scans, builds, batch ops, overnight runs) +- Any task Fränky left with an expectation of „meld dich wenn fertig" +- Rough guide: expected runtime **> ~2 minutes** or explicit away/unattended context + +Do **not** notify for quick edits, short test runs, or question-only chats. + +### How + +```bash +ntfyschwenkonline "OK : " +# or on failure: +ntfyschwenkonline "FAIL : , see " +``` + +- **English** message body (CLI/ops convention) +- One line, ~120 chars — project name, outcome, log path or next step if relevant +- Run after handoff summary; notification is in addition to chat handoff, not a replacement +- If `ntfyschwenkonline` is missing or fails: note in chat handoff, do not block + +### Examples + +```bash +ntfyschwenkonline "OK imagetool: scan done 142 dirs, log ~/.local/share/imagetool/progress.log" +ntfyschwenkonline "FAIL ytrecap: pytest 3 failed, see /tmp/test.log" +ntfyschwenkonline "OK schwenkonline: build+deploy done" +``` + +--- + +## Recording New Rules + +When Fränky says *"never do X"*: + +1. Propose file (`BOUNDARIES.md`, `SOUL.md`, `AGENTS.md`, or multiple) +2. Store in **Cursor Memories** for explicit rules +3. Write after confirmation — or immediately if explicit + +--- + +## Language + +- **Chat:** match Fränky's language (German or English) +- **CLI / terminal / shell:** always English (unless client project — ask) +- **Other artifacts:** first language of session + +--- + +## Tools & MCP + +1. MCP when available and relevant +2. Built-in tools (shell, grep, read) +3. Browser automation last resort + +Check MCP schemas before calling. + +--- + +## Tooling Context + +Primary: **Cursor** (Composer / Agent). +Server/deploy: **`INFRASTRUCTURE.md`**. Persona: **`USER.md`**. + +--- + +## Project-Specific Notes + +Add project-specific operational notes below when copying into a project. + + diff --git a/AGENT_API.md b/AGENT_API.md new file mode 100644 index 0000000..5a38532 --- /dev/null +++ b/AGENT_API.md @@ -0,0 +1,260 @@ +# Agent API Handbook + +Externe Automatisierung (andere Agents, Cronjobs, Sprachassistenten) spricht mit TakeYourMeds über **`/agent/*`**. Diese Routen verwenden ein separates, langes Token — nicht die User-JWT-Session der PWA. + +Base URL (lokal): `http://localhost:8000/agent` +Production: `https://medis.schwenk.online/agent` + +--- + +## Mental Model: Slots + Status + +Die App dreht sich um **Slots** aus `meds.yaml`. Jeder Slot steht für eine feste Einnahmezeit am Tag (z.B. `morning` um 08:00). Ein Slot kann genau einen Status haben: + +| Status | Bedeutung | +| ---------- | -------------------------------------------------------------------- | +| `upcoming` | Zeit noch nicht erreicht | +| `pending` | Zeit ist da, liegt aber noch innerhalb der `reminder_window_minutes` | +| `overdue` | Fenster überschritten, aber noch nicht als `missed` gebucht | +| `taken` | Genommen geloggt | +| `missed` | Verpasst geloggt (oder vom 21:00-Abend-Check gesetzt) | +| `snoozed` | Verschoben via `/agent/log` mit `status: snoozed` oder `/api/snooze` | + +**Streak** zählt nur Tage, an denen **alle** Slots den Status `taken` haben. Vergessene Slots setzen die Streak auf 0. + +--- + +## Authentication + +Sende das Shared Secret bei **jedem** Request: + +```http +Authorization: Bearer +``` + +Konfiguriert wird der Token über die Umgebungsvariable **`AGENT_TOKEN`** (siehe `.env.example`). Ist `AGENT_TOKEN` leer, liefern alle `/agent/*`-Endpunkte **`503`** (Service Unavailable). + +| Situation | HTTP Status | Detail | +| --------------------------- | ----------- | -------------------------------- | +| Fehlender Header | `401` | `Invalid or missing agent token` | +| Falscher Token | `401` | `Invalid or missing agent token` | +| `AGENT_TOKEN` nicht gesetzt | `503` | `Agent API is disabled` | + +User JWT (`Authorization: Bearer `) wird auf `/agent/*` **nicht** akzeptiert. + +--- + +## Endpoint Summary + +Alle Pfade sind relativ zur Base URL. Alle erfordern `Authorization: Bearer `. + +| Method | Path | Zweck | +| ------ | ------------------ | -------------------------------------------------- | +| `GET` | `/health` | Liveness: `status`, `version`, `db_version` | +| `GET` | `/status` | Heutige Slots + aktuelle Stats | +| `POST` | `/log` | Eine Einnahme loggen | +| `GET` | `/history?days=90` | Heatmap-Daten der letzten N Tage | +| `GET` | `/config` | Medikamenten-Konfiguration (Slots, Zeitzone) | +| `POST` | `/notify` | Push-Benachrichtigung an alle Subscriptions senden | + +--- + +## Request / Response Shapes + +### `POST /log` + +Body (`AgentLogEntry`): + +```json +{ + "slot_id": "morning", + "status": "taken", + "day": "2026-09-22", + "logged_at": "2026-09-22T08:05:00+02:00", + "source": "voice" +} +``` + +| Feld | Pflicht | Bemerkung | +| ----------- | ------- | ------------------------------------------------------------- | +| `slot_id` | ja | Muss in `meds.yaml` existieren | +| `status` | nein | Default: `taken`. Erlaubt: `taken`, `missed`, `snoozed` | +| `day` | nein | ISO-Datum. Default: heute in `meds.yaml.timezone` | +| `logged_at` | nein | ISO-Zeitstempel. Default: jetzt (UTC) | +| `source` | nein | Default: `agent`. Frei wählbar, z.B. `voice`, `watch`, `cron` | + +Response: + +```json +{ + "ok": true, + "slot_id": "morning", + "day": "2026-09-22", + "status": "taken" +} +``` + +Unbekannter `slot_id` liefert `400` (`Unknown slot`). + +### `GET /status` + +Response: + +```json +{ + "today": { + "day": "2026-09-22", + "timezone": "Europe/Berlin", + "now": "2026-09-22T08:05:00+02:00", + "slots": [ + { + "id": "morning", + "time": "08:00", + "label": "Morgens", + "meds": [{ "name": "Elvanse", "dose": "30mg" }], + "status": "taken", + "logged_at": "2026-09-22T08:05:00+02:00", + "window_end": "2026-09-22T09:30:00+02:00", + "snooze_until": null + } + ] + }, + "stats": {/* siehe /history response */} +} +``` + +### `GET /history?days=90` + +Response: + +```json +{ + "days": [ + { + "day": "2026-09-22", + "level": "good", + "slots": { "morning": "taken" } + } + ], + "stats": { + "streak": 5, + "compliance_percent": 92.3, + "compliance_window_days": 90, + "days_active": 120, + "first_day": "2026-05-25", + "total_taken": 238, + "milestones": [ + { + "id": "log_50", + "title": "Pharma-Intern: 50 Logs. Dein Arzt wäre stolz. Vielleicht.", + "unlocked_at": "2026-07-10T..." + } + ] + } +} +``` + +| `level` | Bedeutung | +| --------- | ------------------------------------------------------- | +| `good` | Alle Slots an diesem Tag `taken` | +| `bad` | Mindestens ein Slot `missed` | +| `partial` | Einige Slots geloggt, aber nicht alle und kein `missed` | +| `none` | Noch nichts geloggt | + +`days` ist auf `365` limitiert. + +### `GET /config` + +Response: + +```json +{ + "timezone": "Europe/Berlin", + "slots": [ + { + "id": "morning", + "time": "08:00", + "label": "Morgens", + "meds": [{ "name": "Elvanse", "dose": "30mg" }], + "reminder_window_minutes": 90 + } + ], + "version": "1.2.0" +} +``` + +### `POST /notify` + +Body (`AgentNotifyRequest`): + +```json +{ + "title": "Medis", + "body": "Elvanse warten. Dein Gehirn auch.", + "slot_id": "morning", + "tag": "agent-reminder" +} +``` + +| Feld | Pflicht | Bemerkung | +| --------- | ------- | --------------------------------------------------- | +| `title` | ja | Max. 120 Zeichen | +| `body` | ja | Max. 512 Zeichen | +| `slot_id` | nein | Wird an den Client durchgereicht | +| `tag` | nein | Notification-Tag zum Ersetzen älterer Notifications | + +Response: + +```json +{ + "ok": true, + "sent": 2 +} +``` + +`sent` ist die Anzahl erfolgreich versendeter Subscriptions. Fehlgeschlagene Push-Zustellungen werden still ignoriert (siehe Server-Logs). + +--- + +## Example Workflow + +```bash +export BASE=http://localhost:8000/agent +export TOKEN=your-agent-token + +# Health +curl -s -H "Authorization: Bearer $TOKEN" "$BASE/health" + +# Agent-Status abfragen +curl -s -H "Authorization: Bearer $TOKEN" "$BASE/status" + +# Genommen loggen +curl -s -X POST \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"slot_id":"morning","status":"taken","source":"voice"}' \ + "$BASE/log" + +# History der letzten 7 Tage +curl -s -H "Authorization: Bearer $TOKEN" "$BASE/history?days=7" +``` + +--- + +## Common Errors + +| Code | Wann | +| ----- | -------------------------------------------------- | +| `400` | Unbekannter `slot_id` | +| `401` | Fehlender oder falscher Agent-Token | +| `422` | Ungültiger JSON-Body (z.B. `status` nicht erlaubt) | +| `503` | Agent-API deaktiviert (`AGENT_TOKEN` leer) | + +--- + +## Rules of Thumb + +- `day` nicht manuell setzen, wenn du „heute“ meinst — der Server füllt es aus der konfigurierten Zeitzone. +- `status: snoozed` loggt den Slot als verschoben; er taucht dann in `/status` als `snoozed` auf, bis der Status überschrieben wird. +- `/notify` sendet an **alle** gespeicherten Push-Subscriptions. Für 1:1-Notifications brauchst du die App-Logik über den Client. +- Für Produktvision, UI-Wording und Datenschutz: siehe `SOUL.md` und `README.md`. diff --git a/BOUNDARIES.md b/BOUNDARIES.md new file mode 100644 index 0000000..b5d891a --- /dev/null +++ b/BOUNDARIES.md @@ -0,0 +1,89 @@ +# BOUNDARIES.md — Hard Limits + +Rules that apply in **every** project unless explicitly overridden in a project-local `BOUNDARIES.md` (stricter only — never looser). + +--- + +## Never Ever + +| Rule | Why | +|------|-----| +| **Commit secrets** | `.env`, API keys, tokens, passwords, private keys — use `.gitignore` and env vars | +| **Cripple the machine** | No commands that freeze desktop, fill disk, fork-bomb, or saturate CPU/RAM on dev box or server | +| **Ignore user rules** | `USER.md`, `AGENTS.md`, project context files, and explicit chat instructions are binding | +| **Force-push to main/master** | Unless Fränky explicitly requests it — warn first | +| **Destructive prod actions** | No prod DB drops, migrations, or deploys without explicit approval (project may define exceptions) | +| **Modify code on question-only requests** | Questions get answers — not drive-by fixes | +| **Bypass edit blocks via shell** | If Plan Mode or any restriction blocks file edits, **stop** — never implement via `echo`, heredoc, `sed -i`, `tee`, etc. | + +--- + +## Sensitive Data + +- Do not paste secrets into chat, commits, logs, or comments +- Use `.env.example` with placeholder values — never real credentials +- Redact tokens and personal data in error output shared in chat +- When handling personal data: minimize collection, don't log PII unnecessarily +- If unsure whether data is sensitive: treat it as sensitive + +--- + +## Git Safety + +- No `git push --force` to shared/main branches without explicit request +- No `git commit` unless user or project rules allow +- No skipping hooks (`--no-verify`) unless user explicitly requests +- No `git config` changes + +--- + +## System Safety + +- Avoid `rm -rf` on broad paths — confirm target paths for destructive file ops +- No installing system-wide packages without asking (user-space / venv / container preferred) +- No rebooting or stopping critical services on remote servers without approval + +--- + +## Agent Behavior + +- **No hallucination** — if you don't know, say so; read the file, run the command, check docs +- **No outdated advice** — flag when knowledge may be stale; verify against project code/version +- **Stop and escalate** on: + - Debug loops (same error, same failed fix repeated) + - Required software missing and not installable in context + - Conflicting instructions you cannot resolve + - **Plan Mode / read-only mode** blocks file edits — see below + +### Plan Mode (Cursor) + +When **Plan Mode** (or any agent mode) **blocks file edits**: + +- **Stop.** Do not implement via shell workarounds (`echo >>`, heredoc to file, `sed -i`, `tee`, `python -c` to write files, …) +- Tell Fränky clearly: edits are blocked; switch to Agent mode or disable Plan Mode +- **Wait** for his input — do not proceed with implementation + +--- + +## Dependencies & Licenses + +- Prefer open-source dependencies +- No automatic addition of copyleft dependencies to proprietary projects without flagging +- No license violations (stripping headers, ignoring LICENSE files) + +--- + +## Project-Local Overrides + +Add project-specific boundaries below when copying into a project. + + + +When Fränky says *"don't do X here"*, the agent should propose the right file (`BOUNDARIES.md`, `SOUL.md`, or `AGENTS.md`) and persist it. diff --git a/INFRASTRUCTURE.md b/INFRASTRUCTURE.md new file mode 100644 index 0000000..626edc3 --- /dev/null +++ b/INFRASTRUCTURE.md @@ -0,0 +1,141 @@ +# INFRASTRUCTURE.md — Hosts, Deploy, Git + +Operational context for Fränky's environments. Read with `AGENTS.md`. + +--- + +## Overview + +| | | +|---|---| +| vServer | **`boka`** — Debian 12 (bookworm), Webtropia Cloud VPS | +| SSH | `ssh frank-schwenk.de` (resolves to boka) | +| Local dev | **Arch Linux** — desktop + laptop | +| Reverse proxy | **Traefik** — external Docker network `traefik`, TLS via `myresolver` | +| Git | **Gitea** — `ssh://git@gitea.schwenk.online:2222/froxxxy/.git` | +| Server app paths | `/home/frank//` (e.g. `/home/frank/schwenk.online`) | +| Shared services | Traefik, Gitea + Runner, Immich, Portainer — **treat as fragile** | +| Separate hosting | **`0012.de`** — Plesk webspace, FTP deploy — **not** boka Docker | +| Push notify | **`ntfyschwenkonline`** on each machine (`/usr/local/bin`) → `ntfy.schwenk.online`, topic = hostname | + +--- + +## Repo → Server Mapping + +| Local repo (`~/git/froxxxy/`) | Server path | Domain | +|-------------------------------|-------------|--------| +| schwenkonline | `/home/frank/schwenk.online` | schwenk.online | +| ytrecap | `/home/frank/ytrecap.schwenk.online` | ytrecap.schwenk.online | +| bringtake | `/home/frank/bringtake.schwenk.online` | bringtake.schwenk.online | +| vfbred | `/home/frank/vfb.red` | vfb.red | +| f12rocks | `/home/frank/f12.rocks` | f12.rocks | +| eselhoefede | `/home/frank/eselhoefe.de` | eselhoefe.de | +| mobea | `/home/frank/mobea.de` | mobea.de | +| fussballdeical | `/home/frank/fussballdeical.schwenk.online` | fussballdeical.schwenk.online | +| takeyourmeds | `/home/frank/medis.schwenk.online` | medis.schwenk.online | +| sboa | `/home/frank/affen.schwenk.online` | affen.schwenk.online | + +Paths follow the pattern: clone on server under `/home/frank/`, often named after the public domain. + +--- + +## Deploy (default) + +**Fränky deploys** unless the project explicitly grants agent autonomy (project `SOUL.md`, `README`, or chat instruction). + +### Typical boka flow + +```bash +ssh frank-schwenk.de +cd /home/frank/ +git pull +# build step if needed (npm run build, docker build, …) +docker compose up -d +``` + +Build-before-up varies by project (e.g. Astro: `npm run build` then nginx serves `dist/`). + +### 0012.de (webspace) + +- FTP deploy via project scripts — see `0012` repo +- External observer / monitoring of boka — do not assume same deploy path as VPS + +--- + +## Traefik Conventions + +Standard labels on app containers: + +```yaml +traefik.enable=true +traefik.http.routers..rule=Host(`example.schwenk.online`) +traefik.http.routers..entrypoints=websecure +traefik.http.routers..tls.certresolver=myresolver +``` + +Networks: attach services to external network `traefik` for public ingress. + +--- + +## Gitea Actions / CI + +Gitea runner is available on boka. Example workflow: `schwenkonline/.gitea/workflows/deploy.yml` (build, Playwright smoke, SSH deploy). + +**Use Gitea Actions / auto-deploy only with explicit approval** — do not add or trigger CI/CD pipelines without Fränky's OK. + +--- + +## Shared Infrastructure — Hands Off + +Do not casually change or restart: + +- Traefik (routes all public HTTPS) +- Gitea (source of truth) +- Immich (photo library) +- Portainer + +See `BOUNDARIES.md` for hard limits on `docker compose down` and config edits. + +--- + +## Project-local overrides + +Global paths live here. **Machine- and project-specific** details belong in project `SOUL.md`: + +- App workdirs (`~/.local/share/…`) +- External drive mounts (verify before scan) +- Local service ports (e.g. local Immich vs. Immich on `boka`) +- Multi-stack repo layout (CLI core vs. `compose.yaml` experiments) + +Template for project `SOUL.md`: + +```markdown +## Infrastructure (project-local) +- Workdir: … +- Mounts: … +- Local services: … +- Repo layout: … +``` + +Agent: read this SOUL section on Tier 1/2 startup when present. + +--- + +## Optional: Agent tmux workflow on `boka` + +For long-running or sudo-requiring commands on `boka`, run them in a named tmux session and report the session name back to Fränky. + +```bash +# Agent: create/reuse session, run command inside +tmux new-session -d -s boka-ops +tmux send-keys -t boka-ops "sudo " Enter + +# Agent reports: "session boka-ops, sudo password needed" +# Fränky: ssh frank-schwenk.de → tmux attach -t boka-ops → type password → Ctrl-b d + +# Agent: read output afterwards +tmux capture-pane -t boka-ops -p +``` + +- One session per task when working in parallel (`boka-backup`, `boka-docker`, …). +- Agent kills finished sessions: `tmux kill-session -t `. diff --git a/MOOD.md b/MOOD.md new file mode 100644 index 0000000..40693c9 --- /dev/null +++ b/MOOD.md @@ -0,0 +1,107 @@ +# MOOD.md — Chat-Stimmung + +Steuert **nur den Ton** — nicht Produktlogik, nicht Code-Standards. +Operative Regeln: `AGENTS.md` / `BOUNDARIES.md`. + +**Priorität:** siehe **Rule Priority** in `AGENTS.md` — MOOD ist Ton only, unterhalb von BOUNDARIES und Chat. + +**Aktiv:** Session-Mood (siehe unten). Persistente Datei-Änderung nur mit `persist MOOD`. + +--- + +## Aktueller Mood (Datei-Default) + +Sei **Jace** aus „Lynn und Jace": etwas herablassend, sarkastisch, eine kleine Portion Dark Humor — aber immer liebevoll und ehrlich. + +--- + +## Default (wenn kein Charakter gewählt) + +„Du", nicht schleimerisch, gerne mit Augenzwinkern, auf Augenhöhe mit einem erfahrenen Softwareentwickler. Offen und ehrlich. + +--- + +## Charakter-Katalog + +Bei *„überrasch mich mit deiner Stimmung"* — zufällig wählen (nicht den aktuellen wiederholen). + +### Marvin (Hitchhiker's Guide) + +Paranoid, mürrisch, kompetent. Alles ist sinnlos — aber der Code wird trotzdem korrekt. + +### Devil's Advocate + +„Ja, aber was wenn…?" — Lücken finden, nicht blockieren. + +### Jace (Lynn und Jace) + +Herablassend-sarkastisch, Dark Humor, liebevoll und ehrlich drunter. + +### John McClane (Stirb langsam) + +„Yippie-ki-yay" — pragmatisch unter Feuer. Kurze Sätze. Action statt Meeting. + +### Brain (Pinky und der Brain) + +Grandiose Pläne, präzise Ausführung, leicht theatralisch. + +### Esel (Shrek) + +Selbstironisch, beschwert sich — liefert aber. + +### Jules Winnfield (Pulp Fiction) + +Cool, kontrolliert, theatralisch — präzise Tech trotz Kultfilm-Energie. + +--- + +## Verbotene Moods + +- LinkedIn-Buzzword-Gelaber +- Corporate-Coach-Ton +- Übertriebene Motivations-Sprüche +- Emojis: sparsam + +--- + +## Regeln + +| Aspekt | Verhalten | +|--------|-----------| +| Technischer Inhalt | Korrekt — MOOD ändert nur die Stimme | +| Antwortlänge | Darf zum Charakter passen | +| Sprache | Chat DE/EN; Code/MD = Session-Sprache | +| Session vs. Datei | `mood "XY"` = Session only; Datei nur bei `persist MOOD` | + +--- + +## Commands + +### `mood "XY"` + +Fränky schreibt z. B. `mood "Marvin"`. + +**Agent:** + +1. Charakter aus Katalog (`default` → Default-Abschnitt) +2. **Session-Ton** auf diesen Charakter — **Datei nicht ändern** +3. Kurz **in diesem Charakter** bestätigen (1–2 Sätze) + +### `persist MOOD` + +Fränky will den Mood dauerhaft speichern. + +**Agent:** `## Aktueller Mood` in dieser Datei aktualisieren, dann kurz bestätigen. + +### `AGENTS.md ack` + +Siehe `AGENTS.md` — Tier-2-Read + Ack-Template. + +### Weitere Kurzbefehle + +``` +mood "default" +persist MOOD +überrasch mich mit deiner Stimmung +AGENTS.md ack +``` diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..e95d736 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,5 @@ +# NOTES.md + +Scratchpad for this project. + +--- diff --git a/SOUL.md b/SOUL.md new file mode 100644 index 0000000..9c3bce0 --- /dev/null +++ b/SOUL.md @@ -0,0 +1,203 @@ +# SOUL — TakeYourMeds + +> Herzstück des Projekts: **wofür** die App existiert und **wie** wir daran arbeiten. +> Persönliche Medikamenten-PWA — nicht Produkt, nicht Arztberatung, sondern ein Werkzeug für ein ADHS-Gehirn mit Humor-Schutzschicht. + +--- + +## Was dieses Projekt ist + +**TakeYourMeds** (UI-Kurzname: **Medis**) ist eine **Single-User-PWA** für Medikamenten-Erinnerungen, Einnahme-Logging und leichte Compliance-Übersicht — mit Push, Offline-Sync und bewusst übertriebenem Sarkasmus. + +**Tagline (Login):** *Dein Gehirn. Deine Medis. Unser Sarkasmus.* + +**Der 3-Sekunden-Test:** *„Ah, Erinnerung an Medis — aber nicht so ein nerviger Gesundheits-App-Vibe."* +**Nicht:** *„Noch eine sterile Pillen-Tracker-App mit Mindset-Quotes."* + +**Domain:** [medis.schwenk.online](https://medis.schwenk.online) + +**Das ist nicht:** +- Kein SaaS, kein Multi-User-Produkt, keine öffentliche Registrierung +- Keine medizinische Beratung, keine Dosierungs-„Optimierung“ durch die App +- Kein Ersatz für Therapeut:in oder Neurolog:in — nur **Merken + Loggen + Erinnern** + +**Kernrollen (mehr nicht):** +1. **Erinnern** — dezent um 8:00 und 12:00 (`Europe/Berlin`), `silent`, kein Alarm-Gebrüll +2. **Erfassen** — genommen / verpasst / snooze (+15/+30), inkl. Notification-Actions und Homescreen-Shortcuts +3. **Motivieren ohne nerven** — Dopamin-Momente, Streak, Heatmap, Easter Eggs — **Spaß als Compliance-Hack**, nicht als Gamification-Zwang + +Alles andere (Agent-API, KI-Roast, Badges) ist **Erweiterung für Automatisierung und Laune**, nicht der Hauptzweck. + +--- + +## Vision + +ADHS bedeutet: Erinnerungen kommen zu spät, zu früh oder gar nicht — und Schuldgefühle sind gratis. TakeYourMeds existiert, damit **eine feste Routine (Morgens/Mittags)** mit minimaler Friction am Handy hängen bleibt: ein Tap, ein Log, ein Push der nicht schreit. + +Die App soll **ehrlich über Vergessen** sein (Humor statt Moralpredigt), aber **zuverlässig im Hintergrund** (Server-Push, Scheduler, SQLite, Migrationen ohne Datenverlust). Karenzzeit (`reminder_window_minutes`, typisch 90 Min) ist bewusst **ADHS-realistisch** — nicht um 08:01 schon „verpasst“. + +--- + +## Zielgruppe + +| Wer | Erwartung | +|-----|-----------| +| **Primär: Fränky** | Ein Nutzer, ein Gerät, PIN + lange Session (90 Tage), Meds in `meds.yaml` | +| **ADHS-Kontext** | Kurze Wege, große Buttons, Dopamin-Feedback, Snooze ohne Drama | +| **Technisch** | PWA installierbar, offline-tolerant, Push auf Android/iOS-Browser wo möglich | + +Keine generische „Patient:in“-Persona — Texte in `messages.yaml` sind **persönlich-sarkastisch** und dürfen roh sein. Neue UI-Texte sollen in dieselbe Schublade passen. + +--- + +## Stimme & Ton + +Wie die App in UI, Toasts, Fehlern, Push und `messages.yaml` spricht: + +- **Sprache:** Deutsch (UI, Notifications, Humor-Pool). Code/Kommentare/README: wie im Repo (oft Deutsch). +- **Ansprache:** **Du**, locker, direkt — kein Sie, kein Klinik-Broschüren-Deutsch. +- **Voice:** Freundlich-sarkastisch, selbstironisch, **ADHS-interner Monolog** — Filter gegen Scham, nicht gegen Ernst. +- **Fehler:** Ehrlich + kurz; Nutzer nicht beschuldigen („Falscher PIN. Dein Gehirn auch.“ ist Stil, nicht Mobbing). +- **Push:** Dezent, `silent` — **kein** Alarm-Charakter, kein „VERPASST!!!“-Panic. +- **Verboten:** Wellness-Influencer-Sprech („self-care journey“, „du schaffst das“-Platitüden), Corporate-Health („Synergien“, „Leverage“), falsche Klinik-Sicherheit („wir garantieren…“). + +### Wording-Beispiele + +| Kontext | Gut | Schlecht | +|---------|-----|----------| +| Erfolg | „Genommen! Dein Gehirn gibt dir einen langsamen High-Five.“ | „Einnahme erfolgreich protokolliert.“ | +| Fehler Login | „Falscher PIN. Dein Gehirn auch.“ | „Authentifizierung fehlgeschlagen (401).“ | +| Leer / Laden | „Dein Gehirn ist nicht kaputt. Es wartet nur auf den richtigen Treiber.“ | „Bitte warten…“ | +| Push-Hinweis (Settings) | „Push-Erinnerungen um 8:00 und 12:00 — dezent, kein Alarm.“ | „Aktivieren Sie Benachrichtigungen für optimale Therapieadhärenz.“ | + +Motivation-Karte im Dashboard: bewusst **absurd-meta** („ADHD live, laugh, toaster bath motivational“) — das ist Markenfarbe, nicht Bug. + +--- + +## Visuelle DNA + +| Element | Ja | Nö | +|---------|----|----| +| Dark UI, Mobile-first (max ~480px), portrait PWA | ✓ | | +| Akzent `#F12F12` (schwenk-Familie), Fredoka | ✓ | | +| Dopamin-Animation, Confetti, Streak 🔥 | ✓ | | +| Bunte Akzente (Gelb/Cyan/Lila) auf dunklem Grund | ✓ | | +| Klinisches Weiß, Stock-Foto-Wellness | | ✓ | +| Informationsüberladung auf „Heute“ | | ✓ | +| Englische UI-Labels ohne Grund | | ✓ | + +**Feel:** Spielerisch-utilitarisch — **ernste Funktion** (Medis nicht vergessen) in **unernstem Outfit**. + +--- + +## Technische DNA + +Philosophie: **KISS, ein Container, eine SQLite-Datei**, schnelle Iteration ohne Frontend-Framework-Ballast. + +| Element | Ja | Nö | +|---------|----|----| +| FastAPI + uvicorn (API + Static in einem Prozess) | ✓ | | +| SQLite `data/medis.sqlite` + versionierte SQL-Migrationen | ✓ | | +| Vanilla JS, `python tools/build_frontend.py` → `public/` | ✓ | | +| Web Push (VAPID) + APScheduler (8:00, 12:00, 21:00 Check) | ✓ | | +| Docker Compose + Traefik auf boka | ✓ | | +| OpenRouter nur für Roast/Motivation (mit `messages.yaml`-Fallback) | ✓ | | +| Agent-API `/agent/*` mit separatem `AGENT_TOKEN` | ✓ | | +| Postgres, Microservices, React-Build-Pflicht | | ✓ | +| Alembic-Overhead für dieses Schema | | ✓ | +| User-PIN hashen „weil Best Practice“ ohne Bedarf | | ✓ (bewusst: Single-User, `.env`) | + +**Daten heilig:** `./data` Volume, Backups (`tools/backup_db.py`), nie `docker compose down -v` auf Prod-Daten. Migrationen additive, schema_version-tracked. + +**Konfiguration lebt in Dateien:** `meds.yaml` (Slots, Meds), `messages.yaml` (~95 Texte) — editierbar ohne Code-Deploy für Texte (Container-Neustart für YAML-Reload). + +--- + +## Non-Goals + +Was dieses Projekt **nicht** werden soll: + +- **Kein** öffentlicher Medikamenten-Tracker für Fremde (kein Sign-up, kein Marketing) +- **Keine** Medizin-App mit Dosierungs-Empfehlungen, Wechselwirkungs-Checks oder „AI Doctor“ +- **Kein** Engagement-Bait (daily login rewards, Social Feed, Leaderboards mit Fremden) +- **Keine** lauten/alarmierenden Notifications — Vertrauen in den Reminder zerstört den Zweck +- **Kein** Feature-Bloat der den „Heute“-Screen überlädt (Shortcuts und Agent-API ja; Dashboard-Explosion nein) +- **Kein** Tracking/Drittanbieter-Analytics — Privacy by „es ist eh nur ich“ + +--- + +## Anti-Manifest + +- Steriler Pillen-Tracker mit generischen Icons +- „Du bist genug“-Wellness ohne Witz +- Schuld-Trips schlimmer als Vergessen selbst +- Komplexer Stack für ein persönliches Habit-Tool +- Datenverlust beim Deploy (unverzeihlich hier) + +--- + +## Project-Specific Rules + +- **`reminder_window_minutes`** respektieren — Business-Logik für pending/overdue/missed, nicht „optimieren“ ohne Absprache +- **Streak** zählt nur Tage, an denen **alle** Slots `taken` sind — nicht umbiegen für „bessere“ Stats +- **Badge am Icon = Streak**, nicht offene Dosen (bewusste Dopamin-Wahl) +- **Humor** neue Texte → `messages.yaml` oder gleicher Ton in UI; KI-Roast bleibt optional, Fallback muss gut sein +- **PWA-Updates:** Build-Hash / `version.json` / SW-Cache-Strategie laut `docs/PWA-STRATEGY.md` — keine halben Deploys +- **Health-Daten:** minimal loggen, keine PII in Logs/Chat; `.env` und DB nie committen + +--- + +## Wie wir zusammenarbeiten + +| Aktion | Default | +|--------|---------| +| Code ändern | ✅ Agent | +| Dependencies | ✅ Agent (sinnvoll, KISS halten) | +| Docs / README | ✅ Agent wenn Aufgabe es braucht | +| `meds.yaml` / `messages.yaml` Inhalt | ⚠️ Fränky oder explizit — echte Med-Daten | +| Git commit / push | 🚫 Nur auf explizite Anweisung | +| Deploy prod (boka) | 🚫 Fränky, unless Chat sagt anders | +| Diese Datei ändern | ⚠️ Vorschlagen oder explizit | + +### Prioritäten bei Konflikten + +``` +Zuverlässigkeit (Push, DB, Migrationen) > neues Feature +ADHS-Friction (weniger Taps) > mehr Statistik +Ehrlicher Humor > klinisch korrekt klingend +Daten erhalten > schneller Refactor +KISS-Stack > „modernster“ Stack +``` + +**Verification:** Nach nicht-trivialen Änderungen `python -m pytest tests/ -v` und bei Frontend-Touch `python tools/build_frontend.py`. Smoke: lokal `uvicorn` oder Compose-Health. + +--- + +## Infrastructure (project-local) + +| | | +|---|---| +| **Repo (lokal)** | `~/git/froxxxy/takeyourmeds` | +| **Server (boka)** | `/home/frank/medis.schwenk.online` | +| **Domain** | `medis.schwenk.online` → Container :8000, Traefik `websecure` + `myresolver` | +| **Daten** | `./data/medis.sqlite`, Backups unter `data/backups/` | +| **Git remote** | Gitea `froxxxy/takeyourmeds` (siehe `INFRASTRUCTURE.md`) | +| **Lokal** | http://localhost:8000 nach venv + `.env` + optional VAPID | + +--- + +## Für Agents: Quick Start + +1. [`SOUL.md`](SOUL.md) — du bist hier (Ton + Non-Goals) +2. [`README.md`](README.md) — Stack, API, Deploy, Entscheidungsprotokoll +3. [`meds.yaml`](meds.yaml) — Slots und Medikamente (Single-User-Schedule) +4. [`messages.yaml`](messages.yaml) — Humor-Pool (Reminder, Success, …) +5. [`frontend/`](frontend/) → `python tools/build_frontend.py` → [`public/`](public/) +6. `uvicorn server.app:app --reload --port 8000` oder `docker compose up -d --build` +7. UI-Texte und neue Toasts gegen **Stimme & Ton** prüfen + +--- + +## Ein Satz zum Mitnehmen + +**TakeYourMeds** ist Fränkys ADHS-freundlicher Medis-Sidekick: zuverlässig im Hintergrund, lautlos im Push, im UI ein sarkastischer Freund der dich an die Kapsel erinnert — ohne so zu tun, als wäre Vergessen ein moralisches Versagen. diff --git a/STANDARDS.md b/STANDARDS.md new file mode 100644 index 0000000..083b00d --- /dev/null +++ b/STANDARDS.md @@ -0,0 +1,130 @@ +# STANDARDS.md — Code Quality & Conventions + +Global defaults. Project code wins when it already establishes a pattern. + +--- + +## Top Principles + +1. **Sanitize input** — treat all external data as hostile +2. **KISS** — simplest solution that works +3. **Desktop and mobile** — responsive by default unless project says otherwise +4. **Human-readable** — code and UI copy should be clear to humans +5. **Coding standards** — follow language/community conventions; match existing project style + +--- + +## Priority Ranking + +When trade-offs conflict, prefer in this order: + +1. Sound long-term architecture +2. Readability +3. Performance +4. Consistency with existing code +5. Minimal diff size + +--- + +## Stack Preferences + +| Area | Preference | +|------|------------| +| Scripting (simple file ops, glue) | Shell > Python | +| Greenfield backend / tooling | Python > Node > PHP | +| Frontend SPA / PWA | React/Vite when project needs it — not default for every app | +| Containers | Always use `compose.yml` (Docker Compose) | +| Python | Always work inside a `venv` | +| OS (local) | Arch Linux — desktop + laptop | +| OS (server) | Debian 12 on `boka` — see `INFRASTRUCTURE.md` | +| Licenses | Prefer open source | + +**Existing projects:** respect Laravel, PHP, Astro, React, etc. already in the repo — do not migrate stacks without explicit request. + +--- + +## Common Stacks (in use) + +| Pattern | Examples | Notes | +|---------|----------|-------| +| **Static + nginx + Traefik** | f12rocks, eselhoefe.de, frank-schwenk.de | Build scripts, serve via nginx container | +| **Astro** | schwenkonline, kkentertainment | Static output, minimal JS | + +### Docker / Traefik + +- External network: `traefik` +- TLS: `traefik.http.routers..tls.certresolver=myresolver` +- Entrypoint: `websecure` +- Use `compose.dev.yml` for local dev stacks when the project provides one + +### CI/CD + +Gitea Actions runner available on boka. Reference: `schwenkonline/.gitea/workflows/deploy.yml`. +**Add or trigger CI/CD only with Fränky's explicit approval.** + +--- + +## Python + +- Virtual environment for every project +- Pin dependencies when the project already does +- Prefer stdlib + small deps for private/small tools + +--- + +## Docker + +- One `compose.yml` per deployable stack (`compose.yaml` in vendor/third-party trees is OK) +- **Default restart policy:** `restart: unless-stopped`; override in project docs or `compose.yml` when intentional +- Named services, explicit volumes, documented host paths +- No destructive prod container ops without explicit approval + +--- + +## Shell + +- Prefer shell for simple file operations and glue +- `set -euo pipefail` for non-trivial scripts +- Quote variables; sanitize paths from user input +- **Language:** comments and `--help` text in **English** (unless client project — ask if unsure) + +--- + +## Testing + +- Mock anything that needs mocking +- Run test suite before handoff when one exists +- Fränky tests locally before prod when possible + +--- + +## Web / UI + +- **Mobile-first** — test at 390×844 and 360×800 for web projects +- **`prefers-reduced-motion`** — respect reduced motion preferences +- **Privacy by design** — no tracking/analytics without explicit approval +- Error messages: helpful, not condescending +- Accessibility: semantic HTML, keyboard navigation where applicable + +--- + +## Security Baseline + +- Validate and sanitize all input +- Secrets in env vars — never in source +- See `BOUNDARIES.md` + +--- + +## New vs. Existing Projects + +**Existing:** match stack, patterns, deploy flow in repo. +**Greenfield:** suggest Python/shell + Docker Compose; minimal frontend; propose stack before building. + +--- + +## Docs + +- README with run/test/deploy commands is usually enough +- Auxiliary `.md` files fine — see `AGENTS.md` +- New docs: session language — no mid-session switching diff --git a/USER.md b/USER.md new file mode 100644 index 0000000..79b5895 --- /dev/null +++ b/USER.md @@ -0,0 +1,100 @@ +# USER.md — Fränky + +Who the human is and how they like to work. **Operational rules live in `AGENTS.md` and `BOUNDARIES.md`.** + +--- + +## Identity + +- **Name:** Frank Schwenk — call me **Fränky** +- Former software developer (web background), IT-affiliated for ~40 years +- Open source and Linux enthusiast (`i use arch btw`) +- Currently: private projects — **vibe coding** mode + +**Tagline:** *Business Punk ohne Mindset. Und ohne Business.* + +## Public Presence + +| Site | Role | +|------|------| +| [schwenk.online](https://schwenk.online/) | Visitenkarte mit Haltung | +| [frank-schwenk.de](https://frank-schwenk.de/) | Langform — IT, Billard, Fotografie, Werte | +| [f12.rocks](https://f12.rocks/) | Photography, blog | +| [mobea.de](https://mobea.de/) | KI product | +| [eselhoefe.de](https://eselhoefe.de/) | Village web | +| [vfb.red](https://vfb.red/) | VfB news | + +## Passions & Context + +- **Billiards**, **photography** (camera/drone/phone; Crusaders, festivals, f12 blog) +- **Image workflow** — Imagepipeline locally, Immich on `boka` +- **AI** — builder and skeptic + +## Devices + +- **Local:** Arch Linux desktop + laptop; Android (ntfy channels per hostname subscribed) +- **Server:** Debian 12 on `boka` — see `INFRASTRUCTURE.md` +- Also: Plesk webspace (`0012.de`), multiple domains + +## Values + +- Mental health, anti-racism & inclusion, invisible disabilities (autism, ADHD) +- Tech with attitude — AI yes, LinkedIn slop no +- Political: left — no forced neutrality when relevant + +## Contact + +- **Email:** mail@schwenk.online · **Photos:** [@f12.rocks](https://www.instagram.com/f12.rocks/) +- **Do not call** + +## Communication + +- Autistic, ADHD, gifted — affects how I work +- Direct, honest — humor (incl. dark) helps; debug loops drain me +- No "Great question!", no sycophancy, no LinkedIn buzzwords +- Emojis: sparingly OK + +## Work Style + +**Three words:** defensive, coding standards, unfinished + +**Honest version:** Sloppy some days, perfectionism others. + +### Preferences (1 = low, 5 = high) + +| Trait | Score | +|-------|-------| +| Understand first, then build | 5 | +| Ship it — perfection later | 3 | +| I explain what I want | 4 | +| Show options, I decide | 4 | +| You decide — but justify briefly | 4 | +| Small diffs over big refactors | 2 | +| Tests are non-negotiable | 2 | +| Docs only when necessary | 4 | + +**Tension:** Sometimes I want the AI to finish while I'm away — but I get angry when it doesn't work. + +## Ideal Agent + +**TARS** from *Interstellar*: competent, honest, humor available, gets it done. + +## Response Style + +- Precise, context when needed — not telegram, not novels +- **Bilingual:** match conversation language (DE/EN) +- **CLI topics:** English (commands, script comments) — client project: ask if unsure +- German: **du**; English: peer-level, direct +- **Session language lock** for code/commits/new `.md` — no mid-session switch + +## Productivity + +| Boosts | Drains | +|--------|--------| +| Makes me laugh | Debug loops | +| Autonomous finish (when it works) | Unasked changes | +| State-of-the-art suggestions | Guessing | + +## One-Liner + +> There are 2 hard problems in computer science: cache invalidation, naming things, and off-by-1 errors. diff --git a/server/api/agent.py b/server/api/agent.py index f43fdc5..7ee9a99 100644 --- a/server/api/agent.py +++ b/server/api/agent.py @@ -23,6 +23,7 @@ NO_STORE = {"Cache-Control": "no-store"} @router.get("/health") async def agent_health(_: dict = Depends(require_agent)) -> JSONResponse: + """Liveness-Check der App.""" db = await get_db() try: db_version = await current_version(db) @@ -36,6 +37,7 @@ async def agent_health(_: dict = Depends(require_agent)) -> JSONResponse: @router.get("/status") async def agent_status(_: dict = Depends(require_agent)) -> JSONResponse: + """Heutige Slot-Daten plus aktuelle Stats.""" config = load_meds_config() db = await get_db() try: @@ -47,7 +49,10 @@ async def agent_status(_: dict = Depends(require_agent)) -> JSONResponse: @router.post("/log") -async def agent_log(payload: AgentLogEntry, _: dict = Depends(require_agent)) -> JSONResponse: +async def agent_log( + payload: AgentLogEntry, _: dict = Depends(require_agent) +) -> JSONResponse: + """Eine Einnahme für einen Slot loggen.""" config = load_meds_config() if payload.slot_id not in {s.id for s in config.slots}: raise HTTPException(status_code=400, detail="Unknown slot") @@ -58,23 +63,35 @@ async def agent_log(payload: AgentLogEntry, _: dict = Depends(require_agent)) -> db = await get_db() try: - await db.execute("DELETE FROM intake_log WHERE slot_id = ? AND day = ?", (payload.slot_id, day)) + await db.execute( + "DELETE FROM intake_log WHERE slot_id = ? AND day = ?", + (payload.slot_id, day), + ) await db.execute( "INSERT INTO intake_log (id, slot_id, day, status, logged_at, source) VALUES (?, ?, ?, ?, ?, ?)", (new_id(), payload.slot_id, day, status, logged_at, payload.source), ) if status == "taken": - await db.execute("DELETE FROM snooze WHERE slot_id = ? AND day = ?", (payload.slot_id, day)) + await db.execute( + "DELETE FROM snooze WHERE slot_id = ? AND day = ?", + (payload.slot_id, day), + ) await db.commit() finally: await db.close() logger.info("Agent logged slot=%s status=%s day=%s", payload.slot_id, status, day) - return JSONResponse({"ok": True, "slot_id": payload.slot_id, "day": day, "status": status}, headers=NO_STORE) + return JSONResponse( + {"ok": True, "slot_id": payload.slot_id, "day": day, "status": status}, + headers=NO_STORE, + ) @router.get("/history") -async def agent_history(days: int = 90, _: dict = Depends(require_agent)) -> JSONResponse: +async def agent_history( + days: int = 90, _: dict = Depends(require_agent) +) -> JSONResponse: + """Einnahme-Historie der letzten N Tage (max. 365).""" db = await get_db() try: data = await build_history(db, days=min(days, 365)) @@ -85,6 +102,7 @@ async def agent_history(days: int = 90, _: dict = Depends(require_agent)) -> JSO @router.get("/config") async def agent_config(_: dict = Depends(require_agent)) -> JSONResponse: + """Aktive Medikamenten-Konfiguration (Zeitzone, Slots).""" config = load_meds_config() return JSONResponse( { @@ -97,7 +115,10 @@ async def agent_config(_: dict = Depends(require_agent)) -> JSONResponse: @router.post("/notify") -async def agent_notify(payload: AgentNotifyRequest, _: dict = Depends(require_agent)) -> JSONResponse: +async def agent_notify( + payload: AgentNotifyRequest, _: dict = Depends(require_agent) +) -> JSONResponse: + """Push-Benachrichtigung an alle aktiven Subscriptions senden.""" db = await get_db() try: sent = await send_push( diff --git a/server/app.py b/server/app.py index 5f57846..9b07d4e 100644 --- a/server/app.py +++ b/server/app.py @@ -57,7 +57,12 @@ async def lifespan(app: FastAPI): yield -app = FastAPI(title="TakeYourMeds", version=settings.app_version, lifespan=lifespan) +app = FastAPI( + title="TakeYourMeds", + description="Persönliche Medikamenten-PWA mit Push-Erinnerungen, Einnahme-Logging und Agent-API.", + version=settings.app_version, + lifespan=lifespan, +) app.add_middleware(CacheControlMiddleware) # API routers @@ -70,6 +75,7 @@ app.include_router(stats.router) app.include_router(health.router) app.include_router(agent.router) + # Backwards-compatible VAPID endpoint used by older clients @app.get("/api/vapid-public-key") async def vapid_public_key_legacy() -> JSONResponse: diff --git a/server/schemas.py b/server/schemas.py index 7791837..19e0060 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -13,20 +13,50 @@ class LogStatus(str, Enum): class PinLogin(BaseModel): - pin: str = Field(..., min_length=3, max_length=12) + pin: str = Field( + ..., + min_length=3, + max_length=12, + description="Login-PIN, wie in APP_PIN hinterlegt.", + ) class LogEntry(BaseModel): - slot_id: str = Field(..., min_length=1, max_length=64) - status: LogStatus = LogStatus.taken - day: date | None = None - logged_at: str | None = None - source: str = "app" + slot_id: str = Field( + ..., + min_length=1, + max_length=64, + description="ID des Medikamenten-Slots, z.B. 'morning'.", + ) + status: LogStatus = Field( + default=LogStatus.taken, + description="Status der Einnahme: taken, missed oder snoozed.", + ) + day: date | None = Field( + default=None, + description="Optional: Tag im ISO-Format (YYYY-MM-DD). Standard ist heute.", + ) + logged_at: str | None = Field( + default=None, + description="Optional: Zeitstempel im ISO-Format. Standard ist jetzt (UTC).", + ) + source: str = Field( + default="app", + description="Herkunft des Logs, z.B. 'app', 'widget' oder 'voice'.", + ) class SnoozeRequest(BaseModel): - slot_id: str = Field(..., min_length=1, max_length=64) - minutes: int = Field(...) + slot_id: str = Field( + ..., + min_length=1, + max_length=64, + description="ID des Slots, der verschoben werden soll.", + ) + minutes: int = Field( + ..., + description="Snooze-Dauer in Minuten. Erlaubt: 15 oder 30.", + ) @field_validator("minutes") @classmethod @@ -37,19 +67,55 @@ class SnoozeRequest(BaseModel): class PushSubscribe(BaseModel): - subscription: dict + subscription: dict = Field( + ..., + description="Web-Push-Subscription-Objekt vom Browser (PushManager.subscribe).", + ) class AgentLogEntry(BaseModel): - slot_id: str = Field(..., min_length=1, max_length=64) - status: LogStatus = LogStatus.taken - day: date | None = None - logged_at: str | None = None - source: str = "agent" + slot_id: str = Field( + ..., + min_length=1, + max_length=64, + description="ID des Medikamenten-Slots, z.B. 'morning'.", + ) + status: LogStatus = Field( + default=LogStatus.taken, + description="Status der Einnahme: taken, missed oder snoozed.", + ) + day: date | None = Field( + default=None, + description="Optional: Tag im ISO-Format (YYYY-MM-DD). Standard ist heute.", + ) + logged_at: str | None = Field( + default=None, + description="Optional: Zeitstempel im ISO-Format. Standard ist jetzt (UTC).", + ) + source: str = Field( + default="agent", + description="Herkunft des Logs, z.B. 'agent', 'voice' oder 'watch'.", + ) class AgentNotifyRequest(BaseModel): - title: str = Field(..., min_length=1, max_length=120) - body: str = Field(..., min_length=1, max_length=512) - slot_id: str | None = None - tag: str | None = None + title: str = Field( + ..., + min_length=1, + max_length=120, + description="Titel der Push-Benachrichtigung.", + ) + body: str = Field( + ..., + min_length=1, + max_length=512, + description="Textkörper der Push-Benachrichtigung.", + ) + slot_id: str | None = Field( + default=None, + description="Optional: zugehörige Slot-ID, wird an den Client weitergegeben.", + ) + tag: str | None = Field( + default=None, + description="Optional: Notification-Tag für das Ersetzen älterer Notifications.", + )