Skip to content

Surface grant expiry stage in the cluster UI, and route a shut-down cluster to upgrade - #1657

Open
DavidCockerill wants to merge 21 commits into
epic/pricing-updatesfrom
david/pricing-cluster-ui
Open

Surface grant expiry stage in the cluster UI, and route a shut-down cluster to upgrade#1657
DavidCockerill wants to merge 21 commits into
epic/pricing-updatesfrom
david/pricing-cluster-ui

Conversation

@DavidCockerill

@DavidCockerill DavidCockerill commented Aug 27, 2026

Copy link
Copy Markdown
Member

Refs central-manager#583 — Pricing Updates

Companion to the central-manager epic

This is the customer-facing half of the pricing work tracked by
central-manager#583 — calendar-month
blocks, prorated overage, and the 30-day trial converting to a $20/mo Hobbyist plan.

The split is clean: central-manager owns the entitlement and billing engine — grants, the expiry
policy timeline, the runner that walks a cluster through warning, shutdown and deletion, and the
conversion that takes payment and applies the new plan. This PR owns every surface a customer or
Fabric admin actually sees of it.
Studio makes no commercial decisions and encodes no timing; it
renders what the server says and offers the routes the server will accept.

Both repos use the same branch shape: feature branches into epic/pricing-updates, then one epic PR.
This targets studio's epic branch, so closing keywords are deliberately absent — the epic PR into
stage carries those.

That epic lists "a customer can see their block usage and expiration in Studio, and understand an
overage charge without asking" among its success criteria. This is the warning-and-conversion part of
that; usage and overage transparency are not in this PR.

The problem

Studio shows a cluster's runtime status but nothing about its commercial window. A trial gives no warning before it stops, and once stopped there is no route back up — the sidebar still offers "Scaling", the card offers nothing, and a bare start is refused. This adds the customer-facing half of the pricing epic.

🧊 In plain terms

If your trial is about to end, Harper now tells you — on the cluster list, on the cluster's own pages, and inside the cluster while you are working. When it has ended, it tells you the cluster will be deleted and on what date, and gives you a button that lands on the plan you would actually buy. Buttons that could not possibly work — Start on a cluster whose plan has ended — are gone rather than left there to fail.

What it adds

surface what appears
Cluster card stage badge — Ends in 5 days through Plan ended
Cluster pages banner with the dated consequence and the upgrade route
Inside the cluster one-line strip, so a developer sees it where they actually work

Plus: a shut-down cluster routes to the plan editor; container actions that the server would refuse are withheld; the upgrade CTA opens on Hobbyist; the region set locks during conversion, matching the server's own freeze; and /scaling no longer announces "All done!" while a conversion is still applying.

Design notes

One decision point. All three surfaces read describeGrantExpiry, so their copy cannot drift apart.

Studio encodes nothing about expiry timing. No day offsets, no stage counts, no policy-to-behaviour mapping. Every date comes from the timeline central-manager computes per read, so editing a policy moves what customers see with no change here. The deletion date is read off the schedule's own DELETED entry rather than inferred from the policy name — stage lists differ per policy, and most trial grants sit on enterprise-grace, not consumer-trial.

Gates mirror the server, not the grant. isStartBlockedByPlan follows clusterStartBlockedReason: only start is refused, and only when the cluster carries a suspendedReason with no live grant. Stop and Restart stay — the server admits stop unconditionally, and a suspended cluster can be RUNNING for a week under the enterprise policy.

Nothing asserts a state it can read. Whether the cluster is stopped, whether a deletion date is still ahead, whether an ending is in the past — all read, not assumed.

Testing

330 → 332 test files. Two new suites mount the cluster editor and the cluster-actions menu, neither of which had any render coverage: a temporal-dead-zone crash shipped in the editor behind a fully green suite during this work.

Every behavioural fix here is mutation-verified — reverting it fails a test. That mattered: an earlier round had four of five mutants survive the entire suite.

Review

Cross-reviewed before opening, across two rounds.

Round one — the full pre-push pipeline over the whole branch: eleven legs. Two outside models (codex and Gemini) each read the diff cold with no author explanation, then again against the author's claims; five Claude specialists covered bug-hunting, mutation, runtime verification, scope and conventions; then one cross-examination round where each outside model attacked or defended every finding.

