This commit is contained in:
ponzischeme89
2026-08-17 13:13:10 +12:00
parent 12b27c77c3
commit 43ab18854c
39 changed files with 1707 additions and 1528 deletions
-11
View File
@@ -114,17 +114,6 @@ export interface HeroPolicy {
timeZone?: string;
}
export interface ReleaseBuilderStatus {
state: 'idle' | 'running' | 'succeeded' | 'failed';
tag?: string;
mandatory: boolean;
startedAt?: string;
finishedAt?: string;
message?: string;
logs: string[] | null;
fallback: string;
}
export type HeroPlacement = 'home' | 'movies' | 'tv_shows';
export interface HeroPlacementPolicy {
+2 -129
View File
@@ -1,10 +1,9 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { ReleaseBuilderStatus } from '../api/types';
import { useAction } from '../lib/hooks';
import { useGateway } from '../lib/gateway';
import { useToast } from '../lib/toast';
import { Banner, Button, Card, Confirm, Field, Loading, Note, PageHead, Tag, Toggle } from '../components/ui';
import { Banner, Button, Card, Confirm, Field, Loading, PageHead, Tag, Toggle } from '../components/ui';
interface Draft {
version: string;
@@ -21,13 +20,6 @@ export function UpdatesPage() {
const { busy, run } = useAction();
const [draft, setDraft] = useState<Draft | null>(null);
const [confirming, setConfirming] = useState(false);
const [releaseConfirming, setReleaseConfirming] = useState(false);
const [releaseTag, setReleaseTag] = useState('');
const [releaseNotes, setReleaseNotes] = useState('');
const [releaseMandatory, setReleaseMandatory] = useState(false);
const [builder, setBuilder] = useState<ReleaseBuilderStatus | null>(null);
const [builderError, setBuilderError] = useState('');
const completedAt = useRef('');
const policy = status?.updatePolicy;
@@ -47,32 +39,6 @@ export function UpdatesPage() {
});
}, [policy, draft]);
useEffect(() => {
let active = true;
const poll = async () => {
try {
const next = await api.get<ReleaseBuilderStatus>('/admin/api/release-builder');
if (!active) return;
setBuilder(next);
setBuilderError('');
if (next.state === 'succeeded' && next.finishedAt && completedAt.current !== next.finishedAt) {
completedAt.current = next.finishedAt;
setDraft(null);
await reload();
}
} catch (cause) {
if (!active) return;
setBuilderError(cause instanceof Error ? cause.message : 'The release builder is unavailable.');
}
};
void poll();
const timer = window.setInterval(() => void poll(), 3000);
return () => {
active = false;
window.clearInterval(timer);
};
}, [reload]);
const save = (enabled: boolean) =>
run(enabled ? 'save' : 'off', async () => {
if (!draft) return;
@@ -100,92 +66,10 @@ export function UpdatesPage() {
const patch = (next: Partial<Draft>) => setDraft((current) => (current ? { ...current, ...next } : current));
const startRelease = () =>
run('release', async () => {
const next = await wrap(
() =>
api.post<ReleaseBuilderStatus>('/admin/api/release-builder', {
tag: releaseTag.trim(),
notes: releaseNotes.trim(),
mandatory: releaseMandatory,
}),
'Memby release started.',
);
if (next) {
setBuilder(next);
setBuilderError('');
setReleaseConfirming(false);
}
});
const builderTone =
builder?.state === 'succeeded'
? 'ok'
: builder?.state === 'failed'
? 'bad'
: builder?.state === 'running'
? 'warn'
: undefined;
return (
<>
<PageHead title="App updates" intro="Publish an optional or a required client update." />
<Banner message={error} />
<Banner message={builderError} />
<Card
title="Build and publish"
intro="Build the latest tagged Android app in Docker, sign it with Memby's existing certificate, verify it, and publish it to televisions. The builder runs separately from the gateway."
icon="download"
tone="data"
actions={<Tag tone={builderTone}>{builder?.state ?? 'checking'}</Tag>}
footer={
<Button
variant="primary"
busy={busy === 'release' || builder?.state === 'running'}
disabled={Boolean(builderError)}
onClick={() => (releaseMandatory ? setReleaseConfirming(true) : void startRelease())}
>
{releaseTag.trim() ? `Build ${releaseTag.trim()}` : 'Build latest release'}
</Button>
}
>
<div className="fields">
<Field
label="GitHub tag override"
hint="Leave blank for the latest semantic tag. An exact tag bypasses discovery or repeats that tag; the gateway still refuses downgrades."
>
<input
type="text"
value={releaseTag}
disabled={builder?.state === 'running'}
placeholder="v0.2.64 (blank uses latest)"
onChange={(event) => setReleaseTag(event.target.value)}
/>
</Field>
<Field label="Release notes" hint="Leave blank to use this version's CHANGELOG entry.">
<input
type="text"
value={releaseNotes}
disabled={builder?.state === 'running'}
placeholder="What's new on the television"
onChange={(event) => setReleaseNotes(event.target.value)}
/>
</Field>
</div>
<Toggle
label="Make this update required"
hint="Older televisions cannot dismiss the update prompt. You will confirm before the build starts."
checked={releaseMandatory}
disabled={builder?.state === 'running'}
onChange={setReleaseMandatory}
/>
{builder?.message ? <Note tone={builderTone}>{builder.message}</Note> : null}
{builder?.logs?.length ? <pre className="code">{builder.logs.join('\n')}</pre> : null}
<Note>
Command-line fallback: <code>{builder?.fallback ?? 'docker compose run --rm --build memby-builder release'}</code>
</Note>
</Card>
{loading || !draft ? (
<Loading rows={1} />
@@ -303,17 +187,6 @@ export function UpdatesPage() {
onCancel={() => setConfirming(false)}
/>
) : null}
{releaseConfirming ? (
<Confirm
title="Build a required update?"
body="When this signed APK is published, every older television will be blocked until it installs the update."
confirmLabel="Build and publish"
busy={busy === 'release'}
onConfirm={() => void startRelease()}
onCancel={() => setReleaseConfirming(false)}
/>
) : null}
</>
);
}