583 lines
22 KiB
Markdown
583 lines
22 KiB
Markdown
# 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 = <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.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) |
|