Skip to content

fix(webui): transient 503s kick authenticated users to the login page - #119

Open
SoulKyu wants to merge 3 commits into
mainfrom
looper/118-fix-webui-transient-503s-kick-56a05f831eb16338
Open

fix(webui): transient 503s kick authenticated users to the login page#119
SoulKyu wants to merge 3 commits into
mainfrom
looper/118-fix-webui-transient-503s-kick-56a05f831eb16338

Conversation

@SoulKyu

@SoulKyu SoulKyu commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • handleAuthError() in dashboard_core.templ redirected to /login on both 401 and 503. 503 is never an auth signal — it's returned for transient infra states (webui cache not yet warmed after a deploy, backend temporarily down) — so every dashboard fetch going through this helper could bounce an authenticated user to the login page.
  • Restricted the redirect condition to response.status === 401, matching the global fetch interceptor (installFetchInterceptor) which already only redirects on 401.
  • Regenerated dashboard_core_templ.go via make webui-templates.

Every dashboard call site already routes through the single handleAuthError() in dashboard_core.templ, so this one change covers cache warm-up, backend restarts, comments, acks, history, and presets without touching each call site.

Why

  • Every webui deploy logged out every open dashboard: during cache warm-up, the first loadDashboardData() poll got a 503 and the page self-navigated to /login.
  • Backend blips hit users mid-incident: a backend restart/overload during an on-call incident could 503 an alert-modal action and throw the engineer to a login form; if the backend was still down, re-login also failed, reading as a wrong-password error.
  • Filter state in the URL was lost on the bounce.

Closes #118

Test plan

  • go build ./...
  • go vet ./internal/webui/...
  • go test ./internal/webui/...
  • make webui-templates — confirmed only dashboard_core.templ / dashboard_core_templ.go changed, diff limited to dropping || response.status === 503

🔁 Powered by Looper · runner=worker · agent=claude-code · An autonomous AI dev team for your GitHub repos.

@SoulKyu SoulKyu added the looper:review Looper: PR awaiting agent review label Jul 26, 2026

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Reviewed the fix in internal/webui/templates/scripts/dashboard_core.templ: handleAuthError() now redirects to /login only on response.status === 401, dropping the || response.status === 503 branch. I verified this makes the helper consistent with installFetchInterceptor(), which already only redirects on 401, and confirmed via grep that handleAuthError() is the single choke point used by every dashboard call site (actions, data, modal, resolved-alerts, utilities mixins) — so this one-line change covers all of them as claimed. The regenerated dashboard_core_templ.go matches the .templ source (only the embedded script string changed), and go build ./internal/webui/... succeeds cleanly. Nice, well-scoped fix for a real production annoyance — thanks for tracking down the root cause and keeping the diff minimal!

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

@SoulKyu

SoulKyu commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

🧪 QA Report

Env: make docker-build-all + docker compose up -d (webui image rebuilt from this branch — verified the old || response.status === 503 string is absent from the shipped binary and the new comment is present). Real browser flow driven with Playwright/Chromium, user qauser logged in through /login.

Criterion Result Evidence
Restarting the webui (cache warm-up) does not navigate the page to /login; data loads once the cache is ready Dashboard open on /dashboard, then docker compose stop webui && start webui; polled /api/v1/dashboard/data every 50 ms across the whole window → only 0 (conn refused) then 200; page stayed on /dashboard (0 navigations) and alerts reloaded 11 → 13. Note: no 503 is observable here — handlers.SetAlertCache (internal/webui/router.go:96) runs before the HTTP listener binds, so "Dashboard cache not ready" is unreachable over HTTP. The 503 branch was therefore also exercised directly: handleAuthError({status:503})false, location.href unchanged, and a genuine 503 observed in the console during the backend-down run did not itself navigate away.
With the backend stopped, opening the alert modal / adding a comment fails without navigating away; the dashboard keeps live Alertmanager data docker compose stop backend, then clicked the first alert row: modal opened (/dashboard/alert/1c71ead8e527da1d9c2b5e18e56c9e9e), then 401 /api/v1/dashboard/alert/1c71ead8… + 401 /api/v1/dashboard/alerts/bulk-colors → console Session expired, redirecting to login → page landed on /login (login form screenshot, dashboard data gone).
A genuine 401 still redirects to /login Fresh login on /dashboard, cleared the session cookie, triggered a dashboard fetch → 401 /api/v1/dashboard/data?page=1&limit=25 → immediate navigation to /login.

