Ka-Note/help.md

22 KiB
Raw Blame History

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, versionbody 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

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)