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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,7 +13,7 @@
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='16' fill='%2352b54b'/%3E%3Ctext x='16' y='23' font-family='system-ui,sans-serif' font-size='19' font-weight='800' text-anchor='middle' fill='%2306240a'%3EM%3C/text%3E%3C/svg%3E"
/>
<script type="module" crossorigin src="/admin/assets/index-CSt3yVnU.js"></script>
<script type="module" crossorigin src="/admin/assets/index-CEU6X4jy.js"></script>
<link rel="modulepreload" crossorigin href="/admin/assets/router-D9WH5XEU.js">
<link rel="stylesheet" crossorigin href="/admin/assets/index-CASotpHk.css">
</head>
+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>
),