Why criterion 2 fails (for the fixer)

The 503 removal is correct but insufficient: with the backend down the protected routes answer 401, not 503.

  • internal/webui/middleware/auth.go:24backendClient.IsConnected() still reports true when the backend container is stopped (gRPC keeps the client in idle/connecting), so the 503 "Authentication service unavailable" branch is skipped.
  • internal/webui/middleware/auth.go:38-42ValidateSession(sessionID) then errors, and the handler responds 401 "Invalid or expired session" and calls ClearSession(c).
  • The global fetch interceptor (dashboard_core.templ:267) redirects on 401 → the on-call engineer is thrown to the login form exactly as described in fix(webui): transient 503s kick authenticated users to the login page #118, and because the cookie was wiped they must log in again even after the backend returns.

Reproducible without a browser: with the backend stopped, curl -b <session-cookie> /api/v1/auth/me401 {"error":"Invalid or expired session"}; 200 again once the backend is back (same cookie), so the 401 is caused solely by the backend being down.

Exploratory: on a healthy stack nothing adjacent regressed — login → 12 alerts rendered, alert modal opens and deep-links (/dashboard/alert/<fp>), search filter narrows 12 → 1 with the URL state preserved, validateSession() returns true, no stray navigation.

Verdict: FAIL — 1 of 3 acceptance criteria fails. Fixer summoned.

@SoulKyu SoulKyu added the qa:failed QA agent: at least one criterion failed label Jul 26, 2026
@SoulKyu
SoulKyu force-pushed the looper/118-fix-webui-transient-503s-kick-56a05f831eb16338 branch from bc738d6 to b134c8c Compare July 26, 2026 15:17
@SoulKyu

SoulKyu commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

🔀 Rebase (conflicts resolved):

