# IllusionDB

Ein selbstgehostetes Verwaltungstool für bestehende Datenbanken: verbindet sich mit PostgreSQL, MySQL, MariaDB oder SQLite, liest deren Schema ein und stellt eine tabellenkalkulations-artige Oberfläche samt REST-API darüber bereit. Ausgeliefert als ein einziges Binary -- kein separater Webserver, keine externen Abhängigkeiten außer der Zieldatenbank selbst.

## Schnellstart

```bash
./illusiondb
```

Startet auf `http://localhost:8080` (Port konfigurierbar, siehe unten). Beim allerersten Start -- solange noch kein Benutzer existiert -- entweder:

- **`ADMIN_EMAIL`/`ADMIN_PASSWORD`** vorher setzen, dann legt IllusionDB den ersten Admin-Account automatisch beim Start an, oder
- ohne diese Variablen starten und den Einrichtungsdialog nutzen, der im Browser unter der oben geloggten URL erscheint.

Alle Daten (Metadaten, SQLite-eigene Datenquellen, hochgeladene Dateien) landen standardmäßig relativ zum Arbeitsverzeichnis unter `./data/` -- siehe die Pfad-Variablen unten, falls das Binary z.B. als Dienst aus einem anderen Verzeichnis heraus gestartet wird.

### Start per Doppelklick

Unter Windows öffnet ein Doppelklick auf `illusiondb.exe` automatisch ein Konsolenfenster (Windows startet für eine Konsolen-Anwendung von sich aus eine neue Konsole). Unter Linux und macOS tut ein reines Binary das nicht -- ein Doppelklick startet den Server dort lautlos im Hintergrund, ohne sichtbares Log oder die Setup-URL. Dagegen liegt jedem Linux- bzw. macOS-Release-Paket ein Starter bei:

- **Linux:** einmalig `./install-desktop-launcher.sh` ausführen (im entpackten Ordner). Das trägt IllusionDB ins Anwendungsmenü ein (`~/.local/share/applications/illusiondb.desktop`, mit dem zum Entpackungsort passenden absoluten Pfad) -- danach lässt sich IllusionDB wie jede andere installierte Anwendung starten und öffnet dabei ein Terminalfenster. Ein bewusster Umweg über die Anwendungsübersicht statt einer direkt doppelklickbaren Datei im Ordner: ein lose herumliegendes `.desktop`-File verlangt bei jedem Dateimanager erst eine "Starten erlauben"-Bestätigung, ein ins Menü installiertes gilt dagegen von Haus aus als vertrauenswürdig.
- **macOS:** `Start IllusionDB.command` -- per Doppelklick öffnet das Terminal.app und startet `illusiondb` darin. Da das Binary nicht signiert/notarisiert ist, verlangt Gatekeeper beim allerersten Start einmalig Rechtsklick → Öffnen statt eines einfachen Doppelklicks.

Beide Starter erwarten, dass sie im selben Ordner wie `illusiondb` liegen (so wie im Release-Archiv ausgeliefert) -- beim Verschieben des Ordners auf Linux `install-desktop-launcher.sh` einfach erneut ausführen, es überschreibt den alten Menüeintrag mit dem neuen Pfad.

## Umgebungsvariablen

