221 lines
7.9 KiB
Svelte
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>
|