From bd6f77e093c6cb327cf524816447ef896cae3768 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 4 Jun 2026 14:07:14 +0100 Subject: [PATCH] Prepare v6.0.0 release candidate Tighten v5-to-v6 upgrade safety, release installability, provider MSP mode handling, AI cost accounting, metrics flushing, and frontend guardrails for the v6.0.0 GA candidate. --- .github/workflows/backfill-release-assets.yml | 2 +- .github/workflows/create-release.yml | 8 +- .github/workflows/deploy-demo-server.yml | 2 +- Dockerfile | 14 +- .../provider_msp_preflight.go | 4 +- .../provider_msp_preflight_test.go | 34 + cmd/pulse-control-plane/provider_msp_proof.go | 4 +- .../provider_msp_proof_test.go | 16 + deploy/provider-msp/.env.example | 4 + deploy/provider-msp/docker-compose.yml | 2 +- docs/TEMPERATURE_MONITORING.md | 27 +- docs/UPGRADE_v5.md | 11 +- docs/UPGRADE_v6.md | 4 +- docs/architecture/v6-pricing-and-tiering.md | 2 +- .../internal/V5_MAINTENANCE_SUPPORT_POLICY.md | 4 +- ...-promotion-readiness-blocked-2026-04-04.md | 12 +- docs/release-control/v6/internal/status.json | 16 +- .../v6/internal/subsystems/cloud-paid.md | 7 + .../subsystems/deployment-installability.md | 10 +- docs/releases/RELEASE_NOTES_v6.md | 6 +- .../public/docs/MIGRATION_UNIFIED_NAV.md | 4 +- frontend-modern/src/api/dockerMetadata.ts | 26 + .../src/components/Discovery/DiscoveryTab.tsx | 1726 +++++++++-------- .../Discovery/__tests__/DiscoveryTab.test.tsx | 28 +- .../Discovery/useDiscoveryTabState.ts | 81 +- .../ResourceDetailDrawer.discovery.test.ts | 8 +- .../resourceDetailDiscoveryModel.ts | 51 +- .../Settings/reportingResourceTypes.ts | 2 +- .../__tests__/WorkloadsSurface.k8s.test.tsx | 1 + .../src/components/shared/TagBadges.tsx | 5 +- .../shared/__tests__/TagBadges.test.tsx | 33 +- .../__tests__/WebInterfaceUrlField.test.tsx | 36 + .../components/shared/summaryChartLayout.ts | 1 + .../shared/useWebInterfaceUrlFieldState.ts | 9 + .../shared/webInterfaceUrlFieldModel.ts | 2 +- .../features/docker/DockerNetworksTable.tsx | 166 +- .../__tests__/storageAdapters.test.ts | 1 + .../__tests__/useUnifiedResources.test.ts | 1 + .../src/stores/websocket-global.ts | 63 + frontend-modern/src/stores/websocket.ts | 4 + frontend-modern/src/types/api.ts | 1 + frontend-modern/src/types/resource.ts | 1 + frontend-modern/src/useAppRuntimeState.ts | 1 + .../src/utils/__tests__/tagColors.test.ts | 72 +- .../src/utils/resourceMetadataEvents.ts | 2 +- frontend-modern/src/utils/tagColors.ts | 172 +- internal/ai/chat/service.go | 5 +- .../ai/chat/service_patrol_additional_test.go | 65 + internal/ai/cost/coverage_test.go | 58 +- internal/ai/cost/pricing.go | 103 +- internal/ai/patrol_ai.go | 6 + internal/ai/patrol_ai_eval_additional_test.go | 29 + internal/ai/service_coverage_imp_test.go | 13 +- internal/api/chat_service_adapter.go | 7 + internal/api/config_handlers.go | 14 + internal/api/config_handlers_add_test.go | 52 + .../api/config_handlers_setup_script_test.go | 91 +- internal/api/config_node_handlers.go | 4 + internal/api/route_inventory_test.go | 2 + internal/api/router_routes_auth_security.go | 1 + internal/api/router_state_test.go | 95 + internal/api/setup_script_render.go | 81 +- internal/api/state_summary.go | 108 ++ internal/api/unified_agent_download_test.go | 29 + internal/cloudcp/config.go | 47 +- internal/cloudcp/config_test.go | 71 + internal/cloudcp/docker/labels.go | 2 +- internal/cloudcp/provider_msp_backup.go | 8 +- internal/cloudcp/provider_msp_backup_test.go | 20 + internal/cloudcp/provider_msp_bootstrap.go | 8 +- .../cloudcp/provider_msp_bootstrap_test.go | 17 + internal/cloudcp/provider_msp_recovery.go | 4 +- internal/cloudcp/routes.go | 6 +- internal/cloudcp/routes_test.go | 125 +- internal/cloudcp/server.go | 2 +- internal/cloudcp/tenant_runtime_rollout.go | 2 +- internal/hostagent/proxmox_setup.go | 11 +- internal/hostagent/proxmox_setup_test.go | 45 + internal/models/converters.go | 2 + internal/models/converters_additional_test.go | 4 + internal/models/models.go | 18 + internal/models/models_frontend.go | 29 +- internal/models/state_snapshot.go | 7 + .../models/state_snapshot_frontend_test.go | 7 + .../monitoring/canonical_guardrails_test.go | 37 +- internal/monitoring/ceph.go | 12 + internal/monitoring/ceph_test.go | 78 +- internal/monitoring/guest_agent_work.go | 69 + internal/monitoring/guest_memory_agent.go | 36 +- .../monitoring/guest_memory_agent_test.go | 69 + internal/monitoring/guest_metadata.go | 13 +- internal/monitoring/host_agent_temps.go | 12 +- internal/monitoring/host_agent_temps_test.go | 47 +- .../monitoring/memory_source_catalog_test.go | 8 +- internal/monitoring/monitor.go | 36 +- internal/monitoring/monitor_docker_test.go | 55 + .../monitoring/monitor_extra_coverage_test.go | 75 + internal/monitoring/monitor_polling_vm.go | 565 +----- internal/monitoring/monitor_pve.go | 20 + internal/monitoring/monitor_pve_guest_poll.go | 100 +- .../monitoring/monitor_pve_node_vm_builder.go | 60 + internal/monitoring/monitor_pve_storage.go | 2 +- .../monitoring/node_memory_sources_test.go | 60 +- pkg/metrics/store.go | 143 +- pkg/metrics/store_additional_test.go | 73 +- pkg/metrics/store_downsample_test.go | 2 +- pkg/proxmox/ceph.go | 90 +- pkg/proxmox/ceph_test.go | 57 + pkg/proxmox/client.go | 68 + pkg/proxmox/client_test.go | 41 + pkg/proxmox/cluster_client.go | 14 + scripts/install.ps1 | 31 + scripts/install.sh | 263 ++- .../installtests/build_release_assets_test.go | 27 + scripts/installtests/install_ps1_test.go | 22 + scripts/installtests/install_sh_test.go | 149 ++ .../uninstall_sensor_proxy_test.go | 119 ++ .../resolve_release_promotion_test.py | 8 +- scripts/uninstall-sensor-proxy.sh | 454 +++++ 119 files changed, 4753 insertions(+), 2046 deletions(-) create mode 100644 frontend-modern/src/api/dockerMetadata.ts create mode 100644 internal/api/state_summary.go create mode 100644 internal/monitoring/guest_agent_work.go create mode 100644 internal/monitoring/monitor_pve_node_vm_builder.go create mode 100644 scripts/installtests/uninstall_sensor_proxy_test.go create mode 100755 scripts/uninstall-sensor-proxy.sh diff --git a/.github/workflows/backfill-release-assets.yml b/.github/workflows/backfill-release-assets.yml index 5af93250c..956c3ea8f 100644 --- a/.github/workflows/backfill-release-assets.yml +++ b/.github/workflows/backfill-release-assets.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: '1.25.9' + go-version-file: go.mod cache: true - name: Install Syft diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index aea82c65f..1016a60a2 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -253,7 +253,7 @@ jobs: - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: '1.25.9' + go-version-file: go.mod cache: true - name: Run backend tests @@ -484,7 +484,7 @@ jobs: - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: '1.25.9' + go-version-file: go.mod cache: true - name: Build Pulse Docker image for integration tests @@ -583,7 +583,7 @@ jobs: - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: '1.25.9' + go-version-file: go.mod cache: true - name: Set up Node.js @@ -1008,7 +1008,7 @@ jobs: - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: '1.25.9' + go-version-file: go.mod cache: true - name: Install Syft diff --git a/.github/workflows/deploy-demo-server.yml b/.github/workflows/deploy-demo-server.yml index 101890329..c12bec632 100644 --- a/.github/workflows/deploy-demo-server.yml +++ b/.github/workflows/deploy-demo-server.yml @@ -82,7 +82,7 @@ jobs: - name: Set up Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: '1.25.9' + go-version-file: go.mod cache: true - name: Set up Node.js diff --git a/Dockerfile b/Dockerfile index f01fb6ec3..01a1bf2da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -213,7 +213,8 @@ FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6 ARG TARGETARCH ARG TARGETVARIANT -RUN apk --no-cache add ca-certificates tzdata +RUN apk --no-cache add ca-certificates tzdata && \ + mkdir -p /var/lib/pulse-agent WORKDIR /app @@ -234,9 +235,16 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then \ COPY --from=release-assets-builder /app/VERSION /VERSION -ENV PULSE_NO_AUTO_UPDATE=true +ENV PULSE_NO_AUTO_UPDATE=true \ + PULSE_DISABLE_AUTO_UPDATE=true \ + PULSE_ENABLE_HOST=false \ + PULSE_ENABLE_DOCKER=true \ + PULSE_AGENT_ID_FILE=/var/lib/pulse-agent/agent-id \ + PULSE_STATE_DIR=/var/lib/pulse-agent -ENTRYPOINT ["/usr/local/bin/pulse-agent", "--enable-docker", "--enable-host=false"] +VOLUME ["/var/lib/pulse-agent"] + +ENTRYPOINT ["/usr/local/bin/pulse-agent"] # Base Pulse server runtime shared by self-hosted and hosted tenant images. FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS pulse-runtime-base diff --git a/cmd/pulse-control-plane/provider_msp_preflight.go b/cmd/pulse-control-plane/provider_msp_preflight.go index 756572fd2..507b68bac 100644 --- a/cmd/pulse-control-plane/provider_msp_preflight.go +++ b/cmd/pulse-control-plane/provider_msp_preflight.go @@ -94,8 +94,8 @@ func runProviderMSPPreflightWithDependencies(ctx context.Context, cfg *cloudcp.C report.Failures = append(report.Failures, fmt.Sprintf(format, args...)) } - if !cfg.IsProviderHostedMSP() { - addFailure("CP_CONTROL_PLANE_MODE must be %s", cloudcp.ControlPlaneModeProviderHostedMSP) + if !cfg.IsMSPControlPlane() { + addFailure("CP_CONTROL_PLANE_MODE must be %s or %s", cloudcp.ControlPlaneModeProviderHostedMSP, cloudcp.ControlPlaneModePulseHostedMSP) } if cfg.UsesStripeBilling() { addFailure("provider-hosted MSP preflight must be Stripe-free") diff --git a/cmd/pulse-control-plane/provider_msp_preflight_test.go b/cmd/pulse-control-plane/provider_msp_preflight_test.go index 34c93ca12..b4848c189 100644 --- a/cmd/pulse-control-plane/provider_msp_preflight_test.go +++ b/cmd/pulse-control-plane/provider_msp_preflight_test.go @@ -112,6 +112,40 @@ func TestProviderMSPPreflightPassesWithLicenseDockerAndStorage(t *testing.T) { } } +func TestProviderMSPPreflightAcceptsPulseHostedMSPMode(t *testing.T) { + docker := &fakeProviderMSPPreflightDocker{ + report: &cpDocker.RuntimePrerequisiteReport{ + OK: true, + DockerReachable: true, + NetworkName: "pulse-provider-msp", + NetworkOK: true, + NetworkID: "network-test", + ImageRef: "pulse:test", + ImageID: "sha256:test", + ImageAvailable: true, + }, + } + cfg := testProviderMSPPreflightConfig(t, cloudcp.ProviderMSPPlanSourceLicenseFile) + cfg.ControlPlaneMode = cloudcp.ControlPlaneModePulseHostedMSP + + report, err := runProviderMSPPreflightWithDependencies( + context.Background(), + cfg, + providerMSPPreflightOptions{}, + fakeProviderMSPPreflightDependencies(docker, &cloudcp.StorageGuardrailReport{Enabled: false, OK: true}), + ) + + if err != nil { + t.Fatalf("runProviderMSPPreflightWithDependencies: %v", err) + } + if !report.OK { + t.Fatalf("report.OK = false, failures = %v", report.Failures) + } + if report.ControlMode != string(cloudcp.ControlPlaneModePulseHostedMSP) { + t.Fatalf("ControlMode = %q, want %q", report.ControlMode, cloudcp.ControlPlaneModePulseHostedMSP) + } +} + func TestProviderMSPPreflightSkipImagePullUsesInspectOnly(t *testing.T) { docker := &fakeProviderMSPPreflightDocker{ report: &cpDocker.RuntimePrerequisiteReport{ diff --git a/cmd/pulse-control-plane/provider_msp_proof.go b/cmd/pulse-control-plane/provider_msp_proof.go index f39d76014..6ad0fa085 100644 --- a/cmd/pulse-control-plane/provider_msp_proof.go +++ b/cmd/pulse-control-plane/provider_msp_proof.go @@ -153,8 +153,8 @@ func newProviderMSPProofRuntimeFromConfig(cfg *cloudcp.CPConfig) (*providerMSPPr if cfg == nil { return nil, fmt.Errorf("control plane config is required") } - if !cfg.IsProviderHostedMSP() { - return nil, fmt.Errorf("provider MSP proof requires CP_CONTROL_PLANE_MODE=%s", cloudcp.ControlPlaneModeProviderHostedMSP) + if !cfg.IsMSPControlPlane() { + return nil, fmt.Errorf("provider MSP proof requires CP_CONTROL_PLANE_MODE=%s or %s", cloudcp.ControlPlaneModeProviderHostedMSP, cloudcp.ControlPlaneModePulseHostedMSP) } if err := os.MkdirAll(cfg.TenantsDir(), 0o755); err != nil { return nil, fmt.Errorf("create tenants dir: %w", err) diff --git a/cmd/pulse-control-plane/provider_msp_proof_test.go b/cmd/pulse-control-plane/provider_msp_proof_test.go index c47adc3bf..3c2df80ed 100644 --- a/cmd/pulse-control-plane/provider_msp_proof_test.go +++ b/cmd/pulse-control-plane/provider_msp_proof_test.go @@ -65,6 +65,22 @@ func TestProviderMSPProofRequiresLicenseBackedPlanSourceByDefault(t *testing.T) } } +func TestProviderMSPProofRuntimeAcceptsPulseHostedMSPMode(t *testing.T) { + t.Setenv("DOCKER_HOST", "unix:///tmp/pulse-provider-msp-proof-missing-docker.sock") + t.Setenv("DOCKER_TLS_VERIFY", "") + t.Setenv("DOCKER_CERT_PATH", "") + + cfg := testProviderMSPProofConfig(t) + cfg.ControlPlaneMode = cloudcp.ControlPlaneModePulseHostedMSP + cfg.BaseURL = "https://acme.msp.pulserelay.pro" + + rt, err := newProviderMSPProofRuntimeFromConfig(cfg) + if err != nil { + t.Fatalf("newProviderMSPProofRuntimeFromConfig: %v", err) + } + defer rt.close() +} + func TestProviderMSPProofExercisesWorkspaceInstallHandoffAndIsolation(t *testing.T) { t.Setenv("DOCKER_HOST", "unix:///tmp/pulse-provider-msp-proof-missing-docker.sock") t.Setenv("DOCKER_TLS_VERIFY", "") diff --git a/deploy/provider-msp/.env.example b/deploy/provider-msp/.env.example index 1fb654eb0..34dc41bfe 100644 --- a/deploy/provider-msp/.env.example +++ b/deploy/provider-msp/.env.example @@ -1,4 +1,7 @@ # Domain +# Client runtimes are routed as https://.${DOMAIN}/. +# Provider-hosted MSP: use the MSP's own domain, for example pulse.example-msp.com. +# Pulse-hosted MSP: use a Pulse-operated provider namespace, for example example-msp.msp.pulserelay.pro, and set CP_CONTROL_PLANE_MODE=pulse_hosted_msp. DOMAIN=msp.example.com ACME_EMAIL=admin@example.com @@ -13,6 +16,7 @@ CP_PULSE_IMAGE=ghcr.io/rcourtman/pulse@sha256: # Control plane CP_ENV=production +CP_CONTROL_PLANE_MODE=provider_hosted_msp CP_ADMIN_KEY= PULSE_PROVIDER_MSP_DATA_DIR=/data PULSE_PROVIDER_MSP_DOCKER_NETWORK=pulse-provider-msp diff --git a/deploy/provider-msp/docker-compose.yml b/deploy/provider-msp/docker-compose.yml index 19679b942..eaa671429 100644 --- a/deploy/provider-msp/docker-compose.yml +++ b/deploy/provider-msp/docker-compose.yml @@ -53,7 +53,7 @@ services: environment: - CP_DATA_DIR=${PULSE_PROVIDER_MSP_DATA_DIR:-/data} - CP_ENV=${CP_ENV} - - CP_CONTROL_PLANE_MODE=provider_hosted_msp + - CP_CONTROL_PLANE_MODE=${CP_CONTROL_PLANE_MODE:-provider_hosted_msp} - CP_BIND_ADDRESS=0.0.0.0 - CP_PORT=8443 - CP_ADMIN_KEY=${CP_ADMIN_KEY} diff --git a/docs/TEMPERATURE_MONITORING.md b/docs/TEMPERATURE_MONITORING.md index 33763b14e..68b45e191 100644 --- a/docs/TEMPERATURE_MONITORING.md +++ b/docs/TEMPERATURE_MONITORING.md @@ -77,29 +77,18 @@ ssh -i /path/to/key root@node "cat /sys/class/thermal/thermal_zone0/temp" ## Legacy Cleanup (If Upgrading) -If you still have the old sensor proxy installed from prior releases, remove it from each **Proxmox host** (not the Pulse container): +If you still have the old sensor proxy installed from prior releases, remove it from each **Proxmox host** (not the Pulse container) with the supported cleanup helper: ```bash -# Stop and disable all sensor-proxy systemd units -sudo systemctl disable --now pulse-sensor-proxy pulse-sensor-proxy-selfheal.timer pulse-sensor-proxy-selfheal.service pulse-sensor-cleanup.path pulse-sensor-cleanup.service 2>/dev/null +curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ + sudo bash -s -- --uninstall --purge +``` -# Remove systemd unit files -sudo rm -f /etc/systemd/system/pulse-sensor-proxy.service -sudo rm -f /etc/systemd/system/pulse-sensor-proxy-selfheal.timer -sudo rm -f /etc/systemd/system/pulse-sensor-proxy-selfheal.service -sudo rm -f /etc/systemd/system/pulse-sensor-cleanup.service -sudo rm -f /etc/systemd/system/pulse-sensor-cleanup.path -sudo systemctl daemon-reload +If you also want to remove the old `pulse-monitor@pam` API user and tokens before re-adding the node, include `--remove-proxmox-access`: -# Remove sensor-proxy files -sudo rm -rf /opt/pulse/sensor-proxy -sudo rm -rf /etc/pulse-sensor-proxy -sudo rm -rf /var/lib/pulse-sensor-proxy -sudo rm -rf /var/log/pulse/sensor-proxy -sudo rm -rf /run/pulse-sensor-proxy - -# Optional: remove sensor-proxy SSH keys from authorized_keys -sudo sed -i '/# pulse-managed-key$/d;/# pulse-proxy-key$/d' /root/.ssh/authorized_keys +```bash +curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ + sudo bash -s -- --uninstall --purge --remove-proxmox-access ``` Reinstalling or upgrading the Pulse container does **not** remove the sensor proxy from the host — they are separate installations. If you skip this cleanup, the selfheal timer will keep running and may generate recurring `TASK ERROR` entries in the Proxmox task log. diff --git a/docs/UPGRADE_v5.md b/docs/UPGRADE_v5.md index 2a9273cc9..f133a3d8d 100644 --- a/docs/UPGRADE_v5.md +++ b/docs/UPGRADE_v5.md @@ -57,7 +57,16 @@ If you reset auth (for example by deleting `.env`), Pulse may require a bootstra ### Sensor proxy removal -The `pulse-sensor-proxy` from v4 is no longer needed — temperature monitoring is now handled by the unified agent. If you had the sensor proxy installed on your Proxmox hosts, remove it **on each host** after upgrading. See the [Legacy Cleanup](TEMPERATURE_MONITORING.md#legacy-cleanup-if-upgrading) section in the temperature monitoring docs for the full cleanup commands. +The `pulse-sensor-proxy` from v4 is no longer needed — temperature monitoring is now handled by the unified agent. If you had the sensor proxy installed on your Proxmox hosts, remove it **on each host** after upgrading: + +```bash +curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ + sudo bash -s -- --uninstall --purge +``` + +If you deleted the old node from Pulse and want the cleanup to also remove the old `pulse-monitor@pam` API user and tokens before reinstalling, add `--remove-proxmox-access`. + +See the [Legacy Cleanup](TEMPERATURE_MONITORING.md#legacy-cleanup-if-upgrading) section in the temperature monitoring docs for the full cleanup details. Skipping this step will leave a selfheal timer running on the host that generates recurring `TASK ERROR` entries in the Proxmox task log. diff --git a/docs/UPGRADE_v6.md b/docs/UPGRADE_v6.md index 39daa5ee6..5f4f0a0ad 100644 --- a/docs/UPGRADE_v6.md +++ b/docs/UPGRADE_v6.md @@ -96,8 +96,8 @@ servers. ### Can I keep Pulse v5 stable while I test Pulse v6? -Pulse v5 entered maintenance-only support on `2026-06-03` and remains eligible -only for critical maintenance fixes until `2026-09-01`. +Pulse v5 entered maintenance-only support on `2026-06-04` and remains eligible +only for critical maintenance fixes until `2026-09-02`. If you want extra caution, use a staging or otherwise controlled upgrade first and keep a rollback path available, but v6 is now the supported stable line. diff --git a/docs/architecture/v6-pricing-and-tiering.md b/docs/architecture/v6-pricing-and-tiering.md index 74945d0a0..4fe48f920 100644 --- a/docs/architecture/v6-pricing-and-tiering.md +++ b/docs/architecture/v6-pricing-and-tiering.md @@ -529,7 +529,7 @@ explain monitored-system identity: | Date | Change | Author | |---|---|---| -| 2026-06-02 | Reconciled MSP pricing evidence with the provider-hosted model: signed MSP license, Stripe-free provider control plane, isolated Pulse runtime per client, 5/15/40 client workspace caps, and request-assisted access until launch approval. | Richard | +| 2026-06-02 | Reconciled MSP pricing evidence with the provider-operated architecture: signed MSP license, Stripe-free provider control plane, isolated Pulse runtime per client, 5/15/40 client workspace caps, and request-assisted access until launch approval. | Richard | | 2026-04-29 | Replaced stale capacity-style monitoring phrasing with core-monitoring-included language across active v6 docs and upgrade-return copy so Community does not read like a former capacity upsell. | Codex | | 2026-04-23 | Removed stale self-hosted monitored-system capacity and Pro+ public-checkout language. Reaffirmed Community / Relay / Pro as current public self-hosted tiers, with Pro+ as continuity only and Pro value centered on operations, history, and admin controls. | Codex | | 2026-03-17 | Re-locked the self-hosted commercial model around monitored systems rather than installed agents. New self-hosted public pricing: Relay $4.99/$39, Pro $8.99/$79, Pro+ $14.99/$129. Added free-tier grace policy and marked the monitored-system counting migration as still required in code. | Codex + Richard | diff --git a/docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md b/docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md index e533ec66c..139763e08 100644 --- a/docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md +++ b/docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md @@ -64,8 +64,8 @@ These do not qualify as v5 maintenance work: The first stable `v6.0.0` release must publish this exact notice: -> Pulse v5 entered maintenance-only support on 2026-06-03. I will ship only +> Pulse v5 entered maintenance-only support on 2026-06-04. I will ship only > critical security, data-loss, licensing or billing blocker, installer or > updater failure, and safe migration blocker fixes for existing v5 users until -> 2026-09-01. After 2026-09-01, Pulse v5 is end-of-support and new fixes land +> 2026-09-02. After 2026-09-02, Pulse v5 is end-of-support and new fixes land > on v6 unless I publish an explicit exception. diff --git a/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md b/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md index 057c56719..a087baa41 100644 --- a/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md +++ b/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md @@ -20,8 +20,8 @@ 7. `docs/releases/RELEASE_NOTES_v6.md` and `docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md` now carry the currently proposed exact dates for the eventual GA notice: - - `v6` GA date: `2026-06-03` - - `v5` end-of-support date: `2026-09-01` + - `v6` GA date: `2026-06-04` + - `v5` end-of-support date: `2026-09-02` 8. There is still no governed `Release Dry Run` artifact or rehearsal record exercising stable inputs for: - `version=6.0.0` @@ -30,10 +30,10 @@ - the artifact-owned promotion channel for that rehearsal - the artifact-owned promoted prerelease tag for that rehearsal - the artifact-owned rollback target for that stable candidate - - `ga_date=2026-06-03` + - `ga_date=2026-06-04` - an explicit `rollback_version` - the exact derived rollback command that artifact will publish - - `v5_eos_date=2026-09-01` + - `v5_eos_date=2026-09-02` ## Why The Gate Cannot Be Cleared Yet @@ -57,10 +57,10 @@ users would still be the first real cohort for the final promotion path. - an artifact-owned promoted prerelease tag matching that rehearsal - an artifact-owned rollback target for the stable candidate - the exact planned GA and v5 end-of-support dates for the publish notice - - `ga_date=2026-06-03` + - `ga_date=2026-06-04` - an explicit stable `rollback_version` - the exact derived rollback command that artifact will publish - - `v5_eos_date=2026-09-01` + - `v5_eos_date=2026-09-02` 4. Capture the `rc-to-ga-rehearsal-summary` artifact and run URL. 5. Materialize the final rehearsal record from that artifact without hand-repairing any missing candidate tag, promoted prerelease tag, rollback diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index c1ec4d204..cf0f100aa 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -6763,7 +6763,21 @@ ], "coverage_gaps": [], "candidate_lanes": [], - "work_claims": [], + "work_claims": [ + { + "id": "codex-v6-release-release-gate-known-rc-issue-closure-for-ga", + "agent_id": "codex-v6-release", + "summary": "Resolve current v5-to-v6 parity audit gaps that would regress known user-visible fixes before GA publication.", + "target_id": "v6-product-lane-expansion", + "claimed_at": "2026-06-04T11:23:28Z", + "heartbeat_at": "2026-06-04T13:02:12Z", + "expires_at": "2026-06-04T17:02:12Z", + "work_item": { + "kind": "release-gate", + "id": "known-rc-issue-closure-for-ga" + } + } + ], "open_decisions": [], "source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", "resolved_decisions": [ diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index a6eff7a73..515173c98 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -243,6 +243,13 @@ Stripe-free and avoids a cloud-control-plane report data path across clients. into each tenant container, but it must not collect report data or render PDFs in the control plane. Tenant-local reporting and tenant-local licensing decide whether that configured brand appears. + `pulse_hosted_msp` is the Pulse-operated form of the same Stripe-free MSP + control-plane family, not the public Pulse-hosted SaaS checkout path. It + must share the license-backed MSP plan source, workspace limit policy, + disabled public signup and Stripe billing routes, isolated tenant runtime + networks, tenant-local report branding, and provider MSP operator commands + with `provider_hosted_msp` while preserving its own control-plane mode value + for status, backup manifests, and operational audit. 10. `internal/cloudcp/provider_msp_backup.go` shared with `deployment-installability`: provider-hosted MSP backup is both a cloud-paid license/account/runtime continuity boundary and a deployment-installability recovery artifact boundary. License-backed provider MSP backups must include the signed MSP license file as a recovery artifact while exposing only license metadata in command diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 1a33213f2..f8bf55a8d 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -175,6 +175,12 @@ TLS floor in the dynamic config. dry-run restore into a separate target data dir, dry-run failed-workspace recovery, remove proof workspaces when requested, and report final operational status. + The packaged provider MSP compose bundle defaults to + `CP_CONTROL_PLANE_MODE=provider_hosted_msp`, but must allow + `pulse_hosted_msp` as an operator override for Pulse-operated MSP stacks + without forking the deployment artifact. Both modes use the same + `provider-msp` command group, license-backed proof path, isolated client + runtime containers, and runtime URL shape `https://.${DOMAIN}/`. `deploy/provider-msp/run-install-proof.sh` is the compose-level operator wrapper for that rehearsal. It must validate the provider `.env` and compose config, require a reachable Docker daemon, optionally pull the pinned @@ -514,9 +520,9 @@ TLS floor in the dynamic config. rollback target, exact GA date, and exact v5 end-of-support date aligned across release notes, upgrade guidance, support policy, promotion records, and release-promotion resolver proof before workflow dispatch. For the - 2026-06-03 cutover candidate, that packet is + 2026-06-04 cutover candidate, that packet is `promoted_from_tag=v6.0.0-rc.6`, `rollback_version=v5.1.34`, - `ga_date=2026-06-03`, and `v5_eos_date=2026-09-01`. + `ga_date=2026-06-04`, and `v5_eos_date=2026-09-02`. That stable cut must also move the repo-root Docker compose default and `scripts/install-docker.sh` fallback from the final RC image tag to the stable `6.0.0` image tag in the same commit as `VERSION=6.0.0`. diff --git a/docs/releases/RELEASE_NOTES_v6.md b/docs/releases/RELEASE_NOTES_v6.md index f8249f34a..4a107404b 100644 --- a/docs/releases/RELEASE_NOTES_v6.md +++ b/docs/releases/RELEASE_NOTES_v6.md @@ -21,10 +21,10 @@ backend, the navigation shape you already know. ## Pulse v5 Support Transition -Pulse v5 entered maintenance-only support on `2026-06-03`. +Pulse v5 entered maintenance-only support on `2026-06-04`. I will ship only critical security, data-loss, licensing or billing blocker, -installer or updater failure, and safe migration blocker fixes for existing v5 users until `2026-09-01`. -After `2026-09-01`, Pulse v5 is end-of-support and new fixes land on v6 unless +installer or updater failure, and safe migration blocker fixes for existing v5 users until `2026-09-02`. +After `2026-09-02`, Pulse v5 is end-of-support and new fixes land on v6 unless I publish an explicit exception. ## What Is In v6.0.0 diff --git a/frontend-modern/public/docs/MIGRATION_UNIFIED_NAV.md b/frontend-modern/public/docs/MIGRATION_UNIFIED_NAV.md index 84cc2578a..e8cb6f055 100644 --- a/frontend-modern/public/docs/MIGRATION_UNIFIED_NAV.md +++ b/frontend-modern/public/docs/MIGRATION_UNIFIED_NAV.md @@ -2,7 +2,7 @@ > **This migration has been reverted as of `v6.0.0-rc.6`.** Pulse v6 ships > with the platform-shaped top-level navigation existing v5 operators -> already know (Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Standalone, +> already know (Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Machines, > plus Alerts, Patrol, and Settings). The unified > `/infrastructure` / `/workloads` / `/storage` / `/recovery` layout that > briefly shipped across `rc.1` through `rc.5` is retired. The unified @@ -21,7 +21,7 @@ > The keyboard shortcuts follow the platform-shaped shape: > > - `g p` Proxmox, `g d` Docker, `g k` Kubernetes, `g n` TrueNAS, -> `g v` vSphere, `g s` Standalone +> `g v` vSphere, `g s` Machines > - `g a` Alerts, `g r` Patrol, `g t` Settings, `?` shortcuts help > > The "Classic shortcuts" bar referenced in the historical content below diff --git a/frontend-modern/src/api/dockerMetadata.ts b/frontend-modern/src/api/dockerMetadata.ts new file mode 100644 index 000000000..9acf46ee4 --- /dev/null +++ b/frontend-modern/src/api/dockerMetadata.ts @@ -0,0 +1,26 @@ +import { buildMetadataAPI, type ResourceMetadataRecord } from './metadataClient'; + +export interface DockerMetadata extends ResourceMetadataRecord {} + +const dockerMetadataAPI = buildMetadataAPI('/api/docker/metadata'); + +export class DockerMetadataAPI { + static async getMetadata(resourceId: string): Promise { + return dockerMetadataAPI.getMetadata(resourceId); + } + + static async getAllMetadata(): Promise> { + return dockerMetadataAPI.getAllMetadata(); + } + + static async updateMetadata( + resourceId: string, + metadata: Partial, + ): Promise { + return dockerMetadataAPI.updateMetadata(resourceId, metadata); + } + + static async deleteMetadata(resourceId: string): Promise { + await dockerMetadataAPI.deleteMetadata(resourceId); + } +} diff --git a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx index 5e4220a69..d0cdace8d 100644 --- a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx +++ b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx @@ -2,6 +2,7 @@ import { Component, For, Show, createMemo } from 'solid-js'; import CheckIcon from 'lucide-solid/icons/check'; import CopyIcon from 'lucide-solid/icons/copy'; import ExternalLinkIcon from 'lucide-solid/icons/external-link'; +import TriangleAlertIcon from 'lucide-solid/icons/triangle-alert'; import type { ResourceType } from '../../types/discovery'; import { formatDiscoveryAge, @@ -97,6 +98,7 @@ export const DiscoveryTab: Component = (props) => { connectedAgents, copiedDiscoveryValue, discovery, + discoveryFeatureKnownDisabled, discoveryInfo, editingNotes, handleCopyDiscoveryValue, @@ -162,86 +164,164 @@ export const DiscoveryTab: Component = (props) => { // via CSS inheritance, so explanatory copy and command descriptions wrap. return (
- {/* Analysis provider badge - shown when a provider is configured */} - -
- - {/* Cloud icon */} + +
+
+ +
+