Verdict was FAIL — four blocking, twelve should-fix, eleven nits. The findings that mattered most:

  • The build was broken on head, and the typecheck command was reporting a false green because the root tsconfig is {"files": []} with project references, so plain tsc --noEmit compiled nothing.
  • Mutation found four of five mutants surviving the entire suite — the plan filter, the Hobbyist preselect, the region lock and the /scaling change could each be reverted without a single test failing. One test file that read like coverage of that screen mocked a cluster with no grant, so its assertions collapsed to the pre-branch behaviour.
  • The cross-model legs' distinct contribution was auditing the author's claims against the code, which is where two of them turned out to be false as written.

Cross-examination also killed two findings rather than only adding them — both outside models agreed they fell.

Round two — a scoped bug-hunt over the fixes themselves, which confirmed four more, three introduced by the fix commit. Worth naming, because they are the class of thing that hides well: hiding container actions took Stop from a running suspended cluster, guarding the region effects left submit enabled but inert, and a && !stopped reinstated an upsell that had just been removed for being wrong.

Every behavioural fix from both rounds is mutation-verified — reverting it fails a test. Where that required coverage that did not exist, it was written: the cluster editor and the cluster-actions menu had no test that mounted them at all.

Not included, deliberately

  • Scope-aware plan picker. grant.allowedPlanIds / allowedRegionIds are typed but unread, so an admin-scoped comp still offers plans the server will reject. A feature slice, not a fix.
  • regionFrozenPlanIds duplicates a central-manager constant with nothing detecting drift. Removing it needs CM to expose free-restricted-ness.
  • Usage page renders renews <date> directly beneath the new banner's deletion date. Predates this branch; this branch is what puts both on one screen.

Depends on the central-manager side shipping first

Everything here reads a grant projection that central-manager returns on a cluster — the governing
grant plus its computed expiry timeline. That projection (grantView.js) is not yet on any
deployed central-manager
, so this feature is dark until it lands: with no grant on the payload
every surface renders nothing, which is the intended fallback rather than a failure.

So this can be reviewed and merged into studio's epic branch now, but the epic cannot reach stage
ahead of the central-manager work
.

— Claude (Fable 5)

…er to upgrade

Studio showed a cluster's runtime status but nothing about its commercial
window, so a trial gave no warning before it stopped and no route back up
afterwards. central-manager now returns a customer-safe `grant` projection on
both the cluster read and the org listing, so the card and the cluster pages
can say where a cluster sits in its expiry timeline.

`describeGrantExpiry` is the single decision point, so the badge and the banner
cannot disagree. It reads `isActive` rather than `status`: an ACTIVE row past
its endsAt is not live until the expiry runner stamps it, so status alone
reports a withdrawn cluster as healthy.

Once service is withdrawn the only way back up is buying a plan — the start
gate refuses a suspended cluster with a 402, superusers included. So the Start
actions are hidden for that cluster and the card, its menu and the banner all
route to the plan editor instead.

Also fixes /scaling announcing completion too early. A trial->paid conversion
now reaches RUNNING before the server applies the plan, so status alone flipped
the screen to "All done! Your cluster finished updating" while the plan change
was still in flight. Completion is `RUNNING` plus a grant that is no longer
`conversion-pending`, which is exactly when the provisional grant is replaced.

Refs central-manager#583
The banner only offered a route to the plan editor once service had already
been withdrawn, which is the one moment it is too late to be useful. The
warning stages now carry the same CTA, so a customer can act while the cluster
is still running.

`offerUpgrade` is separate from `needsUpgrade` on purpose: the first is "a plan
is worth buying", the second is "the cluster is down and only a plan revives
it". They differ exactly at WARNED/FINAL_WARNING/GRACE, and only the second
should hide the Start actions or re-route the card.

The CTA carries `?upgrade=hobbyist`, and the editor opens on that plan rather
than the one the cluster runs today — landing on the expiring trial would make
the CTA a no-op.

Hobbyist prescribes a single shape: colocated, one region, one distribution.
The deployment, region and distribution controls are locked while it is
selected, and the add/remove-region buttons are hidden, because offering those
choices only produces a request central-manager refuses — it freezes the region
set while a trial or level-0 plan is on the cluster. The lock is driven by the
live form value, so switching to another plan restores the controls.

The plan id is pinned in constants rather than matched on a catalogue
description: studio otherwise picks plans by description, but this one is named
in the flow itself.

Refs central-manager#583
…cted one

The lock keyed off Hobbyist being selected, which was wrong in both directions.

