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:
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { Hammer, Loader2, RotateCw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
|
||||
/**
|
||||
* The two ways to load a change without leaving the browser.
|
||||
*
|
||||
* They look like a pair and they are not, which is the whole content of this screen. One is free; the
|
||||
* other costs you every open connection. Presenting them identically would invite the reflex of pressing
|
||||
* the second when the first would have done.
|
||||
*/
|
||||
export const MaintenanceSection = () => {
|
||||
const client = useClient();
|
||||
const [rebuilding, setRebuilding] = useState(false);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
const [lastBuild, setLastBuild] = useState<string | null>(null);
|
||||
|
||||
const rebuild = async () => {
|
||||
setRebuilding(true);
|
||||
try {
|
||||
const res = await client.post<{ ok: boolean; ms: number; outputs: number; error?: string }>(
|
||||
'/server-settings/rebuild-frontend',
|
||||
{},
|
||||
);
|
||||
if (!res.ok) throw new Error(res.error ?? 'Build failed');
|
||||
setLastBuild(`${(res.ms / 1000).toFixed(1)}s · ${res.outputs} files`);
|
||||
// Nothing reloads itself: the bundle on disk changed, this page is still the old one. Saying so is
|
||||
// the difference between "it worked" and "why does my change not show".
|
||||
toast.success('Frontend rebuilt — refresh this page to see it');
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Rebuild failed');
|
||||
} finally {
|
||||
setRebuilding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
setRestarting(true);
|
||||
try {
|
||||
await client.post('/server-settings/restart', {});
|
||||
toast.success('Restarting — the page will reconnect on its own');
|
||||
} catch {
|
||||
// The restart may well have happened: the process answering this request is the one being replaced,
|
||||
// so a dropped connection here is the expected shape of success.
|
||||
toast.success('Restarting — the page will reconnect on its own');
|
||||
}
|
||||
// Deliberately left true. The button must not become pressable again while the server is coming back.
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">Rebuild the frontend</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Picks up changes to the interface. Nothing restarts, no connection drops, and this chat keeps its session —
|
||||
refresh the page when it finishes.
|
||||
</p>
|
||||
{lastBuild && <p className="mt-1 text-xs text-muted-foreground/70">Last build: {lastBuild}</p>}
|
||||
</div>
|
||||
<Button disabled={rebuilding || restarting} onClick={() => void rebuild()} className="shrink-0">
|
||||
{rebuilding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Hammer className="mr-2 h-4 w-4" />}
|
||||
{rebuilding ? 'Building…' : 'Rebuild'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-6 border-t border-border pt-6">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">Restart the server</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
The only way to load changed server code — Bun caches modules for the life of the process, so rebuilding
|
||||
cannot see an edit to the backend.
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Costs every open connection: websockets drop and in-memory chat sessions are lost. Background jobs survive —
|
||||
they are their own PM2 processes.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={rebuilding || restarting}
|
||||
onClick={() => void restart()}
|
||||
className="shrink-0"
|
||||
>
|
||||
{restarting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RotateCw className="mr-2 h-4 w-4" />}
|
||||
{restarting ? 'Restarting…' : 'Restart'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Bot, Settings, Mail, Volume2, Mic, ScanText } from 'lucide-react';
|
||||
import { Bot, Hammer, Mail, Mic, ScanText, Settings, Volume2 } from 'lucide-react';
|
||||
import type { LayoutNode, PanelComponents } from 'officerdev';
|
||||
import { WorkspaceLayout } from 'officerdev';
|
||||
|
||||
import { SettingsRoute, createSettingsPanelComponents, type SettingsSectionGroup } from './SettingsPanel';
|
||||
import { SMTPSection } from './ServerSettings/SMTPSection';
|
||||
import { MaintenanceSection } from './ServerSettings/MaintenanceSection';
|
||||
import { TTSSection } from './ServerSettings/TTSSection';
|
||||
import { STTSection } from './ServerSettings/STTSection';
|
||||
import { OCRSection } from './ServerSettings/OCRSection';
|
||||
@@ -31,6 +32,19 @@ const groups: SettingsSectionGroup[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Maintenance',
|
||||
icon: Hammer,
|
||||
sections: [
|
||||
{
|
||||
key: 'maintenance',
|
||||
icon: Hammer,
|
||||
title: 'Rebuild & restart',
|
||||
description: 'Load changes without a terminal',
|
||||
content: <MaintenanceSection />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
icon: Mail,
|
||||
|
||||
Reference in New Issue
Block a user