AI Discovery Disabled

+

+ Enable infrastructure discovery in Settings -> AI before using this tab. +

+
+
+
+
+ + + {/* Analysis provider badge - shown when a provider is configured */} + +
+ + {/* Cloud icon */} + + + + Analysis: {discoveryInfo()?.ai_provider?.label} + + } + > + + {/* Server/local icon */} Analysis: {discoveryInfo()?.ai_provider?.label} - } - > - - {/* Server/local icon */} - - - - Analysis: {discoveryInfo()?.ai_provider?.label} - - -
-
- - -
-
-
-
Discovery run
-

{manualRunSummary()}

-
- +
-
-
+
- {/* "What Discovery Does" explanation - shown when no discovery exists yet */} - -
-
-
+ +
+
+
+
Discovery run
+

{manualRunSummary()}

+
+ +
+
+
+ + {/* "What Discovery Does" explanation - shown when no discovery exists yet */} + +
+
+
+ + + +
+

What Discovery Does

+

{DISCOVERY_ANALYSIS_EXPLANATION}

+
+
+ +
+
+
+ + {/* Commands Preview - Expandable before first scan */} + 0 + } + > +
+ { + e.preventDefault(); + setShowCommandsPreview(!showCommandsPreview()); + }} + > = (props) => { stroke-linecap="round" stroke-linejoin="round" stroke-width="2" - d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" + d="M9 5l7 7-7 7" /> -
-

What Discovery Does

-

{DISCOVERY_ANALYSIS_EXPLANATION}

-
-
- -
-
-
- - {/* Commands Preview - Expandable before first scan */} - 0 - } - > -
- { - e.preventDefault(); - setShowCommandsPreview(!showCommandsPreview()); - }} - > - - - - Commands that will run ({discoveryInfo()?.commands?.length || 0}) - - -
- - {(cmd) => ( -
-
- - {cmd.command} - + Commands that will run ({discoveryInfo()?.commands?.length || 0}) + + +
+ + {(cmd) => ( +
+
+ + {cmd.command} + +
+

{cmd.description}

-

{cmd.description}

-
- )} - -
- -
-
+ )} + +
+
+ + - {/* Loading state - delayed to prevent flash for fast loads */} - -
-
- {getDiscoveryLoadingState().text} -
-
+ {/* Loading state - delayed to prevent flash for fast loads */} + +
+
+ {getDiscoveryLoadingState().text} +
+
- {/* Scan Progress Bar */} - -
-
-
-
- - {scanProgress()?.current_step || 'Scanning...'} + {/* Scan Progress Bar */} + +
+
+
+
+ + {scanProgress()?.current_step || 'Scanning...'} + +
+ + {Math.round(scanProgress()?.percent_complete || 0)}%
- - {Math.round(scanProgress()?.percent_complete || 0)}% - -
-
-
-
- -
- Running: {scanProgress()?.current_command} +
+
- - {/* Live elapsed time and hint */} -
- Elapsed: {liveElapsedSeconds()}s - - Analysis time varies by model. You can navigate away — results save automatically. - -
-
-
- - {/* Scanning state without WebSocket progress - show live timer */} - -
-
-
- - Running discovery... - -
-
- Elapsed: {liveElapsedSeconds()}s - - Analysis time varies by model. You can navigate away — results save automatically. - -
-
-
- - {/* Scan Success */} - -
-
- - - -

