11 KiB
name, title, description
| name | title | description |
|---|---|---|
| frontend-debugging | Frontend Debugging Patterns | 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 DOMappendChildcalls 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:
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:
<!-- 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 juststopPropagation) — the module handler is on the same element - Wrap in IIFE
(function(e){...})(event)sostopImmediatePropagationreceives 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 loadheader-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:
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:
- Hard refresh — should load dark
- Click every UI element that previously controlled dark mode — no change
- Open settings modals — any dark mode toggle should be inoperable
- Search localStorage in DevTools —
darkModeshould 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:
- Open page in incognito/private tab
- Hard refresh (Ctrl+Shift+R / Cmd+Shift+R)
- Add cache-busting meta tags to
<head>:<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"> - Restart the server to force fresh TCP connections
Mobile Responsiveness Pitfalls
Hamburger Menu Not Working
Check four things in order:
- The hamburger button exists in HTML (search for
mobile-menu-btn) - The target
#mobile-navigationpanel exists in the HTML - Module conflict:
shared/header-functionality.jsadds its own click handler that also toggleshidden— the inline onclick and module handler cancel each other out. AddstopImmediatePropagation()to the inline onclick (see Cause C above) - 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
hiddenin 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:
// 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:
// 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:
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-700onbg-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