mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Use source-filtered facets for Proxmox tab evidence
Prevent unrelated estate-wide VM and storage counts from advertising Proxmox sections, while retaining direct-link hydration and the PBS-only Backups path. Revalidate the compact facet request when an all-resources cache supplies rows without scoped counts; cover mixed-source counts in component and browser proof and update both subsystem contracts. Change-source: pulse-maintainer
This commit is contained in:
@@ -404,7 +404,15 @@ and thermal facts remain discoverable from the PBS surface.
|
||||
Overview must not duplicate that domain table between its node and guest
|
||||
regions; the tab boundary owns PBS server, datastore, and artifact detail.
|
||||
The shared Proxmox section rail renders only tabs backed by current capability
|
||||
evidence. While resource counts are unknown it must not show every optional
|
||||
evidence. It reads the source-filtered `facets.byType` from one compact
|
||||
`type=pmg&source=proxmox,pbs,pmg,agent` request, not estate-wide
|
||||
`aggregations.byType`: unrelated VMware VMs and TrueNAS storage must not
|
||||
advertise Proxmox workflows. Agent is included for Proxmox-owned physical
|
||||
disks, whose fact source may remain `agent`; because that source is shared, a
|
||||
generic agent disk can still expose Storage until the route filters its rows.
|
||||
A fresh all-resources cache does not carry source-filtered facets, so the
|
||||
compact query must revalidate even when that cache can paint rows. While the
|
||||
facets are unknown the rail must not show every optional
|
||||
tab as a loading fallback; an independently fetched positive replication-job
|
||||
count may still expose Replication. A bookmarked section remains the active
|
||||
hydration target until counts can distinguish unavailable from unsupported,
|
||||
|
||||
@@ -795,9 +795,11 @@ state effects remain dormant and a forced Proxmox storage scope is not
|
||||
serialized as redundant URL state. Backups reuses the source-scoped Overview
|
||||
guest snapshot and adds only the PBS resource family, so it cannot repeat the
|
||||
large VM/LXC enumeration under a second request owner.
|
||||
Unknown resource aggregations must not advertise optional Proxmox workflows as
|
||||
if their providers are present. The navigation rail may expose Replication
|
||||
from its independent positive job count, while Storage, Backups, Ceph, and Mail
|
||||
Unknown source-scoped resource facets must not advertise optional Proxmox
|
||||
workflows as if their providers are present. Estate-wide aggregations are not
|
||||
tab evidence: unrelated provider rows can share VM or storage types. The
|
||||
navigation rail may expose Replication from its independent positive job count,
|
||||
while Storage, Backups, Ceph, and Mail
|
||||
wait for positive resource counts. A direct link keeps its requested route
|
||||
hydrating while counts are unknown; after counts arrive, an unsupported route
|
||||
renders Overview instead. Hiding a tab is navigation presentation, not a
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Browser-only resource-hook seam for #2321. The production page and shared
|
||||
// tab rail stay real; counts/resources are synthetic and switch on command.
|
||||
// tab rail stay real; source-scoped facets/resources are synthetic and switch
|
||||
// on command. Global counts deliberately contain unrelated provider rows.
|
||||
import { createSignal } from 'solid-js';
|
||||
import type { Resource } from '../src/types/resource';
|
||||
import type { UnifiedResourceAggregations } from '../src/hooks/useUnifiedResources';
|
||||
import type { UnifiedResourceFacets } from '../src/hooks/useUnifiedResources';
|
||||
|
||||
export * from '../src/hooks/useUnifiedResources';
|
||||
|
||||
@@ -18,7 +19,7 @@ const pmg = {
|
||||
sources: ['pmg'], status: 'online', lastSeen: Date.now(),
|
||||
} as Resource;
|
||||
|
||||
const [counts, setCounts] = createSignal<UnifiedResourceAggregations | null>(null);
|
||||
const [counts, setCounts] = createSignal<UnifiedResourceFacets | null>(null);
|
||||
(window as unknown as { __proxmoxTabProof: { setCounts: typeof setCounts } }).__proxmoxTabProof = {
|
||||
setCounts,
|
||||
};
|
||||
@@ -36,8 +37,8 @@ export function useUnifiedResources(options?: { cacheKey?: string }) {
|
||||
};
|
||||
return {
|
||||
resources,
|
||||
aggregations: counts,
|
||||
facets: () => null,
|
||||
aggregations: () => ({ total: 5, byType: { storage: 1, vm: 1, ceph: 1, pmg: 1 } }),
|
||||
facets: options?.cacheKey === 'proxmox-tab-evidence' ? counts : () => null,
|
||||
policyPosture: () => null,
|
||||
resourceSnapshotChange: () => ({ version: 0, changedIds: null }),
|
||||
loading: () => false,
|
||||
|
||||
@@ -49,7 +49,7 @@ const { chromium } = require('playwright');
|
||||
await page.getByTestId('proxmox-page').waitFor();
|
||||
const nav = page.getByRole('navigation', { name: 'Proxmox sections' });
|
||||
const labels = async () => (await nav.locator('a').allTextContents()).map((label) => label.trim());
|
||||
const setCounts = async (byType) => page.evaluate((value) => window.__proxmoxTabProof.setCounts({ total: 1, byType: value }), byType);
|
||||
const setCounts = async (byType) => page.evaluate((value) => window.__proxmoxTabProof.setCounts({ incidentCount: 0, byType: value }), byType);
|
||||
const screenshot = async (state) => {
|
||||
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||
const rail = await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="Proxmox sections"] a')].map((link) => ({
|
||||
|
||||
@@ -1,47 +1,53 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "c54d361acf0d38d3f90767022513f718e05e1b69",
|
||||
"verified_at": "2026-09-29T11:23:17Z",
|
||||
"base_sha": "efd04b11c944ffccd4ec556f0b35cf2bf80bbbd1",
|
||||
"verified_at": "2026-09-29T13:54:15Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "37ce3942c563770c3bdd494a692ac9fe22cf367479b4f788136ad5d8faaaf625"
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "67da1ba826ad85f30e8b1ab61c3477badf52629ba4d063537bada4003650d5d7"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/mail (direct link, synthetic capability fixture)",
|
||||
"/proxmox/backups/date (tab navigation, synthetic capability fixture)"
|
||||
],
|
||||
"viewports": [
|
||||
{ "width": 1365, "height": 900 },
|
||||
{ "width": 390, "height": 844 }
|
||||
{
|
||||
"width": 1365,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"unknown resource counts on direct Mail Gateway link",
|
||||
"PBS-only resource counts with Mail Gateway unsupported",
|
||||
"unknown source-scoped facets despite positive unrelated global counts on direct Mail Gateway link",
|
||||
"PBS-only scoped facets with Mail Gateway, Ceph and Storage unsupported despite unrelated global counts",
|
||||
"PBS Backups tab active",
|
||||
"positive Storage, Ceph and Mail Gateway counts",
|
||||
"positive scoped Storage, Ceph and Mail Gateway facets",
|
||||
"Mail Gateway tab active",
|
||||
"Mail Gateway count removed after navigation",
|
||||
"no optional service counts"
|
||||
"Mail Gateway facet removed after navigation",
|
||||
"no optional service facets"
|
||||
],
|
||||
"interactions": [
|
||||
"hydrate bookmarked Mail Gateway route before counts settle",
|
||||
"reveal PBS-only Backups while hiding unsupported Replication, Ceph and Mail Gateway tabs",
|
||||
"hydrate bookmarked Mail Gateway route before source-scoped facets settle",
|
||||
"reveal PBS-only Backups while ignoring unrelated estate-wide aggregation counts",
|
||||
"activate Backups by pointer on desktop and keyboard on phone",
|
||||
"activate Mail Gateway after positive counts",
|
||||
"fall back to Overview when Mail Gateway count disappears without rewriting the URL",
|
||||
"activate Mail Gateway after positive scoped facets",
|
||||
"fall back to Overview when Mail Gateway facet disappears without rewriting the URL",
|
||||
"verify selected-tab semantics, desktop and phone rail layout, and no document overflow"
|
||||
],
|
||||
"command": "pulse-worker-browser frontend-modern/browser-tests/proxmox-tabs-2321.cjs from assigned root, with browser-tests/proxmox-tabs-2321-run.json set to 1365 then 390; Chromium 141.0.7390.37 / Playwright 1.56.1",
|
||||
"artifacts": [
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-2321-desktop.log",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-2321-phone.log",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-unknown-direct-mail-1365.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-pbs-only-backups-390.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-available-mail-390.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-18cny1xf/browser-images/proxmox-tabs-2321-removed-mail-1365.png"
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-ycx7l47_/browser-2321-desktop-final.log",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-ycx7l47_/browser-2321-phone-final.log",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-ycx7l47_/browser-images-final/proxmox-tabs-2321-unknown-direct-mail-390.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-ycx7l47_/browser-images-final/proxmox-tabs-2321-pbs-only-backups-1365.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-ycx7l47_/browser-images-final/proxmox-tabs-2321-available-mail-390.png",
|
||||
"/var/lib/pulse-maintainer/worker-outputs/web-product-ycx7l47_/browser-images-final/proxmox-tabs-2321-removed-mail-1365.png"
|
||||
],
|
||||
"notes": "Production ProxmoxPageSurface and PlatformSectionTabs were mounted in a Vite/Chromium fixture. Only useUnifiedResources and API responses were synthetic; no installed server, agent, reporter topology or release was exercised. Both runs exited 0 with no page errors or horizontal overflow. Final screenshots were inspected for selected rail, table placement and narrow reachability."
|
||||
"notes": "Production ProxmoxPageSurface and PlatformSectionTabs were mounted in a Vite/Chromium fixture. Only useUnifiedResources and API responses were synthetic; the fixture returns unrelated positive estate-wide aggregations and independent Proxmox-scoped facets. No installed server, agent, reporter topology or release was exercised. Both runs exited 0 with no page errors or horizontal overflow. Selected screenshots were inspected for rail state and narrow reachability."
|
||||
}
|
||||
|
||||
@@ -104,8 +104,23 @@ export function ProxmoxPageSurface() {
|
||||
const segment = location.pathname.split('/').filter(Boolean)[1] as ProxmoxPageTabId | undefined;
|
||||
return segment && VALID_TABS.has(segment) ? segment : 'overview';
|
||||
});
|
||||
const [resourceAggregations, setResourceAggregations] =
|
||||
createSignal<ReturnType<ReturnType<typeof useUnifiedResources>['aggregations']>>(null);
|
||||
// List aggregations count the whole estate, not the query scope. The small
|
||||
// PMG row query returns source-scoped type facets for every Proxmox workflow
|
||||
// without hydrating each inactive section's inventory.
|
||||
const tabEvidence = useUnifiedResources({
|
||||
query: 'type=pmg&source=proxmox,pbs,pmg,agent',
|
||||
cacheKey: 'proxmox-tab-evidence',
|
||||
initialHydration: 'prefer-ws-then-rest',
|
||||
});
|
||||
createEffect(() => {
|
||||
// A fresh all-resources cache can seed rows but not source-filtered facets.
|
||||
// Force this compact query once instead of leaving the rail unresolved
|
||||
// until a websocket event happens to arrive.
|
||||
if (tabEvidence.facets?.() !== null) return;
|
||||
void Promise.resolve()
|
||||
.then(() => tabEvidence.refetch())
|
||||
.catch(() => undefined);
|
||||
});
|
||||
// Replication jobs come straight from /api/replication/jobs (they bypass
|
||||
// the unified-resource pipeline), so the surface owns the fetch: the job
|
||||
// count gates the Replication tab and the same data feeds the table.
|
||||
@@ -117,10 +132,10 @@ export function ProxmoxPageSurface() {
|
||||
);
|
||||
const visibleTabs = createMemo(() => {
|
||||
// An unknown snapshot is not evidence that every optional integration is
|
||||
// present. Keep only sections backed by an independent signal until the
|
||||
// resource counts arrive (or if an older response omits them).
|
||||
// present. Never use estate-wide aggregations here: unrelated VMware VMs
|
||||
// or TrueNAS storage would otherwise advertise empty Proxmox sections.
|
||||
return buildVisibleProxmoxTabSpecsFromCounts(
|
||||
resourceAggregations()?.byType ?? {},
|
||||
tabEvidence.facets?.()?.byType ?? {},
|
||||
replicationJobCount(),
|
||||
);
|
||||
});
|
||||
@@ -131,7 +146,7 @@ export function ProxmoxPageSurface() {
|
||||
const requested = requestedTab();
|
||||
// Do not discard a direct link while counts are still unknown: its own
|
||||
// resource query must be allowed to hydrate before deciding it is absent.
|
||||
return !resourceAggregations() || visibleTabIds().has(requested) ? requested : 'overview';
|
||||
return !tabEvidence.facets?.() || visibleTabIds().has(requested) ? requested : 'overview';
|
||||
});
|
||||
const shouldHydrateTab = (tab: ProxmoxPageTabId) => activeTab() === tab;
|
||||
const overviewResources = useUnifiedResources({
|
||||
@@ -221,10 +236,6 @@ export function ProxmoxPageSurface() {
|
||||
activeTab() === 'backups'
|
||||
? Promise.all([overviewResources.refetch(), backupResources.refetch()])
|
||||
: resourceState().refetch();
|
||||
createEffect(() => {
|
||||
const next = resourceState().aggregations?.();
|
||||
if (next) setResourceAggregations(next);
|
||||
});
|
||||
const normalizeSnapshot = (snapshot: Resource[] | undefined) =>
|
||||
Array.isArray(snapshot) ? snapshot : [];
|
||||
const buildModel = (snapshot: Resource[] | undefined) =>
|
||||
|
||||
+20
-12
@@ -2,7 +2,7 @@ import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { Route, Router } from '@solidjs/router';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { UnifiedResourceAggregations } from '@/hooks/useUnifiedResources';
|
||||
import type { UnifiedResourceFacets } from '@/hooks/useUnifiedResources';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { ProxmoxPageSurface } from '../ProxmoxPageSurface';
|
||||
import proxmoxPageSurfaceSource from '../ProxmoxPageSurface.tsx?raw';
|
||||
@@ -443,10 +443,11 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
value as {
|
||||
query: string;
|
||||
cacheKey: string;
|
||||
enabled: () => boolean;
|
||||
enabled?: () => boolean;
|
||||
},
|
||||
);
|
||||
expect(options.map((value) => value.cacheKey)).toEqual([
|
||||
'proxmox-tab-evidence',
|
||||
'proxmox-overview',
|
||||
'proxmox-storage-shell',
|
||||
'proxmox-replication-shell',
|
||||
@@ -455,6 +456,7 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
'proxmox-mail',
|
||||
]);
|
||||
expect(options.map((value) => value.query)).toEqual([
|
||||
'type=pmg&source=proxmox,pbs,pmg,agent',
|
||||
'type=agent,vm,system-container,oci-container&source=proxmox',
|
||||
'type=agent,pbs,storage,physical_disk,ceph&source=proxmox,pbs,agent',
|
||||
'type=agent&source=proxmox',
|
||||
@@ -462,8 +464,9 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
'type=ceph&source=proxmox',
|
||||
'type=pmg&source=pmg',
|
||||
]);
|
||||
expect(options[0].enabled()).toBe(true);
|
||||
expect(options.slice(1).every((value) => value.enabled() === false)).toBe(true);
|
||||
expect(options[0].enabled).toBeUndefined();
|
||||
expect(options[1].enabled?.()).toBe(true);
|
||||
expect(options.slice(2).every((value) => value.enabled?.() === false)).toBe(true);
|
||||
expect(proxmoxPageSurfaceSource).not.toContain('backgroundHydrationTabs');
|
||||
expect(proxmoxPageSurfaceSource).not.toContain('requestIdleCallback');
|
||||
expect(proxmoxPageSurfaceSource).toContain('resourceSource={storageResources}');
|
||||
@@ -504,9 +507,10 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
renderSurface();
|
||||
|
||||
const options = mockUseUnifiedResources.mock.calls.map(
|
||||
([value]) => value as { cacheKey: string; query: string; enabled: () => boolean },
|
||||
([value]) => value as { cacheKey: string; query: string; enabled?: () => boolean },
|
||||
);
|
||||
expect(options.map((value) => value.enabled())).toEqual([
|
||||
expect(options.map((value) => value.enabled?.() ?? true)).toEqual([
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
@@ -514,7 +518,7 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
expect(options[3]).toMatchObject({
|
||||
expect(options[4]).toMatchObject({
|
||||
cacheKey: 'proxmox-backups-shell',
|
||||
query: 'type=pbs,agent&source=pbs',
|
||||
});
|
||||
@@ -590,13 +594,16 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
|
||||
it('hydrates a direct link while counts are unknown, then gates it on actual capabilities', async () => {
|
||||
mockPathname.mockReturnValue('/proxmox/mail');
|
||||
const [aggregations, setAggregations] = createSignal<UnifiedResourceAggregations | null>(null);
|
||||
const [facets, setFacets] = createSignal<UnifiedResourceFacets | null>(null);
|
||||
const refetch = vi.fn(async () => []);
|
||||
mockUseUnifiedResources.mockReturnValue({
|
||||
resources: () => [],
|
||||
aggregations,
|
||||
// Estate-wide counts include unrelated providers and must not unlock tabs.
|
||||
aggregations: () => ({ total: 5, byType: { storage: 1, vm: 1, ceph: 1, pmg: 1 } }),
|
||||
facets,
|
||||
loading: () => false,
|
||||
error: () => null,
|
||||
refetch: vi.fn(),
|
||||
refetch,
|
||||
});
|
||||
|
||||
renderSurface();
|
||||
@@ -608,14 +615,15 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
([value]) => value as { cacheKey: string; enabled: () => boolean },
|
||||
);
|
||||
expect(options.find((option) => option.cacheKey === 'proxmox-mail')?.enabled()).toBe(true);
|
||||
await waitFor(() => expect(refetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
setAggregations({ total: 2, byType: { pbs: 1, pmg: 1 } });
|
||||
setFacets({ incidentCount: 0, byType: { pbs: 1, pmg: 1 } });
|
||||
await waitFor(() => {
|
||||
expect(tabs).toHaveAttribute('data-tabs', 'overview,backups,mail');
|
||||
expect(tabs).toHaveAttribute('data-active', 'mail');
|
||||
});
|
||||
|
||||
setAggregations({ total: 1, byType: { pbs: 1 } });
|
||||
setFacets({ incidentCount: 0, byType: { pbs: 1 } });
|
||||
await waitFor(() => {
|
||||
expect(tabs).toHaveAttribute('data-tabs', 'overview,backups');
|
||||
expect(tabs).toHaveAttribute('data-active', 'overview');
|
||||
|
||||
Reference in New Issue
Block a user