- Discovery complete! -

-
-
-
- - {/* Scan Error */} - -
-
- - - -
-

Discovery Failed

-

{scanError()}

+ +
+ Running: {scanProgress()?.current_command} +
+
+ {/* Live elapsed time and hint */} +
+ Elapsed: {liveElapsedSeconds()}s + + Analysis time varies by model. You can navigate away — results save automatically. +
-
+ + + {/* Scanning state without WebSocket progress - show live timer */} + +
+
+
+ + Running discovery... + +
+
+ Elapsed: {liveElapsedSeconds()}s + + Analysis time varies by model. You can navigate away — results save automatically. + +
+
+
+ + {/* Scan Success */} + +
+
+ - -
-
-
- - {/* No discovery yet - only show after initial fetch completes to prevent flash */} - -
-
- - - - -

{getDiscoveryInitialEmptyState(false).title}

-

- {getDiscoveryInitialEmptyState(false).description} -

- - } - > -

{getDiscoveryInitialEmptyState(true).title}

-

- {getDiscoveryInitialEmptyState(true).description} +

+ Discovery complete!

-
+
+
- {/* Connection Status Warning - Show when commands are needed but not available */} - - -
-
- - - -
-

- Commands not enabled -

-

- Discovery requires command execution. Enable Pulse commands from{' '} - - {commandSettingsTarget.label} - - . -

