Commit Graph

386 Commits

Author SHA1 Message Date
pulse-triage[bot] 2dd856e9aa Merge upstream main with provider Plan verification
Preserve the reviewed provider Plan return history and incorporate current upstream changes. Rebuild the portal bundle and manifest from the combined source.

Change-source: pulse-maintainer
2026-09-24 16:04:28 +01:00
rcourtman 951424d4ff Keep the provider portal up when the MSP licence lapses
A provider-hosted control plane refused to start once its licence was past
expiry and the 7-day grace, because startup validated it with
ValidateLicense. So at the next restart after a 60-day evaluation ran out,
or after a paying provider's subscription lapsed, the portal went down, and
the portal's Plan tab is the only place a provider can buy or renew.
Reproduced on the walkthrough lab.

Startup now accepts an authentic, key-bound licence even when lapsed
(ValidateLicenseAllowingLapse), preferring a current licence and otherwise
the one that expired later, and logs the lapse. It unlocks nothing: client
runtimes still verify the provider licence in every lease with
ValidateLicense and drop MSP capabilities, new clients are refused with
provider_msp_license_lapsed, and the refresher still refuses a lapsed
licence. The Plan panel says the evaluation or plan ended and offers the
plans, and provider-msp status reports license_lapsed without failing so a
lapsed install can still upgrade.

The same run showed the portal printing control plane error codes such as
provider_msp_license_lapsed instead of their message. The portal now
prefers an error's message over its code, which also fixes
already_subscribed and checkout_unavailable in the Plan tab.
2026-09-24 13:07:10 +01:00
rcourtman 353e88d22b Merge pull request #2232 from rcourtman/claude/msp-refresh-test-utc
Compare the renewed licence expiry in UTC in its test
2026-09-24 10:24:12 +01:00
pulse-triage[bot] 4b515c0dbf Merge verified provider Plan return onto current main
Compose the previously reviewed Plan-return repair with current main for exact-lock source proof.

Change-source: pulse-maintainer
2026-09-24 09:58:47 +01:00
rcourtman b815ea141e Compare the renewed licence expiry in UTC in its test
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.
2026-09-24 09:22:17 +01:00
pulse-triage[bot] d1c3ac82e1 test(release): gate provider-pair Docker isolation
Exercise two provider installations with the same tenant ID, unowned-network collision and cleanup isolation. Gate the release-line push before packet freeze and repeat against digest-verified candidate images during container qualification. Update the deployment-installability contract and pin both workflow gates in the release-asset verification suite.

Change-source: pulse-maintainer
2026-09-24 09:04:56 +01:00
pulse-triage[bot] a3299d19d4 Merge provider Plan return verification with current main
Carry the reviewed checkout and billing-return confirmation fix on the assigned source after the Core registry merge. Keep the paid-plan announcement tied to the running control-plane response and retain the embedded portal bundle.

Change-source: pulse-maintainer
2026-09-24 08:02:24 +01:00
pulse-triage[bot] 1a65c70561 fix(provider-msp): scope tenant cleanup to owning provider
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
2026-09-24 06:10:12 +01:00
pulse-triage[bot] a631fc8039 fix(portal): avoid charge claims from cancelled return
Treat checkout return values as navigation hints rather than evidence of a completed or absent charge. Keep the cancelled message conditional and ignore unrecognised values.

Change-source: pulse-maintainer
2026-09-24 05:08:47 +01:00
pulse-triage[bot] ac721a264b fix(portal): preserve trailing billing return check
When the licence check beats the initial Plan load, show a matching running paid plan without interpreting it as a billing change, but retain the second check so a trailing webhook can still apply a later update.

Change-source: pulse-maintainer
2026-09-24 05:07:50 +01:00
pulse-triage[bot] 8a59a8df7f fix(portal): recognise already-applied billing returns
Confirm a matching paid licence in the running plan even when the background refresher applied it before the Manage billing return check. Keep unchanged visits silent and guard stale or unmatched plan responses with regression coverage.

