7 Bugs in 2 Hours: What an AI QA Engineer Found in My Homelab Apps
I pointed an AI-powered bug hunter at two homelab apps I use daily. It found 7 bugs across 44 pages in 2 hours: modal overflows, missing Alpine plugins, raw JSON rendering, broken keyboard shortcuts. All fixed and deployed.
I pointed my bug-hunter skill at two apps I use daily: Mission Control (the Express.js dashboard for my AI agent team) and Life Hub (my FastAPI + HTMX personal dashboard). Both had been "working fine" for weeks. The bug hunter found seven bugs across both apps, fixed all of them, and opened two PRs, in about two hours.
Here's what it found, how it found it, and what each fix looked like.
Target 1: Mission Control
Mission Control is a lightweight Express.js app that runs as a sidecar container in the OpenClaw pod. It's the task board, session viewer, and operational dashboard for my seven AI agents. Vanilla JavaScript frontend, about 800 lines of server code and 1,400 lines of client-side JS.
The bug hunter read server.js and all 13 frontend JS files, then drove Playwright through all 12 pages.
Bug 1: Modal Overflow, Buttons Unreachable [HIGH]
The "New Task" modal has 12 form fields. At 918 pixels tall, it overflowed the 788-pixel viewport. The Cancel and Create buttons at the bottom were below the fold: invisible, unclickable. Playwright caught this because it literally couldn't click the Cancel button:
TimeoutError: page.click: Timeout 5000ms exceeded.
- element is outside of the viewportThe .modal CSS had no max-height and no overflow-y. The fix was two lines:
.modal {
max-height: 90vh;
overflow-y: auto;
}This is the kind of bug you don't notice when you're the one who built the form, because you know the fields and fill them quickly. But anyone else (or an agent creating a detailed task) hits a form that can't be submitted.
Bug 2: Escape Key Doesn't Close Modals [HIGH]
Every modern web app closes modals on Escape. Mission Control didn't. The hideModal() function existed and was bound to overlay clicks and the Cancel button, but nobody added a keyboard listener.
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') hideModal();
});Three lines. Should have been there from the start.
Bug 3: Life-Hub Habits Renders Raw JSON [MEDIUM]
The Life-Hub page in Mission Control has a "Habits Today" card. When the Life-Hub API returns habits as an object ({"total": 0, "completed": 0, "rate": 0, "breakdown": []}) instead of an array, the rendering code fell through to JSON.stringify(). Users saw raw JSON where they should have seen "No habits logged today."
The fix: handle the object format by extracting the breakdown array, and show summary stats in the card header.
Bug 4: Infra Page Shows Raw JSON Dump [LOW]
The Infrastructure page was supposed to show service health, but the renderInfraData() function checked for data.services while the Uptime Kuma API actually returns data.monitors. Every field name mismatch meant the code fell through to the raw JSON fallback.
The fix added proper handling for the Uptime Kuma response shape: stat cards showing Up/Down/Total/Avg Response, and a formatted monitor list with green/red status indicators and response times.
Target 2: Life Hub
Life Hub is a much larger application: a FastAPI backend with HTMX + Alpine.js + Tailwind frontend, 7,300 lines of route handlers, 32 pages, SQLite database. The bug hunter smoke-tested all 32 pages, then focused on the issues it found.
Bug 5: Alpine.js Collapse Plugin Missing [HIGH]
This was the highest-impact find. Six warnings per page, across every page with collapsible sections:
Alpine Warning: You can't use [x-collapse] without
first installing the "Collapse" plugin.The templates used x-collapse in 12 places across 5 templates (Today, Life Wheel, Finance, Insights, mood logging). The base.html loaded alpine.3.min.js but not the separate @alpinejs/collapse plugin. Functionally, the sections still toggled via x-show, but they snapped open/closed instead of animating smoothly. Over 100 console warnings per session.
The fix: download alpine-collapse.min.js (1.4 KB) and add a script tag before the Alpine core script. Plugins must load before Alpine initializes.
<script defer src="/static/vendor/alpine-collapse.min.js"></script>
<script defer src="/static/vendor/alpine.3.min.js"></script>Bug 6: Chart.js "Canvas Already in Use" [MEDIUM]
The Life Wheel page renders a radar chart with Chart.js. On every page load:
Error: Canvas is already in use. Chart with ID '0' must be
destroyed before the canvas with ID 'lifeWheelChart' can be reused.The createRadarChart() method created a new Chart instance without destroying the previous one. Alpine.js component re-initialization triggered this on every page visit.
// Destroy existing chart before creating new one
const existingChart = Chart.getChart(ctx);
if (existingChart) existingChart.destroy();Bug 7: HTMX Requests Missing API Key [LOW]
Life Hub's HTMX endpoints require API key authentication. The base template had a meta tag with the server-injected API key, but nothing read it. HTMX requests went out without the X-API-Key header, causing 401 errors on endpoints like /htmx/nav-badges.
This only manifested on port-forward access (production uses Authentik forward-auth which injects headers), but it meant nav badge counts never loaded during development.
<script>
document.addEventListener('htmx:configRequest', function(e) {
var key = document.querySelector('meta[name="life-hub-api-key"]');
if (key && key.content) e.detail.headers['X-API-Key'] = key.content;
});
</script>The Scorecard
| App | Pages Tested | Bugs Found | Fixed | PR |
|---|---|---|---|---|
| Mission Control | 12 | 4 | 4 | #1 |
| Life Hub | 32 | 3 | 3 | #1 |
44 pages tested, 7 bugs found, 7 bugs fixed, 2 PRs merged and deployed. Zero false positives: every finding was a real bug.
What the Bug Hunter Didn't Catch
For honesty's sake: there are categories of bugs this approach misses.
- Performance issues. The bug hunter doesn't measure load times, memory leaks over time, or database query performance. It tests functionality, not speed.
- Authentication edge cases. Testing via port-forward bypasses Authentik, so auth-boundary bugs aren't fully exercised.
- Multi-user concurrency. It tests as a single user. Race conditions between concurrent users won't surface.
- Subtle visual regressions. It takes screenshots but doesn't compare them against a baseline. A button that moved 3 pixels won't register.
These are genuine gaps. The bug hunter is a QA pass, not a replacement for all testing.
What I Learned
Apps you use daily hide bugs in plain sight. I'd been using Mission Control every day for a week. I'd opened that modal dozens of times. I never noticed the buttons were unreachable because I always filled the form fast enough that the modal was short enough to fit. The bug hunter tested methodically, with all 12 fields visible, and found the overflow immediately.
Console warnings are bugs. 100+ Alpine.js warnings per session wasn't just noise. It indicated a missing dependency that degraded UX across the entire app. The bug hunter flagged it; I would have kept ignoring the console.
The fix is usually small. Seven bugs, seven fixes, none longer than 10 lines. The hard part isn't the fix. It's finding the bug and understanding the root cause. That's exactly what this skill is designed to do: the systematic exploration that humans skip because it's tedious.
The bug-hunter skill took about 30 minutes to write. It's found more bugs in its first two hours of use than I found manually in the previous two weeks. That's the value of systematic testing over incidental discovery. And it's exactly the kind of work that benefits from delegation to an AI that doesn't get bored.