Skip to content

Phase 45 — feature wave: vet geolocation, stable self-update, correction-learning, reports fix#260

Merged
RJK134 merged 10 commits into
mainfrom
feature/45-feature-wave-deferrals
Jun 13, 2026
Merged

Phase 45 — feature wave: vet geolocation, stable self-update, correction-learning, reports fix#260
RJK134 merged 10 commits into
mainfrom
feature/45-feature-wave-deferrals

Conversation

@RJK134

@RJK134 RJK134 commented Jun 13, 2026

Copy link
Copy Markdown
Owner

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.

  • Additive nullable Staff columns (locationSharingEnabled, lastKnownLat/Lng, lastLocationAt).
  • VET-gated self-update PATCH /api/staff/me/location; the planner toggle drives the browser Geolocation API.
  • Server-authoritative: the generate route resolves the signed-in vet's persisted fix via resolveRouteOrigin and 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.

  • The existing inbound "update my stable" detector now raises a StableUpdateRequest (PENDING → APPROVED/REJECTED).
  • The vet reviews/edits the proposed address (Zod-validated) and approves — atomic ($transaction) yard write + status flip, re-geocode post-commit, fully audited — or rejects with a reason.
  • Best-effort, always-logged customer acknowledgement. Nothing is auto-applied. Surfaced at /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. /reports total-billed fix — the billed aggregate / trend / lag queries now filter status: { 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

  • Additive, idempotent migration 20260613120000_phase45_feature_wave (Staff geo columns + StableUpdateRequest / StableUpdateStatus). Guarded for a db 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-sensitive logger test flaked once under concurrent build CPU load and passes in isolation).
  • Mobile checked at 390px; EN/FR i18n parity (parity tests pass).

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

  • D-1 is the sanctioned exception to internal-first — the vet approves every yard write; all writes are audited; all outbound messaging is logged.
  • approve/reject are gated NURSE+ (consistent with direct yard edits across the repo; documented in the route docstrings).
  • KI-016 O (full intake → confirmation e2e) remains deferred.

Docs updated: docs/BUILD_PLAN.md (Phase 45), docs/KNOWN_ISSUES.md (Phase 45 section + KI-016 L closed + KI-029/KI-030), CLAUDE.md build-state header.

🤖 Generated with Claude Code

RJK134 and others added 10 commits June 13, 2026 11:46
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>

@RJK134 RJK134 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

@RJK134 RJK134 marked this pull request as ready for review June 13, 2026 13:35
@RJK134 RJK134 merged commit a3f58dd into main Jun 13, 2026
10 checks passed
@RJK134 RJK134 deleted the feature/45-feature-wave-deferrals branch June 13, 2026 13:35

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 6 potential issues.

Fix All in Cursor

❌ 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39b7b92. Configure here.

RJK134 added a commit that referenced this pull request Jun 16, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant