diff --git a/.changeset/admin-health.md b/.changeset/admin-health.md new file mode 100644 index 0000000..ef40f94 --- /dev/null +++ b/.changeset/admin-health.md @@ -0,0 +1,7 @@ +--- +'@goto/web': minor +'@goto/worker': minor +'@goto/cli': minor +--- + +Add a read-only Health page with safe active settings, report data cutoff and a manual bounded check of the existing worker health endpoint. Use the configured worker base URL without adding a startup dependency. Make the worker check and its result prominent with a bordered action button, checking indicator and status banner. Warn when enabled analytics reports are unavailable or more than 75 minutes old. diff --git a/apps/web/e2e/auth.e2e.ts b/apps/web/e2e/auth.e2e.ts index 56e9538..d700eb8 100644 --- a/apps/web/e2e/auth.e2e.ts +++ b/apps/web/e2e/auth.e2e.ts @@ -3,7 +3,7 @@ import { expect, test } from './support/test.ts'; test.use({ withWorker: true }); -test('shared credentials protect overview, domain and link pages and sign-out removes access', async ({ +test('shared credentials protect admin pages and sign-out removes access', async ({ page, appServer }) => { @@ -140,7 +140,7 @@ test('shared credentials protect overview, domain and link pages and sign-out re await expect(browsers.getByRole('rowheader', { name: 'Other', exact: true })).toBeVisible(); const overviewLink = page .getByRole('navigation', { name: 'Administration', exact: true }) - .getByRole('link'); + .getByRole('link', { name: 'Goto', exact: true }); await expect(overviewLink).toHaveAttribute('href', '/admin?period=all'); await expect(page.getByRole('link', { name: 'Overview', exact: true })).toHaveCount(0); await expect(graph).toHaveAttribute('aria-busy', 'false'); @@ -293,6 +293,24 @@ test('shared credentials protect overview, domain and link pages and sign-out re ); await expect(visitsValue).toHaveText('2'); + await page + .getByRole('navigation', { name: 'Administration', exact: true }) + .getByRole('link', { name: 'Health', exact: true }) + .click(); + await expect(page).toHaveURL(`${appServer.origin}/admin/health`); + await expect(page.getByRole('heading', { name: 'Health', exact: true })).toBeVisible(); + await expect(page.getByRole('region', { name: 'Application', exact: true })).toContainText( + 'Shared credentials' + ); + await expect( + page.getByRole('region', { name: 'Analytics', exact: true }).getByRole('alert') + ).toHaveCount(0); + const workerHealth = page.getByRole('region', { name: 'Worker', exact: true }); + await expect(workerHealth.getByRole('status')).toHaveText('Not checked'); + await workerHealth.getByRole('button', { name: 'Test worker', exact: true }).click(); + await expect(workerHealth.getByRole('alert')).toContainText('Worker is reachable.'); + await expect(workerHealth.locator('time')).toBeVisible(); + const html = await page.content(); await expect(html).not.toContain('e2e-only-password'); await page.getByRole('button', { name: 'Sign out', exact: true }).click(); @@ -305,7 +323,7 @@ test('shared credentials protect overview, domain and link pages and sign-out re test.describe('analytics disabled', () => { test.use({ withWorker: false, retentionMonths: 0 }); - test('overview remains inventory-only without analytics controls or columns', async ({ + test('disabled analytics keeps inventory and settings usable without traffic controls', async ({ page, appServer }) => { @@ -351,5 +369,18 @@ test.describe('analytics disabled', () => { await expect(page.getByRole('region', { name: 'Visits over time', exact: true })).toHaveCount( 0 ); + await page + .getByRole('navigation', { name: 'Administration', exact: true }) + .getByRole('link', { name: 'Health', exact: true }) + .click(); + await expect( + page.getByRole('region', { name: 'Analytics', exact: true }).locator('dd') + ).toHaveText(['Disabled', 'Disabled', 'Disabled']); + await expect( + page.getByRole('region', { name: 'Analytics', exact: true }).getByRole('alert') + ).toHaveCount(0); + await expect( + page.getByRole('region', { name: 'Worker', exact: true }).getByRole('status') + ).toHaveText('Not checked'); }); }); diff --git a/apps/web/e2e/server.mjs b/apps/web/e2e/server.mjs index d819b2b..b493f27 100644 --- a/apps/web/e2e/server.mjs +++ b/apps/web/e2e/server.mjs @@ -24,7 +24,7 @@ await new Promise((resolve, reject) => ); process.env.PORT = String(address.port); const configuration = { - url: { app: `http://localhost:${address.port}` }, + url: { app: `http://localhost:${address.port}`, worker: 'http://goto-worker:9000' }, auth: { credentials: { username: 'example', password: 'e2e-only-password' } }, analytics: { retention_months: Number(process.env.GOTO_E2E_RETENTION_MONTHS ?? '3') }, domains: { @@ -118,4 +118,6 @@ async function startWorker() { }); }); if (ready.type !== 'ready') throw new Error('Invalid worker readiness'); + configuration.url.worker = `http://127.0.0.1:${ready.port}`; + await writeFile(join(runtimeDirectory, 'config/goto.yaml'), JSON.stringify(configuration)); } diff --git a/apps/web/src/lib/components/admin/AdminNav.svelte b/apps/web/src/lib/components/admin/AdminNav.svelte index 15824fb..2278c75 100644 --- a/apps/web/src/lib/components/admin/AdminNav.svelte +++ b/apps/web/src/lib/components/admin/AdminNav.svelte @@ -52,6 +52,10 @@ {title} + Health