From 31a027c93a713366864a7a912ee0ded61230865f Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 24 Aug 2026 09:25:36 +0200 Subject: [PATCH] =?UTF-8?q?feat(aether):=20initial=20commit=20=E2=80=93=20?= =?UTF-8?q?self-hosted=20Cloud,=20Phasen=201=E2=80=937?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .dockerignore | 26 + .gitignore | 28 + CLAUDE.md | 165 + README.md | 417 ++ TESTING.md | 1079 +++ backend/cmd/aether/main.go | 622 ++ backend/db/migrations/000001_init.down.sql | 12 + backend/db/migrations/000001_init.up.sql | 103 + backend/db/migrations/000002_phase2.down.sql | 3 + backend/db/migrations/000002_phase2.up.sql | 35 + .../db/migrations/000003_favorites.down.sql | 1 + backend/db/migrations/000003_favorites.up.sql | 11 + .../migrations/000004_phase3_admin.down.sql | 4 + .../db/migrations/000004_phase3_admin.up.sql | 35 + backend/db/migrations/000005_oidc.down.sql | 1 + backend/db/migrations/000005_oidc.up.sql | 3 + .../000006_file_versioning.down.sql | 1 + .../migrations/000006_file_versioning.up.sql | 11 + .../db/migrations/000007_user_colors.down.sql | 3 + .../db/migrations/000007_user_colors.up.sql | 9 + .../000008_use_custom_theme.down.sql | 1 + .../migrations/000008_use_custom_theme.up.sql | 13 + .../migrations/000009_app_passwords.down.sql | 1 + .../db/migrations/000009_app_passwords.up.sql | 15 + .../db/migrations/000010_audit_log.down.sql | 1 + backend/db/migrations/000010_audit_log.up.sql | 27 + .../db/migrations/000011_acl_entries.down.sql | 1 + .../db/migrations/000011_acl_entries.up.sql | 51 + .../migrations/000012_group_folders.down.sql | 18 + .../db/migrations/000012_group_folders.up.sql | 35 + .../migrations/000013_search_indexes.down.sql | 5 + .../migrations/000013_search_indexes.up.sql | 12 + .../migrations/000014_document_text.down.sql | 2 + .../db/migrations/000014_document_text.up.sql | 63 + backend/db/migrations/000015_tags.down.sql | 4 + backend/db/migrations/000015_tags.up.sql | 75 + .../db/migrations/000016_text_pages.down.sql | 1 + .../db/migrations/000016_text_pages.up.sql | 13 + .../migrations/000017_correspondents.down.sql | 3 + .../migrations/000017_correspondents.up.sql | 52 + .../db/migrations/000018_tag_icons.down.sql | 2 + backend/db/migrations/000018_tag_icons.up.sql | 15 + backend/go.mod | 34 + backend/go.sum | 124 + backend/internal/acl/acl.go | 351 + backend/internal/acl/acl_test.go | 60 + backend/internal/acl/handler.go | 263 + backend/internal/acl/store.go | 288 + backend/internal/admin/appicon.go | 142 + backend/internal/admin/appicon_test.go | 122 + backend/internal/admin/audit_handler.go | 99 + backend/internal/admin/branding_handler.go | 416 ++ backend/internal/admin/groupfolder_handler.go | 115 + backend/internal/admin/handler.go | 490 ++ backend/internal/admin/manifest_handler.go | 95 + backend/internal/admin/model.go | 41 + backend/internal/admin/store.go | 297 + backend/internal/audit/audit.go | 207 + backend/internal/auth/apppassword.go | 264 + backend/internal/auth/apppassword_test.go | 66 + backend/internal/auth/handler.go | 266 + backend/internal/auth/middleware.go | 67 + backend/internal/auth/model.go | 37 + backend/internal/auth/oidclink_test.go | 90 + backend/internal/auth/password.go | 49 + backend/internal/auth/password_test.go | 39 + backend/internal/auth/session.go | 118 + backend/internal/auth/store.go | 211 + backend/internal/documents/diskspace.go | 66 + backend/internal/documents/extract.go | 302 + backend/internal/documents/handler.go | 86 + backend/internal/documents/store.go | 315 + backend/internal/documents/worker.go | 239 + backend/internal/files/access.go | 63 + backend/internal/files/groupfolder.go | 165 + backend/internal/files/handler.go | 781 ++ backend/internal/files/handler_favorites.go | 131 + backend/internal/files/handler_folders.go | 273 + backend/internal/files/handler_text.go | 50 + backend/internal/files/handler_trash.go | 144 + backend/internal/files/handler_versions.go | 202 + backend/internal/files/handler_zip.go | 210 + backend/internal/files/mime_test.go | 37 + backend/internal/files/model.go | 64 + backend/internal/files/naming.go | 108 + backend/internal/files/quota.go | 63 + backend/internal/files/replace.go | 93 + backend/internal/files/search.go | 199 + backend/internal/files/search_test.go | 22 + backend/internal/files/store.go | 535 ++ backend/internal/files/store_favorites.go | 129 + backend/internal/files/store_folders.go | 209 + backend/internal/files/store_webdav.go | 102 + backend/internal/files/store_zip.go | 120 + backend/internal/gc/gc.go | 245 + backend/internal/gc/gc_test.go | 28 + backend/internal/office/discovery.go | 200 + backend/internal/office/handler.go | 305 + backend/internal/office/office.go | 167 + backend/internal/oidc/handler.go | 196 + backend/internal/sharing/handler.go | 202 + backend/internal/sharing/model.go | 18 + backend/internal/sharing/store.go | 123 + backend/internal/storage/localfs.go | 280 + backend/internal/storage/localfs_test.go | 223 + backend/internal/storage/storage.go | 70 + backend/internal/tags/correspondents.go | 235 + backend/internal/tags/handler.go | 362 + backend/internal/tags/store.go | 535 ++ backend/internal/thumbnails/thumbnails.go | 95 + backend/internal/webdav/fs.go | 873 +++ backend/internal/webdav/fs_test.go | 59 + backend/internal/webdav/handler.go | 155 + backend/internal/webdav/scope.go | 310 + backend/internal/webdav/scope_test.go | 64 + backend/pkg/config/config.go | 271 + backend/pkg/db/db.go | 107 + .../pkg/db/migrations/000001_init.down.sql | 12 + backend/pkg/db/migrations/000001_init.up.sql | 103 + .../pkg/db/migrations/000002_phase2.down.sql | 3 + .../pkg/db/migrations/000002_phase2.up.sql | 35 + .../db/migrations/000003_favorites.down.sql | 1 + .../pkg/db/migrations/000003_favorites.up.sql | 11 + .../migrations/000004_phase3_admin.down.sql | 4 + .../db/migrations/000004_phase3_admin.up.sql | 35 + .../pkg/db/migrations/000005_oidc.down.sql | 1 + backend/pkg/db/migrations/000005_oidc.up.sql | 3 + .../000006_file_versioning.down.sql | 1 + .../migrations/000006_file_versioning.up.sql | 11 + .../db/migrations/000007_user_colors.down.sql | 3 + .../db/migrations/000007_user_colors.up.sql | 9 + .../000008_use_custom_theme.down.sql | 1 + .../migrations/000008_use_custom_theme.up.sql | 13 + .../migrations/000009_app_passwords.down.sql | 1 + .../db/migrations/000009_app_passwords.up.sql | 15 + .../db/migrations/000010_audit_log.down.sql | 1 + .../pkg/db/migrations/000010_audit_log.up.sql | 27 + .../db/migrations/000011_acl_entries.down.sql | 1 + .../db/migrations/000011_acl_entries.up.sql | 51 + .../migrations/000012_group_folders.down.sql | 18 + .../db/migrations/000012_group_folders.up.sql | 35 + .../migrations/000013_search_indexes.down.sql | 5 + .../migrations/000013_search_indexes.up.sql | 12 + .../migrations/000014_document_text.down.sql | 2 + .../db/migrations/000014_document_text.up.sql | 63 + .../pkg/db/migrations/000015_tags.down.sql | 4 + backend/pkg/db/migrations/000015_tags.up.sql | 75 + .../db/migrations/000016_text_pages.down.sql | 1 + .../db/migrations/000016_text_pages.up.sql | 13 + .../migrations/000017_correspondents.down.sql | 3 + .../migrations/000017_correspondents.up.sql | 52 + .../db/migrations/000018_tag_icons.down.sql | 2 + .../pkg/db/migrations/000018_tag_icons.up.sql | 15 + backend/pkg/middleware/failurelimit.go | 102 + backend/pkg/middleware/failurelimit_test.go | 63 + backend/pkg/middleware/logger.go | 43 + backend/pkg/middleware/proxy.go | 75 + backend/pkg/middleware/security.go | 55 + backend/pkg/response/response.go | 27 + cloud-projekt-plan.md | 596 ++ deploy/.env.example | 160 + deploy/Dockerfile | 58 + deploy/docker-compose.yml | 137 + frontend/index.html | 22 + frontend/package-lock.json | 6522 +++++++++++++++++ frontend/package.json | 31 + frontend/public/icon-192.png | Bin 0 -> 6372 bytes frontend/public/icon-512.png | Bin 0 -> 13418 bytes frontend/public/icon-maskable-512.png | Bin 0 -> 36793 bytes frontend/public/icon.svg | 12 + frontend/src/App.tsx | 63 + frontend/src/api/client.ts | 813 ++ frontend/src/components/AppPasswords.tsx | 217 + frontend/src/components/AppSwitcher.tsx | 114 + frontend/src/components/Breadcrumb.tsx | 87 + frontend/src/components/ConfirmDialog.tsx | 55 + frontend/src/components/ContextMenu.tsx | 110 + frontend/src/components/Dialog.tsx | 66 + frontend/src/components/FileCover.tsx | 144 + frontend/src/components/FileInfoDialog.tsx | 185 + frontend/src/components/FilePreview.tsx | 373 + frontend/src/components/FileViews.tsx | 511 ++ frontend/src/components/MoveTo.tsx | 320 + frontend/src/components/OfficeEditor.tsx | 137 + frontend/src/components/PageHeader.tsx | 193 + frontend/src/components/PdfViewer.tsx | 957 +++ frontend/src/components/SearchBox.tsx | 258 + frontend/src/components/ShareDialog.tsx | 190 + frontend/src/components/ShareWithDialog.tsx | 272 + frontend/src/components/Shell.tsx | 101 + frontend/src/components/Sidebar.tsx | 745 ++ frontend/src/components/Tabs.tsx | 45 + frontend/src/components/TagChip.tsx | 84 + frontend/src/components/TagDialog.tsx | 340 + frontend/src/components/TagEditDialog.tsx | 322 + frontend/src/components/UploadQueue.tsx | 57 + frontend/src/components/UserMenu.tsx | 204 + frontend/src/components/VersionsDialog.tsx | 216 + frontend/src/components/filetypes.ts | 80 + frontend/src/components/icons.tsx | 256 + frontend/src/components/pdf.css | 208 + frontend/src/hooks/useAppSettings.tsx | 115 + frontend/src/hooks/useAuth.ts | 55 + frontend/src/hooks/useNavHistory.ts | 176 + frontend/src/hooks/useOverlayDismiss.ts | 25 + frontend/src/index.css | 82 + frontend/src/main.tsx | 12 + frontend/src/pages/AdminPage.tsx | 54 + frontend/src/pages/FileBrowser.tsx | 1072 +++ frontend/src/pages/LoginPage.tsx | 163 + frontend/src/pages/ProfilePage.tsx | 429 ++ frontend/src/pages/SharePage.tsx | 177 + frontend/src/pages/SharedPage.tsx | 170 + frontend/src/pages/SharedWithMePage.tsx | 129 + frontend/src/pages/TagsPage.tsx | 221 + frontend/src/pages/TrashPage.tsx | 198 + frontend/src/pages/admin/AuditTab.tsx | 329 + frontend/src/pages/admin/GroupsTab.tsx | 357 + frontend/src/pages/admin/ThemingTab.tsx | 295 + frontend/src/pages/admin/UsersTab.tsx | 264 + frontend/src/pages/admin/ui.tsx | 119 + frontend/src/test-setup.ts | 1 + frontend/src/theme.ts | 43 + frontend/tsconfig.json | 23 + frontend/vite.config.ts | 104 + 225 files changed, 38042 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 TESTING.md create mode 100644 backend/cmd/aether/main.go create mode 100644 backend/db/migrations/000001_init.down.sql create mode 100644 backend/db/migrations/000001_init.up.sql create mode 100644 backend/db/migrations/000002_phase2.down.sql create mode 100644 backend/db/migrations/000002_phase2.up.sql create mode 100644 backend/db/migrations/000003_favorites.down.sql create mode 100644 backend/db/migrations/000003_favorites.up.sql create mode 100644 backend/db/migrations/000004_phase3_admin.down.sql create mode 100644 backend/db/migrations/000004_phase3_admin.up.sql create mode 100644 backend/db/migrations/000005_oidc.down.sql create mode 100644 backend/db/migrations/000005_oidc.up.sql create mode 100644 backend/db/migrations/000006_file_versioning.down.sql create mode 100644 backend/db/migrations/000006_file_versioning.up.sql create mode 100644 backend/db/migrations/000007_user_colors.down.sql create mode 100644 backend/db/migrations/000007_user_colors.up.sql create mode 100644 backend/db/migrations/000008_use_custom_theme.down.sql create mode 100644 backend/db/migrations/000008_use_custom_theme.up.sql create mode 100644 backend/db/migrations/000009_app_passwords.down.sql create mode 100644 backend/db/migrations/000009_app_passwords.up.sql create mode 100644 backend/db/migrations/000010_audit_log.down.sql create mode 100644 backend/db/migrations/000010_audit_log.up.sql create mode 100644 backend/db/migrations/000011_acl_entries.down.sql create mode 100644 backend/db/migrations/000011_acl_entries.up.sql create mode 100644 backend/db/migrations/000012_group_folders.down.sql create mode 100644 backend/db/migrations/000012_group_folders.up.sql create mode 100644 backend/db/migrations/000013_search_indexes.down.sql create mode 100644 backend/db/migrations/000013_search_indexes.up.sql create mode 100644 backend/db/migrations/000014_document_text.down.sql create mode 100644 backend/db/migrations/000014_document_text.up.sql create mode 100644 backend/db/migrations/000015_tags.down.sql create mode 100644 backend/db/migrations/000015_tags.up.sql create mode 100644 backend/db/migrations/000016_text_pages.down.sql create mode 100644 backend/db/migrations/000016_text_pages.up.sql create mode 100644 backend/db/migrations/000017_correspondents.down.sql create mode 100644 backend/db/migrations/000017_correspondents.up.sql create mode 100644 backend/db/migrations/000018_tag_icons.down.sql create mode 100644 backend/db/migrations/000018_tag_icons.up.sql create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/internal/acl/acl.go create mode 100644 backend/internal/acl/acl_test.go create mode 100644 backend/internal/acl/handler.go create mode 100644 backend/internal/acl/store.go create mode 100644 backend/internal/admin/appicon.go create mode 100644 backend/internal/admin/appicon_test.go create mode 100644 backend/internal/admin/audit_handler.go create mode 100644 backend/internal/admin/branding_handler.go create mode 100644 backend/internal/admin/groupfolder_handler.go create mode 100644 backend/internal/admin/handler.go create mode 100644 backend/internal/admin/manifest_handler.go create mode 100644 backend/internal/admin/model.go create mode 100644 backend/internal/admin/store.go create mode 100644 backend/internal/audit/audit.go create mode 100644 backend/internal/auth/apppassword.go create mode 100644 backend/internal/auth/apppassword_test.go create mode 100644 backend/internal/auth/handler.go create mode 100644 backend/internal/auth/middleware.go create mode 100644 backend/internal/auth/model.go create mode 100644 backend/internal/auth/oidclink_test.go create mode 100644 backend/internal/auth/password.go create mode 100644 backend/internal/auth/password_test.go create mode 100644 backend/internal/auth/session.go create mode 100644 backend/internal/auth/store.go create mode 100644 backend/internal/documents/diskspace.go create mode 100644 backend/internal/documents/extract.go create mode 100644 backend/internal/documents/handler.go create mode 100644 backend/internal/documents/store.go create mode 100644 backend/internal/documents/worker.go create mode 100644 backend/internal/files/access.go create mode 100644 backend/internal/files/groupfolder.go create mode 100644 backend/internal/files/handler.go create mode 100644 backend/internal/files/handler_favorites.go create mode 100644 backend/internal/files/handler_folders.go create mode 100644 backend/internal/files/handler_text.go create mode 100644 backend/internal/files/handler_trash.go create mode 100644 backend/internal/files/handler_versions.go create mode 100644 backend/internal/files/handler_zip.go create mode 100644 backend/internal/files/mime_test.go create mode 100644 backend/internal/files/model.go create mode 100644 backend/internal/files/naming.go create mode 100644 backend/internal/files/quota.go create mode 100644 backend/internal/files/replace.go create mode 100644 backend/internal/files/search.go create mode 100644 backend/internal/files/search_test.go create mode 100644 backend/internal/files/store.go create mode 100644 backend/internal/files/store_favorites.go create mode 100644 backend/internal/files/store_folders.go create mode 100644 backend/internal/files/store_webdav.go create mode 100644 backend/internal/files/store_zip.go create mode 100644 backend/internal/gc/gc.go create mode 100644 backend/internal/gc/gc_test.go create mode 100644 backend/internal/office/discovery.go create mode 100644 backend/internal/office/handler.go create mode 100644 backend/internal/office/office.go create mode 100644 backend/internal/oidc/handler.go create mode 100644 backend/internal/sharing/handler.go create mode 100644 backend/internal/sharing/model.go create mode 100644 backend/internal/sharing/store.go create mode 100644 backend/internal/storage/localfs.go create mode 100644 backend/internal/storage/localfs_test.go create mode 100644 backend/internal/storage/storage.go create mode 100644 backend/internal/tags/correspondents.go create mode 100644 backend/internal/tags/handler.go create mode 100644 backend/internal/tags/store.go create mode 100644 backend/internal/thumbnails/thumbnails.go create mode 100644 backend/internal/webdav/fs.go create mode 100644 backend/internal/webdav/fs_test.go create mode 100644 backend/internal/webdav/handler.go create mode 100644 backend/internal/webdav/scope.go create mode 100644 backend/internal/webdav/scope_test.go create mode 100644 backend/pkg/config/config.go create mode 100644 backend/pkg/db/db.go create mode 100644 backend/pkg/db/migrations/000001_init.down.sql create mode 100644 backend/pkg/db/migrations/000001_init.up.sql create mode 100644 backend/pkg/db/migrations/000002_phase2.down.sql create mode 100644 backend/pkg/db/migrations/000002_phase2.up.sql create mode 100644 backend/pkg/db/migrations/000003_favorites.down.sql create mode 100644 backend/pkg/db/migrations/000003_favorites.up.sql create mode 100644 backend/pkg/db/migrations/000004_phase3_admin.down.sql create mode 100644 backend/pkg/db/migrations/000004_phase3_admin.up.sql create mode 100644 backend/pkg/db/migrations/000005_oidc.down.sql create mode 100644 backend/pkg/db/migrations/000005_oidc.up.sql create mode 100644 backend/pkg/db/migrations/000006_file_versioning.down.sql create mode 100644 backend/pkg/db/migrations/000006_file_versioning.up.sql create mode 100644 backend/pkg/db/migrations/000007_user_colors.down.sql create mode 100644 backend/pkg/db/migrations/000007_user_colors.up.sql create mode 100644 backend/pkg/db/migrations/000008_use_custom_theme.down.sql create mode 100644 backend/pkg/db/migrations/000008_use_custom_theme.up.sql create mode 100644 backend/pkg/db/migrations/000009_app_passwords.down.sql create mode 100644 backend/pkg/db/migrations/000009_app_passwords.up.sql create mode 100644 backend/pkg/db/migrations/000010_audit_log.down.sql create mode 100644 backend/pkg/db/migrations/000010_audit_log.up.sql create mode 100644 backend/pkg/db/migrations/000011_acl_entries.down.sql create mode 100644 backend/pkg/db/migrations/000011_acl_entries.up.sql create mode 100644 backend/pkg/db/migrations/000012_group_folders.down.sql create mode 100644 backend/pkg/db/migrations/000012_group_folders.up.sql create mode 100644 backend/pkg/db/migrations/000013_search_indexes.down.sql create mode 100644 backend/pkg/db/migrations/000013_search_indexes.up.sql create mode 100644 backend/pkg/db/migrations/000014_document_text.down.sql create mode 100644 backend/pkg/db/migrations/000014_document_text.up.sql create mode 100644 backend/pkg/db/migrations/000015_tags.down.sql create mode 100644 backend/pkg/db/migrations/000015_tags.up.sql create mode 100644 backend/pkg/db/migrations/000016_text_pages.down.sql create mode 100644 backend/pkg/db/migrations/000016_text_pages.up.sql create mode 100644 backend/pkg/db/migrations/000017_correspondents.down.sql create mode 100644 backend/pkg/db/migrations/000017_correspondents.up.sql create mode 100644 backend/pkg/db/migrations/000018_tag_icons.down.sql create mode 100644 backend/pkg/db/migrations/000018_tag_icons.up.sql create mode 100644 backend/pkg/middleware/failurelimit.go create mode 100644 backend/pkg/middleware/failurelimit_test.go create mode 100644 backend/pkg/middleware/logger.go create mode 100644 backend/pkg/middleware/proxy.go create mode 100644 backend/pkg/middleware/security.go create mode 100644 backend/pkg/response/response.go create mode 100644 cloud-projekt-plan.md create mode 100644 deploy/.env.example create mode 100644 deploy/Dockerfile create mode 100644 deploy/docker-compose.yml create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/icon-192.png create mode 100644 frontend/public/icon-512.png create mode 100644 frontend/public/icon-maskable-512.png create mode 100644 frontend/public/icon.svg create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/components/AppPasswords.tsx create mode 100644 frontend/src/components/AppSwitcher.tsx create mode 100644 frontend/src/components/Breadcrumb.tsx create mode 100644 frontend/src/components/ConfirmDialog.tsx create mode 100644 frontend/src/components/ContextMenu.tsx create mode 100644 frontend/src/components/Dialog.tsx create mode 100644 frontend/src/components/FileCover.tsx create mode 100644 frontend/src/components/FileInfoDialog.tsx create mode 100644 frontend/src/components/FilePreview.tsx create mode 100644 frontend/src/components/FileViews.tsx create mode 100644 frontend/src/components/MoveTo.tsx create mode 100644 frontend/src/components/OfficeEditor.tsx create mode 100644 frontend/src/components/PageHeader.tsx create mode 100644 frontend/src/components/PdfViewer.tsx create mode 100644 frontend/src/components/SearchBox.tsx create mode 100644 frontend/src/components/ShareDialog.tsx create mode 100644 frontend/src/components/ShareWithDialog.tsx create mode 100644 frontend/src/components/Shell.tsx create mode 100644 frontend/src/components/Sidebar.tsx create mode 100644 frontend/src/components/Tabs.tsx create mode 100644 frontend/src/components/TagChip.tsx create mode 100644 frontend/src/components/TagDialog.tsx create mode 100644 frontend/src/components/TagEditDialog.tsx create mode 100644 frontend/src/components/UploadQueue.tsx create mode 100644 frontend/src/components/UserMenu.tsx create mode 100644 frontend/src/components/VersionsDialog.tsx create mode 100644 frontend/src/components/filetypes.ts create mode 100644 frontend/src/components/icons.tsx create mode 100644 frontend/src/components/pdf.css create mode 100644 frontend/src/hooks/useAppSettings.tsx create mode 100644 frontend/src/hooks/useAuth.ts create mode 100644 frontend/src/hooks/useNavHistory.ts create mode 100644 frontend/src/hooks/useOverlayDismiss.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/AdminPage.tsx create mode 100644 frontend/src/pages/FileBrowser.tsx create mode 100644 frontend/src/pages/LoginPage.tsx create mode 100644 frontend/src/pages/ProfilePage.tsx create mode 100644 frontend/src/pages/SharePage.tsx create mode 100644 frontend/src/pages/SharedPage.tsx create mode 100644 frontend/src/pages/SharedWithMePage.tsx create mode 100644 frontend/src/pages/TagsPage.tsx create mode 100644 frontend/src/pages/TrashPage.tsx create mode 100644 frontend/src/pages/admin/AuditTab.tsx create mode 100644 frontend/src/pages/admin/GroupsTab.tsx create mode 100644 frontend/src/pages/admin/ThemingTab.tsx create mode 100644 frontend/src/pages/admin/UsersTab.tsx create mode 100644 frontend/src/pages/admin/ui.tsx create mode 100644 frontend/src/test-setup.ts create mode 100644 frontend/src/theme.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5c9e027 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# Node — muss im Image via `npm ci` frisch gebaut werden, +# sonst überschreibt der Host-Stand die Builder-Stage (tsc: Permission denied) +frontend/node_modules/ +frontend/dist/ + +# Go +backend/aether +backend/*.test +backend/vendor/ + +# Secrets — Werte kommen zur Laufzeit über env_file, nie ins Image +deploy/.env +.env + +# Daten +data/ +backend/data/ + +# Git / IDE / OS +.git/ +.gitignore +.idea/ +.vscode/ +*.swp +.DS_Store +Thumbs.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ccf0641 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Secrets +deploy/.env +.env + +# Go +backend/aether +backend/*.test +backend/vendor/ + +# Node +frontend/node_modules/ +frontend/dist/ + +# Data (never commit real data) +data/ +backend/data/ + +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp + +# Von vite.config.ts aus node_modules kopiert (PDF.js-Standardschriften) +frontend/public/pdfjs/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..94d5b5a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,165 @@ +# Aether – Projektkonventionen für Claude Code + +## Projekt-Überblick + +Self-hosted Cloud-Alternative zu Nextcloud. Go-Backend + React-Frontend, modularer Monolith. +Projektplan: `cloud-projekt-plan.md` + +## Monorepo-Struktur + +``` +aether/ +├── backend/ # Go – modularer Monolith +│ ├── cmd/aether/ # main.go – Einstiegspunkt +│ ├── internal/ # Domänen-Pakete (nie von außen importieren) +│ │ ├── auth/ # Login, Sessions, CSRF, Argon2id, OIDC-User-Anlage +│ │ ├── storage/ # Storage-Interface + LocalFS (atomar, SHA-256) +│ │ ├── files/ # Datei-/Ordner-CRUD, Trash, Versionen, Favoriten +│ │ ├── sharing/ # Öffentliche Share-Links +│ │ ├── admin/ # Admin-Panel: User, Gruppen(-mitgliedschaft), Settings/Theming +│ │ ├── oidc/ # OIDC-Login (optional, via .env) +│ │ ├── webdav/ # WebDAV-Endpoint /dav/ (Basic Auth – nur App-Passwörter) +│ │ ├── acl/ # Interne Freigaben an User/Gruppen (Rechteauflösung) +│ │ ├── audit/ # Audit-Log (Schreibpfad; Endpunkte liegen in admin/) +│ │ ├── gc/ # Blob-GC: Platte gegen DB abgleichen +│ │ ├── thumbnails/ # Vorschaubild-Generierung +│ │ ├── groups/ # LEERER STUB – Gruppenordner liegen in files/groupfolder.go +│ │ ├── documents/ # Volltext: OCR-Warteschlange + Arbeiter (Phase 6, Teil 1) +│ │ ├── tags/ # Ordnungsmerkmale einer Datei: Etiketten, Belegdatum, Absender +│ │ └── office/ # WOPI-Host: Collabora/OnlyOffice, ein Adapter fuer beide +│ ├── db/migrations/ # SQL-Migrations – Spiegelkopie (siehe Migrations-Regeln!) +│ └── pkg/ # Hilfspakete: config, middleware, response, db (embed migrations) +├── frontend/ # React + TS + Vite + Tailwind +│ └── src/ +│ ├── components/ +│ ├── pages/ +│ ├── api/ # Typed API-Client +│ └── hooks/ +├── deploy/ # Docker Compose, Dockerfile, .env.example +└── CLAUDE.md +``` + +## Backend-Konventionen (Go) + +- **Module-Pfad**: `aether` +- **Go-Version**: 1.25 (siehe `go.mod`) +- **Router**: `github.com/go-chi/chi/v5` +- **DB-Treiber**: `github.com/jackc/pgx/v5` (pgxpool für Connection-Pool) +- **Migrations**: `github.com/golang-migrate/migrate/v4` +- **Logging**: `github.com/rs/zerolog` – strukturiert, immer mit Kontext (`log.Ctx(ctx)`) +- **Hashing**: `golang.org/x/crypto/argon2` (Argon2id) – App-Passwörter dagegen SHA-256 (zufälliges Token, jeder WebDAV-Request authentifiziert einzeln) +- **Sessions**: `github.com/gorilla/sessions` mit securecookie + +### Paket-Regeln + +- Alle Domänen-Pakete unter `internal/` – kein Import von außen +- Keine zirkulären Abhängigkeiten; Dependency-Richtung: `cmd → internal/* → pkg/*` +- Handler in `internal//handler.go`, Business-Logic in `internal//service.go`, DB in `internal//store.go` +- Interfaces für alle externen Abhängigkeiten (Storage, DB) → leicht mockbar + +### Code-Stil + +- `gofmt` + `goimports` – immer formatiert +- Fehler nie ignorieren (`_`), immer wrappen mit `fmt.Errorf("context: %w", err)` +- Keine Magic-Numbers – Konstanten mit sprechenden Namen +- HTTP-Handler signieren: `func (h *Handler) MethodName(w http.ResponseWriter, r *http.Request)` +- Middleware-Chain: Auth → CSRF → RateLimit → Handler + +### Tests + +- Testdatei neben der Quelldatei: `auth_test.go` neben `auth.go` +- Integrationstests mit echtem Postgres via `testcontainers-go` +- Pflicht-Tests: Path-Traversal, Upload-Abbruch, Checksum-Verifikation, Soft-Delete-Recovery +- `go test ./...` muss grün sein vor jedem Commit + +## Frontend-Konventionen (React + TS) + +- **Bundler**: Vite +- **Styling**: Tailwind CSS + CSS-Variablen für Design-Tokens +- **Testing**: Vitest + Testing Library; E2E: Playwright +- **API-Client**: typed fetch-Wrapper unter `src/api/` + +### Design-Tokens (CSS-Variablen) + +Alle Farben laufen über CSS-Custom-Properties: + +```css +:root { + --color-primary: ...; + --color-accent: ...; + --color-background: ...; + --color-surface: ...; + --color-text: ...; +} +``` + +Admin setzt globale Tokens; User-Override nur für eigene Palette (Scope: `[data-user-theme]`). + +### Komponenten-Regeln + +- Komponenten in `PascalCase.tsx` +- Kein `dangerouslySetInnerHTML` mit User-Input (XSS) +- API-Calls nur über `src/api/`-Funktionen, nie direkt `fetch` in Komponenten + +## Sicherheits-Pflichten + +- Path-Traversal: jeden Dateipfad kanonisieren und gegen Storage-Root prüfen +- CSRF: Double-Submit-Cookie oder synchronizer-Token +- Rate-Limiting: Login-Endpunkt + Admin-Login strenger +- Auth-Check: jede Route serverseitig prüfen – nie Frontend vertrauen +- SQL: ausschließlich parametrisierte Queries (pgx unterstützt das nativ) +- Uploads: außerhalb Webroot, keine ausführbaren Rechte, MIME-Prüfung +- WebDAV: **nur App-Passwörter**, nie das Kontopasswort – es ist zugleich der Admin-Zugang +- Kein Zugriff auf fremde IDs → `404`, nicht `403`: sonst verrät die Antwort die Existenz +- Listen und Suche schneiden den erreichbaren Bereich **in der Abfrage** zu, nie nachträglich im Handler +- Hochgeladene Dateien nur für eine Positivliste harmloser Typen `inline` ausliefern (sonst läuft fremdes HTML im eigenen Ursprung) + +## Datenintegrität (nicht verhandelbar) + +1. **Atomares Schreiben**: Temp-Datei → `fsync` → `rename`. Niemals in-place. +2. **SHA-256**: beim Upload berechnen + in DB; beim Download verifizieren. +3. **Soft-Delete only**: kein hartes Löschen. Bytes erst per GC entfernen. +4. **DB-Commit nach Blob-Write**: Metadaten erst committen, wenn Blob erfolgreich. + +## Migrations-Regeln + +- Dateien: `000001_init.up.sql` / `000001_init.down.sql` +- **Jede Migration in BEIDE Verzeichnisse legen:** `backend/pkg/db/migrations/` (wird ins Binary embedded und beim Start ausgeführt) UND `backend/db/migrations/` (Spiegelkopie). Beide müssen identisch sein. +- Keine destruktiven Änderungen ohne explizites Down-Migration +- Migration beim App-Start automatisch ausführen (via `migrate.Up()`) +- Aktueller Stand: bis `000018_tag_icons` (Emoji an Etiketten und Absendern) + +## Commit-Regeln + +Format: `(): ` + +Typen: `feat`, `fix`, `test`, `refactor`, `docs`, `chore`, `security` + +Beispiele: +- `feat(auth): add argon2id password hashing` +- `test(storage): add checksum verification test` +- `security(admin): enforce server-side admin check on all routes` + +Kein Commit ohne grüne Tests. + +## Reverse Proxy & Proxy-Awareness + +- **Kein Proxy-Container** in docker-compose.yml – TLS und Domain gehören zum Wirt, + nicht ins Projekt. Nichts hier soll einen mitgelieferten Proxy voraussetzen. +- App lauscht auf reinem HTTP (`APP_LISTEN_ADDR`). TLS = externer Proxy. +- `X-Forwarded-For` / `X-Forwarded-Proto` werden **nur** von `TRUSTED_PROXIES` (IPs/CIDRs) akzeptiert. +- Echte Client-IP immer über `middleware.RealIP(r)` abrufen – niemals `r.RemoteAddr` direkt. +- `Secure`-Cookie-Flag: über `middleware.IsHTTPS(r)` setzen (berücksichtigt Forwarded-Proto). +- `PUBLIC_URL` für OIDC-Redirects, WOPI-Callbacks und Share-Links verwenden. +- HSTS gehört auf den Edge-Proxy, **nicht** in die App-Header. + +## Docker & Secrets + +- **Backend immer via Docker starten/laufen lassen (`docker compose ...` in `deploy/`), nie `go run`.** Der Host hat kein Go-Toolchain installiert; Builds/Compile-Checks laufen über das `golang:1.25-alpine`-Image. Migrations + DB-Verbindung sind auf die Compose-Umgebung ausgelegt. +- Secrets **nur** über `.env` / Compose-Env – nie im Image oder Repo +- Multi-Stage-Build: Builder-Stage (Go-Compiler) → Alpine final +- `.env` ist in `.gitignore`; `.env.example` zeigt alle Keys ohne Werte + +## Phasen-Stopp + +Nach Phase 1: **STOPP** – Zusammenfassung + Phase-2-Vorschlag, erst nach OK weitermachen. diff --git a/README.md b/README.md new file mode 100644 index 0000000..913a067 --- /dev/null +++ b/README.md @@ -0,0 +1,417 @@ +# Aether + +Self-hosted cloud storage — schnelle, schlanke Alternative zu Nextcloud. + +**Stack:** Go + chi · React + TS + Vite · PostgreSQL · Docker Compose + +--- + +## Reverse Proxy (Pflicht) + +Aether lauscht intern auf reinem HTTP. TLS wird von deinem eigenen Reverse Proxy terminiert. +**nginx, Traefik, Cloudflare Tunnel** – alles funktioniert. Dieses Projekt +liefert bewusst **keinen** Proxy mit: TLS, Zertifikate und Domain gehören in +deine Hand, nicht in ein Compose-File, das du ohnehin überschreiben müsstest. + +Der Proxy muss folgende Headers setzen: + +``` +X-Forwarded-For: +X-Forwarded-Proto: https +``` + +Die IP deines Proxys muss in `TRUSTED_PROXIES` stehen – sonst ignoriert Aether diese Header +(Sicherheitsmaßnahme gegen IP-Spoofing). + +Der Proxy muss folgendes durchreichen (keine eigene Verarbeitung): +- Alle HTTP-Methoden inkl. `PROPFIND`, `MKCOL`, `MOVE`, `COPY` (für WebDAV) +- Große Uploads (Timeout und Body-Size-Limit großzügig setzen) +- WebSocket-Verbindungen (für Office-Co-Editing, Phase 6) +- HSTS-Header setzen (z. B. `Strict-Transport-Security: max-age=31536000; includeSubDomains`) + +## Schnellstart (lokal) + +### Voraussetzungen + +- Docker + Docker Compose +- Go 1.25+ und Node 20+ **nur**, wenn du aus dem Quelltext bauen willst – + zum Betreiben genügt das fertige Abbild (siehe Schritt 3) + +### 1. Repository klonen + +```bash +git clone aether +cd aether +``` + +### 2. `.env` anlegen + +```bash +cp deploy/.env.example deploy/.env +``` + +Dann in `deploy/.env` ausfüllen: + +```env +SESSION_SECRET=$(openssl rand -hex 32) +DB_PASSWORD= +PUBLIC_URL=https://cloud.example.com +TRUSTED_PROXIES= +ADMIN_USERNAME=admin +ADMIN_PASSWORD= +``` + +### 3. Starten + +```bash +cd deploy +docker login gitea.honeyy.duckdns.org # nur beim ersten Mal +docker compose up -d +``` + +Das Compose zieht ein **fertiges Abbild** aus der eigenen Registry +(`gitea.honeyy.duckdns.org/honey/aether`). Für eine neue Umgebung reichen damit +drei Dateien – `docker-compose.yml`, `.env` und das Datenverzeichnis. Backend +und Frontend müssen dort nicht liegen und nicht gebaut werden. + +Wer aus dem Quelltext bauen will, kommentiert in `docker-compose.yml` `image:` +aus und den `build:`-Block ein, dann: + +```bash +docker compose build app && docker compose up -d +``` + +Aktualisieren: + +```bash +docker compose pull app && docker compose up -d app +``` + +Beim ersten Start: +- Postgres-Migrations laufen automatisch +- Der erste Admin-Account wird aus `ADMIN_USERNAME` / `ADMIN_PASSWORD` angelegt +- Nach dem Start: `ADMIN_PASSWORD` aus `.env` entfernen oder leer lassen + +### 4. Öffnen + +Die App ist über deinen Reverse Proxy erreichbar (z. B. `https://cloud.example.com`). +Direkt (ohne Proxy): http://localhost:8080/health + +--- + +## Abbild bauen und veröffentlichen + +```bash +# im Wurzelverzeichnis des Projekts, nicht in deploy/ +IMG=gitea.honeyy.duckdns.org/honey/aether +docker build -f deploy/Dockerfile -t $IMG:$(date +%Y.%m.%d) -t $IMG:latest . +docker push $IMG:$(date +%Y.%m.%d) +docker push $IMG:latest +``` + +**Zwei Tags, mit Absicht.** `latest` ist der bewegliche Name zum Aufbauen, der +datierte der feste zum Zurückrollen. Wer nur `latest` fährt, hat im Fehlerfall +nichts, worauf er zurück kann – der Name zeigt dann schon auf die Fassung, die +gerade Ärger macht. + +Das Abbild wiegt rund **540 MB**. Den Löwenanteil machen `ocrmypdf`, +`tesseract` samt Sprachmodellen und `poppler-utils` aus – ohne sie wären es +etwa 210 MB, aber auch keine Volltextsuche in Scans. Wer das nicht braucht, +streicht die zweite `apk`-Zeile im Dockerfile und setzt `OCR_ENABLED=false`; +der Server prüft beim Start selbst, ob die Programme da sind, und läuft sonst +ohne Volltextsuche weiter. + +Gebaut wird für die Architektur des Wirts. Für ein anderes Ziel (etwa ein +ARM-Gerät) braucht es `docker buildx build --platform linux/arm64`. + +--- + +## Lokale Entwicklung + +### Backend (immer via Docker) + +Das Backend wird **ausschließlich über Docker Compose** gestartet – nie mit `go run`. +Postgres-Migrations und DB-Verbindung sind auf die Compose-Umgebung ausgelegt. + +```bash +cd deploy +docker compose up -d --build # Backend + Postgres starten +docker compose logs -f app # Logs verfolgen +docker compose down # stoppen +``` + +Code-Änderungen am Backend: `docker compose up -d --build` neu ausführen (Image neu bauen). + +**Compile-Check ohne lokales Go-Toolchain** (der Host hat kein Go installiert): + +```bash +docker run --rm -v "$PWD/../backend":/src -w /src golang:1.25-alpine go build ./... +``` + +### Frontend + +```bash +cd frontend +npm install +npm run dev # http://localhost:5173 (Vite dev server, spricht Backend auf :8080 an) +npm run build # Production-Build (wird vom Backend-Image mitgebaut) +``` + +### Tests + +```bash +# Backend (im Go-Image, da Host kein Go hat) +docker run --rm -v "$PWD/backend":/src -w /src golang:1.25-alpine go test ./... + +# Frontend +cd frontend && npm test +``` + +--- + +## Struktur + +``` +aether/ +├── backend/ +│ ├── cmd/aether/ # Einstiegspunkt +│ ├── internal/ # Domänen-Pakete (auth, storage, files, …) +│ ├── db/migrations/ # SQL-Migrations +│ └── pkg/ # Shared utils (config, middleware, response) +├── frontend/ +│ └── src/ +│ ├── api/ # Typed API-Client +│ ├── components/ +│ ├── hooks/ +│ └── pages/ +├── deploy/ +│ ├── docker-compose.yml # app + postgres (kein Proxy-Container) +│ ├── Dockerfile +│ └── .env.example +└── CLAUDE.md # Projektkonventionen +``` + +--- + +## Wo die Dateien liegen + +Nicht in einem Docker-Volume, sondern in einem gewöhnlichen Verzeichnis auf dem +Wirt – `DATA_PATH` in der `.env`. Der Aufbau darin ist derselbe wie in der +Oberfläche: + +``` +/ +├── users//home/… ← die eigenen Dateien, echte Namen +│ /.trash/… ← Papierkorb +│ /.versions/… ← frühere Fassungen +├── groups//home/… ← Gruppenordner +├── thumbs/ ← Vorschaubilder +├── branding/ ← Logo und App-Symbol +└── tmp/ ← Zwischendateien, wird selbst geräumt +``` + +**Warum das so ist:** Ein Archiv ist nur dann eines, wenn man ohne die +Anwendung an seine Unterlagen kommt. Hier reicht dafür `scp -r` oder ein Blick +in den Dateimanager – keine Datenbank, kein Export, kein laufender Container +nötig. Bei einem benannten Volume läge alles unter +`/var/lib/docker/volumes/…` und gehörte `root`. + +Damit die Dateien **dir** gehören und nicht einem Container-Nutzer, läuft der +Container unter deiner Kennung (`APP_UID`/`APP_GID`, siehe `.env`). Damit +funktioniert SFTP direkt: + +```bash +sftp dein-nutzer@dein-server +get -r /pfad/zu/aether-data/users/deinname/home +``` + +> **Nur lesen.** Wer von Hand eine Datei hineinlegt, hat sie auf der Platte, +> aber nicht in der Datenbank: Die Cloud sieht sie nicht, und die +> Speicherbereinigung räumt sie irgendwann weg. Zum Hineinlegen ist WebDAV der +> Weg (siehe unten). + +### Von einem benannten Volume umsteigen + +Ältere Installationen hatten ein Volume `deploy_data`. Umzug mit Prüfung: + +```bash +cd deploy +docker compose stop app +mkdir -p /pfad/zu/aether-data +docker run --rm -v deploy_data:/src:ro -v /pfad/zu/aether-data:/dst alpine:3.20 \ + sh -c 'cp -a /src/. /dst/ && chown -R $(id -u):$(id -g) /dst' + +# Prüfen, bevor umgeschaltet wird: beide Summen müssen gleich sein +docker run --rm -v deploy_data:/src:ro alpine:3.20 \ + sh -c 'cd /src && find . -type f -exec sha256sum {} \; | LC_ALL=C sort | sha256sum' +(cd /pfad/zu/aether-data && find . -type f -exec sha256sum {} \; | LC_ALL=C sort | sha256sum) + +# erst dann DATA_PATH in die .env eintragen und starten +docker compose up -d app +``` + +Das alte Volume **nicht sofort löschen**. Es ist der Rückfall, solange nicht +alles geprüft ist; `docker volume rm deploy_data` kann warten, bis du sicher +bist. + +--- + +## WebDAV + +Adresse: `https:///dav/` + +Angemeldet wird sich mit dem **Benutzernamen** und einem **App-Passwort** +(Profil → App passwords). Das Kontopasswort wird an `/dav/` **abgelehnt** – es +ist zugleich der Admin-Zugang und hat auf Geräten nichts verloren. Ein +App-Passwort gilt nur für WebDAV, nie fürs Web-Login, und lässt sich einzeln +widerrufen, ohne die anderen Geräte auszusperren. + +Der Aufbau entspricht dem der Weboberfläche: eigene Ordner und Gruppenordner +liegen nebeneinander in der Wurzel, Freigaben von Personen unter +`/dav/Shared///`. Trägt ein Gruppenordner denselben Namen wie +ein eigener, behält der eigene ihn und der Gruppenordner bekommt den +Gruppennamen angehängt (`Projekt (Buero)`). + +Nach zehn Fehlanmeldungen in 15 Minuten sperrt die IP für den Rest des Fensters +(`429`). Normale Anfragen zählen nicht mit – Sync-Clients senden legitim +hunderte pro Minute. + +--- + +## Office (Collabora / OnlyOffice) + +Aether ist **WOPI-Host**. Das ist die Schnittstelle, über die Office-Server +sich eine Datei holen und wieder zurückschreiben – **Collabora und OnlyOffice +sprechen beide dieselbe.** Deshalb wird hier *ein* Adapter gebaut und der +Anbieter per `.env` umgeschaltet, statt zweimal dasselbe zu integrieren. + +### Was dazukommt + +| | | +|---|---| +| `OFFICE_PROVIDER` | `collabora`, `onlyoffice` oder leer (aus) | +| `OFFICE_URL` | wo das **Backend** den Office-Server erreicht | +| `OFFICE_PUBLIC_URL` | wo der **Browser** ihn erreicht (leer = wie oben) | +| `OFFICE_WOPI_URL` | wo der **Office-Server** Aether erreicht (leer = `PUBLIC_URL`) | +| `OFFICE_ALLOWED_HOSTS` | Ziele, die das Backend anfragen darf – gegen SSRF | + +**Drei Adressen für drei Richtungen.** Das wirkt übertrieben, bis man es einmal +aufgebaut hat: Im Compose-Netz heißt der Office-Server `collabora`, im Browser +muss dort ein auflösbarer Name stehen, und der Office-Server wiederum erreicht +Aether unter `app:8080` – „localhost" ist aus seinem Container heraus er selbst. + +Der Container startet nur mit dem Profil, sonst kostet er Arbeitsspeicher, ohne +gebraucht zu werden: + +```bash +docker compose --profile office up -d +``` + +### Welche Dateien, und wie gut + +Bearbeitet wird nur, was auf der **Positivliste** steht: +`odt docx doc rtf txt · ods xlsx xls csv · odp pptx ppt · odg`. +PDF steht bewusst **nicht** darauf – es hat seinen eigenen Betrachter, und ein +Office-Server würde beim Speichern ein anderes Dokument daraus machen. + +Collabora Online *ist* LibreOffice, mit derselben Datei-Unterstützung. Für die +Microsoft-Formate heißt das: + +| | | +|---|---| +| `docx`, `xlsx`, `pptx` | öffnen und speichern zuverlässig, Format bleibt erhalten | +| Alte `doc`, `xls`, `ppt` | gehen, sind aber Rückwärtskompatibilität – kein Ziel der Entwicklung | +| Komplexes Layout | Seitenumbrüche und Schriftabstände können um Kleinigkeiten abweichen | +| Sehr große Tabellen, Pivot, seltene Formeln | die häufigste Stelle für echte Abweichungen | +| Makros (VBA) | laufen nicht – und das ist Absicht, nicht Mangel | + +Fehlende **Schriften** sind der häufigste Grund für „sieht anders aus": Der +Container bringt Calibri und Cambria nicht mit. Wer Dokumente von außen +bearbeitet, legt die Schriften nach. + +Für ein Belegarchiv ist das unkritisch – da wird gelesen und selten geändert. +Für den täglichen Umgang mit fremden Word-Dateien gilt: **Rundlauf prüfen**, +nicht darauf vertrauen. + +### Wie das zusammenspielt + +Der Office-Server ist **kein Client deines Browsers allein** – er holt die +Datei selbst beim Backend ab: + +``` +Browser ──────────► Office-Server (Bedienoberfläche im