docs: add Agent API docs, OpenAPI field descriptions, and project context files
This commit is contained in:
@@ -0,0 +1,298 @@
|
|||||||
|
<!-- vibesoul-updated: 2026-07-24T09:12:40+02:00 -->
|
||||||
|
# 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 <project>: <one-line result>"
|
||||||
|
# or on failure:
|
||||||
|
ntfyschwenkonline "FAIL <project>: <one-line error>, see <log-path>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
<!-- Delete these examples when filling in:
|
||||||
|
|
||||||
|
- Project: [name] — one-line context or special workflow
|
||||||
|
- Custom handoff convention: ...
|
||||||
|
|
||||||
|
-->
|
||||||
+260
@@ -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 <agent_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <user-token>`) wird auf `/agent/*` **nicht** akzeptiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoint Summary
|
||||||
|
|
||||||
|
Alle Pfade sind relativ zur Base URL. Alle erfordern `Authorization: Bearer <agent_token>`.
|
||||||
|
|
||||||
|
| 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`.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
<!-- Delete these examples when filling in:
|
||||||
|
|
||||||
|
## Project-Specific
|
||||||
|
|
||||||
|
- (example) Never touch the legacy PHP monolith in /old/
|
||||||
|
- (example) Auto-deploy to staging is OK; prod requires manual approval
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -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/<repo>.git` |
|
||||||
|
| Server app paths | `/home/frank/<domain>/` (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/<project>
|
||||||
|
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.<name>.rule=Host(`example.schwenk.online`)
|
||||||
|
traefik.http.routers.<name>.entrypoints=websecure
|
||||||
|
traefik.http.routers.<name>.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 <cmd>" 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 <name>`.
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
+130
@@ -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.<name>.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
|
||||||
@@ -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.
|
||||||
+27
-6
@@ -23,6 +23,7 @@ NO_STORE = {"Cache-Control": "no-store"}
|
|||||||
|
|
||||||
@router.get("/health")
|
@router.get("/health")
|
||||||
async def agent_health(_: dict = Depends(require_agent)) -> JSONResponse:
|
async def agent_health(_: dict = Depends(require_agent)) -> JSONResponse:
|
||||||
|
"""Liveness-Check der App."""
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
try:
|
try:
|
||||||
db_version = await current_version(db)
|
db_version = await current_version(db)
|
||||||
@@ -36,6 +37,7 @@ async def agent_health(_: dict = Depends(require_agent)) -> JSONResponse:
|
|||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
async def agent_status(_: dict = Depends(require_agent)) -> JSONResponse:
|
async def agent_status(_: dict = Depends(require_agent)) -> JSONResponse:
|
||||||
|
"""Heutige Slot-Daten plus aktuelle Stats."""
|
||||||
config = load_meds_config()
|
config = load_meds_config()
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
try:
|
try:
|
||||||
@@ -47,7 +49,10 @@ async def agent_status(_: dict = Depends(require_agent)) -> JSONResponse:
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/log")
|
@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()
|
config = load_meds_config()
|
||||||
if payload.slot_id not in {s.id for s in config.slots}:
|
if payload.slot_id not in {s.id for s in config.slots}:
|
||||||
raise HTTPException(status_code=400, detail="Unknown slot")
|
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()
|
db = await get_db()
|
||||||
try:
|
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(
|
await db.execute(
|
||||||
"INSERT INTO intake_log (id, slot_id, day, status, logged_at, source) VALUES (?, ?, ?, ?, ?, ?)",
|
"INSERT INTO intake_log (id, slot_id, day, status, logged_at, source) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
(new_id(), payload.slot_id, day, status, logged_at, payload.source),
|
(new_id(), payload.slot_id, day, status, logged_at, payload.source),
|
||||||
)
|
)
|
||||||
if status == "taken":
|
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()
|
await db.commit()
|
||||||
finally:
|
finally:
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
logger.info("Agent logged slot=%s status=%s day=%s", payload.slot_id, status, day)
|
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")
|
@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()
|
db = await get_db()
|
||||||
try:
|
try:
|
||||||
data = await build_history(db, days=min(days, 365))
|
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")
|
@router.get("/config")
|
||||||
async def agent_config(_: dict = Depends(require_agent)) -> JSONResponse:
|
async def agent_config(_: dict = Depends(require_agent)) -> JSONResponse:
|
||||||
|
"""Aktive Medikamenten-Konfiguration (Zeitzone, Slots)."""
|
||||||
config = load_meds_config()
|
config = load_meds_config()
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{
|
{
|
||||||
@@ -97,7 +115,10 @@ async def agent_config(_: dict = Depends(require_agent)) -> JSONResponse:
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/notify")
|
@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()
|
db = await get_db()
|
||||||
try:
|
try:
|
||||||
sent = await send_push(
|
sent = await send_push(
|
||||||
|
|||||||
+7
-1
@@ -57,7 +57,12 @@ async def lifespan(app: FastAPI):
|
|||||||
yield
|
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)
|
app.add_middleware(CacheControlMiddleware)
|
||||||
|
|
||||||
# API routers
|
# API routers
|
||||||
@@ -70,6 +75,7 @@ app.include_router(stats.router)
|
|||||||
app.include_router(health.router)
|
app.include_router(health.router)
|
||||||
app.include_router(agent.router)
|
app.include_router(agent.router)
|
||||||
|
|
||||||
|
|
||||||
# Backwards-compatible VAPID endpoint used by older clients
|
# Backwards-compatible VAPID endpoint used by older clients
|
||||||
@app.get("/api/vapid-public-key")
|
@app.get("/api/vapid-public-key")
|
||||||
async def vapid_public_key_legacy() -> JSONResponse:
|
async def vapid_public_key_legacy() -> JSONResponse:
|
||||||
|
|||||||
+84
-18
@@ -13,20 +13,50 @@ class LogStatus(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class PinLogin(BaseModel):
|
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):
|
class LogEntry(BaseModel):
|
||||||
slot_id: str = Field(..., min_length=1, max_length=64)
|
slot_id: str = Field(
|
||||||
status: LogStatus = LogStatus.taken
|
...,
|
||||||
day: date | None = None
|
min_length=1,
|
||||||
logged_at: str | None = None
|
max_length=64,
|
||||||
source: str = "app"
|
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):
|
class SnoozeRequest(BaseModel):
|
||||||
slot_id: str = Field(..., min_length=1, max_length=64)
|
slot_id: str = Field(
|
||||||
minutes: int = 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")
|
@field_validator("minutes")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -37,19 +67,55 @@ class SnoozeRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PushSubscribe(BaseModel):
|
class PushSubscribe(BaseModel):
|
||||||
subscription: dict
|
subscription: dict = Field(
|
||||||
|
...,
|
||||||
|
description="Web-Push-Subscription-Objekt vom Browser (PushManager.subscribe).",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AgentLogEntry(BaseModel):
|
class AgentLogEntry(BaseModel):
|
||||||
slot_id: str = Field(..., min_length=1, max_length=64)
|
slot_id: str = Field(
|
||||||
status: LogStatus = LogStatus.taken
|
...,
|
||||||
day: date | None = None
|
min_length=1,
|
||||||
logged_at: str | None = None
|
max_length=64,
|
||||||
source: str = "agent"
|
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):
|
class AgentNotifyRequest(BaseModel):
|
||||||
title: str = Field(..., min_length=1, max_length=120)
|
title: str = Field(
|
||||||
body: str = Field(..., min_length=1, max_length=512)
|
...,
|
||||||
slot_id: str | None = None
|
min_length=1,
|
||||||
tag: str | None = None
|
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.",
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user