diff --git a/internal/ui/web/src/stores/sites.test.ts b/internal/ui/web/src/stores/sites.test.ts index 612e98a2..8e8ccaf3 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 9eed419d..fc5c21a2 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 82e97d9f..6e889e4f 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);