initial commit
This commit is contained in:
+53
@@ -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.
|
||||
+75
@@ -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.
|
||||
+126
@@ -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).
|
||||
+46
@@ -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
|
||||
+54
@@ -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`.
|
||||
+50
@@ -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.
|
||||
+121
@@ -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
|
||||
Reference in New Issue
Block a user