rebuild the frontend without restarting, and restart when you must
Settings → System → Maintenance. Two buttons that are deliberately not a pair. ── Rebuild: real, and free ── The same `rebuildFrontend` a plugin install already runs. `Bun.build` reads source from disk on every call and writes `build/`, and the server serves that directory by reading each file per request — so a rebuild lands on the next page load with no process restart, no dropped websocket and no chat session lost. ── Restart: the only way to load server code, and it costs ── Bun caches modules for the life of the process. Measured rather than assumed: editing a file and re-importing returns the ORIGINAL export, and a cache-busting query picks up that one specifier while everything it imports stays cached. So `rebuildHonoApp` — which install already does — re-wires the same module instances into a new tree and cannot see a backend edit. Busting the whole graph would duplicate singletons: two database clients, two session maps, one process. There is therefore no third button, and the screen says why. A "remount the API" control would look like it reloaded the backend and would not. The restart answers BEFORE it schedules itself, because the process serving that request is the one being replaced. It refuses outright when PM2 does not know this process — in development the platform runs under `bun --watch`, where the restart would fail into a promise nobody awaits while the button reported success. Verified: `POST /rebuild-frontend` → 200, 3.0s, 3 outputs; anonymous → 401. The restart button is deliberately untested from here — pressing it would end the session this was written in, which is the same reason it needs a label that says so. tsgo clean, frontend builds, 914 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { rebuildFrontend } from '../../plugins/generate';
|
||||
import { isKnownToPm2, restartProcess } from '../../app-store/pm2';
|
||||
import { PLATFORM_DIR } from '../../data-path';
|
||||
import { readdirSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { readServerSettings, writeServerSettings } from 'officerdb';
|
||||
@@ -18,6 +21,57 @@ serverSettingsRouter.route('/ocr', ocrRouter);
|
||||
|
||||
export { readServerSettings as readSettings };
|
||||
|
||||
/**
|
||||
* Rebuild the SPA from source, without restarting anything.
|
||||
*
|
||||
* This is the same `rebuildFrontend` a plugin install runs, exposed as a button. It works — and it is
|
||||
* worth being precise about WHY, because its neighbour below does not:
|
||||
*
|
||||
* `Bun.build` reads the source from disk on every call and writes `build/`, and the server serves that
|
||||
* directory by reading each file per request. So a rebuild is visible on the next page load with no
|
||||
* process restart, no dropped websocket, and no chat session lost.
|
||||
*
|
||||
* Frontend changes only. Nothing here can pick up an edit to server code — see `/restart`.
|
||||
*/
|
||||
serverSettingsRouter.post('/rebuild-frontend', async (ctx) => {
|
||||
const result = await rebuildFrontend();
|
||||
if (!result.ok) return ctx.json({ ok: false, error: result.error }, 500);
|
||||
return ctx.json({ ok: true, ms: result.ms, outputs: result.outputs });
|
||||
});
|
||||
|
||||
/**
|
||||
* Restart the platform process. The only way to load changed SERVER code.
|
||||
*
|
||||
* ── Why there is no gentler option ──
|
||||
*
|
||||
* Bun caches modules for the life of the process. Measured rather than assumed: editing a file and
|
||||
* re-importing it returns the ORIGINAL export, and a cache-busting query picks up only that one
|
||||
* specifier while everything it imports stays cached. Rebuilding the Hono app — which install already
|
||||
* does — therefore re-wires the same module instances into a new router tree and cannot see an edit.
|
||||
*
|
||||
* Busting the whole graph would not be a fix either: singletons would be duplicated, giving two database
|
||||
* clients and two in-memory session maps in one process.
|
||||
*
|
||||
* So this is honest about what it costs. Websockets drop, in-memory chat sessions are lost, and the
|
||||
* browser must reconnect. It saves a terminal, not a reconnection.
|
||||
*
|
||||
* The response is sent BEFORE the restart is scheduled, or the caller would never learn it worked: the
|
||||
* process serving this request is the one about to be replaced.
|
||||
*/
|
||||
serverSettingsRouter.post('/restart', async (ctx) => {
|
||||
// Refuse when PM2 does not know this process, rather than reporting a restart that cannot happen. In
|
||||
// development the platform runs under `bun --watch`, where `pm2 restart officer` would exit non-zero
|
||||
// into a promise nobody awaits — and the button would say it worked.
|
||||
if (!(await isKnownToPm2('officer', PLATFORM_DIR))) {
|
||||
return ctx.json({ ok: false, error: 'not running under PM2 — restart it the way you started it' }, 400);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
void restartProcess('officer', PLATFORM_DIR);
|
||||
}, 250);
|
||||
return ctx.json({ ok: true, restartingIn: 250 });
|
||||
});
|
||||
|
||||
serverSettingsRouter.get('/settings', async (ctx) => {
|
||||
return ctx.json(await readServerSettings());
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user