# 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 ``` --- ## 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 (1–4 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. --- ## 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 | 9 | | 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 | **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 | | `aiLocks` | PK `userId`, Lock-Token + Ablaufzeit | **Drizzle-Migrationen** laufen automatisch beim Server-Start (`drizzle/` Verzeichnis, 7 Migrationen 0000–0007). ### 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 contexts.json — alle Kontexte topics.json — alle Themen ratings.json — alle Bewertungen history/{id}.meta.json — Metadaten des HistoryEntry (ohne text) history/{id}.md — Nur der Fließtext 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, body, sortOrder, isPinned, deletedAt, updatedAt, 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! ### 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. --- ## 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 `