Skip to content

fix: resolve duplicate 401 toasts, stale PWA cache, annotation dates, and dev proxy#180

Merged
berntpopp merged 3 commits intomasterfrom
fix/frontend-bugs-177-178-179
Feb 9, 2026
Merged

fix: resolve duplicate 401 toasts, stale PWA cache, annotation dates, and dev proxy#180
berntpopp merged 3 commits intomasterfrom
fix/frontend-bugs-177-178-179

Conversation

@berntpopp
Copy link
Owner

Summary

Fixes three frontend/infrastructure bugs plus two issues discovered during testing:

  • Duplicate 401 error toasts shown when authentication token expires #179 — Duplicate 401 error toasts: Centralized 401 handling in an axios response interceptor that clears auth state, redirects to /Login, and returns a never-resolving promise to suppress all downstream .catch() toasts. Removed per-component toast calls from AppNavbar. Improved useToast error extraction for Axios error objects.
  • App shows stale version after deployment until hard reload (Ctrl+Shift+R) #178 — Stale app version after deployment: Added cache-control headers to prod.conf (sw.js/manifest never cached, hashed assets immutable 1yr, HTML never cached). Replaced legacy register-service-worker with vite-plugin-pwa prompt mode and a ReloadPrompt.vue component that shows an update banner with periodic 60-min checks.
  • ManageAnnotations: Ontology and HGNC 'Last update' dates are stale after successful updates #177 — Stale annotation dates: annotation_dates endpoint now queries get_job_history() for the most recent successful completion per operation type, falling back to file metadata for fresh installs.
  • Bearer null bug (found during testing): Only set Authorization header when a token exists — prevents 401s on public endpoints for logged-out users.
  • Vite dev proxy (found during testing): Set Host: localhost header in proxy config so Traefik correctly routes API requests from inside the Docker container.

Closes #177, closes #178, closes #179

Test plan

  • ESLint: 0 errors, 0 warnings
  • TypeScript type-check: clean
  • Prettier: all files formatted
  • Vitest: 230/230 tests passed
  • Production build: succeeds with PWA sw.js generated
  • Playwright E2E (port 80 — Traefik): home page loads with real API data, zero toasts, zero console errors
  • Playwright E2E (port 5173 — Vite dev proxy): home page loads with real API data, zero toasts, zero console errors
  • 401 flow: fake expired token → single redirect to /Login, zero error toasts, localStorage cleared
  • Public page as logged-out user: no spurious 401 redirects

