TestProviderMSPLicenseRefreshAppliesShorterPaidPeriod compared the
refresher's UTC expiry ("...Z") with the expected time formatted in the
machine's local zone ("...+01:00"), so it failed on any machine not set
to UTC, including a maintainer Mac on British Summer Time, while CI in
UTC passed. Format the expectation in UTC. It now passes under
Europe/London, America/Los_Angeles and UTC.
Every IncidentStore mutation queues a coalesced saveAsync goroutine
that later runs MkdirAll on the data directory and writes its .tmp
file. Nothing tracked those goroutines, so a t.TempDir-backed test
could finish while one was still queued. The save then either wrote
into the directory while TempDir's RemoveAll was deleting it (CI:
"unlinkat ...: directory not empty" in
TestIncidentQueryCanonicalOccurrencesAndNotes/memory, run 35932105100)
or recreated the directory after cleanup and leaked it. The test's
synchronous saveToDisk did not help: it cannot join a save that is
already queued.
Track queued saves with a WaitGroup and add an unexported flush(),
mirroring KnowledgeStore (47cadcc188). The disk-backed incident tests
now join through flush in place of polling, a 20ms sleep or waiting
for a save count. The canonical-occurrences test reloads from
RecordNote's own asynchronous save instead of a manual checkpoint.
TestIncidentStoreFlushJoinsQueuedSave holds the save lock to prove
flush waits for a queued save that has not started.
With a 0-3ms delay injected before each save, the old code leaked a
recreated data directory in 233 of 300 runs; with this change, none.
Checkpoint timing, coalescing and the stored format are unchanged.
Keep canonical change classification while avoiding two full List clones on accepted ingest. Document the registry and monitoring boundaries, add exact verification tests, and retain 1,000-resource benchmarks for #2199.
Change-source: pulse-maintainer
Only remove a managed client network with the expected name and tenant labels. Select attached support containers by role and provider ingress before forced disconnect, avoiding cross-provider mutations on shared Docker hosts. Update both cloud-paid and deployment-installability contracts with the regression boundary.
Change-source: pulse-maintainer
Carry the prior source candidate onto the reviewed installer guidance tip so ownership-isolated recovery can receive exact-head proof.
Change-source: pulse-maintainer
Keep first-run evaluation and summary guidance conditional on actual Plan availability and confirmed active limits. Remove the ordinary manual-key purchase prompt while retaining the separate custom-license binding path, and update the installability contract and focused assertions in the same commit.
Change-source: pulse-maintainer
setup.sh still told an evaluating provider to "request an upgrade" at
/msp.html#request, a form that waited on a human reply with a checkout
link. The portal now sells plans itself (Plan tab, Stripe checkout,
applied in seconds), so both the evaluation log line and the closing
summary send the provider there. The summary also stops printing the
lease signing public key as something the licence "must bind": the
portal purchase binds it without a copy step, and the key is still
available through --print-lease-signing-public-key for a custom licence.
Require exact tenant and runtime labels before adopting an existing network, share that check with support-container recovery, and cover wrong or absent labels. Update the cloud-paid and deployment contracts to state the provisioning boundary.
Change-source: pulse-maintainer
Merge the reviewed recovery candidate onto current main while retaining the already landed PR implementation. Filter shared support roles by the configured provider network, verify tenant-network ownership before attachment, and cover two same-host providers plus startup reconciliation.
Change-source: pulse-maintainer
Preserve the refreshed-plan confirmation and its generated portal assets over provider MSP client recovery. Change-source: pulse-maintainer
Change-source: pulse-maintainer
Combine the already published #2216 recovery with the local first-run setup correction; preserve both release-control contract notes.
Change-source: pulse-maintainer
Exercise two provider installations sharing a Docker daemon and role labels. Verify that each recreated support pair only rejoins its own client network and that an unhealthy client is restarted, never stopped.
Change-source: pulse-maintainer
Carry the checked checkout-return race correction onto the current main snapshot. Await the refreshed licence identity before announcing the paid plan; ignore overtaken plan reads after restart.
Contract-Neutral: Restores the existing provider-hosted Plan confirmation without changing plan, entitlement, route, price, or role contracts.
Change-source: pulse-maintainer
Compose the provider-hosted Plan return race correction with the current provider MSP deployment and shorter-expiry repairs. The portal waits for the exact refreshed licence after control-plane restart and ignores an overtaken initial plan response.
Contract-Neutral: This restores the existing cloud-paid Plan contract to confirm the restarted control plane serves the refreshed licence; no plan, entitlement, route, price, or role contract changes.
Change-source: pulse-maintainer
Combine the current provider setup correction with the isolated-network recovery candidate. Retain both deployment contracts and preserve the client-restart and support-container ownership guards.
Change-source: pulse-maintainer
Start the provider services after licensed compose validation so the printed bootstrap and portal link have a live control plane. Correct the first-run prompt to name only operator-provided values and retain an install contract regression test.
Change-source: pulse-maintainer
Reconcile PR #2216 with current main and retain the paid-plan contracts. Reattach support containers to active tenant networks at monitor startup and before checks, and restart unhealthy clients rather than stopping them under Docker unless-stopped.
Require exact tenant-network ownership labels and select support containers on the configured provider ingress network to preserve isolation. Cover startup recovery, restart behaviour, missing networks and wrong-owner networks.
Change-source: pulse-maintainer
A licence refresh can finish before the initial plan request, so the old
evaluation or renewal date was reported as active after restart. Match the
returned licence identity and expiry, ignore stale plan responses, and keep
polling until the serving control plane exposes that state. Add race and
same-plan renewal regression tests and update the embedded portal bundle.
Contract-Neutral: This restores the existing cloud-paid Plan contract to poll until the restarted control plane serves the refreshed licence; no plan, entitlement, route, price, or role contract changes.
Change-source: pulse-maintainer
A billing-cycle change can retain the licence ID and plan while moving the signed paid period earlier. Compare the full signed expiry, replace the old token and restart, with regression coverage and the matching cloud-paid contract guarantee.
Change-source: pulse-maintainer
A sandbox run of the purchase path showed that a provider who changed
plan in the Stripe billing portal came back to the portal's Clients page
still on the old plan. It stayed that way until the next six-hourly
refresh, unless they found the "Just paid? Apply my purchase now" button,
whose wording does not fit a plan change. Manage billing now returns to
?provider_msp_checkout=billing, and the Plan panel checks the licence
twice on the way back, which is enough for the plan-change webhook to
land without polling on every visit. In the same run Solo to Starter and
back applied about ten seconds after returning.
The paid panel also dropped "Licence valid to <date>". That date is the
paid period end plus 14 days of grace, so a buyer read it as the date they
had paid until. It now appears only once the licence is inside that grace,
as a warning that the subscription has not renewed.
Every upgrade.sh run recreates the control plane, and a recreated support
container is no longer attached to any client's isolated tenant network.
The health monitor then could not reach the clients, counted three
failures, and stopped each one, expecting the unless-stopped restart policy
to bring it back. Docker never restarts a container stopped through the
API, so every client workspace stayed down until someone started it by
hand. A Traefik recreate also left client routes returning 504.
Reproduced on a v6.4.1 provider bundle with two clients.
The monitor now reattaches Traefik and the control plane to each active
client's network once at startup and on every pass, and recovers an
unhealthy client with a single restart instead of a stop. With the fix on
the same install, a control-plane recreate rejoined both networks within a
second, a Traefik-only recreate had its routes back on the next pass, and
no client was stopped.
GetUnifiedReadStateOrSnapshot built a full unified state view only to
return its readState, which for a store-backed monitor is the live store
itself. Building the view deep-clones every resource through GetAll, and
the host-continuity wrapper clones them again, so each call copied the
whole estate twice and discarded both copies.
Alert evaluation reaches this once per resource per poll: the default
5-minute CPU evaluation window sends every CPU-bearing resource through
metricWindowPoints and MetricsTargetForResource, so poll cost grew with
the square of the estate. Measured per call on main: 2.6 ms and 1.7 MB
at 130 resources, 32 ms and 19 MB at 1,040, 85 ms and 56 MB at 2,080.
At 1,040 resources that is roughly 22 CPU-seconds per 10-second poll.
Returning the store directly costs 2.5-25 us and 9 allocations at every
size.
Mock mode and monitors without a ReadState store keep the view path. The
new test holds the lookup's allocations flat from 8 to 1,000 resources;
before this change it measured 545 and 66,019.
Refs #2199
Contract-Neutral: Read-state accessor performance only; callers receive the same live store, no monitoring or agent-lifecycle contract change
The portal had no billing surface in provider-hosted mode, so a provider
on the free evaluation could not see when it expired, how many client
slots it had left, or any way to pay. Provider-hosted mode now shows the
billing section as Plan. It lists the evaluation or paid plan, its
expiry and the workspaces in use, and on an evaluation it offers the
plans above the current cap at the licence server's Stripe prices.
Owners and admins get Buy, Manage billing, and "Apply my purchase now";
read-only members are told who can buy. After Stripe returns to the
portal the panel keeps refreshing the licence until the purchase lands,
then polls until the restarted control plane serves the new plan,
without reloading the page.
Exercised on a fresh v6.4.1 provider bundle running this build against
a stand-in licence server: the tab, the evaluation expiry and slot
usage, monthly and annual pricing, Buy reaching checkout with the
platform's licence ID and portal return URL, the periodic refresh
signed with the lab's lease key, and a 390px layout with no horizontal
overflow.
A provider could stand the MSP bundle up and evaluate it without asking
anyone, but paying still meant emailing its public key to Pulse and
copying a licence file onto the host by hand. With the licence server's
self-serve purchase path (pulse-pro license-server/provider_msp_purchase),
the control plane can do the whole thing itself.
The control plane now signs a challenge with its lease signing key to
fetch the licence its subscription entitles, validates it exactly as
startup does, requires it to bind this platform's key, and keeps it in
the data directory, because the host licence is a read-only secret
mount. Startup prefers that renewed licence while it validates, so a
paying provider keeps starting after the self-issued evaluation on the
host expires. A changed licence restarts the control plane through the
existing graceful shutdown, since the plan version is read at load by
every workspace-limit, backup and status path; client workspaces keep
running. Preflight, proof, recovery and backup accept either signed
source.
The portal gains owner and admin routes for plan state, checkout, the
Stripe billing portal and an immediate licence refresh, and the plan list
comes from the licence server so it shows only what can be bought, at
Stripe's price. Adds the msp_solo plan: 3 client workspaces, the first
paid step above the 2-workspace evaluation.