---
name: web-ui-repair
description: "Fix common static web app UI issues: broken shared dependencies, readability, browser cachiness, and dev server setup."
version: 1.0.0
author: Hermes Agent
license: MIT
platforms: [linux, macos]
metadata:
hermes:
tags: [web, frontend, html, css, javascript, ui, debugging, repair]
related_skills: [systematic-debugging, spike, codebase-audit]
---
# Web UI Repair
## Overview
Patterns for fixing common issues in static HTML/CSS/JS web apps — multi-page sites where some pages work and others don't, readability problems, and browser cache issues after fixes.
This skill covers **post-investigation repair actions**. For root cause investigation methodology, use `systematic-debugging`. For diagnostic patterns (JS-overrides-HTML, missing script imports, class-based dark mode search), see `references/frontend-debugging-patterns.md` (absorbed from `frontend-debugging`).
---
## 1. Readability & Contrast Fixes
### The Problem
Colored text on colored backgrounds (e.g., `text-yellow-600` on `bg-yellow-50`, or `text-red-500` on `bg-red-50`) makes content unreadable, especially in dark mode.
### The Fix
Use a **white/dark card with colored accent border** pattern for alert banners OR a **subtle colored bg with bold text** pattern for stat badges:
**Pattern A — Alert banners (white card + colored left-border accent):**
```
Overdue Tasks:
3
task(s) are overdue!
```
**Pattern B — Stat badges (colored bg + bold colored text):**
```
```
**Key rules for readability:**
- NEVER use colored text on a same-colored background (e.g. `text-yellow-700` on `bg-yellow-50`)
- For the white-card + accent pattern: use `text-gray-900 dark:text-white` for headings, `text-gray-700 dark:text-gray-300` for body text
- For the colored-bg badge pattern: use the **darkest shade** of the color for text (`text-red-700`, `text-amber-700`) — NOT a washed-out tone
- Numbers/values should be bold and high-contrast
- **Avoid washed-out grays** for primary content — reserve `text-gray-500` and below for secondary/helper text only
**Color scheme mapping for badges/stat cards:**
| Intent | Border accent | Value color |
|--------|--------------|-------------|
| Overdue / Error | `border-red-200 dark:border-red-900/50` | `text-red-600 dark:text-red-400` |
| Due Today / Warning | `border-amber-200 dark:border-amber-900/50` | `text-amber-600 dark:text-amber-400` |
| Upcoming / Info | `border-blue-200 dark:border-blue-900/50` | `text-blue-600 dark:text-blue-400` |
| Success | `border-green-200 dark:border-green-900/50` | `text-green-600 dark:text-green-400` |
---
## 2. Browser Cache Busting
### The Problem
After editing HTML/CSS/JS files on a static server (Python http.server, Nginx, etc.), the user's browser may serve cached versions of the old files — they won't see your changes.
### The Fix
**Add cache-busting meta tags to the ``:**
```html
```
**Also tell the user to hard refresh:**
- Desktop: `Ctrl+Shift+R` / `Cmd+Shift+R`
- Phone: close tab completely, open fresh
**Pitfalls:**
- `Cache-Control` meta tags don't always work on all browsers (especially mobile Safari/Chrome). When they don't, the meta tags still act as a signal to the user to try again.
- If meta tags aren't enough, restart the dev server after file changes (forces a fresh connection).
- For production sites, use file-versioned URLs (e.g., `style.css?v=2`) or content hashing in the filename.
**⚠️ Server-side cache pitfall — `Cache-Control: immutable`:**
Nginx configs often set aggressive caching on static assets:
```nginx
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
```
The `immutable` directive tells the browser **"this file will never change — don't even ask the server if there's a newer version."** After editing JS/CSS files, the browser serves the stale cached version forever. Meta tags in HTML don't override this.
**Fix:** Change to must-revalidate with a shorter expiry:
```nginx
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
```
Then reload nginx: `sudo nginx -t && sudo systemctl reload nginx`
**Verification:**
```bash
curl -sI -k https://your-site.com/path/to/file.js | grep -i cache
# Should show: Cache-Control: public, must-revalidate (NOT "immutable")
```
**⚠️ Before debugging JS logic, verify the served HTML matches what's on disk.**
When a fix was applied to a file but the user reports it didn't take effect, the FIRST diagnostic step is to confirm what nginx is actually serving — not what's on the filesystem:
```bash
# Compare what's on disk vs what's served
grep 'fix-pattern' /path/to/file.html # Should match
curl -sk https://localhost/file.html | grep 'fix-pattern' # Should ALSO match
```
If they differ, the problem is between disk and server — not in the JS logic. Common causes:
- **Nginx serving from a different root** than the file you're editing (check `root` directive in all `server` blocks, especially `sites-enabled/` vs `sites-available/`)
- **Multiple server blocks on different ports** — the `sites-enabled/` version may have different `listen` and `root` directives than the `sites-available/` version you inspected
- **Symlink vs copy** — `sites-enabled/` entries may be symlinks to `sites-available/` OR standalone files with different content
- **Browser cache** — curl bypasses browser cache; if curl shows the fix but the user doesn't see it, the issue is client-side caching
**Runtime cache-busting for redirects — `?cb=` pattern:**
When the user's browser already has a stale cached page (from before the nginx fix), even a hard refresh may not clear it. Add a cache-busting query parameter to ALL redirects so the browser treats each navigation as a fresh request:
```javascript
// Auth guard redirect (with cache-busting)
location.replace('login.html?cb=' + Date.now());
// Sign-out redirect
localStorage.removeItem('pocketbase_auth');
location.replace('login.html?cb=' + Date.now());
// Login success redirect
location.replace('index.html?cb=' + Date.now());
```
Use `location.replace()` (not `location.href`) to prevent back-button loops. The `?cb=` parameter is ignored by static servers but forces the browser to bypass its cache and fetch a fresh copy. Apply this to ALL redirects: sign-out buttons, auth guards, login form success handlers — every path that navigates between pages.
---
## 3. Dev Server Port Selection
### The Problem
Starting a static HTTP server on a port already in use causes it to fail silently or conflict with another service.
### The Fix
1. Check which ports are in use first:
```bash
ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null
```
2. Pick a port NOT in the list. Common safe picks that are rarely used by system services:
- `5500`, `8081`, `8888`, `9999`, `3333`, `4444`, `5555`, `7777`
3. Verify the server started successfully:
```bash
ss -tlnp | grep
```
4. Common ports that ARE often in use and should be avoided:
- `3000` (often used by dev tools like Selkies, React dev server)
- `8000`, `8080` (common HTTP alternatives)
- `8443`, `9090`, `9443` (dashboard tools)
- `3001`, `8787`, `2283` (self-hosted apps)
**Pitfalls:**
- Some services (like Selkies) may use port 3000 without showing in `ss` output — avoid port 3000 by default.
- Python's `http.server` needs `SIGINT` (Ctrl+C) or `kill` to stop cleanly before restarting on a new port.
---
## 5. Compact Nav-Bar Header Redesign
### Context
Multi-page static web apps (Dashboard, Repair Orders, Customers, Appointments) often have a tall header with logo, title, subtitle, and a separate navigation bar below. Users may want a **compact, single-row header** that removes the logo/title section entirely.
### The Pattern
Instead of two sections (top content + nav bar), combine everything into one nav bar row:
```
┌──────────────────────────────────────────────────┐
│ [☰] Dashboard Repair Orders Appts Customers │ [🌙] [👤] │
├──────────────────────────────────────────────────┤
│ Mobile Navigation (hidden) │
└──────────────────────────────────────────────────┘
```
**Structure breakdown (left to right):**
| Element | Desktop | Mobile |
|---------|---------|--------|
| Hamburger button (`#mobile-menu-btn`) | Hidden (`lg:hidden`) | Visible |
| Desktop nav links | Visible (`hidden lg:flex`) | Hidden |
| **Mobile page title** | Hidden (`lg:hidden`) | **Visible center** |
| Dark mode toggle | Visible (sm+) | Hidden (in dropdown) |
| User menu button | Visible | Visible |
### Implementation Steps
1. **Remove the top content section** — logo icon, page title ``, subtitle `
` — everything inside the header-card's inner content div
2. **Merge controls into the nav bar** — move the mobile menu button, dark mode toggle, and user menu into the same flex row as the nav links
3. **Add mobile page title** — a `Page Name` between the left and right sections
4. **Tighten padding** — outer container: `p-2 md:p-4`, inner: `py-2 md:py-2.5`, nav link padding: `py-1.5 md:py-2`
### Key CSS Classes to Use
**Desktop nav links:**
- Active page: `bg-white/20 text-white shadow-sm`
- Inactive pages: `text-blue-100 hover:text-white hover:bg-white/10`
**Mobile nav links:**
- Active page: `bg-white/20 text-white shadow-sm`
- Inactive pages: `text-blue-100 hover:bg-white/10 hover:text-white`
**Mobile navigation panel:**
- Hidden by default: `lg:hidden hidden`
- Toggled by `#mobile-menu-btn` click handler in `shared/header-functionality.js`
### Pitfalls
- **Active page must be correct.** Each page's nav has the active page link styled with `bg-white/20 text-white shadow-sm`. Triple-check: if you copy-paste the header from dashboard.html to repair-orders.html, the active link (Dashboard with bg-white/20) will show incorrectly on the repair-orders page.
- **⚠️ Active nav-link may be too subtle.** On a blue gradient header, `bg-white/20` (20% white) vs the parent's `bg-white/10` may not be enough contrast. The difference is particularly hard to see on mobile. Fix: add a dedicated CSS class for the active page with higher opacity (35%) and a subtle shadow:
```css
.nav-link.active-page {
background: rgba(255,255,255,0.35) !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
}
.dark .nav-link.active-page {
background: rgba(255,255,255,0.25) !important;
}
```
Add the `active-page` class to the current page's nav link (``). Apply to ALL pages' active nav links for consistency. This makes the active pill visibly brighter than the inactive `text-blue-100` links.
- **⚠️ Missing `.nav-link` CSS on individual pages.** The `nav-link` class may have associated CSS rules (`position: relative; overflow: hidden;` plus a `::before` hover shine animation) defined inline in some pages' `