Ka-Note/help.md

583 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Ka-Note — Handbuch
---
## Hierarchie
```
Kontext (AgendaContext) z.B. "JF Team Sysadmins", "Project TISAX"
└─ Thema (Topic) z.B. "TISAX: Sperren Produktionsrechner"
└─ Eintrag (HistoryEntry) z.B. "Hr. Müller angerufen. Server down."
└─ Bewertung (Rating) z.B. STEFE gibt 3/4
Wiki (separat, kein Kontext-Bezug nötig):
Notizbuch (Notebook) z.B. "Sysadmin-Doku", "Onboarding"
└─ Seite (Page) z.B. "VPN Setup", "AD-Gruppen"
(Seiten können mehreren Notizbüchern zugeordnet sein — Many-to-Many)
```
---
## Entitäten im Detail
### 1. Kontext (AgendaContext)
Oberster Container. Gruppiert Themen nach Anlass.
| Feld | Beschreibung |
|------|-------------|
| `id` | UUID (Sonderfall: `"daily-log"` ist hartkodiert) |
| `name` | Anzeigename |
| `type` | `meeting` · `project` · `person` · `company` |
| `sortOrder` | Reihenfolge in der Sidebar |
| `meta` | Typ-abhängig: ProjectMeta, PersonMeta oder CompanyMeta |
| `archivedAt` | Archiviert (soft) |
| `isFavorite` | In Sidebar angeheftet |
**Vier Typen:**
| Typ | Zweck | Meta-Felder |
|-----|-------|-------------|
| `meeting` | Regelmeetings, Daily Log | — |
| `project` | Projektbezogene Themen | status, owner, links |
| `person` | Personenbezogene Themen | fullName, email, phone, duSince, personSubType, notes |
| `company` | Firmenbezogene Themen | website, address, notes |
**Sonder-Kontext:** `daily-log` — immer vorhanden, Standard-Inbox, immer Meeting-Modus.
---
### 2. Thema (Topic)
Ein Diskussionspunkt / Agenda-Item / Aufgabenblock innerhalb eines Kontexts.
**Bündelt mehrere Einträge (Notizen).**
| Feld | Beschreibung |
|------|-------------|
| `id` | UUID |
| `contextId` | Verweis auf übergeordneten Kontext |
| `title` | Titel des Themas |
| `status` | `active` · `snoozed` · `done` · `archived` |
| `snoozeUntil` | Datum, ab dem snoozed-Thema wieder erscheint |
| `sortOrder` | Reihenfolge innerhalb des Kontexts |
| `isNew` | Zeigt grünes "NEU"-Badge bis erster Eintrag erstellt |
**Status-Bedeutung:**
| Status | Bedeutung |
|--------|-----------|
| `active` | Offen, wird angezeigt |
| `snoozed` | Versteckt bis `snoozeUntil`-Datum |
| `done` | Erledigt / archiviert |
| `archived` | Dauerhaft archiviert |
**Sonder-Themen (automatisch, versteckt):**
- `daily-log-journal` — Journal-Einträge im Daily Log
- `{contextId}-notes` — Freie Notizen je Projekt/Person
---
### 3. Eintrag (HistoryEntry)
Eine einzelne, datierte Notiz innerhalb eines Themas. Markdown-formatiert.
| Feld | Beschreibung |
|------|-------------|
| `id` | UUID |
| `topicId` | Verweis auf übergeordnetes Thema |
| `date` | Datum (YYYY-MM-DD) |
| `text` | Markdown-Inhalt mit Inline-Tags |
| `sortOrder` | Reihenfolge (neueste oben) |
| `linkedContextId` | Optionaler Verweis auf anderen Kontext |
| `doneAt` | Erledigt-Zeitstempel (toggle) |
| `wiedervorlageDate` | Wiedervorlage-Datum (YYYY-MM-DD) |
| `wiedervorlageResolvedAt` | Zeitstempel, wann Wiedervorlage abgehakt wurde |
**Inline-Tags im Text:**
| Syntax | Zweck | Beispiel |
|--------|-------|---------|
| `-> NAME` | Zuweisung an Person | `-> STEFE` |
| `@P:PROJEKT` | Projekt-Referenz | `@P:TISAX` |
| `@F:FIRMA` | Firmen-Referenz | `@F:VENDOR-X` |
| `@NAME` | Person erwähnen | `@CHFI` |
| `ka-img:{id}` | Eingebettetes Bild | `ka-img:abc123` |
---
### 4. Bewertung (Rating)
Bewertung eines Eintrags durch eine Person (14 Skala).
| Feld | Beschreibung |
|------|-------------|
| `id` | UUID |
| `topicId` | Verweis auf Thema |
| `historyEntryId` | Verweis auf bewerteten Eintrag |
| `personName` | Name des Bewertenden |
| `value` | `1` · `2` · `3` · `4` |
| `comment` | Optionaler Kommentar |
---
## Zusammenfassung: "Thema anlegen" vs. "Eintrag hinzufügen"
| Aktion | Erzeugt | Was passiert |
|--------|---------|-------------|
| **Thema anlegen** | Topic | Neuer Diskussionspunkt/Block. Kann danach beliebig viele Einträge enthalten. Erscheint mit "NEU"-Badge. |
| **Eintrag hinzufügen** | HistoryEntry | Einzelne datierte Notiz innerhalb eines bestehenden Themas. Markdown mit Inline-Tags. |
**Kurz:** Ein Thema ist der Ordner, ein Eintrag ist das Blatt darin.
---
### 5. Wiki-Seite (Page)
Frei-Form Markdown-Dokument. Nicht an einen Kontext gebunden.
| Feld | Beschreibung |
|------|-------------|
| `id` | UUID |
| `title` | Seitentitel — wird für `[[Wiki-Links]]` verwendet |
| `body` | Markdown-Inhalt |
| `isPrivate` | `true` = nur persönlich sichtbar (wie Journal) |
| `sortOrder` | Reihenfolge innerhalb von Notizbüchern |
**Wiki-Links:** `[[Titel]]` im Text einer Seite navigiert zu der Seite mit diesem Titel.
Bei Umbenennungen werden alle `[[AlterTitel]]`-Referenzen automatisch angepasst.
---
### 6. Notizbuch (Notebook)
Organisierer für Seiten — ähnlich einer Kategorie.
| Feld | Beschreibung |
|------|-------------|
| `id` | UUID |
| `name` | Anzeigename |
| `contextId` | Optional: Notizbuch ist einem Kontext zugeordnet (z.B. Projekt-Doku) |
| `isPrivate` | Privates Notizbuch |
| `sortOrder` | Reihenfolge in der Sidebar |
- `contextId = null` → eigenständiges Wiki-Notizbuch
- `contextId = <id>` → gehört zu einem Projekt/Meeting/Person-Kontext
---
### 7. Notizbuch-Seiten-Zuordnung (PageNotebook)
Join-Tabelle (Many-to-Many: eine Seite kann in mehreren Notizbüchern sein).
| Feld | Beschreibung |
|------|-------------|
| `pageId` | Verweis auf Page |
| `notebookId` | Verweis auf Notebook |
| `sortOrder` | Reihenfolge der Seite innerhalb des Notizbuchs |
---
## Querschnittliche Konzepte
### Soft-Delete & Purge (zweistufiges Löschen)
| Stufe | Feld | Auswirkung |
|-------|------|------------|
| Soft-Delete | `deletedAt` gesetzt | Eintrag im Papierkorb, aus UI verborgen, wiederherstellbar, wird weiterhin synchronisiert |
| Purge | `purgedAt` gesetzt | Clients löschen den Eintrag beim nächsten Pull aus ihrer lokalen DB. Server behält den Tombstone 30 Tage. Danach Vacuum. |
**Warum zweistufig?** Hard-Delete auf dem Server würde den Tombstone entfernen. Andere Clients, die noch nicht synchronisiert haben, würden beim nächsten inkrementellen Pull (`WHERE updated_at > since`) nichts finden — die Löschung käme nie an. Das `purgedAt`-Feld ist der Tombstone, der das Signal sicher überträgt.
### Basis-Felder (SyncEntity)
Jede Entität hat: `id`, `updatedAt`, `deletedAt`, `purgedAt`, `version`.
### Session-State (nicht persistiert)
- `processedInCurrentSession` — Thema wurde in laufender Sitzung besprochen (sessionStorage, verschwindet bei Tab-Schließung)
- `isCollapsed` — UI-only, ob Thema-Karte eingeklappt ist (nur im Svelte-Store)
### Modi
- **Prep-Modus** — Vorbereitung: Themen anlegen, Einträge schreiben
- **Meeting-Modus** — Durchführung: Themen als besprochen markieren, aufteilen in "Aktuell" / "Bereits besprochen"
---
## Datenspeicherung
### Client: IndexedDB via Dexie.js
Alle Daten liegen **lokal im Browser** in einer IndexedDB-Datenbank.
| Eigenschaft | Wert |
|-------------|------|
| DB-Name | `ka-note` |
| Library | Dexie.js v4 |
| Schema-Version | 10 |
| Schema-Datei | `client/src/lib/db/schema.ts` |
**Tabellen & Indizes:**
| Tabelle | Indizes | Zweck |
|---------|---------|-------|
| `contexts` | `id, type, sortOrder, deletedAt, archivedAt, isFavorite` | Kontexte |
| `topics` | `id, contextId, status, sortOrder, deletedAt` | Themen |
| `historyEntries` | `id, topicId, date, sortOrder, deletedAt, linkedContextId, doneAt, wiedervorlageDate, wiedervorlageResolvedAt` | Einträge |
| `ratings` | `id, topicId, historyEntryId, personName, deletedAt` | Bewertungen |
| `syncMeta` | `id, entityType, entityId, synced` | Sync-Status |
| `imageBlobs` | `id, contentHash, deletedAt, updatedAt` | Bilder als Binärdaten |
| `pages` | `id, deletedAt, isPrivate` | Wiki-Seiten |
| `notebooks` | `id, deletedAt, contextId` | Wiki-Notizbücher |
| `pageNotebooks` | `id, pageId, notebookId, deletedAt` | Seite-Notizbuch-Zuordnung |
**Reaktive Abfragen:** Svelte-Stores nutzen Dexie `liveQuery` (`client/src/lib/stores/agenda.ts`) — UI aktualisiert sich automatisch bei DB-Änderungen.
**CRUD-Schicht:** `client/src/lib/db/repositories.ts`
**Seed-Daten:** `client/src/lib/db/seed.ts` — Beispieldaten beim ersten Start.
### Server: SQLite via Drizzle ORM + Hono
| Eigenschaft | Wert |
|-------------|------|
| Framework | Hono v4 + @hono/node-server |
| ORM | Drizzle ORM (libsql-client) |
| Datenbank | SQLite (WAL-Modus, Foreign Keys aktiv) |
| DB-Dateipfad (Docker) | `/data/ka-note.db` |
| Env-Variable | `DATABASE_PATH` |
| Port | 3001 |
**Server-Tabellen (identische Struktur wie Client, zusätzlich `userId`):**
| Tabelle | Besonderheit |
|---------|-------------|
| `contexts` | Composite PK `(id, userId)` |
| `topics` | FK → contexts |
| `historyEntries` | FK → topics |
| `ratings` | FK → topics + historyEntries |
| `imageBlobs` | Binärdaten als BLOB |
| `pages` | Wiki-Seiten (id, title, body, isPrivate) |
| `notebooks` | Wiki-Notizbücher (id, name, contextId nullable) |
| `page_notebooks` | Many-to-Many Join (pageId, notebookId, sortOrder) |
| `aiLocks` | PK `userId`, Lock-Token + Ablaufzeit |
**Drizzle-Migrationen** laufen automatisch beim Server-Start (`drizzle/` Verzeichnis, Migrationen 00000007 + Wiki-Migration).
### API-Endpunkte
| Methode | Pfad | Funktion |
|---------|------|---------|
| `GET` | `/api/health` | Health-Check |
| `GET` | `/api/fetch-title` | URL-Titel scrapen (5s Timeout) |
| `POST` | `/api/sync/push` | Client → Server (Version-Konflikt-Erkennung) |
| `GET` | `/api/sync/pull` | Server → Client (`?since=ISO` oder alles) |
| `GET` | `/api/sync/status` | Diagnose: Zähler + userId + Server-Timestamp |
| `DELETE` | `/api/trash` | Purge-Tombstone setzen (soft-delete + purgedAt); kein Hard-Delete |
| `POST` | `/api/admin/vacuum` | Zeilen mit purgedAt > 30 Tage physisch löschen |
| `GET` | `/api/ai/status` | AI-Lock Status prüfen |
| `GET` | `/api/ai/download` | Workspace als ZIP exportieren + Lock setzen |
| `POST` | `/api/ai/upload` | ZIP importieren + Lock freigeben |
| `POST` | `/api/ai/unlock` | Lock manuell freigeben |
---
## Sync-Mechanismus
### Ablauf (bidirektional, optimistisch)
```
Client Server
│ │
│ 1. pushAll() │
│ Alle lokalen Entitäten │
│ POST /api/sync/push ─────────►│
│ │ version_client > version_server?
│ │ → update. Sonst: conflict.
│ ◄─────────────── {accepted, conflicts}
│ │
│ 2. pullAndMerge(since?) │
│ GET /api/sync/pull ──────────►│
│ │ since=null → alles
│ │ since=ISO → nur updatedAt > since
│ ◄─────────────── {changes, serverTimestamp}
│ │
│ Merge: version_server > version_local?
│ → db.put(). Sonst: behalten. │
│ │
│ lastSyncAt = serverTimestamp │
```
**Sync-Intervall:** Automatisch alle 30 Sekunden + bei Tab-Fokus-Wechsel.
**Full Sync:** ⟳-Button in der Sidebar — ignoriert `lastSyncAt`, holt alles.
### Konfliktauflösung
Höhere `version`-Nummer gewinnt. Gleichstand → Server behält seine Version (Client-Änderung wird als Konflikt geloggt, aber nicht überschrieben).
### Bilder
Bilder werden als `Blob` in IndexedDB gespeichert. Beim Sync werden sie in Base64 umgewandelt (chunked, 8 KB, um Stack-Overflow zu vermeiden) und mit den übrigen Entitäten übertragen.
---
## AI-Bundle-Workflow (Scripts)
Für KI-gestützte Massenimporte/-bearbeitungen gibt es einen Lock-basierten Export/Import-Workflow.
### Ablauf
```
download.ps1 → [Bearbeitung der JSON/MD-Dateien] → upload.ps1
↓ ↓
Lock wird gesetzt Lock wird freigegeben
(Push-Sync blockiert) (normaler Sync wieder aktiv)
```
### ZIP-Struktur
```
manifest.json — lockToken, userId, exportedAt, expiresAt, exportVersion
README.md — Workflow-Anleitung für LLMs (automatisch generiert)
contexts.json — alle Kontexte
topics.json — alle Themen
ratings.json — alle Bewertungen
notebooks.json — alle Wiki-Notizbücher
page_notebooks.json — Seite-Notizbuch-Zuordnungen (Join-Tabelle)
history/{id}.meta.json — Metadaten des HistoryEntry (ohne text)
history/{id}.md — Nur der Fließtext
wiki/{id}.meta.json — Metadaten der Wiki-Seite (ohne body)
wiki/{id}.md — Wiki-Seiteninhalt (Markdown)
images/{id}.{ext} — Bilder (SHA-256 contentHash als Dateiname)
```
### Pflichtfelder
**Context:** `id, name, type, sortOrder, updatedAt, deletedAt, version, archivedAt, isFavorite, meta`
**Topic:** `id, contextId, title, status, snoozeUntil, sortOrder, isNew, updatedAt, deletedAt, version`
**HistoryEntry (meta.json):** `id, topicId, date (YYYY-MM-DD), sortOrder, linkedContextId, doneAt, wiedervorlageDate, wiedervorlageResolvedAt, updatedAt, deletedAt, version`
→ Kein `contextId`, kein `title` im meta.json!
**Page (meta.json):** `id, title, isPrivate, sortOrder, updatedAt, deletedAt, version`
`body` ist in der `.md`-Datei.
**Notebook:** `id, name, contextId, isPrivate, sortOrder, updatedAt, deletedAt, version`
**PageNotebook:** `id, pageId, notebookId, sortOrder, updatedAt, deletedAt, version`
### Scripts (`ka-note/scripts/`)
| Script | Funktion |
|--------|---------|
| `download.ps1` | Export + Lock setzen, entpackt in `work/` |
| `upload.ps1` | ZIP hochladen + Lock freigeben |
| `unlock.ps1` | Lock manuell freigeben |
| `show-bundle.ps1` | Bundle-Inhalt anzeigen |
| `get-token.ps1` | Token aus `~\.ka-note\token.txt` lesen/prüfen |
| `set-token.ps1` | Token speichern (Argument oder Clipboard) |
| `import-helpers.ps1` | Hilfsfunktionen: `Upsert-Context`, `Add-Topic`, `Add-HistoryEntry` |
**Hinweis:** `work/` wird bei jedem `download.ps1` gelöscht. Token nie dort speichern.
---
## Lokale LLMs & AI-Bundle-Workflow
Lokale LLMs (z.B. Ollama, LM Studio, llama.cpp) können Ka-Note-Daten bearbeiten — genauso wie cloud-basierte LLMs — über denselben ZIP-Export/Import-Mechanismus.
### Warum funktioniert es für lokale LLMs?
- Das ZIP enthält alle Daten als plaintext JSON + Markdown.
- Das `README.md` im ZIP erklärt dem LLM alle Entitäten, Felder und den Upload-Workflow.
- Kein Internet-Zugang nötig: LLM liest ZIP lokal, bearbeitet Dateien, schickt neues ZIP zurück.
### Ablauf für lokale LLMs
```
1. download.ps1 → ZIP herunterladen, Lock gesetzt
2. ZIP entpacken → work/ Verzeichnis mit allen JSON/MD-Dateien
3. LLM-Kontext bauen: → README.md + relevante Dateien in Kontext laden
4. LLM bearbeitet Daten → JSON/MD-Dateien ändern (version++, updatedAt setzen)
5. ZIP packen → nur geänderte Dateien + manifest.json
6. upload.ps1 → ZIP hochladen, Lock freigegeben
```
### Empfehlungen für lokale LLMs
| Thema | Empfehlung |
|-------|-----------|
| **Kontextgröße** | Nur relevante history/*.md laden — nicht alle auf einmal. Großes Modell (≥ 8B) für komplexe Edits bevorzugen. |
| **System-Prompt** | README.md aus dem ZIP als System-Prompt verwenden. |
| **Nur Änderungen** | LLM soll nur geänderte Dateien zurückgeben. Unverändertes nicht in Upload-ZIP einschließen. |
| **Wiki-Seiten** | `wiki/*.md` sind normale Markdown-Dateien — für LLM ideal zu bearbeiten. `[[Links]]` werden automatisch aufgelöst. |
| **Versioning** | `version` MUSS bei jeder Änderung um 1 erhöht werden — sonst lehnt der Server die Änderung ab (Skip). |
| **Encoding** | ZIP mit Forward-Slashes in Pfaden (Unix-Style). `fflate` auf dem Server normalisiert automatisch. |
### Minimales Upload-ZIP (nur geänderte Dateien)
```
manifest.json ← immer erforderlich (lockToken)
wiki/abc123.meta.json ← geänderte Wiki-Seite (Metadaten)
wiki/abc123.md ← geänderter Wiki-Seitentext
```
### Häufige Fehler
| Fehler | Ursache | Lösung |
|--------|---------|--------|
| 409 Conflict | `version` nicht erhöht | version++ bei jeder Änderung |
| 400 Bad Request | `manifest.json` fehlt im ZIP | Immer einschließen |
| 500 Server Error | Ungültige `contextId` (soft-deleted) | Context zuerst restaurieren |
| Änderungen kommen nicht an | `updatedAt` nicht aktualisiert | Immer ISO-Timestamp setzen |
---
## Authentifizierung & Login-Ablauf
### Beteiligte Parteien
| Partei | Rolle |
|--------|-------|
| **Browser (SvelteKit SPA)** | Holt Access-Token von Microsoft, schickt ihn bei jedem API-Call mit |
| **Ka-Note Server (Hono)** | Prüft Token, identifiziert User per `oid`-Claim |
| **Microsoft Entra ID (Azure AD)** | Stellt Token aus, prüft Identität |
### Erstanmeldung (Login-Redirect-Flow)
```
Browser Microsoft Entra ID
│ │
│ 1. Klick "Sign in with Microsoft" │
│ msalInstance.loginRedirect() │
│ ──────────────────────────────────► │
│ │ 2. User gibt Credentials ein
│ 3. Redirect zurück zur App │ (oder SSO greift)
│ https://ka-note.../#code=... │
│ ◄──────────────────────────────── │
│ │
│ 4. handleRedirectPromise() │
│ → tauscht Code gegen Token │
│ ──────────────────────────────────► │
│ ◄────────────────────────── AccessToken + RefreshToken
│ │
│ 5. Token in localStorage (MSAL) │
│ 6. account-Store gesetzt │
│ 7. App startet, Sync beginnt │
```
### Token-Nutzung beim Sync
Der Server extrahiert `userId = payload.oid` aus dem JWT. Alle Daten im Server sind per `userId` isoliert — deshalb landen Änderungen vom Desktop automatisch auf iOS (gleicher Microsoft-Account → gleiche `oid`).
### Silent Token Refresh
Access-Token sind ~1 Stunde gültig. MSAL erneuert sie automatisch per verstecktem `<iframe>`:
```
acquireTokenSilent()
→ Token abgelaufen
→ MSAL öffnet hidden iframe → login.microsoftonline.com/authorize?prompt=none
→ Microsoft prüft Session-Cookie → OK
→ iframe navigiert zu /auth/redirect#code=...
→ handleRedirectPromise() → neuer Token in localStorage
```
**Browser-Warnung** (`Unsafe attempt to initiate navigation`): Die Microsoft-Login-Seite versucht dabei `window.top.location` zu setzen (Fallback-Code). Browser blocken das cross-origin — die Warnung ist **harmlos**, MSAL nutzt parallel `postMessage`.
### Token abgelaufen
```
acquireTokenSilent() schlägt fehl
→ syncService: syncStatus = 'auth-required'
→ Sidebar zeigt "⚠ Neu anmelden" (gelb)
→ Klick → login() → Redirect-Flow
```
### Konfiguration
| Parameter | Wert | Quelle |
|-----------|------|--------|
| `clientId` | Azure App Registration ID | `VITE_AZURE_CLIENT_ID` |
| `tenantId` | Azure AD Tenant | `VITE_AZURE_TENANT_ID` |
| `redirectUri` (Login) | `https://ka-note.azurewebsites.net` | msalConfig |
| `redirectUri` (Silent) | `https://ka-note.azurewebsites.net/auth/redirect` | acquireTokenSilent |
| Token-Cache | `localStorage` | geräteübergreifend persistent |
**Azure App Registration — Pflicht-Redirect-URIs:**
```
https://ka-note.azurewebsites.net
https://ka-note.azurewebsites.net/auth/redirect
```
---
## Deployment
### Docker
```bash
docker-compose -f ka-note/docker-compose.yml up --build
```
| Eigenschaft | Wert |
|-------------|------|
| Image | node:20-alpine (multistage build) |
| Port | 8080 → 3001 |
| Volume | `./data:/data` (SQLite-Persistenz) |
| Migrations | Automatisch beim Start |
### Umgebungsvariablen
| Variable | Wo | Beschreibung |
|----------|----|-------------|
| `AZURE_CLIENT_ID` | Server | Azure App Registration ID |
| `AZURE_TENANT_ID` | Server | Azure AD Tenant ID |
| `DATABASE_PATH` | Server | SQLite-Pfad (Standard: `/data/ka-note.db`) |
| `AI_LOCK_EXPIRY_HOURS` | Server | Lock-Ablauf in Stunden (Standard: 168 = 7 Tage) |
| `DEV_AUTH_BYPASS` | Server | `true` = JWT-Prüfung überspringen (nur Dev) |
| `VITE_AZURE_CLIENT_ID` | Client (Build) | Gleich wie Server |
| `VITE_AZURE_TENANT_ID` | Client (Build) | Gleich wie Server |
| `VITE_API_URL` | Client (Build) | API-Basis-URL (leer = gleiche Origin) |
| `VITE_DEV_AUTH_BYPASS` | Client (Build) | `true` = MSAL überspringen (nur Dev) |
### Build & Dev
```bash
# Aus ka-note/
npm install
# Dev (Client + Server parallel)
npm run dev
# Nur Client (http://localhost:5173)
npm run dev -w client
# Nur Server (http://localhost:3001)
npm run dev -w server
# Produktions-Build
npm run build
```
---
## Einstellungsseite (`/settings`)
Diagnose- und Wartungsseite — kein DevTools nötig.
| Bereich | Funktion |
|---------|---------|
| **Lokale Datenbank** | Live-Zähler (Dexie `liveQuery`) für alle Tabellen + letzter Sync-Zeitstempel |
| **Full Reset** | Löscht IndexedDB + `lastSyncAt` → Reload → Full Sync |
| **Server Status** | userId (OID aus JWT), Server-Timestamp, Zähler (gesamt / aktiv / gelöscht) pro Tabelle |
| **Vacuum** | Ruft `POST /api/admin/vacuum` auf — entfernt physisch alle Zeilen mit `purgedAt` älter als 30 Tage |
---
## Bekannte Einschränkungen
| Thema | Status |
|-------|--------|
| WebSocket-Sync (Echtzeit) | Nicht implementiert — nur REST-Polling (30s) |
| Konflikt-Anzeige im UI | Konflikte werden erkannt und geloggt, aber dem User nicht angezeigt |
| Offline-Push | Offline-Änderungen werden beim nächsten Sync nachgereicht (kein explizites Queue-Handling) |