Files
learnharness/components/update-banner.tsx
T
amosblomqvist e8e00a5237 auto-update wiring: silent download, restart banner, local-feed drill seam
electron-updater in the shell (packaged builds only; dev never updates).
Checks at startup + every 24h, downloads silently, and the renderer shows
a Restart banner once the update is staged (it installs on quit either
way). Manual "Check for Updates..." under the app menu answers via dialog.
Every updater event routes through the shell log and is caught - the
updater can never take the app down (verified live: a feed failure logs
three lines and the app runs on).

LH_UPDATE_FEED overrides the baked GitHub feed with a generic local
server - the pre-release drill: serve latest-mac.yml + zip + blockmap
from localhost and a real installed copy updates itself, no release, no
public repo.

The asar allow-list re-includes electron-updater + its 16-package runtime
tree explicitly (gitignore last-match-wins; asar verified at 2.1 MB).
The banner rides the preload bridge (onUpdateStatus/installUpdate) and
renders nothing in the browser. window.lh typing centralized in
sidebar.tsx's declaration.
2026-10-10 14:11:19 +03:00

51 lines
1.7 KiB
TypeScript

"use client";
/**
* The auto-update banner (Wave C, owner 2026-10-10: silent download +
* restart-banner UX). The shell pushes "lh:update-status" once a staged
* update exists (electron/main.cjs update-downloaded); this is the whole
* renderer surface — a small fixed card, bottom-right, offering Restart.
* Browser builds have no window.lh and never see a push: renders null.
*
* No dismiss: a downloaded update installs on quit anyway
* (autoInstallOnAppQuit); the banner is a courtesy, not a gate.
*/
import { useEffect, useState } from "react";
// The window.lh bridge type lives in components/sidebar.tsx (the incumbent
// declare-global) — onUpdateStatus/installUpdate are declared there too.
type UpdateStatus = { status: string; version: string | null };
export default function UpdateBanner() {
const [ready, setReady] = useState<UpdateStatus | null>(null);
useEffect(() => {
// Absent bridge (browser / dev) → stays hidden forever.
return window.lh?.onUpdateStatus?.((s) => {
if (s?.status === "downloaded") setReady(s);
});
}, []);
if (!ready) return null;
return (
<div
data-testid="update-banner"
className="fixed right-4 bottom-4 z-50 flex items-center gap-3 rounded-lg border border-border bg-popover px-4 py-3 shadow-lg"
>
<div className="text-sm">
<span className="font-medium">LearnHarness {ready.version ?? "update"} is ready</span>
<span className="text-muted-foreground"> — restart to finish installing.</span>
</div>
<button
type="button"
onClick={() => window.lh?.installUpdate?.()}
className="rounded-md bg-accent px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent/80"
>
Restart
</button>
</div>
);
}