initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
@@ -0,0 +1,53 @@
# Context Budget Discipline
Practical rules for keeping orchestrator context lean when spawning subagents or reading large artifacts. Use these whenever you're running a multi-step agent loop that will consume significant context — plan execution, subagent orchestration, review pipelines, multi-file refactors.
Adapted from the GSD (Get Shit Done) project's context-budget reference — MIT © 2025 Lex Christopherson ([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done)).
## Universal rules
Every workflow that spawns agents or reads significant content must follow these:
1. **Never read agent definition files.** `delegate_task` auto-loads them — you reading them too just doubles the cost.
2. **Never inline large files into subagent prompts.** Tell the agent to read the file from disk with `read_file` instead. The subagent gets full content; your context stays lean.
3. **Read depth scales with context window.** See the table below.
4. **Delegate heavy work to subagents.** The orchestrator routes; it doesn't execute.
5. **Proactively warn** the user when you've consumed significant context ("Context is getting heavy — consider checkpointing progress before we continue").
## Read depth by context window
Check the model's actual context window (not "it's Claude so 200K"). Some Sonnet deployments are 1M, some are 200K. If you don't know, assume the smaller one — err toward leanness.
| Context window | Subagent output reading | Summary files | Verification files | Plans for other phases |
|----------------|-------------------------|---------------|--------------------|-----------------------|
| < 500k (e.g. 200k) | Frontmatter only | Frontmatter only | Frontmatter only | Current phase only |
| >= 500k (1M models) | Full body permitted | Full body permitted | Full body permitted | Current phase only |
"Frontmatter only" means: read enough to see the final status/verdict/conclusion. If the subagent wrote a 3000-line debug log, read the summary section it produced, not the log.
## Four-tier degradation model
Monitor your context usage and shift behavior as you climb the tiers. The point is to notice *before* you hit the wall, not when responses start truncating.
| Tier | Usage | Behavior |
|------|-------|----------|
| **PEAK** | 0 30% | Full operations. Read bodies, spawn multiple agents in parallel, inline results freely. |
| **GOOD** | 30 50% | Normal operations. Prefer frontmatter reads. Delegate aggressively. |
| **DEGRADING** | 50 70% | Economize. Frontmatter-only reads, minimal inlining, **warn the user** about budget. |
| **POOR** | 70%+ | Emergency mode. **Checkpoint progress immediately.** No new reads unless critical. Finish the current task and stop cleanly. |
## Early warning signs (before panic thresholds fire)
Quality degrades *gradually* before hard limits hit. Watch for these:
- **Silent partial completion.** Subagent claims done but implementation is incomplete. Self-checks catch file existence, not semantic completeness. Always verify subagent output against the plan's must-haves, not just "did a file appear?"
- **Increasing vagueness.** Agent starts using phrases like "appropriate handling" or "standard patterns" instead of specific code. This is context pressure showing up before budget warnings fire.
- **Skipped protocol steps.** Agent omits steps it would normally follow. If success criteria has 8 items and the report covers 5, suspect context pressure, not "the agent decided 5 was enough."
When these signs appear, checkpoint the work and either reset context or hand off to a fresh subagent.
## Fundamental limitation
When you orchestrate, you cannot verify semantic correctness of subagent output — only structural completeness ("did the file appear?", "does the test pass?"). Semantic verification requires either running the code yourself or delegating a review pass to another fresh subagent.
**Mitigation:** in every task you delegate, include explicit "must-have" truths the subagent must confirm in its response (e.g., "confirm your test actually tests X, not just that X was imported"). The subagent re-asserting concrete facts is evidence; vague summaries are not.
@@ -0,0 +1,93 @@
# Gates Taxonomy
Canonical gate types for validation checkpoints across any workflow that spawns subagents, runs review loops, or has human-approval pauses. Every validation checkpoint maps to one of these four types — naming them explicitly makes the workflow legible and prevents "what happens when this check fails?" confusion.
Adapted from the GSD (Get Shit Done) project's gates reference — MIT © 2025 Lex Christopherson ([gsd-build/get-shit-done](https://github.com/gsd-build/get-shit-done)).
## The four gate types
### 1. Pre-flight gate
**Purpose:** Validates preconditions before starting an operation.
**Behavior:** Blocks entry if conditions unmet. No partial work created — bail before anything changes.
**Recovery:** Fix the missing precondition, then retry.
**Examples:**
- Implementation phase checks that the plan file exists before it starts writing code.
- Delegated subagent checks that required env vars are set before making API calls.
- Commit checks that tests passed before pushing.
### 2. Revision gate
**Purpose:** Evaluates output quality and routes to revision if insufficient.
**Behavior:** Loops back to the producer with specific feedback. Bounded by an iteration cap (typically 3).
**Recovery:** Producer addresses feedback; checker re-evaluates. The loop escalates early if issue count does not decrease between consecutive iterations (stall detection). After max iterations, escalates to the user unconditionally — never loop forever.
**Examples:**
- Plan reviewer reads a draft plan, returns specific issues, planner revises, reviewer re-reads (max 3 cycles).
- Code reviewer checks subagent-produced code against must-haves; dispatches fixes back to the implementer if any must-have failed.
- Test coverage checker validates new tests exercise the new paths; if not, sends back to author.
### 3. Escalation gate
**Purpose:** Surfaces unresolvable issues to the human for a decision.
**Behavior:** Pauses workflow, presents options, waits for human input. Never guesses, never picks a default.
**Recovery:** Human chooses action; workflow resumes on the selected path.
**Examples:**
- Revision loop exhausted after 3 iterations.
- Merge conflict during automated worktree cleanup.
- Ambiguous requirement — two reasonable interpretations and the choice changes the approach.
- Subagent reports "the plan says X but the codebase actually does Y" — human decides which is right.
### 4. Abort gate
**Purpose:** Terminates the operation to prevent damage or waste.
**Behavior:** Stops immediately, preserves state (checkpoint current progress), reports the specific reason.
**Recovery:** Human investigates root cause, fixes, restarts from checkpoint.
**Examples:**
- Context window critically low during execution (POOR tier, >70%) — abort cleanly rather than produce truncated output.
- Critical dependency unavailable mid-run (network down, API key revoked).
- Unrecoverable filesystem state (disk full, permissions lost).
- Safety invariant violated (agent attempted an irreversible destructive action outside approved scope).
## How to use this in a skill
When you write an orchestration skill that has validation checkpoints, **name each checkpoint by its gate type explicitly** and answer three questions:
1. **What condition triggers this gate?** (e.g., "plan file missing", "issue count didn't decrease", "context >70%")
2. **What happens when it fails?** (block / loop back / ask human / abort)
3. **Who resumes, and from where?** (fix precondition + retry, revise + re-check, human decision, restart from checkpoint)
Answering these three up front means your skill never hits "what do we do now?" at runtime.
## Example — a review loop with all four gate types
```
[Pre-flight] plan.md exists and is non-empty? → no: bail, ask user to write a plan first
↓ yes
[Execute] subagent implements task
[Revision] reviewer checks against must-haves → fail: loop back to subagent (max 3)
↓ pass
[Pre-flight] tests pass? → no: bail, report failing tests
↓ yes
[Commit]
(on revision loop exhaustion)
[Escalation] "3 review cycles failed to converge on issue X — pick: force-merge, rewrite task, abandon"
↓ user picks
(on any tier-POOR context pressure during loop)
[Abort] "context at 73%, checkpointing and stopping"
```
The vocabulary is small on purpose. Every gate in every workflow should fit one of these four. If you find yourself inventing a fifth, it's probably a revision gate with extra branching, or an escalation gate in disguise.
@@ -0,0 +1,75 @@
# PocketBase Data Normalization Pitfalls
## JSON fields come back as strings
PocketBase's `json` field type does NOT guarantee parsed JSON arrays on read. The SDK may return JSON fields as **serialized strings** instead of parsed arrays/objects. Always normalize after fetching:
```typescript
const records = await pb.collection('repairOrders').getList(1, 200, { ... });
const normalized = records.items.map((item: any) => {
let services = item.services || [];
if (typeof services === 'string' && services.trim()) {
try { services = JSON.parse(services); } catch { services = []; }
}
if (!Array.isArray(services)) services = [];
return { ...item, services };
});
```
**Symptom:** `e.reduce is not a function` or `e.map is not a function` on fields you expected to be arrays.
**Also affects:** edit modals that pre-populate from fetched records — apply the same normalization before `setState()`.
## Empty/malformed JSON strings
Even when a JSON field has a value, it might be an empty string `""` or partially corrupted. JSON.parse on these throws `"Unexpected end of JSON input"`. Always wrap in try/catch:
```typescript
try { parsed = JSON.parse(raw); } catch { parsed = []; }
```
## Missing system fields (created, updated)
Some PocketBase collections lack the standard `created`/`updated` auto-fields. This happens when collections were created via raw SQL or imported. **Symptom:** queries with `sort: '-created'` or `fields: '...,created'` return 400 errors.
**Fix:** Use `sort: '-id'` (PocketBase IDs are time-sortable) and avoid requesting `created`/`updated` in the `fields` parameter.
**Detection:** Test with `curl` first:
```bash
curl -s "http://127.0.0.1:8091/api/collections/NAME/records?sort=-created&perPage=1" \
-H "Authorization: $TOKEN"
```
If it returns 400, the collection lacks `created`.
## Collection naming conventions
PocketBase collection names are case-sensitive. `repairOrders` and `repair_orders` are different collections. When porting from one naming convention to another, test each collection name directly against the API.
**Detection:**
```python
for name in ['repairOrders', 'repair_orders', 'repairorders']:
r = fetch(f'http://127.0.0.1:8091/api/collections/{name}/records?perPage=1')
print(f"{name}: {'EXISTS' if r.status == 200 else 'MISSING'}")
```
## PocketBase SDK error structure
The `ClientResponseError` thrown by the PocketBase JS SDK has this shape:
```
error.message → top-level message ("Failed to create record.")
error.status → HTTP status (400)
error.response → full API response body: {
data: { email: { message: "Value must be unique." } },
message: "Failed to create record.",
status: 400
}
```
For field-level validation errors, access `error.response.data`. Do NOT assume `error.data` is the field errors — in some SDK versions `error.data` is an alias for `error.response` (the full response body), so field errors are at `error.response.data` or `error.data.data`.
Simple reliable pattern:
```typescript
const message = err instanceof Error ? err.message : 'Failed';
```
The PocketBase SDK's `.message` already includes the user-facing error text.
@@ -0,0 +1,126 @@
# PocketBase SDK Path Construction
## The double `/api` trap
When building a reverse proxy for a PocketBase-backed SPA, the PB JS SDK constructs its own API paths. Understanding this avoids the most common proxy bug.
### How the PB SDK builds URLs
The PocketBase JS SDK's `buildURL` method concatenates `baseURL` + `path`:
```js
// If you create: new PocketBase('/pb')
// Then calling pb.collection('users').authWithPassword(email, password)
// SDK builds: GET /pb/api/collections/users/auth-with-password
// ^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// baseURL SDK-added /api/ path
```
The SDK ALWAYS prepends `/api/` to its paths. Your proxy must strip only the `/pb` prefix and forward the rest as-is — including the `/api/` that the SDK already added.
### The bug
```python
# WRONG — this adds a second /api/, producing /api/api/collections/...
if self.path.startswith('/pb'):
url = PB_BACKEND + '/api' + self.path[3:] # DOUBLE /api!
```
PocketBase returns `404 {"message": "The requested resource wasn't found."}` for the double-path URL, which is the same error message as genuinely missing resources — making this a silent, confusing failure.
### The fix
```python
# CORRECT — preserve the original path, which already includes /api/
if self.path.startswith('/pb'):
url = PB_BACKEND + self.path[3:] # /pb/X → /X on backend
```
## Python `http.server` path behavior
`BaseHTTPRequestHandler.path` includes the query string. When forwarding, the query string is already part of the path. `urllib.request.Request(url)` handles query strings correctly, so no special parsing is needed.
## PocketBase admin auth
PocketBase admin/superuser authentication is version-dependent:
- **v0.22+**: `POST /api/collections/_superusers/auth-with-password`
- **Older**: `POST /api/admins/auth-with-password` (may be a 404)
- The admin dashboard is at `/_/` (web UI only, not an API endpoint)
If neither endpoint works, check the PocketBase version. The admin user must be created via the `./pocketbase superuser` CLI command or through the web UI on first run.
## PocketBase Query Field Errors (400 "Something went wrong")
PocketBase returns `400 {"message": "Something went wrong while processing your request."}` when a query references fields that don't exist in the collection schema. This is NOT a 404 — it's a 400, and the error message is generic, making it easy to misdiagnose as a permissions or setup issue.
### The `created` / `updated` system field trap
Collections created via the PocketBase API (rather than the admin UI) may lack the `created` and `updated` system fields. Any query that sorts or filters on these fields will fail with 400.
**Symptom:** `sort: '-created'` succeeds via curl against a valid collection but returns 400 when tested against the same collection on a different PB instance. The collection EXISTS but the query fails because the field doesn't.
**Detection:** Test the exact query parameters one at a time:
```python
# Isolate which parameter breaks
# Test: ?page=1 (should work)
# Test: ?page=1&sort=-created (fails → field missing)
# Test: ?page=1&fields=created (may fail or be silently ignored)
```
**Workaround:** Replace `sort: '-created'` with `sort: '-id'` — PocketBase record IDs are sortable and roughly chronological. For `fields`, remove any references to `created`/`updated` if the collection lacks them.
**Root cause:** Subagents building against one PB instance may use system fields that work there, but the target deployment's PB instance has collections created via API without auto-system-fields enabled.
### Collection naming mismatches
PocketBase collection names are case-sensitive and can use either camelCase (`repairOrders`) or snake_case (`repair_orders`) depending on how they were created. The API silently returns 404 for the wrong case, and the error message is identical to a genuinely missing collection. Always verify the exact collection name by querying `GET /api/collections` before assuming a collection doesn't exist.
## PocketBase Sign-Up Error Handling
When `pb.collection('users').create()` fails, the PocketBase SDK throws a `ClientResponseError` with a generic top-level `.message` (e.g., `"Failed to create record."`) and field-level details in `.data`:
```json
{
"data": {
"email": {"code": "validation_not_unique", "message": "Value must be unique."}
},
"message": "Failed to create record.",
"status": 400
}
```
**Never use the raw `.message` directly** — it's always `"Failed to create record."` for any validation failure. Instead, unwrap `.data` to show the user which field failed:
```typescript
} catch (err: unknown) {
let message = 'Failed to create account.';
if (err && typeof err === 'object' && 'data' in err) {
const pbErr = err as { data?: Record<string, { message: string }> };
if (pbErr.data) {
const fieldErrors = Object.entries(pbErr.data)
.map(([field, info]) => `${field}: ${info.message}`)
.join('; ');
if (fieldErrors) message = fieldErrors;
}
}
setError(message);
}
```
This produces user-friendly messages like `email: Value must be unique.` instead of the unhelpful `Failed to create record.`
Common PocketBase validation errors on user creation:
- **email not unique** → `email: Value must be unique.`
- **password too short** → `password: Must be at least 8 characters.`
- **missing required field** → `email: Cannot be blank.`
## PocketBase SMTP Configuration
PocketBase requires SMTP to be configured for email verification to work. Check the `_params` table in the PocketBase data directory:
```sql
SELECT value FROM _params WHERE id='settings'
```
If `smtp.enabled` is `false`, verification emails will never send. Configure SMTP via the PocketBase admin UI (`/_/`) or by updating the `_params` row directly with valid SMTP credentials (host, port, username, password, TLS).
@@ -0,0 +1,46 @@
# PB Migration Sequencing Pitfalls (Subagent-Driven Development)
## The Problem
When a plan involves creating/updating PocketBase collections AND frontend UI in the same batch, the frontend code inevitably references the new collection name. If the PB migration hasn't been applied yet, the frontend will:
- Type-check fine (no compile-time dependency on PB schema)
- Build fine (no import dependency)
- **Crash at runtime** with "Missing collection" errors
## When This Happens
The standard batch-parallel pattern (`delegate_task` with independent goals) is susceptible because:
1. Subagent A creates `pb_migrations/M25_new_collection.js`
2. Subagent B writes `pb.collection('newCollection').getList(...)` in a UI component
3. Both execute in parallel — neither knows about the other
4. The PB migration still needs to be `docker cp`'d and `migrate up` applied
5. The frontend code is deployed, but the collection doesn't exist in PB yet
## Mitigation Strategy
**For the agent orchestrating the batch:**
- **Deploy PB migrations BEFORE dispatching frontend subagents.** Wrap the sequence as:
1. Write all PB migration files
2. Copy to container and `migrate up`
3. Verify success with `docker exec` / `--dir` check
4. THEN dispatch frontend subagents
- **If forced to parallelize** (e.g., PB and frontend for different features are interleaved), add a guard in the frontend subagent's context: "The collection [name] already exists in PB — query it directly." Verify the collection exists first with a curl health check before dispatching.
- **For deployment safety**, add a try/catch around PB queries in new frontend code:
```typescript
try {
const records = await pb.collection('newCollection').getFullList({...});
} catch (err) {
console.warn('[newCollection] not available yet');
return [];
}
```
## Why This Is Tricky
Unlike module imports (which fail at build time), PB collection references are runtime API calls. The JS bundler doesn't know about them. In the SPQ-v2 project:
- All PB queries go through `pb.collection('name').method()`
- The collection name is a runtime string — no compile-time validation
- Missing collections produce a `404` response from PB, which the frontend may or may not handle gracefully
@@ -0,0 +1,54 @@
# React Component Extraction — Mega-File Split Pattern
## When you need this
A single `.tsx` file has grown past ~1,000 lines with inline sub-components, inline helper functions, and inline type definitions. The established pattern is:
1. **Pure helpers → `src/lib/<domain>.ts`** — formatters, PB query builders, status label/color mappers, date formatters
2. **Types → shared module** — interfaces can stay in the page file if re-exported (for external importers), or move to `src/components/<domain>/types.ts`
3. **Inline components → `src/components/<domain>/`** — one file per component, barrel `index.ts`
4. **Page becomes orchestrator** — imports from barrel, keeps all state + handlers
## Proven pattern (from Appointments.tsx, Customers.tsx splits)
| Extraction target | Destination | Pattern |
|---|---|---|
| `function HelperFn(...)` | `src/lib/<domain>.ts` | Pure function, no hooks, no JSX |
| `interface X` | `src/components/<domain>/types.ts` (or re-export from page) | Re-export from page if external files import from the old path |
| `function Skeleton()`, `function EmptyState()` | `EmptyStates.tsx` | Plain function component |
| `function Card(props): JSX` | `<Name>Card.tsx` | `memo(function NameCardImpl(props) { ... })` |
| `function FormModal(props)` | `<Name>FormModal.tsx` | Keep as-is (has internal state — memo adds nothing) |
| `function DetailView(props)` | `<Name>DetailView.tsx` | `memo(function DetailViewImpl(props) { ... })` |
| `function DeleteConfirmModal(props)` | `DeleteConfirmModal.tsx` | Keep as-is |
| Barrel | `index.ts` | Re-export all components and types |
## Import compatibility pattern
When external files import types from the old page path (e.g., `import type { CustomerWithVehicles } from '../pages/Customers'`), the page MUST re-export those types:
```tsx
// src/pages/Customers.tsx — at the top level
export type { CustomerRecord, CustomerWithVehicles, CustomerFormData }
from '../components/customers/types';
```
Do NOT change the import paths in the 5 external files — the re-export keeps them working.
## Pitfalls discovered during extraction
### 1. Fragment shorthand → Fragment key mismatch
The original file uses `<>...</>` (no key prop). The extracted equivalent needs `<Fragment key={expr}>`. But the closing paren/bracket structure differs because the Fragment opening tag is part of the JSX tree rather than being a structural wrapper.
**Trace template:**
```
Original: return ( <><tr/><tr/></> );
After extract: return ( <Fragment key={x}><tr/><tr/></Fragment> );
```
The closing `));` after `</>` in a `.map()` callback becomes `);` after `</Fragment>`, then `})}` for block-end, `.map()` close, JSX expression close. **Count explicitly.**
### 2. `useCallback` dependency drift
When moving a handler from the orchestrator page that references a callback defined in the same component, the `useCallback` deps array may reference a function that's now imported. The import is stable (never changes identity), so the dep IS needed in the array to be correct, but TypeScript won't warn either way. **Check:** every `useCallback(fn, deps)` where `fn` calls an imported function — that imported function should be in the deps array, or the callback is stale.
### 3. `formatDate` name collision
The page likely has its own `function formatDate(...)` helper. The extracted lib module uses the same name. Rename to `formatCustomerDate` or `formatDomainDate` — the app's `src/lib/format.ts` may already export a global `formatDate`.
@@ -0,0 +1,50 @@
# React Runtime Crash Patterns
## Invalid Date → RangeError (white screen)
`new Date(undefined)` or `new Date(null)` creates an Invalid Date. Calling `.toLocaleDateString()` or `Intl.DateTimeFormat.format()` on it throws `RangeError: Invalid time value` — an uncaught runtime error that blanks the page.
**Fix:** Guard all date formatting functions:
```typescript
function formatDate(iso: string | undefined | null) {
if (!iso) return '—';
return new Intl.DateTimeFormat('en-US', { ... }).format(new Date(iso));
}
```
**Detection:** grep for `new Date(` calls in page components and verify each has a null/undefined guard.
## Nested component re-mount (input focus loss)
Defining a child component as a nested function inside a parent causes React to destroy/recreate it on every parent render. If the parent re-renders from a state update (e.g., zustand store change while typing), the input loses focus on every keystroke.
**Symptom:** Each character typed dismisses focus; user must click back into the field for the next character.
**Fix:** Extract to a file-level `memo` component:
```typescript
const ServiceRow = memo(function ServiceRow({ service, onUpdate, ... }: Props) {
return <input value={service.price} onChange={e => onUpdate({ price: parseFloat(e.target.value) })} />
});
```
**Anti-pattern (causes focus loss):**
```typescript
function Parent() {
const Child = ({ item }) => <input ... />; // New identity every render
return items.map(i => <Child key={i.id} item={i} />);
}
```
## Blank screen with empty console
When a React lazy-loaded chunk has an import error, the page shows blank with zero console errors. The error is swallowed by the Suspense boundary. **Fix:** temporarily switch to eager imports in App.tsx to surface the error, then revert to lazy once fixed.
## tsc clean ≠ runtime clean
TypeScript compilation success does not catch:
- JSON.parse on potentially-malformed strings
- `new Date(undefined)` → RangeError
- `.reduce()` / `.map()` on non-array values
- Missing context providers (useToast without ToastProvider)
These all surface as blank pages or runtime crashes. After parallel fan-outs, smoke-test each page by navigating to its URL.
@@ -0,0 +1,121 @@
# Local SPA Proxy Server Pattern
When testing a React/Vite SPA locally that needs to reach a backend API on a different port (e.g., PocketBase on 8091, or any API server), use a Python HTTP server that serves static files AND proxies API paths to the real backend.
## Problem
SPAs built with Vite often reference backend APIs at relative paths like `/pb` or `/api`. When served from a simple `python3 -m http.server`, these requests go to the wrong origin and fail with 404 or CORS errors.
## Solution
A ~60-line Python script that:
1. Serves static files from the `dist/` directory
2. Proxies API paths (`/pb/*`, `/api/*`) to the real backend
3. Adds CORS headers to all responses
4. Handles SPA fallback (serves `index.html` for unknown paths)
5. Supports GET, POST, PUT, PATCH, DELETE, OPTIONS methods
## Template
Save as `serve-proxy.py` in the project root:
```python
#!/usr/bin/env python3
"""Serve SPA dist + proxy /pb to backend."""
from http.server import HTTPServer, SimpleHTTPRequestHandler
import urllib.request, os
DIST = './dist'
BACKEND = 'http://127.0.0.1:8091'
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIST, **kwargs)
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
super().end_headers()
def do_OPTIONS(self):
self.send_response(204)
self.end_headers()
def do_GET(self):
if self.path.startswith('/pb') or self.path.startswith('/api'):
self.proxy()
else:
full = os.path.join(DIST, self.path.lstrip('/'))
if not os.path.exists(full) or os.path.isdir(full) and self.path != '/':
self.path = '/index.html'
super().do_GET()
def do_POST(self): self.proxy() if self.is_api_path() else self.send_error(405)
def do_PUT(self): self.proxy()
def do_PATCH(self): self.proxy()
def do_DELETE(self): self.proxy()
def is_api_path(self):
return self.path.startswith('/pb') or self.path.startswith('/api')
def proxy(self):
url = BACKEND + self.path
# Strip proxy prefix if the SDK already includes it
if self.path.startswith('/pb'):
url = BACKEND + self.path[3:]
if not url.startswith(BACKEND + '/api'):
url = BACKEND + '/api' + self.path[3:]
body = None
if self.headers.get('Content-Length'):
body = self.rfile.read(int(self.headers['Content-Length']))
req = urllib.request.Request(url, data=body, method=self.command)
for k, v in self.headers.items():
if k.lower() not in ('host', 'connection', 'origin', 'referer'):
req.add_header(k, v)
try:
resp = urllib.request.urlopen(req)
self.send_response(resp.status)
for k, v in resp.headers.items():
if k.lower() not in ('transfer-encoding', 'connection'):
self.send_header(k, v)
self.end_headers()
self.wfile.write(resp.read())
except urllib.error.HTTPError as e:
self.send_response(e.code)
self.end_headers()
self.wfile.write(e.read())
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 4173), Handler)
print(f'Serving on http://0.0.0.0:4173 → {BACKEND}')
server.serve_forever()
```
## Common Pitfalls
### Double `/api` prefix
The PocketBase JS SDK constructs paths like `/pb/api/collections/...`. The proxy must strip `/pb` and append the remainder, NOT add another `/api/`. Check what paths the SDK actually builds by inspecting the built JS or testing with curl.
### PocketBase SDK adds `/api/` to the base URL
`new PocketBase('/pb')` builds URLs as: `/pb/api/collections/...`. The SDK always inserts `/api/` between the base URL and the collection path.
### Missing CORS on error responses
If `urllib.error.HTTPError` is caught, the error response body is written but CORS headers from `end_headers()` may not be called in the right order. Ensure `end_headers()` is called before `wfile.write()` in error paths.
### Empty browser console errors
React lazy-loaded chunk failures often produce empty exceptions in browser consoles. If a page renders blank with no visible error, revert to eager imports temporarily to surface the actual error message.
## Usage
```bash
# Start server in background
python3 serve-proxy.py &
# Access at http://localhost:4173 or http://<lan-ip>:4173
```
## When to Use
- Testing a local SPA build that needs API access
- Debugging frontend-backend integration without nginx reverse proxy
- Quick demo serving with `python3 -m http.server` + CORS + API proxy