mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Use agent update target for stale notices
This commit is contained in:
parent
259476907a
commit
17e854e7b6
28 changed files with 264 additions and 60 deletions
|
|
@ -180,14 +180,18 @@ cannot prove agent-token continuity.
|
|||
Stale-agent platform notices and the Infrastructure `agentUpdates` workspace
|
||||
route are part of that same lifecycle contract. Platform pages may link to the
|
||||
central Infrastructure update dialog with a scoped list of affected agent
|
||||
connection IDs, but command rendering stays lifecycle-owned in Settings. For
|
||||
Unix-like agents that Pulse already sees, the copied stale-agent update command
|
||||
must use `scripts/install.sh --update` and recover URL, token, identity, custom
|
||||
CA, and insecure transport from installer-owned saved state instead of asking
|
||||
the operator to mint a fresh install token or exposing agent IDs in the copied
|
||||
command. Windows stale-agent update commands remain on the existing
|
||||
token-gated install transport until the Windows installer owns an equivalent
|
||||
saved-state update mode.
|
||||
connection IDs, but command rendering stays lifecycle-owned in Settings.
|
||||
`currentAgentTargetVersion()` is the canonical agent update target and must be
|
||||
projected to browser clients as `/api/version.agentUpdateTargetVersion`;
|
||||
platform pages and Settings must not compare agent binaries against the app
|
||||
build `version` when the server would tell agents `dev`. For Unix-like agents
|
||||
that Pulse already sees, the copied stale-agent update command must use
|
||||
`scripts/install.sh --update` and recover URL, token, identity, custom CA, and
|
||||
insecure transport from installer-owned saved state instead of asking the
|
||||
operator to mint a fresh install token or exposing agent IDs in the copied
|
||||
command. Windows stale-agent update commands remain on the existing token-gated
|
||||
install transport until the Windows installer owns an equivalent saved-state
|
||||
update mode.
|
||||
|
||||
Agent lifecycle and fleet-operation surfaces may consume
|
||||
`POST /api/actions/plan` for resource capability planning, but the action plan
|
||||
|
|
|
|||
|
|
@ -120,8 +120,12 @@ update commands opened from platform notices must not serialize API tokens,
|
|||
agent IDs, or hostnames into Unix shell payloads. Those commands route through
|
||||
the `agentUpdates` settings query for scoped row selection and then call the
|
||||
installer-owned `scripts/install.sh --update` saved-state path on the target
|
||||
host. Windows remains on the existing token-gated PowerShell payload until its
|
||||
installer owns the same saved-state update contract.
|
||||
host. Stale-agent UI must consume `/api/version`'s
|
||||
`agentUpdateTargetVersion`, not the app build `version`, so development/source
|
||||
builds that intentionally report `dev` to agents do not surface update nags
|
||||
when no real agent update target exists. Windows remains on the existing
|
||||
token-gated PowerShell payload until its installer owns the same saved-state
|
||||
update contract.
|
||||
|
||||
Summary-chart response caching is a shared API boundary:
|
||||
`internal/api/router.go` may serve a short cached JSON payload for repeated
|
||||
|
|
|
|||
|
|
@ -127,6 +127,11 @@ TLS floor in the dynamic config.
|
|||
## Shared Boundaries
|
||||
|
||||
1. `frontend-modern/src/api/updates.ts` shared with `api-contracts`: the updates frontend client is both a deployment-installability control surface and a canonical API payload contract boundary.
|
||||
The version payload consumed by this client must preserve the distinction
|
||||
between the running app build `version` and the deployable
|
||||
`agentUpdateTargetVersion`; update/install surfaces may display the app
|
||||
build, but agent update prompts must only use the agent target when the
|
||||
backend exposes one.
|
||||
2. `internal/api/updates.go` shared with `api-contracts`: update handlers are both a deployment-installability control surface and a canonical API payload contract boundary.
|
||||
3. `internal/cloudcp/docker/labels.go` shared with `cloud-paid`: hosted tenant Docker labels are both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary.
|
||||
4. `internal/cloudcp/docker/manager.go` shared with `cloud-paid`: hosted tenant container management is both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary.
|
||||
|
|
|
|||
|
|
@ -233,9 +233,12 @@ surface a compact stale-agent cue when their row model carries Pulse agent
|
|||
identity and version evidence, but the CTA must route to the canonical
|
||||
Infrastructure settings update-command surface with scoped agent IDs instead of
|
||||
duplicating installer command assembly, tokens, or lifecycle copy in each
|
||||
platform page. Agentless API-only platforms such as vSphere must not grow this
|
||||
notice unless a concrete guest or monitored system row actually carries a Pulse
|
||||
agent identity.
|
||||
platform page. These notices must compare against the API-owned
|
||||
`agentUpdateTargetVersion` rather than the app build version, so development
|
||||
builds can show their dirty server version without implying agents can or
|
||||
should update toward that build. Agentless API-only platforms such as vSphere
|
||||
must not grow this notice unless a concrete guest or monitored system row
|
||||
actually carries a Pulse agent identity.
|
||||
Kubernetes policy inventory follows that same primitive boundary while the
|
||||
unified-resource owner supplies NetworkPolicy policy type and rule-count
|
||||
columns, PodDisruptionBudget budget and observed health columns, ResourceQuota
|
||||
|
|
|
|||
|
|
@ -141,7 +141,6 @@ regression protection.
|
|||
15. `frontend-modern/src/routing/routePreload.ts` shared with `frontend-primitives`: the app-shell route preload registry is both a canonical frontend shell boundary and an authenticated hot-path performance boundary.
|
||||
16. `frontend-modern/src/useAppRuntimeState.ts` shared with `cloud-paid`: the authenticated app runtime bootstrap is both a hosted commercial org-context boundary and a protected app-shell performance boundary.
|
||||
17. `internal/api/slo.go` shared with `api-contracts`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary.
|
||||
|
||||
## Extension Points
|
||||
|
||||
1. Add performance budgets through SLO or contract tests
|
||||
|
|
@ -177,6 +176,10 @@ regression protection.
|
|||
process version once, but they must not add per-request release lookups,
|
||||
filesystem walks, or other heavy work just to compute whether an attached
|
||||
agent is current.
|
||||
The `/api/version.agentUpdateTargetVersion` projection follows that same
|
||||
rule: it may mirror the cached deployable agent target exposed to agents,
|
||||
but it must not inspect attached agents, download artifacts, or resolve
|
||||
release metadata on each request.
|
||||
Update-readiness planning follows the same bounded-router rule. The router
|
||||
may wire cached config and monitor host snapshots into the updates handler,
|
||||
but it must not add release lookups, broad persistence scans, or live agent
|
||||
|
|
|
|||
|
|
@ -121,13 +121,17 @@ controls as normal product settings.
|
|||
storing a brand setting never becomes a free branding bypass.
|
||||
16. `internal/cloudcp/auth/magiclink.go` shared with `cloud-paid`: control-plane magic-link HMAC handling is both a Pulse Cloud account-access boundary and a security/privacy token-secrecy boundary.
|
||||
17. `internal/cloudcp/auth/magiclink_store.go` shared with `cloud-paid`: control-plane magic-link persistence is both a Pulse Cloud account-access boundary and a security/privacy storage-hardening boundary.
|
||||
|
||||
## Extension Points
|
||||
|
||||
1. Change privacy disclosures, usage-data vocabulary, or outbound-data guarantees through `docs/PRIVACY.md` and `internal/telemetry/telemetry.go` together.
|
||||
2. Change security policy, hardening guidance, or supported auth boundaries through `SECURITY.md`.
|
||||
3. Change telemetry/privacy settings state handling through `frontend-modern/src/components/Settings/useSystemSettingsState.ts`.
|
||||
4. Change security/auth/token transport behavior through the shared `frontend-modern/src/api/security.ts`, `frontend-modern/src/components/Settings/APITokenManager.tsx`, `frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, `frontend-modern/src/components/Settings/useAPITokenManagerState.ts`, `internal/api/security.go`, `internal/api/security_tokens.go`, and `internal/api/system_settings.go` boundary.
|
||||
Release metadata surfaced through `/api/version` remains outside token,
|
||||
auth, and privacy state. Adding or changing `agentUpdateTargetVersion`
|
||||
must stay limited to non-secret deployable release identity and must not
|
||||
expose agent inventory, scoped update selections, or command authorization
|
||||
state.
|
||||
CSRF token-store behavior in `internal/api/csrf_store.go` is part of that
|
||||
shared browser-auth trust boundary: parallel stale-token mutations may
|
||||
receive distinct bounded replacement tokens for one session, but explicit
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ plumbing even though `ProxmoxPageSurface` is a storage/recovery canonical file.
|
|||
Those notices may link an operator to scoped agent update commands for
|
||||
agent-contributed node detail and command support, but they must not be
|
||||
interpreted as backup visibility, recovery readiness, restore capability, or a
|
||||
storage/recovery-owned command path.
|
||||
storage/recovery-owned command path. Suppressing those notices when the API has
|
||||
no deployable agent update target is likewise lifecycle/frontend behavior and
|
||||
does not change Proxmox backup or recovery evidence.
|
||||
Tenant report branding settings are adjacent tenant-local configuration, not a
|
||||
storage or recovery product state. `reportBranding` persisted in a tenant
|
||||
runtime's `system.json` should be preserved by the existing tenant data
|
||||
|
|
|
|||
|
|
@ -488,6 +488,9 @@ AI-only summary payloads, or page-local heuristics.
|
|||
route to the affected agents. That scoped lifecycle handoff must not become a
|
||||
new resource-action authority, a page-local command runner, or a substitute
|
||||
for the `/api/connections` fleet command-policy truth described above.
|
||||
Resource consumers must also use the API-owned agent update target when
|
||||
comparing resource-carried agent versions; the running app build version is
|
||||
not a resource freshness contract.
|
||||
`internal/unifiedresources/top_level_systems.go`
|
||||
Explicit linked-host correlation is canonical here: when Kubernetes node
|
||||
ingest has a resolved backing host agent, the registry must merge that node
|
||||
|
|
|
|||
|
|
@ -15,6 +15,23 @@ describe('UpdatesAPI', () => {
|
|||
apiFetchJSONMock.mockReset();
|
||||
});
|
||||
|
||||
it('preserves the app version and agent update target from version payloads', async () => {
|
||||
const response = {
|
||||
version: '6.0.0-rc.6+git.174.g259476907.dirty',
|
||||
build: 'development',
|
||||
runtime: 'go',
|
||||
isDocker: false,
|
||||
isSourceBuild: false,
|
||||
isDevelopment: true,
|
||||
deploymentType: 'development',
|
||||
agentUpdateTargetVersion: '6.0.0-rc.6',
|
||||
};
|
||||
apiFetchJSONMock.mockResolvedValueOnce(response as any);
|
||||
|
||||
await expect(UpdatesAPI.getVersion()).resolves.toEqual(response);
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/version');
|
||||
});
|
||||
|
||||
it('encodes optional update-check channel safely', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({ available: false } as any);
|
||||
await UpdatesAPI.checkForUpdates('rc');
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export interface VersionInfo {
|
|||
isSourceBuild: boolean;
|
||||
isDevelopment: boolean;
|
||||
deploymentType?: string;
|
||||
agentUpdateTargetVersion?: string;
|
||||
}
|
||||
|
||||
export type UpdateReadinessStatus = 'ready' | 'attention' | 'blocked';
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
const agentUpdateTargets = createMemo(() =>
|
||||
collectInfrastructureAgentUpdateTargets(
|
||||
rows(),
|
||||
updateStore.versionInfo()?.version,
|
||||
updateStore.versionInfo()?.agentUpdateTargetVersion,
|
||||
agentUpdateScope(),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -435,6 +435,62 @@ describe('InfrastructureWorkspace', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not invent scoped update commands when the backend publishes no agent target', async () => {
|
||||
routeState.search = '?agentUpdates=1&agents=agent%3Aagent-zeus';
|
||||
const primaryConnection = connectionFixture();
|
||||
const attachedAgent = connectionFixture({
|
||||
id: 'agent:agent-zeus',
|
||||
type: 'agent',
|
||||
name: 'zeus-agent',
|
||||
address: 'zeus.lab',
|
||||
surfaces: ['host'],
|
||||
scope: { host: true },
|
||||
source: 'agent',
|
||||
agentVersion: '5.1.34',
|
||||
agentUpdateAvailable: false,
|
||||
agentIdentity: {
|
||||
hostname: 'zeus',
|
||||
platform: 'linux',
|
||||
},
|
||||
capabilities: { supportsPause: false, supportsScope: false, supportsTest: false },
|
||||
});
|
||||
connectionState.connections = [primaryConnection, attachedAgent];
|
||||
connectionState.rows = [
|
||||
{
|
||||
id: primaryConnection.id,
|
||||
ownerType: 'pve',
|
||||
name: 'homelab',
|
||||
subtitle: 'via platform API and Pulse Agent',
|
||||
source: 'both',
|
||||
host: primaryConnection.address,
|
||||
coverageLabels: ['VMs', 'Host telemetry'],
|
||||
statusLabel: 'Active',
|
||||
statusClassName: 'bg-green-100 text-green-800',
|
||||
agentUpdateCount: 0,
|
||||
lastActivityText: '1m ago',
|
||||
...emptyFleetRow,
|
||||
enabled: true,
|
||||
canEdit: true,
|
||||
canPause: true,
|
||||
canRemove: true,
|
||||
isAgent: false,
|
||||
isCluster: false,
|
||||
attachedConnections: [attachedAgent],
|
||||
members: [],
|
||||
connection: primaryConnection,
|
||||
},
|
||||
];
|
||||
|
||||
renderWorkspace();
|
||||
|
||||
const dialog = await screen.findByRole('dialog');
|
||||
expect(
|
||||
within(dialog).getByText('Pulse does not currently see any agents behind the target version.'),
|
||||
).toBeInTheDocument();
|
||||
expect(within(dialog).queryByText('zeus')).not.toBeInTheDocument();
|
||||
expect(within(dialog).queryByText(/upgrade agent:agent-zeus/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps source groups in the catalog order instead of count order', async () => {
|
||||
const pveConnection = connectionFixture({
|
||||
id: 'pve:zeus',
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ describe('infrastructure agent update commands model', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('falls back to the app build version when the connections ledger has no target version', () => {
|
||||
it('uses the explicit agent update target when the connection has no target version', () => {
|
||||
const staleAgent = connection({
|
||||
id: 'agent:agent-delly',
|
||||
type: 'agent',
|
||||
|
|
@ -144,7 +144,7 @@ describe('infrastructure agent update commands model', () => {
|
|||
attachedConnections: [staleAgent],
|
||||
}),
|
||||
],
|
||||
'6.0.0-rc.6+git.172.g2c360f779.dirty',
|
||||
'v6.0.0-rc.6',
|
||||
);
|
||||
|
||||
expect(targets).toHaveLength(1);
|
||||
|
|
@ -155,6 +155,29 @@ describe('infrastructure agent update commands model', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not infer stale targets without an agent update target', () => {
|
||||
const staleAgent = connection({
|
||||
id: 'agent:agent-delly',
|
||||
type: 'agent',
|
||||
name: 'delly',
|
||||
address: 'delly',
|
||||
surfaces: ['host'],
|
||||
scope: { host: true },
|
||||
source: 'agent',
|
||||
agentVersion: 'v6.0.0-rc.5',
|
||||
agentUpdateAvailable: false,
|
||||
capabilities: { supportsPause: false, supportsScope: false, supportsTest: false },
|
||||
});
|
||||
|
||||
const targets = collectInfrastructureAgentUpdateTargets([
|
||||
row({
|
||||
attachedConnections: [staleAgent],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(targets).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters to scoped agent IDs when a platform notice links to specific hosts', () => {
|
||||
const staleDelly = connection({
|
||||
id: 'agent:agent-delly',
|
||||
|
|
|
|||
|
|
@ -64,14 +64,17 @@ export function DockerPageSurface() {
|
|||
const activeTab = createMemo<DockerPageTabId>(() =>
|
||||
tabs().some((tab) => tab.id === requestedTab()) ? requestedTab() : 'overview',
|
||||
);
|
||||
const agentUpdateTargetVersion = createMemo(
|
||||
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
|
||||
);
|
||||
const outdatedAgentHosts = createMemo(() =>
|
||||
collectOutdatedAgentHosts(model().hosts, updateStore.versionInfo()?.version),
|
||||
collectOutdatedAgentHosts(model().hosts, agentUpdateTargetVersion()),
|
||||
);
|
||||
const outdatedAgentUpdatePath = createMemo(() =>
|
||||
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
|
||||
);
|
||||
const serverVersionDisplay = createMemo(() =>
|
||||
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
|
||||
formatAgentVersionDisplay(agentUpdateTargetVersion()),
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -69,7 +69,10 @@ vi.mock('@/hooks/useUnifiedResources', () => ({
|
|||
|
||||
vi.mock('@/stores/updates', () => ({
|
||||
updateStore: {
|
||||
versionInfo: () => ({ version: '6.0.0-rc.6' }),
|
||||
versionInfo: () => ({
|
||||
version: '6.0.0-rc.6',
|
||||
agentUpdateTargetVersion: '6.0.0-rc.6',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -69,14 +69,17 @@ export function KubernetesPageSurface() {
|
|||
tabs().some((tab) => tab.id === requestedTab()) ? requestedTab() : 'overview',
|
||||
);
|
||||
const controllerResources = createMemo(() => getKubernetesControllerResources(model()));
|
||||
const agentUpdateTargetVersion = createMemo(
|
||||
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
|
||||
);
|
||||
const outdatedAgentHosts = createMemo(() =>
|
||||
collectOutdatedAgentHosts(model().nodes, updateStore.versionInfo()?.version),
|
||||
collectOutdatedAgentHosts(model().nodes, agentUpdateTargetVersion()),
|
||||
);
|
||||
const outdatedAgentUpdatePath = createMemo(() =>
|
||||
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
|
||||
);
|
||||
const serverVersionDisplay = createMemo(() =>
|
||||
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
|
||||
formatAgentVersionDisplay(agentUpdateTargetVersion()),
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -215,7 +215,10 @@ describe('KubernetesPageSurface contract', () => {
|
|||
});
|
||||
|
||||
it('surfaces stale agent-backed Kubernetes nodes', () => {
|
||||
mockVersionInfo.mockReturnValue({ version: 'v6.0.0-rc.6' });
|
||||
mockVersionInfo.mockReturnValue({
|
||||
version: 'v6.0.0-rc.6',
|
||||
agentUpdateTargetVersion: 'v6.0.0-rc.6',
|
||||
});
|
||||
setResources([
|
||||
makeResource({
|
||||
id: 'agent:k8s-node-1',
|
||||
|
|
|
|||
|
|
@ -85,14 +85,17 @@ export function ProxmoxPageSurface() {
|
|||
if (!segment || !VALID_TABS.has(segment)) return 'overview';
|
||||
return visibleTabIds().has(segment) ? segment : 'overview';
|
||||
});
|
||||
const agentUpdateTargetVersion = createMemo(
|
||||
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
|
||||
);
|
||||
const outdatedAgentHosts = createMemo(() =>
|
||||
collectOutdatedAgentHosts(model().pveNodes, updateStore.versionInfo()?.version),
|
||||
collectOutdatedAgentHosts(model().pveNodes, agentUpdateTargetVersion()),
|
||||
);
|
||||
const outdatedAgentUpdatePath = createMemo(() =>
|
||||
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
|
||||
);
|
||||
const serverVersionDisplay = createMemo(() =>
|
||||
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
|
||||
formatAgentVersionDisplay(agentUpdateTargetVersion()),
|
||||
);
|
||||
|
||||
// The hosts table at the top and the embedded WorkloadsSurface below share
|
||||
|
|
|
|||
|
|
@ -123,7 +123,10 @@ describe('ProxmoxPageSurface contract', () => {
|
|||
});
|
||||
|
||||
it('surfaces stale agent-backed Proxmox nodes', () => {
|
||||
mockVersionInfo.mockReturnValue({ version: 'v6.0.0-rc.6' });
|
||||
mockVersionInfo.mockReturnValue({
|
||||
version: 'v6.0.0-rc.6',
|
||||
agentUpdateTargetVersion: 'v6.0.0-rc.6',
|
||||
});
|
||||
setResources([
|
||||
makeResource({
|
||||
id: 'agent:delly',
|
||||
|
|
@ -149,4 +152,25 @@ describe('ProxmoxPageSurface contract', () => {
|
|||
'/settings/infrastructure?agentUpdates=1&agents=agent%3Aagent-delly',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not surface stale-agent notices for development builds without an agent target', () => {
|
||||
mockVersionInfo.mockReturnValue({
|
||||
version: '6.0.0-rc.6+git.172.g2c360f779.dirty',
|
||||
isDevelopment: true,
|
||||
});
|
||||
setResources([
|
||||
makeResource({
|
||||
id: 'agent:delly',
|
||||
name: 'delly',
|
||||
displayName: 'delly',
|
||||
type: 'agent',
|
||||
proxmox: { nodeName: 'delly', clusterName: 'homelab' },
|
||||
agent: { agentId: 'agent-delly', agentVersion: 'v5.1.34' },
|
||||
}),
|
||||
]);
|
||||
|
||||
render(() => <ProxmoxPageSurface />);
|
||||
|
||||
expect(screen.queryByTestId('platform-outdated-agent-notice')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ describe('proxmoxBackupRecoveryModel', () => {
|
|||
|
||||
expect(model.coverageRows).toHaveLength(1);
|
||||
expect(model.coverageRows[0].posture).toBe('uncovered');
|
||||
expect(model.coverageSummary.uncovered).toBe(1);
|
||||
expect(model.coverageRows[0].pbsCount).toBe(0);
|
||||
expect(model.coverageRows[0].archiveCount).toBe(0);
|
||||
expect(model.coverageRows[0].snapshotCount).toBe(0);
|
||||
|
|
|
|||
|
|
@ -69,14 +69,17 @@ export function StandalonePageSurface() {
|
|||
model().machines.length === 0 &&
|
||||
model().availabilityChecks.length === 0,
|
||||
);
|
||||
const agentUpdateTargetVersion = createMemo(
|
||||
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
|
||||
);
|
||||
const outdatedAgentHosts = createMemo(() =>
|
||||
collectOutdatedAgentHosts(model().machines, updateStore.versionInfo()?.version),
|
||||
collectOutdatedAgentHosts(model().machines, agentUpdateTargetVersion()),
|
||||
);
|
||||
const outdatedAgentUpdatePath = createMemo(() =>
|
||||
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
|
||||
);
|
||||
const serverVersionDisplay = createMemo(() =>
|
||||
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
|
||||
formatAgentVersionDisplay(agentUpdateTargetVersion()),
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -200,7 +203,7 @@ export function StandalonePageSurface() {
|
|||
emptyIcon={machineIcon()}
|
||||
emptyTitle="No machines"
|
||||
emptyDescription="Install Pulse Agent on Linux, macOS, Windows, or Unraid systems for full CPU, memory, disk, and network telemetry."
|
||||
targetAgentVersion={updateStore.versionInfo()?.version}
|
||||
targetAgentVersion={agentUpdateTargetVersion()}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -137,7 +137,10 @@ describe('StandalonePageSurface', () => {
|
|||
});
|
||||
|
||||
it('surfaces stale Pulse Agent binaries on the Machines page', () => {
|
||||
mocks.versionInfo.mockReturnValue({ version: 'v6.0.0-rc.6' });
|
||||
mocks.versionInfo.mockReturnValue({
|
||||
version: 'v6.0.0-rc.6',
|
||||
agentUpdateTargetVersion: 'v6.0.0-rc.6',
|
||||
});
|
||||
mocks.useUnifiedResources.mockReturnValue({
|
||||
resources: () => [
|
||||
resource({
|
||||
|
|
|
|||
|
|
@ -77,14 +77,17 @@ export function TrueNASPageSurface() {
|
|||
const activeTab = createMemo<TrueNASPageTabId>(() =>
|
||||
tabs().some((tab) => tab.id === requestedTab()) ? requestedTab() : 'overview',
|
||||
);
|
||||
const agentUpdateTargetVersion = createMemo(
|
||||
() => updateStore.versionInfo()?.agentUpdateTargetVersion,
|
||||
);
|
||||
const outdatedAgentHosts = createMemo(() =>
|
||||
collectOutdatedAgentHosts(model().systems, updateStore.versionInfo()?.version),
|
||||
collectOutdatedAgentHosts(model().systems, agentUpdateTargetVersion()),
|
||||
);
|
||||
const outdatedAgentUpdatePath = createMemo(() =>
|
||||
buildInfrastructureAgentUpdatesPath(outdatedAgentHosts().map((host) => host.agentId)),
|
||||
);
|
||||
const serverVersionDisplay = createMemo(() =>
|
||||
formatAgentVersionDisplay(updateStore.versionInfo()?.version),
|
||||
formatAgentVersionDisplay(agentUpdateTargetVersion()),
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -134,7 +134,10 @@ describe('TrueNASPageSurface contract', () => {
|
|||
});
|
||||
|
||||
it('surfaces stale agent-backed TrueNAS systems', () => {
|
||||
mockVersionInfo.mockReturnValue({ version: 'v6.0.0-rc.6' });
|
||||
mockVersionInfo.mockReturnValue({
|
||||
version: 'v6.0.0-rc.6',
|
||||
agentUpdateTargetVersion: 'v6.0.0-rc.6',
|
||||
});
|
||||
setResources([
|
||||
makeResource({
|
||||
id: 'agent:truenas-scale',
|
||||
|
|
|
|||
|
|
@ -5275,16 +5275,17 @@ func (r *Router) handleVersion(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
// Convert to typed response
|
||||
response := VersionResponse{
|
||||
Version: versionInfo.Version,
|
||||
BuildTime: versionInfo.Build,
|
||||
Build: versionInfo.Build,
|
||||
GoVersion: runtime.Version(),
|
||||
Runtime: versionInfo.Runtime,
|
||||
Channel: versionInfo.Channel,
|
||||
IsDocker: versionInfo.IsDocker,
|
||||
IsSourceBuild: versionInfo.IsSourceBuild,
|
||||
IsDevelopment: versionInfo.IsDevelopment,
|
||||
DeploymentType: versionInfo.DeploymentType,
|
||||
Version: versionInfo.Version,
|
||||
BuildTime: versionInfo.Build,
|
||||
Build: versionInfo.Build,
|
||||
GoVersion: runtime.Version(),
|
||||
Runtime: versionInfo.Runtime,
|
||||
Channel: versionInfo.Channel,
|
||||
IsDocker: versionInfo.IsDocker,
|
||||
IsSourceBuild: versionInfo.IsSourceBuild,
|
||||
IsDevelopment: versionInfo.IsDevelopment,
|
||||
DeploymentType: versionInfo.DeploymentType,
|
||||
AgentUpdateTargetVersion: currentAgentTargetVersion(),
|
||||
}
|
||||
|
||||
// Detect containerization (LXC/Docker)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,31 @@ func TestHandleVersion_Success(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleVersion_IncludesReleaseAgentUpdateTarget(t *testing.T) {
|
||||
oldBuildVersion := updates.BuildVersion
|
||||
updates.BuildVersion = "6.0.0-rc.6"
|
||||
t.Cleanup(func() {
|
||||
updates.BuildVersion = oldBuildVersion
|
||||
})
|
||||
|
||||
router := &Router{updateManager: updates.NewManager(&config.Config{})}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/version", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.handleVersion(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
var payload VersionResponse
|
||||
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if payload.AgentUpdateTargetVersion != "6.0.0-rc.6" {
|
||||
t.Fatalf("AgentUpdateTargetVersion = %q, want 6.0.0-rc.6", payload.AgentUpdateTargetVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMetricsHistory_MethodNotAllowed(t *testing.T) {
|
||||
router := &Router{}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/metrics/history", nil)
|
||||
|
|
|
|||
|
|
@ -25,20 +25,21 @@ func (r HealthResponse) NormalizeCollections() HealthResponse {
|
|||
|
||||
// VersionResponse represents version information
|
||||
type VersionResponse struct {
|
||||
Version string `json:"version"`
|
||||
BuildTime string `json:"buildTime,omitempty"`
|
||||
Build string `json:"build,omitempty"`
|
||||
GoVersion string `json:"goVersion,omitempty"`
|
||||
Runtime string `json:"runtime,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
IsDocker bool `json:"isDocker"`
|
||||
IsSourceBuild bool `json:"isSourceBuild"`
|
||||
IsDevelopment bool `json:"isDevelopment"`
|
||||
DeploymentType string `json:"deploymentType,omitempty"`
|
||||
UpdateAvailable bool `json:"updateAvailable"`
|
||||
LatestVersion string `json:"latestVersion,omitempty"`
|
||||
Containerized bool `json:"containerized"`
|
||||
ContainerID string `json:"containerId,omitempty"`
|
||||
Version string `json:"version"`
|
||||
BuildTime string `json:"buildTime,omitempty"`
|
||||
Build string `json:"build,omitempty"`
|
||||
GoVersion string `json:"goVersion,omitempty"`
|
||||
Runtime string `json:"runtime,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
IsDocker bool `json:"isDocker"`
|
||||
IsSourceBuild bool `json:"isSourceBuild"`
|
||||
IsDevelopment bool `json:"isDevelopment"`
|
||||
DeploymentType string `json:"deploymentType,omitempty"`
|
||||
AgentUpdateTargetVersion string `json:"agentUpdateTargetVersion,omitempty"`
|
||||
UpdateAvailable bool `json:"updateAvailable"`
|
||||
LatestVersion string `json:"latestVersion,omitempty"`
|
||||
Containerized bool `json:"containerized"`
|
||||
ContainerID string `json:"containerId,omitempty"`
|
||||
}
|
||||
|
||||
// ChartResponse represents chart data
|
||||
|
|
|
|||
|
|
@ -2860,7 +2860,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
{
|
||||
"heading": "## Shared Boundaries",
|
||||
"path": "internal/api/access_control_handlers.go",
|
||||
"line": 360,
|
||||
"line": 364,
|
||||
"heading_line": 113,
|
||||
}
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue