/sendoff — pick files, pick one of your own machines or a person who has allowed you, send. The consent list sits on the same screen rather than in settings, because the two questions arrive together: the first time somebody opens this and finds nobody to send to, the reason is that consent runs the other way, and the answer has to be visible where the question was asked. There is deliberately no received view. Files land in Documents/SendOff/ inside the recipient's own tree, so the file browser already IS the inbox — a second view of the same directory would be a worse copy of one that works, kept in sync by hand. Two details worth keeping. The file input resets its value after every pick, or choosing the same file twice in a row does nothing, because `change` does not fire on an unchanged value. And a send clears the files but keeps the target, since several batches to the same place is the common second action and re-picking it each time is the sort of small tax that stops somebody using a thing. The multipart goes through `client.post`, which already recognises a FormData and drops the JSON content-type so the browser picks the boundary. That was the only thing making this look like it needed a raw fetch, and a raw fetch would have carried its own copy of the token resolution — which is a chain of seven sources. Co-Authored-By: Claude Opus 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="/minesweeper" element={<Dashboard.MinesweeperScreen />} />
|
|
<Route path="/sendoff" element={<Dashboard.SendOffScreen />} />
|
|
<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>
|
|
);
|
|
}
|