feat: finish the homepage plugin, move service links and canvas out of core (B19)

This commit is contained in:
LukeGus committed 2026-09-24 15:07:25 -05:00
1 parent 21937a6b02
commit d4ec444558
256 files changed
+27365 -9077

No files matched your search

+64 -10
View File
@@ -478,11 +478,17 @@ Slots core owns: `terminal.toolbar`, `terminal.toolbarStatus`,
renamed `terminal.dock` to `terminal.sidePanel` and added the status slot,
where host-metrics puts its CPU, memory and disk bars), `onboarding.steps`, `onboarding.features`,
`onboarding.workflow`, `hosts.importMenu`, `hosts.panel`, `proxmox.hostEditor`,
`shell.overlay`, `dashboard.hostMetrics` and `homepage.hostMetrics`. **B16**
added the last two, component slots in the dashboard's host status card
(`{ hostId, online }`, one per host row) and the homepage's host status widget
(`{ hostId, shownMetrics, online }`): core draws the status and a plugin draws
the numbers, so core no longer calls the metrics API. **B12** added `shell.overlay`, a component slot the
`shell.overlay`, `dashboard.hostMetrics`, `homepage.hostMetrics` and
`dashboard.secondaryView`. **B16** added the middle two, component slots in
the dashboard's host status card (`{ hostId, online }`, one per host row) and
the homepage's host status widget (`{ hostId, shownMetrics, online }`): core
draws the status and a plugin draws the numbers, so core no longer calls the
metrics API. **B19** added `dashboard.secondaryView` (component slot, one
contribution rendered at a time, `titleKey` names its tab): the dashboard's
"Dashboard / <plugin>" toggle renders whatever is contributed instead of
knowing about the homepage plugin by name, so the toggle disappears with no
contributor and would show a second plugin's view the same way if one ever
claimed it too. **B12** added `shell.overlay`, a component slot the
shell renders once at its root for always-mounted plugin UI: session-sharing
keeps its share dialog and its room-invite watcher there.
`session.remoteDisplay` moved to session-sharing, which declares it and draws
@@ -591,8 +597,9 @@ connection helpers (`isElectron`, `resolveConnectionOrigin`, `pluginWsUrl`,
stopgap in the right place: D1 turns them into typed bridge members.
**B16** added `LineChart`, `useAdaptivePolling`, `useAreaPreferences` (the
Appearance density and chart options), and the homepage widget pieces
`WidgetTitle` and `runVisibleInterval`, for host-metrics's cards and its
metrics chart widget. **B15** added `useUiPreferencesContext`, whose `setOverride` the
`WidgetTitle` and `runVisibleInterval` (moved to `src/ui/lib/` in **B19**,
once a plugin rather than a core feature became their biggest caller), for
host-metrics's cards and its metrics chart widget. **B15** added `useUiPreferencesContext`, whose `setOverride` the
Docker manager uses to remember the card or table layout the user picked.
The `docker` and `hostMetrics` areas are still declared in core's
`types/ui-preferences.ts`; a plugin cannot contribute its own area yet. The frontend SDK gained `useHostStatus(hostId)`: core's
@@ -605,6 +612,11 @@ host lookup. **B17** added `NotificationChannelDialog` and the channel API
so a plugin that manages them shows core's dialog. `useConnectionDefaults` now carries terminal defaults only; the
RDP half became remote desktop's user settings. Publishing its
`.d.ts` for plugins outside this repo is a follow-up for the repo split.
**B19** added `PluginViewPlaceholder`, for the homepage plugin's widget
shell to render in place of a widget type whose owning plugin (docker,
tunnels, host-metrics, file-manager) is off, and moved `WidgetTitle` and
`runVisibleInterval` here from the deleted homepage feature directory
(above), unchanged for their existing callers.
#### Strings
@@ -779,7 +791,7 @@ as `allowPrivateHosts`, and runs approved commands through
else from optional services: `snippets.access`, `fleets.access`,
`automations.access`, `workspaces.saved`, `network-topology.graph`,
`terminal.history` and `homepage.items` v1 (`list()`, which the homepage
plugin provides from B19). A tool names the service it needs, and a tool
plugin provides, B19). A tool names the service it needs, and a tool
whose service has no running provider is not offered to the model at all. B18
made the write methods of `snippets.access` and `fleets.access` check the
same permission their routes do (`snippets.create`/`edit`/`delete`,
@@ -791,6 +803,48 @@ and `allowReadOnlyCommands` plus a custom `providers` field, and the host
and keys. The shell knows nothing about it: availability is the plugin being
enabled, `ai.use`, and those settings.
**Homepage (B19)** is the widget canvas and the dashboard's service links.
It imports nothing from core. It adopts `homepage_items`, `homepage_layouts`
and `dashboard_service_links`, keeping the legacy sync wire names
(`homepageItems`, `dashboardServiceLinks`). Its outbound routes (favicon, RSS,
ping, proxy widgets) all go through `ctx.fetch`, so they keep the SSRF
guard every other outbound call gets. It provides `homepage.items` v1
(`list()`), which the AI assistant reads as an optional service. Core's own
uptime, recent-activity and database-health routes are unrelated to the
canvas and stayed on the main server (`database/routes/dashboard-routes.ts`,
mounted at `/dashboard`); only the widget canvas and the service-links table
left with this plugin. The two ports it used to run on (30006 dashboard,
30012 homepage) are gone.
Both dashboard cards (`service_links`, `homepage_preview`) and the
`?view=homepage` full-screen link are the plugin's own, registered through
`registerDashboardCard`/`registerTab`. `registerDashboardCard` gained
`defaultPanel` (`"main"` or `"side"`), so a preset that lays out cards for a
new user reads a plugin's own card's height and panel from the registry
(`service_links` asks for the side column) instead of a core file having to
spell every plugin's card id and its layout by hand. The dashboard's "Dashboard / Homepage"
toggle is generic: it renders whatever a plugin contributes to a new
`dashboard.secondaryView` action slot (component kind, `titleKey` for the tab
label), so core carries no homepage-specific header or copy-link button of
its own. A widget any plugin registers with `app.registerHomepageWidget`
still goes into a registry core owns (`src/ui/plugin-host/homepage-widget-registry.ts`),
the same as `dashboard-cards-registry.ts`, so docker's, tunnels', file-manager's
and host-metrics's own homepage widgets keep working with homepage disabled;
only the canvas that renders them is gone. The frontend SDK gained
`useHostActions()` (every host action any plugin registered, for a widget
that offers a picker over what it can do with a host, such as Quick
Connect), `useActivityTypes()` and `activityTarget(type)` (the recent-activity
type list and its icon/tab/label, for a filter or an activity feed widget),
and `useHomepageWidgetTypes()`/`homepageWidgetType(id)` (the read side of
`registerHomepageWidget`, for the canvas and its add-widget menu).
`@termix/plugin-sdk/ui` gained `PluginViewPlaceholder`, and `WidgetTitle` and
`runVisibleInterval` moved from the homepage feature directory to
`src/ui/lib/` now that a plugin, not a core feature, is their biggest
caller. The shared `host`/`selectHost`/`noHostConfigured`/`loading` strings
several widget-owning plugins relied on through the `homepage` namespace's
core fallback moved to core's `common.*`, which is what that fallback is
actually for.
#### Host status is core
The status dot in the host list, the dashboard and the homepage widget is
@@ -2187,7 +2241,7 @@ Then, with the app running:
The bundled plugins predate the SDK, apart from workspaces (A9), snippets
(B2), remote-desktop (B14), docker (B15), host-metrics (B16), automations
(B17) and ai (B18), which import
(B17), ai (B18) and homepage (B19), which import
nothing from core. The others still reach core by relative
path (`../../../../src/backend/...`), which an esbuild plugin,
`packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle
@@ -2215,7 +2269,7 @@ What the lint fence enforces today, in `eslint.config.mjs`:
| Core importing a plugin backend | **Error** | 0 | - |
| A plugin backend importing frontend code or `@/` | **Error** | 0 | - |
| The shell importing plugin code | **Error** | 0 | - |
| A plugin frontend importing core through `@/` | Warning | 55 files | D1 |
| A plugin frontend importing core through `@/` | Warning | 53 files | D1 |
| A plugin importing core by relative path | Warning | 4 files | D1 |
| A plugin importing another plugin's source | Warning | 0 files | - |
+32 -4
View File
@@ -404,10 +404,6 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
ever is not, the run now fails on its first SSH step instead of being
skipped silently. Owner: none unless a background "is this user unlocked"
check is added to the SDK.
- **B18 (ai), for B19:** the assistant's `list_homepage_items` tool calls
the optional `homepage.items` v1 service, `list(): Promise<Array<{ id,
typeId, title }>>`, as the acting user. Until B19 provides it the tool is
not offered to the model. Owner: B19.
- **B18 (ai):** the "allow read-only diagnostic commands" user setting only
adds a line to the system prompt. No tool runs a command directly;
`tools/command-allowlist.ts` (`isReadOnlyCommand`) is tested but unused, so
@@ -433,6 +429,28 @@ typeId, title }>>`, as the acting user. Until B19 provides it the tool is
- **B18:** tunnels builds its own SSE URL off the axios base instead of the
new `app.fetch`. Owner: D1.
- **Pre-existing, found in B19:** `createMockCtx`'s `wsRoutes` double (B15)
records `{ path, raw, handler, options }` per route, but
`plugins/serial/tests/backend/activate.test.ts`,
`plugins/ssh-terminal/tests/backend/activate.test.ts` and
`plugins/tunnels/tests/backend/activate.test.ts` still assert the old
two-field shape (`{ path, raw }`) and fail on `toEqual`. Not touched by
this step; update each assertion to match or to check only the fields it
cares about. Owner: D0.
- **Pre-existing, found in B19:** `src/backend/hosts/file-manager/` (21
files) is dead code left over from before the file-manager plugin (B6):
nothing in `src/backend/starter.ts` or elsewhere imports
`hosts/file-manager/index.ts`, so its own port (30004, still listed in
`src/backend/utils/swagger.ts`'s server list) never actually starts and
its `axios.post("http://localhost:30006/activity/log", ...)` calls (now a
dead port after this step) never run. Delete the directory once confirmed
nothing else references it. Owner: D0.
- **Pre-existing, found in B19:** `src/ui/main-axios.ts`'s `tmuxMonitorApi`
still points at `getApiUrl("/tmux_monitor", 30010)`, a port tmux-monitor's
own B10 step moved off of (per `plugins/tmux-monitor/CHANGELOG.md`) without
updating this frontend constant to `/plugin-api/tmux-monitor`. Not
something this step touched or verified further. Owner: D0.
## Manual checks after 2.9.0
- SSH terminal: password auth, key auth (and an encrypted key's passphrase
@@ -489,3 +507,13 @@ typeId, title }>>`, as the acting user. Until B19 provides it the tool is
skipped and the others keep running, then enable it again; the AI
assistant lists and creates automations, and says they are unavailable
with automations off.
- Homepage: on a 2.8 database every widget, its layout and every service
link are there; add, move, resize, edit and delete a widget of each type;
the favicon, RSS, ping and custom API widgets against a real remote URL,
and each refuses a private/loopback target; the dashboard's Service
Links and Homepage Preview cards; the Dashboard/Homepage toggle and its
copy-link and open-full-view buttons; `?view=homepage`; docker's, tunnels'
and file-manager's own homepage widgets still work with the homepage
plugin disabled; the AI assistant lists homepage items and says
unavailable with homepage off; disable homepage in admin (the tab, its
dashboard cards and the toggle disappear) then enable it again.
+61
View File
@@ -372,11 +372,15 @@ export interface DashboardCardContribution {
id: string;
titleKey: string;
defaultHeight?: number;
/** Which column a preset places this card in by default. Defaults to "main". */
defaultPanel?: "main" | "side";
component: ComponentType<DashboardCardProps>;
}
export interface HomepageWidgetContribution<C = Record<string, unknown>> {
id: string;
/** Set by core to the registering plugin's id; a plugin never sets this itself. */
pluginId?: string;
name: string;
description: string;
category: "links" | "info" | "system" | "monitoring";
@@ -701,6 +705,19 @@ export interface PluginHostBridge {
) => ComponentType<Record<string, unknown>> | undefined;
hostProtocols: (host: PluginHostRecord) => string[];
useHostStatus: (hostId: number | undefined) => HostStatusInfo | null;
useHostActions: () => HostActionContribution[];
useActivityTypes: () => string[];
activityTarget: (type: string) => ActivityTargetInfo | undefined;
useHomepageWidgetTypes: () => HomepageWidgetContribution[];
homepageWidgetType: (id: string) => HomepageWidgetContribution | undefined;
}
/** What a recent-activity entry opens and how it is labelled. */
export interface ActivityTargetInfo {
icon?: IconComponent;
/** Tab type the entry reopens. */
tab: string;
titleKey?: string;
}
/**
@@ -847,6 +864,50 @@ export function useSlotContributions(
return requireHost().useSlotContributions(slotId, context);
}
/**
* Every host action any running plugin (and core) registered with
* registerHostAction, for a plugin that offers a picker over "what can I do
* with this host" without knowing which plugin provides each one - a quick
* connect widget, say. Reactive: it re-renders as plugins enable and disable.
*/
export function useHostActions(): HostActionContribution[] {
return requireHost().useHostActions();
}
/**
* Recent-activity types every registered tab claims (registerTab's
* activityTypes), core's and plugins'. For a filter that lists what a user
* can pick, without knowing which plugin owns each activity type.
*/
export function useActivityTypes(): string[] {
return requireHost().useActivityTypes();
}
/**
* What a recent-activity entry of this type opens and how it is labelled -
* the tab type claiming it through registerTab's activityTypes, core's or a
* plugin's. undefined for a type nothing claims (its plugin is gone).
*/
export function activityTarget(type: string): ActivityTargetInfo | undefined {
return requireHost().activityTarget(type);
}
/**
* Every homepage widget type any running plugin registered with
* registerHomepageWidget, for the homepage plugin's own canvas and add-widget
* menu. Reactive: it re-renders as plugins enable and disable.
*/
export function useHomepageWidgetTypes(): HomepageWidgetContribution[] {
return requireHost().useHomepageWidgetTypes();
}
/** A single registered homepage widget type, by id, or undefined if none. */
export function homepageWidgetType(
id: string,
): HomepageWidgetContribution | undefined {
return requireHost().homepageWidgetType(id);
}
/**
* The component another plugin registered under `id` with
* app.registerComponent, or undefined while that plugin is off.