From 8ad5a5563e965810d10979b9547b450a1602235e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 16 Aug 2026 09:20:11 +0000 Subject: [PATCH] clicking a plugin works again, and offscale gets its icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions and one long-standing hole, all found by actually opening the page. ── Clicking a row did nothing. That one is mine ── Rows link to `?selected=`, and they still pointed at `/plugins`. My redirect was ``, which DROPS the query string — so a click went to `/plugins?selected=music`, redirected to a bare `/marketplace`, and the detail pane sat on "Select a plugin." Both halves worked; the redirect threw away what the link had just set. Rows now point at `/marketplace` directly. The redirect also preserves `search`, because old bookmarks carry the same param and losing it is exactly what made this wrong the first time. ── Offscale's icon 404'd since it shipped one ── Assets were published to `public/plugins//` at INSTALL and nowhere else. Offscale was installed before it had an `assets/icon.png`, so the directory never existed and the tile drew a 404 permanently — nothing short of uninstalling and reinstalling would have fixed it. "Reinstall the plugin to see its new icon" is the same bad answer as "restart the server to see it", which the route right next to this already refuses to accept. Publishing now happens on every mount. It is a copy of files that still exist in the plugin's own source, so re-running it is free and cannot lose anything — the property that already makes unpublish the one thing uninstall may delete. A failure is logged, never thrown: an icon is not worth refusing to mount over. Published offscale's by hand too, so the fix is visible without waiting for a restart. Verified 200 through the running server for both plugins. ── The third report was not a bug ── "No connection to the marketplace" is the catalogue working correctly: all three plugins are already on disk here, and `router.ts` drops catalogue entries whose plugin is installed, because offering to install something installed is worse than showing nothing. With nothing left to offer, a working store and a broken one look identical on that screen. Nothing in the logs, which is what a reachable store looks like — the client only speaks when it cannot reach one. tsgo clean, frontend builds, 873 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 --- src/apps/officer-web/App.tsx | 20 ++++++++++++++++-- src/servers/plugins/mount.ts | 21 +++++++++++++++++++ .../src/apps/Plugins/PluginsList.tsx | 4 ++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx index 99789e3c..b2bbca2c 100644 --- a/src/apps/officer-web/App.tsx +++ b/src/apps/officer-web/App.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router'; +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'; @@ -10,6 +10,22 @@ import { useInitialData } from '@/state/useInitialData'; // 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. + * + * `` drops it, and that was not theoretical: every row in the plugin list + * linked to `/plugins?selected=`, 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 ; +}; + export function App() { const { isLoading, isAuthenticated } = useAuth(); const { plugins, isLoading: isServerSettingsLoading } = useServerSettings(); @@ -73,7 +89,7 @@ export function App() { {/* 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. */} - } /> + } /> {/* 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. */} diff --git a/src/servers/plugins/mount.ts b/src/servers/plugins/mount.ts index 1fd0d775..1ffff8aa 100644 --- a/src/servers/plugins/mount.ts +++ b/src/servers/plugins/mount.ts @@ -6,6 +6,7 @@ import { discoverPlugins } from './discover'; import { mountPrefix, type DiscoveredPlugin } from './manifest'; import { generatePluginsModule, rebuildFrontend } from './generate'; import { iconUrl } from '../app-store/assets'; +import { publishAssets } from '../app-store/assets'; // Turning what is on disk plus what is in the database into a mounted application. // @@ -146,6 +147,26 @@ export async function refreshPluginMounts(options: { skipFrontend?: boolean } = const mounted = await mountablePlugins(snapshot); rebuildHonoApp(mounted); + // Republish every enabled plugin's `assets/` on the way past. + // + // Publishing used to happen ONLY at install, and `offscale` proved why that is not enough: it was + // installed before it shipped an icon, so `public/plugins/offscale/` never existed and the tile drew a + // 404 forever. Nothing short of uninstalling and reinstalling would have fixed it, and "reinstall the + // plugin to see its new icon" is the same bad answer as "restart the server to see it". + // + // A copy of files that already exist in the plugin's own source, so re-running it is free and cannot + // lose anything — the same property that makes `unpublishAssets` the one thing uninstall may delete. + // Failures are logged, never thrown: an icon is not worth refusing to mount a plugin over. + await Promise.all( + enabled.map(async ({ plugin }) => { + try { + await publishAssets(plugin.appName, plugin.dir); + } catch (err) { + console.error(`[plugins] ${plugin.appName}: assets not published —`, err instanceof Error ? err.message : err); + } + }), + ); + // The frontend half. `Plugins.gen.tsx` is what the bundler reads, so it is written first and the SPA is // rebuilt from it — ~1s for a 25MB bundle, which is why an install can do this rather than ask for a // restart. Skipped at boot, where the build already happened and repeating it would delay `serve()`. diff --git a/src/workspaces/officerdev/src/apps/Plugins/PluginsList.tsx b/src/workspaces/officerdev/src/apps/Plugins/PluginsList.tsx index 42c85bcf..f21b9f90 100644 --- a/src/workspaces/officerdev/src/apps/Plugins/PluginsList.tsx +++ b/src/workspaces/officerdev/src/apps/Plugins/PluginsList.tsx @@ -34,7 +34,7 @@ export const PluginsList = () => { {plugins.map((plugin) => ( { {available.map((plugin) => (