Files
platform/src/apps/officer-web/App.tsx
T
pastilhasandClaude Opus 5 ca7d3c657b a send form for SendOff, and no receiving one
/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>
2026-08-23 02:47:47 +00:00

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>
);
}