From Crawler Chaos to Mobile Nirvana: A Session in Bugs, Breakthroughs, and Beautiful Dashboards
Join me as I recount a challenging dev session, tackling a stubborn HTML stripping bug in our site crawler and rolling out a complete mobile-first CSS overhaul for our dashboard pages.
Every developer knows those sessions. The ones where you dive in with a clear plan, only to hit unexpected roadblocks, learn critical lessons the hard way, and emerge hours later, exhausted but exhilarated, with significant progress under your belt. This past Wednesday, March 11th, was one of those days.
My mission: First, squash a gnarly HTML stripping bug in our site crawler that was polluting our RAG (Retrieval Augmented Generation) data. Second, embark on a full mobile-first CSS overhaul for every single dashboard page. Ambitious? Absolutely. Achieved? You bet.
Taming the HTML Stripper: A Crawler's Tale
Our RAG system relies heavily on clean, well-formatted text. Imagine our dismay when we discovered that documents sourced from certain URLs were appearing in our system with their HTML tags intact, rather than properly stripped and parsed. This wasn't just a cosmetic issue; it was directly impacting the quality of our AI's responses.
The culprit turned out to be a two-pronged attack:
-
The
processDocument()Fallback: Oursrc/server/services/rag/document-processor.tshad a logic flaw. InprocessDocument(), if a local file wasn't found, it would always fall back to re-fetching from thesourceUrlwithout proper checks. The fix was simple but crucial: anfs.access()check to ensure the local file truly didn't exist before hitting the network again. This prevents unnecessary re-fetching and ensures we're processing the most accurate source.typescript// src/server/services/rag/document-processor.ts:397 (simplified) async function processDocument(documentPath: string, sourceUrl?: string) { let content: string; try { // Check if local file exists and is accessible await fs.promises.access(documentPath, fs.constants.F_OK); content = await fs.promises.readFile(documentPath, 'utf8'); } catch (error) { if (sourceUrl) { // Fallback to re-fetch only if local file truly not found content = await fetchAndProcess(sourceUrl); } else { throw new Error('Document content not found locally and no source URL provided.'); } } // ... rest of processing logic } -
The MimeType Mix-up: The second part of the puzzle was a subtle
mimeTypemisconfiguration in oursite-crawler-service.ts. It was incorrectly identifyingtext/htmlcontent astext/plain, which meant our HTML stripping logic wasn't even being triggered for certain documents. A quick change fromtext/htmltotext/plain(counter-intuitive, perhaps, but correct for how our pipeline expects raw text for stripping) resolved this.
With the bug squashed, the next step was a surgical cleanup. I deleted 153 tainted BetrVG documents from production to ensure a fresh start. This also necessitated manually creating the crawl_jobs table via SQL, as it was a new requirement not yet integrated into our Prisma migrations.
The result? Commit 0957b1b deployed, 318 tests passing, typecheck clean. The crawler now correctly processes documents, and our RAG system can breathe a sigh of relief.
The Mobile Makeover: Crafting a Responsive Dashboard
With the backend stabilized, it was time to shift gears to the frontend. Our dashboard, while functional on desktop, desperately needed a mobile-first overhaul. The goal wasn't just responsiveness, but a genuinely pleasant experience on smaller screens.
I started with a dedicated design doc (docs/plans/2026-03-11-mobile-first-css-design.md) and then an implementation plan (docs/plans/2026-03-11-mobile-first-css.md) outlining six key tasks. What made this particularly interesting was executing these tasks via "subagent-driven development"—essentially, breaking down the problem into small, well-defined prompts for an AI assistant, then meticulously reviewing and integrating its output.
Here's a breakdown of the key changes:
-
Viewport & Safe Areas (
4d43396):- Removed
maximumScale:1from the viewport meta tag for better zoom accessibility. - Added
viewportFit:coverand safe-area padding to the mobile navigation. - Introduced a
pb-20on the main content area to prevent content from being hidden by the fixed mobile nav.
- Removed
-
Adaptive Sidebar Layout (
7aee355):- The traditional desktop sidebar (
hidden md:block) now transforms into a horizontal, scrollable tab bar on mobile (md:hidden). - The
SidebarPageLayoutcomponent now dynamically adjusts its flex direction:flex-colon mobile,md:flex-rowon desktop.
- The traditional desktop sidebar (
-
Responsive Dialogs (
1fc31a3):- Dialogs now take up more width on mobile (
w-[calc(100%-2rem)]) while reverting to full width on larger screens (md:w-full). - Padding adjusted (
p-4 md:p-6) and crucialmax-h-[85vh] overflow-y-autoadded to ensure dialog content is always scrollable and doesn't push off-screen.
- Dialogs now take up more width on mobile (
-
Smarter Tables (
97861ee):- Our model usage tables, which had many columns (Cost, Calls, Duration, Energy), were overwhelming on mobile.
- Key columns now use
hidden md:table-cell, collapsing them on smaller screens. - A "tap-to-expand" functionality with a
ChevronDownicon was added to reveal full row details on demand.
-
Fluid Detail Pages (
07817b5):- Pages like "Ipcha" and "Projects detail" often featured horizontally laid-out elements that broke on mobile.
- I applied
flex-col gap-2 sm:flex-rowto five specific layout elements, ensuring they stack vertically on smaller screens and revert to horizontal on small-to-medium viewports.
-
Mobile Navigation Polish (
19ea037):- "Style" in the mobile nav was replaced with "Memory" (represented by a Brain icon) to better reflect its function.
- A critical fix for the sidebar component within a mobile sheet: the sidebar had
hidden md:flex, making it invisible within the sheet below themd:breakpoint. The workaround involved adding aclassNameprop toSidebarand passingclassName="flex w-full"from the Sheet component to override the defaulthiddenbehavior.
Lessons Learned the Hard Way (The "Pain Log")
No session is complete without a few head-scratching moments. These often turn into the most valuable lessons:
-
SSH Heredocs vs.
docker exec psql:- Tried: Piping multi-statement SQL via SSH heredoc to
docker exec psql. - Failed: The heredoc syntax didn't pass through the SSH and Docker
execchain correctly, leading to syntax errors. - Lesson: For complex SQL on a remote Docker container, either create a
.sqlfile and copy it over, or (for simpler cases) use the single-line-cflag with carefully escaped quotes. It's tedious, but reliable.
- Tried: Piping multi-statement SQL via SSH heredoc to
-
npx prisma db pushon Production:- Tried: Running
npx prisma@5.22.0 db pushinside the production container to apply a new table. - Failed:
db pushis designed for rapid prototyping and development environments. It can irrevocably drop existing data, specificallypgvectorembeddingcolumns in our case, if not handled with extreme care. - Lesson: NEVER use
prisma db pushon production. Always rely onprisma migrate deployfor applying migrations, or for ad-hoc schema changes, manualCREATE TABLE/ALTER TABLEstatements viadocker exec psql. This was a near-disaster averted by a quick rollback.
- Tried: Running
-
Responsive Component Visibility within Other Components:
- Tried: Placing a
Sidebarcomponent (which hadhidden md:flex) directly inside aSheetcomponent for mobile navigation. - Failed: Below the
md:breakpoint, theSidebar'shiddenclass took precedence, making it invisible inside the mobile sheet. - Lesson: When nesting responsive components, be acutely aware of how parent/child
displayandvisibilityutility classes interact. Design components with override props (likeclassName) to allow parents to dictate their children's visual state in specific contexts.
- Tried: Placing a
Looking Ahead
While the immediate goals are complete, the journey continues:
- Verification: The user needs to re-crawl the BetrVG documents and verify clean text extraction. I also need to test the mobile UI on a real device to ensure the horizontal tab bar, dialogs, table expand, and sidebar sheet behave as expected.
- Security: The
crawl_jobstable is currently unprotected. An RLS (Row Level Security) policy needs to be considered and implemented. - Code Cleanup: Consolidating the duplicate
.no-scrollbarand.scrollbar-noneutilities inglobals.cssis a minor but important cleanup task. - Polish: A follow-up "touch target" polish pass will ensure all interactive elements meet the 44px minimum recommendation for mobile accessibility.
This session was a microcosm of full-stack development: debugging a backend data pipeline, meticulously crafting a frontend user experience, and navigating the operational pitfalls of deployment. It's a reminder that every line of code, every design decision, and every painful lesson learned contributes to a more robust and user-friendly product.
{
"thingsDone": [
"Fixed HTML stripping bug in document processor",
"Corrected crawler mimeType handling",
"Deleted tainted production documents",
"Manually created crawl_jobs table on production",
"Designed mobile-first CSS approach",
"Implemented 6 tasks for mobile-first CSS overhaul",
"Deployed all changes to production"
],
"pains": [
"SSH heredoc failure for multi-statement SQL",
"Accidental `prisma db push` on production dropping columns",
"Responsive component visibility conflict within nested components"
],
"successes": [
"Successful bug fix for data integrity",
"Complete mobile-first redesign of dashboard UI",
"Successful deployment with all tests passing",
"Effective use of subagent-driven development for frontend tasks",
"Learning critical lessons about production database management"
],
"techStack": [
"TypeScript",
"Node.js",
"Tailwind CSS",
"React",
"Next.js",
"Prisma",
"PostgreSQL",
"Docker",
"SSH"
]
}