Phase 3a (1/3): CRM services + API, schema-parity instrument - #3
Conversation
CRM: 18/18 v1 operations (phone calls, recordings with ETag/304, endpoints CRUD, provider settings), exact-URL pins for the external provider portal, requests-based client, celery tasks + beat-in-code, 48 tests. Infrastructure: scripts/schema_parity_diff.py (live OpenAPI vs v1's frozen contract, ledger-aware, wire-level) wired into CI. Stacked 1/3; company impl and tests follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4BKR43FfAFod7TFXMe222
Warning Review limit reached
Next review available in:8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (22)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Recording-download params: null origin/destination stays empty (v1 omit-on-None semantics), not the literal string None (provider-boundary parity). - Phone-endpoint number stripped as v1's serializer did. - schema_parity_diff.py: ratcheting baseline of ever-matched operationIds (scripts/schema-parity-baseline.txt) - a ported operation vanishing is now DRIFT, not 'not yet ported'; malformed ledger entries hard-fail; --update-baseline grows the file deliberately. (Reviewer caught the instrument's regression blindness.) - Ledger: loose v1 query values (page=last, yes-booleans) now 422. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4BKR43FfAFod7TFXMe222
Uh oh!
There was an error while loading. Please reload this page.
* Purchasing PO create/list/detail: po-created-by green PoCreatePage (CompanyLookup + reference + save -> 201 redirect), PoListPage, PoDetailPage with PoSummaryCard (created-by input, reference autosave, status select), usePoLines over the single PATCH endpoint, and the missing 'po' concurrency invalidator so 412/428 recovery actually refetches. Ports createTestPurchaseOrder + waitForPoAutosave and the po-created-by spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * PO lines grid: create-purchase-order green PoLinesTable as the third grid on the useReactTable + useDraftRows pattern (phantom row, no add-line button, row-exit draft commit — unit-cost stays the row's last focusable cell so the spec's Tab exits the row). ItemSelect generalised for stock-only consumers (optional jobId/line, label + wrapper overrides; labour-rates query gated on jobId presence only, since textOnly labels need rate names). Inline JobSelect over purchasing_all_jobs_retrieve — the unfiltered endpoint v1's PO page uses, because fresh jobs are draft and the filtered sibling excludes them. Spec ported with the autosave waiter armed before the pick/status clicks (v2 saves immediately; v1's debounce is what made arm-after work). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Stock page: stock-search green Spec-lean StockPage: full active-stock list on load, 300ms-debounced server FTS from 3 characters, enabled-gated so clearing the box renders the cached list with no /search/ request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Rewrite-status: purchasing slice recorded (29 of 40) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Unit tests for PO line draft rules and JobSelect filtering Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Unify the table layer: shared DataTable/ListTable, no cross-domain imports The purchasing slice's PoLinesTable was about to become a third hand-rolled grid emitting the DataTable-row-N/data-grid-* contract inline. features/shared/DataTable.tsx is now the one owner of that contract; SmartTimesheetTable, CostLineGrid and PoLinesTable all render through it. Auditing further found the same pathology in the plain list pages: PoListPage and StockPage (both new this slice) had copied CompaniesListPage's table shell and loading/error/retry block verbatim, taking an existing duplicate (also in WipReportPage) from 2 instances to 4. features/shared/ListTable.tsx is the one owner of that block instead -- deliberately separate from DataTable, since it has no react-table dependency and forcing static lists through column-def machinery would be indirection, not rigor. A hand-rolled debounce-into-query-state pattern in CompaniesListPage and StockPage is now features/shared/useDebouncedValue.ts (KanbanSearchInput keeps its own URL-driven debounce -- a different concept). ItemSelect, the decimal helpers and the Save-failed badge move to features/shared/ because purchasing consumed them cross-domain -- a domain feature is not a library. ADR 0039 strengthened: unification is never deferred, shared concepts get shared homes, the bar is reference quality. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Review fixes: QueryState primitive, item-label bug, company relocated Adversarial review on the branch found the ListTable audit had stopped short: JobMovementReportPage and CompanyDetailPage hand-rolled the same loading/error/retry block ListTable was built to own (6 real instances, only 4 fixed). Split the block itself out as features/shared/QueryState.tsx -- the pending/error gate alone, no table -- so it fits pages that show something other than a table too. ListTable now composes QueryState instead of duplicating it. PoDetailPage, CostLineGrid, JobMovementReportPage and CompanyDetailPage all render through it. Fixed: PoLinesTable's item-picker label read `item_code ?? 'Select Item'` with no description fallback, so a bound stock item with a null code (nullable, v1 parity) misread as unbound. Now poLineItemLabel() in lines.ts, unit-tested. Moved features/company to features/shared/company: it was already cross-imported by features/job (JobCreatePage, JobSettingsTab) before this slice added a third importer (purchasing/PoSummaryCard) -- it never had a route of its own, a shared widget library in a domain-shaped box. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * ADR 0039: name the deadline-vs-duplicate resolution explicitly Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Docs: Slice 3 (SSE + serving model) is MUST before cutover, not deferred Reversed 2026-08-11. The 2026-08-10 record said SSE ships with the production-serving decision, filed under "Post-cutover -- decided, deliberately NOT before 15 August." That was overturned: racing bad architecture into production defeats the point of the rewrite (non-negotiable #3 in the Cutover section), and the interim polling shape plus the un-runnable apps/xero/sync_stream.py view are exactly that. Slice 3 -- live updates done properly (serving model fix + SSE ticker + discard the interim shortcuts) moves to its own MUST-tier section with a milestone checkbox, and every stale "deferred post-cutover" cross-reference in the file is corrected. The purchasing-slice PR is unaffected -- it is unrelated and merges as planned; this correction only fixes the durable record for the next session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Third review pass: 4 more QueryState conversions; rewrite-status cleanup QueryState grew optional loadingNode/errorNode override props so a spinner-based caller keeps its visual shell instead of losing it to the plain-text default. JobFinishTab, DailyOverviewPage, and both gates in TimesheetEntryPage's EntryWorkspace now render through it. XeroQuoteCard, JobInvoiceCard and JobSettingsTab's pay-item field stay excluded -- richer branching than binary success/fail, not the page-level gate QueryState owns. TimesheetEntryPage's own outer gate stays as guard-clause `if` returns, matching CLAUDE.md's stated preference rather than converting to a shape that would abandon it. Also: rewrite-status.md's narration of "review round found X, we fixed Y" replaced with what the outcomes actually are -- which shared component owns which contract, which sites are deliberately excluded and why, which constraints a future change must respect. And the Cutover section now states the actual two-question go/no-go criteria (functional parity, proxied by MUST-tier E2E; materially better architecture, judged directly) rather than only the E2E proxy, with the honest fallback (abort and stay on v1) stated explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Address CodeRabbit review: real bugs and comment cleanup - CompaniesListPage/JobFinishTab: QueryState was passed the raw isError, breaking the first-load-only rule the other converted sites already follow -- a background refetch failure was unmounting already-rendered content instead of leaving it on screen. - JobSelect: closing the picker without a selection (blur or Escape) left `editing` true forever, so the input kept showing the abandoned search term instead of reverting to the bound job; the blur timer was untracked and could fire a state update after unmount. Also added keyboard selection (arrow keys + Enter, role=listbox/option, aria-activedescendant) -- the dropdown was mouse-only. - PoListPage: the row was reachable only by mouse click; added a real Link on the PO number cell, matching CompaniesListPage's existing pattern. - decimal.ts: parseDecimalInput accepted anything Number() parses, including hex/octal/binary literals and bare exponents, and sent them to the wire verbatim as garbage Decimal strings. Validates fixed-point syntax explicitly now. - Stale features/company/* paths in rewrite-status.md's build-order table, left behind by the features/shared/company move. - Comments narrating port history or test intent rewritten to state the rejected alternative and the constraint (ADR 0043) in ItemSelect, SaveFailedBadge, CreateCompanyModal, CompanyLookup; two flagged sites (the E2E-repair-loop comment, the sonner test comment) were already compliant and left as-is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Fix uv.lock/pyproject.toml desync on the django constraint PR #54 (main) widened pyproject.toml's django specifier to >=6.0,<6.2 but never regenerated the matching uv.lock, which still recorded <6.1 in its requires-dist metadata. Every `uv run` this session silently self-corrected the lock locally; committing that fix rather than leaving a lockfile permanently out of sync with the manifest it's supposed to pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Gitignore the root-level test-results/ Playwright writes too frontend/test-results/ was covered; run_e2e.sh's Playwright invocation also writes artifacts relative to the repo root in some invocations, leaving an untracked test-results/ dangling after every E2E run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L * Fix the actual bug behind the stray root test-results/: cwd-relative config playwright.config.ts resolved .env.test, testDir, outputDir and the html reporter's folder relative to process.cwd(). Any invocation whose cwd wasn't frontend/ (npm --prefix from the repo root, a bare npx playwright test) silently missed .env.test -- dropping E2E_TEST_USERNAME/PASSWORD with no error -- or wrote artifacts to the wrong location instead of erroring loudly, which is how a root-level test-results/ kept reappearing. Anchored every one of these to import.meta.dirname instead: verified from an unrelated cwd, .env/.env.test still resolve correctly and no stray directory gets created anywhere. The root-level gitignore entry from the previous commit stays as a defensive backstop, but this is the real fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WENcwU8rC6o7bxuHL6md2L --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
CRM: 18/18 v1 operations with exact-URL pins for the external phone-provider portal; celery tasks + beat-in-code; 48 tests. Infra: schema_parity_diff.py CI gate (wire-level, ledger-aware). Part 1 of the 3a stack.
🤖 Generated with Claude Code
https://claude.ai/code/session_01J4BKR43FfAFod7TFXMe222