197 lines
8.4 KiB
Markdown
197 lines
8.4 KiB
Markdown
# 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
|