BasaltNotes
Handbuch
Selbst gehostete Notiz-App nach dem Vorbild von Obsidian – mit mehreren Vaults, [[Wiki-, Backlinks,
Eigenschaften, Volltextsuche und einem KI-Kurator, der neue Inhalte (z. B. Session-Recaps aus WhisperNotes)
selbstständig in den Bestand einsortiert: neue NPCs, Orte und Quests anlegen, bestehende ergänzen,
Schreibvarianten („Cydia“ → Zylia) als Alias lernen – und nichts still überschreiben.
Die Dateien sind die Wahrheit: Jeder Vault ist ein normaler Ordner mit Markdown-Dateien (und git-Verlauf) und lässt sich parallel in Obsidian öffnen.
Funktionen
| Bereich | Was es kann |
|---|---|
| Notizen | Markdown + YAML-Frontmatter, stabile IDs (ULID), Typen mit Ordnern, Vorlagen und Eigenschaften. D&D-Typen: Kampagne, Session, Skript (Vorbereitung), Charakter, NPC, Monster, Ort, Fraktion, Gegenstand, Quest, Lore, Fakten. Alltag: Notiz, Tagebuch, Aufgabe, Liste, Termin, Rezept, Idee, Lesezeichen, Anleitung, Besprechung, Person, Projekt – plus eigene Typen mit Feldern, Ordner und Vorlage (Vault-Einstellungen → Typen) |
| Freie Vaults | Vault-Art „Frei“: nur der Typ „Notiz“, Ordner frei wählbar, Eigenschaften frei, neue Notiz = Titel + Ordner. Datumsfelder im Kopf (start, date, due) erscheinen im Kalender |
| Verknüpfen | „Erwähnungen verknüpfen“ im Notiz-Menü: macht die erste Erwähnung anderer Notizen (Titel, Aliasse) zum Link – mit Vorschau, ohne KI. Als Kurator-Modus „Nur verknüpfen“ läuft das auch automatisch für den Eingang, selbst wenn die KI ausgeschaltet ist |
| Plugins | Eigene Erweiterungen mit Server- und Oberflächen-Teil (Seiten, Menüpunkte, Einstellungen, Endpunkte); Kalender & CalDAV sind ein mitgeliefertes Plugin |
| Home Assistant | Kalender und To-do-Listen per CalDAV (Plugin „Kalender & CalDAV“) (auch Handy/Thunderbird; Listen-Punkte abhaken und hinzufügen landet in der Notiz), Notizen per Sprache/Automation, Notizen fürs Dashboard |
| Kalender (Plugin) | Monats- und Listenansicht je Notizbuch oder über alle: Termine (auch ganztägig, mehrtägig, wiederkehrend), fällige Aufgaben zum Abhaken, Sessions, Besprechungen, Vorbereitungen und eigene Typen mit Datumsfeld |
| Alltag | „Heute“ öffnet den Tagebuch-Eintrag des Tages, Checklisten (-) lassen sich in der Leseansicht abhaken – z. B. Einkaufs- und Packlisten |
| Editor | CodeMirror 6 mit Live-Vorschau, [[-Autovervollständigung inkl. Aliasse, Bilder einfügen/ziehen, Autosave, Konflikterkennung |
| Verknüpfung | Backlinks mit Kontext, ausgehende Links, nicht aufgelöste Links legen per Klick die Notiz an, Umbenennen passt alle Links an |
| Suche | Volltext (SQLite FTS5) mit Treffer-Ausschnitten, Filter nach Typ/Kampagne/Tag/Eigenschaft, Schnellöffner Strg+O, Dataview-ähnliche Abfragen |
| Verlauf | git-Commit pro Speichern (gebündelt) und pro Kurator-Lauf, Diff und Wiederherstellen je Notiz, Papierkorb, ZIP-Export |
| Fragen | Chat mit den Notizen: passende Einträge werden ohne LLM vorausgewählt (Namen, Tippfehler, Übersichten, Erwähnungen, Volltext), das Modell bekommt nur diese – gebaut für kleine lokale Modelle |
| KI optional | Kurator und Chat sind bei einer neuen Einrichtung aus – einschalten im Einrichtungsschritt nach dem ersten Login oder unter Einstellungen → Funktionen; erst dann erscheinen Eingang, Vorschläge, Kurator-Protokoll und Fragen im Menü |
| KI-Kurator | Erkennen → Abgleichen → Planen → Anwenden/Vorschlagen; Regeln je Vault; Trockenlauf; Fortschrittsanzeige mit Stopp-Knopf; „Lauf rückgängig“ |
| Dokumente | PDF, Word, Text und Markdown in den Eingang hochladen: Text wird ausgelesen, Original als Anhang abgelegt, der Kurator sortiert ein |
| Vorbereitung | Typ „Skript (Vorbereitung)“ für Pre-Game-Skripte: eigener Ordner, Vorlage mit Szenen und Geheimnissen, Status geplant/gespielt/verworfen, nur für den DM; auf der Kampagnenseite unter „Vorbereitung“ |
| Benutzer | Mehrere Konten mit Rollen (Admin, Bearbeiten, Lesen, Spieler), Vault-Zugriff je Konto, Sperren, Passwort-Reset |
| Kampagnen | Startseite mit „Was bisher geschah“, Timeline, offene Quests, Charaktere, zuletzt geänderte Einträge, Spieler-Sicht |
| Einbindung | REST-API, OpenAPI, Server-Sent Events, Webhooks (HMAC-signiert), iframe-Einbettungen mit postMessage, JS-SDK |
| Oberfläche | Deutsch und Englisch (Umschalter DE/EN unten in der Seitenleiste), Hell/Dunkel, bis Handybreite, installierbar als PWA (Offline-Lesen), Graph-Ansicht |
Installation auf TrueNAS SCALE
Das fertige Image liegt auf Docker Hub: poldion/.
- Dataset für die Vaults anlegen: Datasets → Add Dataset, Name
BasaltVaults(Preset „Generic“ oder „Apps“). Rechte musst du nicht setzen – der Container übernimmt das beim Start. - Apps → Discover Apps → ⋮ → Install via YAML und den Inhalt von
docker-einfügen. Anpassen:compose. yml .durch den Pfad des Datasets ersetzen, z. B./ vaults /(Poolmnt/ Storage/ BasaltVaults Storage)ADMIN_– Passwort für den ersten BenutzerPASSWORD admin(nur beim ersten Start)LLM_– Adresse von Ollama bzw. des Ollama-Proxys (nur für die KI-Funktionen; Zeile einkommentieren)URL - ggf. den Port (
4400:8080→HOST:8080)
- Starten und
http:/öffnen, als/ :4400 adminanmelden. OhneADMIN_wird ein Passwort erzeugt und einmalig im App-Log ausgegeben.PASSWORD - Unter Einstellungen → Benutzer weitere Konten anlegen, unter API-Tokens Tokens für andere Apps.
Speicher:
| Was | Wo | Inhalt |
|---|---|---|
| App-Daten | Docker-Volume basaltnotes- → / (legt TrueNAS automatisch an) | config., index. (Benutzer, Tokens, Eingang, Kurator-Protokoll, Suchindex) |
| Vaults | Dataset BasaltVaults → / (direkt gemountet) | je Vault ein Ordner mit Markdown-Dateien, Anhängen und git-Verlauf |
Der Container startet kurz als root, setzt die Besitzrechte von / (inkl. Vaults) auf PUID:PGID
(Standard 568:568 = TrueNAS-Benutzer „apps“) und läuft dann ohne root-Rechte weiter.
Mit Docker Compose an anderer Stelle genauso: docker compose up -.
Update
Image neu ziehen und App neu starten (TrueNAS: „Update“ bzw. docker compose pull && docker compose up -).
Daten und Index bleiben im Volume, der Index wird bei Bedarf automatisch neu aufgebaut.
Konfiguration
Umgebungsvariablen haben Vorrang vor /.
| Variable | Standard | Bedeutung |
|---|---|---|
ADMIN_ | – | Passwort des ersten Benutzers (nur wenn noch kein Benutzer existiert) |
ADMIN_ | admin | Name des ersten Benutzers |
ADMIN_ | false | true: setzt das Passwort von ADMIN_ beim Start auf ADMIN_ (Notfall) |
FEATURE_ / FEATURE_ | false | KI-Kurator bzw. KI-Chat beim ersten Start einschalten. Danach gilt der Schalter unter Einstellungen → Funktionen (gespeichert in config.). Bestehende Installationen, die den Kurator schon benutzen, behalten ihn beim Update |
LLM_ | http:/ | Ollama bzw. Ollama-Proxy für Kurator und Chat (im Container meist http:/); lässt sich auch unter Einstellungen → „LLM-Server und Modell“ ändern |
LLM_ | – | leer = aktuell geladenes Modell (/, sonst /) |
LLM_ | – | leer = automatisch: / → Proxy / (forced_) → 4096 |
LLM_ | 600000 | Höchstens so lange auf das erste Token warten (Einlesen, ggf. Warten hinter anderen Anfragen) |
LLM_ | 120000 | Höchstens so lange ohne neues Token. Antworten werden gestreamt – langsame Modelle dürfen beliebig lange schreiben, solange sie liefern |
LLM_ / LLM_ | off | Nachdenken (Thinking) im Chat bzw. beim Kurator: off, on, low, medium, high (Stufen nur bei gpt-oss, sonst wie on). Modelle ohne Thinking schalten automatisch zurück auf aus |
LLM_ | 6000 | Höchstens so viele Zeichen Notizen gehen pro Chat-Frage an das LLM (zusätzlich begrenzt durch den Kontext des Modells). Kleiner = schneller |
CORS_ | – | Kommagetrennte Ursprünge, die per Browser auf die API dürfen (z. B. das DM-Panel) |
EMBED_ | = CORS_ | Ursprünge, die BasaltNotes als iframe einbetten dürfen |
AUTH_ | – | Anmeldung über Reverse-Proxy, z. B. Remote- (Authelia/Authentik). Nur setzen, wenn der Container ausschließlich über den Proxy erreichbar ist. Der Benutzer muss in BasaltNotes existieren. |
SESSION_ | 720 | Gültigkeit einer Anmeldung |
PUID / PGID | 568 / 568 | Benutzer/Gruppe, unter dem die App läuft und dem die Dateien gehören |
UMASK | 002 | Rechte neu angelegter Dateien |
PORT / DATA_ | 8080 / / | |
COMMIT_ | 5000 | Speichervorgänge werden so lange zu einem git-Commit gebündelt |
WATCH_ | true | Änderungen von außen (SMB, Obsidian) erkennen |
LOG_ / LOG_ | info / false | Protokollierung |
Adresse, Modell, Kontextgröße und Nachdenken lassen sich auch in der Oberfläche ändern (Einstellungen → LLM-Server und Modell, nur Admins): „Verbindung testen“ listet die installierten Modelle, Speichern gilt sofort für Kurator und Chat, ohne Neustart. Die Werte landen als
llm_inoverride /und haben Vorrang vordata/ config. yaml LLM_/URL LLM_/MODEL LLM_; „Auf Compose-Werte zurücksetzen“ entfernt sie wieder.NUM_ CTX
Ablage im Volume
/data
├─ config.yaml # Einstellungen (Secret, LLM, …)
├─ index.sqlite # Index, Benutzer, Tokens, Eingang, Läufe, Vorschläge
└─ vaults/
├─ dnd/ # Vault „DnD“ – normaler Markdown-Ordner
│ ├─ .basalt/vault.yaml # Typen, Ordner, Kurator-Regeln
│ ├─ .basalt/templates/ # Vorlagen je Typ
│ ├─ .git/ # Verlauf
│ ├─ _inbox/ _attachments/ .trash/
│ └─ Fluch des Strahd/NPCs/Zylia.md …
└─ notizen/
Backup: Das Dataset BasaltVaults samt git ist das Backup der Notizen (dazu TrueNAS-Snapshots).
index. im App-Volume enthält außerdem Benutzer, Tokens und das Kurator-Protokoll – das Volume liegt im
Apps-Pool und wird von dessen Snapshots erfasst.
Benutzer und Rollen
| Rolle | Darf |
|---|---|
| Administration | alles, inkl. Benutzer, Tokens, Webhooks, Vault-Einstellungen |
| Bearbeiten | Notizen lesen/schreiben, Eingang, Kurator-Vorschläge annehmen, Läufe rückgängig machen |
| Lesen | alles lesen |
| Spieler | nur Notizen und Bereiche mit visibility: players, optional nur eine Kampagne; kein Eingang/Kurator |
Jedes Konto kann auf einzelne Vaults beschränkt werden. Gesperrte Konten und Konten mit geänderten Rechten werden sofort abgemeldet. Der letzte Admin kann weder gelöscht noch herabgestuft werden. Passwörter werden mit Argon2 gespeichert, Tokens nur als Hash.
KI-Kurator
Eingang → 1 Erkennen → 2 Abgleichen → 3 Planen → 4 Anwenden/Vorschlagen → Protokoll + git-Commit
- Erkennen: Das LLM bekommt den Text und die bekannten Namen/Aliasse der Kampagne und antwortet nach
festem JSON-Schema (Ollama
format). Lange Texte werden in Abschnitte von etwa vier Seiten geteilt; die Antwort wird gestreamt, die Oberfläche zeigt Abschnitt und erzeugte Tokens. - Dauer: Bestimmt vor allem die Antwortgeschwindigkeit des Modells. Bei ~8 Tokens/s etwa 1–4 Minuten je Abschnitt, plus 10–20 s je unsicherem Namen.
- Verknüpfen: Das LLM liefert auch Beziehungen (Questgeber, Ziel einer Quest, Aufenthaltsort, Fraktion, Besitzer,
„liegt in“, Verbündete, Feinde). Beide Notizen bekommen einen Abschnitt „Beziehungen“ mit Links in beide Richtungen
(z. B. Quest „Ziel: [[Muffgallerte]]“ ↔ Monster „Ziel von: [[Beseitigung des Gestanks]]“). Passende Eigenschaften
(
giver,location,faction,owner,parent) werden bei neuen Einträgen direkt gesetzt, bei bestehenden nur vorgeschlagen und nur, wenn das Feld leer ist. - Duplikate vermeiden: Einträge ohne Kampagne gelten kampagnenübergreifend; Kurzformen werden dem vollen Namen zugeordnet („Fyrn“ → „Fyrn Linstök“, Artikel zählen nicht: „Die Eber Jagd“ = „Eberjagd“); existiert ein Eintrag mit gleichem Titel und Typ, wird er ergänzt statt „Name 2“ anzulegen.
- Duplikate suchen: Knopf auf der Vorschlags-Seite (ohne LLM, Sekunden). Findet gleiche Titel, Titel = Alias, Kurzform/voller Name, ähnliche Schreibweisen und gleiche Titel mit anderem Typ und legt Zusammenführ-Vorschläge an. Abgelehnte Paare werden nicht erneut vorgeschlagen. Beim Zusammenführen wandern Kurator-Beiträge als Bereiche mit, fehlende Eigenschaften werden übernommen, Links umgebogen.
- Räume: Räume und Bereiche innerhalb eines Ortes (z. B. „Raum A“ eines Dungeons) werden keine eigenen Notizen,
sondern Unterabschnitte unter „Räume“ im übergeordneten Ort. Beziehungen verlinken direkt auf den Raum
(
[[Die uralten Tunnel#Raum C – …]]). - Abgleichen: exakter Treffer über Titel/Alias → sonst Kandidaten über Jaro-Winkler, Levenshtein und Kölner Phonetik → bei Zweifel entscheidet das LLM.
- Planen:
create_,entity append_,block add_,mention add_laufen standardmäßig automatisch;alias update_,property contradiction,merge_,entities update_,quest deletewerden immer Vorschläge. Alles untermin_wird Vorschlag. Einstellbar inconfidence_ auto vault..yaml - Sicherheit: Der Kurator schreibt nur in verwaltete Bereiche
(
-). Eigener Text bleibt byte-genau erhalten. Gleiche Quelle → Bereiche werden ersetzt, nicht verdoppelt. DM-Wissen landet in Bereichen mitbasalt:block key="…" - - >…- / basalt:block - - > visibility="dm". - Rückgängig: per git-Revert; wurden die Notizen seitdem bearbeitet, werden nur die Beiträge des Laufs zurückgenommen.
- Kurator-Modi je Vault (Vault-Einstellungen): Aus, Nur verknüpfen (Erwähnungen vorhandener Notizen werden zu Links, ohne LLM, es wird nichts angelegt – geht auch bei ausgeschalteter KI) und Einsortieren (KI).
Fragen an die Notizen (Chat)
Unter Fragen in der Seitenleiste (oder im Notizmenü ⋯ → Fragen zu dieser Notiz) lässt sich mit den
Notizen chatten. Damit auch kleine Modelle gute Antworten geben, sucht BasaltNotes vorher ohne LLM heraus, was
zur Frage gehört, und schickt nur das – höchstens LLM_ Zeichen:
- Hauptthemen: Titel und Aliasse in der Frage – auch gebeugt („Kefirs“), als Teilname („Fyrn“ für „Fyrn Linstök“) oder mit Tippfehler („Muffelgallerte“). Bei Rückfragen („Und wo wohnt er?“) gilt das Thema der vorigen Antwort weiter. Vom Hauptthema geht der Inhalt mit Eigenschaften mit; lange Einträge werden auf die Abschnitte gekürzt, in denen die Begriffe der Frage vorkommen.
- Erwähnungen: die Textstellen anderer Einträge (Sessions und Skripte zuerst), die auf das Hauptthema verlinken.
- Verknüpft: direkt verlinkte Einträge als Einzeiler (Ort, Questgeber, …).
- Übersichten: „Welche Quests sind offen?“, „Welche NPCs gibt es?“ → Liste des Typs, ggf. nach Status gefiltert.
- Stand und Verlauf: „Was ist der aktuelle Stand?“, „Was ist zuletzt passiert?“, „Wo ist die Gruppe gerade?“ → Zeitlinie der Sessions in der Reihenfolge, in der sie angelegt bzw. hochgeladen wurden, die neueste Session ausführlich (Abschnitte wie „Ereignisse“ und „Offene Handlungsstränge“ zuerst), dazu das Ende von Chroniken. Vorbereitete Skripte erscheinen nur als Liste, weil sie nicht unbedingt gespielt sind.
- Volltext: Einträge, in denen Begriffe der Frage vorkommen, wenn noch Platz ist.
Rückfragen ohne eigenes Thema („Nee, die sind schon abgereist“) werden zusammen mit der vorigen Frage ausgewertet.
Findet sich nichts, antwortet BasaltNotes direkt, ohne das LLM zu fragen. Unter jeder Antwort stehen die Quellen und auf Wunsch genau der Text, den das LLM bekommen hat. Spieler-Konten sehen nur, was für Spieler freigegeben ist (ohne DM-Bereiche und Abschnitte mit „(DM)“ in der Überschrift). Läuft gerade der Kurator, kommt die Frage nach dem aktuellen Arbeitsschritt dran.
Plugins
BasaltNotes lässt sich mit Plugins erweitern. Der Kalender samt CalDAV ist selbst ein Plugin
(mitgeliefert, eingeschaltet) und dient als Beispiel: plugins/.
Ein- und ausschalten: Einstellungen → Plugins (wirkt sofort, die Seite lädt neu).
Aufbau – ein Ordner mit manifest.:
{
"id": "mein-plugin",
"name": "Mein Plugin",
"version": "1.0.0",
"description": "Was es tut",
"server": "dist/server.js",
"web": "dist/web.js",
"styles": "dist/web.css",
"default_enabled": false,
"http_methods": []
}
Eigene Plugins kommen nach / (Neustart des Containers lädt sie); mitgelieferte liegen im
Programm. Ein eigenes Plugin kann ein mitgeliefertes nicht ersetzen (gleiche id wird ignoriert).
Server-Teil (server, ESM): exportiert { api?(app, ctx), root?(app, ctx) }.
api– Endpunkte unter/. Anmeldung, CSRF, Rechte und Freigaben gelten wie im Kern.api/ v1 root– Endpunkte ab/(z. B./), mit eigener Anmeldung (dav ctx.).helpers. authenticateToken ctxbietet:db(lesend wie schreibend auf den Index),vaults(Notizen lesen/anlegen/ändern),helpers(vaultFor,noteFor,requireScope,accessOf,restrictSql,restrictionAllows,assertInArea,listTasks/setTask/appendTask,json,stripLink,error…),settings(eigene Einstellungen inconfig.),yaml allowArea(method, url, 'read'|'write')(Endpunkt für Zugänge mit Bereichs-Einschränkung freigeben) undlog.- Ausgeschaltete Plugins antworten automatisch mit 404, ohne Neustart.
Oberfläche (web, ESM im Browser): exportiert default function activate(ctx) mit
ctx. (Seite unter / bzw. /),
ctx. (Seitenleiste) und ctx.
(Karte unter Einstellungen). React, react- und @basaltnotes/ (api, useApp,
useTypes, useVaultEvents, Modal, Spinner, TypeDot …) stellt die App bereit – nicht mitbündeln.
Bauen: Typen und Build-Hilfe liegen in packages/:
// build.mjs
import { buildPlugin } from '@basaltnotes/plugin-sdk/build'
await buildPlugin({ root: import.meta.dirname, server: 'src/server.ts', web: 'src/web.tsx', styles: 'src/web.css' })
Plugins laufen mit den Rechten der App (Server) bzw. der angemeldeten Person (Oberfläche) – nur Plugins installieren, denen man vertraut.
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | angemeldet | Installierte Plugins {id, name, version, description, builtin, enabled, error, web, styles} |
| PUT | / | admin | Ein-/ausschalten {enabled} |
| GET | / (ohne /) | – | Oberflächen-Dateien (nur aus dem Ordner von web/styles) |
Integration
WhisperNotes → Eingang
POST /api/v1/vaults/dnd/inbox
Authorization: Bearer <token mit Rechten „Nur Eingang“ + „Lesen“>
Idempotency-Key: whispernotes:job:ab12cd34:v3
{
"source": { "app": "whispernotes", "key": "whispernotes:job:ab12cd34", "url": "http://…" },
"note": { "type": "session", "title": "2026-10-11 Session 1",
"frontmatter": { "campaign": "Fluch des Strahd", "date": "2026-10-11T22:00:00+02:00", "players": [ … ] },
"body": "## Was bisher geschah\n…" },
"attachments": [ { "name": "Welt & Lore", "kind": "lore", "markdown": "…", "visibility": "dm" } ],
"curate": "auto"
}
Antwort 202 {inbox_. Die Session-Notiz wird sofort angelegt, der Kurator läuft im
Hintergrund. Gleiche source. → Beiträge werden ersetzt. Whisper-Glossar:
GET / (Feld glossary).
Home Assistant
1. Zugang anlegen: Einstellungen → API-Tokens → Zugang für eine App, z. B. „Home Assistant“: Notizbuch
„Notizen“ mit lesen + schreiben (für D&D-Termine optional „DnD“ mit nur lesen). Das Token in
secrets. ablegen:
basalt_token: "Bearer bn_…"
2. Kalender und To-do-Listen (CalDAV, Plugin „Kalender & CalDAV“ muss eingeschaltet sein): Einstellungen → Geräte & Dienste → Integration hinzufügen → CalDAV
| Feld | Wert |
|---|---|
| URL | http:/ |
| Benutzername | beliebig, z. B. ha |
| Passwort | das Token (ohne „Bearer“) |
Danach gibt es je Notizbuch einen Kalender calendar. (Termine, Sessions, fällige Einträge –
Typ „Termin“ ist auch aus Home Assistant bearbeitbar) und To-do-Listen: „todo.). Abhaken, Hinzufügen und Löschen in Home
Assistant ändert direkt die Checkliste in der Notiz. Damit funktioniert auch Assist von Haus aus:
„Füge Milch zu Einkauf hinzu“.
Dieselbe Adresse funktioniert in Thunderbird, auf iOS (Einstellungen → Kalender → Accounts → CalDAV) und unter
Android mit DAVx⁵. Hinweise: Wiederholungen verstehen BasaltNotes und die Apps als täglich, wöchentlich, monatlich
oder jährlich (bis zu einem Enddatum) – komplexere Regeln (z. B. „jeden 2. Dienstag“) aus anderen Apps werden als
einfache Wiederholung gespeichert. Uhrzeiten gelten in der Zeitzone TZ des Containers (Standard Europe/).
3. Notizen per Sprache oder Automation: POST / legt Text ab – ohne Ziel als Zeile
mit Uhrzeit im heutigen Tagebuch, mit "list": "Einkauf" als Punkt auf einer Liste, mit "to": "note" als neue
Notiz.
# configuration.yaml
rest_command:
basalt_notiz:
url: "http://nas.local:4400/api/v1/vaults/notizen/capture"
method: POST
headers:
Authorization: !secret basalt_token
content_type: "application/json; charset=utf-8"
payload: '{"text": {{ text | tojson }}}'
intent_script:
BasaltNotiz:
action:
- action: rest_command.basalt_notiz
data:
text: "{{ text }}"
speech:
text: "Notiert."
# custom_sentences/de/basalt.yaml – „Notiere Heizung entlüftet“
language: "de"
intents:
BasaltNotiz:
data:
- sentences:
- "notiere {text}"
- "schreib ins tagebuch {text}"
lists:
text:
wildcard: true
In Automationen genauso, z. B. beim Verlassen des Hauses: action: rest_ mit
data: { text: "Haus verlassen, Fenster zu" }.
4. Notizen auf dem Dashboard: ?clean=1 liefert den Text ohne Frontmatter und ohne [[ ]].
# configuration.yaml
rest:
- resource: "http://nas.local:4400/api/v1/vaults/notizen/notes/by-title?title=Wochenplan&fields=title,body&clean=1"
headers:
Authorization: !secret basalt_token
scan_interval: 300
sensor:
- name: "Wochenplan"
value_template: "{{ value_json.title }}"
json_attributes:
- body
Markdown-Karte im Dashboard: content: "{{ state_. Termine zeigt die normale
Kalender-Karte mit calendar., Listen die To-do-Karte.
DM-Panel
- Kampagnen-Startseite in einem Aufruf:
GET /api/ v1/ vaults/ dnd/ campaigns/ {kampagne}/ overview?visibility=dm|players - Wiki im eigenen Layout:
GET …/+notes/ {id}/ html?link_ base=/ wiki/ {vault}/ {id} …/backlinks - Bearbeiten im Panel: iframe
/embed/ dnd/ note/ {id}?mode=edit&links=parent&theme=dark - Vorschläge prüfen: iframe
/embed/ dnd/ proposals?campaign=… - Live:
GET /(SSE) oder Webhooks (api/ v1/ events?vault=dnd X-)Basalt- Signature: sha256=HMAC(secret, body)
postMessage der Einbettungen: basalt:ready, basalt:navigate {noteId, title, noteType}, basalt:resize {height},
basalt:saved {noteId, version}; an die Einbettung: basalt:open {noteId}, basalt:setTheme {theme}, basalt:search {q}.
JS-SDK
packages/ (@basaltnotes/):
import { BasaltNotes } from '@basaltnotes/client'
const b = new BasaltNotes({ baseUrl: 'https://notes.home', token })
const overview = await b.campaigns.overview('dnd', 'fluch-des-strahd')
const html = await b.notes.html('dnd', noteId, { linkBase: '/wiki/{vault}/{id}' })
b.events.on('inbox.processed', (e) => toast(`${e.data.created} neue Einträge`), { vault: 'dnd' })
Alle Endpunkte stehen in der API-Referenz.
API-Referenz
Basis-URL: http:/ · JSON (UTF-8) · Zeitstempel ISO-8601 mit Zeitzone.
Die maschinenlesbare Beschreibung (OpenAPI 3) liefert der Server unter GET /.
Grundlagen
Anmeldung
| Weg | Verwendung |
|---|---|
Authorization: Bearer | Andere Apps (WhisperNotes, DM-Panel, Skripte). Tokens legt ein Admin unter Einstellungen → API-Tokens an. |
?token= | Wo keine Header möglich sind: EventSource, Bilder, iframe-Einbettungen |
Session-Cookie basalt_ | Web-Oberfläche. Schreibende Aufrufe brauchen zusätzlich X- (aus Login bzw. /). |
Proxy-Header (AUTH_) | Optional, z. B. Remote- von Authelia/Authentik |
Ohne Anmeldung erreichbar: GET /, POST /, GET /.
Rechte (Scopes)
| Scope | Erlaubt |
|---|---|
read | Lesen (in write und curate enthalten) |
write | Notizen, Ordner, Bereiche, Anhänge, Vorlagen schreiben (enthält inbox) |
inbox | Nur in den Eingang einliefern |
curate | Kurator: Vorschläge annehmen/ablehnen, Läufe rückgängig machen, Notizen kuratieren |
admin | Alles, inkl. Benutzer, Tokens, Webhooks, Vault-Einstellungen |
Tokens und Benutzer können auf einzelne Vaults beschränkt sein. Tokens/Konten mit Spieler-Sicht
(visibility: players) sehen nur Notizen und Bereiche mit visibility: players und haben keinen Zugriff
auf Eingang, Vorschläge und Kurator-Läufe. Rollen der Benutzerkonten: admin (alle Scopes),
editor (read, write, inbox, curate), viewer (read), player (read + Spieler-Sicht). Sichtbarkeit (visibility) gibt es nur in D&D-Vaults.
Fehlerformat
{ "error": { "code": "version_conflict", "message": "Die Notiz wurde inzwischen geändert.", "details": { "current_version": "sha1:…" } } }
message ist standardmäßig deutsch und mit X- oder Accept- englisch; maßgeblich ist code.
| Code | HTTP | Bedeutung |
|---|---|---|
validation_ | 400 | Ungültige Eingabe (z. B. fehlender Titel, fehlendes If-) |
unauthorized | 401 | Nicht angemeldet, Token ungültig/abgelaufen, Konto gesperrt |
forbidden | 403 | Fehlender Scope, kein Zugriff auf den Vault, CSRF-Token fehlt |
not_ | 404 | Vault, Notiz, Version, Vorschlag … nicht gefunden |
version_ | 409 | If- passt nicht – details. und details. enthalten den aktuellen Stand |
conflict | 409 | Sonstiger Konflikt (Name vergeben, Lauf schon rückgängig, letzter Admin …) |
rate_ | 429 | Zu viele Anfragen (Login: 10/min, sonst 1200/min je IP) |
llm_ | 503 | LLM nicht erreichbar |
Versionen, Idempotenz, Seiten
- Version/ETag: Jede Notiz hat
version(sha1:…des Dateiinhalts), auch alsETag-Header.PUTbrauchtIf-(Match: " " *= ohne Prüfung).PATCHprüft nur, wennIf-gesetzt ist. Verwaltete Bereiche (Match /) brauchen keinblocks If-.Match - Idempotenz:
POSTmit HeaderIdempotency-liefert bei Wiederholung dieselbe Antwort (HeaderKey: Idempotent-), 7 Tage lang.Replayed: true - Seiten: Listen akzeptieren
limit(Standard 50, max. 500) undcursor; die Antwort enthältnext_(cursor null= Ende). - Sichtbarkeit: Viele Lese-Endpunkte akzeptieren
?visibility=playersund filtern dann DM-Inhalte heraus. - CORS: Nur Ursprünge aus
CORS_; keine Wildcard.ORIGINS
System & Anmeldung
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | – | Status, Version, Notizen je Vault, LLM (erreichbar, Modell, Kontextgröße und Quelle), Warteschlange. ?refresh=1 prüft das LLM neu |
| GET | / | – | OpenAPI-Beschreibung |
| GET | / | angemeldet | Eingeschaltete Funktionen {curator, chat, setup_ (auch in /) |
| PUT | / | admin | Funktionen schalten {curator?, chat?, setup_ – gilt sofort. Ausgeschaltet antworten Kurator- (Vorschläge, Läufe, Kurator-Status, Notiz kuratieren) bzw. Chat-Endpunkte mit 403; der Eingang nimmt weiter an und legt nur ab |
| GET | / | admin | LLM-Einstellungen {url, model, num_ |
| GET | / | admin | Installierte Modelle eines Servers (?url= zum Testen vor dem Speichern) → {models: [{name, parameter_ |
| PUT | / | admin | {url?, model?, num_ speichern (gilt sofort, Vorrang vor Umgebungsvariablen); {"reset": true} entfernt die eigenen Werte |
| POST | / | – | {username, password} → setzt Cookie, liefert {user, csrf, expires} |
| POST | / | angemeldet | Abmelden |
| GET | / | angemeldet | {kind, name, user_ |
| POST | / | Benutzerkonto | Eigenes Passwort ändern {current, new} (andere Sitzungen werden beendet) |
Benutzer
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | admin | Alle Benutzer + verfügbare Rollen |
| POST | / | admin | Anlegen {username, password, role, vaults?, campaign?, display_ |
| PATCH | / | admin | Ändern: role, vaults, campaign, display_, disabled, password (Reset) |
| DELETE | / | admin | Löschen (nicht das eigene Konto, nicht den letzten Admin) |
vaults ist "*" oder eine Liste von Vault-IDs. Benutzername: 2–63 Zeichen (a-),
Passwort mindestens 8 Zeichen.
Tokens
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | admin | Tokens (ohne Geheimnis) mit „zuletzt benutzt“ |
| POST | / | admin | Zugang für eine App: {name, grants: [Freigabe], expires?} · klassisch: {name, scopes, vaults?, expires?, visibility?: "players", campaign?} → {token, …} – das Token wird nur einmal angezeigt |
| PATCH | / | admin | Freigaben ändern {grants} – der Token-Wert bleibt gleich |
| POST | / | read/write im Vault | Kurzlebiges Token für Einbettungen {vault, scope?: "read"|"write", ttl? (s, Standard 3600, max. |
| DELETE | / | admin | Widerrufen |
Beispiel WhisperNotes (klassisch): {"name": "WhisperNotes", "scopes": ["inbox", "read"], "vaults": ["dnd", "notizen"]}.
Freigaben je Notizbuch (Einstellungen → API-Tokens → Zugang für eine App): Jede Freigabe nennt ein Notizbuch und ob der Zugang nur lesen oder lesen + schreiben darf – optional beschränkt auf einen Bereich:
{
"name": "D&D-Tool",
"grants": [
{ "vault": "dnd", "access": "read", "campaign": "Regarion", "types": ["lore", "npc", "location"] },
{ "vault": "notizen", "access": "write", "folders": ["Rezepte"] }
]
}
| Feld | Bedeutung |
|---|---|
campaign | Nur Einträge dieser Kampagne. shared (Standard true) nimmt Einträge ohne Kampagne dazu (allgemeine Welt/Regeln) |
types | Nur diese Typen (z. B. lore, npc) |
folders | Nur Einträge in diesen Ordnern (inkl. Unterordner) |
Ein Zugang mit Bereich sieht in Liste, Suche, Abfrage, Namen, Graph, Kampagnen und Chat nur passende Einträge; Notizen außerhalb sind „nicht gefunden“, Links darauf werden nicht aufgelöst. Schreiben legt neue Notizen im Bereich an (Kampagne wird gesetzt, Ordner gewählt) und verweigert Änderungen, die eine Notiz aus dem Bereich schieben. Funktionen, die Inhalte außerhalb zeigen oder ändern könnten (Verlauf, Papierkorb, Export, Eingang, Kurator, Anhänge, Ordner, Tags, Live-Ereignisse), sind für solche Zugänge gesperrt. Kurator- und Admin-Rechte gibt es nur über klassische Tokens.
Vaults
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | angemeldet | Vaults (gefiltert nach Zugriff) mit note_, types, open_, inbox_ |
| POST | / | admin | Anlegen {name, preset: "dnd" | "notes" | "empty", lang?: "de" | "en"} – empty = frei (nur „Notiz“, Kurator „nur verknüpfen“); lang bestimmt die Sprache der Ordnernamen der Vorlage (Standard: Sprache der Anfrage) |
| POST | / | admin | Beispiel-Vault „Beispiel – Die Nebelküste“ (erfundene D&D-Kampagne) anlegen; 409, wenn es ihn schon gibt |
| GET | / | read | Details inkl. Kurator-Einstellungen |
| PATCH | / | admin | {name?, curator?: {enabled, mode: "full" | "link", auto_ – mode: link = nur Erwähnungen verlinken (ohne LLM) |
| DELETE | / | admin | Löschen (Ordner wird nach data/ verschoben) |
| GET | / | read | Typen mit Ordner, Vorlage und Eigenschafts-Schema (für dynamische Formulare); builtin = mitgeliefert |
| GET | / | read | Ein Typ mit template_ (Vorlage) und note_ |
| PUT | / | admin | Typ anlegen/ändern {label, folder, properties: [{name, label, type, options?, link_ – type: text, number, date, datetime, boolean, select, list, link, url. Ordner-Platzhalter {year}, {month}, {campaign}, {title} |
| DELETE | / | admin | Typ entfernen (nur ohne Notizen dieses Typs) |
| POST | / | write | Index aus den Dateien neu aufbauen → {notes, ms} |
| GET / PUT | / | admin | vault. lesen / ersetzen {yaml} |
| GET | / | read | {folders, notes: [{id, path, title, type}]} für Dateibäume |
| POST | / | write | Ordner anlegen {path} |
| PATCH | / | write | Ordner umbenennen/verschieben {from, to} |
| DELETE | / | write | Ordner löschen (Notizen → Papierkorb) |
| GET | / | read | Papierkorb |
| POST | / | write | Wiederherstellen {path} |
| DELETE | / | admin | Papierkorb leeren (Stände bleiben in git) |
| GET | / | read | Vorlagen {name, content} |
| PUT / DELETE | / | write | Vorlage speichern {content} / löschen. Platzhalter: {{title}}, {{date}}, {{campaign}}, {{type}} |
| GET | / | read | Vault als ZIP |
Notizen
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | read | Kalender (Plugin „Kalender & CalDAV“) ?from=JJJJ- (Standard: −7 bis +42 Tage, höchstens 400 Tage) → {items: [{id, note_. Quelle ist das erste Datumsfeld eines Typs: start (+end, all_, recurrence: täglich/wöchentlich/monatlich/jährlich, until), date, due (Aufgaben, done aus status), planned_. Zeiten sind Ortszeit (YYYY-), Wiederholungen sind aufgelöst |
| GET | / | angemeldet | Dasselbe über alle zugänglichen Notizbücher (?vaults=a,b zum Eingrenzen) |
| POST | / | write | Text schnell ablegen {text, to?: "journal"|"list"|"note", list?, title?, heading?} – Standard: Zeile mit Uhrzeit im heutigen Tagebuch; mit list als Punkt auf der Liste mit diesem Titel |
| POST | / | write | Tagebuch-Eintrag für heute (oder {date: "YYYY-) – wird angelegt, falls nötig → Notiz + created |
| GET | / | read | Aufgaben der Notiz (-) → {items: [{index, checked, text}], version} |
| POST | / | write | Erwähnungen vorhandener Notizen verlinken (ohne LLM) {dry_ → {linked, diff, note?}; je Ziel nur die erste Erwähnung, Code/Links/Überschriften bleiben unberührt |
| POST | / | write | Aufgabe abhaken/zurücksetzen {checked, text?} – ändert nur das Häkchen (und den Änderungszeitpunkt); passt text nicht mehr, gibt es 409 |
| GET | / | read | Liste. Filter: type (auch kommagetrennt), tag, campaign, folder, prop., q, updated_, sort (z. B. -), fields (z. B. title,type,status), limit, cursor |
| GET | / | read | Notiz. ?format=json (Standard) | markdown | html; ?fields=…; ?clean=1: Text ohne Frontmatter, Bereichs-Markierungen und [[ ]] (z. B. für Dashboards; auch bei by-) |
| GET | / | read | Über Pfad finden |
| GET | / | read | Über Titel, Dateiname oder Alias (&type=, &campaign= optional) |
| POST | / | write | Anlegen {title, type?, frontmatter?, body?, folder?, template?} → 201 |
| PUT | / | write | Ersetzen, If- nötig. Body {raw} (ganze Datei) oder {frontmatter, body} |
| PATCH | / | write | {frontmatter?: {…zusammenführen}, remove?: ["feld"], append?: "Text" | {text, heading}} |
| DELETE | / | write | In den Papierkorb |
| POST | / | write | {title?, folder?} – ID bleibt, alle Links im Vault werden angepasst → {…note, updated_ |
| GET | / | read | Eingehende Links {source_ |
| GET | / | read | Ausgehende Links {target, target_ |
| GET | / | read | Versionen aus git {rev, date, author, message} |
| GET | / | read | Inhalt einer Version; ?diff=1 als Unified-Diff zur aktuellen |
| POST | / | write | Version wiederherstellen {rev} |
| POST | / | curate | Notiz durch den Kurator schicken. ?dry_ → Plan mit Diffs (synchron), sonst 202 {job_; ?mode=review = alles als Vorschlag |
Antwort GET /:
{
"id": "01JA8Z6K3P8Q2V9W4X5Y6Z7A8B",
"vault": "dnd",
"path": "Fluch des Strahd/NPCs/Zylia.md",
"type": "npc",
"title": "Zylia",
"aliases": ["Cydia", "Sylvia"],
"tags": ["karawane"],
"campaign": "Fluch des Strahd",
"visibility": "players",
"frontmatter": { "campaign": "Fluch des Strahd", "role": "Karawanenführerin", "status": "lebendig", "visibility": "players" },
"body": "# Zylia\n\nEigener Text …",
"raw": "---\nid: 01JA…\n---\n# Zylia …",
"blocks": [{ "key": "whispernotes:job:ab12cd34", "source_note": "[[2026-10-11 Session 1]]", "visibility": null, "updated": "2026-10-12T01:40" }],
"links": [{ "target_id": "01JB…", "text": "Barovia", "target": "Barovia", "heading": null, "embed": false, "resolved": true, "type": "location" }],
"backlink_count": 4,
"version": "sha1:9f2c…",
"created": "2026-10-12T01:40:00+02:00",
"updated": "2026-10-12T01:40:00+02:00"
}
frontmatter enthält alle Eigenschaften außer id, type, title, aliases, created, updated.
raw fehlt in der Spieler-Sicht.
Ändern mit Konfliktschutz:
PUT /api/v1/vaults/dnd/notes/01JA8Z…
If-Match: "sha1:9f2c…"
Content-Type: application/json
{ "raw": "---\ntype: npc\ntitle: Zylia\n---\n# Zylia\n\nNeuer Text\n" }
→ 200 mit neuer version, oder 409 version_ mit dem aktuellen Stand in details..
Verwaltete Bereiche
Maschinen schreiben in markierte Abschnitte, eigener Text außerhalb bleibt byte-genau erhalten:
## Aus den Sessions
<!-- basalt:block key="whispernotes:job:ab12cd34" source="[[2026-10-11 Session 1]]" updated="2026-10-12T01:40" -->
- Führt die Karawane.
<!-- /basalt:block -->
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| PUT | / | write | Setzen/ersetzen {content, heading?, source_. Gleicher key ersetzt, neuer key hängt unter heading an (Überschrift wird bei Bedarf angelegt) |
| DELETE | / | write | Bereich entfernen |
| DELETE | / | write | Alle Bereiche dieses Schlüssels im Vault entfernen; &prefix=1 inkl. key:…-Unterschlüssel |
key URL-kodiert übergeben (whispernotes%3Ajob%3Aab12cd34). source_ darf eine Notiz-ID oder ein Titel sein.
Suche, Abfragen, Namen
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | read | Volltext (Präfixsuche, alle Wörter) mit highlights (…) und title_. Filter: type, campaign, tag, limit, cursor |
| POST | / | read | Strukturierte Abfrage (siehe unten) |
| GET | / | read | Kandidaten mit score 0–1 und match (exact, similar, alias_, fulltext); &type=, &campaign= |
| GET | / | read | Alle Titel + Aliasse: {items: [{id, name, type, campaign, aliases}], glossary: [.. ?campaign=…&types=npc,location |
| GET | / | read | [{tag, count}] |
| GET | / | read | {nodes: [{id, title, type, campaign, degree}], edges: [{source, target}]}; ?campaign=, ?type= |
| POST | / | read | Beliebiges Markdown im Kontext des Vaults zu bereinigtem HTML {markdown, link_ |
Abfrage:
POST /api/v1/vaults/dnd/query
{
"where": [
{ "field": "type", "op": "=", "value": "npc" },
{ "field": "campaign", "op": "=", "value": "Fluch des Strahd" },
{ "field": "attitude", "op": "in", "value": ["feindlich", "neutral"] }
],
"sort": [{ "field": "updated", "dir": "desc" }],
"select": ["title", "role", "faction", "status", "updated"],
"limit": 100
}
→ {items: [{id, title, role, …}], total, next_.
Felder: id, type, title, campaign, path, visibility, created, updated, tag sowie jede Eigenschaft
aus dem Frontmatter. Operatoren: =, !=, in, not_, contains, exists, not_, >, <, >=, <=
(Vergleiche ohne Groß-/Kleinschreibung; Link-Werte wie [[Barovia]] werden als Barovia verglichen).
Zusätzlich q für Volltext.
Chat
| Methode | Pfad | Scope | Beschreibung |
|---|---|---|---|
| POST | / | read | Frage beantworten → {answer, llm, sources, focus, chars, budget} |
| POST | / | read | Nur die Vorauswahl ohne LLM → {focus, sources, text, chars, budget, list_ |
Body: {"question": "Wer ist Kefir?", "history": [{role, content}], "previous": [".
Mit "stream": true kommt die Antwort als Server-Sent Events: context (Quellen), start (LLM liest ein),
thinking ({text}, nur mit eingeschaltetem Nachdenken), token ({text}), done ({answer, llm}) bzw. error. sources[].: focus, mention, related, list, fulltext.
Eingang (Inbox)
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| POST | / | inbox | Einliefern → 202 {inbox_ |
| POST | / | inbox | Dokument hochladen (multipart, siehe unten) → 202 {inbox_ |
| GET | / | read | Liste; ?status=queued,running,processed,stored,error,cancelled,discarded |
| GET | / | read | Status, Notiz, Ergebnis {run_, Lauf, Vorschläge, Rohdaten |
| POST | / | curate | Verarbeitung abbrechen (wartend → sofort, laufend → vor dem Schreiben); Status danach cancelled |
| POST | / | curate | Erneut verarbeiten (ersetzt Beiträge derselben Quelle); optional {curate: "auto" | "review"} |
| DELETE | / | curate | Verwerfen |
POST /api/v1/vaults/dnd/inbox
Idempotency-Key: whispernotes:job:ab12cd34:v3
{
"source": { "app": "whispernotes", "key": "whispernotes:job:ab12cd34", "url": "http://whispernotes.local:8000/dashboard/#dnd" },
"note": {
"type": "session",
"title": "2026-10-11 Session 1",
"frontmatter": {
"campaign": "Fluch des Strahd",
"date": "2026-10-11T22:00:00+02:00",
"players": [{ "player": "Robin", "character": null, "role": "dm" }, { "player": "Mara", "character": "Lyssara" }],
"transcript_url": "http://whispernotes.local:8000/api/jobs/ab12cd34/transcript?format=md"
},
"body": "## Was bisher geschah\n…\n## Ereignisse\n…"
},
"attachments": [
{ "name": "Spieler-Recaps", "kind": "players_recap", "markdown": "## Lyssara (Mara)\n…" },
{ "name": "Welt & Lore", "kind": "lore", "markdown": "## NPCs\n…", "visibility": "dm" }
],
"curate": "auto"
}
curate:auto(Regeln ausvault.),yaml review(alles als Vorschlag),none(nur ablegen).- Die Notiz wird sofort angelegt bzw. bei gleicher
source.aktualisiert (Titeländerung = Umbenennen).key bodyundattachmentslanden als verwaltete Bereiche in der Notiz, eigener Text darin bleibt erhalten. - Kampagne wird per Name zugeordnet (Groß-/Kleinschreibung egal) und bei Bedarf als Notiz angelegt;
Sessions ohne
numberwerden durchnummeriert. - Ohne
notegeht auch reiner Text:{"text": "Gruppe hat den Schlüssel verkauft", "campaign": "…", "curate": "review"}.
Dokumente hochladen (multipart/):
| Feld | Pflicht | Bedeutung |
|---|---|---|
file | ja | ., ., ., . (max. 50 MB) |
campaign | – | Kampagne (wird bei Bedarf angelegt) |
title | – | Titel der Notiz (Standard: Dateiname) |
type | – | Notiztyp (Standard: script – Skript (Vorbereitung) – im D&D-Vault, sonst note) |
visibility | – | Nur D&D-Vault: dm (Standard) oder players. Im Notizen-Vault gibt es keine Sichtbarkeit. |
curate | – | auto (Standard), review, none |
curl -X POST "$API/vaults/dnd/inbox/upload" -H "Authorization: Bearer $TOKEN" \
-F "file=@Fluch des Strahd.pdf" -F "campaign=Fluch des Strahd" -F "curate=review"
- Das Original landet in
_, die Notiz enthältattachments/ ![[datei.und den ausgelesenen Text (bei PDFs mitpdf]] ## Seite N-Überschriften) als verwalteten Bereich – damit ist alles durchsuchbar. - Bei
visibility: dmund bei Skripten behandelt der Kurator alles Erkannte als DM-Wissen: neue NPCs, Orte und Quests werden mitvisibility: dmangelegt und erscheinen nicht in der Spieler-Sicht. - Bei Skripten weiß der Kurator, dass die Ereignisse geplant und noch nicht geschehen sind.
- Lange Dokumente werden in Abschnitte geteilt und nacheinander ausgewertet; schlägt ein Abschnitt fehl, wird er übersprungen und im Kurator-Lauf vermerkt.
- Dieselbe Datei erneut hochladen aktualisiert dieselbe Notiz (Erkennung über den Inhalt).
- Eingescannte PDFs ohne Textebene werden abgelehnt (keine Texterkennung/OCR).
Vorschläge und Kurator-Läufe
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | read | ?status=open (Standard) | accepted | rejected | superseded | discarded | all, &campaign=, &run_, &diff=1 |
| GET | / | read | Vorschlag mit operation, reason, confidence und diff (unified) |
| PATCH | / | curate | Vor dem Annehmen anpassen: to, title, alias, field, frontmatter, type, note_, other_, block: {content} |
| POST | / | curate | Annehmen (wird angewendet und committet) |
| POST | / | curate | Ablehnen |
| POST | / | curate | {ids: [. |
| GET | / | read | Was der Kurator gerade tut: {running: [{run_ |
| POST | / | curate | Duplikate suchen ohne LLM → Zusammenführ-Vorschläge {found, known, created, pairs}; ?dry_ nur auflisten, ?min_ |
| POST | / | curate | Kurator stoppen: laufenden Lauf abbrechen, Warteschlange leeren → {stopped_ |
| GET | / | read | Läufe mit Status, Commit, Anzahl Änderungen/Vorschläge, Modell |
| GET | / | read | Eingabe, Erkennung (extraction inkl. resolutions), Plan, angewendete Operationen, Vorschläge |
| POST | / | curate | Rückgängig → {revert_ |
Ein Abschnitt, bei dem das LLM zu lange gar nichts liefert oder dessen Antwort abgeschnitten wird, wird übersprungen
(im Lauf unter llm. vermerkt); solche Fehler werden nicht automatisch wiederholt. Nur wenn das LLM nicht
erreichbar ist, versucht BasaltNotes es später erneut.
Abbrechen geschieht immer vor dem Schreiben: Die laufende LLM-Anfrage wird getrennt, der Lauf bekommt den Status
cancelled, im Vault ändert sich nichts. Ist ein Lauf schon beim Anwenden (wenige Sekunden), wird er noch fertig.
Abgebrochene Eingangseinträge lassen sich mit reprocess erneut verarbeiten.
Operationen (operation.): create_, append_, add_, add_ (standardmäßig automatisch),
update_, contradiction, merge_, update_, delete (immer Vorschlag).
Kampagnen (für das DM-Panel)
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | read | Kampagnen {id, title, slug, status, system, players} |
| GET | / | read | Alles für eine Startseite (siehe unten); ?visibility=dm|players |
| GET | / | read | Sessions chronologisch {id, title, number, date, transcript_ |
{kampagne} darf ID, Titel oder Kurzform sein (fluch-).
{
"campaign": { "id": "…", "title": "Fluch des Strahd", "slug": "fluch-des-strahd", "status": "aktiv", "system": "5e", "players": [], "dm": null },
"last_session": { "id": "…", "title": "2026-10-11 Session 1", "number": 1, "date": "…", "recap_excerpt": "Die Gruppe …" },
"sessions": [{ "id": "…", "number": 1, "date": "…", "title": "…" }],
"open_quests": [{ "id": "…", "title": "Brücke über den Bach", "status": "offen", "giver": "[[Zylia]]" }],
"characters": [{ "id": "…", "title": "Lyssara", "player": "Mara", "class": null, "status": null }],
"recent_entities": [{ "id": "…", "type": "npc", "title": "Zylia", "updated": "…" }],
"counts": { "npc": 12, "location": 7, "item": 4, "faction": 2 },
"scripts": [{ "id": "…", "title": "Abend 5 – Vallaki", "planned_for": "2026-10-25", "updated": "…" }],
"open_proposals": 3
}
Darstellung und Anhänge
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | read | Bereinigtes HTML; ?link_ (Platzhalter {vault}, {id}, {title}), ?visibility=players |
| GET | / | read | Anhang (Bilder, PDFs, Audio) |
| POST | / | write | Upload (multipart, Feld file, max. 50 MB) → {name, path, url, embed: "![[name]]"} |
Links im HTML: note-;
nicht aufgelöste Links tragen class="internal-.
Ereignisse und Webhooks
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | / | read | Server-Sent Events (mehrere Vaults kommagetrennt) |
| GET | / | admin | Webhooks mit den letzten 20 Zustellungen |
| POST | / | admin | {url, events?: [. → Secret wird einmal angezeigt |
| DELETE | / | admin | Löschen |
Ereignisse: note., note., note., note., inbox., inbox.,
inbox., inbox., proposal., proposal., run..
{ "event": "inbox.processed", "vault": "dnd", "at": "2026-10-12T01:42:00+02:00",
"data": { "inbox_id": "…", "note_id": "…", "run_id": "…", "created": 3, "updated": 5, "proposals": 2 } }
- SSE:
event:+data:; alle 25 s ein Kommentar als Keep-alive. Änderungen von außen (SMB/Obsidian) tragendata..external: true - Webhooks:
POSTmit JSON-Body und den HeadernX-,Basalt- Event X-undBasalt- Delivery X-. Bei Fehler (kein 2xx, Zeitlimit 10 s) bis zu 6 Versuche mit wachsendem Abstand.Basalt- Signature: sha256= SHA256(secret, body) hex>
Signatur prüfen (Node):
const sig = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex')
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.headers['x-basalt-signature']))
Einbett-Ansichten (iframe)
| URL | Inhalt |
|---|---|
/ | Eine Notiz lesen oder bearbeiten |
/ | Kampagnen-Startseite |
/ | Suche |
/ | Vorschläge prüfen |
/ | Graph |
Parameter: theme=light|dark|auto, visibility=dm|players, links=internal|parent, token=.
Erlaubte einbettende Seiten: EMBED_.
postMessage (nur mit erlaubten Ursprüngen):
| Richtung | Nachricht |
|---|---|
| Einbettung → Gast | {type: "basalt:ready"}, {type: "basalt:resize", height}, {type: "basalt:navigate", noteId, title, noteType} (bei links=parent), {type: "basalt:saved", noteId, version} |
| Gast → Einbettung | {type: "basalt:open", noteId}, {type: "basalt:setTheme", theme}, {type: "basalt:search", q} |
Anmeldung in Einbettungen: entweder gleiche Domain hinter dem Reverse-Proxy (Session-Cookie) oder ein
kurzlebiges Token, das der Gast-Server über POST / holt und als ?token= anhängt.
Beispiele mit curl
TOKEN=bn_… # aus Einstellungen → API-Tokens
API=http://nas:4400/api/v1
# Notiz anlegen
curl -s -X POST "$API/vaults/dnd/notes" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"type":"npc","title":"Zylia","frontmatter":{"campaign":"Fluch des Strahd","role":"Karawanenführerin"}}'
# Volltextsuche
curl -s "$API/vaults/dnd/search?q=karawane&type=npc" -H "Authorization: Bearer $TOKEN"
# Bereich in eine Notiz schreiben (ersetzt bei gleichem key)
curl -s -X PUT "$API/vaults/dnd/notes/<id>/blocks/panel%3Aencounter%3A42" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"heading":"Begegnungen","content":"- Kampf gegen Schleimmonster gewonnen"}'
# Whisper-Glossar
curl -s "$API/vaults/dnd/names?campaign=Fluch%20des%20Strahd&types=npc,location,character" -H "Authorization: Bearer $TOKEN"
# Live-Ereignisse
curl -N "$API/events?vault=dnd" -H "Authorization: Bearer $TOKEN"
Entwicklung
Voraussetzungen: Node.js ≥ 22.13 (empfohlen 24), git.
npm install
npm run dev # API auf :8080 (DATA_DIR=./data)
npm run dev:web # Oberfläche auf :5173 mit Proxy auf die API
npm test # Server-Tests (inkl. Golden-Tests für den Kurator)
npm run build # Server, Oberfläche und SDK bauen
Docker-Image bauen:
docker build -t poldion/basaltnotes:latest .
Aufbau: server/ (Fastify, TypeScript, SQLite über node:sqlite), web/ (React, CodeMirror 6, Vite),
packages/ (SDK). Die Anforderungen stehen in anforderungen. (historisch).