Too strict after: central-manager's freeze applies while a trial or level-0
plan is on the cluster, and Hobbyist is neither — so a Hobbyist customer may
add or move regions, but the UI refused to let them.

Too loose before: converting a trial to Level 1 is frozen just the same, and
that path had no lock at all, so a region change went to the server and came
back a 400.

Keying on the cluster's current plans matches the server rule exactly, and the
freeze lifts by itself once the paid plan lands. The deployment picker stays
tied to Hobbyist, which really is colocated-only.

Also says why the control is locked rather than presenting a dead select.

Refs central-manager#583
The "Premium" badge marked regions no base-level plan could reach, where base
meant the lowest planLevel for the deployment. Hobbyist is planLevel 0 — the
same as the trial — so a $20 plan's reach defined what counted as free.

That is about to matter: Hobbyist's region cap is being lifted in the plan
catalogue, since price already scales with distribution (priceUsd *
instanceCount / 2, so $20 in us-1 and $100 in us-5) and the cap excluded
regions like us-ne-1 that cost exactly what us-1 costs. Under the old rule,
lifting it would have made every region reachable by a "base" plan and silently
erased every Premium badge — leaving trial users with regions disabled and no
explanation of why.

Keyed on priceUsd instead, the badge keeps meaning "costs money to reach": the
free trial's cap still defines it, and Hobbyist counts as paid. Both the
all-free and all-paid cases badge nothing, which is what they mean.

Refs central-manager#583
central-manager's scopeViolations no longer resolves a region family and
compares purchasedBlockMultiplier — it is a plain includes() on the id list
(grant-scoping a3e6bbaa). Recording it here because the old wording would have
had a scope-aware plan picker infer sizes that are not covered.

Refs central-manager#583
`regionSetFrozen` was placed directly under the search-param hooks, two lines
above the `useRouteContext` destructure it reads `cluster` from. That is a
temporal dead zone: the whole cluster editor threw "Cannot access 'cluster'
before initialization" on load, so /edit was a Component Error page.

Nothing caught it — `tsc --noEmit` and oxlint both passed on the broken tree,
and no test renders the editor, so 2733 green tests said nothing. Found by
opening the page.

Refs central-manager#583
The performance picker listed every plan for the deployment, so editing a
cluster offered "30-day trial (1K read/min)" as a target. A trial is granted at
signup and cannot be re-entered — picking it is a change central-manager would
refuse, and on the upgrade path it sits directly above the plan the customer
came there to leave.

Free managed plans are now filtered out when editing an existing cluster.
Keyed on priceUsd rather than a plan id, so a catalogue rename cannot quietly
re-admit one. Self-hosted is exempt: its free tier is a real ongoing plan.

The cluster's own plan always stays listed, or a customer still on the trial
would open the editor to a picker with nothing selected.

Creating a cluster is untouched — there the free plan is the offer.

Refs central-manager#583
The banner told a cluster in its grace period that it had been stopped, while
it was still running and serving traffic.

GRACE runs `expireGrant` but not `stopCluster` — the shutdown is a separate
stage seven days later — so the grant is inactive while the cluster is up. The
`!isActive` branch ran ahead of the stage switch and swallowed GRACE, which
made the switch's own GRACE arm dead code. It is answered first now, and the
dead arm is gone.

The same assumption was wrong more generally: the runner expiring a grant and
stopping the cluster are separate acts, and the runner can lag. So the copy no
longer infers the stop from the grant at all — it reads whether the cluster is
actually down (`status`, `suspendedReason`) and otherwise says the cluster will
be stopped rather than that it has been.

Found by David on clu-6xm9wlx1dfnwzfop.

Refs central-manager#583
…n ending

Not one of them can succeed. The start gate refuses a suspended cluster with a
402 for everyone, superusers included, and there is nothing running to restart
or stop — so the group offered a menu of guaranteed failures.

Hiding only Start left the "Container" heading over an empty section, which
reads as broken rather than deliberate, so the whole group goes. Applied to
both surfaces: the card's menu and ClusterStateMenu on the cluster page.
Terminate stays in both — leaving is always allowed.

Deliberately narrow: it keys on the cluster being stopped AND its plan having
ended, so a cluster whose grant lapsed but which is still running keeps Stop
and Restart, which central-manager does allow on the way down.

