fix: harden security, fix concurrency bugs, and address review findings
Build / build (push) Successful in 11m42s
Build / build (push) Successful in 11m42s
Security: - rate limit /api/webhook routes per-IP and cap concurrent site syncs - global SSE connection cap (256) with new sse_gate - validate ?tail= and cap JSON log responses at 4 MiB - strip ANSI/CSI/OSC and control bytes from streamed log lines - redact webhook secret from request log middleware - scrub host details from /api/health for non-admin viewers - drop container_id from /api/system/stats/top for non-admins - generate webhook secrets via crypto/rand; require >=32 chars on insert - verify iid path consistency in streamContainerLogs - LimitReader on site webhook body; reject malformed non-empty bodies Concurrency / correctness: - stats collector: Stop() no longer hangs without Start(), semaphore acquired in parent loop so ctx cancellation short-circuits the queue, in-flight tick cancellable via shared base context, zero-ts guard - webhook handler: replace fire-and-forget goroutine with WaitGroup-tracked workers + Drain() wired into graceful shutdown - $derived(() => ...) mis-idiom fixed in ContainerStats / InstanceCard / ProjectCard (returned function instead of value) - SystemResourcesCard: rename `window` and `t` locals to avoid shadowing globalThis.window and the i18n `t` import Quality / performance: - replace O(n^2) insertion sort with sort.Slice in stats top - runMigrations only swallows duplicate-column / already-exists errors - PruneStatsSamplesBefore wrapped in a transaction - collapse N+1 in unusedImageStats / pruneImages to one ListAllInstances pass; surface DB errors instead of silently treating them as inactive - run Docker Info + DiskUsage in parallel via errgroup - container log SSE emits `: ping` heartbeat every 20 s - imageMatches case-insensitive on registry host (RFC behaviour) - log warning on invalid stage tag pattern instead of silent skip - reject malformed non-empty site webhook payloads Frontend / i18n: - shared formatBytes utility replaces three local copies - statsInterval store drives dynamic "no samples / collection disabled" copy across ContainerStats and SystemResourcesCard - top consumers row now shows owner_name (project/stage or site name) - drop seven `as any` casts on the Settings type; add cloudflare_api_token write-only field - move "Service status", "Docker daemon", "Docker unreachable", "Proxy unreachable", "reachable", and "Docker daemon is not reachable." strings into en/ru i18n bundles
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
import { t } from '$lib/i18n';
|
||||
import { navCounts, startNavCountsPolling, stopNavCountsPolling, refreshNavCounts } from '$lib/stores/navCounts';
|
||||
import { health, startHealthPolling, stopHealthPolling, refreshHealth } from '$lib/stores/health';
|
||||
import { effectiveTimezone, formatOffsetLabel } from '$lib/stores/timezone';
|
||||
import { fmt } from '$lib/format/datetime';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
@@ -54,15 +56,17 @@
|
||||
const proxyHealth = $derived($health.proxy);
|
||||
const healthChecked = $derived($health.checked);
|
||||
|
||||
// Live UTC forge clock (refreshes every second). A small thing, but it makes
|
||||
// Live forge clock (refreshes every second). A small thing, but it makes
|
||||
// the sidebar feel alive and reinforces the "control room" aesthetic.
|
||||
let nowUtc = $state('');
|
||||
// Renders in the user's chosen timezone via the shared formatter.
|
||||
let nowTick = $state(new Date());
|
||||
let clockTimer: ReturnType<typeof setInterval> | null = null;
|
||||
function tickClock() {
|
||||
const d = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
nowUtc = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
||||
nowTick = new Date();
|
||||
}
|
||||
const clockDisplay = $derived($fmt.clock(nowTick));
|
||||
const clockOffset = $derived(formatOffsetLabel($effectiveTimezone, nowTick));
|
||||
const clockTitle = $derived(`${$effectiveTimezone.replace(/_/g, ' ')} · ${clockOffset}`);
|
||||
|
||||
// Keyboard quick-nav: "g" then a letter jumps to a section (vim-style).
|
||||
// g+d → dashboard, g+p → projects, g+s → sites, g+k → stacks, g+x → deploy,
|
||||
@@ -194,14 +198,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Daemon health chips (Docker + proxy provider) -->
|
||||
<div class="brand-rail" aria-label="Service status">
|
||||
<div class="brand-rail" aria-label={$t('layout.serviceStatus')}>
|
||||
{#if healthChecked}
|
||||
<button
|
||||
type="button"
|
||||
class="chip"
|
||||
class:chip-live={dockerConnected}
|
||||
class:chip-down={!dockerConnected}
|
||||
title={dockerConnected ? `Docker daemon · ${dockerHealth?.version ?? 'reachable'}` : dockerHealth?.error ?? 'Docker unreachable'}
|
||||
title={dockerConnected
|
||||
? `${$t('daemons.docker')} · ${dockerHealth?.version ?? $t('daemons.reachable')}`
|
||||
: dockerHealth?.error ?? $t('daemons.dockerUnreachable')}
|
||||
onclick={() => { if (!dockerConnected) hintsExpanded = !hintsExpanded; }}
|
||||
>
|
||||
<span class="chip-dot" aria-hidden="true"></span>
|
||||
@@ -218,7 +224,9 @@
|
||||
class="chip"
|
||||
class:chip-live={proxyConnected}
|
||||
class:chip-down={!proxyConnected}
|
||||
title={proxyConnected ? `${proxyProviderName.toUpperCase()} · ${proxyHealth.latency_ms ?? '?'} ms` : proxyHealth.error ?? 'Proxy unreachable'}
|
||||
title={proxyConnected
|
||||
? `${proxyProviderName.toUpperCase()} · ${proxyHealth.latency_ms ?? '?'} ms`
|
||||
: proxyHealth.error ?? $t('daemons.proxyUnreachable')}
|
||||
onclick={() => { if (!proxyConnected) proxyHintsExpanded = !proxyHintsExpanded; }}
|
||||
>
|
||||
<span class="chip-dot" aria-hidden="true"></span>
|
||||
@@ -323,10 +331,10 @@
|
||||
</div>
|
||||
<div class="forge-footline">
|
||||
<span class="forge-footline-version">{$t('app.name')} {$t('app.version')}</span>
|
||||
<span class="forge-footline-clock" title="UTC">
|
||||
<span class="forge-footline-clock" title={clockTitle}>
|
||||
<span class="clock-dot"></span>
|
||||
<span class="clock-time">{nowUtc || '--:--:--'}</span>
|
||||
<span class="clock-suffix">UTC</span>
|
||||
<span class="clock-time">{clockDisplay}</span>
|
||||
<span class="clock-suffix">{clockOffset}</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="forge-nav-hint" title="Press 'g' then a letter to jump between sections">
|
||||
@@ -599,7 +607,7 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ── Sidebar footline (version + live UTC clock) ───────────── */
|
||||
/* ── Sidebar footline (version + live timezone-aware clock) ───────────── */
|
||||
.forge-footline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user