The filesystem is the database: GET reads the tree through the same module the index generator now imports, PATCH splices frontmatter (status, priority, due, order) and never touches a body byte, and every write regenerates TODO.md. Columns open/doing/blocked/done, native drag between them, selection and theme filter in the URL. Registry entry 'todos' (kind admin, 22 entries, docs:check green), isSuperAdmin double-gate on the router. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
134 lines
8.5 KiB
TypeScript
134 lines
8.5 KiB
TypeScript
import { BrowserRouter, Routes, Route, Navigate, useLocation } 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';
|
|
|
|
/**
|
|
* `/plugins` → `/marketplace`, KEEPING the query string.
|
|
*
|
|
* `<Navigate to="/marketplace" />` drops it, and that was not theoretical: every row in the plugin list
|
|
* linked to `/plugins?selected=<app>`, so clicking one redirected to a bare `/marketplace` and the detail
|
|
* pane sat on "Select a plugin." The screen looked broken while both halves were working — the link set a
|
|
* param the redirect then threw away.
|
|
*
|
|
* Rows now point at `/marketplace` directly, so this only serves old links and bookmarks. It preserves
|
|
* the search anyway, because that is what made the redirect wrong in the first place.
|
|
*/
|
|
const PluginsRedirect = () => {
|
|
const { search } = useLocation();
|
|
return <Navigate to={{ pathname: '/marketplace', search }} replace />;
|
|
};
|
|
|
|
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 /command-hq and /soulseek 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 />} />
|
|
{/* Splat, because the folder is the URL now: `/files/Tests/test folder`. The bare `/files`
|
|
still matches with an empty splat and means home, so every existing link and the dock
|
|
entry keep working. Only DIRECTORIES live here — an open file stays `?view=`, since it is
|
|
what is on top of you rather than where you are. */}
|
|
<Route path="/files/*" element={<Dashboard.FilesScreen />} />
|
|
<Route path="/calendar" element={<Dashboard.CalendarScreen />} />
|
|
<Route path="/contacts" element={<Dashboard.ContactsScreen />} />
|
|
<Route path="/command-hq" element={<Dashboard.CommandHqScreen />} />
|
|
<Route path="/command-hq/:sessionId" element={<Dashboard.CommandHqScreen />} />
|
|
<Route path="/app-store" element={<Dashboard.AppStoreScreen />} />
|
|
<Route path="/marketplace" element={<Dashboard.PluginsScreen />} />
|
|
{/* The screen was `/plugins` until marketplace.officer.dev existed. Redirected rather than
|
|
dropped: the word appears in the dock, in docs, and in whatever anyone bookmarked, and a
|
|
404 on a route that worked yesterday reads as the feature having been removed. */}
|
|
<Route path="/plugins" element={<PluginsRedirect />} />
|
|
{/* 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 (`/soulseek/: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="/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="/sendoff" element={<Dashboard.SendOffScreen />} />
|
|
<Route path="/todos" element={<Dashboard.TodosScreen />} />
|
|
<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="/terminal" element={<Dashboard.TerminalScreen />} />
|
|
<Route path="/auth/signout" element={<Authentication.SignoutScreen />} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</Dashboard.DashboardLayout>
|
|
)}
|
|
</BrowserRouter>
|
|
);
|
|
}
|