From 8af96828e898ea61c4b3f9b8fdb08ba45cf62e0d Mon Sep 17 00:00:00 2001 From: George Dumitrescu Date: Thu, 23 Jul 2026 16:29:49 +0300 Subject: [PATCH] fix(ui): offer the Logs tab when a site's only source is a worker The site detail gated its Logs tab on app logs, a PHP-FPM or container runtime, or a supervised dev server. A site whose sole log source is a background worker, such as a proxy-only host-proxy site running a Stripe listener, matched none of those, so the whole Logs tab was hidden even though the inner logs view would have built a Stripe sub-tab and the stream worked. Whether to offer the tab now runs through a single siteHasLogSources helper that mirrors the sources the logs view actually builds, running or failing workers included, so the tab appears exactly when there is something to show. --- internal/ui/web/src/stores/sites.test.ts | 25 +++++++++++++++++++ internal/ui/web/src/stores/sites.ts | 22 ++++++++++++++++ .../ui/web/src/tabs/sites/SiteDetail.svelte | 9 +++---- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/internal/ui/web/src/stores/sites.test.ts b/internal/ui/web/src/stores/sites.test.ts index 612e98a24..8e8ccaf3c 100644 --- a/internal/ui/web/src/stores/sites.test.ts +++ b/internal/ui/web/src/stores/sites.test.ts @@ -247,4 +247,29 @@ describe('sites store', () => { expect(res.error).toBe('writing temp file: no space'); vi.unstubAllGlobals(); }); + + it('siteHasLogSources sees a proxy-only host site whose only source is a stripe listener', async () => { + const { siteHasLogSources } = await import('./sites'); + const proxyOnly = { + domain: 'gonitro.test', + host_proxy: true, + host_has_dev_server: false, + has_app_logs: false, + uses_php: false, + stripe_running: true + } as unknown as import('./sites').Site; + expect(siteHasLogSources(proxyOnly)).toBe(true); + }); + + it('siteHasLogSources covers running and failing workers, and is false with no source', async () => { + const { siteHasLogSources } = await import('./sites'); + const base = { domain: 'x.test', host_proxy: true } as unknown as import('./sites').Site; + expect(siteHasLogSources(base)).toBe(false); + expect(siteHasLogSources({ ...base, queue_running: true })).toBe(true); + expect(siteHasLogSources({ ...base, schedule_failing: true })).toBe(true); + expect(siteHasLogSources({ ...base, host_has_dev_server: true })).toBe(true); + expect( + siteHasLogSources({ ...base, framework_workers: [{ name: 'vite', running: true }] }) + ).toBe(true); + }); }); diff --git a/internal/ui/web/src/stores/sites.ts b/internal/ui/web/src/stores/sites.ts index 9eed419d7..fc5c21a21 100644 --- a/internal/ui/web/src/stores/sites.ts +++ b/internal/ui/web/src/stores/sites.ts @@ -182,6 +182,28 @@ export function siteWorkerFailing(s: Site): boolean { ); } +// siteHasLogSources reports whether a site exposes any streamable log tab: app +// logs, a PHP-FPM/container runtime, a host dev server, or any running/failing +// worker (queue, horizon, stripe, schedule, reverb, or a framework worker). +// Kept in sync with the tab list SiteLogs builds so the Logs tab is offered iff +// SiteLogs has something to show; a proxy-only host site whose sole source is a +// stripe listener would otherwise never get the tab. +export function siteHasLogSources(s: Site): boolean { + return Boolean( + s.has_app_logs || + s.uses_php || + s.custom_container || + s.host_has_dev_server || + s.queue_running || + s.horizon_running || + s.stripe_running || + s.schedule_running || + s.reverb_running || + (s.framework_workers || []).some((w) => w.running) || + siteWorkerFailing(s) + ); +} + export type WorkerDotColor = 'amber' | 'violet' | 'emerald' | 'sky' | 'indigo'; // Colors for each running worker, used as little status dots in list rows and diff --git a/internal/ui/web/src/tabs/sites/SiteDetail.svelte b/internal/ui/web/src/tabs/sites/SiteDetail.svelte index 82e97d9fb..6e889e4f9 100644 --- a/internal/ui/web/src/tabs/sites/SiteDetail.svelte +++ b/internal/ui/web/src/tabs/sites/SiteDetail.svelte @@ -7,7 +7,7 @@ import SiteEnvTab from './SiteEnvTab.svelte'; import SiteNginxModal from '../../modals/SiteNginxModal.svelte'; import SiteDebugTab from '$tabs/sites/SiteDebugTab.svelte'; - import { resumeSite, loadSites, activeWorktreeDomain, type Site } from '$stores/sites'; + import { resumeSite, loadSites, activeWorktreeDomain, siteHasLogSources, type Site } from '$stores/sites'; import { routeRest, goToTab } from '$stores/route'; import { m } from '../../paraglide/messages.js'; @@ -44,10 +44,9 @@ const canDumps = $derived(Boolean(site.uses_php)); const canEnv = $derived(Boolean(site.has_env)); // Logs get their own tab in the resource layout rather than living under the - // overview. Offer it whenever the site exposes any log source. - const canLogs = $derived( - Boolean(site.has_app_logs || site.uses_php || site.custom_container || site.host_has_dev_server) - ); + // overview. Offer it whenever the site exposes any log source, including a + // worker-only source like a stripe listener on a proxy-only host site. + const canLogs = $derived(siteHasLogSources(site)); // A lone Overview tab can't be switched to anything, so don't render the tab // row at all when no other tab is available (e.g. static sites). const hasExtraTabs = $derived(canLogs || canEnv || canTinker || canDumps);