| Variable | Default | Zweck |
|---|---|---|
| `PORT` | `8080` | Port, auf dem der HTTP-Server lauscht. |
| `META_DB_PATH` | `./data/illusiondb.db` | Pfad zur internen SQLite-Metadatenbank (Verbindungen, Tabellen-Metadaten, Benutzer, Views, Audit-Log usw. -- **nicht** deine eigentlichen Daten, die bleiben immer in der jeweils angebundenen externen Datenbank). |
| `ATTACHMENT_STORAGE_DIR` | `./data/attachments` | Ablageort für hochgeladene Dateien (Attachment-Feldtyp). |
| `SQLITE_DATA_DIR` | `./data/sqlite` | Ablageort für SQLite-Dateien, die über "Neue SQLite-Datenbank" direkt in IllusionDB angelegt werden (nicht für extern bereits bestehende SQLite-Dateien, die als Datenquelle verbunden werden). |
| `SCRIPTS_DIR` | `./data/scripts` | Ablageort, an dem jedes Scripting-Engine-Skript zusätzlich zu seiner Zeile in der Metadatenbank als `<id>.js` gespiegelt wird -- reine Backup-/Versionierungs-/Inspektions-Hilfe, keine zweite Quelle der Wahrheit (die App liest beim Start nie von dort; eine direkt auf der Platte bearbeitete Datei wirkt sich erst aus, wenn sie über die Oberfläche neu importiert wird). |
| `ADMIN_EMAIL` / `ADMIN_PASSWORD` | -- | Zugangsdaten für den automatisch angelegten Erst-Admin. Nur beim allerersten Start relevant (wenn noch keine Benutzer existieren); danach ohne Wirkung. |
| `APP_ENCRYPTION_KEY` | -- (unsicherer, öffentlich bekannter Dev-Schlüssel) | Base64-kodierter 32-Byte-Schlüssel (AES-256) zum Verschlüsseln gespeicherter Datenquellen-Passwörter und Webhook-Secrets. **Für jeden echten Betrieb setzen** -- ohne eigenen Schlüssel verweigert IllusionDB das Speichern neuer Secrets. Erzeugen z.B. mit `openssl rand -base64 32`. |
| `TRUST_PROXY_HEADERS` | `false` | Nur setzen, wenn IllusionDB hinter einem Reverse Proxy läuft, der `X-Forwarded-For`/`X-Forwarded-Proto` selbst setzt/überschreibt (Caddy, nginx, Traefik o.ä.). Wird dafür gebraucht, dass der Login-Rate-Limiter die echte Client-IP sieht und das Session-Cookie hinter TLS-Terminierung korrekt das `Secure`-Flag bekommt. Ohne echten vorgeschalteten Proxy **nicht** setzen -- sonst könnte ein Client diese Header selbst fälschen. |
| `LOGIN_RATE_LIMIT` | `10` | Maximale Login-/Setup-Versuche pro IP innerhalb von `LOGIN_RATE_WINDOW_SECONDS`. |
| `LOGIN_RATE_WINDOW_SECONDS` | `300` | Zeitfenster (Sekunden) für obiges Limit. |
| `OPEN_BROWSER` | `false` | Öffnet beim Start automatisch die Standard-Browser mit der App-URL -- praktisch für lokale/Desktop-Nutzung, für Server-Deployments (Dienst, Container, headless) weglassen. Die URL wird unabhängig davon immer geloggt. |

Empfehlung für einen echten (nicht-Dev-)Betrieb: **absolute Pfade** für `META_DB_PATH`, `ATTACHMENT_STORAGE_DIR`, `SQLITE_DATA_DIR` und `SCRIPTS_DIR` setzen, da das Binary von einem beliebigen Arbeitsverzeichnis aus gestartet werden kann (systemd-Unit, Windows-Dienst, Doppelklick) und die relativen Defaults sich sonst auf das jeweilige Startverzeichnis beziehen.

## Unterstützte Datenbanken

PostgreSQL, MySQL, MariaDB, SQLite -- sowohl bestehende Datenquellen anbinden als auch (nur SQLite) neue Datenbanken direkt aus der Oberfläche heraus anlegen.

## JavaScript-Scripting-Engine

Unter *Einstellungen → Skripte* (nur für Admins) können eigene JavaScript-Skripte angelegt werden, die entweder als **Custom Endpoint** (eigener HTTP-Endpunkt) oder als **Event-Hook** (reagiert auf Datensatz-Änderungen) laufen. Skripte laufen serverseitig in einer eingebetteten JS-Engine (goja) mit vollem Zugriff auf die Backend-Daten -- daher ausschließlich für Admins gedacht, kein Sandboxing gegenüber der eigenen Datenbank.

### Custom Endpoints

Ein Skript, gebunden an eine Methode + einen Pfad, ist sofort unter `/api/v1/scripts/<pfad>` erreichbar -- kein Neustart nötig. `auth_mode` steuert, ob der Endpunkt öffentlich oder nur für angemeldete Nutzer erreichbar ist.

Der Pfad kann `:name`-Segmente (fängt genau ein Pfad-Segment ein) und ein abschließendes `*` (fängt den kompletten Rest, auch mit Slashes, ein) enthalten, z. B. `/users/:id` oder `/files/:bucket/*`. Treffen mehrere registrierte Pfade auf denselben Request zu, gewinnt der spezifischere: wörtlich vor `:name` vor `*`. Bei gleicher Spezifität gewinnt der zuerst angelegte.

Der Skript-Code läuft in einer eigenen Funktion (nicht "auf oberster Ebene") -- `return wert;` ist
also ganz normales JavaScript und die einzige Möglichkeit, ein Ergebnis zu liefern; ein Skript ohne
`return` liefert `undefined`.

Im Skript stehen zwei Objekte zur Verfügung:

- **`request`** -- `method`, `path`, `params` (Objekt der aus `:name`/`*`-Segmenten eingefangenen Werte, leer wenn der Pfad keine hat), `query` (Objekt der Query-Parameter), `headers`, `body` (bei JSON-Body automatisch geparst, sonst der rohe Text).
- **`response`** -- verkettbar:
  - `response.status(code)`
  - `response.json(objekt)`
  - `response.send(text)`
  - `response.setHeader(name, wert)`

