22 KiB
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-NotizbuchcontextId = <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 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.mdim 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
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
# 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) |