0.2.73
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user