feat(aether): initial commit – self-hosted Cloud, Phasen 1–7

Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).

Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 5 committed 2026-08-24 09:25:36 +02:00
commit 31a027c93a
225 files changed
+38042

No files matched your search

@@ -0,0 +1,63 @@
-- Volltext über den *Inhalt* – die zweite Hälfte der Suche.
--
-- Migration 000013 machte Datei*namen* durchsuchbar. Das reicht, solange
-- Dateien sprechende Namen haben. Ein Scanner nennt seine Ablage aber
-- "scan_0042.pdf", und genau die Belege sollen auffindbar werden.
-- ── Der erkannte Text ─────────────────────────────────────────────────────────
-- Eigene Tabelle statt einer Spalte an files: Der Text ist um Größenordnungen
-- länger als der Rest der Zeile, und files wird bei jedem Auflisten gelesen.
-- Postgres lagert lange Werte zwar aus (TOAST), aber die tsvector-Spalte und
-- ihr GIN-Index gehörten dann trotzdem an eine Tabelle, die ständig in
-- Bewegung ist.
CREATE TABLE file_text (
file_id UUID PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
content TEXT NOT NULL,
-- Woher der Text stammt. 'layer' = das PDF hatte bereits eine Textebene
-- (pdftotext, verlustfrei); 'ocr' = aus dem Bild erkannt, kann Fehler
-- enthalten. Der Unterschied ist für die Fehlersuche wichtig: Findet die
-- Suche ein Wort nicht, sagt die Herkunft, ob OCR es verlesen hat.
source TEXT NOT NULL CHECK (source IN ('layer', 'ocr')),
-- Die Sprachen, mit denen erkannt wurde ("deu+eng"). Bei 'layer' leer.
lang TEXT NOT NULL DEFAULT '',
-- Zeichenzahl des Originals vor dem Kürzen – siehe maxTextBytes im Worker.
chars INTEGER NOT NULL DEFAULT 0,
pages INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Berechnete Spalte statt eines Triggers: Sie kann nie mit dem Inhalt
-- auseinanderlaufen, weil Postgres sie bei jedem Schreiben selbst füllt.
--
-- 'german' fest verdrahtet, nicht 'simple': Ohne Stammformen fände
-- "Rechnung" die "Rechnungen" nicht. Deutsch ist die Sprache der Belege,
-- um die es hier geht; englische Wörter darin verlieren nur ihre
-- Stammformbildung, sie bleiben wörtlich auffindbar. Eine Änderung wäre
-- eine Migration – der Preis dafür, dass der Ausdruck IMMUTABLE sein muss.
tsv tsvector GENERATED ALWAYS AS (to_tsvector('german', content)) STORED
);
CREATE INDEX idx_file_text_tsv ON file_text USING gin (tsv);
-- ── Die Warteschlange ─────────────────────────────────────────────────────────
-- Warum eine Tabelle und nicht einfach ein Goroutine-Aufruf wie bei den
-- Vorschaubildern: OCR dauert Sekunden bis Minuten. Startet der Server in
-- dieser Zeit neu – Update, Absturz, Neustart des Wirts –, ist ein Auftrag im
-- Speicher verloren, und die Datei bleibt für immer unauffindbar, ohne dass es
-- jemand merkt. Bei einem Vorschaubild fällt das auf; bei fehlendem Text nicht.
CREATE TABLE ocr_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Ein Auftrag je Datei. Wird dieselbe Datei erneut hochgeladen, ersetzt der
-- neue Auftrag den alten (ON CONFLICT), statt sich daneben zu stapeln.
file_id UUID NOT NULL UNIQUE REFERENCES files(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'running', 'done', 'failed')),
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Der Arbeiter fragt nur nach offenen Aufträgen. Ein Teilindex bleibt klein,
-- auch wenn sich zehntausend erledigte Zeilen ansammeln.
CREATE INDEX idx_ocr_jobs_pending ON ocr_jobs(created_at)
WHERE status IN ('pending', 'running');