Also stops the price display floating over the expiry banner. The price is
absolutely positioned against the cluster layout's `relative` box, and the
banner was rendered at the top of that same box. The banner now sits above it,
with matching padding so the price stays flush to the content edge.

And makes the suite committable: an inert WebSocket stub in vitest setup.
Anything mounting the app shell starts the SystemStatus socket, which under
jsdom reaches undici 8's WebSocket and throws a cross-realm Event TypeError as
an unhandled error — tests all pass, vitest exits non-zero, the hook rejects.
It was racy, so committing here was a coin flip. Distinct from the undici-7
pin in AGENTS.md, which is a missing module rather than a realm mismatch.

Refs central-manager#583
It cost a run of pre-commit rejections on a tree whose tests were all passing,
and the failure mode (green tests, non-zero exit, racy) does not lead anyone to
the cause on its own.

Refs central-manager#583
…t warning on

The settings-page banner is where the full story lives, but a developer
building against their cluster may never open those pages between signup and
shutdown — so the entire warning sequence could play out somewhere nobody was
looking, and the first they knew was a stopped cluster.

Two deliberate differences from the settings banner, because this sits above
work rather than beside it:

The first warning is excluded. It runs for a week, and a banner shown for a
week on a surface someone is actively using becomes furniture — taking the
final warning down with it. `isUrgentExpiry` names that rule and keeps it with
the other expiry logic, so it can be argued with in one place.

It is a line, not a block: no heading, no button, just the same sentence and a
link. A full Alert is right above a settings page and wrong above a data grid.

Not dismissible. At this point there are at most two days left, and a trial
that stops without warning is the outcome this work exists to prevent.

Copy comes from describeGrantExpiry like the other two surfaces, so the three
cannot drift.

Refs central-manager#583
…ast two days

David's call, overriding the gate I shipped in the previous commit. The
argument for holding back — a week-long banner on a working surface becomes
furniture — loses to the simpler one: the whole point of putting it here is
that the settings pages may never be opened, and starting at the final warning
leaves a customer two days to act on a plan change they have not thought about.

`isUrgentExpiry` is renamed `isExpiryWarning`, because with the first stage
included the rule is no longer about urgency: it is every expiry stage except a
conversion in flight, which is progress rather than a warning. Leaving the old
name over the new behaviour would have been the misleading half of the change.

Refs central-manager#583
The third surface carrying container actions, missed when the card menu and
ClusterStateMenu were done. An instance stopped by trial expiry still offered
Start and Start in safe mode, both of which the start gate refuses with a 402.

Scoped to the stopped-instance actions rather than the whole group: a still
running instance keeps Restart and Stop, which central-manager allows on the
way down. That matches the rule used on the other two surfaces.

The hook takes only an instance, so it reads the cluster from the cached query
its callers' route already loads, rather than threading a flag through
InstanceActionsMenu and InstanceRowContextMenu.

Found by David.

Refs central-manager#583
…data with it

"The cluster has been stopped. Choose a paid plan to start it again" reads as
reversible and open-ended. It is neither: a consumer trial deletes the cluster
14 days after shutdown and enterprise-grace 21, taking the data. That is the
actual stake and the banner never mentioned it.

Deliberately no date. The day counts live in central-manager's policy table,
whose own comment says changing the timeline is a change to that file only — a
copy here would go on confidently showing a wrong deletion date for a
destructive action. Studio keys on which policies end in `deleteCluster`
instead, which is stable, and an unrecognised policy simply makes no claim.

A precise date needs the next-stage-and-due-date projection central-manager
does not expose yet; `stageDueAt` is computed per request and never stored.

Guarded so it stays true: a lapsed purchased grant carries no policy and
nothing deletes it, so that case keeps the old wording.

Refs central-manager#583
…edule

The banner said the cluster and its data would be deleted but could not say
when, because studio had no access to the stage offsets. central-manager now
returns the resolved schedule on the grant projection, so it reads "deleted on
15 September" — the difference between a warning someone acts on and one they
scroll past.

The policy-name guess this replaces is gone. Rather than a local set of which
policies end in deletion, the copy reads the DELETED entry off the schedule and
uses its date. That removes the last place studio encoded anything about the
expiry timeline, and it drops an assumption that was wrong anyway: stage lists
differ per policy — enterprise-grace has five stages, consumer-trial four — and
most trial grants sit on enterprise-grace, so `source: 'trial'` never implied
the consumer timeline.

