style(web): fix prettier config, format entire web/ tree
.prettierrc.json was missing "semi": false, so prettier wanted to add semicolons to a codebase written without them (763 semicolon-free statements vs. 150 with, in hand-written .ts; zero hand-written .svelte files use them at all). That's why prettier --check failed on 249 files — not because the code was unformatted, but because the config didn't match the actual house style. Added "semi": false; left printWidth/etc as configured (printWidth barely moves the failure count: 218/213/212 files at 100/120/140). Ran `prettier --write .` with the corrected config. Verified semantics-preserving before and after: - eslint: 142 problems both before and after, byte-identical - build passes, 38/38 tests pass - token-stream diff (whitespace/semicolons/quotes normalized) on all 218 changed files: only 52 had any remaining token change, all either trailing-comma removal (matching trailingComma: "none") or import/ ternary reflow — no semantic changes - live smoke test: Knowledge, Tasks, Fleet map, and a chat window (AgentTrace, markdown, Scope graph, activity rail) all render correctly, no console errors Most of the diff is shadcn/ui vendor files (lib/components/ui/) moving from the CLI's own style (double quotes, tabs, semicolons) to house style; re-running `shadcn-svelte add` on a component will need a follow-up format pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,8 +26,7 @@
|
||||
<header class="shrink-0 border-b px-4 py-3">
|
||||
<h1 class="text-base font-semibold">App Store</h1>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
Installable apps. Local bundles for now — a remote catalog + sandboxing
|
||||
is Phase 4.
|
||||
Installable apps. Local bundles for now — a remote catalog + sandboxing is Phase 4.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -40,20 +39,27 @@
|
||||
{@const m = entry.manifest}
|
||||
{@const isOn = installedSet.has(m.id)}
|
||||
<li class="flex items-start gap-3 rounded-lg border p-3">
|
||||
<div class="flex size-10 shrink-0 items-center justify-center rounded-md border bg-muted/40">
|
||||
<div
|
||||
class="flex size-10 shrink-0 items-center justify-center rounded-md border bg-muted/40"
|
||||
>
|
||||
<entry.icon class="size-5" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="truncate text-sm font-medium">{m.title}</h2>
|
||||
<span class="shrink-0 text-[10px] font-mono text-muted-foreground">v{m.version}</span>
|
||||
{#if m.author}<span class="shrink-0 text-[10px] text-muted-foreground">by {m.author}</span>{/if}
|
||||
<span class="shrink-0 text-[10px] font-mono text-muted-foreground"
|
||||
>v{m.version}</span
|
||||
>
|
||||
{#if m.author}<span class="shrink-0 text-[10px] text-muted-foreground"
|
||||
>by {m.author}</span
|
||||
>{/if}
|
||||
</div>
|
||||
<p class="mt-0.5 line-clamp-2 text-xs text-muted-foreground">{m.description}</p>
|
||||
<div class="mt-1.5 flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||
<ShieldIcon class="size-3" />
|
||||
<span>
|
||||
{#if m.permissions.length}{m.permissions.join(', ')}{:else}no permissions requested{/if}
|
||||
{#if m.permissions.length}{m.permissions.join(', ')}{:else}no permissions
|
||||
requested{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,8 +90,8 @@
|
||||
<div class="mt-6 rounded-md border border-dashed p-3 text-xs text-muted-foreground">
|
||||
<p class="flex items-center gap-1.5">
|
||||
<CheckCircleIcon class="size-3.5" />
|
||||
Installed apps appear on the desktop immediately. Uninstalling closes
|
||||
any open window for that app.
|
||||
Installed apps appear on the desktop immediately. Uninstalling closes any open window for that
|
||||
app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,12 +92,15 @@
|
||||
<ConfigBackground />
|
||||
|
||||
<div class="relative z-10 flex h-full items-center justify-center p-6">
|
||||
<div class="w-full max-w-[26rem] space-y-8 rounded-2xl border border-white/8 bg-card/60 p-8 shadow-2xl shadow-black/40 backdrop-blur-xl">
|
||||
|
||||
<div
|
||||
class="w-full max-w-[26rem] space-y-8 rounded-2xl border border-white/8 bg-card/60 p-8 shadow-2xl shadow-black/40 backdrop-blur-xl"
|
||||
>
|
||||
<!-- Logo + heading -->
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<svg viewBox="0 0 91 100" class="h-16 w-16 fill-white/90" aria-hidden="true">
|
||||
<path d="m45.601 1q20.993 0 33.71 15.946 10.799 13.625 10.799 31.287 0 12.414-5.9548 25.131-5.9548 12.717-16.451 19.176-10.395 6.4592-23.213 6.4592-20.892 0-33.205-16.653-10.395-14.029-10.395-31.489 0-12.717 6.2577-25.232 6.3584-12.616 16.653-18.57 10.295-6.0556 21.801-6.0556zm-3.128 6.5605q-5.3492 0-10.799 3.2296-5.3492 3.1287-8.68 11.102-3.3305 7.9735-3.3305 20.488 0 20.185 7.973 34.82 8.0743 14.634 21.195 14.634 9.7896 0 16.149-8.0743 6.3584-8.0743 6.3584-27.755 0-24.627-10.597-38.756-7.1657-9.6888-18.268-9.6888z" />
|
||||
<path
|
||||
d="m45.601 1q20.993 0 33.71 15.946 10.799 13.625 10.799 31.287 0 12.414-5.9548 25.131-5.9548 12.717-16.451 19.176-10.395 6.4592-23.213 6.4592-20.892 0-33.205-16.653-10.395-14.029-10.395-31.489 0-12.717 6.2577-25.232 6.3584-12.616 16.653-18.57 10.295-6.0556 21.801-6.0556zm-3.128 6.5605q-5.3492 0-10.799 3.2296-5.3492 3.1287-8.68 11.102-3.3305 7.9735-3.3305 20.488 0 20.185 7.973 34.82 8.0743 14.634 21.195 14.634 9.7896 0 16.149-8.0743 6.3584-8.0743 6.3584-27.755 0-24.627-10.597-38.756-7.1657-9.6888-18.268-9.6888z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-foreground">Connect to Oikos</h1>
|
||||
@@ -109,7 +112,9 @@
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Label for="server-url" class="text-xs font-medium">Server URL</Label>
|
||||
<div class="relative">
|
||||
<Server class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50" />
|
||||
<Server
|
||||
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50"
|
||||
/>
|
||||
<Input
|
||||
id="server-url"
|
||||
type="url"
|
||||
@@ -122,7 +127,9 @@
|
||||
|
||||
{#if showOidcContinue}
|
||||
<!-- OIDC authenticated state -->
|
||||
<div class="flex flex-col items-center gap-3 rounded-xl border border-white/5 bg-background/40 p-5">
|
||||
<div
|
||||
class="flex flex-col items-center gap-3 rounded-xl border border-white/5 bg-background/40 p-5"
|
||||
>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Logged in as <span class="font-semibold text-foreground">{oidcUser}</span>
|
||||
</p>
|
||||
@@ -134,9 +141,7 @@
|
||||
<Button type="button" variant="outline" onclick={onCancel}>Cancel</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="sm" onclick={logoutOIDC}>
|
||||
Sign out
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" size="sm" onclick={logoutOIDC}>Sign out</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Auth options: Authentik + token -->
|
||||
@@ -155,23 +160,25 @@
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<Separator decorative class="flex-1" />
|
||||
<span class="text-[10px] font-medium uppercase tracking-widest text-muted-foreground/40">or use</span>
|
||||
<span class="text-[10px] font-medium uppercase tracking-widest text-muted-foreground/40"
|
||||
>or use</span
|
||||
>
|
||||
<Separator decorative class="flex-1" />
|
||||
</div>
|
||||
|
||||
<!-- Token row -->
|
||||
<form
|
||||
class="flex flex-col gap-2.5"
|
||||
onsubmit={(e) => { e.preventDefault(); connect() }}
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault()
|
||||
connect()
|
||||
}}
|
||||
>
|
||||
<div class="relative">
|
||||
<Lock class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50" />
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Bearer token"
|
||||
class="pl-9"
|
||||
bind:value={token}
|
||||
<Lock
|
||||
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50"
|
||||
/>
|
||||
<Input type="password" placeholder="Bearer token" class="pl-9" bind:value={token} />
|
||||
</div>
|
||||
<Button type="submit" disabled={connecting} class="w-full">
|
||||
{connecting ? 'Connecting…' : 'Connect'}
|
||||
@@ -182,7 +189,9 @@
|
||||
|
||||
<!-- Error -->
|
||||
{#if error}
|
||||
<p class="rounded-lg bg-destructive/10 px-3 py-2 text-center text-sm text-destructive">{error}</p>
|
||||
<p class="rounded-lg bg-destructive/10 px-3 py-2 text-center text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- Footer: cancel + forget -->
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { fetchAllEntities, fetchOntology, fetchGraph, type Entity, type EntityType, type Ontology } from '$lib/api'
|
||||
import {
|
||||
fetchAllEntities,
|
||||
fetchOntology,
|
||||
fetchGraph,
|
||||
type Entity,
|
||||
type EntityType,
|
||||
type Ontology
|
||||
} from '$lib/api'
|
||||
import { liveEvents, subscribeEvents } from '$lib/stores/events'
|
||||
import EntityTable from '$lib/components/EntityTable.svelte'
|
||||
import FleetMap from '$lib/components/FleetMap.svelte'
|
||||
@@ -120,7 +127,9 @@
|
||||
const hierRels = relationshipTypes.filter((rt) => rt.cardinality !== 'many-to-many')
|
||||
const relTypeNames = hierRels.map((rt) => rt.name)
|
||||
const cardinalityByType = new Map(hierRels.map((rt) => [rt.name, rt.cardinality]))
|
||||
const specificityByType = new Map(hierRels.map((rt) => [rt.name, typeDepth(byName, rt.source_type)]))
|
||||
const specificityByType = new Map(
|
||||
hierRels.map((rt) => [rt.name, typeDepth(byName, rt.source_type)])
|
||||
)
|
||||
const slugs = new Set(entities.map((e) => e.slug))
|
||||
|
||||
// One whole-graph fetch instead of one rooted fetch per candidate parent
|
||||
@@ -129,7 +138,10 @@
|
||||
// browser's concurrent-connection limit (ERR_INSUFFICIENT_RESOURCES).
|
||||
const g = await fetchGraph({ relType: relTypeNames })
|
||||
const pairs = (g?.edges ?? [])
|
||||
.filter((edge) => cardinalityByType.has(edge.type) && slugs.has(edge.source) && slugs.has(edge.target))
|
||||
.filter(
|
||||
(edge) =>
|
||||
cardinalityByType.has(edge.type) && slugs.has(edge.source) && slugs.has(edge.target)
|
||||
)
|
||||
.map((edge) => {
|
||||
const [child, parent] =
|
||||
cardinalityByType.get(edge.type) === 'many-to-one'
|
||||
@@ -189,14 +201,21 @@
|
||||
highlights nodes, table filters rows); the Inactive toggle + count are
|
||||
table-only; the graph/table switch sits inline on the right. -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Filter / highlight by slug or name…" bind:value={search} class="h-8 max-w-xs text-xs" />
|
||||
<Input
|
||||
placeholder="Filter / highlight by slug or name…"
|
||||
bind:value={search}
|
||||
class="h-8 max-w-xs text-xs"
|
||||
/>
|
||||
|
||||
{#if view === 'table'}
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Switch id="show-inactive" bind:checked={showInactive} />
|
||||
<Label for="show-inactive" class="text-xs font-normal text-muted-foreground">Inactive</Label>
|
||||
<Label for="show-inactive" class="text-xs font-normal text-muted-foreground">Inactive</Label
|
||||
>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">{filteredEntities.length} of {allEntities.length}</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>{filteredEntities.length} of {allEntities.length}</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
<div class="ml-auto inline-flex overflow-hidden rounded-md border">
|
||||
@@ -229,7 +248,13 @@
|
||||
{#if view === 'graph'}
|
||||
<FleetMap selectedSlug={lastOpened} onSelect={select} {search} />
|
||||
{:else}
|
||||
<EntityTable entities={filteredEntities} loading={entitiesLoading} selectedSlug={lastOpened} onSelect={select} {childToParent} />
|
||||
<EntityTable
|
||||
entities={filteredEntities}
|
||||
loading={entitiesLoading}
|
||||
selectedSlug={lastOpened}
|
||||
onSelect={select}
|
||||
{childToParent}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,11 +85,15 @@
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title class="text-sm">Execution outcomes — last 30 days</Card.Title>
|
||||
<Card.Description class="text-xs">Every gated action, by day it ran, succeeded vs failed.</Card.Description>
|
||||
<Card.Description class="text-xs"
|
||||
>Every gated action, by day it ran, succeeded vs failed.</Card.Description
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if trend.length === 0}
|
||||
{#if !loading}<p class="py-8 text-center text-sm text-muted-foreground">No executions in the last 30 days yet.</p>{/if}
|
||||
{#if !loading}<p class="py-8 text-center text-sm text-muted-foreground">
|
||||
No executions in the last 30 days yet.
|
||||
</p>{/if}
|
||||
{:else}
|
||||
<div bind:this={chartEl} class="w-full"></div>
|
||||
{/if}
|
||||
@@ -98,8 +102,12 @@
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title class="flex items-center gap-1.5 text-sm"><TrendingUpIcon class="size-4" /> Capability timeline</Card.Title>
|
||||
<Card.Description class="text-xs">What Nomos has learned to do, ordered by when it first succeeded.</Card.Description>
|
||||
<Card.Title class="flex items-center gap-1.5 text-sm"
|
||||
><TrendingUpIcon class="size-4" /> Capability timeline</Card.Title
|
||||
>
|
||||
<Card.Description class="text-xs"
|
||||
>What Nomos has learned to do, ordered by when it first succeeded.</Card.Description
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -108,13 +116,17 @@
|
||||
<div>
|
||||
<span class="font-mono text-sm">{item.verb}</span>
|
||||
<span class="ml-2 text-xs text-muted-foreground">
|
||||
{item.first_success ? `first succeeded ${fmtDate(item.first_success)}` : 'no successes yet'}
|
||||
{item.first_success
|
||||
? `first succeeded ${fmtDate(item.first_success)}`
|
||||
: 'no successes yet'}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant={timelineVariant(item)}>{item.successes}/{item.total}</Badge>
|
||||
</div>
|
||||
{:else}
|
||||
{#if !loading}<p class="py-8 text-center text-sm text-muted-foreground">No executions yet.</p>{/if}
|
||||
{#if !loading}<p class="py-8 text-center text-sm text-muted-foreground">
|
||||
No executions yet.
|
||||
</p>{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Card.Content>
|
||||
@@ -123,12 +135,15 @@
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title class="text-sm">Patterns</Card.Title>
|
||||
<Card.Description class="text-xs">Statistically validated behaviors, extracted from outcome feedback.</Card.Description>
|
||||
<Card.Description class="text-xs"
|
||||
>Statistically validated behaviors, extracted from outcome feedback.</Card.Description
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if patterns.length === 0}
|
||||
<p class="py-6 text-center text-sm text-muted-foreground">
|
||||
No patterns learned yet — patterns emerge once outcome feedback is recorded for repeated actions.
|
||||
No patterns learned yet — patterns emerge once outcome feedback is recorded for repeated
|
||||
actions.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -136,7 +151,8 @@
|
||||
<div class="flex items-center justify-between gap-3 rounded-lg border px-3 py-2">
|
||||
<div>
|
||||
<span class="text-sm">{p.pattern}</span>
|
||||
<span class="ml-2 text-xs text-muted-foreground">{p.applies_type} · {p.action}</span>
|
||||
<span class="ml-2 text-xs text-muted-foreground">{p.applies_type} · {p.action}</span
|
||||
>
|
||||
</div>
|
||||
<Badge variant="outline">{(p.confidence * 100).toFixed(0)}% conf.</Badge>
|
||||
</div>
|
||||
@@ -148,8 +164,12 @@
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title class="flex items-center gap-1.5 text-sm"><SparklesIcon class="size-4" /> Promoted skills</Card.Title>
|
||||
<Card.Description class="text-xs">Procedures promoted from validated patterns.</Card.Description>
|
||||
<Card.Title class="flex items-center gap-1.5 text-sm"
|
||||
><SparklesIcon class="size-4" /> Promoted skills</Card.Title
|
||||
>
|
||||
<Card.Description class="text-xs"
|
||||
>Procedures promoted from validated patterns.</Card.Description
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if skills.length === 0}
|
||||
|
||||
@@ -73,32 +73,116 @@
|
||||
const pendingApprovals = $derived(approvals.filter((a) => a.status === 'pending'))
|
||||
const decidedApprovals = $derived(approvals.filter((a) => a.status !== 'pending'))
|
||||
|
||||
const pendingColumns = $derived.by(() => [
|
||||
{ key: 'subject', header: 'Subject', class: 'font-mono text-xs', width: '180px', accessor: (a: Approval) => a.subject ?? '—', truncate: true },
|
||||
{ key: 'action', header: 'Action', truncate: true },
|
||||
{ key: 'risk_class', header: 'Risk', render: 'status-badge', renderProps: { kind: 'risk' }, width: '120px' },
|
||||
{ key: 'status', header: 'Status', render: 'status-badge', renderProps: { kind: 'execution' }, width: '100px' },
|
||||
{ key: 'expires_at', header: 'Expires', render: 'date', width: '170px' },
|
||||
{ key: '_actions', header: '', render: ApprovalActions,
|
||||
renderProps: { deciding, onApprove: (id: string) => decide(id, 'approve'), onDeny: (id: string) => decide(id, 'deny') },
|
||||
align: 'right', headerClass: 'text-right', width: '220px' },
|
||||
] as DataTableColumn<Approval>[])
|
||||
const pendingColumns = $derived.by(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
key: 'subject',
|
||||
header: 'Subject',
|
||||
class: 'font-mono text-xs',
|
||||
width: '180px',
|
||||
accessor: (a: Approval) => a.subject ?? '—',
|
||||
truncate: true
|
||||
},
|
||||
{ key: 'action', header: 'Action', truncate: true },
|
||||
{
|
||||
key: 'risk_class',
|
||||
header: 'Risk',
|
||||
render: 'status-badge',
|
||||
renderProps: { kind: 'risk' },
|
||||
width: '120px'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
render: 'status-badge',
|
||||
renderProps: { kind: 'execution' },
|
||||
width: '100px'
|
||||
},
|
||||
{ key: 'expires_at', header: 'Expires', render: 'date', width: '170px' },
|
||||
{
|
||||
key: '_actions',
|
||||
header: '',
|
||||
render: ApprovalActions,
|
||||
renderProps: {
|
||||
deciding,
|
||||
onApprove: (id: string) => decide(id, 'approve'),
|
||||
onDeny: (id: string) => decide(id, 'deny')
|
||||
},
|
||||
align: 'right',
|
||||
headerClass: 'text-right',
|
||||
width: '220px'
|
||||
}
|
||||
] as DataTableColumn<Approval>[]
|
||||
)
|
||||
|
||||
const decidedColumns: DataTableColumn<Approval>[] = [
|
||||
{ key: 'subject', header: 'Subject', class: 'font-mono text-xs', width: '180px', accessor: (a) => a.subject ?? '—', truncate: true },
|
||||
{
|
||||
key: 'subject',
|
||||
header: 'Subject',
|
||||
class: 'font-mono text-xs',
|
||||
width: '180px',
|
||||
accessor: (a) => a.subject ?? '—',
|
||||
truncate: true
|
||||
},
|
||||
{ key: 'action', header: 'Action', truncate: true },
|
||||
{ key: 'status', header: 'Status', render: 'status-badge', renderProps: { kind: 'execution' }, width: '100px' },
|
||||
{ key: 'decided_at', header: 'Decided', render: 'date', width: '170px', accessor: (a) => a.decided_at ?? '—' },
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
render: 'status-badge',
|
||||
renderProps: { kind: 'execution' },
|
||||
width: '100px'
|
||||
},
|
||||
{
|
||||
key: 'decided_at',
|
||||
header: 'Decided',
|
||||
render: 'date',
|
||||
width: '170px',
|
||||
accessor: (a) => a.decided_at ?? '—'
|
||||
}
|
||||
]
|
||||
|
||||
const activityColumns: DataTableColumn<ActivityItem>[] = [
|
||||
{ key: 'target', header: 'Target', class: 'font-mono text-xs', width: '180px', accessor: (a) => a.target ?? '—', truncate: true },
|
||||
{
|
||||
key: 'target',
|
||||
header: 'Target',
|
||||
class: 'font-mono text-xs',
|
||||
width: '180px',
|
||||
accessor: (a) => a.target ?? '—',
|
||||
truncate: true
|
||||
},
|
||||
{ key: '_action', header: 'Action', render: ActivityAction, truncate: true },
|
||||
{ key: 'risk_class', header: 'Risk', render: 'status-badge', renderProps: { kind: 'risk' }, width: '120px' },
|
||||
{ key: 'status', header: 'Status', render: 'status-badge', renderProps: { kind: 'execution' }, width: '110px' },
|
||||
{ key: 'duration_ms', header: 'Duration', render: DurationRenderer, width: '90px', align: 'right' },
|
||||
{
|
||||
key: 'risk_class',
|
||||
header: 'Risk',
|
||||
render: 'status-badge',
|
||||
renderProps: { kind: 'risk' },
|
||||
width: '120px'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
render: 'status-badge',
|
||||
renderProps: { kind: 'execution' },
|
||||
width: '110px'
|
||||
},
|
||||
{
|
||||
key: 'duration_ms',
|
||||
header: 'Duration',
|
||||
render: DurationRenderer,
|
||||
width: '90px',
|
||||
align: 'right'
|
||||
},
|
||||
{ key: 'created_at', header: 'When', render: 'relative-time', width: '100px' },
|
||||
{ key: '_cancel', header: '', render: ActivityCancel, renderProps: { onCancel: cancel }, align: 'right', headerClass: 'text-right', width: '100px' },
|
||||
{
|
||||
key: '_cancel',
|
||||
header: '',
|
||||
render: ActivityCancel,
|
||||
renderProps: { onCancel: cancel },
|
||||
align: 'right',
|
||||
headerClass: 'text-right',
|
||||
width: '100px'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -108,13 +192,19 @@
|
||||
<Tabs.Root value="approvals" class="flex flex-1 flex-col overflow-hidden">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="approvals">
|
||||
Approvals {#if pendingApprovals.length}<Badge variant="destructive" class="ml-1">{pendingApprovals.length}</Badge>{/if}
|
||||
Approvals {#if pendingApprovals.length}<Badge variant="destructive" class="ml-1"
|
||||
>{pendingApprovals.length}</Badge
|
||||
>{/if}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="executions">Activity</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="approvals" class="flex-1 overflow-auto">
|
||||
<DataTable columns={pendingColumns} data={pendingApprovals} emptyMessage="No pending approvals." />
|
||||
<DataTable
|
||||
columns={pendingColumns}
|
||||
data={pendingApprovals}
|
||||
emptyMessage="No pending approvals."
|
||||
/>
|
||||
|
||||
{#if decidedApprovals.length}
|
||||
<p class="mt-4 text-xs text-muted-foreground">Recently decided</p>
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
let filter = $state<'all' | Bucket>('all')
|
||||
|
||||
const counts = $derived.by(() => {
|
||||
const c: Record<string, number> = { all: $sessions.length, running: 0, input: 0, done: 0, failed: 0 }
|
||||
const c: Record<string, number> = {
|
||||
all: $sessions.length,
|
||||
running: 0,
|
||||
input: 0,
|
||||
done: 0,
|
||||
failed: 0
|
||||
}
|
||||
for (const s of $sessions) c[bucket(s)]++
|
||||
return c
|
||||
})
|
||||
@@ -53,10 +59,31 @@
|
||||
})
|
||||
|
||||
const columns: DataTableColumn<Session>[] = [
|
||||
{ key: '_status', header: 'Status', render: StatusDotRenderer, sortable: true, accessor: (s) => bucket(s), width: '140px' },
|
||||
{
|
||||
key: '_status',
|
||||
header: 'Status',
|
||||
render: StatusDotRenderer,
|
||||
sortable: true,
|
||||
accessor: (s) => bucket(s),
|
||||
width: '140px'
|
||||
},
|
||||
{ key: '_heading', header: 'Task', sortable: true, accessor: heading, truncate: true },
|
||||
{ key: 'summary', header: 'Summary', accessor: (s) => s.summary || '—', truncate: true, headerClass: 'hidden md:table-cell', class: 'hidden md:table-cell' },
|
||||
{ key: 'last_active_at', header: 'Last active', render: 'relative-time', sortable: true, width: '112px', align: 'right' },
|
||||
{
|
||||
key: 'summary',
|
||||
header: 'Summary',
|
||||
accessor: (s) => s.summary || '—',
|
||||
truncate: true,
|
||||
headerClass: 'hidden md:table-cell',
|
||||
class: 'hidden md:table-cell'
|
||||
},
|
||||
{
|
||||
key: 'last_active_at',
|
||||
header: 'Last active',
|
||||
render: 'relative-time',
|
||||
sortable: true,
|
||||
width: '112px',
|
||||
align: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
const emptyMessage = $derived(
|
||||
@@ -67,7 +94,6 @@
|
||||
</script>
|
||||
|
||||
<div class="relative flex h-full flex-col gap-3 overflow-hidden p-2">
|
||||
|
||||
<div class="relative z-10 flex flex-wrap items-center gap-1.5">
|
||||
{#each FILTERS as f}
|
||||
<button
|
||||
@@ -88,13 +114,9 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 min-h-0 flex-1 overflow-hidden rounded-xl border bg-card/70 backdrop-blur">
|
||||
<DataTable
|
||||
{columns}
|
||||
data={visible}
|
||||
{emptyMessage}
|
||||
bordered={false}
|
||||
onRowClick={openTask}
|
||||
/>
|
||||
<div
|
||||
class="relative z-10 min-h-0 flex-1 overflow-hidden rounded-xl border bg-card/70 backdrop-blur"
|
||||
>
|
||||
<DataTable {columns} data={visible} {emptyMessage} bordered={false} onRowClick={openTask} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,14 @@
|
||||
import { Separator } from '$lib/components/ui/separator'
|
||||
import { Checkbox } from '$lib/components/ui/checkbox'
|
||||
import { Slider } from '$lib/components/ui/slider'
|
||||
import { fetchWithAuth, setConfig, initConfig, getConfig, clearConfig, type OikosConfig } from '$lib/config'
|
||||
import {
|
||||
fetchWithAuth,
|
||||
setConfig,
|
||||
initConfig,
|
||||
getConfig,
|
||||
clearConfig,
|
||||
type OikosConfig
|
||||
} from '$lib/config'
|
||||
import { startLogin, logout as oidcLogout, getUser, isOIDCConfigured } from '$lib/oidc'
|
||||
import { getTheme, setTheme, THEME_LABELS, type Theme } from '$lib/stores/theme.svelte'
|
||||
import {
|
||||
@@ -138,234 +145,302 @@
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<nav class="flex w-44 shrink-0 flex-col gap-0.5 border-r bg-muted/20 p-2">
|
||||
{#each SECTIONS as s (s.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors {section === s.id
|
||||
? 'bg-muted font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'}"
|
||||
onclick={() => (section = s.id)}
|
||||
>
|
||||
<s.icon class="size-4 shrink-0" />
|
||||
{s.label}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<nav class="flex w-44 shrink-0 flex-col gap-0.5 border-r bg-muted/20 p-2">
|
||||
{#each SECTIONS as s (s.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors {section ===
|
||||
s.id
|
||||
? 'bg-muted font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'}"
|
||||
onclick={() => (section = s.id)}
|
||||
>
|
||||
<s.icon class="size-4 shrink-0" />
|
||||
{s.label}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto p-6">
|
||||
{#if section === 'connection'}
|
||||
<div class="mx-auto flex max-w-md flex-col gap-5">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold">Connection</h2>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">Where the control room talks to your Oikos server.</p>
|
||||
</div>
|
||||
|
||||
{#if oidcUser}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Signed in via Authentik as <span class="font-medium text-foreground">{oidcUser}</span>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Label for="settings-server-url" class="text-xs font-medium">Server URL</Label>
|
||||
<div class="relative">
|
||||
<ServerIcon class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50" />
|
||||
<Input id="settings-server-url" type="url" placeholder="https://oikos.hubris.network" class="pl-9" bind:value={apiUrl} />
|
||||
<div class="min-h-0 flex-1 overflow-y-auto p-6">
|
||||
{#if section === 'connection'}
|
||||
<div class="mx-auto flex max-w-md flex-col gap-5">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold">Connection</h2>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
Where the control room talks to your Oikos server.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="button" variant="secondary" disabled={oidcLoggingIn} onclick={loginWithAuthentik} class="w-full gap-2">
|
||||
<LogInIcon class="size-4" />
|
||||
{oidcLoggingIn ? 'Redirecting…' : oidcConfigured ? 'Switch account with Authentik' : 'Login with Authentik'}
|
||||
</Button>
|
||||
{#if oidcUser}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Signed in via Authentik as <span class="font-medium text-foreground">{oidcUser}</span>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<Separator decorative class="flex-1" />
|
||||
<span class="text-[10px] font-medium uppercase tracking-widest text-muted-foreground/40">or use</span>
|
||||
<Separator decorative class="flex-1" />
|
||||
</div>
|
||||
|
||||
<form class="flex flex-col gap-2.5" onsubmit={(e) => { e.preventDefault(); save() }}>
|
||||
<div class="relative">
|
||||
<LockIcon class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50" />
|
||||
<Input type="password" placeholder="Bearer token" class="pl-9" bind:value={token} />
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Label for="settings-server-url" class="text-xs font-medium">Server URL</Label>
|
||||
<div class="relative">
|
||||
<ServerIcon
|
||||
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50"
|
||||
/>
|
||||
<Input
|
||||
id="settings-server-url"
|
||||
type="url"
|
||||
placeholder="https://oikos.hubris.network"
|
||||
class="pl-9"
|
||||
bind:value={apiUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" disabled={saving} class="w-full">
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={oidcLoggingIn}
|
||||
onclick={loginWithAuthentik}
|
||||
class="w-full gap-2"
|
||||
>
|
||||
<LogInIcon class="size-4" />
|
||||
{oidcLoggingIn
|
||||
? 'Redirecting…'
|
||||
: oidcConfigured
|
||||
? 'Switch account with Authentik'
|
||||
: 'Login with Authentik'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{#if error}
|
||||
<p class="rounded-lg bg-destructive/10 px-3 py-2 text-center text-sm text-destructive">{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if existing.token || oidcConfigured}
|
||||
<div class="flex justify-center pt-1">
|
||||
<Button type="button" variant="ghost" size="sm" onclick={forgetConnection}>Forget saved connection</Button>
|
||||
<div class="flex items-center gap-3">
|
||||
<Separator decorative class="flex-1" />
|
||||
<span class="text-[10px] font-medium uppercase tracking-widest text-muted-foreground/40"
|
||||
>or use</span
|
||||
>
|
||||
<Separator decorative class="flex-1" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if section === 'appearance'}
|
||||
<div class="mx-auto flex max-w-md flex-col gap-5">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold">Appearance</h2>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">Pick the theme for the whole desktop.</p>
|
||||
|
||||
<form
|
||||
class="flex flex-col gap-2.5"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault()
|
||||
save()
|
||||
}}
|
||||
>
|
||||
<div class="relative">
|
||||
<LockIcon
|
||||
class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50"
|
||||
/>
|
||||
<Input type="password" placeholder="Bearer token" class="pl-9" bind:value={token} />
|
||||
</div>
|
||||
<Button type="submit" disabled={saving} class="w-full">
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{#if error}
|
||||
<p class="rounded-lg bg-destructive/10 px-3 py-2 text-center text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if existing.token || oidcConfigured}
|
||||
<div class="flex justify-center pt-1">
|
||||
<Button type="button" variant="ghost" size="sm" onclick={forgetConnection}
|
||||
>Forget saved connection</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if section === 'appearance'}
|
||||
<div class="mx-auto flex max-w-md flex-col gap-5">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold">Appearance</h2>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
Pick the theme for the whole desktop.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each Object.entries(THEME_LABELS) as [id, label] (id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-between gap-1.5 rounded-lg border px-2.5 py-1.5 text-left text-sm transition-colors {getTheme() === (id as Theme)
|
||||
? 'border-primary/50 bg-primary/5 text-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted/50'}"
|
||||
onclick={() => pickTheme(id as Theme)}
|
||||
>
|
||||
{label}
|
||||
{#if getTheme() === (id as Theme)}<CheckIcon class="size-3.5 shrink-0 text-primary" />{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each Object.entries(THEME_LABELS) as [id, label] (id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-between gap-1.5 rounded-lg border px-2.5 py-1.5 text-left text-sm transition-colors {getTheme() ===
|
||||
(id as Theme)
|
||||
? 'border-primary/50 bg-primary/5 text-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted/50'}"
|
||||
onclick={() => pickTheme(id as Theme)}
|
||||
>
|
||||
{label}
|
||||
{#if getTheme() === (id as Theme)}<CheckIcon
|
||||
class="size-3.5 shrink-0 text-primary"
|
||||
/>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Separator decorative />
|
||||
<Separator decorative />
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold">Desktop background</h3>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
A subtle CSS pattern behind your icons, in the style of
|
||||
<a href="https://www.magicpattern.design/tools/css-backgrounds" target="_blank" rel="noreferrer">magicpattern.design</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold">Desktop background</h3>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
A subtle CSS pattern behind your icons, in the style of
|
||||
<a
|
||||
href="https://www.magicpattern.design/tools/css-backgrounds"
|
||||
target="_blank"
|
||||
rel="noreferrer">magicpattern.design</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
{#each PATTERNS as p (p.id)}
|
||||
{@const active = getBackground().pattern === p.id}
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-1.5 rounded-lg border p-2 transition-colors {active
|
||||
? 'border-primary/60 bg-primary/5'
|
||||
: 'border-border hover:bg-muted/50'}"
|
||||
onclick={() => setBackgroundPattern(p.id)}
|
||||
title={p.label}
|
||||
>
|
||||
<span
|
||||
class="h-10 w-full rounded-md border border-border/60 bg-muted"
|
||||
style={swatchStyle(p.id, getBackground().color)}
|
||||
></span>
|
||||
<span class="flex items-center gap-1 text-[11px] {active ? 'font-medium text-foreground' : 'text-muted-foreground'}">
|
||||
{p.label}
|
||||
{#if active}<CheckIcon class="size-3 text-primary" />{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
{#each PATTERNS as p (p.id)}
|
||||
{@const active = getBackground().pattern === p.id}
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-1.5 rounded-lg border p-2 transition-colors {active
|
||||
? 'border-primary/60 bg-primary/5'
|
||||
: 'border-border hover:bg-muted/50'}"
|
||||
onclick={() => setBackgroundPattern(p.id)}
|
||||
title={p.label}
|
||||
>
|
||||
<span
|
||||
class="h-10 w-full rounded-md border border-border/60 bg-muted"
|
||||
style={swatchStyle(p.id, getBackground().color)}
|
||||
></span>
|
||||
<span
|
||||
class="flex items-center gap-1 text-[11px] {active
|
||||
? 'font-medium text-foreground'
|
||||
: 'text-muted-foreground'}"
|
||||
>
|
||||
{p.label}
|
||||
{#if active}<CheckIcon class="size-3 text-primary" />{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Colors: pattern (foreground shapes) + an optional fill behind
|
||||
<!-- Colors: pattern (foreground shapes) + an optional fill behind
|
||||
them. Fill defaults to "Auto", i.e. transparent, so the app's
|
||||
own theme background shows through the gaps — same as before
|
||||
this control existed. -->
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<Label for="bg-color" class="text-xs font-medium">Pattern color</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="bg-color"
|
||||
type="color"
|
||||
class="size-7 cursor-pointer rounded-md border border-border bg-transparent p-0.5"
|
||||
value={getBackground().color}
|
||||
oninput={(e) => setPatternColor((e.currentTarget as HTMLInputElement).value)}
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<Label for="bg-color" class="text-xs font-medium">Pattern color</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="bg-color"
|
||||
type="color"
|
||||
class="size-7 cursor-pointer rounded-md border border-border bg-transparent p-0.5"
|
||||
value={getBackground().color}
|
||||
oninput={(e) => setPatternColor((e.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
<span class="font-mono text-xs text-muted-foreground">{getBackground().color}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="bg-fill-toggle"
|
||||
checked={getBackground().fillColor !== null}
|
||||
onCheckedChange={(v) => onFillToggle(!!v)}
|
||||
/>
|
||||
<Label for="bg-fill-toggle" class="text-xs font-medium">Custom background fill</Label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="bg-fill-color"
|
||||
type="color"
|
||||
class="size-7 cursor-pointer rounded-md border border-border bg-transparent p-0.5 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
value={getBackground().fillColor ?? fillDraft}
|
||||
disabled={getBackground().fillColor === null}
|
||||
oninput={onFillColorInput}
|
||||
/>
|
||||
<span class="font-mono text-xs text-muted-foreground"
|
||||
>{getBackground().fillColor ?? 'Auto'}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="bg-opacity" class="text-xs font-medium">Opacity</Label>
|
||||
<span class="font-mono text-xs text-muted-foreground"
|
||||
>{Math.round(getBackground().opacity * 100)}%</span
|
||||
>
|
||||
</div>
|
||||
<Slider
|
||||
id="bg-opacity"
|
||||
type="single"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getBackground().opacity}
|
||||
onValueChange={setBackgroundOpacity}
|
||||
/>
|
||||
<span class="font-mono text-xs text-muted-foreground">{getBackground().color}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="bg-fill-toggle"
|
||||
checked={getBackground().fillColor !== null}
|
||||
onCheckedChange={(v) => onFillToggle(!!v)}
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="bg-fade" class="text-xs font-medium">Fade mask</Label>
|
||||
<span class="font-mono text-xs text-muted-foreground">
|
||||
{getBackground().fade === 0 ? 'Off' : `${Math.round(getBackground().fade * 100)}%`}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="bg-fade"
|
||||
type="single"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getBackground().fade}
|
||||
onValueChange={setBackgroundFade}
|
||||
/>
|
||||
<Label for="bg-fill-toggle" class="text-xs font-medium">Custom background fill</Label>
|
||||
<p class="text-[11px] text-muted-foreground">
|
||||
Fades the pattern out toward the edges, like a vignette.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="bg-fill-color"
|
||||
type="color"
|
||||
class="size-7 cursor-pointer rounded-md border border-border bg-transparent p-0.5 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
value={getBackground().fillColor ?? fillDraft}
|
||||
disabled={getBackground().fillColor === null}
|
||||
oninput={onFillColorInput}
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="bg-scale" class="text-xs font-medium">Size</Label>
|
||||
<span class="font-mono text-xs text-muted-foreground"
|
||||
>{Math.round(getBackground().scale * 100)}%</span
|
||||
>
|
||||
</div>
|
||||
<Slider
|
||||
id="bg-scale"
|
||||
type="single"
|
||||
min={0.4}
|
||||
max={3}
|
||||
step={0.05}
|
||||
value={getBackground().scale}
|
||||
onValueChange={setBackgroundScale}
|
||||
disabled={noPattern}
|
||||
/>
|
||||
<span class="font-mono text-xs text-muted-foreground">{getBackground().fillColor ?? 'Auto'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="bg-opacity" class="text-xs font-medium">Opacity</Label>
|
||||
<span class="font-mono text-xs text-muted-foreground">{Math.round(getBackground().opacity * 100)}%</span>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="bg-rotation" class="text-xs font-medium">Rotation</Label>
|
||||
<span class="font-mono text-xs text-muted-foreground"
|
||||
>{Math.round(getBackground().rotation)}°</span
|
||||
>
|
||||
</div>
|
||||
<Slider
|
||||
id="bg-rotation"
|
||||
type="single"
|
||||
min={0}
|
||||
max={359}
|
||||
step={1}
|
||||
value={getBackground().rotation}
|
||||
onValueChange={setBackgroundRotation}
|
||||
disabled={noPattern}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
id="bg-opacity"
|
||||
type="single"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getBackground().opacity}
|
||||
onValueChange={setBackgroundOpacity}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="bg-fade" class="text-xs font-medium">Fade mask</Label>
|
||||
<span class="font-mono text-xs text-muted-foreground">
|
||||
{getBackground().fade === 0 ? 'Off' : `${Math.round(getBackground().fade * 100)}%`}
|
||||
</span>
|
||||
</div>
|
||||
<Slider id="bg-fade" type="single" min={0} max={1} step={0.01} value={getBackground().fade} onValueChange={setBackgroundFade} />
|
||||
<p class="text-[11px] text-muted-foreground">Fades the pattern out toward the edges, like a vignette.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="bg-scale" class="text-xs font-medium">Size</Label>
|
||||
<span class="font-mono text-xs text-muted-foreground">{Math.round(getBackground().scale * 100)}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="bg-scale"
|
||||
type="single"
|
||||
min={0.4}
|
||||
max={3}
|
||||
step={0.05}
|
||||
value={getBackground().scale}
|
||||
onValueChange={setBackgroundScale}
|
||||
disabled={noPattern}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="bg-rotation" class="text-xs font-medium">Rotation</Label>
|
||||
<span class="font-mono text-xs text-muted-foreground">{Math.round(getBackground().rotation)}°</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="bg-rotation"
|
||||
type="single"
|
||||
min={0}
|
||||
max={359}
|
||||
step={1}
|
||||
value={getBackground().rotation}
|
||||
onValueChange={setBackgroundRotation}
|
||||
disabled={noPattern}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="shrink-0 border-t px-3 py-1.5 text-right text-[11px] text-muted-foreground">
|
||||
Oikos {VERSION}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 border-t px-3 py-1.5 text-right text-[11px] text-muted-foreground">Oikos {VERSION}</div>
|
||||
</div>
|
||||
|
||||
@@ -71,24 +71,51 @@
|
||||
return severityFilter === 'all' ? list : list.filter((s) => s.severity === severityFilter)
|
||||
}
|
||||
|
||||
const open = $derived(bySeverity(signals.filter((s) => ['raised', 'acknowledged', 'acting'].includes(s.state))))
|
||||
const open = $derived(
|
||||
bySeverity(signals.filter((s) => ['raised', 'acknowledged', 'acting'].includes(s.state)))
|
||||
)
|
||||
const muted = $derived(bySeverity(signals.filter((s) => s.state === 'muted')))
|
||||
const resolved = $derived(bySeverity(signals.filter((s) => ['resolved', 'failed'].includes(s.state))))
|
||||
const resolved = $derived(
|
||||
bySeverity(signals.filter((s) => ['resolved', 'failed'].includes(s.state)))
|
||||
)
|
||||
|
||||
function makeColumns(showActions: boolean, actingVal: string | null): DataTableColumn<Signal>[] {
|
||||
const base: DataTableColumn<Signal>[] = [
|
||||
{ key: 'target', header: 'Target', class: 'font-mono text-xs', width: '180px', accessor: (s) => s.target ?? '—', truncate: true },
|
||||
{
|
||||
key: 'target',
|
||||
header: 'Target',
|
||||
class: 'font-mono text-xs',
|
||||
width: '180px',
|
||||
accessor: (s) => s.target ?? '—',
|
||||
truncate: true
|
||||
},
|
||||
{ key: 'kind', header: 'Kind', width: '120px' },
|
||||
{ key: 'severity', header: 'Severity', render: 'status-badge', renderProps: { kind: 'severity' }, width: '100px' },
|
||||
{ key: 'state', header: 'State', render: 'status-badge', renderProps: { kind: 'state' }, width: '110px' },
|
||||
{
|
||||
key: 'severity',
|
||||
header: 'Severity',
|
||||
render: 'status-badge',
|
||||
renderProps: { kind: 'severity' },
|
||||
width: '100px'
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
header: 'State',
|
||||
render: 'status-badge',
|
||||
renderProps: { kind: 'state' },
|
||||
width: '110px'
|
||||
},
|
||||
{ key: 'occurrence_count', header: 'Occurrences', width: '100px', align: 'right' },
|
||||
{ key: 'last_seen_at', header: 'Last seen', render: 'date', width: '170px' },
|
||||
{ key: 'last_seen_at', header: 'Last seen', render: 'date', width: '170px' }
|
||||
]
|
||||
if (showActions) {
|
||||
base.push({
|
||||
key: '_actions', header: '', render: SignalActions,
|
||||
key: '_actions',
|
||||
header: '',
|
||||
render: SignalActions,
|
||||
renderProps: { acting: actingVal, onAck: ack, onMute: mute, onResolve: resolve },
|
||||
align: 'right', headerClass: 'text-right', width: '220px'
|
||||
align: 'right',
|
||||
headerClass: 'text-right',
|
||||
width: '220px'
|
||||
})
|
||||
}
|
||||
return base
|
||||
|
||||
Reference in New Issue
Block a user