# Debugging Firebase → PocketBase Migration Bugs Common bug patterns discovered during migration validation, with root cause analysis and fix strategies. ## 1. Dual Render Path Inconsistency After completing a repair order, it remains visible in the Active tab despite `workStatus` being set to `'completed'`. ### Root Cause The app has **two render paths** that apply different filters: ``` loadRepairOrders() renderActiveRepairOrders() │ │ │ filter: workStatus !== │ NO FILTER — passes entire │ 'completed' && status !== │ repairOrders array to │ 'completed' │ renderRepairOrders() │ │ ▼ ▼ renderRepairOrders(filtered) renderRepairOrders(all) ``` - **`loadRepairOrders()`** (called on page load) fetches from DB, then filters out completed orders at lines ~2276-2278 - **`renderActiveRepairOrders()`** (called after every UI mutation) renders the local `repairOrders` array **without any filter** When `handleFinancialCompletion()` sets `workStatus = 'completed'` and calls `renderActiveRepairOrders()`, the completed RO stays visible because the re-render path doesn't filter. ### Fix ```javascript function renderActiveRepairOrders() { const activeOrders = repairOrders.filter(ro => { return ro.workStatus !== 'completed' && ro.status !== 'completed'; }); renderRepairOrders(activeOrders); } ``` This uses the same filter predicate as `loadRepairOrders()`. ### How to detect Search for all functions that render the same list. The app typically has: - A **load** function (fetches from DB, applies filters, assigns to global `repairOrders`, calls render) - A **render** function (re-renders from the global array, used for reactive UI updates) If the load function filters but the render function doesn't, this bug exists. To verify: ``` grep -n "function renderActiveRepairOrders\|function loadRepairOrders\|function renderRepairOrders" *.js ``` Then check: 1. Does `loadRepairOrders()` filter out completed items before calling `renderRepairOrders()`? 2. Does `renderActiveRepairOrders()` pass the raw array or a filtered one? ### Search pattern ``` grep -n "function.*render.*Orders\|function.*load.*Orders\|\.filter.*completed" repair-orders.js ``` --- ## 2. Silent Errors in setTimeout / Asynchronous Callbacks A DOM element is missing from the HTML, causing a `TypeError` that crashes silently. The UI appears to "do nothing" when a button is clicked, with no console error visible. ### Root Cause ```javascript // Outer try/catch: try { // ... sync code ... setTimeout(() => { // This code runs AFTER the try/catch scope has exited document.getElementById('missing-element').textContent = value; // TypeError here is UNCAUGHT — it fires in the event loop tick // after the try/catch already returned }, 100); return; // <-- try/catch scope exits } catch (error) { // Never reaches here for the setTimeout error console.error('Error:', error); } ``` The `try/catch` only protects the code inside its lexical scope. `setTimeout` (and `Promise.then`, `requestAnimationFrame`, `setInterval`) execute in a **new execution context** — the outer `try/catch` is already gone. ### How to find 1. Look for DOM operations inside `setTimeout()`, `requestAnimationFrame()`, or `.then()` callbacks 2. Check if they're inside a `try/catch` — if the `try/catch` wraps the `setTimeout` call (not the callback body), the callback is unprotected 3. The ONLY way to catch these is a `try/catch` **inside** the callback, or `window.onerror` / `window.addEventListener('unhandledrejection', ...)` ### Fix ```javascript // Option A: try/catch INSIDE the callback setTimeout(() => { try { document.getElementById('missing-element').textContent = value; } catch (innerError) { console.error('setTimeout error:', innerError); } }, 100); // Option B: Replace setTimeout with synchronous code when possible // (Only if the delay isn't needed for DOM readiness) ``` ### Detection script Run this in browser DevTools to detect pattern: ```javascript // Check all JS files for setTimeout with DOM ops outside try/catch document.querySelectorAll('script[src]').forEach(s => { fetch(s.src).then(r => r.text()).then(code => { const matches = code.match(/setTimeout\([^)]*getElementById[^)]*\)/g); if (matches) console.log(`${s.src}: ${matches.length} vulnerable setTimeout(s)`); }); }); ``` --- ## 3. "Looks like it works but data is missing" — PocketBase Silently Drops Undefined Fields After a successful API call (HTTP 200), the saved record is missing fields the app wrote. ### Root Cause PocketBase is **strictly typed** — fields not defined in the collection schema are **silently dropped** on write. Firestore is schemaless, so any field in the JavaScript object gets stored. ### How to find missing fields systematically ```bash # 1. Get the PocketBase collection schema field names PB_FIELDS=$(curl -s http://127.0.0.1:8091/api/collections/repairOrders | \ python3 -c "import sys,json; print('\n'.join(f['name'] for f in json.load(sys.stdin).get('fields',[])))") # 2. Get all field names the JavaScript app writes (from CRUD operations) JS_FIELDS=$(grep -ohP '\b(customerName|workStatus|roNumber|vehicleInfo|vin|mileage|services|estimatedTime|promisedTime|lastModified|completedTime|financial|writeupTime|status|userId|customerPhone|deviceModel|deviceType|issue|priority|technician|notes|price|customerId|quoteId|invoiceId|lastActivity|createdAt|updatedAt)\b' repair-orders.js | sort -u) # 3. Compare — fields in JS that are NOT in PB schema echo "Missing from PB schema:" for f in $JS_FIELDS; do echo "$PB_FIELDS" | grep -q "$f" || echo " - $f" done ``` Commonly missed fields in a shop management app migration: | Field | Type | Where It's Written | |---|---|---| | `workStatus` | text | `updateRepairOrderWorkStatus()`, `createNewRepairOrder()` | | `financial` | json | `handleFinancialCompletion()` — nested object with cpTotal, cpCost, etc. | | `roNumber` | text | `createNewRepairOrder()` — auto-generated RO-XXXXXXX | | `vehicleInfo` | text | Repair order create form & customer data | | `vin` | text | Repair order create form | | `mileage` | text | Repair order create form | | `services` | text | Repair order services field (comma-separated) | | `estimatedTime` | text | Repair order promised time calculation | | `promisedTime` | text | Created from estimatedTime + currentTime | | `lastModified` | text | Updated on every mutation | | `completedTime` | text | Set when workStatus becomes 'completed' | ### Symptom: "Completing an RO doesn't stick" If the user reports: "I marked it complete, it showed completed, but after refresh it's back in Active" — this is ALWAYS a schema mismatch. The app updates the local JavaScript array (shows correct UI), but the PocketBase write silently drops `workStatus` and `financial` because those fields aren't in the collection schema. On page reload, `loadRepairOrders()` fetches from PB and the RO has its original `workStatus` (missing = undefined), so it passes the "not completed" filter. This is subtly different from the Dual Render Path bug (which happens BEFORE refresh — RO stays in Active tab immediately). The schema mismatch bug: RO correctly disappears from Active tab after completion, but reappears on next page load. ### How to find --- ## 4. Reference Build Comparison Technique When a Firebase build works but the PocketBase port doesn't, compare the same function in both: ```bash # Find where functions live in both builds grep -n "updateRepairOrderWorkStatus\|handleFinancialCompletion" \ /path/to/firebase/repair-orders.js \ /path/to/pocketbase/repair-orders.js # Read and diff diff <(sed -n '1747,1823p' /path/to/firebase/repair-orders.js) \ <(sed -n '1747,1823p' /path/to/pocketbase/repair-orders.js) ``` If the functions are **identical**, the bug is not in the PocketBase adapter — it's a pre-existing bug in the app code that was hidden by Firebase behavior (realtime listeners auto-fixing the UI, different execution timing, etc.). In this case, the Firebase build had the same `renderActiveRepairOrders` filter bug, but it may have appeared to work because: - Users refreshed the page (which calls `loadRepairOrders()` with the filter) - The issue existed but was never noticed because the financial completion was never tested end-to-end