Change-source: pulse-maintainer
2026-09-24 05:06:04 +01:00
pulse-triage[bot] d1ea9ce0db fix(portal): confirm paid provider plan from running licence
Treat the provider checkout return as navigation, not payment evidence. Confirm the signed active refresh against the paid running plan even when background refresh applied it without another restart, keep an unmatched return conditional, cover manual Apply and adverse paths, rebuild the embedded bundle, and record the Plan confirmation contract in cloud-paid.

Change-source: pulse-maintainer
2026-09-24 04:53:06 +01:00
pulse-triage[bot] 4f7718a27f fix(cloudcp): reject unowned tenant networks during provisioning
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
2026-09-24 03:50:21 +01:00
pulse-triage[bot] 55b3b757d3 Merge commit '8430764009d3ec84b2798fbf105516f66c4d7b8d' into maintainer-async/20260924T024010Z-core-runtime/pulse
Change-source: pulse-maintainer
2026-09-24 03:48:59 +01:00
pulse-triage[bot] 65c8011d77 Merge provider Plan return correction with current main
Preserve the refreshed-plan confirmation and its generated portal assets over provider MSP client recovery. Change-source: pulse-maintainer

Change-source: pulse-maintainer
2026-09-24 03:21:23 +01:00
pulse-triage[bot] d7bf38a647 test(cloudcp): isolate provider MSP recovery by ingress network
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
2026-09-24 03:06:17 +01:00
rcourtman a27a7d07f1 Merge pull request #2216 from rcourtman/claude/msp-support-network-reconcile
Keep provider MSP clients running across upgrades
2026-09-24 03:04:48 +01:00
pulse-triage[bot] 1a4504e1b2 fix(portal): confirm applied provider plan on current main candidate
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
2026-09-24 02:41:39 +01:00
pulse-triage[bot] 82f07453f8 fix(cloudcp): recover provider MSP clients after support recreates
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
2026-09-24 02:24:14 +01:00
rcourtman bd4cad1bb9 Merge remote-tracking branch 'origin/main' into claude/msp-support-network-reconcile
# Conflicts:
#	docs/release-control/v6/internal/subsystems/cloud-paid.md
#	docs/release-control/v6/internal/subsystems/deployment-installability.md
2026-09-24 02:23:30 +01:00
pulse-triage[bot] 53206a1e60 fix(portal): confirm applied provider plan before announcing it
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
2026-09-24 01:54:47 +01:00
pulse-triage[bot] 08b4fa835c fix(cloudcp): apply shortened provider MSP licence periods
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
2026-09-24 01:47:02 +01:00
rcourtman 5c2b4de313 Apply a plan changed in Manage billing on return
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.
2026-09-24 00:28:19 +01:00
rcourtman 4daf0df92c Keep provider MSP clients running across upgrades
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.
2026-09-24 00:06:49 +01:00
rcourtman 15ffacab59 Merge remote-tracking branch 'origin/main' into claude/msp-portal-plan-panel
# Conflicts:
#	docs/release-control/v6/internal/subsystems/cloud-paid.md
#	docs/release-control/v6/internal/subsystems/deployment-installability.md
2026-09-23 23:47:42 +01:00
rcourtman edda116236 Show a provider-hosted platform its plan and let it buy one
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.
2026-09-23 23:45:28 +01:00
rcourtman 185c6ac80b Let a provider-hosted platform buy and renew its own licence
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.
2026-09-23 23:30:34 +01:00
rcourtman f338bd4765 Judge client health from Docker when status runs out of band
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.
2026-09-23 22:46:47 +01:00
pulse-triage[bot] 92f2619509 build(deps): bump portal frontend esbuild to 0.28.2
Apply Dependabot #2142 (esbuild 0.28.1 -> 0.28.2) and regenerate the committed portal build manifest so the source_hash covers the changed package.json. The lockfile and package.json reproduce the Dependabot blobs exactly; the manifest hash is e65ea72f, matching the value the failing backend check computed.

