Files
hermes-config/skills/self-hosting/firebase-to-pocketbase-migration/references/debugging-migration-bugs.md
T
2026-07-12 10:17:17 -04:00

8.4 KiB

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

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

// 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

// 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:

// 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

# 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:

# 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