The schedule is computed per read and never stored, so a policy edit moves
these dates with no migration. Don't cache them.

Undated wording is kept for the cases with no schedule: a forever grant, an
unparseable endsAt, or a lapsed purchased grant that nothing deletes.

Refs central-manager#583
`upgradeHref && update && {…}` puts `""` into the union when upgradeHref is
undefined, and excludeFalsy doesn't narrow the string out, so EntityMenuItem[]
no longer typechecks and `npm run build` fails.

It went unnoticed because the root tsconfig is `{"files": []}` with project
references: plain `tsc --noEmit` compiles nothing and reports a false green.
`tsc -b --noEmit` is the command that actually checks this repo.

Refs central-manager#583
…eal coverage

A /review-gate run returned FAIL with four blocking findings. This fixes those,
eight of the should-fix items, and the coverage gap that let them through.

BLOCKING

A failed conversion rendered as a permanent spinner. Nothing ever rewrites
`expiryPolicy`, so a lapsed provisional grant still reads `conversion-pending`
forever; testing that before `isActive` meant the terminal state of a failed
upgrade was "Upgrading", with no CTA and a Start that 402s. The live check now
gates it, and the switch's unreachable SHUTDOWN/DELETED arms are removed.

The region lock disabled the control but not the value. Three effects wrote
through it, so a mutated region set was submitted and refused; one of them
blanked a frozen region, leaving the form permanently unsatisfiable because
nothing could put a value back. All three now stand down when frozen.

The card's "Choose a Plan" omitted `?upgrade=hobbyist`, so the primary route
off the cluster list opened the editor on the expired trial with submit
disabled. EntityMenuItem gained `search`, since a query string inside `to` is
not parsed by the router.

`upgradeHref &&` put `""` into the menu-item union and broke `npm run build`.
It went unseen because the root tsconfig is `{"files": []}` with project
references: `tsc --noEmit` compiles nothing. `tsc -b --noEmit` is the real check.

HONEST COPY

Container actions now key on `isStartBlockedByPlan`, mirroring the server's own
`clusterStartBlockedReason` instead of the grant's state — the old predicate was
wrong in both directions, hiding controls the API would have accepted and
leaving a Start the API refuses.

No deletion date is quoted for a revoked grant (the runner skips those, so the
schedule is never walked) or for a date already past (that means the runner is
behind, and naming a stale date reads as a system that has lost track). Nothing
claims a past ending for an absent or future date — reachable via a revoked
forever-grant and via a future startsAt. A grace period no longer asserts the
cluster is running without checking, and no longer sells a $20 self-serve plan
to an account with a negotiated enterprise agreement. Countdowns compare
calendar days: rounding up put two hours before midnight into "ends tomorrow",
inside the window where the next stage stops the cluster.

"Premium" means the same thing in both halves of the form; the Hobbyist
deployment lock no longer fires on the create page; one optional chain that
threw on `plans: []`.

COVERAGE

The editor had no test that mounted it. Four of five mutants had survived the
whole suite. `ClusterDetails.test.tsx` mounts it and kills three; the fourth was
`Scaling.tsx` reverting wholesale, which survived because `Scaling.test.tsx`
mocked a cluster with no `grant` — so every assertion there collapsed to
`status === 'RUNNING'` and pinned the pre-branch behaviour. All four are
mutation-verified dead.

Radix renders a Select's options only while open, and in jsdom ArrowDown opens
one where pointerDown does not — so an "option is absent" assertion passes
whether or not the filter works. That is how the plan filter survived. The
polyfills Radix needs are in test setup, guarded for the node-environment tests
that have no DOM, and the trap is written up in AGENTS.md.

NOT FIXED, deliberately: the scope-aware plan picker (grant.allowedPlanIds is
declared and unread) is a feature slice, not a fix; `regionFrozenPlanIds`
duplicates a CM constant with nothing detecting drift, and removing it needs CM
to expose free-restricted-ness; the Usage page's "renews <date>" contradicts the
new banner but predates this branch.

Refs central-manager#583
A scoped bug-hunt on the previous commit confirmed four, three of them
introduced by that commit. Its verdict on the fourth risk area held: the
jsdom polyfills change no existing test's behaviour, measured by removing them
and running the suite.

