Skip to content

feat(web): read-only DCA plan card on /rules - #850

Merged
eaitbrahim merged 3 commits into
mainfrom
feat/dca-plan-web
Sep 28, 2026
Merged

eaitbrahim merged 3 commits into
mainfrom
feat/dca-plan-web

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

PR 2 of docs/superpowers/plans/2026-09-27-dca-plan.md (Tasks 7–8). This PR builds on PR 1 (#846) as merged, not on the plan's snippets.

What it does

  • GET /api/dca-plan?budget=&buffer= answers from the same service as keel dca plan (build_dca_plan). The admission screen is resolved per call.
    • It is read-only and writes nothing, which is pinned by a rules-row count after a request. It is GET only; a POST gets a 404.
    • Money crosses the wire as strings, values arrive presentation-ready, and state is an explicit field.
  • No budget: the route answers an awaiting_budget payload with the same keys as the ready one, every figure absent, and the command template.
  • The worst-month cap check (dca plan: cap check uses an average month; rail 14 caps the calendar month, so 5-buy months veto #847) is on the wire and on the card.
    • cap_check is the worst UTC calendar month rail 14 was compared against, with the checked total as its value.
    • It is judged bad over the cap, good within it, and neutral when the tier is unlimited.
    • The card shows it as "checked against the cap: worst calendar month for a 7-day cadence, 5 buy day(s) x $103.47 per cycle = $517.35".
  • The card on /rules sits under the rule ledger and reads the page's own address: /rules?budget=500&buffer=0.1.
    • It shows the summary figures, the per-asset schedule, the excluded assets with their reasons, the existing DCA rules (unchanged), the blockers, the notes, and the exact CLI command with --config/--db for the served deployment.
    • It has no button, anchor, form or input, and no event handler.
  • A refused read (a bad ?budget=, or a config the service refuses) leaves the ledger standing. The card says it could not read the proposal and places the server's reason, in the service's own words.

Rulings

  • R21: every DcaPlanError is a 400 ApiRefusal. This includes the ones build_dca_plan raises over the config (target_weights keys colliding by case, a non-finite weight).
    • Why: ApiRefusal is this module's only way of saying "declined, and here is why". The CLI refuses the same error as a clean message. A 500 would read "That report could not be built" over a message that already names the fix.
    • Cost if wrong: a config problem is reported with a client-error status. The body still names the config key to fix.
  • R22: the worst-month sentence is written once, by a new service function dca_plan.worst_month_text(plan). The CLI's checked against the cap: line and the payload's cap_check.display both use it.
  • R23: the card receives the secondary reading's error as well as its data (rulesView(data, sort, onSort, plan, planError)).
    • Why: with data alone, a 400 would show a bare "could not be read" and throw away the reason, such as the fraction hint for buffer=10.
    • Cost if wrong: one extra parameter.
  • R24: no service-worker cache bump. The worker's cache name is keel-shell-<full_version>, which is <version>+<commit>, so every new commit is a new cache. /api/ is never cached, and a new test pins that every API_ROUTES path sits under API_PREFIX.
    • Why: earlier web PRs changed static JS without a bump for the same reason.
    • Cost if wrong: none; the mechanism is structural.
  • R25: the card's three row collections (buys, excluded, existing) are enrolled in the row-key scan, not exempted. A seeder patches the screen to admit BTC and reject PAXG, and writes an existing ETH DCA rule. The endpoint is asked with a budget, so every collection has a row.
    • Why: the plan said not to use an exemption.
    • Cost if wrong: _seed_for now takes monkeypatch.

Test evidence

  • uv run pytest -q: 7021 passed, 3 skipped (exit 0).
  • uv run ruff check keel tests: exit 0.
  • uv run ruff format --check keel tests: exit 0.
  • uv run mypy (bare): exit 0.
  • node --check on render.js and main.js: exit 0 for both.
  • Structural client tests:
    • The card's tags are on a closed list, with a scan that sees a mutated-in button.
    • Exactly one code element, paired with plan.command.
    • One table, with no sort pair.
    • Every served key and summary figure is paired with a read.
    • Exactly one figure reads plan.cap_check, under "checked against the cap".
    • The ROUTES row, and the mount call passing both the reading's data and its error.
    • The page-address seeding: one location.search read, names budget/buffer only, and the dca-plan bag only.
  • Mutations applied and killed, each confirmed to have changed the source before the run:
    • A module-level screen_product import fails the route tests (the patch no longer reaches the reader).
    • Removing the DcaPlanError catch fails the config-refusal test.
    • Renaming row keys (row.fee, item.reasons, rule.per_buy) fails the row-key scan.
    • Dropping the cap_check figure, dropping a table cell, dropping planReading.error, widening the seeded names, and not placing error.detail each fail their test.
  • Browser check (Playwright): a real keel serve on a copy of keel/templates/config.yaml with a temp DB, on a free port. The service worker was unregistered and its caches deleted first.
    • /rules?budget=500&buffer=0.1 shows the blocked state with the real screen: nothing admitted on an empty DB, every asset listed as excluded with the screen's reasons, and the exact command.
    • /rules with no query shows the awaiting state and the command template.
    • Both loads had 0 console errors or warnings, 0 controls in the card, and 1 code element. The rule ledger stays in place.
    • At 390px there is no horizontal scroll.
    • buffer=10 shows the 400 detail ("…0.1 means hold back 10%") under the standing ledger. The only console entry is the browser's network log of that 400.
    • A second server with the screen patched to admit everything and a $600 attested cap showed the populated 7-row schedule, "ready to apply from a terminal", and the worst-month check $517.20 judged good.

Deviations from the plan

  • The payload has one more key than the plan's shape: cap_check, the dca plan: cap check uses an average month; rail 14 caps the calendar month, so 5-buy months veto #847 amendment.
  • rulesView takes a fifth parameter, planError (R23).
  • The card's excluded and existing lists are built with .map over explicit keys, so the row-key scan checks them. The plan had one generic list helper that read every key.
  • The empty-table text is "No buys planned." rather than "No asset is eligible.", which was false in the awaiting state.
  • A small .dca-plan .dca-command CSS rule shows the command as a selectable block.

🤖 Generated with Claude Code

eaitbrahim and others added 2 commits September 27, 2026 23:30
…e, read-only

The reader calls build_dca_plan with the admission screen resolved per
call; no budget answers an awaiting_budget payload of the same shape, and
every DcaPlanError (bad input, or a config refusal such as case-colliding
target_weights) is a 400 ApiRefusal carrying the service's own words.

The payload carries cap_check: the worst UTC calendar month rail 14 was
checked against (#847), worded by a new service function,
worst_month_text, which the CLI's 'checked against the cap:' line now
also uses -- one sentence for both front-ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ommand

The rules view reads /api/dca-plan as its secondary endpoint; its inputs
come from the page address (?budget=&buffer=), copied once into that
endpoint's query only. The card builds no control: tags on a closed
list, one code element filled from plan.command, one unsorted table. It
shows the worst-calendar-month figure as 'checked against the cap', and
a refused read keeps the ledger standing and places the server's reason.

Structural client tests: the card's three collections are enrolled in the
row-key scan with a seeded, admitted plan; every payload key and summary
figure is paired with a read; the SW pin covers every API route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread keel/web/api.py Outdated
Comment thread keel/web/payload.py Outdated
Comment thread keel/web/payload.py
…ip bonus (#851, #852)

- The awaiting template carries --config/--db like the filled command (R17, #851).
- A degraded rail 14 cap says why, as the CLI prints it: "$0.00 (because ...)" (#852).
- An existing rule's row carries the CLI's (dip_bonus_pct X) marker.
- parse_plan_inputs refuses more than 12 decimal places, so a 25-character
  1e-10000000 can no longer be spelled out as megabytes of JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@eaitbrahim eaitbrahim left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Round 2: all three round-1 threads verified on e46ee5e and resolved (#851, #852 closed). The 12-decimal-place bound refuses 1e-10000000 for budget and buffer, and 0e10000000 formats as 0. No new findings. Tests (tests/web and test_dca_plan.py, 1136) pass and node --check is clean on render.js and main.js.

@eaitbrahim
eaitbrahim merged commit 62a2f84 into main Sep 28, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat/dca-plan-web branch September 28, 2026 04:16
eaitbrahim added a commit that referenced this pull request Sep 28, 2026
…n and its read-only card (#877)

MINOR: a rail changes scope for DCA (#869) and new commands and routes land
(#846, #850). No schema change since 0.19.0.

What lands:
  #846 (#831 follow-up) -- keel dca plan: schedule by target weights within
  rail 14's worst calendar month; [Y] writes candidate rules only.
  #850 -- GET /api/dca-plan and a read-only card on /rules.
  #869 (#853) -- rail 6 (per-asset) no longer vetoes DCA buys; the plan
  blocks on rail 3 and warns on rail 5.
  #867 (#854), #876 (#874) -- a blocked plan names the largest passing
  budget and smallest passing buffer, clearing rails 14, 3 and 2.
  #870 (#856) -- admission screen cached 5 minutes.
  #858 (#855) -- deploy/live-rules.json synced.
  #859 (#857) -- design spec for the DCA sleeve's sell side.

Co-authored-by: Claude Opus 5.5 <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