This commit is contained in:
ponzischeme89
2026-08-17 07:34:23 +12:00
parent 93fb0fd728
commit 36cb1324fd
56 changed files with 1177 additions and 168 deletions
+11
View File
@@ -65,6 +65,16 @@ export interface Maintenance {
updatedAt?: string;
}
export interface QuietTime {
enabled: boolean;
active: boolean;
startTime: string;
endTime: string;
message: string;
timeZone: string;
updatedAt?: string;
}
export interface UpdatePolicy {
enabled: boolean;
latestVersion: string;
@@ -218,6 +228,7 @@ export interface AdminStatus {
serverVersion: string;
currentUser?: string;
maintenance: Maintenance;
quietTime: QuietTime;
updatePolicy: UpdatePolicy;
library: LibraryStats;
syncRunning: boolean;
+5 -4
View File
@@ -139,9 +139,10 @@ export function Layout() {
const [confirmingOffline, setConfirmingOffline] = useState(false);
const location = useLocation();
const offline = Boolean(status?.maintenance?.enabled);
const quiet = Boolean(status?.quietTime?.active);
const account = useRef<HTMLDivElement>(null);
const initial = Array.from(currentUser.trim())[0]?.toLocaleUpperCase('en-NZ') || 'A';
const statusTone = status && online && !offline ? 'ok' : status || !loading ? 'bad' : 'checking';
const statusTone = status && online && !offline && !quiet ? 'ok' : status || !loading ? 'bad' : 'checking';
const toggleAvailability = async () => {
if (changingAvailability || !status) return;
@@ -227,9 +228,9 @@ export function Layout() {
className="topbar-status"
data-tone={statusTone}
aria-pressed={offline}
disabled={!status || !online || changingAvailability}
title={!status ? 'Checking Memby status' : offline ? 'Bring Memby back online' : online ? 'Take Memby offline' : 'Memby is not responding'}
aria-label={offline ? 'Memby is offline. Bring it online' : status && online ? 'Memby is online. Take it offline' : loading ? 'Checking Memby status' : 'Memby is not responding'}
disabled={!status || !online || changingAvailability || quiet}
title={!status ? 'Checking Memby status' : quiet ? 'Memby quiet time is active' : offline ? 'Bring Memby back online' : online ? 'Take Memby offline' : 'Memby is not responding'}
aria-label={quiet ? 'Memby quiet time is active' : offline ? 'Memby is offline. Bring it online' : status && online ? 'Memby is online. Take it offline' : loading ? 'Checking Memby status' : 'Memby is not responding'}
onClick={() => offline ? void toggleAvailability() : setConfirmingOffline(true)}
>
<span className="dot" aria-hidden="true" />
+1 -1
View File
@@ -265,7 +265,7 @@ export const nav: NavGroup[] = [
path: '/admin/maintenance',
label: 'Maintenance',
title: 'Maintenance',
intro: 'Take Memby offline for every television.',
intro: 'Take Memby offline now or schedule daily quiet time.',
icon: 'wrench',
},
{
+62 -2
View File
@@ -3,7 +3,7 @@ import { api } from '../api/client';
import { useAction } from '../lib/hooks';
import { useGateway } from '../lib/gateway';
import { useToast } from '../lib/toast';
import { Banner, Button, Card, Confirm, Field, Loading, PageHead, Tag } from '../components/ui';
import { Banner, Button, Card, Confirm, Field, Loading, PageHead, Tag, Toggle } from '../components/ui';
export function MaintenancePage() {
const { status, error, loading, reload } = useGateway();
@@ -12,6 +12,11 @@ export function MaintenancePage() {
const [message, setMessage] = useState('');
const [confirming, setConfirming] = useState(false);
const [touched, setTouched] = useState(false);
const [quietEnabled, setQuietEnabled] = useState(false);
const [quietStart, setQuietStart] = useState('23:00');
const [quietEnd, setQuietEnd] = useState('07:00');
const [quietMessage, setQuietMessage] = useState('');
const [quietTouched, setQuietTouched] = useState(false);
const enabled = Boolean(status?.maintenance?.enabled);
@@ -23,6 +28,14 @@ export function MaintenancePage() {
if (!touched && status) setMessage(status.maintenance?.message ?? '');
}, [status, touched]);
useEffect(() => {
if (quietTouched || !status?.quietTime) return;
setQuietEnabled(status.quietTime.enabled);
setQuietStart(status.quietTime.startTime);
setQuietEnd(status.quietTime.endTime);
setQuietMessage(status.quietTime.message);
}, [status, quietTouched]);
const set = (next: boolean) =>
run(next ? 'on' : 'off', async () => {
await wrap(
@@ -34,9 +47,24 @@ export function MaintenancePage() {
await reload();
});
const saveQuietTime = () =>
run('quiet', async () => {
await wrap(
() => api.post('/admin/api/quiet-time', {
enabled: quietEnabled,
startTime: quietStart,
endTime: quietEnd,
message: quietMessage,
}),
quietEnabled ? 'Quiet time saved.' : 'Quiet time turned off.',
);
setQuietTouched(false);
await reload();
});
return (
<>
<PageHead title="Maintenance" intro="Take Memby offline for every television." />
<PageHead title="Maintenance" intro="Take Memby offline now or schedule daily quiet time." />
<Banner message={error} />
{loading ? (
@@ -76,6 +104,38 @@ export function MaintenancePage() {
</Card>
)}
{!loading ? (
<Card
title="Quiet time"
intro={`Pause new television requests and server background work every day in ${status?.quietTime?.timeZone ?? 'the household timezone'}. Work already under way finishes safely. The admin console and health checks stay available so the schedule can always be changed.`}
icon="clock"
tone={status?.quietTime?.active ? 'warn' : 'info'}
actions={status?.quietTime?.active ? <Tag tone="warn">active now</Tag> : quietEnabled ? <Tag tone="ok">scheduled</Tag> : <Tag>off</Tag>}
footer={
<Button variant="primary" busy={busy === 'quiet'} onClick={() => void saveQuietTime()}>
Save quiet time
</Button>
}
>
<Toggle
label="Pause server activity during quiet time"
checked={quietEnabled}
onChange={(next) => { setQuietEnabled(next); setQuietTouched(true); }}
/>
<div className="fields">
<Field label="Starts" hint="Uses the household's 24-hour clock.">
<input type="time" value={quietStart} onChange={(event) => { setQuietStart(event.target.value); setQuietTouched(true); }} />
</Field>
<Field label="Ends" hint="May be on the following day, for example 23:00 to 07:00.">
<input type="time" value={quietEnd} onChange={(event) => { setQuietEnd(event.target.value); setQuietTouched(true); }} />
</Field>
</div>
<Field label="Message shown on the television" hint="Shown when a television contacts Memby during quiet time.">
<input type="text" value={quietMessage} placeholder="Quiet time — try again after 7 am" onChange={(event) => { setQuietMessage(event.target.value); setQuietTouched(true); }} />
</Field>
</Card>
) : null}
{confirming ? (
<Confirm
title="Take Memby offline?"
+2
View File
@@ -108,6 +108,8 @@ export function OverviewPage() {
label: 'Availability',
value: status.maintenance?.enabled ? (
<Tag tone="bad">offline for maintenance</Tag>
) : status.quietTime?.active ? (
<Tag tone="warn">quiet time active</Tag>
) : (
<Tag tone="ok">online</Tag>
),