Change-source: pulse-maintainer
Contract-Neutral: Dev-only portal bundler patch bump (esbuild 0.28.1 -> 0.28.2); no public contract or runtime behavior changed.
2026-09-23 06:14:00 +01:00
rcourtman 019852f1f3 chore(portal): patch Vitest security dependency
Update the Pulse Account portal Vitest dependency to 4.1.11, clearing Dependabot security alerts 154 and 155 for Vitest and @vitest/mocker.

Contract-Neutral: Dev-only Vitest security dependency update; no product contract or runtime behavior changed.
2026-09-17 00:32:56 +01:00
rcourtman 1b51a9c948 Use example.com for the dev-mock portal login email
The portal dev harness mocked the signed-in user as a plausibly real
gmail address in this public repo; the other mock members already use
example.com addresses. Align the login email with them.
2026-09-01 16:11:09 +01:00
pulse-triage[bot] ba4c157aa2 Bound control-plane request reads
Change-source: pulse-maintainer
2026-09-01 15:15:27 +01:00
pulse-triage[bot] 560c2de026 Automate dependency trust maintenance 2026-08-30 04:02:27 +01:00
rcourtman 56262c6368 Fix provider MSP evaluation setup flow 2026-08-11 16:51:15 +01:00
rcourtman aae8aef4ab fix(security): bound provider MSP restore writes
restoreProviderMSPArchiveFile copied each tar entry with an unbounded
io.Copy, so the only limit on what a restore wrote to disk was the size
the archive declared for itself. A gzip bomb, a PAX sparse entry claiming
a huge logical size, or a corrupt stream could fill the target volume.

Bound extraction the way readProviderMSPBackupManifestBytes already
bounds the manifest: a per-entry cap and a cumulative cap across the
whole restore, both enforced against the bytes actually copied rather
than the declared header size. That size comes from the archive, so it
is only good for an early reject, never as the bound. An entry that
overruns fails the restore and its partial file is removed, rather than
being silently truncated into a file that looks complete.

A restore that failed partway had already deleted whatever it replaced,
leaving a half-populated control plane that looks bootable and forcing
the retry to use replace. Roll the partial restore back to an empty
target instead, and say so in the error.

Follows the zip-slip fix in the same function (CodeQL alert 314).
2026-08-06 22:39:20 +01:00
rcourtman 9615b5f2b0 fix(security): close the four open CodeQL findings
Resolves every open code scanning alert on the repository. Dependabot and
secret scanning were already clear.

SMART temperature truncation (alerts 312, 313). parseRawValue returns a
64-bit raw attribute value, but DiskSMART.Temperature is an int, which is
32 bits wide on the 386 and arm release builds Pulse ships. The range check
ran after the narrowing conversion, so a raw value of 4294967316 truncated
to 20 and was published as a plausible 20 degree reading.
validSMARTTemperature64 now gates the conversion.

Provider MSP restore archive names (alert 314). cleanProviderMSPArchiveName
rejected a leading "../" but not a bare "..", which path.Clean produces from
entries such as ".." and "a/../..". pathIsInside caught the escape
downstream, so this was not exploitable, but the sanitizer now rejects it
outright instead of depending on a second gate.

TrueNAS device paths (alert 315). vdev.Device is supplied by the appliance,
concatenated into a path and published verbatim on ZFSDevice.Path, so values
like "//evil.example.com/share" and "/\evil.example.com" passed straight
through. devicePath now drops traversal segments and backslashes and
collapses a leading double slash. The alert's open-redirect framing does not
apply here, there is no redirect sink on this path, but the value is
untrusted input rendered as a path and is worth normalising.

Patrol readiness cache key (alert 311). The key is persisted to
ai_patrol_model_readiness.json and embedded an unkeyed SHA-256 of the Ollama
Basic Auth username and password. That password is chosen by a human, so
anyone holding the evidence file could recover it offline at two SHA-256
operations per guess. The fingerprint is now HMAC-SHA256 keyed with a
32-byte per-install salt stored beside the evidence at mode 600. Credential
rotation still invalidates the cache and the key still survives a restart.

