From 03abdeb246b4b39dd60c4e82f7c3643323e3455b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 21 Aug 2026 04:22:30 +0000 Subject: [PATCH] an Update button, so a server is not a terminal away from current MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintenance had "rebuild the frontend" and "restart the server" — one compiles what is on disk, the other reloads it, and neither CHANGES what is on disk. Doing that meant ssh, on every server, every time. Refuses a dirty tree and a non-fast-forward, the same two refusals as pullPlugin and for the same reason: this checkout is somebody's working copy on at least one machine, and a button that discards uncommitted work is worse than no button. Fetches `origin HEAD` rather than a branch name, because a checkout made before the default branch was renamed is on one the remote no longer has — the trap plugins/update.ts already hit, and it bites hardest on the installs most in need of updating. It deliberately does not chain into anything. A pull touching bun.lock means an install, which the frozen-lockfile rule exists to keep deliberate; one touching a schema means db:push against a live database. So it REPORTS what the update implies — install, db:push, restart or rebuild — from the file list, and leaves the decision with whoever pressed it. The commits pulled are shown, because "it worked" is less useful than knowing what arrived. Co-Authored-By: Claude Opus 5 --- .../ServerSettings/MaintenanceSection.tsx | 78 +++++++++++++++++- .../api/server-settings/server-settings.ts | 80 +++++++++++++++++++ 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/MaintenanceSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/MaintenanceSection.tsx index 0d6f88e2..581a839f 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/MaintenanceSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/MaintenanceSection.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Hammer, Loader2, RotateCw } from 'lucide-react'; +import { Download, Hammer, Loader2, RotateCw } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { useClient } from 'hooks/useClient'; @@ -31,10 +31,45 @@ export function errorMessage(err: unknown, fallback: string): string { export const MaintenanceSection = () => { const client = useClient(); + const [pulling, setPulling] = useState(false); + const [pulled, setPulled] = useState<{ from: string; to: string; commits: string[]; needs: string[] } | null>(null); const [rebuilding, setRebuilding] = useState(false); const [restarting, setRestarting] = useState(false); const [lastBuild, setLastBuild] = useState(null); + /** + * Pull, and then say what the pull implies rather than acting on it. + * + * Deliberately does not chain into a rebuild or a restart. A pull that touched `bun.lock` means an + * install, which the frozen-lockfile rule exists to keep deliberate; one that touched a schema means + * `db:push` against a live database. Neither is a thing a button should decide, so `needs` is reported + * and the buttons below stay where they are. + */ + const pull = async () => { + setPulling(true); + try { + const res = await client.post<{ + ok: boolean; + updated: boolean; + from?: string; + to?: string; + commits: string[]; + needs: string[]; + }>('/server-settings/pull', {}); + if (!res.updated) { + setPulled(null); + toast.success('Already up to date'); + return; + } + setPulled({ from: res.from ?? '', to: res.to ?? '', commits: res.commits, needs: res.needs }); + toast.success(`Pulled ${res.commits.length} commit${res.commits.length === 1 ? '' : 's'}`); + } catch (err) { + toast.error(errorMessage(err, 'Pull failed')); + } finally { + setPulling(false); + } + }; + const rebuild = async () => { setRebuilding(true); try { @@ -90,6 +125,43 @@ export const MaintenanceSection = () => { return (
+
+
Update the platform
+

+ Pulls this server’s repository. Refuses if anything is uncommitted or the checkout has diverged — it + will never discard local work. +

+

+ Changes nothing that is running. Whatever the update needs is listed here, and stays your call. +

+ {pulled && ( +
+

+ {pulled.from} → {pulled.to} +

+
    + {pulled.commits.slice(0, 5).map((line) => ( +
  • + {line} +
  • + ))} + {pulled.commits.length > 5 && ( +
  • +{pulled.commits.length - 5} more
  • + )} +
+ {pulled.needs.length > 0 && ( +

Needs: {pulled.needs.join(', ')}

+ )} +
+ )} +
+ +
+ +
Rebuild the frontend

@@ -98,7 +170,7 @@ export const MaintenanceSection = () => {

{lastBuild &&

Last build: {lastBuild}

}
- @@ -118,7 +190,7 @@ export const MaintenanceSection = () => {