# SPA Auth Redirect Loop — Diagnostic Workflow

## Symptoms

User visits `https://domain/` and the page flickers/refreshes repeatedly, alternating between two URLs (e.g., `index.html` and `login.html`). The browser URL bar visibly changes back and forth.

## Root Causes (check in order)

1. **Host header mismatch** — VPS `proxy_set_header` sends a Host the home nginx doesn't recognize. Request hits the default server block instead of the correct one. PocketBase/Firebase API calls fail or hit wrong context → auth state flips → redirect loop.

2. **Stale auth token in localStorage** — PocketBase JS SDK reads an expired token from `localStorage`, initially considers it valid (JWT not yet expired), but the first API call returns 401 → authStore clears → redirect fires. On the next page load, the token is read from localStorage again → cycle repeats.

3. **Bounce-back doesn't clear the stale token** — Even after the architecture is fixed (one-way redirects), the protected pages' `onAuthStateChanged(null)` handlers redirect to the login page WITHOUT clearing the stale `pocketbase_auth` from localStorage. The login page's inline check (`if(localStorage.getItem('pocketbase_auth'))`) fires again → redirects back → infinite loop. **Fix**: add `localStorage.removeItem('pocketbase_auth')` immediately before `window.location.href = 'index.html'` in every protected page's auth-failure path. See `references/auth-redirect-loop.md` in the `shop-pro-quote` skill for the full file list.

4. **Browser-cached 302 redirect** — after fixing the server, the user still sees the old loop because their browser cached the 302 redirect from before the fix. `curl -skI` returns 200, but the browser shows a redirect.

## Diagnostic Steps

### 0. FULL-TEXT GREP FIRST (do not skip this)

**Before looking at individual files or nav links, grep EVERY JS file for all redirect sources at once.** The redirect can be buried anywhere — an auth listener, a guard function, a keyboard shortcut handler, an inline script. Nav links are the most visible but often NOT the root cause.

```bash
# Find ALL redirect sources in one sweep
grep -rn "window\.location\|location\.replace\|location\.href\|meta.*refresh" --include="*.js" --include="*.html" .
```

Missing a hidden `onAuthStateChanged` redirect is the #1 diagnostic failure in SPA redirect loops. The fix may look deceptively simple (rename files, update nav links) but the real loop lives in auth listeners that fire on state changes.

### 1. Check what the server actually returns

```bash
curl -skI https://domain/
# HTTP/1.1 200 OK → server is fine, problem is client-side
# HTTP/1.1 302 + Location: login.html → server still has old redirect
```

### 2. Check if the target file exists

```bash
curl -skI https://domain/login.html
# 404 → file was deleted, redirect is cached
# 200 → file still exists, redirect is real
```

### 3. Check VPS ↔ Home Host header alignment

```bash
# On VPS: what Host header goes to home?
grep "proxy_set_header Host" /etc/nginx/sites-enabled/<service>

# On home: what server_name does nginx expect?
grep "server_name" /etc/nginx/sites-enabled/* /etc/nginx/sites-available/*
```

### 4. Check for stale PocketBase auth token

Open browser DevTools → Application → Local Storage → look for `pocketbase_auth` key. If present, the SDK will try to use it on page load. If the token is expired server-side but not client-side, this creates the loop.

## Fix Patterns

### Pattern A: Server alias (preferred)

Add the VPS domain as a server_name alias on the home nginx config:

```bash
sudo sed -i 's/server_name grajmedia.duckdns.org;/server_name grajmedia.duckdns.org <vps-domain>;/' /etc/nginx/sites-enabled/<config>
sudo nginx -t && sudo systemctl reload nginx
```

Allows `Host: <vps-domain>` to reach the correct server block without changing the VPS config.

### Pattern B: Fix Host header on VPS

```nginx
proxy_set_header Host grajmedia.duckdns.org;
```

Matches what the home nginx expects. Only affects one service.

### Pattern C: File rename — swap index and login

When the root `index.html` is the dashboard (auth-guarded) and `login.html` is separate, visiting `/` triggers: load dashboard → no auth → redirect to login.html → has auth → redirect to index.html → loop.

**Fix**: Make `index.html` the login page, rename dashboard to `dashboard.html`, update ALL references:

