Files
nadir-webui/src/routes/dashboard/[machineId]/system/date-time/+page.svelte
T
2026-06-25 23:11:32 +02:00

172 lines
5.5 KiB
Svelte

<script lang="ts">
import CheckIcon from '@lucide/svelte/icons/check';
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import { page } from '$app/state';
import PageMeta from '$lib/components/seo/page-meta.svelte';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import * as Command from '$lib/components/ui/command';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Popover from '$lib/components/ui/popover';
import { Switch } from '$lib/components/ui/switch';
import { m } from '$lib/paraglide/messages';
import {
listTimezones,
setNtp,
setTime,
setTimezone,
systemTime
} from '$lib/remotes/system.remote';
import { getWhoami } from '$lib/remotes/system.remote';
import { extractErrorMessage, hasPermission } from '$lib/utils';
import { toast } from 'svelte-sonner';
const machineId = $derived(page.params.machineId!);
const time = $derived(systemTime(machineId));
const tzs = $derived(listTimezones(machineId));
const formId = $props.id();
const whoami = $derived(getWhoami(machineId));
const canWrite = $derived(hasPermission('system', 'write', whoami.current?.permissions));
let tzOpen = $state(false);
let saving = $state(false);
// ponytail: builds an RFC3339 UTC string from a datetime-local value (treated as local).
function rfc3339FromLocal(v: string) {
return new Date(v).toISOString().replace(/\.\d{3}Z$/, 'Z');
}
async function withSaving<T>(fn: () => Promise<T>) {
saving = true;
try {
await fn();
toast.success(m.saved());
} catch (e) {
toast.error(extractErrorMessage(e) ?? m.errors_generic());
} finally {
saving = false;
}
}
</script>
<PageMeta title={m.seo_title_system_datetime()} description={m.seo_desc_system_datetime()} />
<div class="mx-auto flex w-full max-w-4xl flex-col gap-4 p-4">
<h1 class="text-2xl font-semibold tracking-tight">{m.nav_system_datetime()}</h1>
<svelte:boundary>
{#snippet failed(err)}
<Card.Root>
<Card.Content class="text-destructive py-6">{(err as Error).message}</Card.Content>
</Card.Root>
{/snippet}
{@const t = await time}
{@const zones = await tzs}
<Card.Root>
<Card.Header>
<Card.Title>{m.system_time_current()}</Card.Title>
</Card.Header>
<Card.Content class="flex flex-wrap items-center gap-x-8 gap-y-2 text-sm">
<div class="flex items-baseline gap-2">
<span class="text-muted-foreground text-xs font-medium uppercase tracking-wider"
>{m.system_time_timezone()}</span
>
<span class="font-mono font-medium">{t.timezone}</span>
</div>
<div class="flex items-baseline gap-2">
<span class="text-muted-foreground text-xs font-medium uppercase tracking-wider"
>{m.system_time_clock()}</span
>
<span class="font-mono tabular-nums">{t.time}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-muted-foreground text-xs font-medium uppercase tracking-wider">NTP</span
>
<Switch
checked={t.ntp}
disabled={saving || !t.can_ntp || !canWrite}
onCheckedChange={(v) => withSaving(() => setNtp({ enabled: v, machineId }))}
/>
</div>
<div class="text-muted-foreground text-xs">
{t.ntp_synchronized ? m.system_time_ntp_synced() : m.system_time_ntp_not_synced()}
</div>
</Card.Content>
<Card.Content class="border-t pt-4">
<Popover.Root bind:open={tzOpen}>
<Popover.Trigger>
{#snippet child({ props })}
<Button
variant="outline"
role="combobox"
aria-expanded={tzOpen}
class="w-full justify-between sm:w-80"
disabled={!canWrite}
{...props}
>
{t.timezone}
<ChevronsUpDownIcon class="size-4 opacity-50" />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-80 p-0">
<Command.Root>
<Command.Input placeholder={m.system_time_search_timezone_placeholder()} />
<Command.List class="max-h-72">
<Command.Empty>{m.system_time_no_timezone_found()}</Command.Empty>
{#each zones as z (z)}
<Command.Item
value={z}
onSelect={async () => {
tzOpen = false;
if (z === t.timezone) return;
await withSaving(() => setTimezone({ machineId, timezone: z }));
}}
>
<CheckIcon
class={'mr-2 size-4 ' + (z === t.timezone ? 'opacity-100' : 'opacity-0')}
/>
{z}
</Command.Item>
{/each}
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title>{m.system_time_manual()}</Card.Title>
<Card.Description>{m.system_time_manual_hint()}</Card.Description>
</Card.Header>
<Card.Content>
<form
class="flex flex-col gap-2 sm:flex-row sm:items-end"
onsubmit={async (e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const v = String(fd.get('time') ?? '').trim();
if (!v) return;
await withSaving(() => setTime({ machineId, time: rfc3339FromLocal(v) }));
}}
>
<div class="flex grow flex-col gap-1.5">
<Label for={formId + 'time'}>{m.system_time_current()}</Label>
<Input
id={formId + 'time'}
name="time"
type="datetime-local"
step="1"
disabled={t.ntp}
/>
</div>
<Button type="submit" disabled={t.ntp || saving || !canWrite}>{m.save()}</Button>
</form>
</Card.Content>
</Card.Root>
</svelte:boundary>
</div>