Use agent update target for stale notices

This commit is contained in:
rcourtman 2026-06-03 14:42:43 +01:00
parent 259476907a
commit 17e854e7b6
28 changed files with 264 additions and 60 deletions

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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');

View file

@ -30,6 +30,7 @@ export interface VersionInfo {
isSourceBuild: boolean;
isDevelopment: boolean;
deploymentType?: string;
agentUpdateTargetVersion?: string;
}
export type UpdateReadinessStatus = 'ready' | 'attention' | 'blocked';

View file

@ -195,7 +195,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
const agentUpdateTargets = createMemo(() =>
collectInfrastructureAgentUpdateTargets(
rows(),
updateStore.versionInfo()?.version,
updateStore.versionInfo()?.agentUpdateTargetVersion,
agentUpdateScope(),
),
);

View file

@ -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',

View file

@ -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',

View file

@ -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 (

View file

@ -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',
}),
},
}));

View file

@ -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 (

View file

@ -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',

View file

@ -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

View file

@ -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();
});
});

View file

@ -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);

View file

@ -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>

View file

@ -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({

View file

@ -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 (

View file

@ -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',

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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,
}
],