Each fix carries a regression test confirmed to fail against the previous
implementation.

monitoring.md carries the one warranted contract refinement. It already
required SMART temperature selection to accept only plausible readings, and
that rule now states the width at which plausibility is decided.

Contract-Neutral: CodeQL security fixes with no public-contract delta and no payload change. monitoring.md carries the one warranted refinement (SMART plausibility decided at 64-bit width). Residual demands are inapplicable: ai-runtime readiness prose documents interruption semantics, not cache-key derivation, and the credential-invalidation contract is unchanged; cloud-paid and deployment-installability contracts never name archive-entry sanitisation; agent-lifecycle owns smartctl.go but its SMART temperature prose lives in the staged monitoring.md.
2026-08-06 18:11:51 +01:00
rcourtman 9d9f13e7b0 Self-issue the MSP evaluation licence, and fix two things it exposed
The evaluation mode added earlier today was hollow. An unlicensed control
plane starts, but release-build client runtimes only trust entitlement
leases chained to a Pulse-signed licence, so its client workspaces ran
without the capabilities the provider was evaluating. Standing the stack
up proved the portal and the isolation boundary and nothing else.

setup.sh now requests a capped evaluation licence from the licence server
when no licence path is set, sending only the public half of the key it
generated locally. It degrades rather than blocks: a missing key, an
unreachable server, or a licence-free response leaves the install
unlicensed with an explicit warning, an existing licence on disk is
reused, and PULSE_PROVIDER_MSP_SKIP_EVAL_LICENSE skips it for air-gapped
hosts. Guarded with an if-test rather than a trailing true inside the
command substitution, because the derive helper calls die and exit in a
subshell is not a status that can be caught, so setup.sh aborted under
set -e.

Second fix. The lease capability ceiling was selected by licence
presence, so an unlicensed provider control plane fell through to the
Pulse-hosted branch and minted leases claiming relay, mobile and push,
which a provider deployment cannot serve and which previously caused
repeating relay registration failures in client runtimes. The ceiling now
follows hosting via SetProviderHosted. providerChained keeps its narrower
meaning of having a licence available to embed.

Third. Corrects a sentence I wrote into cloud-paid.md this afternoon
claiming msp_eval carries the same MSP capabilities. A plan version
selects the workspace cap; it does not entitle a workspace.

The regression test was negative-tested by reverting the ceiling selector
and confirming it catches relay. All three setup.sh degradation paths
were exercised directly. Licence server side is pulse-pro 7f6a319 and is
not live until the next deploy-license-server run.
2026-08-03 18:14:05 +01:00
rcourtman 6be2af1c19 Let providers evaluate MSP without asking permission first
Two mandatory round-trips stood between an interested MSP and their first
screen, and neither was technical.

setup.sh required four image digests shipped as literal <pin>
placeholders, so the only way to get them was to ask. All four images are
publicly readable, so there was never anything to hand out. setup.sh now
resolves each blank pin to an immutable digest from its published tag via
buildx imagetools and writes it back to .env; hand-set values are left
alone.

setup.sh then died outright without a licence file, so nobody could start
the stack, create a workspace, or see the portal until a human minted a
licence for them. The control plane already ran unlicensed via
ProviderMSPPlanSourceEnvFallback; only the installer refused. A licence
path that is set but missing is still a hard failure, since that is a
misconfiguration rather than a choice.

Unlicensed now means evaluation rather than the cheapest paid tier. The
env fallback defaulted to msp_starter, handing every unlicensed
deployment the full 5-client Starter allowance and leaving no boundary
between evaluating and buying. Adds msp_eval at 2 workspaces: same
capabilities, smaller cap, not purchasable, not on the public ladder.

An isolation guarantee is the one claim a provider cannot evaluate from a
screenshot, and both MSP leads this year went quiet at exactly this step.

Contracts: cloud-paid records the unlicensed plan rule and the
strictly-below-paid invariant; deployment-installability records
credential-free, correspondence-free installability.

