# 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 (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. --- ### 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 = ` → 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 0000–0007 + 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 `