---
name: frontend-debugging
title: Frontend Debugging Patterns
description: Non-trivial debugging patterns for static HTML/CSS/JS web apps — JS-overrides-HTML, missing script imports, cache-busting, and common pitfalls.
---

# Frontend Debugging Patterns

## When Static HTML Edits Don't Take Effect

**Check if JS dynamically generates the HTML at runtime.** This is the #1 cause of "I edited the file but nothing changed."

- Search the JS files for `innerHTML`, `createElement`, `textContent =`, or DOM `appendChild` calls that target the same element IDs/classes
- The DOM inspector in DevTools shows live state — if it differs from your source file, JS is overwriting it
- Fix: edit the JS template string or the JS rendering function, not just the static HTML

**Special case: color/readability fixes.** When a user reports "yellow on yellow" or "hard to read" and you fix the static HTML but they still see the old colors, the culprit is almost certainly a JS `innerHTML` template string that dynamically generates the colored elements. Search for `.innerHTML` in JS files and look for `text-yellow-*`, `bg-yellow-*`, or similar color classes — that's where the real fix lives.

## Missing Script/Module Imports

A page element exists in HTML but has no event handler → the JS file that wires it up probably isn't loaded.

- Check the `<script>` tags at the bottom of the page — compare against a page where the feature works
- Common misses: shared module files like `shared/header-functionality.js`, `shared/dropdown-manager.js`
- Also check that the JS actually targets the right element ID (typo in getElementById)

## When Inline onclick Exists but the Button Still Fails

An inline `onclick` handler is present on the element and the logic looks correct, but clicking does nothing. Two silent causes:

**Cause A — The target element is missing from this page.** The onclick calls `getElementById('some-modal')` but that modal div only exists on other pages. The `if(m)` guard silently returns — no error, no visual feedback. Fix: add the missing modal HTML to the page. Audit all `getElementById` targets across pages with:
```bash
for page in *.html; do
  grep -oP "getElementById\('([^']+)'\)" "$page" | \
    sed "s/getElementById('//;s/')//" | sort -u | while read id; do
    grep -q "id=\"$id\"" "$page" || echo "MISSING: $id in $page"
  done
done
```

**Cause B — File permissions blocking the module chain.** A shared dependency (e.g., `pocketbase.js`, `shared/sanitize.js`) has owner-only permissions (600). nginx runs as `www-data` and returns HTTP 403. The browser can't load it → every ES module that imports it silently fails → no JS handlers attach. Even inline onclick on other elements may work, but module-reliant features die. Fix: `find . -name "*.js" -exec chmod 644 {} +`, then `curl -skI https://site.com/key-file.js | head -1` to verify 200.

**Cause C — Module handler double-toggle conflict.** An inline `onclick` and a JS module handler (e.g., from `shared/header-functionality.js`) both attach to the same button and BOTH toggle the same `hidden` class. The inline fires first → toggles hidden OFF (visible). The module fires second → toggles hidden ON (hidden again). Net result: nothing happens. **This is especially common with hamburger menus and user dropdowns** after inline onclick fallbacks are added to pages that also load the module-driven header.

Fix: wrap the inline onclick in an IIFE that calls `e.stopImmediatePropagation()` to prevent the module handler from also firing:

```html
<!-- BEFORE (double-toggle, no visible effect) -->
<button onclick="var n=document.getElementById('mobile-navigation');if(n){n.classList.toggle('hidden')}">

<!-- AFTER (inline fires first, module suppressed) -->
<button onclick="(function(e){e.stopImmediatePropagation();var n=document.getElementById('mobile-navigation');if(!n)return;var hidden=n.classList.toggle('hidden');n.style.display=hidden?'none':'block';n.style.zIndex='10000'})(event)">
```

Key details:
- Use `stopImmediatePropagation()` (not just `stopPropagation`) — the module handler is on the same element
- Wrap in IIFE `(function(e){...})(event)` so `stopImmediatePropagation` receives the actual event object
- Explicitly set `style.display` (not just class toggle) for defense-in-depth against CSS specificity
- `index.html` (dashboard) often does NOT load `header-functionality.js` — no conflict, no fix needed
- Pages that DO load the module (repair-orders, appointments, customers) all need the fix

## Class-Based Dark Mode: When Fixes Don't Stick

**Symptom:** You add `class="dark"` to `<html>`, or fix a dark mode toggle handler, but something still switches the site to light mode.

**Root cause:** Multiple JS files independently control the `.dark` class. You patched one but missed others hiding in settings sync code.

**The fix requires patching EVERY instance of `classList.toggle('dark', ...)` and `classList.remove('dark')` across ALL JS files — not just the obvious UI toggle handlers:**

```bash
grep -rn "classList.*toggle.*dark\|classList.*remove.*dark" *.js **/*.js
```

Common hidden locations:
- Settings sync code in `dashboard.js`, `customers.js`, `settings.js`
- Remote settings listeners that apply dark mode after a server sync
- Mobile-specific toggle handlers in shared modules

**Verification after patching:**
1. Hard refresh — should load dark
2. Click every UI element that previously controlled dark mode — no change
3. Open settings modals — any dark mode toggle should be inoperable
4. Search localStorage in DevTools — `darkMode` should always be `'true'`

