171 lines
11 KiB
Markdown
171 lines
11 KiB
Markdown
---
|
|
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 |