Phase 45 — feature wave: vet geolocation, stable self-update, correction-learning, reports fix#260
Conversation
Shared/contended layer for the 4-item deferral-backlog wave, centralised to keep the per-item build agents on disjoint files: - schema: Staff geolocation columns (KI-016 L); StableUpdateRequest model + StableUpdateStatus enum + Customer/Yard/Staff back-relations (D-1) - migration 20260613120000_phase45_feature_wave (additive, idempotent) - i18n EN/FR: planning.location*, nav.stableUpdates, enquiries.aiDialogue edit keys (correction-learning), new stableUpdate.* namespace - lib/auth/session-staff.ts shared getSessionStaff helper (items 1 + 2) prisma validate clean; prisma generate OK; i18n parity tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rm gate (D-1) Item 2 (D-1): upgrade stable-change detection into a structured approval flow. A customer-reported yard change now raises a PENDING StableUpdateRequest alongside the existing LOCATION_CHECK task. Nothing is auto-applied: a vet types/edits the address on /admin/stable-updates and approves, which is the only path that writes a yard. Cross-customer yard ids are refused; the vet-entered address is Zod-validated; inbound text is display-only (never treated as an address); the optional customer ack is best-effort and logged. - lib/validations/stable-update.schema.ts: approve/reject schemas - lib/services/stable-update.service.ts: createFromDetection (idempotent + best-effort ack), approve (yard write + blank geocode + re-geocode, MapsBudgetExceededError defers geocode without failing), reject - auto-triage: additionally raise a StableUpdateRequest (best-effort) - task.service centreView: surface PENDING requests, deep-link /admin/stable-updates - API: GET list (NURSE+), POST approve/reject via getSessionStaff - /admin/stable-updates page + list/card client components + sidebar link - unit tests for the service; existing task/auto-triage tests updated Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…AI gap (§16) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- planner: derive stale-fix banner from a ticked `now` state instead of calling Date.now() during render (react-hooks/purity) - QualificationPanel: reseed the editable draft via the render-time prev-prop pattern instead of a setState-in-effect (react-hooks/set-state-in-effect) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e wave - BUILD_PLAN: Phase 45 entry (4 items, scope, verification) - KNOWN_ISSUES: Phase 45 section; KI-016 L closed; KI-029/KI-030 accepted-low - CLAUDE.md: current-state header advanced to Phase 45 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (KI-016 L review)
…bel, i18n fallback (D-1 review)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 6 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 39b7b92. Configure here.
| if (watchIdRef.current !== null && navigator.geolocation) { | ||
| navigator.geolocation.clearWatch(watchIdRef.current); | ||
| } | ||
| watchIdRef.current = null; |
There was a problem hiding this comment.
Server sharing stays on after denial
Medium Severity
When the browser geolocation watch fails (permission denied or unavailable), the UI moves to denied/unavailable and clears the watch, but it never PATCHes enabled: false to /api/staff/me/location. Route generation reads persisted sharing flags server-side, so generate can still use a stale live origin while the vet believes location is off.
Reviewed by Cursor Bugbot for commit 39b7b92. Configure here.
| } else if (edited) { | ||
| // The edit was recorded as a training example for future drafts. | ||
| setSavedCorrection(true); | ||
| } |
There was a problem hiding this comment.
False correction saved on 409
Low Severity
After approve-draft, HTTP 409 (NO_DRAFT) is treated like success for edited drafts: the UI can show the green “correction saved” message even though nothing was sent and no feedback row is written when there is no queued draft.
Reviewed by Cursor Bugbot for commit 39b7b92. Configure here.
| <span className="font-medium text-foreground"> | ||
| {request.customer?.fullName ?? t('customerFallback')} | ||
| </span> | ||
| <span>{t('requestedAt', { date: requestedAt })}</span> |
There was a problem hiding this comment.
Stable card uses bare locale dates
Low Severity
The stable-update card formats createdAt with toLocaleString() in a client component, so French locale users get browser-default (often English) datetime strings instead of next-intl formatting.
Triggered by learned rule: Flag bare Date locale methods in client pages — use next-intl useFormatter
Reviewed by Cursor Bugbot for commit 39b7b92. Configure here.
| <blockquote className="mt-2 rounded-md border-l-4 border-primary/40 bg-primary/5 p-3 text-sm whitespace-pre-wrap"> | ||
| {draftText} | ||
| </blockquote> | ||
| <label htmlFor="qualification-draft" className="mt-2 block text-xs font-medium text-muted"> |
There was a problem hiding this comment.
Draft timestamp uses bare locale
Low Severity
The qualification panel still renders draftQueuedAt with toLocaleString() after the §16 edit changes, so queued-draft timestamps ignore the user’s EN/FR locale.
Triggered by learned rule: Flag bare Date locale methods in client pages — use next-intl useFormatter
Reviewed by Cursor Bugbot for commit 39b7b92. Configure here.
| dueAt: null, | ||
| assignedTo: null, | ||
| href: '/admin/stable-updates', | ||
| overdue: false, |
There was a problem hiding this comment.
Missing task centre source label
Low Severity
Task centre items now use source STABLE_UPDATE, but tasks.source in EN/FR messages was not extended. TaskCard renders t('source.STABLE_UPDATE'), so users see a missing-key string instead of a translated badge.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 39b7b92. Configure here.
| // Default the selected yard to the request's stored yard, else the first | ||
| // active yard, else none. | ||
| const initialYardId = request.yardId ?? yards[0]?.id ?? ''; | ||
| const [yardId, setYardId] = useState(initialYardId); |
There was a problem hiding this comment.
Deleted yard id breaks approve
Low Severity
The yard selector initializes yardId from request.yardId even when that yard is tombstoned and absent from the active yards list, so approve can POST a deleted yard id and fail until the vet manually re-selects a yard.
Reviewed by Cursor Bugbot for commit 39b7b92. Configure here.
* docs: refresh current-state to Phase 46 (KI-016 O merged in #263) - CLAUDE.md current-state header: add the Phase 46 bullet (intake→completion e2e demo-spine + CI Postgres job, KI-031), bump phase history 0–45 → 0–46 and the KI range to KI-031; correct the Phase 45 bullet to merged (#260). - docs/BUILD_PLAN.md: add the Phase 46 entry (scope, the no-integration-bug finding, the three corrected status-map facts, verification, #261→#263). - .claude/memory.md: capture the GitGuardian-ephemeral-CI-cred lesson and the clean-history rebuild workaround (merge --squash → fresh branch/PR) used to clear it when the git-proxy blocks force-push. Docs only; no code or schema. Final merge left to Richard / Freddie. https://claude.ai/code/session_01VzJJTUcvzZgS9jN8aap9iv * docs: describe the flagged CI value instead of quoting it (Bugbot #264) The GitGuardian lesson bullet quoted the literal old POSTGRES_PASSWORD value, which would let GitGuardian re-flag it on this very PR — the exact loop the #263 rebuild closed. Describe the value instead of pasting it. https://claude.ai/code/session_01VzJJTUcvzZgS9jN8aap9iv --------- Co-authored-by: Claude <noreply@anthropic.com>


Phase 45 — feature wave (deferral backlog)
Four independent deferred-backlog items, delivered as one PR. Built via parallel
git-worktree subagents over a centralised schema/i18n scaffolding commit, then
hardened by an adversarial multi-agent review pass (six streams, every finding
independently verified before it counted).
What's included
1. Vet geolocation opt-in (KI-016 L) — an opt-in live vet location becomes the route origin to sharpen proposals.
Staffcolumns (locationSharingEnabled,lastKnownLat/Lng,lastLocationAt).PATCH /api/staff/me/location; the planner toggle drives the browser Geolocation API.resolveRouteOriginand enforces a 30-minute staleness window server-side (home base otherwise) — the client never supplies the origin. The n8n generate path always uses the home base.2. Customer stable self-update (D-1) — the app's only customer-initiated write, vet pre-confirm gated.
StableUpdateRequest(PENDING → APPROVED/REJECTED).$transaction) yard write + status flip, re-geocode post-commit, fully audited — or rejects with a reason./admin/stable-updates+ the task centre.3. Correction-learning extension (§16) — the supervised qualification draft is now editable; the vet's edits are captured as
AiDecisionFeedback{decisionType:'QUALIFICATION_DRAFT'}and fed back as few-shot examples into future drafts. The deterministic demo/no-key fallback is unchanged.4.
/reportstotal-billed fix — the billed aggregate / trend / lag queries now filterstatus: { notIn: ['DRAFT','SUPERSEDED','CANCELLED'] }, so amended (SUPERSEDED), CANCELLED and unissued DRAFT invoices no longer double-count or inflate "total billed" (= issued live documents).Schema / migration
20260613120000_phase45_feature_wave(Staff geo columns +StableUpdateRequest/StableUpdateStatus). Guarded for adb push-historical production per KI-028.Gates (all green on the integrated tree)
lint✅ ·typecheck✅ ·prisma validate✅ ·build✅ ·test✅ (~2,831 passing, 4 skipped; one timing-sensitiveloggertest flaked once under concurrent build CPU load and passes in isolation).Adversarial review (self-run, six streams)
10 findings confirmed, 1 dropped. Fixed: server-authoritative geolocation + route-handler tests; atomic stable-update approve; logged-only ack; reject-toggle label; DRAFT exclusion; unpaid-query boundary test. Accepted + documented: KI-029 (StableUpdateRequest dedupe race — single-practice, cosmetic), KI-030 (§16 correction examples not language-scoped — tone-only; the system-prompt hard-rule prevents any output-language flip).
Decisions / notes
Docs updated:
docs/BUILD_PLAN.md(Phase 45),docs/KNOWN_ISSUES.md(Phase 45 section + KI-016 L closed + KI-029/KI-030),CLAUDE.mdbuild-state header.🤖 Generated with Claude Code