-
-
+ {/* Scan Error */} + +
+
+ + + +
+

Discovery Failed

+

{scanError()}

- - -
-
- - - -
-

- Agent not connected for commands -

-

- Commands are enabled, but the agent isn't connected via WebSocket. Check that - the API token has the{' '} - - agent:exec - {' '} - scope in{' '} - - {apiAccessSettingsTarget.label} - - . -

-
-
-
-
- -
-
- - - -

- Agent connected and ready for command execution -

-
-
-
- - - - - -
- - - {/* Discovery exists but has no meaningful data - show re-scan option */} - -
-
- - - -

Unknown Service

-

- Discovery completed but couldn't identify a known service. -

- -

- Last scanned: {formatDiscoveryAge(discovery()!.updated_at)} -

-
+ +
- - - -
-
+ - {/* Discovery data with valid results */} - - {(d) => ( -
- {/* Service Header */} -
-
-
-
-

- {d().service_name || 'Unknown Service'} -

- -
- -
- - Version {d().service_version} - -
-
-
- - - {getCategoryDisplayName(d().category)} - - -
- - -

- {confidenceInfo()!.label} ({Math.round((d().confidence || 0) * 100)}%) + {/* No discovery yet - only show after initial fetch completes to prevent flash */} + +

+
+ + + + +

{getDiscoveryInitialEmptyState(false).title}

+

+ {getDiscoveryInitialEmptyState(false).description} +

+ + } + > +

{getDiscoveryInitialEmptyState(true).title}

+

+ {getDiscoveryInitialEmptyState(true).description}

-
- - - - Last observed {formatDiscoveryAge(d().updated_at)} - - - - Available to Pulse Assistant - -
- -
-
- Web Interface Suggestion - -
- -

{getDiscoverySuggestedURLFallback().title}

-

- {getDiscoverySuggestedURLFallback(d().suggested_url_diagnostic).description} + {/* Connection Status Warning - Show when commands are needed but not available */} + + +

+
+ + + +
+

+ Commands not enabled +

+

+ Discovery requires command execution. Enable Pulse commands from{' '} + + {commandSettingsTarget.label} + + .

- } - > - -