Verification: TestMSPEvalCapStaysBelowCheapestPaidTier,
TestCanonicalizePlanVersion_MSPEval,
TestProviderMSPSetupScriptSupportsUnlicensedEvaluation. The last was
negative-tested by reintroducing a <pin> placeholder and confirming it
fails. ensure_image_pins exercised against the live registries.
licensing, cloudcp, control-plane and installtests all green.
2026-08-03 16:31:24 +01:00
rcourtman f668614afd Add a portal preview scenario fit to be seen in public
The existing preview scenarios exercise edge cases and are named for it
(MSP Test Workspace A/B/C), and none of them set the alert counts, so
the Alerts column renders 'unknown' and the fleet rollup is invisible.
That is fine for a dev harness and useless for showing anyone what the
provider portal does.

Adds a 'demo' scenario: six settled clients with invented names on
reserved example domains, one critical and one warning, so the portal
surfaces a real 'Next: Review Hilltop Care' and the fleet summary reads
the way it does on a normal morning. This is what the pulserelay.pro MSP
walkthrough is recorded against.

Preview harness only, no production path touched. Portal suite green,
103 tests.
2026-08-03 15:34:20 +01:00
courtmanr@gmail.com 802986add4 Bump postcss to 8.5.23 in both frontend lockfiles
Resolves the two open Dependabot alerts for GHSA-r28c-9q8g-f849
(source-map auto-loading path traversal, patched in 8.5.18). postcss
is a build-time dependency, so no shipped runtime code was affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:46:28 +01:00
rcourtman 6e9fb01887 Cover eval CLI parsers, cloud audit predicates and vmware fixture fetching
Five new branch-coverage tests over the remaining pure vein, with no source or
existing test touched.

cmd/eval: twelve argument and environment parsers taken to full coverage,
including the two provider-filter variants driven so their differing default
behaviour is proved rather than assumed, the model list and exclude keyword
splitters over empty, whitespace and duplicate input, the selection reason
across every reason it can return, and the provider ordering proved
deterministic across repeated runs over the same map.

internal/actionlifecycle: the plan audit persistence asserted by reading the
record back through the store and by proving a store error is propagated rather
than swallowed, the policy mutation wrapper on both the success and error paths,
and the availability check error unwrapped through errors.Is.

internal/cloudcp: the audit failure accumulator including the monotonic OK flag
and the absence of deduplication, the container health predicate over each state
and health string it recognises with the case sensitivity of the state check
pinned, the tenant display name resolver against a real registry, and the
storage admission guard proved never to reach Docker when disabled.

internal/vmware: the fixture fetcher with independence asserted in both
directions and across consecutive calls, the refresh error wrapping asserted by
sentinel and message, the cached snapshot proved sorted, and the transport close
proved behaviourally by counting accepted connections on a loopback server
rather than by absence of error.

Contract-Neutral: test-only branch coverage, no contract surface touched
2026-07-23 08:07:35 +01:00
rcourtman c757a73382 Cover replay bundle loading, magic link expiry and subscription mapping
Three new branch-coverage tests taking six previously unreached functions from
zero to covered, with no source or existing test touched.

internal/ai/qualification: the replay bundle loader across a missing path, a
directory where a file is expected, invalid JSON, trailing JSON values and a
valid bundle round-tripped field by field, plus the artifact root creation
including the arm where the parent path is a regular file. Fixtures are written
into a temp directory so nothing outside it is read or created.

internal/api: the in-memory and SQLite magic link stores both deleting expired
entries, with the boundary case of a link expiring exactly at the passed instant
asserted on the real comparison, the valid entries proved still present, and
repeat deletion proved idempotent.

internal/cloudcp/stripe: the subscription mapper resolving a real customer over
the fallback, the fallback when no customer is present, item and price
extraction across empty and multiple item lists, and the two provisioner options
applied and overridden in order.

Reported for follow-up, not fixed here: mapStripeSubscription dereferences its
subscription pointer without a nil guard and is safe only because its single
caller skips nil subscriptions.

