dorfteich/apps/web/src/App.tsx
Claude Fable 5 dbbe229cb9
Some checks failed
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
Pond knowledge graph view (#112)
/p/:pondSlug/graph (static segment ranked above :pageSlug, same
documented reserved-slug gap as trash/settings) renders the pond's
readable wikilink graph from GET /ponds/:id/links: pages as nodes
colored by their first label (legend included, DEFAULT_LABEL_COLOR for
unlabeled), resolved links as edges, phantom targets as dashed nodes —
clicking one offers to create the page, which resolves its links.

Rendering is a self-contained SVG force graph: only d3-force is
bundled (no d3 DOM/zoom modules, zero external requests); the layout
runs synchronously to rest, zoom/pan/node-drag are plain pointer math.
SVG over canvas deliberately — every node carries a data-testid the
e2e packs can click. Ponds beyond 500 pages get a capped-view notice.

Sidebar footer links every member to the graph (trash stays
owner-only). New i18n namespace graph (de+en).

Verified live: nodes/edges/legend render, node click opens the page,
phantom click creates it and the node turns solid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:59:07 +02:00

110 lines
5.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { RequireAnonymous, RequireAuth, RequireSiteAdmin } from './auth/guards';
import { PondGraphPage } from './graph/PondGraphPage';
import { MAINTENANCE_EVENT } from './lib/api';
import { MaintenancePage } from './pages/MaintenancePage';
import { AppLayout } from './layout/AppLayout';
import { AdminSettingsPage } from './pages/AdminSettingsPage';
import { AdminSystemPage } from './pages/AdminSystemPage';
import { FontCatalogPage } from './pages/FontCatalogPage';
import { HomePage } from './pages/HomePage';
import { LegalPage } from './pages/LegalPage';
import { NotFoundPage } from './pages/NotFoundPage';
import { PageEditorPage } from './pages/PageEditorPage';
import { PluginPreviewPage } from './pages/PluginPreviewPage';
import { PondHomePage } from './pages/PondHomePage';
import { PondSettingsPage } from './pages/PondSettingsPage';
import { PublicPageView } from './pages/PublicPageView';
import { SettingsPage } from './pages/SettingsPage';
import { TrashPage } from './pages/TrashPage';
import { ForgotPasswordPage } from './pages/auth/ForgotPasswordPage';
import { LoginPage } from './pages/auth/LoginPage';
import { ResetPasswordPage } from './pages/auth/ResetPasswordPage';
import { SignupPage } from './pages/auth/SignupPage';
import { VerifyEmailPage } from './pages/auth/VerifyEmailPage';
import { SetupPendingPage } from './setup/SetupPendingPage';
import { SetupWizardPage } from './setup/SetupWizardPage';
import { useSetupStatus } from './setup/use-setup-status';
export function App(): React.JSX.Element {
const setup = useSetupStatus();
// Maintenance gate (issue #103): once any api call answers 503
// maintenance_mode — a backup restore is running — the whole UI becomes
// the maintenance screen, which polls the exempt status endpoint and
// reloads when the instance is back.
const [maintenance, setMaintenance] = useState(false);
useEffect(() => {
const onMaintenance = (): void => setMaintenance(true);
window.addEventListener(MAINTENANCE_EVENT, onMaintenance);
return () => window.removeEventListener(MAINTENANCE_EVENT, onMaintenance);
}, []);
if (maintenance) return <MaintenancePage />;
// First-run gate (issue #81): while setup is pending the api 503s almost
// everything, so the SPA offers only the wizard, the login (to resume a
// started wizard as the Site Admin), and a pending notice — without the
// regular chrome, whose pond/search queries would all fail. If the status
// probe itself fails (offline reload, #38), fall through to the app.
if (setup.isPending) return <></>;
if (setup.data?.status === 'required') {
return (
<div className="setup-shell">
<Routes>
<Route path="setup" element={<SetupWizardPage />} />
<Route path="login" element={<LoginPage />} />
<Route path="*" element={<SetupPendingPage />} />
</Routes>
</div>
);
}
return (
<Routes>
<Route element={<AppLayout />}>
<Route index element={<HomePage />} />
<Route element={<RequireAnonymous />}>
<Route path="login" element={<LoginPage />} />
<Route path="signup" element={<SignupPage />} />
<Route path="forgot-password" element={<ForgotPasswordPage />} />
</Route>
{/* Verify/reset work regardless of session state (mail links). */}
<Route path="verify-email" element={<VerifyEmailPage />} />
<Route path="reset-password" element={<ResetPasswordPage />} />
{/* Public read-only page view — reachable without a session (issue #56). */}
<Route path="public/:pondSlug/:pageSlug" element={<PublicPageView />} />
{/* Legal pages — public, no session required (issue #82). */}
<Route path="legal/:kind" element={<LegalPage />} />
{/* Setup is done exactly once — afterwards the wizard url just goes home. */}
<Route path="setup" element={<Navigate to="/" replace />} />
<Route element={<RequireAuth />}>
<Route path="settings" element={<SettingsPage />} />
<Route path="fonts" element={<FontCatalogPage />} />
<Route path="p/:pondSlug" element={<PondHomePage />} />
{/* Static segment "trash" wins react-router's ranking over the
dynamic :pageSlug sibling below — a page slugged "trash"
would be unreachable via direct URL, an accepted v1 gap. */}
<Route path="p/:pondSlug/trash" element={<TrashPage />} />
{/* Static "settings" wins over :pageSlug, like "trash" above. */}
<Route path="p/:pondSlug/settings" element={<PondSettingsPage />} />
{/* Static "graph" wins over :pageSlug, like "trash" above (#112). */}
<Route path="p/:pondSlug/graph" element={<PondGraphPage />} />
<Route path="p/:pondSlug/:pageSlug" element={<PageEditorPage />} />
</Route>
<Route element={<RequireSiteAdmin />}>
<Route path="admin" element={<AdminSettingsPage />} />
<Route path="admin/system" element={<AdminSystemPage />} />
{/* Sandbox preview of one installed plugin (issue #73). */}
<Route path="admin/plugins/:pluginId/preview" element={<PluginPreviewPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
);
}