**Pitfall:** Don't rely solely on CSS `!important` overrides to force dark colors (e.g. `.bg-white { background: #1f2937 !important; }`). Tailwind CDN generates styles dynamically that can compete with your overrides. Patching the JS at the source is more reliable.

## Cache-Busting for Static Servers

Python's `http.server` and similar dev servers serve files fresh each request, but **browsers cache aggressively**, especially on mobile.

Solutions in order of effectiveness:
1. Open page in incognito/private tab
2. Hard refresh (Ctrl+Shift+R / Cmd+Shift+R)
3. Add cache-busting meta tags to `<head>`:
   ```html
   <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">
   ```
4. Restart the server to force fresh TCP connections

## Mobile Responsiveness Pitfalls

### Hamburger Menu Not Working
Check four things in order:
1. The hamburger button exists in HTML (search for `mobile-menu-btn`)
2. The target `#mobile-navigation` panel exists in the HTML
3. **Module conflict**: `shared/header-functionality.js` adds its own click handler that also toggles `hidden` — the inline onclick and module handler cancel each other out. Add `stopImmediatePropagation()` to the inline onclick (see Cause C above)
4. The JS file that handles the click is loaded (check `<script>` tags)

### Hidden-by-Default Mobile Elements
Mobile nav panels are usually `hidden` by default and toggled by JS. If the panel never appears:
- Check the CSS class includes `hidden` in the initial state
- Verify the JS toggle uses `classList.toggle('hidden')` and isn't broken by JS errors earlier in execution

## CSS Consistency Across Pages

## Escaped Characters in Inline Scripts Breaking Selectors

**Symptom:** An inline `<script>` block has a `document.addEventListener('click', ...)` handler that checks for close buttons via `[aria-label="Close"]`, but clicking the (X) button does nothing. The button clearly has `aria-label="Close"` in the HTML. The same handler works on other pages. Console shows no errors.

**Root cause:** Double-escaping in inline `<script>` blocks. When the HTML source contains `\\\"` (backslash-backslash-quote), the browser's HTML parser resolves it to a literal backslash-quote sequence (`\"`) in the JavaScript string. A CSS selector like `[aria-label=\"Close\"]` then matches the literal attribute value `"Close"` (with quote characters) instead of the actual HTML attribute value `Close` (without quotes).

**The broken pattern:**
```javascript
// In the HTML source, this looks like: [aria-label=\\\"Close\\\"]
// After HTML parsing, JS sees: [aria-label=\"Close\"]
// This selector matches: aria-label='"Close"' (quotes in value) — NEVER correct
var closeBtn = e.target.closest('[aria-label=\\\"Close\\\"]');
```

**The fix — use normal quotes:**
```javascript
// In the HTML source, this looks like: [aria-label="Close"]
// After HTML parsing, JS sees: [aria-label="Close"]
// This selector matches: aria-label="Close" — CORRECT
var closeBtn = e.target.closest('[aria-label="Close"]');
```

**Alternative fix — use single quotes in JS:**
```javascript
var closeBtn = e.target.closest("[aria-label='Close']");
```

**Diagnosis:** Use `read_file` on the HTML and look at the raw text. If you see `\\\"` sequences inside `<script>` tags, the selectors are silently broken. The terminal command `sed -n 'LINENOp' file.html | cat -A` will show the raw bytes — `\\` displays as literal backslashes.

**Verification after fixing:** `curl -sk https://site.com/page.html | grep 'aria-label.*Close'` should show `aria-label="Close"` (no backslashes between label and value).

When making the same header/nav change across multiple pages:
- Do ONE page first, get user approval, then apply the same diff pattern to the others
- Search for the exact HTML structure in each file — they may have drifted (different padding classes, different wrapper divs)
- For shared CSS classes (`.header-card`, `.nav-link`), editing the CSS file once fixes all pages

## IIFE Capture-Phase Handler + data-close-modal on Save Buttons

**Symptom:** On a page with the Universal Modal Manager IIFE, clicking the Save button in a settings modal closes the modal but changes don't persist. The same modal works perfectly on other pages. Close/Cancel buttons work fine — only Save is broken.

**Root cause:** `ensureSiteSettingsCloseAttributes()` (or dashboard.js) sets `data-close-modal` on the Save button. The IIFE capture-phase handler matches `[data-close-modal]`, calls `stopPropagation()`, and closes the modal — before any save handler fires.

**Diagnosis:** Compare a working page against the broken page:
- Working pages (e.g., index.html, appointments.html) don't have the capture-phase IIFE
- Broken page (repair-orders.html) has the IIFE + save button tagged with `data-close-modal`

**Fix:** See `references/event-propagation-iife-conflict.md` for full trace, three-part fix, and alternative approach.

## Color/Readability Rules of Thumb

- Never use colored text on the same-colored background (e.g. `text-yellow-700` on `bg-yellow-50`)
- Use white/dark cards with **colored left-border accents** for alert banners — clean, modern, readable
- For count badges: use subtle colored background (`bg-red-50`) with **bold darker text** (`text-red-700`), not washed-out tones
- Labels should use neutral gray tones (`text-gray-500/600/700`) for readability across themes