Metric identity lived in idx_metrics_lookup, ordered (type, id, metric,
tier, timestamp). Once a series retains history, that order gives every
series its own insertion point, so each poll's commit rewrites one index
leaf page per series through the WAL and again at checkpoint. On a
three-node homelab running v6.4.5-rc.2 that came to 330-607 KB/s of disk
writes, about 10.6 KB per ~60-byte sample and 27-50 GB a day (#1966).
idx_metrics_query_all already holds the same five columns time-major, so
one poll's samples for a resource share pages. It now carries uniqueness
and serves every read, and the metric-major lookup index is dropped. With
an hour of retained history for 480 series, 30 polls write 6,022 WAL
frames instead of 25,310 (4.2x fewer); the empty-table issue-1124 estate
drops from 36,308 frames to 20,223, and its ceiling tightens from 40,000
to 23,000.
The tier-reconciliation overlap probe now names the identity index too;
left to the planner it seeks on (tier, timestamp) alone once the
metric-major tree is gone, which TestRetainedQueryPlansUseIndexes caught.
Metric-filtered reads now scan a resource's other metrics in the window:
a single-metric 500-node query went from 45 to 59 us and the 24-hour
50-disk smart_temp batch from about 0.25 to 0.49 s. All-metric dashboard
reads are unchanged. Continuous write volume is the user-visible harm, so
the contract records that trade explicitly.
The migration reuses the crash-safe swap: databases whose identity is
already enforced by a unique index defer the rebuild to startup
maintenance and swap in one transaction, and a database with no unique
index migrates synchronously. Keeping the name idx_metrics_query_all
means a downgrade rebuilds only the lookup index and leaves no orphan.
Refs #1966
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.
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.
provider-msp status, which upgrade.sh and the install proof gate on,
runs in a one-off `docker compose run` container attached only to the
provider ingress network. Its health check dialled each client's
container on the client's isolated network, which that container cannot
reach, and an unreachable client counts as unhealthy. So on any install
with a real client, status failed with "unhealthy active workspaces" and
./upgrade.sh --dry-run exited 1: the backup-gated upgrade path refused to
run as soon as a provider onboarded anyone. CI proofs never saw it
because they only ever run against an empty install. Reproduced on a
fresh v6.4.1 provider bundle with one client whose container Docker
reported healthy.
The runtime image declares a HEALTHCHECK that probes the same
/api/health inside the container, and Docker records the result where
any caller with the socket can read it. Use that when it exists and keep
the HTTP probe for images without one. With the change the same install
reports healthy_tenants=1 and upgrade.sh --dry-run plans cleanly.