Für den einfachen Fall (200, JSON-Body, keine besonderen Header) reicht ein bloßes `return wert;` --
ruft das Skript `response.*` nie auf, wird sein Rückgabewert automatisch als JSON mit Status 200
gesendet (bzw. mit dem Status aus `response.status(code)`, falls der gesetzt, aber `.json()`/`.send()`
nie aufgerufen wurde). `response.*` bleibt der Weg für alles darüber hinaus -- eigene Status-Codes,
Header, Nicht-JSON-Antworten.

```js
// GET /api/v1/scripts/hello?name=Welt -- einfacher Fall, kein response.* nötig
return { message: "Hallo " + (request.query.name || "Welt") };
```

```js
// Pfad-Pattern "/users/:id" -- GET /api/v1/scripts/users/42
// explizit, z.B. weil ein Nicht-200-Status gebraucht wird
if (!request.params.id) {
	response.status(400).json({ error: "id fehlt" });
} else {
	response.status(200).json({ userId: request.params.id });
}
```

### Event-Hooks

Ein Skript, gebunden an `record.created`, `record.updated` oder `record.deleted` (optional auf eine einzelne Tabelle beschränkt, sonst für alle Tabellen), läuft im Hintergrund nach dem eigentlichen Schreibvorgang -- blockiert oder verhindert den ursprünglichen Write nie, auch nicht bei einem Fehler im Skript.

Im Skript steht `event` zur Verfügung: `event.name` (Ereignisname), `event.table` (`{id, schema_name, table_name}`), `event.record` (der geschriebene/gelöschte Datensatz).

```js
// Hook auf record.created: schreibt einen Log-Eintrag in eine andere Tabelle
illusiondb.createRecord("<audit-log-tabellen-id>", {
	message: "Neuer Datensatz in " + event.table.table_name + ": " + JSON.stringify(event.record)
});
```

### Backend-API (`illusiondb.*`)

In beiden Kontexten (Custom Endpoint und Hook) steht dasselbe Objekt für Datenzugriffe zur Verfügung. `tableId` ist überall die interne Tabellen-ID (aus der URL der Tabelle in IllusionDB selbst ablesbar), `pk` der Wert der Primärschlüssel-Spalte. Ein fehlgeschlagener Aufruf wirft eine JS-Exception (`try`/`catch` nutzbar).

| Funktion | Beschreibung |
|---|---|
| `illusiondb.getRecord(tableId, pk)` | Ein Datensatz per Primärschlüssel, oder `null` wenn nicht vorhanden. |
| `illusiondb.query(tableId, { filter, limit, offset })` | Liste von Datensätzen. `filter` ist ein Objekt `{spalte: wert}` (nur Gleichheit, per UND verknüpft), `limit`/`offset` optional (Standard-Limit: 1000). |
| `illusiondb.createRecord(tableId, values)` | Legt einen neuen Datensatz an (`values` = Objekt Spalte→Wert), gibt den geschriebenen Datensatz zurück. |
| `illusiondb.updateRecord(tableId, pk, values)` | Aktualisiert einen bestehenden Datensatz, gibt den aktualisierten Datensatz zurück (oder `null`, falls `pk` nicht existiert). |
| `illusiondb.deleteRecord(tableId, pk)` | Löscht einen Datensatz, gibt `true`/`false` zurück, je nachdem ob wirklich etwas gelöscht wurde. |
| `log(...)` | Schreibt ins Server-Log, mit dem Skriptnamen getaggt -- da ein Skript keine eigene Konsole hat, ist das die einzige Möglichkeit, während der Entwicklung etwas auszugeben. |

**Grenzen (bewusst, nicht bloß noch nicht umgesetzt):**

- Jeder Skriptlauf wird nach **3 Sekunden** abgebrochen (`vm.Interrupt()`) -- eine Endlosschleife blockiert damit nie dauerhaft einen Request oder Hook.
- Kein `fetch()` für ausgehende HTTP-Requests (SSRF-Risiko) und kein Dateisystemzugriff.
- Schreibende Aufrufe über `illusiondb.*` durchlaufen nicht die feldtyp-spezifische Schreib-Normalisierung der normalen REST-API (z. B. das Entfernen des UTC-Suffixes bei Datum/Zeit-Werten) -- ein Skript übergibt den Wert 1:1 an die Datenbank.
- `illusiondb.query`s `filter` unterstützt nur Gleichheitsvergleiche, keine Bereichs-/Textsuche-Operatoren wie die normale `?where=`-API.

## Weiterführendes

Eine vollständige REST-API-Dokumentation (inkl. Beispiel-`curl`-Aufrufen) ist direkt in der laufenden Anwendung enthalten, unter *Einstellungen → API-Dokumentation*.