Contract-Neutral: test-only branch coverage, no contract surface touched
2026-07-23 06:59:05 +01:00
rcourtman 133a47d284 Cover agent policy, alias normalization and agent report helpers
Eight new branch-coverage tests taking thirty-one previously unreached
functions from zero to covered, with no source or existing test touched.

internal/kubernetesagent: twenty-one pure report helpers, including the pointer
converters proved non-aliasing in both directions, the ingress host and address
collectors across their trim, dedupe and insertion-order arms, the endpoint
slice readiness count where a nil Ready field counts as ready, and the target
role predicate.

internal/agentexec: the sudo long-option value gate over the real option list
including the inline equals form, and the approval grant verification error
unwrapped through errors.Is.

internal/alerts: the alert config alias normalization across the nil config
guard, the empty threshold early return, the blank type-key continue arm and
the legacy-delete versus supported-keep split, asserting both maps stay
independent.

internal/alerts/specs: the resource incident rollup evidence validation, each
failure arm asserted on its concrete error and the check order pinned when
several fields are invalid at once.

internal/cloudcp/docker: the not-found predicate through a wrapped error, the
route host label precedence, and the Traefik host rule parser across quoting
styles, combined matchers, multiple host clauses and malformed input.

internal/cloudcp/portal: the anonymous bootstrap builder, asserting no tenant
or user identity field is ever populated on the anonymous result.

internal/config: the legacy OIDC environment provider, including the arm where
an already-configured provider is present and the redirect derivation from a
public URL with a trailing slash.

internal/dockeragent: the update-all payload decode across wrong-typed and
missing fields, and the docker filter conversion.

Contract-Neutral: test-only branch coverage, no contract surface touched
2026-07-23 06:44:33 +01:00
rcourtman 82083378a2 Cover cloud tenant registry queries, unified views and slowlog wrappers
Three new branch-coverage tests taking eighteen previously unreached functions
from 0.0% to covered.

internal/cloudcp/registry: the workspace limit error message on both the nil
and populated receiver, the active workspace count per account, the tenant
lookup across owning account, foreign account and missing rows, the invitation
listing by email including case and whitespace normalization, and the
invitation delete for both an existing pair and a pair that never existed.

internal/unifiedresources: the Docker container and Kubernetes node typed
views, the presentation listing and the metrics target, each asserted on the
nil receiver, the nil nested payload and the populated case, with returned
slices proven independent of the store.

pkg/db: the tracing wrappers around BeginTx, Query, QueryRow and their context
variants plus the pool setters, with exact histogram deltas proving the
observe wiring rather than that SQLite works.

All three files are new; no source or existing test was touched.

Contract-Neutral: test-only: new Go branch-coverage tests, no source or contract change
2026-07-22 21:41:02 +01:00
rcourtman ff93d99fa1 Add Go branch-coverage tests for pure helpers across six packages
Test-only wave, contract-neutral. New *_branchcov0719pm_test.go files cover
previously-uncovered pure value-in/value-out helpers, each verified to move
its target functions from 0% to full coverage:

- internal/agentcontext: formatKubernetesServicePorts (empty/single/cap/overflow
  arms) and addMetricFact (nil-metric, percent/value/ratio arms) now 100%.
- pkg/reporting: reportLogoTypeFromPath 0->100, reportLogoTypeFromData 28.6->100,
  scaledLogoSize 70->90 (extension and aspect branches).
- internal/cloudcp/email: RenderMagicLinkEmail 0->80 (render success path; the
  compile-time template-error arm is unreachable and left uncovered).
- internal/recovery: recoveryDetailString (nil map, missing key, non-string,
  string arms) and recoveryPointObservedAt 40->100.
- internal/ai/tools: ErrStrictResolution/ErrRoutingMismatch ToToolResponse 100.
- internal/ai/providers: every NormalizeCollections receiver 0->100.

No source or existing test modified.
2026-07-19 15:11:02 +01:00
rcourtman 7791b35ae9 Add Go branch-coverage tests for canonical trust and recovery helpers
Cover pure functions the recent canonical Operational Trust and protection
posture work landed with no unit test:

- recovery/model posture: enum Valid, struct Clone/Validate/Payload and the
  normalize/cloneTime/validOutcome/sortedUniqueStrings/compareProviderStates
  helpers (20 funcs, all 0%->100%)
- operationaltrust contracts: EvidencePayloadRef/Acknowledgement/Suppression/
  LifecycleTransition Validate plus LifecycleTransition/NotificationLink Clone
- cloudcp/proxytrust: ClientIP, ExtractRemoteIP, rightMostUntrustedForwardedIP
  and IsTrustedProxyIP forwarded-header and CIDR parsing
- mockmodel: NormalizeBlendWeight, SeriesForTimestamps, seriesForProfile,
  diskIOValue and flatValue deterministic seeded math
- updatesignature: DecodePrivateKey and HasTrustedPublicKeys
- recovery keys: ProxmoxPBSGuestLooseContinuityKey branch guards
- vmware SourceID and truenas availableAppLogContainers formatting

Test-only, contract-neutral. New *_branchcov0719_test.go files only; no source
or existing test touched.
2026-07-19 15:11:01 +01:00
rcourtman ce7702db7a Add Go branch-coverage tests for models, cloudcp and actionlifecycle helpers
New *_branchcov0718_test.go files extend the Go coverage wave to three more
areas of previously-uncovered pure logic. Covered areas include the domain-type
NormalizeCollections normalizers in models.go, the cloud control-plane pure
helpers for hosted-entitlement org-id normalization, magic-link URL redaction
and default MSP backup paths, and the actionlifecycle dispatch-attempt context
round-trip plus the typed-error Error and Unwrap methods.

Test-only, with no runtime or subsystem-contract change. Impure targets that
need a live Docker manager or tenant registry were skipped per the purity gate
and noted in the wave reports. Verified in a clean worktree at HEAD with go vet
and package tests green, gofmt clean, and every covered target moved from 0
percent toward full coverage.
2026-07-19 15:10:09 +01:00
rcourtman 206b68cc94 Clarify commercial offer boundaries 2026-07-14 17:18:14 +01:00
rcourtman 6b18652812 Point the portal operations-guide link at the repository root
The MSP portal support row deep-linked docs/MSP.md at blob/main, which
the repo-docs link-drift guard rightly rejects: a released portal build
must not reference branch-tip docs that can move out from under it.
The row keeps the docs/MSP.md path as text and links the repository
root instead, matching every other runtime surface. Portal dist rebuilt
alongside.
2026-07-11 11:11:58 +01:00
rcourtman 0db74c7eab Retheme the provider portal onto the product design tokens with dark mode
The portal shipped with a GitHub-Primer palette, light-only, while every
"Open client" hands the operator into the slate/blue Pulse product UI
(which most monitoring users run dark). The portal now uses the product's
Tailwind slate/blue semantic tokens so both halves of the MSP workflow
read as one product:

- Light theme mirrors the product exactly: slate-100 page, white surfaces,
  slate borders/inks, blue-600 solid buttons.
- Full dark theme (slate-900/800 surfaces, blue-400 links, product-style
  translucent status chip backgrounds with readable 300-series text),
  applied via prefers-color-scheme by default with a persisted header
  toggle (data-theme override in localStorage, nonce'd no-flash bootstrap
  in the page template).
- Hardcoded badge/status border literals, the toast, the danger-button
  hover, and border-only panels that relied on the old white page are all
  tokenized so every state renders in both themes.
- Dropped two dead Roboto @font-face blocks and their data-URI payloads
  (nothing referenced the family): portal_app.css shrinks 135KB -> 43KB.

Verified live in both themes: workspace roster, setup queue, client
onboarding panel chips, Access invite panel, signed-out instruction card,
and the error toast; theme choice survives reload and sign-out. Portal
vitest suite 103/103 incl. new theme tests; frontend/dist sync test green.
2026-07-10 11:59:06 +01:00