Repository navigation
fix(dashboard): wire upcoming-purchase buttons to plan endpoints (closes #204, #205) - #207
Conversation
… exec endpoints (closes #204, #205) The dashboard's "Upcoming Scheduled Purchases" panel rendered "View Details" / "Cancel" buttons whose data-id was a PurchasePlan.ID and hit /api/purchases/{id} and /api/purchases/cancel/{id} — execution- level endpoints. Those endpoints look the value up in purchase_executions, where no row exists yet for an upcoming purchase (the execution is created when the schedule fires). Result: every click 500'd. PR #206's idempotent migration cleared a downstream symptom (the column-doesn't-exist failure mode), but the upstream data-flow bug remained: CloudWatch (post-#206): [ERROR] API error: execution not found: execution not found: 1cf88fc4-b48e-4303-9ba0-32f000ccaf31 [ERROR] API error: failed to get execution: execution not found: 1cf88fc4-b48e-4303-9ba0-32f000ccaf31 (`1cf88fc4-...` was a plan_id all along.) ## Backend * `internal/api/types.go`: `UpcomingPurchase.ExecutionID` → `UpcomingPurchase.PlanID`; JSON tag `execution_id` → `plan_id`. Doc comment now warns that wiring this to execution-level endpoints is exactly the bug pattern from #204/#205. * `internal/api/handler_dashboard.go:upcomingFromPlan`: emit `PlanID: plan.ID` instead of `ExecutionID: plan.ID`. * `internal/api/handler_dashboard_test.go`: assertion updated. ## Frontend * `types.ts` + `api/types.ts`: `UpcomingPurchase.execution_id` → `plan_id`. * `dashboard.ts`: - Added an in-memory `Map<string, UpcomingPurchase>` populated by `renderUpcomingPurchases`. The dialog renders from this snapshot — no API call is needed because the upcoming-list response carries every field the dialog displays, and there's no execution row to fetch yet. - Buttons now carry `data-id="${p.plan_id}"`. - `viewPurchaseDetails(planId)` is sync; renders the modal via DOM API (createElement + textContent) instead of innerHTML, which closes a class of XSS regressions and avoids the project's innerHTML security hook. - `cancelScheduledPurchase(planId)` calls `api.deletePlannedPurchase(planId)` instead of `api.cancelPurchase(executionId)`. - The in-modal "Cancel Purchase" button (when the modal is open) likewise routes to `deletePlannedPurchase`. * `dashboard.test.ts`: fixtures and mocks updated; the now-impossible "API rejection" failure-mode test for view-details was removed (the call path is sync); cancel tests assert `deletePlannedPurchase` is called with the plan_id. ## Verification * `go build ./...` clean * `go test ./...` — every package green * `tsc --noEmit` clean * `npx jest` — 1386/1386 tests pass (incl. the 18 dashboard tests) Closes #204 Closes #205
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThe pull request refactors how the dashboard identifies and manages upcoming scheduled purchases by switching from Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 3/5 reviews remaining, refill in 16 minutes and 14 seconds. Comment |
✅ Actions performedReview triggered.
|
…ase (CR pass 1) Self-correction: PR #207's first pass routed Cancel to api.deletePlannedPurchase, but that endpoint operates on purchase_executions and would 404 on a plan_id (the same data-flow mismatch this PR was trying to fix, just shifted from /api/purchases/ cancel to /api/purchases/planned). The right plan-level destructive operation is DELETE /api/plans/{id} -> Handler.deletePlan(planID), which calls config.DeletePurchasePlan and stops all future scheduled steps. Changes: * dashboard.ts cancelScheduledPurchase + in-modal Cancel: switch to api.deletePlan(planId). * Confirm-dialog body updated to make the destructive scope explicit ("deletes the plan and stops all future scheduled steps"). * dashboard.test.ts: added deletePlan to the api mock; assertions switched to expect(api.deletePlan).toHaveBeenCalledWith('plan-123'). View Details path is unchanged (already correct: renders from the in-memory upcomingPurchasesIndex, no API call). The "missing data" edge case still degrades gracefully — toast on miss, return early. If you'd prefer a soft-disable that preserves the plan record, swap to api.patchPlan(planId, { enabled: false }) — one-line change. Going with deletePlan to match the user-mental-model of "Cancel" as a destructive stop. Verification: * go test ./... -- all green * tsc --noEmit -- clean * npx jest -- 1386/1386 pass (18 dashboard tests)
|
Self-correction — pushed The first commit on this PR routed Cancel to Now: Cancel calls Confirm-dialog body updated to make the destructive scope explicit ("deletes the plan and stops all future scheduled steps"). If a soft-disable is preferred (preserve the plan record, allow re-enable), swap one line to Edge cases now (in answer to the question that surfaced this):
Verification:
@coderabbitai review |
…ec-id} (#213) * fix(dashboard): cancel upcoming via DELETE /api/plans, not /api/purchases/planned PR #207 wired the dashboard "Upcoming Scheduled Purchases" Cancel button to api.deletePlannedPurchase(planId), but that frontend helper hits DELETE /api/purchases/planned/{id}, whose backend handler still operates on purchase_executions rows via TransitionExecutionStatus. Passing a PurchasePlan.ID matches no execution → 4xx → toast "Failed to cancel purchase". CloudWatch (post-#207 deploy): [ERROR] API error: failed to get execution: execution not found: 1cf88fc4-b48e-4303-9ba0-32f000ccaf31 (`1cf88fc4-…` is a plan_id.) Switch the two callsites in dashboard.ts (inline Cancel button + in-modal Cancel-Purchase button) to api.deletePlan, which targets DELETE /api/plans/{id} → deletePlanHandler → DeletePurchasePlan. That's the right endpoint for "stop this plan from firing its next upcoming step". Tests: dashboard.test.ts updated to assert deletePlan is called and deletePlannedPurchase is not. All 1411 frontend tests pass. Closes #208 (assumed; if filed under a different number, re-link). * fix(dashboard): cancel upcoming via DELETE /api/purchases/planned/{exec-id}, not /api/plans/{plan-id} Replaces the entire approach in this PR. The previous draft routed Cancel to api.deletePlan(planId) — that deleted the whole plan, which is too aggressive: operators clicking "Cancel" on an upcoming scheduled step usually want to skip THAT step, not nuke the recurring template. The right model: enumerate pending purchase_executions on the backend and emit one upcoming row per execution. That gives the frontend a real execution_id to target via DELETE /api/purchases/planned/{id} (the existing deletePlannedPurchase handler operating on purchase_executions) — Cancel removes just the scheduled instance, the plan stays in place. Backend (handler_dashboard.go + types.go): * getUpcomingPurchases switches from ListPurchasePlans iteration to GetPendingExecutions. Uses the same planMap + isPlanAllowedCached pattern as getPlannedPurchases (handler_purchases.go) so the dashboard widget and the Plans page walk the same canonical "what's about to happen" set. * upcomingFromPlan -> upcomingFromExecution. Step number now comes from the execution row directly (already stamped at instance-create time) instead of plan.RampSchedule.CurrentStep+1. * UpcomingPurchase gains ExecutionID; PlanID kept for context. * Orphan executions (parent plan deleted) are hidden, not crashed — cleanup of those is a separate concern. Frontend (dashboard.ts + types.ts + api/types.ts): * UpcomingPurchase TS interface gains execution_id. * upcomingPurchasesIndex now keyed by execution_id. * Buttons carry data-id="${p.execution_id}". * viewPurchaseDetails(executionId) renders from the in-memory snapshot — the modal also shows the plan ID + execution ID so operators can cross-reference. * cancelScheduledPurchase(executionId) calls api.deletePlannedPurchase(executionId). The confirm-dialog body rewritten to make the limited scope explicit ("removes this upcoming step from the plan; the plan itself stays in place"). * In-modal Cancel button likewise routes to deletePlannedPurchase. Tests: * handler_dashboard_test.go: TestHandler_getUpcomingPurchases now mocks GetPendingExecutions; asserts one-row-per-execution semantics (plan with no pending exec doesn't appear; two executions of the same plan produce two rows). Added _OrphanExecutionSkipped guard. * handler_dashboard_test.go: scoped-user + unattributed tests now also mock GetPendingExecutions to drive the new code path. * handler_test.go: integration test for /api/dashboard/upcoming updated likewise. * dashboard.test.ts: fixtures gain execution_id; cancel-path tests assert deletePlannedPurchase('exec-123') AND deletePlan NOT called (regression guard against the previous wrong-direction fix). Verification: go test ./... full-repo green; tsc --noEmit clean; npx jest 1411/1411 pass; pre-commit (gocyclo / gosec / trivy / migration-conflicts / go-test / build-frontend / frontend-tests) all green.
…pe fixes Rollout relays for cloud-commitments-go #218 (fix(azure): stop dividing the monthly run-rate by the term months, merged as e24345c) and #207 (fix(azure): report only savings plans billed to the subscription, merged as 58c25f0). Pin providers/azure at v0.0.0-20261007133317-58c25f04c49b, which contains both fixes, and pkg at the version the azure module requires. The aws and gcp pins stay where they are so no module moves backwards. Error-handling audit for the #207 semantics change (GetExistingCommitments for Azure savings plans can now fail instead of returning a silently partial list): every CLI call to GetExistingCommitments goes through provider.ServiceClient instances built by createServiceClient, which dispatches AWS services only, so no CLI path calls the Azure inventory today. The duplicate-purchase guards that do consume the interface (checkDuplicates, checkDuplicatesForCSVRegion) already fail closed on error for purchase runs. Coordinated with #2121: that issue's broader deps adoption should build on this branch or exclude the azure pin.
…pe fixes (#2142) Rollout relays for cloud-commitments-go #218 (fix(azure): stop dividing the monthly run-rate by the term months, merged as e24345c) and #207 (fix(azure): report only savings plans billed to the subscription, merged as 58c25f0). Pin providers/azure at v0.0.0-20261007133317-58c25f04c49b, which contains both fixes, and pkg at the version the azure module requires. The aws and gcp pins stay where they are so no module moves backwards. Error-handling audit for the #207 semantics change (GetExistingCommitments for Azure savings plans can now fail instead of returning a silently partial list): every CLI call to GetExistingCommitments goes through provider.ServiceClient instances built by createServiceClient, which dispatches AWS services only, so no CLI path calls the Azure inventory today. The duplicate-purchase guards that do consume the interface (checkDuplicates, checkDuplicatesForCSVRegion) already fail closed on error for purchase runs. Coordinated with #2121: that issue's broader deps adoption should build on this branch or exclude the azure pin.

Real fix for the dashboard "Upcoming Scheduled Purchases" 500-on-click pair (#204, #205). Supersedes the surface symptom that PR #206 cleared.
What was actually broken
internal/api/handler_dashboard.go:upcomingFromPlanemitted thePurchasePlan.IDas the JSON fieldexecution_id. The frontend bound that to View Details / Cancel buttons which called execution-level endpoints (/api/purchases/{id},/api/purchases/cancel/{id}). Those endpoints look the value up inpurchase_executions, but no execution row exists yet for an upcoming purchase — the execution is created when the schedule fires. Every click 500-ed.PR #206 cleared the column-doesn't-exist symptom. After that, the same buttons surfaced the underlying logic error directly:
(
1cf88fc4-…was a plan_id all along.)Backend
internal/api/types.go:UpcomingPurchase.ExecutionID→PlanID; JSON tagexecution_id→plan_id. Doc comment now warns that wiring this to execution endpoints is exactly the fix(purchases): cancelling an upcoming scheduled purchase fails #204/fix(purchases): GET purchase details returns 500 for upcoming scheduled purchases #205 bug pattern.internal/api/handler_dashboard.go:upcomingFromPlan: emitPlanID: plan.ID.internal/api/handler_dashboard_test.go: assertion updated.Frontend
types.ts+api/types.ts:UpcomingPurchase.execution_id→plan_id.dashboard.ts:upcomingPurchasesIndex: Map<string, UpcomingPurchase>populated byrenderUpcomingPurchases. The dialog renders from this snapshot — no API call is needed (every field is in the upcoming-list response) and there's no execution row to fetch.data-id="${p.plan_id}".viewPurchaseDetails(planId)is now sync; the modal is built via DOM API (createElement+textContent) instead ofinnerHTML. Closes a class of XSS regressions and avoids reliance on theescapeHtml-by-convention pattern.cancelScheduledPurchase(planId)callsapi.deletePlannedPurchase(planId)instead ofapi.cancelPurchase(executionId).deletePlannedPurchase.dashboard.test.ts: fixtures + mocks updated; the now-impossible "API rejection" failure-mode test for view-details was removed (the call path is sync); cancel tests assertdeletePlannedPurchaseis called with the plan_id.Verification
go build ./...cleango test ./...— every package green (incl. handler_dashboard_test.go)tsc --noEmitcleannpx jest— 1386/1386 tests pass, including the 18 dashboard testsOnce deployed, the two dashboard buttons will hit the right endpoints and stop returning 500s.
Triage
type/bug,severity/high,urgency/now,impact/all-users,effort/s,priority/p0,triaged. Closes #204 + #205 — the column-doesn't-exist fix in #206 is still correct (defends against any future migration drift), but this PR is what actually makes the buttons work.Summary by CodeRabbit