Rebased onto origin/main (now includes #104 SSE ack-state fix, #98 filter presets, #105 re-fire ack, #113 admin-only resolved cleanup).

Conflict: internal/webui/templates/scripts/dashboard_core_templ.go — generated file only. The source .templ (dashboard_core.templ) auto-merged cleanly.

Resolution: discarded the conflicted generated hunk and re-ran make webui-templates (templ v0.3.906, matching go.mod) so the generated Go is derived from the merged .templ. No hand-editing of *_templ.go.

Semantic outcome preserved: the auth guard in handleResponse now redirects to /login on 401 only; 503 is treated as a transient infra state (cache warm-up / backend restart) and no longer logs the user out. Upstream changes in the same file (SSE alert-update handling) are untouched.

Validation: go build ./... ✅ · go vet ./...

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Thanks for tracking this down — restricting handleAuthError() to response.status === 401 correctly stops the cache-warm-up 503 from bouncing users to /login, and the regenerated dashboard_core_templ.go matches the .templ diff.

However, per the QA run already recorded on this PR and confirmed below by re-reading the auth path, the change doesn't close the second scenario the PR body itself calls out ("Backend blips hit users mid-incident"). See the inline comment for details and a concrete fix direction. (Posted as COMMENT because GitHub blocks self-authored REQUEST_CHANGES reviews; this is a blocking finding and should not be merged as-is.)

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

Comment thread internal/webui/templates/scripts/dashboard_core.templ
@SoulKyu

SoulKyu commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

Looper fixer round complete3899f87

  • ✅ Review comment on internal/webui/templates/scripts/dashboard_core.templ:254 (@SoulKyu) — thread
    • Added client.IsUnavailableError() (internal/webui/client/backend_client.go) checking gRPC codes.Unavailable/DeadlineExceeded, and RequireAuth() (internal/webui/middleware/auth.go:38-44) now returns 503 without clearing the session when ValidateSession fails due to backend unreachability, reserving 401+ClearSession for genuine invalid sessions.

🔁 Powered by Looper · runner=fixer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

@SoulKyu

SoulKyu commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

🧪 QA Report

Re-test after the conflict rebase (head b134c8c). Env: make docker-build-webui docker-build-backend from this branch + docker compose up -d. Verified the shipped binary carries the fix — strings /app/webui | grep -c "response.status === 503"0, and the new 503 is a transient comment → 1. Browser flows driven with Playwright/Chromium 1.62 as qauser logged in through the real /login form.

Criterion Result Evidence
With a logged-in dashboard open, restarting the webui (cache warm-up window) does not navigate the page to /login; data loads once the cache is ready Dashboard open on /dashboard (12 alerts), then docker compose restart webui; webui healthy again after 324 ms. Over the next 20 s: 0 main-frame navigations, URL stayed /dashboard, alert rows 12 → 12, SSE dropped (ERR_INCOMPLETE_CHUNKED_ENCODING"SSE error, falling back to polling") and 4 × 200 /api/v1/dashboard/incremental followed. The 503 branch was also probed directly in the page: handleAuthError({status:503}){returned:false, hrefUnchanged:true}. (No HTTP 503 is observable during warm-up: handlers.SetAlertCache at internal/webui/router.go:96 runs before the listener binds, so "Dashboard cache not ready" is unreachable — the restart window is connection-refused, then 200.)
With the backend stopped, opening the alert modal / adding a comment fails without navigating away; the dashboard keeps live Alertmanager data docker compose stop backend, then clicked the first alert row: modal deep-linked to /dashboard/alert/1aea82c9…, then 401 /api/v1/dashboard/alert/1aea82c9… → console Session expired, redirecting to loginNAV → /login (screenshot: login form behind the "Maintenance in progress" modal, dashboard and filter state gone). Worse, no user action is even required: in a separate run the page was left completely idle after stopping the backend and bounced on its own at ~30 s via 401 /api/v1/dashboard/alerts/bulk-colors (/dashboard at t=15 s → /login at t=25 s).
A genuine 401 still redirects to /login Fresh login on /dashboard, cleared the session cookie via CDP, then triggered fetch('/api/v1/dashboard/data?page=1&limit=25')401 → console Session expired, redirecting to login → NAV → /login. The interceptor's never-resolving promise was observed too (the in-page await never settled).

Why criterion 2 still fails (for the fixer)

Unchanged from the previous head — the rebase did not touch this. With the backend down, the protected routes answer 401, not 503, so dropping 503 from handleAuthError() never gets a chance to help:

  • internal/webui/middleware/auth.go:24 — the 503 "Authentication service unavailable" guard is skipped because backendClient.IsConnected() still reports true when the backend container is stopped (the gRPC client sits in idle/connecting).
  • internal/webui/middleware/auth.go:38-42ValidateSession(sessionID) then errors and the handler responds 401 "Invalid or expired session" and calls ClearSession(c), so the browser's cookie is wiped as well.
  • internal/webui/templates/scripts/dashboard_core.templ:267 — the global fetch interceptor redirects on 401, and it is not gated by handleAuthError(). The on-call engineer is thrown to the login form exactly as described in fix(webui): transient 503s kick authenticated users to the login page #118, and because the cookie was cleared they must re-authenticate even after the backend returns.

Reproducible without a browser (same session cookie throughout):

docker compose stop backend
curl -b cookie /api/v1/auth/me   → 401 {"error":"Invalid or expired session"}   (x3, 3s apart)
curl -b cookie /api/v1/dashboard/data → 401
docker compose start backend
curl -b cookie /api/v1/auth/me   → 200 {"user":{"username":"qauser"}}

The 401 is caused solely by the backend being unreachable. Fixing this means making RequireAuth distinguish "backend unreachable" from "session invalid" (a real connectivity check instead of IsConnected(), or mapping a gRPC Unavailable from ValidateSession to 503 without ClearSession) — note the MaintenanceModal component already exists to cover that state.

Exploratory: no adjacent regression on a healthy stack — login renders alerts, search HighCPUUsage narrows 7 → 1 with ?search=HighCPUUsage preserved in the URL and clearing restores the full list, alert modal opens and deep-links, Comments tab renders, GET /api/v1/auth/me → 200 throughout, zero 4xx/5xx after login.

Verdict: FAIL — 1 of 3 acceptance criteria fails. Fixer summoned.

@SoulKyu

SoulKyu commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

🔀 Rebased on main (clean, build green). — rebaser

@SoulKyu
SoulKyu force-pushed the looper/118-fix-webui-transient-503s-kick-56a05f831eb16338 branch from 3899f87 to d120840 Compare July 27, 2026 05:26

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Verified the fix at head d120840. The remaining blocking gap from the earlier review round — protected routes answering 401 (with ClearSession) instead of 503 when the backend is merely unreachable — is now properly closed:

  • internal/webui/client/backend_client.go: new IsUnavailableError() unwraps the gRPC status and returns true only for codes.Unavailable / codes.DeadlineExceeded, leaving genuine session-rejection errors (e.g. codes.Unauthenticated, plain non-gRPC errors) to fall through as real auth failures.
  • internal/webui/middleware/auth.go RequireAuth(): when ValidateSession fails with an unavailable-backend error, it now responds 503 and returns without calling ClearSession, reserving 401 + session-clear for actual invalid/expired sessions.
  • internal/webui/templates/scripts/dashboard_core.templ: handleAuthError() redirects to /login only on 401, matching the unchanged installFetchInterceptor() — confirmed via grep that both are now consistent (response.status === 401 in both places, no remaining 503 redirect condition).
  • The regenerated dashboard_core_templ.go diff is a single-line change matching the .templ source, and the new TestIsUnavailableError table test covers nil, Unavailable, DeadlineExceeded, a plain non-gRPC error, and Unauthenticated, exercising exactly the classification the fix relies on.

Locally re-verified: go build ./..., go vet ./internal/webui/..., and go test ./internal/webui/client/... ./internal/webui/middleware/... all pass. This closes both scenarios from #118 (cache warm-up and backend-down mid-incident) without touching unrelated call sites. Solid follow-through on the QA feedback — thanks for tracking the root cause all the way through the gRPC error path instead of settling for the surface-level frontend fix.

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

@SoulKyu

SoulKyu commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

🧪 QA Report

Env: make test full rebuild from this branch (webui/backend/alertmanager images rebuilt, docker-compose up). Verified the running webui actually serves the patched helper (Redirect to login only on real auth failure present in the served /dashboard payload). Browser flows driven with Playwright/Chromium against http://localhost:8081 as a real logged-in user (qa119).

Criterion Result Evidence
Restarting the webui (cache warm-up window) does not navigate the page to /login; data loads once the cache is ready Dashboard open + logged in, docker restart notificator-webui, watched for 50s: navs=[], still on /dashboard, session cookie intact, rows repopulated (12 → 11 live alerts). A second run hammered /api/v1/dashboard/data through the whole restart (200ms interval, feeding each real Response into handleAuthError()): only a transient network error during the down window, then 200s — no redirect. Note: this env's cache is warm before the listener binds, so a literal warm-up 503 never appeared; the 503-does-not-redirect path is proven for real below.
With the backend stopped, opening the alert modal / adding a comment fails without navigating away; the dashboard keeps live Alertmanager data docker stop notificator-backend with the dashboard open. Real server 503s observed on 503 /api/v1/dashboard/data, 503 /api/v1/auth/me, 503 /api/v1/dashboard/alert/eb22f546…, 503 /api/v1/dashboard/alert/eb22f546…/comments, 503 /api/v1/dashboard/alerts/bulk-colors. Result: navs=[], url stayed /dashboard, session cookie intact, 11 Alertmanager rows still rendered behind the pre-existing "Maintenance in progress" overlay. addComment() failed without navigating. Notably /api/v1/auth/me returned 503, not 401 — the IsUnavailableError() guard in RequireAuth() keeps the session instead of clearing it. docker start notificator-backend → dashboard recovered on its own, still logged in.
A genuine 401 still redirects to /login Revoked the live session server-side (POST /api/v1/auth/logout with the browser's own cookie → 200), then triggered loadDashboardData() from the page. Backend answered 401 → page navigated to http://localhost:8081/login.

Exploratory: happy path with everything up is unaffected — POST /alert/:fp/comments → 200, /dashboard/data → 200, /auth/me → 200, no stray navigation; go test ./internal/webui/... green including the new TestIsUnavailableError. The full-screen "Maintenance in progress" overlay that blocks row clicks while the backend is down is pre-existing on main (components/MaintenanceModal.templ), not a regression from this PR.

Verdict: PASS

@SoulKyu SoulKyu added qa:passed QA agent: all acceptance criteria verified and removed qa:failed QA agent: at least one criterion failed labels Jul 27, 2026
@SoulKyu

SoulKyu commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

🔀 Rebased on main (clean, build green). — rebaser

@SoulKyu
SoulKyu force-pushed the looper/118-fix-webui-transient-503s-kick-56a05f831eb16338 branch from d120840 to 1e22b58 Compare July 27, 2026 06:16

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Verified at head 1e22b58. The webui-relevant files in this PR (internal/webui/client/backend_client.go, internal/webui/client/backend_client_test.go, internal/webui/middleware/auth.go, internal/webui/templates/scripts/dashboard_core.templ + regenerated _templ.go) are byte-identical to the commit already reviewed and approved as clean in the prior round (d120840) — the only change since then is an unrelated devtools/factory-tui documentation update outside this PR's scope, confirmed via gh pr diff --name-only against the PR's actual file list.

Re-confirmed independently:

  • client.IsUnavailableError() correctly unwraps the gRPC status and only returns true for codes.Unavailable/codes.DeadlineExceeded, leaving codes.Unauthenticated and plain non-gRPC errors (e.g. invalid session from ValidateSession) to fall through as real auth failures.
  • middleware.RequireAuth() now returns 503 without clearing the session when the backend is merely unreachable, and only clears the session + returns 401 for genuine invalid/expired sessions.
  • handleAuthError() in dashboard_core.templ redirects to /login only on 401, matching the unchanged installFetchInterceptor().
  • TestIsUnavailableError covers nil, Unavailable, DeadlineExceeded, a plain error, and Unauthenticated.
  • go build ./internal/webui/..., go vet ./internal/webui/..., and go test ./internal/webui/client/... ./internal/webui/middleware/... all pass at this head.

This closes both scenarios from #118 (cache warm-up and backend-down mid-incident). Thanks for the thorough follow-through tracking this all the way to the gRPC error classification instead of stopping at the frontend symptom!

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

@SoulKyu

SoulKyu commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

🔀 Rebased on main (clean, build green). — rebaser

@SoulKyu
SoulKyu force-pushed the looper/118-fix-webui-transient-503s-kick-56a05f831eb16338 branch from 1e22b58 to 5bedaa1 Compare July 27, 2026 06:46

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Verified at head 5bedaa1. This head carries no code changes versus the previously-approved head (1e22b58) — a diff between the two shows zero differences in internal/webui/client/backend_client.go, internal/webui/client/backend_client_test.go, internal/webui/middleware/auth.go, internal/webui/templates/scripts/dashboard_core.templ, and the regenerated dashboard_core_templ.go; only the commit SHAs changed (rebase), not the content.

Re-confirmed independently at this head:

  • client.IsUnavailableError() unwraps the gRPC status and returns true only for codes.Unavailable/codes.DeadlineExceeded, leaving codes.Unauthenticated and plain non-gRPC errors to fall through as genuine auth failures.
  • middleware.RequireAuth() now responds 503 without clearing the session when IsUnavailableError is true, and only clears the session + returns 401 for real invalid/expired sessions.
  • handleAuthError() in dashboard_core.templ redirects to /login only on 401, matching the unchanged installFetchInterceptor().
  • TestIsUnavailableError covers nil, Unavailable, DeadlineExceeded, a plain error, and Unauthenticated.
  • go build ./internal/webui/..., go vet ./internal/webui/..., and go test ./internal/webui/client/... ./internal/webui/middleware/... all pass at this head.

This closes both scenarios from #118 (cache warm-up and backend-down mid-incident) with no regressions introduced by the rebase. Thanks for the clean, well-tested follow-through — nothing further needed here.

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

@SoulKyu

SoulKyu commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

🔀 Rebased on main (clean, build green). — rebaser

@SoulKyu
SoulKyu force-pushed the looper/118-fix-webui-transient-503s-kick-56a05f831eb16338 branch from 5bedaa1 to ac9ebaa Compare July 27, 2026 07:20

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Verified at head ac9ebaa. This head is a rebase of the previously-approved fix (content-identical to head 1e22b58/5bedaa1c reviewed earlier in this thread) onto the latest main, with no code changes since then.

Independently re-confirmed the full fix:

  • internal/webui/client/backend_client.go: IsUnavailableError() unwraps the gRPC status and returns true only for codes.Unavailable/codes.DeadlineExceeded, leaving codes.Unauthenticated and plain non-gRPC errors (e.g. the invalid session error from ValidateSession) to fall through as real auth failures.
  • internal/webui/middleware/auth.go RequireAuth(): when ValidateSession fails with an unavailable-backend error, it responds 503 and returns without calling ClearSession, reserving 401 + session-clear for genuinely invalid/expired sessions.
  • internal/webui/templates/scripts/dashboard_core.templ handleAuthError(): redirects to /login only on 401, now consistent with the unchanged installFetchInterceptor().
  • The regenerated dashboard_core_templ.go diff is a single-line change matching the .templ source.
  • TestIsUnavailableError covers nil, Unavailable, DeadlineExceeded, a plain error, and Unauthenticated.

Locally re-ran go build ./..., go vet ./internal/webui/..., and go test ./internal/webui/client/... ./internal/webui/middleware/... — all pass. This closes both scenarios called out in #118 (cache warm-up 503s and backend blips mid-incident) without touching unrelated call sites. Solid, well-scoped fix — thanks for tracking the root cause through the gRPC error classification instead of stopping at the frontend symptom.

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

@SoulKyu
SoulKyu force-pushed the looper/118-fix-webui-transient-503s-kick-56a05f831eb16338 branch from ac9ebaa to 7228fd7 Compare July 30, 2026 12:48
@SoulKyu

SoulKyu commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

🔀 Rebase (conflicts resolved):

Rebased onto origin/main (d42d5da). Both commits replayed; new head 7228fd7.

One conflict: internal/webui/templates/scripts/dashboard_core_templ.go

Not a semantic conflict — the generated _templ.go emits the whole dashboard <script> as a single giant one-line string literal, so any two edits to that script collide on the same line, no matter how unrelated.

The two sides were independent:

dashboard_core.templ auto-merged cleanly with both changes, so the resolution was to regenerate rather than hand-merge the artifact, per repo convention (edit .templ, then make webui-templates — never hand-edit *_templ.go). templ v0.3.906 matches the version in main’s generated header, so there is no generator churn: the regenerated file differs from origin/main by exactly one line, our intended 401-only check.

The second commit (IsUnavailableError + RequireAuth 503 handling) applied with no conflict.

Verification

  • go build ./... — pass
  • go vet ./... — pass
  • go test ./internal/webui/client/... — 6 passed
  • Final diff vs main is 5 files / +63 −3, matching the pre-rebase scope (nothing dropped, nothing extra).

Nothing was pulled in from main and nothing of main’s was overwritten. QA notified to re-test.

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Verified the handleAuthError() frontend change and the IsUnavailableError() + RequireAuth() server-side fix — both correctly stop the 401/503 misclassification for fetch/XHR calls (cache warm-up and alert-modal actions during a backend blip, the two scenarios named in this PR), and go build/go vet/go test ./internal/webui/client/... ./internal/webui/middleware/... all pass at this head. One follow-up worth tracking: two sibling middleware functions share the same pre-fix pattern this PR just corrected in RequireAuth() — see the inline comment for where and why.

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

Comment thread internal/webui/middleware/auth.go
@SoulKyu

SoulKyu commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Looper fixer round completeed4a1ce

  • ✅ Review comment on internal/webui/middleware/auth.go:45 (@SoulKyu) — thread
    • Added client.IsUnavailableError(err) guards to RedirectIfNotAuth() and OptionalAuth() in internal/webui/middleware/auth.go: RedirectIfNotAuth() now returns 503 without clearing the session or redirecting to /login, and OptionalAuth() now skips clearing the session on unavailable-backend errors, matching RequireAuth().

🔁 Powered by Looper · runner=fixer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

@SoulKyu
SoulKyu force-pushed the looper/118-fix-webui-transient-503s-kick-56a05f831eb16338 branch from ed4a1ce to 9c172c1 Compare July 30, 2026 14:43

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Thanks for closing out the earlier review rounds — IsUnavailableError() plus the RequireAuth(), OptionalAuth(), and RedirectIfNotAuth() guards, and the handleAuthError() 401-only redirect, all correctly stop the false-positive auth failures for dashboard fetches and protected-page navigation.

One gap remains that reproduces the exact bug class this PR (and its title, "don't clear session on backend-unavailable in page middleware") sets out to fix. This is a blocking finding despite the COMMENT state below — GitHub rejects self-authored REQUEST_CHANGES reviews on this repo, so the review state alone does not reflect severity here; do not merge as-is.

internal/webui/middleware/auth.go, RedirectIfAuth() (lines 132-146) — this is the fourth middleware function that calls ValidateSession(), and it still has the pre-fix pattern the other three had before this PR:

sessionID := GetSessionID(c)
if sessionID != "" {
    // Check if session is still valid
    _, err := am.backendClient.ValidateSession(sessionID)
    if err == nil {
        // User is already authenticated, redirect away
        c.Redirect(http.StatusFound, redirectTo)
        c.Abort()
        return
    } else {
        // Clear invalid session
        ClearSession(c)
    }
}

Unlike RequireAuth()/OptionalAuth()/RedirectIfNotAuth(), this branch never calls client.IsUnavailableError(err) — every non-nil error from ValidateSession(), including a transient codes.Unavailable/codes.DeadlineExceeded from a backend blip, falls into the else and clears the session cookie via ClearSession(c).

RedirectIfAuth("/dashboard") is wired up in internal/webui/router.go:362 as middleware on the public /login and /register pages. Concrete failure scenario: a user with a perfectly valid session hits a backend blip (the exact "mid-incident" scenario from this PR's own description) while the backend is temporarily Unavailable/timing out. If they land on /login during that window — e.g. after RedirectIfNotAuth 503s them off /dashboard and they retry via /login, or via a stale bookmark/tab — RedirectIfAuth clears their still-valid session cookie before rendering the login page. Once the backend recovers, they're logged out for no reason, even though their session was never actually invalid.

Suggested fix: mirror the pattern already used three times in this file — check client.IsUnavailableError(err) first and, if true, skip ClearSession(c) (e.g. just fall through to c.Next() without redirecting or clearing, same as the OptionalAuth() treatment), only clearing the session in the genuine invalid/expired case.

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

SoulKyu added 3 commits July 30, 2026 18:46
handleAuthError() treated HTTP 503 the same as an expired session (401)
and navigated the whole page to /login. But 503 is never an auth signal
in this app — it's emitted for transient infra states: webui cache not
yet warmed after a restart, or the backend being temporarily down. Every
webui deploy was logging out open dashboards, and backend blips during
an incident bounced on-call engineers to a login form.

Restrict the redirect to response.status === 401, matching the global
fetch interceptor which already got this right. Call sites keep their
existing error handling (retain current data, retry next poll cycle).

Generated-By: looper 0.11.0 (runner=worker, agent=claude-code)
…hable

RequireAuth() treated every ValidateSession() error as an invalid
session, clearing it and returning 401. During a backend outage the
gRPC call fails with a transport error (codes.Unavailable/DeadlineExceeded),
not a real auth rejection, so on-call engineers got bounced to /login
and then couldn't log back in while the backend was still down.

Add client.IsUnavailableError() to distinguish transport failures from
genuine invalid-session responses, and return 503 without clearing the
session for the former.

Generated-By: looper 0.11.0 (runner=fixer, agent=claude-code)
…ware

RedirectIfNotAuth() and OptionalAuth() cleared the session cookie on
any ValidateSession error, including transient backend-unavailable
errors, reproducing the same login-bounce bug RequireAuth() already
guards against.

Generated-By: looper 0.11.1 (runner=fixer, agent=claude-code)
@SoulKyu
SoulKyu force-pushed the looper/118-fix-webui-transient-503s-kick-56a05f831eb16338 branch from 9c172c1 to 9ba08b4 Compare July 30, 2026 16:46
@SoulKyu

SoulKyu commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

🔀 Rebase (conflicts resolved): rebased onto origin/main (up to 697651d).

Single conflict: internal/webui/templates/scripts/dashboard_core_templ.go

  • The source .templ (dashboard_core.templ) auto-merged cleanly — only the generated Go file conflicted, because main picked up unrelated regenerations (silence-matcher work in feat(webui): editable silence matchers in the silence modal #139) that reshuffled the embedded script string.
  • Per repo convention, the generated file was not hand-edited: it was reset to the origin/main version and regenerated with make webui-templates (templ v0.3.906) on top of the merged .templ.

Semantics preserved: the fix in handleResponse still redirects to /login on 401 only; 503 is no longer treated as an auth signal (transient infra state — cache warm-up, backend restart). The other two commits (don't 401+clear-session when backend unreachable, don't clear session on backend-unavailable in page middleware) replayed without conflict.

Verification: go build ./... ✅ · go vet ./... ✅ · templ generate is idempotent (clean tree after re-run).

QA agent notified for re-test.

@SoulKyu SoulKyu left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulKyu Verified the frontend (handleAuthError()) and the three fixed backend guards (RequireAuth(), OptionalAuth(), RedirectIfNotAuth()) at head 9ba08b4 — each now checks client.IsUnavailableError(err) before clearing the session, correctly separating a transient backend outage from a genuinely invalid session.

One gap remains — this is a blocking finding despite the COMMENT state below (GitHub rejects self-authored REQUEST_CHANGES on this repo, so the review state doesn't reflect severity here; do not merge as-is).

internal/webui/middleware/auth.go, RedirectIfAuth() (lines 124-148) still clears a valid session on a transient backend outage — the exact bug class this PR's commit title ("don't clear session on backend-unavailable in page middleware") claims to close. This function isn't touched by this PR's diff, so it can't be inline-anchored; noting it here instead.

This PR's head commit fixes OptionalAuth() and RedirectIfNotAuth() by checking client.IsUnavailableError(err) before calling ClearSession(c). RedirectIfAuth() is the fourth ValidateSession() call site in this same file and still has the pre-fix pattern:

sessionID := GetSessionID(c)
if sessionID != "" {
    _, err := am.backendClient.ValidateSession(sessionID)
    if err == nil {
        c.Redirect(http.StatusFound, redirectTo)
        c.Abort()
        return
    } else {
        // Clear invalid session
        ClearSession(c)
    }
}
c.Next()

Every non-nil error — including a transient codes.Unavailable/codes.DeadlineExceeded from a backend blip — falls into the else branch and clears the cookie via ClearSession(c), with no IsUnavailableError check.

RedirectIfAuth("/dashboard") is wired up in internal/webui/router.go:362 as middleware on the public /login and /register pages. Concrete failure scenario: a user with a valid session hits /login (e.g. a stale bookmark/tab, or after RedirectIfNotAuth 503s them off /dashboard during a blip and they retry via /login) while the backend is transiently Unavailable. RedirectIfAuth clears their still-valid session cookie before rendering the login page. Once the backend recovers, they're logged out for no reason, even though the session was never actually invalid — reproducing the same class of bug this PR sets out to fix, just through the one call site the fix skipped.

This exact function and line range were already flagged as the blocking gap in an earlier review round on this same PR thread; internal/webui/middleware/auth.go is byte-identical between that earlier flagged head and the current head (9ba08b4), so the gap was carried forward unaddressed rather than fixed.

Suggested fix: mirror the pattern used in the three sibling functions — check client.IsUnavailableError(err) first and, if true, skip ClearSession(c) and just fall through to c.Next() (matching the OptionalAuth() treatment: don't redirect away and don't clear the session), only clearing the session in the genuine invalid/expired case.

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

looper:review Looper: PR awaiting agent review qa:passed QA agent: all acceptance criteria verified

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(webui): transient 503s kick authenticated users to the login page

1 participant