Files
platform/src/apps/officer-web/App.tsx
T
pastilhasandClaude Opus 5 4a9f23c759 the design doc moves into the plugin it produced
docs/offscale-plugin.md becomes plugins/offscale/PLUGIN.md. most of it is about
the plugin system generally rather than about offscale, which is exactly why it
belongs with the worked example — the reasoning is most useful beside the code
it produced, and the platform's docs should not carry the history of something
it no longer knows exists.

every reference updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:07:48 +00:00

129 lines
8.5 KiB
TypeScript

import { BrowserRouter, Routes, Route, Navigate } from 'react-router';
import * as Authentication from './Screens/Authentication';
import * as Dashboard from './Screens/Dashboard';
import { useAuth } from 'hooks/useAuth';
import { useServerSettings } from 'state/useServerSettings';
import { useServerEnvironment } from 'state/useServerEnvironment';
import { useInitialData } from '@/state/useInitialData';
// `installedPlugins`, not `plugins`: App.tsx already destructures a `plugins` from useServerSettings(),
// which is the DEAD plugin system — /server-settings/plugins scans src/workspaces/plugins/, a directory
// that does not exist, so it is always []. Different thing entirely; see plugins/offscale/PLUGIN.md.
import { plugins as installedPlugins } from './Plugins.gen';
export function App() {
const { isLoading, isAuthenticated } = useAuth();
const { plugins, isLoading: isServerSettingsLoading } = useServerSettings();
useServerEnvironment();
useInitialData();
if (isLoading || isServerSettingsLoading) return null;
return (
<BrowserRouter>
{!isAuthenticated && (
<Authentication.AuthenticationLayout>
<Routes>
<Route path="/" element={<Authentication.LandingPage />} />
<Route path="/auth/forgot-password" element={<Authentication.ForgotPassword />} />
<Route path="/auth/reset-password" element={<Authentication.ResetPassword />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Authentication.AuthenticationLayout>
)}
{isAuthenticated && (
<Dashboard.DashboardLayout>
<Routes>
<Route path="/" element={<Dashboard.HomeScreen />} />
{/* Route pairs, like /headscale and /photos below: the bare page route exists so the
existing links into it keep working, and its `SettingsRoute` guard redirects to the
first section. The section is the URL rather than a `*_SELECTED` global, so a settings
sub-page is linkable, bookmarkable and cmd-clickable. */}
<Route path="/settings/profile" element={<Dashboard.ProfileSettings />} />
<Route path="/settings/profile/:section" element={<Dashboard.ProfileSettings />} />
<Route path="/settings/ai" element={<Dashboard.AISettings />} />
<Route path="/settings/ai/:section" element={<Dashboard.AISettings />} />
<Route path="/settings/system" element={<Dashboard.SystemSettings />} />
<Route path="/settings/system/:section" element={<Dashboard.SystemSettings />} />
<Route path="/settings/integrations" element={<Dashboard.IntegrationsSettings />} />
<Route path="/settings/integrations/:section" element={<Dashboard.IntegrationsSettings />} />
<Route path="/settings/user-management" element={<Dashboard.UserManagementSettings />} />
<Route path="/settings/user-management/:section" element={<Dashboard.UserManagementSettings />} />
{/* A project group is a working directory, so it is spelled as a path, not ?cwd= — see
apps/ChatHistory/chat-routes.ts. The splat has to be last in a pattern, which is why
"new" comes before the group rather than after it. A session carries no group: the
transcript records its own cwd and the server resolves it by id. */}
<Route path="/chat" element={<Dashboard.SessionListPage />} />
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
<Route path="/chat/new/g/*" element={<Dashboard.SessionListPage isNew />} />
<Route path="/chat/g/*" element={<Dashboard.SessionListPage />} />
<Route path="/chat/:sessionId" element={<Dashboard.SessionListPage />} />
<Route path="/files" element={<Dashboard.FilesScreen />} />
<Route path="/calendar" element={<Dashboard.CalendarScreen />} />
<Route path="/contacts" element={<Dashboard.ContactsScreen />} />
<Route path="/music" element={<Dashboard.MusicScreen />} />
<Route path="/soulseek" element={<Dashboard.SoulseekScreen />} />
<Route path="/soulseek/:section" element={<Dashboard.SoulseekScreen />} />
<Route path="/photos" element={<Dashboard.PhotosScreen />} />
<Route path="/photos/:section" element={<Dashboard.PhotosScreen />} />
<Route path="/app-store" element={<Dashboard.AppStoreScreen />} />
<Route path="/plugins" element={<Dashboard.PluginsScreen />} />
{/* Installed plugins. Core routes above stay hand-written; everything below is generated from
what is installed, because a bundler cannot follow a runtime import specifier. The wildcard
hands the whole subtree to the plugin's own router, which react-router nests natively. */}
{installedPlugins.flatMap((plugin) => [
<Route key={plugin.appName} path={plugin.route} element={<Dashboard.PluginScreen {...plugin} />} />,
// The section pair, exactly as the core screens do it (`/headscale/:section`): the plugin's
// panels read `useParams` themselves, so which section is open is the URL rather than state
// passed between them.
<Route
key={`${plugin.appName}-section`}
path={`${plugin.route}/:section`}
element={<Dashboard.PluginScreen {...plugin} />}
/>,
])}
<Route path="/jellyfin" element={<Dashboard.JellyfinScreen />} />
<Route path="/jellyfin/:section" element={<Dashboard.JellyfinScreen />} />
<Route path="/transmission" element={<Dashboard.TransmissionScreen />} />
<Route path="/transmission/:section" element={<Dashboard.TransmissionScreen />} />
<Route path="/gitea" element={<Dashboard.GiteaScreen />} />
<Route path="/gitea/:section" element={<Dashboard.GiteaScreen />} />
<Route path="/gitea/repo/:owner/:name" element={<Dashboard.GiteaScreen />} />
<Route path="/gitea/repo/:owner/:name/:tab" element={<Dashboard.GiteaScreen />} />
<Route path="/gitea/repo/:owner/:name/:tab/:item" element={<Dashboard.GiteaScreen />} />
<Route path="/invoices" element={<Dashboard.InvoicesScreen />} />
<Route path="/invoices/:section" element={<Dashboard.InvoicesScreen />} />
<Route path="/wallet" element={<Dashboard.WalletScreen />} />
<Route path="/wallet/:section" element={<Dashboard.WalletScreen />} />
<Route path="/system-monitor" element={<Dashboard.SystemMonitorScreen />} />
<Route path="/system-monitor/:scope" element={<Dashboard.SystemMonitorScreen />} />
{/* TEMPORARY / EXPERIMENTAL — offline file transfer over animated QR codes */}
<Route path="/qr-transfer" element={<Dashboard.QrTransferScreen />} />
<Route path="/activity" element={<Dashboard.ActivityScreen />} />
<Route path="/activity/:id" element={<Dashboard.ActivityScreen />} />
<Route path="/code-editor" element={<Dashboard.CodeEditor />} />
<Route path="/skills" element={<Dashboard.Skills />} />
<Route path="/skills/:dirName" element={<Dashboard.Skills />} />
<Route path="/tasks" element={<Dashboard.Tasks />} />
<Route path="/tasks/:dirName" element={<Dashboard.Tasks />} />
<Route path="/processes" element={<Dashboard.Processes />} />
<Route path="/processes/:dirName" element={<Dashboard.Processes />} />
<Route path="/jobs" element={<Dashboard.JobsPage />} />
<Route path="/jobs/:id" element={<Dashboard.JobsPage />} />
<Route path="/dashboards" element={<Dashboard.DashboardsScreen />} />
<Route path="/dashboards/:id" element={<Dashboard.DashboardScreen />} />
<Route path="/email" element={<Dashboard.EmailScreen />} />
<Route path="/email/:emailId" element={<Dashboard.EmailScreen />} />
<Route path="/browser" element={<Dashboard.BrowserScreen />} />
<Route path="/browser/:tabId" element={<Dashboard.BrowserScreen />} />
<Route path="/terminal" element={<Dashboard.TerminalScreen />} />
<Route path="/desktop" element={<Dashboard.DesktopScreen />} />
<Route path="/auth/signout" element={<Authentication.SignoutScreen />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Dashboard.DashboardLayout>
)}
</BrowserRouter>
);
}