Files
tiny-forge/web/src/lib/components/StaleContainerCard.svelte
T
alexei.dolgolyov 90e6e59d9e
Build / build (push) Successful in 10m35s
feat: daemon health panel, brand-rail status chips, user timezone selector
- Health API now surfaces Docker /info + /version (version, platform,
  kernel, container/image counts, storage driver, memory, latency) and
  NPM aggregates (proxy host total, managed-by-Tinyforge count, access
  lists, certificates, endpoint URL).
- Docker/NPM indicators moved out of the sidebar footer and into a
  compact mono-styled rail directly under the Tinyforge brand title,
  with pulse/fault animations and click-to-expand error hints.
- New SystemDaemonsCard on the dashboard: two terminal-styled panels
  (Docker Engine + Proxy) with a running/paused/stopped stacked bar,
  key-value diagnostics, and a total-vs-managed proportion meter on
  the proxy-hosts tile.
- Shared health store so the sidebar and dashboard share a single
  30 s poll instead of duplicating traffic.
- User-facing timezone preference with auto-detect fallback; all
  dates across projects, sites, stacks, settings, backup, event log
  and stale containers now render through \$fmt.date / \$fmt.datetime.
- en/ru translations for both features.
2026-04-23 14:32:30 +03:00

82 lines
2.9 KiB
Svelte

<!--
Card displaying a single stale container with cleanup action.
-->
<script lang="ts">
import type { StaleContainer } from '$lib/types';
import { IconClock, IconTag, IconTrash } from '$lib/components/icons';
import { t } from '$lib/i18n';
import { fmt } from '$lib/format/datetime';
interface Props {
container: StaleContainer;
cleaning?: boolean;
oncleanup: (id: string) => void;
}
const { container, cleaning = false, oncleanup }: Props = $props();
const badgeClass = $derived(
container.days_stale >= 14
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
);
const displayName = $derived(
`${container.project_name}-${container.stage_name}-${container.instance.image_tag}`
);
</script>
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)] transition-colors hover:bg-[var(--surface-card-hover)]">
<!-- Header row -->
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<h3 class="truncate text-sm font-semibold text-[var(--text-primary)]" title={displayName}>
{displayName}
</h3>
<div class="mt-1.5 flex flex-wrap items-center gap-2">
<span class="inline-flex items-center gap-1 rounded-md bg-[var(--color-brand-50)] px-2 py-0.5 text-xs font-medium text-[var(--color-brand-600)]">
{container.project_name}
</span>
<span class="inline-flex items-center gap-1 rounded-md bg-[var(--surface-card-hover)] px-2 py-0.5 text-xs font-medium text-[var(--text-secondary)]">
{container.stage_name}
</span>
</div>
</div>
<!-- Days stale badge -->
<span class="inline-flex flex-shrink-0 items-center gap-1 rounded-full px-2.5 py-1 text-xs font-semibold {badgeClass}">
<IconClock size={12} />
{container.days_stale} {$t('stale.daysStale')}
</span>
</div>
<!-- Details -->
<div class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-[var(--text-secondary)]">
<span class="inline-flex items-center gap-1">
<IconTag size={12} />
{container.instance.image_tag}
</span>
<span class="inline-flex items-center gap-1">
<IconClock size={12} />
{$t('stale.lastAlive')}: {$fmt.shortDate(container.instance.last_alive_at)}
</span>
<span class="rounded bg-[var(--surface-card-hover)] px-1.5 py-0.5 font-mono text-[10px]">
{container.instance.status}
</span>
</div>
<!-- Cleanup button -->
<div class="mt-4 flex justify-end">
<button
type="button"
disabled={cleaning}
onclick={() => oncleanup(container.instance.id)}
class="inline-flex items-center gap-1.5 rounded-lg border border-[var(--color-danger)] px-3 py-1.5 text-xs font-medium text-[var(--color-danger)] transition-colors hover:bg-[var(--color-danger-light)] disabled:opacity-50 active:animate-press"
>
<IconTrash size={14} />
{$t('stale.cleanup')}
</button>
</div>
</div>