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

221 lines
7.9 KiB
Svelte

<script lang="ts">
import CheckIcon from '@lucide/svelte/icons/check';
import CompassIcon from '@lucide/svelte/icons/compass';
import CpuIcon from '@lucide/svelte/icons/cpu';
import FingerprintIcon from '@lucide/svelte/icons/fingerprint';
import HardDriveIcon from '@lucide/svelte/icons/hard-drive';
import Loader2Icon from '@lucide/svelte/icons/loader-2';
import MonitorCogIcon from '@lucide/svelte/icons/monitor-cog';
import NetworkIcon from '@lucide/svelte/icons/network';
import PackageIcon from '@lucide/svelte/icons/package';
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
import ShieldAlertIcon from '@lucide/svelte/icons/shield-alert';
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check';
import UsersIcon from '@lucide/svelte/icons/users';
import XIcon from '@lucide/svelte/icons/x';
import { page } from '$app/state';
import PageMeta from '$lib/components/seo/page-meta.svelte';
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { m } from '$lib/paraglide/messages';
import { getModules, getWhoami } from '$lib/remotes/system.remote';
const machineId = $derived(page.params.machineId!);
const modulesResource = $derived(getModules(machineId));
const whoamiResource = $derived(getWhoami(machineId));
const dataPromise = $derived(Promise.all([modulesResource, whoamiResource]));
let refreshing = $state(false);
async function handleRefresh() {
refreshing = true;
try {
await Promise.all([modulesResource.refresh(), whoamiResource.refresh()]);
} finally {
refreshing = false;
}
}
function getModuleIcon(id: string) {
switch (id) {
case 'networking':
return NetworkIcon;
case 'packages':
return PackageIcon;
case 'services':
return CpuIcon;
case 'storage':
return HardDriveIcon;
case 'system':
return MonitorCogIcon;
case 'users':
return UsersIcon;
default:
return CompassIcon;
}
}
function hasPermission(
moduleId: string,
perm: string,
userPerms: Record<string, null | string[]>
) {
if (userPerms['*']) return true;
const modPerms = userPerms[moduleId];
if (!modPerms) return false;
return modPerms.includes('*') || modPerms.includes(perm);
}
</script>
<PageMeta title={m.seo_title_system_nadir()} description={m.seo_desc_system_nadir()} />
<div class="mx-auto flex w-full max-w-4xl flex-col gap-6 p-4">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="flex flex-col gap-0.5">
<h1 class="text-2xl font-semibold tracking-tight">{m.seo_title_system_nadir()}</h1>
</div>
</div>
<Button variant="outline" size="icon" onclick={handleRefresh} disabled={refreshing}>
<RefreshCwIcon class="size-4 {refreshing ? 'animate-spin' : ''}" />
</Button>
</div>
<svelte:boundary>
{#snippet failed(err)}
<Card.Root class="border-destructive/20 bg-destructive/5">
<Card.Content class="text-destructive py-6 font-mono text-sm">
{(err as Error).message || m.errors_generic()}
</Card.Content>
</Card.Root>
{/snippet}
{#await dataPromise}
<div class="flex items-center justify-center py-20">
<Loader2Icon class="text-muted-foreground size-8 animate-spin" />
</div>
{:then [modulesData, whoamiData]}
{@const hasGlobalWildcard = !!whoamiData.permissions['*']}
<div class="grid gap-6 md:grid-cols-3">
<!-- Whoami Details Card -->
<Card.Root class="md:col-span-1 border border-border">
<Card.Header class="pb-3">
<div class="flex items-center gap-2 text-emerald-500">
<FingerprintIcon class="size-5" />
<Card.Title class="text-sm font-semibold tracking-wide uppercase"
>{m.system_nadir_username()}</Card.Title
>
</div>
</Card.Header>
<Card.Content class="flex flex-col gap-4">
<div class="flex flex-col">
<span class="text-2xl font-bold tracking-tight break-all">{whoamiData.username}</span>
<span class="text-muted-foreground mt-1 text-xs font-mono">Agent Identity</span>
</div>
<div class="border-t pt-3 space-y-2">
<span class="text-muted-foreground text-xs font-medium block uppercase tracking-wider"
>{m.system_nadir_permissions()}</span
>
{#if hasGlobalWildcard}
<div
class="flex items-center gap-2 rounded-md bg-emerald-500/10 p-2 text-xs border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 font-medium"
>
<ShieldCheckIcon class="size-4 shrink-0" />
<span>Administrator (Full Access)</span>
</div>
{:else}
<div
class="flex items-center gap-2 rounded-md bg-sky-500/10 p-2 text-xs border border-sky-500/20 text-sky-600 dark:text-sky-400 font-medium"
>
<ShieldAlertIcon class="size-4 shrink-0" />
<span>Restricted Module Access</span>
</div>
{/if}
</div>
</Card.Content>
</Card.Root>
<!-- Module List / Grid -->
<div class="flex flex-col gap-4 md:col-span-2">
<h2 class="text-lg font-medium tracking-tight flex items-center gap-2">
<span>{m.system_nadir_modules()}</span>
<Badge variant="secondary" class="font-mono text-xs"
>{modulesData?.modules?.length ?? 0}</Badge
>
</h2>
<div class="grid gap-4 sm:grid-cols-2">
{#each modulesData?.modules ?? [] as mod (mod.id)}
{@const Icon = getModuleIcon(mod.id)}
{@const hasModuleWildcard =
!hasGlobalWildcard && whoamiData.permissions[mod.id]?.includes('*')}
<Card.Root
class="group transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md border border-border/80 hover:border-emerald-500/30"
>
<Card.Header class="pb-3 flex flex-row items-start justify-between gap-2 space-y-0">
<div class="flex items-center gap-3">
<div
class="rounded-lg bg-muted/40 p-2 text-muted-foreground group-hover:text-emerald-500 group-hover:bg-emerald-500/10 transition-colors"
>
<Icon class="size-4" />
</div>
<div class="flex flex-col gap-0.5">
<Card.Title
class="text-sm font-semibold tracking-tight leading-none capitalize"
>{mod.name}</Card.Title
>
<span class="text-muted-foreground text-[10px] font-mono leading-none"
>{mod.id}</span
>
</div>
</div>
{#if hasGlobalWildcard || hasModuleWildcard}
<Badge
class="bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-0 p-1 rounded-full"
title="Full access"
>
<ShieldCheckIcon class="size-3.5" />
</Badge>
{/if}
</Card.Header>
<Card.Content class="flex flex-col gap-3">
<div class="flex flex-wrap gap-1.5">
{#if mod.permissions && mod.permissions.length > 0}
{#each mod.permissions as perm (perm)}
{@const ok = hasPermission(mod.id, perm, whoamiData.permissions)}
<Badge
variant={ok ? 'default' : 'outline'}
class="text-[10px] py-0.5 px-2 font-mono transition-all flex items-center gap-1 leading-none rounded-full
{ok
? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-0 hover:bg-emerald-500/20'
: 'bg-muted/10 text-muted-foreground hover:bg-muted/20 border-dashed border-border/70'}"
>
{#if ok}
<CheckIcon class="size-2.5 shrink-0" />
{:else}
<XIcon class="size-2.5 shrink-0" />
{/if}
{perm}
</Badge>
{/each}
{:else}
<span class="text-muted-foreground text-xs italic"
>{m.system_nadir_no_permissions()}</span
>
{/if}
</div>
</Card.Content>
</Card.Root>
{/each}
</div>
</div>
</div>
{/await}
</svelte:boundary>
</div>