…tes (#177, #178, #179)

- Centralize 401 handling in axios interceptor to prevent duplicate error
  toasts on JWT expiry (#179)
- Add nginx cache-control headers, replace legacy register-service-worker
  with vite-plugin-pwa prompt mode and ReloadPrompt component (#178)
- Use job history timestamps for annotation dates instead of file
  metadata, with file-based fallback for fresh installs (#177)
Setting `Bearer null` when no token caused the API to return 401 on
public endpoints, triggering the interceptor redirect for logged-out
users. Also clear the default header on 401 logout.
Inside Docker the Vite proxy targets http://traefik:80, but Traefik
routes by Host header (localhost|sysndd.dbmr.unibe.ch). Without the
override, requests arrived with Host: traefik and got 404.
@berntpopp berntpopp requested a review from Copilot February 9, 2026 13:05
@berntpopp berntpopp merged commit 296dcfa into master Feb 9, 2026
11 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses several user-facing reliability issues across the Vue app, PWA update behavior, nginx caching, and the admin annotation dates API by centralizing auth error handling, improving cache/update flows, and deriving annotation “last updated” timestamps from job history.

Changes:

  • Centralize Axios configuration + 401 handling, and improve toast message extraction for Axios error objects.
  • Switch PWA registration to prompt mode with an in-app reload banner; adjust production nginx cache headers to prevent stale deployments.
  • Update /api/admin/annotation_dates to prefer timestamps from job history over file metadata; adjust Vite dev proxy host routing.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
app/vite.config.ts Switch PWA to prompt and adjust dev proxy headers for Traefik routing.
app/tsconfig.json Add PWA Vue type declarations.
app/src/registerServiceWorker.js Remove legacy Vue CLI-era service worker registration.
app/src/plugins/axios.ts New centralized Axios setup (defaults + response interceptor).
app/src/plugins/axios.js Remove old Vue plugin-based axios wrapper.
app/src/main.ts Register Axios onto Vue global properties / DI and remove legacy SW import.
app/src/composables/useToast.ts Improve error message extraction for non-string error shapes.
app/src/components/ReloadPrompt.vue Add UI prompt for PWA updates + periodic update checks.
app/src/components/AppNavbar.vue Remove per-component toast on auth check failure (avoid duplicate toasts).
app/src/App.vue Mount the PWA reload prompt globally.
app/package.json Drop register-service-worker; add workbox-window.
app/package-lock.json Lockfile updates for dependency changes.
app/docker/nginx/prod.conf Add cache-control headers for SW/manifest/assets/html.
api/endpoints/admin_endpoints.R Prefer job-history timestamps (fallback to file metadata) for annotation dates.
Files not reviewed (1)
  • app/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +51 to +55
body =
(respData?.message as string) ||
(respData?.error as string) ||
(msg.message as string) ||
String(message);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The message extraction uses type assertions like (respData?.message as string), which can assign non-string truthy values (objects) into body and potentially break toast rendering or show [object Object]. Guard with typeof ... === 'string' (and optionally fall back to JSON.stringify/String) before using these fields.

Suggested change
body =
(respData?.message as string) ||
(respData?.error as string) ||
(msg.message as string) ||
String(message);
const dataMessage =
respData && typeof (respData as any).message === 'string'
? ((respData as any).message as string)
: undefined;
const dataError =
respData && typeof (respData as any).error === 'string'
? ((respData as any).error as string)
: undefined;
const msgMessage =
typeof (msg as any).message === 'string'
? ((msg as any).message as string)
: undefined;
body = dataMessage ?? dataError ?? msgMessage ?? String(message);

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +22
const { needRefresh, updateServiceWorker } = useRegisterSW({
onRegisteredSW(_swUrl: string, registration: ServiceWorkerRegistration | undefined) {
if (registration) {
setInterval(() => {
registration.update();
}, intervalMS);
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The update polling interval isn’t cleaned up and registration.update()’s returned promise is ignored. Store the interval id and clear it on unmount, and handle/catch update errors (or check navigator.onLine) to avoid unhandled rejections and noisy logs over long-running sessions.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +41
// Return a never-resolving promise to suppress downstream .catch() toasts
return new Promise(() => {});
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The 401 interceptor returns a never-resolving promise. Any caller awaiting an axios request will hang forever (and any finally/loading cleanup will never run), which can leave the UI stuck and accumulate pending promises during repeated 401s. Prefer letting the promise settle (e.g., reject with a marked/cancelled error that toast logic ignores, or resolve/reject after redirect) so downstream code can clean up deterministically.

Suggested change
// Return a never-resolving promise to suppress downstream .catch() toasts
return new Promise(() => {});
// Mark this error as a handled 401 so downstream logic can choose to ignore toasts,
// but still let the promise settle so callers' finally/cleanup logic can run.
(error as any).isHandled401 = true;
return Promise.reject(error);

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +137
// but the routing rule expects 'localhost', so override it explicitly.
headers: { Host: 'localhost' },
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The proxy forces Host: localhost for all /api requests. This will break when VITE_API_URL points to a non-localhost backend (or when developing against a remote environment), since the upstream will receive an unexpected Host header. Make the Host override conditional (e.g., only when targeting Traefik in Docker / when VITE_MODE=docker) or derive it from the target URL.

Suggested change
// but the routing rule expects 'localhost', so override it explicitly.
headers: { Host: 'localhost' },
// but the routing rule expects 'localhost', so override it explicitly
// only when running in the Docker/Traefik environment.
headers: process.env.VITE_MODE === 'docker' ? { Host: 'localhost' } : undefined,

Copilot uses AI. Check for mistakes.
Comment on lines +689 to +705
# Helper: get most recent completed_at for matching operations
get_last_successful_run <- function(operations) {
history <- tryCatch(get_job_history(100), error = function(e) {
data.frame(
operation = character(0),
status = character(0),
completed_at = character(0)
)
})
if (nrow(history) == 0) return(NA)
matches <- history[
history$operation %in% operations & history$status == "completed",
]
if (nrow(matches) == 0) return(NA)
# get_job_history returns newest first
matches$completed_at[1]
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

get_last_successful_run() calls get_job_history(100) every time it’s invoked, so this endpoint queries job history 3x per request. Fetch job history once and reuse it for each operation filter to avoid repeated work and reduce latency.

Copilot uses AI. Check for mistakes.
Comment on lines +703 to +704
# get_job_history returns newest first
matches$completed_at[1]
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

get_last_successful_run() assumes the first matching row is the most recent successful completion, but get_job_history() is sorted by submitted_at, not completed_at. If jobs overlap or complete out of order, this can report the wrong “Last” date. Filter to completed jobs and select the max by completed_at (or explicitly sort matches by completed_at desc) before returning.

Suggested change
# get_job_history returns newest first
matches$completed_at[1]
# Explicitly select the latest completion time in case history is not
# ordered by completed_at
latest_idx <- order(matches$completed_at, decreasing = TRUE)[1]
matches$completed_at[latest_idx]

Copilot uses AI. Check for mistakes.
berntpopp added a commit that referenced this pull request Feb 10, 2026
- useToast: replace unsafe `as string` casts with typeof guards, skip
  __handled401 errors to prevent duplicate toasts
- ReloadPrompt: clean up setInterval on unmount, add network/installing
  guards and try/catch per vite-plugin-pwa best practices
- axios: replace never-resolving promise with marked rejection so
  .finally() cleanup blocks still run
- vite proxy: only set Host:localhost header when using default Traefik
  target, skip when VITE_API_URL overrides it
- annotation_dates: fetch job history once instead of 3x, sort by
  completed_at desc instead of submitted_at
berntpopp added a commit that referenced this pull request Feb 10, 2026
fix: address Copilot review comments from PR #180
@berntpopp berntpopp deleted the fix/frontend-bugs-177-178-179 branch February 10, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

Comments