```bash
# 1. Backup
cp login.html _login.html.bak && cp index.html _index.html.bak

# 2. Swap
cp login.html index.html && cp _index.html.bak dashboard.html && rm login.html

# 3. Update JS references (grep first to find them all)
grep -rn "login\.html\|index\.html" --include="*.js" --include="*.html" .

# 4. Bulk-fix auth redirects: login.html → index.html
grep -rl "'login\.html'" --include="*.js" . | xargs sed -i "s/'login\.html'/'index.html'/g"

# 5. Bulk-fix nav links: index.html → dashboard.html  
grep -rl 'href="index\.html"' --include="*.html" . | xargs sed -i 's|href="index\.html"|href="dashboard.html"|g'

# 6. Fix login-redirect targets: index.html? → dashboard.html?
sed -i "s/'index\.html?/'dashboard.html?/g" login.js  # after-login redirect
```

Full file list from a real fix (13 files, 28 references):
- `dashboard.js`, `login.js`, `main.js`, `appointments.js`, `repair-orders.js`, `customers.js`
- `shared/header-functionality.js`
- `dashboard.html`, `appointments.html`, `repair-orders.html`, `customers.html`
- `index.html` (the new login page — inline redirect scripts)

### Pattern D: Stub page cache-breaker

When the browser has a cached 302 redirect to a now-deleted file, put a minimal stub back that redirects to the correct page with no-cache headers:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <meta http-equiv="refresh" content="0; url=index.html">
    <title>Redirecting…</title>
</head>
<body><p>Redirecting… <a href="index.html">click here</a> if not redirected.</p></body>
</html>
```

This breaks the cached redirect: the browser loads the stub, which instantly takes them to the correct page. After a few visits the cache clears naturally.

### Pattern E: Remove auto-redirect from login page's onAuthStateChanged

When BOTH the login page and the dashboard have `onAuthStateChanged` listeners that redirect to each other, any auth-state instability creates a loop:

```
dashboard.js: onAuthStateChanged(null) → redirect to login.html
login.js: onAuthStateChanged(user) → redirect to index.html
→ LOOP
```

**Fix**: Patch the login page so `onAuthStateChanged` only manipulates UI (show/hide elements), never redirects. Only explicit login/signup button click handlers should redirect. Use an inline `<script>` at the top of the login page body to handle the "already logged in" redirect instead:

```html
<script>if(localStorage.getItem('pocketbase_auth')){location.replace('dashboard.html?cb='+Date.now())}</script>
```

This localStorage check fires before any modules load, has no auth-state dependency, and never causes a loop. It delegates the "already logged in" redirect to a simple token presence check, while login.js's `onAuthStateChanged` becomes UI-only.

## Worked Example: ShopProQuote

**Domain**: `grajmedia.duckdns.org` (VPS proxied to home:443)  
**Backend**: PocketBase at `127.0.0.1:8091`  
**Problem**: Infinite redirect between `index.html` (dashboard) and `login.html`, later between `login.html` → `dashboard.html` → `index.html`

**Root causes found (all three were active simultaneously)**:
1. `shopproquote.graj-media.com` VPS config sent `Host: graj-media.com` but home nginx only had `server_name grajmedia.duckdns.org` → Host header mismatch → request landed on wrong server block
2. `login.js`'s `onAuthStateChanged` auto-redirected to `dashboard.html` on auth state — coupled with dashboard.js's redirect back to `index.html` on null, any PocketBase auth instability created a loop. Also fired on `onAuthStateChanged` registration which could race with the initial state.
3. After all server-side fixes, browser had cached 302 redirects from the old setup

**Fix applied (order matters — do all three)**:
1. File swap (Pattern C): `index.html` → login page, `dashboard.html` → dashboard, 28 references across 13 files updated
2. Patch `login.js` (Pattern E): `onAuthStateChanged` → UI-only (shows/hides form vs loading), no redirect. Only explicit login/signup + inline localStorage check redirect.
3. Server alias (Pattern A): added `graj-media.com shopproquote.graj-media.com` to home shopproquote nginx `server_name`
4. Stub page (Pattern D): created minimal `login.html` with no-cache redirect to `index.html` to break cached browser redirects, then deleted after cache cleared
