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

+101
View File
@@ -0,0 +1,101 @@
import { useState } from 'react'
import type { FolderItem, User } from '../api/client'
import { useNavHistory, type View } from '../hooks/useNavHistory'
import Sidebar from './Sidebar'
import FileBrowser from '../pages/FileBrowser'
import TrashPage from '../pages/TrashPage'
import SharedPage from '../pages/SharedPage'
import SharedWithMePage from '../pages/SharedWithMePage'
import AdminPage from '../pages/AdminPage'
import ProfilePage from '../pages/ProfilePage'
import TagsPage from '../pages/TagsPage'
import { UserMenuProvider } from './UserMenu'
export type { View }
interface Props {
user: User
onLogout: () => Promise<void>
/** Lädt den Nutzer neu – für Speicherverbrauch und Anzeigename. */
onUserChanged: () => Promise<void>
}
export default function Shell({ user, onLogout, onUserChanged }: Props) {
const nav = useNavHistory()
const { view, folderPath } = nav
const [sidebarOpen, setSidebarOpen] = useState(false)
// Aus der Seitenleiste kennen wir nur Kennung und Name – die Kette darueber
// holt die Liste nicht mit, also faengt der Pfad hier beim Ordner selbst an.
function navigateToFolder(folderId: string, folderName: string) {
setSidebarOpen(false)
const stub: FolderItem = {
id: folderId, owner_id: null, group_id: null, parent_id: null,
name: folderName, created_at: '',
}
nav.setFolderPath([stub])
}
return (
<UserMenuProvider value={{
user,
onNavigate: (v) => { nav.setView(v); setSidebarOpen(false) },
onLogout,
onOpenSidebar: () => setSidebarOpen(true),
}}>
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
{/* Mobile overlay */}
{sidebarOpen && (
<div
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 40 }}
onClick={() => setSidebarOpen(false)}
/>
)}
<Sidebar
user={user}
view={view}
activeTagId={nav.tagId}
onOpenTag={nav.setTag}
activeCorrespondentId={nav.correspondentId}
onOpenCorrespondent={nav.setCorrespondent}
onNavigate={(v) => { nav.setView(v); setSidebarOpen(false) }}
onNavigateToFolder={navigateToFolder}
open={sidebarOpen}
/>
{/* Main content */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
{view === 'files' && (
<FileBrowser
user={user}
folderPath={folderPath}
onNavigate={nav.setFolderPath}
onUserChanged={onUserChanged}
/>
)}
{view === 'trash' && <TrashPage onUserChanged={onUserChanged} />}
{view === 'incoming' && <SharedWithMePage onOpenFolder={navigateToFolder} />}
{view === 'shared' && <SharedPage />}
{view === 'admin' && <AdminPage />}
{view === 'profile' && <ProfilePage user={user} onUserChanged={onUserChanged} />}
{/* Etikett und Absender teilen sich die Seite – es ist dieselbe Frage
(„alles mit diesem Merkmal"), nur ein anderes Merkmal. */}
{(view === 'tags' || view === 'from') && (
<TagsPage
tagId={view === 'tags' ? nav.tagId : undefined}
correspondentId={view === 'from' ? nav.correspondentId : undefined}
onOpenFolder={(id, name) => {
// Klick auf einen Treffer springt zum Ordner – dieselbe Regel wie
// in der Suche: Geöffnet wird per Doppelklick auf die Datei.
if (id) navigateToFolder(id, name)
else nav.setFolderPath([])
}}
/>
)}
</div>
</div>
</UserMenuProvider>
)
}