- Why this URL: {suggestedURLReasonText()} -

-
-
- {d().suggested_url} - - - -
-

- Save it in the Web Interface URL field before Pulse uses it as a link. +

+ + +
+
+ + + +
+

+ Agent not connected for commands +

+

+ Commands are enabled, but the agent isn't connected via WebSocket. Check + that the API token has the{' '} + + agent:exec + {' '} + scope in{' '} + + {apiAccessSettingsTarget.label} + + . +

+
+
+
+
+ +
+
+ + + +

+ Agent connected and ready for command execution +

+
+
+
+ + + + + +
+
+ + {/* Discovery exists but has no meaningful data - show re-scan option */} + +
+
+ + + +

Unknown Service

+

+ Discovery completed but couldn't identify a known service. +

+ +

+ Last scanned: {formatDiscoveryAge(discovery()!.updated_at)} +

+
+
+ + + +
+
+ + {/* Discovery data with valid results */} + + {(d) => ( +
+ {/* Service Header */} +
+
+
+
+

+ {d().service_name || 'Unknown Service'} +

+ +
+ +
+ + Version {d().service_version} + +
+
+
+ + + {getCategoryDisplayName(d().category)} + + +
+ + +

+ {confidenceInfo()!.label} ({Math.round((d().confidence || 0) * 100)}%)

-
- - - {/* CLI Access */} - -
-
- CLI Access - -
- -
-
- - {/* Configuration, Data & Log Paths */} - 0 || - d().data_paths?.length > 0 || - d().log_paths?.length > 0 - } - > -
- 0}> -
-
- Config Paths - -
-
- - {(path) => ( - - )} - -
-
-
- 0}> -
-
- Data Paths - -
-
- - {(path) => ( - - )} - -
-
-
- 0}> -
-
- Log Paths - -
-
- - {(path) => ( - - )} - -
-
-
-
-
- - {/* Ports */} - 0}> -
-
- Listening Ports - -
-
- - {(port) => ( - - )} - +
+ + + + Last observed {formatDiscoveryAge(d().updated_at)} + + + + Available to Pulse Assistant +
- - {/* Key Facts */} - 0}> -
-
- Discovered Facts - -
-
- - {(fact) => ( -
- {fact.key} -
- - {fact.value} - - -
-
- )} -
-
-
-
- - {/* User Notes */} -
-
-
- Your Notes -
- -
+ + + {/* CLI Access */} + +
+
+ CLI Access + +
+ +
+
+ + {/* Configuration, Data & Log Paths */} + 0 || + d().data_paths?.length > 0 || + d().log_paths?.length > 0 + } + > +
+ 0}> +
+
+ Config Paths + +
+
+ + {(path) => ( + + )} + +
+
+
+ 0}> +
+
+ Data Paths + +
+
+ + {(path) => ( + + )} + +
+
+
+ 0}> +
+
+ Log Paths + +
+
+ + {(path) => ( + + )} + +
+
+
+
+
+ + {/* Ports */} + 0}> +
+
+ Listening Ports + +
+
+ + {(port) => ( + + )} + +
+
+
+ + {/* Key Facts */} + 0}> +
+
+ Discovered Facts + +
+
+ + {(fact) => ( +
+ {fact.key} +
+ + {fact.value} + + +
+
+ )} +
+
+
+
+ + {/* User Notes */} +
+
+
+ Your Notes +
+ + + +
+ + + {getDiscoveryNotesEmptyState().text} +

+ } + > +

{d().user_notes}

+
+ } + > +
+