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
/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>
110 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|