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

+86
View File
@@ -0,0 +1,86 @@
package documents
import (
"net/http"
"github.com/google/uuid"
"aether/pkg/response"
)
// Handler zeigt den Stand der Texterkennung.
//
// Ohne diese Auskunft ist nicht zu erkennen, ob der Volltext funktioniert:
// Eine Suche, die nichts findet, sieht genauso aus wie eine Suche in einem
// Bestand, der noch gar nicht erkannt wurde. Nach dem ersten Start läuft der
// Arbeiter je nach Menge Stunden – ohne Zähler weiß niemand, ob er
// vorankommt.
type Handler struct {
store *Store
// isAdmin und userID trennen die Rechteprüfung von diesem Paket.
// documents kennt weder Sessions noch Rollen, und soll es auch nicht.
isAdmin func(r *http.Request) bool
userID func(r *http.Request) uuid.UUID
}
func NewHandler(store *Store, isAdmin func(r *http.Request) bool, userID func(r *http.Request) uuid.UUID) *Handler {
return &Handler{store: store, isAdmin: isAdmin, userID: userID}
}
// Status beantwortet GET /api/v1/admin/ocr – der Gesamtstand des Servers.
func (h *Handler) Status(w http.ResponseWriter, r *http.Request) {
if !h.isAdmin(r) {
response.Error(w, http.StatusForbidden, "admin required")
return
}
// h.store nil heißt: OCR ist abgeschaltet oder die Programme fehlen. Das
// ist eine Auskunft, kein Fehler – die Oberfläche soll „aus" anzeigen
// können, statt in einen Fehlerzustand zu laufen.
if h == nil || h.store == nil {
response.JSON(w, http.StatusOK, map[string]any{"enabled": false})
return
}
stats, err := h.store.Stats(r.Context())
if err != nil {
response.Error(w, http.StatusInternalServerError, "ocr status failed")
return
}
response.JSON(w, http.StatusOK, map[string]any{
"enabled": true,
"pending": stats.Pending,
"running": stats.Running,
"done": stats.Done,
"failed": stats.Failed,
"indexed": stats.Indexed,
})
}
// MyStatus beantwortet GET /api/v1/ocr – der Stand der **eigenen** Dateien.
//
// Ohne diesen Endpunkt wüsste nur der Administrator, ob ein Dokument schon
// gelesen wurde. Das ist die falsche Person: Wissen muss es, wer die Rechnung
// hochgeladen hat und sie gerade nicht findet.
func (h *Handler) MyStatus(w http.ResponseWriter, r *http.Request) {
uid := h.userID(r)
if uid == uuid.Nil {
response.Error(w, http.StatusUnauthorized, "not signed in")
return
}
if h == nil || h.store == nil {
response.JSON(w, http.StatusOK, map[string]any{"enabled": false})
return
}
stats, err := h.store.StatsForUser(r.Context(), uid)
if err != nil {
response.Error(w, http.StatusInternalServerError, "ocr status failed")
return
}
response.JSON(w, http.StatusOK, map[string]any{
"enabled": true,
"pending": stats.Pending,
"running": stats.Running,
"done": stats.Done,
"failed": stats.Failed,
"indexed": stats.Indexed,
})
}