Hiding the container group on "start is blocked" took Stop and Restart from a
RUNNING cluster. central-manager writes `suspendedReason` BEFORE stopping, and
defers to a later pass if the cluster is mid-transition, so suspended-and-still-
up is a persistent state — and its own gate reads `action !== 'stop'`, admitting
stop unconditionally. The customer's cluster was up, billing, and offered
nothing but Terminate. Only the Start actions are blocked now; the group is
hidden solely when it would be a heading over nothing.

Guarding the region effects removed the blanking that used to make the form
invalid, and nothing else objected: `refineZod` computed the region but raised
no issue when it failed to resolve, so submit stayed enabled and the click threw
before the toast — a dead button, permanently. Validation now refuses a region
that no longer resolves, and the submit path no longer indexes blind.

`&& !stopped` on the grace arm sent a stopped grace-period cluster into the
withdrawn copy, restoring the $20 self-serve CTA the same commit had removed for
being the wrong answer to an enterprise agreement — and advising a plan purchase
where the server would have accepted a plain Start. Grace now reads the status
without leaving its own arm.

The upgrade CTA still landed on a disabled submit: preselecting the target plan
makes it the form's default, so `isDirty` is false. That gate predates this
work, but the CTA routes into it, so the blocking finding was not closed. Same
escape hatch as the existing version resubmit.

Also, per David: the trial no longer lists once the selection has moved off it.
Keeping the current plan listed was to stop the picker opening blank, which only
needs it while it is the tier being shown — after the CTA preselects Hobbyist,
listing the trial just offers a move back onto a plan nobody can re-enter.

Every fix is mutation-verified: reverting each one fails a test. The container
gate needed a new ClusterStateMenu suite, since reverting it passed 112.

Refs central-manager#583
@DavidCockerill
DavidCockerill marked this pull request as ready for review August 27, 2026 15:02
@DavidCockerill
DavidCockerill requested a review from a team as a code owner August 27, 2026 15:02
@DavidCockerill
DavidCockerill requested review from dawsontoth and removed request for a team August 27, 2026 15:02

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a robust mechanism for handling cluster plan and grant expiry, including UI banners, status strips, and restricted container actions for expired plans. It also adds comprehensive tests and test setup polyfills to prevent JSDOM and WebSocket-related test failures. The review feedback highlights a timezone-dependency issue in date calculations that should be resolved by using UTC methods, suggests safer handling of optional parameters in the expiry strip, and recommends avoiding non-null assertions in the cluster form.

Comment thread src/features/clusters/lib/grantExpiry.ts
Comment thread src/features/clusters/lib/grantExpiry.test.ts
Comment thread src/features/cluster/components/ClusterExpiryStrip.tsx
Comment thread src/features/clusters/upsert/index.tsx Outdated
The generated RegionPlan type requires planId, so the assertion asserted
nothing while telling readers the field might be missing. Flagged by Gemini on
the PR; its suggested runtime guard is equally unnecessary for the same reason.

Refs central-manager#583
central-manager now marks a conversion on the cluster: APPLYING while it starts
the cluster and lands the plan, FAILED from the single catch every failure path
passes through, null once the plan lands. Before this, a failed conversion was
indistinguishable from a slow one for up to four hours — the provisional
grant's whole window — and the UI could only say "still finishing".

The marker is the direct signal; the conversion-pending grant check stays as
the belt for a lost write, because the marker is best-effort: FAILED is
reliable, a missing marker on a still-live provisional grant means "probably
still applying", and a lapsed one still falls through to the honest withdrawn
copy rather than reading as progress.

FAILED preempts both the progress copy and the plain-expiry copy, on every
surface via describeGrantExpiry, and /scaling gets an explicit failure state
instead of a spinner. The copy promises "nothing was charged" flatly — payment
is only verified up front and blocks mint inside the plan change itself, so a
failed conversion cannot have billed anything.

The Scaling test's mocked grant gains the isActive the real projection always
sends: the belt rightly demands it true, since a lapsed provisional grant must
read as ended, not as still applying.

Refs central-manager#583
@dawsontoth

Copy link
Copy Markdown
Contributor

Can I have more pictures? 😆

@dawsontoth

Copy link
Copy Markdown
Contributor

(Screenshots.)

].filter(Boolean) as SubNavItem[];

return (
// `relative` so absolutely-positioned page furniture (the cluster editor's price display) anchors

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.

I'm going to need to be able to see this to verify it hasn't borked everything, but I can't do that without also having central-manager and various things setup and running locally on your branch, too. Bit of a catch 22.

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.

2 participants