Files
tiny-forge/web/src/lib/stores/navCounts.ts
T
alexei.dolgolyov a182a93950
Build / build (push) Successful in 10m29s
feat: nav counter badges, login backdrop, events i18n + misc fixes
Nav & UI polish
- Sidebar nav items show monospace count badges (projects, sites, stacks,
  proxies). Events badge shows error count only, styled red as actionable
- New $lib/stores/navCounts.ts polls all counts in parallel every 60s and
  refreshes on route change so badges track mutations
- Login page gets a dynamic forge backdrop: rotating conic glow, drifting
  embers, dot-grid texture, vignette — all pure CSS, reduced-motion safe
- main element gets scrollbar-gutter: stable so Settings tab switching no
  longer shifts horizontally when content heights differ

Events i18n
- events.source.* dictionary rewritten to match actually-emitted backend
  sources (deploy, static_site, stale_scanner, stale_cleanup, admin);
  dead keys (container, proxy, system) removed
- EventLogFilter.allSources + /events default sources state updated to match
- Localize "{N} total" via events.totalCount in the page hero toolbar

Backend
- Stage API accepts enable_proxy on create/update (defaults to true) so
  proxy registration can be opted out per stage

Concurrency
- api.ts: queued request waiters no longer double-increment the inflight
  counter; releasing a slot hands it off directly

Reactive effects
- project detail / env / volumes pages wrap side-effect calls in untrack()
  to prevent $effect feedback loops when their loaders mutate tracked state
2026-04-22 18:30:34 +03:00

84 lines
2.4 KiB
TypeScript

/**
* Small store that exposes counts for sidebar nav badges.
*
* Values reflect the last successful poll. Individual sources fail
* independently — a failure keeps the previous value and flips `stale` true
* so the UI can dim the badge if desired. The poller is intentionally
* forgiving: if the user is unauthenticated or the backend isn't ready,
* it silently retries on the next tick.
*/
import { writable, type Readable } from 'svelte/store';
import * as api from '$lib/api';
import { isAuthenticated } from '$lib/auth';
export interface NavCounts {
projects: number | null;
sites: number | null;
stacks: number | null;
proxies: number | null;
/** Error-severity events only; dashboard surfaces total separately. */
eventsErrors: number | null;
}
const EMPTY: NavCounts = {
projects: null,
sites: null,
stacks: null,
proxies: null,
eventsErrors: null
};
const store = writable<NavCounts>(EMPTY);
export const navCounts: Readable<NavCounts> = { subscribe: store.subscribe };
let pollTimer: ReturnType<typeof setInterval> | null = null;
let inFlight = false;
async function refreshOnce(): Promise<void> {
if (inFlight || !isAuthenticated()) return;
inFlight = true;
try {
const [projects, sites, stacks, proxies, eventStats] = await Promise.allSettled([
api.listProjects(),
api.listStaticSites(),
api.listStacks(),
api.listProxyRoutes(),
api.fetchEventLogStats()
]);
store.update((prev) => ({
projects: projects.status === 'fulfilled' ? projects.value.length : prev.projects,
sites: sites.status === 'fulfilled' ? sites.value.length : prev.sites,
stacks: stacks.status === 'fulfilled' ? stacks.value.length : prev.stacks,
proxies: proxies.status === 'fulfilled' ? proxies.value.length : prev.proxies,
eventsErrors: eventStats.status === 'fulfilled' ? eventStats.value.error : prev.eventsErrors
}));
} finally {
inFlight = false;
}
}
/**
* Start periodic polling of nav counts. Safe to call repeatedly —
* subsequent calls are no-ops until `stopNavCountsPolling()` is called.
*/
export function startNavCountsPolling(intervalMs = 60_000): void {
if (pollTimer) return;
void refreshOnce();
pollTimer = setInterval(() => void refreshOnce(), intervalMs);
}
export function stopNavCountsPolling(): void {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
/** Trigger an out-of-band refresh (e.g. after a mutation). */
export function refreshNavCounts(): void {
void refreshOnce();
}