gitea, extracted into plugins/gitea — the platform half

Eleventh extraction, and the only one that deleted its UI instead of moving it.
Seventeen files under apps/Gitea/ reimplemented a fraction of an interface
Gitea already has; the plugin renders the real one in an iframe over a
same-origin proxy.

Deleting rather than moving removed a platform change nobody now has to build:
useGiteaLocation read :owner/:name/:tab/:item from five route patterns where a
plugin gets two, so an as-is extraction would have required teaching the plugin
system to declare extra route patterns. Nothing inside an iframe needs a route.

Nothing working was lost. The router had been commented out of hono.ts since
2026-08-13 with api: [], and officer-gitea was running nowhere — the screen
rendered and every request 404'd, like soulseek, jellyfin and invoices before
it.

Gitea also stops being a CORE DOCK ITEM. It was hardcoded in CORE_DOCK_ITEMS,
the only installable feature that was, on the reasoning that it fronts a remote
instance and installs nothing locally. As a plugin its tile arrives from
assets/icon.png like every other — which is also why the icon added to
public/icons/ this morning was reverted rather than kept.

'gitea' stays in the ServiceName union: the connection lives in core
service_connections, and gitea is the reason that table has its "member rows
carry only a token and inherit the owner's URL" shape at all.

registry.test.ts's grantable-permission test named gitea as "the case this was
built for"; a plugin's permissions are registered at install and are never in
that compile-time list, so it now names a core one. catalogue.test.ts
re-anchored to vnc/notify.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 07:59:10 +00:00
co-authored by Claude Opus 5
parent d8cccad84c
commit d49776307d
35 changed files with 29 additions and 3527 deletions
-5
View File
@@ -102,11 +102,6 @@ export function App() {
element={<Dashboard.PluginScreen {...plugin} />}
/>,
])}
<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="/system-monitor" element={<Dashboard.SystemMonitorScreen />} />
<Route path="/system-monitor/:scope" element={<Dashboard.SystemMonitorScreen />} />
{/* TEMPORARY / EXPERIMENTAL — offline file transfer over animated QR codes */}
@@ -1,38 +0,0 @@
import { Navigate, useParams } from 'react-router';
import type { LayoutNode } from 'officerdev';
import { WorkspaceView, DEFAULT_GITEA_SECTION, giteaSectionPath, isGiteaSection } from 'officerdev';
import { useDashboardState } from 'state/useDashboardState';
import { defaultLayout } from './defaultLayout';
// /gitea uses the Workspace/Panel system (like /command-hq): a section nav on the left and the section view
// on the right. Both talk to the officer-gitea sidecar through the /api/gitea auth proxy, which holds no
// Gitea credentials of its own — the instance URL and the access token live in the sidecar.
//
// The open section is :section in the URL, so the panels read it with useParams instead of passing state
// between themselves over a channel. This screen backs both /gitea and /gitea/:section and is the single
// place that decides what an absent or bogus section means.
export const GiteaScreen = () => {
const { section, owner, name } = useParams();
const workspace = useDashboardState<LayoutNode>('screens/gitea', defaultLayout);
// Bare /gitea, or a section that doesn't exist, resolves to a canonical URL rather than rendering a default
// while the address bar says something else — the nav highlight is derived from the URL.
//
// A repository route (/gitea/repo/:owner/:name/…) has no :section at all and is exempt: it is a second,
// deeper shape on the same screen, not a malformed section.
const inRepo = !!owner && !!name;
if (!inRepo && !isGiteaSection(section)) {
return <Navigate to={giteaSectionPath(DEFAULT_GITEA_SECTION)} replace />;
}
return (
<div className="h-full w-full pt-2">
<WorkspaceView
workspace={workspace}
locked
appTypes={{ allowed: ['gitea-nav', 'gitea-view'], fallback: 'gitea-view' }}
/>
</div>
);
};
@@ -1,11 +0,0 @@
import type { LayoutNode } from 'officerdev';
export const defaultLayout: LayoutNode = {
type: 'group',
id: 'gitea-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'gitea-nav', appType: 'gitea-nav' }, size: 22 },
{ node: { type: 'panel', id: 'gitea-view', appType: 'gitea-view' }, size: 78 },
],
};
@@ -1 +0,0 @@
export * from './GiteaScreen';
@@ -173,7 +173,6 @@ export const CORE_DOCK_ITEMS: DockItem[] = [
{ label: 'Files', to: '/files', icon: FolderOpen, color: '#fbbf24' },
{ label: 'Chat', to: '/chat', icon: MessageCircle, color: '#60a5fa' },
{ label: 'Command HQ', to: '/command-hq', icon: Radar, color: '#f43f5e' },
{ label: 'Gitea', to: '/gitea', icon: GitBranch, color: '#609926' },
{ label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
{ label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' },
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
@@ -14,7 +14,6 @@ export * from './Files';
export * from './Calendar';
export * from './Contacts';
export * from './CommandHQ';
export * from './Gitea';
export * from './SystemMonitor';
export * from './Activity';
export * from './CodeEditor';
@@ -24,7 +24,6 @@ const RULES: TitleRule[] = [
{ match: (p) => p.startsWith('/marketplace'), title: 'Marketplace' },
{ match: (p) => p.startsWith('/plugins'), title: 'Marketplace' },
{ match: (p) => p.startsWith('/command-hq'), title: 'Command HQ' },
{ match: (p) => p.startsWith('/gitea'), title: 'Gitea' },
{ match: (p) => p.startsWith('/system-monitor'), title: 'System Monitor' },
{ match: (p) => p.startsWith('/qr-transfer'), title: 'QR Transfer' },
{ match: (p) => p.startsWith('/activity'), title: 'Activity' },