Add sidebar navigation and global command search

This commit is contained in:
2026-10-07 18:59:39 -04:00
parent 9999e80226
commit e4f37f6964
33 changed files with 660 additions and 151 deletions
+1
View File
@@ -16,6 +16,7 @@
let search = $state(''); let status = $state('');
let loadSequence = 0;
const canManage = $derived($page.data.user?.role === 'admin');
$effect(() => { if (canManage && $page.url.searchParams.get('action') === 'add') addOpen = true; });
const attention = $derived(hosts.filter(host => ['attention', 'offline'].includes(hostListStatus(host, now).key)).length);
const ready = $derived(hosts.filter(host => hostListStatus(host, now).key === 'ready').length);
const filtersVisible = $derived(hosts.length > 5 || !!search || !!status);
+1 -4
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import BackLink from '$ui/components/BackLink.svelte';
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
import Tabs from '$ui/components/primitives/Tabs.svelte';
import { contextHref } from '$lib/navigation-context.js';
@@ -73,7 +72,6 @@
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Hosts', href: '/hosts' }, { label: host?.name ?? 'Host' }]} />{/snippet}
{#snippet actions()}<Button href={contextHref(`/capacity?host=${encodeURIComponent($page.params.id ?? "")}`, $page.url)}>Capacity</Button><Button href={contextHref(`/hosts/operations?host=${encodeURIComponent($page.params.id ?? "")}`, $page.url)}>Operation history</Button>{#if canManage && host && (!cleaning || host.decommission?.retryable)}<details class="host-menu"><summary>More actions</summary><div><Button variant="danger" disabled={busy} onclick={() => deleteOpen = true}>{cleaning ? 'Retry cleanup' : 'Delete host'}</Button></div></details>{/if}{/snippet}
</SectionHeader>
<BackLink fallback="/hosts" />
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}{#if notice}<p role="status">{notice}</p>{/if}
{#if loading}<p role="status">Loading host…</p>{:else if host}
{#if host.decommission}<section class="panel" aria-live="polite"><h2>Host cleanup</h2><p>{hostDeletionMessage(host)}</p>{#if host.decommission.error}<p class="ui-error">{host.decommission.error}</p>{/if}<p>Requested {new Date(host.decommission.requestedAt).toLocaleString()}. This page closes only after the cleanup worker confirms removal.</p>{#if host.decommission.stage === 'cleaned'}<p>The agent is expected to go offline during uninstall. If final confirmation never arrives, inspect the machine; loss of contact alone does not prove completion.</p>{/if}</section>{/if}
@@ -84,7 +82,6 @@
<Tabs {tabs} value={tab} onchange={changeTab} ariaLabel="Host sections" idPrefix="host-tabs" panelId="host-panel" />
<div class="host-panel" id="host-panel" role="tabpanel" aria-labelledby={`host-tabs-${tab}`}>
{#if tab === 'overview'}
<div class="overview-actions"><Button onclick={() => changeTab('servers')}>View {instances.length} servers</Button></div>
<section class="panel"><h2>Agent workers</h2>
{#if host.inventory.agentLanes?.length}<dl>{#each host.inventory.agentLanes as lane (lane.name)}
<dt>{hostAgentLaneLabel(lane.name)}</dt><dd>{hostAgentLaneState(lane)}{#if !host.connected} (last reported){/if}<br />Last successful poll: {lane.lastPollAt ? new Date(lane.lastPollAt).toLocaleString() : 'Not yet reported'}</dd>
@@ -161,6 +158,6 @@
.host-menu>div { position:absolute; right:0; top:100%; z-index:5; padding:12px; min-width:160px; background:var(--surface-raised); border:1px solid var(--border-strong); border-radius:var(--radius-md); }
.instances { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr)); gap:16px; }
.host-panel { display:grid; gap:24px; min-width:0; } .overview-actions { display:flex; flex-wrap:wrap; gap:12px; }
.host-panel { display:grid; gap:24px; min-width:0; }
.page { display:grid; gap:24px; } .panel { border:1px solid var(--border-strong); border-radius:var(--radius-md); background:var(--surface); padding:24px; min-width:0; } h2 { margin:0 0 16px; font-size:var(--text-md); text-transform:capitalize; } p, li, dd, dt, span { font-size:var(--text-sm); line-height:1.65; } p,li,dt,span { color:var(--text-secondary); } .row,.actions,.fields { display:flex; gap:12px; flex-wrap:wrap; } .row { justify-content:space-between; } .actions { margin-top:20px; } dl { display:grid; grid-template-columns:1fr 1.3fr; gap:12px; margin:0; } dd { margin:0; overflow-wrap:anywhere; } fieldset { display:grid; gap:16px; border:0; padding:0; margin:0; } label { display:grid; gap:8px; font-size:var(--text-sm); } input { width:100%; min-width:0; padding:10px; color:var(--text-primary); background:var(--surface-sunken); border:1px solid var(--border-control); border-radius:var(--radius-sm); } .fields>* { flex:1; min-width:120px; } pre { white-space:pre-wrap; overflow-wrap:anywhere; padding:16px; background:var(--surface-sunken); font-size:var(--text-xs); }
</style>