Skip to content

v0.25.0 - budget by plan, and the answer the review queue could not give

Choose a tag to compare

@AmanSK5 AmanSK5 released this 02 Sep 19:42
· 40 commits to main since this release
682ad7a

Five things, batched as agreed so one release carries them.

1. An organisation does not have one Claude subscription

It has a Teams contract and a handful of individual Max seats, on different plans, different renewal dates, often different owners. budget_subscriptions keyed on tool_id alone:

CREATE TABLE budget_subscriptions (
  tool_id TEXT PRIMARY KEY,   -- ← why Claude greyed out the second time
  plan    TEXT NOT NULL DEFAULT '',

So the choice was track the contract and be blind to the seats, or the reverse. Both totals were wrong and neither said so.

Three tables move to (tool_id, plan_key). SQLite cannot alter a primary key, so each is rebuilt and copied — proven against a database in the old shape: stored vendor key intact, members and connection carried onto the plan their subscription named, no orphans, idempotent on restart.

Every card button carried the tool and every handler did find(s => s.tool_id === k), which returns whichever plan sorts first. Editing your Max seats would have opened the Teams contract; importing users into one would have replaced the other's list. They carry the subscription now.

The page's own bSubMonthly/bSpendParts, run against three plans on one tool:

per subscription : Team=1000  Max 20=270  Max 5=108
page total       : GBP 1378.00
old model showed : 1000   (one plan only)

Four decisions, all about not producing a plausible wrong number:

  • Ambiguity is refused, not guessed — a member write that doesn't name a plan, on a tool with two, gets claude has 2 subscriptions (max-20, team) - say which plan this is for. Guessing moves seats between contracts and the total still looks fine.
  • The picker no longer treats a tool as used up. It says which plans are linked and stays selectable. Only a licence genuinely billed twice is disabled — the covers rule still catches that, it just no longer mistakes two plans for one double-bill.
  • Renaming works — editing sends plan_key, so Team → Teams updates the row instead of quietly creating a second.
  • Orphan adoption — a key can still be stored and synced before the subscription is written down; those rows are adopted when the tool's first subscription appears.

2. The review queue's answer went nowhere

firezone-client-tunnel turned up on a real estate — the first time the process-candidate path has fired outside a seeded demo. It's a VPN tunnel daemon: it resolves DNS for everything behind the tunnel, so naming it says "something on this device, through the VPN", the same non-answer as a stub resolver.

But the queue offered only add to registry or dismiss, and dismissing cleared the card while leaving the process on the Agentic view for everagentic_from never consulted candidate decisions. A question whose answer went nowhere.

There's a third disposition now: the name identifies no program. That's a fact about the name, so it reaches every view that reads one, and the receiver stops asking about it.

3. VPN clients are a category, not a one-off

Tailscale, OpenVPN, WireGuard, Cloudflare WARP, AnyConnect, GlobalProtect, Zscaler, Netskope and the rest join the floor, on the same reasoning that put svchost and taskhostw there. The queue handles the tail.

4. stable comes out of Warp's exe_names

It solved the mystery it was added for, and it is a word. Any process literally named stable was being silently excused as Warp. The queue can ask instead.

5. The overview queue emptied itself

Adding a candidate to the registry set CAND = null and called render() — but the queue is only ever fetched by load(), behind a check for whether the widget is on the overview. So it drew an empty queue and left the rest invisible until a manual reload. Three handlers had that shape; there's one reloadDerived() now.

Checked

Suites: registry 23, portal 627, scanner 210, receiver 300. Plus collector parity, build.py --check, both byte-identical invariants, chart copies, shellcheck.

Two things worth knowing before this ships:

  • The migration runs on the live database at receiver start. Proven and idempotent, but it rebuilds three tables — worth a copy of the state volume first.
  • I have not seen the budget page render two plans in a browser. The arithmetic is exercised directly and the suites pass, but the managed-mode UI needs a login I avoided. That click-through is still owed.