mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
parent
d0a60e1a92
commit
59d320d1d4
6 changed files with 112 additions and 13 deletions
|
|
@ -215,13 +215,14 @@ work extends shared components instead of creating new local variants.
|
|||
reconstructing raw `current / limit` slash math or `0 remaining` copy in
|
||||
the banner shell, state owner, or shared model.
|
||||
5. Keep shared platform-connections shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`, `frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`, and `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source instead of creating provider-local summary fetches or VMware-only shell vocabulary.
|
||||
6. Keep shared storage feature presenters on canonical platform truth. When reusable storage presenters under `frontend-modern/src/features/storageBackups/` classify canonical resources for the shared storage route, API-backed virtualization datastores such as VMware must stay inventory-only datastores instead of inheriting PBS-specific backup-repository or protected-target copy from older fallback branches.
|
||||
7. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest.
|
||||
8. Keep top-of-page summary interaction on shared primitives. Infrastructure, workloads, and storage summary cards must route sticky-shell behavior through `frontend-modern/src/components/shared/StickySummarySection.tsx` and route row-hover or focused-series rendering through shared chart primitives such as `frontend-modern/src/components/shared/InteractiveSparkline.tsx` and `frontend-modern/src/components/shared/DensityMap.tsx`, rather than page-local sticky wrappers or metric-card-specific hover logic. The shared summary-card contract must also own stable summary-card geometry for chart-backed cards so row hover, focus, synchronized readouts, or idle header metadata cannot ratchet the sticky summary taller across rerenders.
|
||||
9. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. `frontend-modern/src/components/Recovery/RecoverySummary.tsx` is explicitly outside this interaction dialect: recovery posture cards may share summary framing, but they must not silently grow row/group/chart hover behavior without a separate governed product decision.
|
||||
10. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states.
|
||||
11. Keep contextual row focus on the shared summary primitive. Summary surfaces and same-route table drill-ins must reuse `frontend-modern/src/components/shared/contextualFocus.ts` for interactive-series filtering, focused-name lookup, active-series derivation, local scroll preservation, and deliberate inline-detail reveal instead of rebuilding page-local `Set` filters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface.
|
||||
12. Keep summary-to-table coordination deliberate, explicit, and reversible. Shared summary hover may highlight the matching table row when it is already visible, but transient chart hover must not auto-filter tables, auto-scroll the page, or reshuffle table ordering. Pinned page/group/entity scope on workloads, infrastructure, or storage must stay row-first: the pinned row or group header is the visible scoped state, not a second strip or search-row widget. Page shells therefore must not reintroduce always-on scope banners, preview bars, page-local chips, breadcrumbs, or search/filter-row scope accessories just to explain pinned state. When the active row is off-screen, page owners must still route through `frontend-modern/src/components/shared/summaryTableFocus.ts` and surface a lightweight `Jump to row` affordance that reveals and scrolls only on explicit user action. That same shared table-focus owner now also owns reversible clearing: pinned scope may clear only from governed neutral interaction-surface space or the shared `Escape` path, with page owners binding a broader clear-surface root separately from the row-lookup table root when needed and supplying one page-level reset callback for filters plus summary-linked selections. Row cells, group headers, inline detail, summary cards, and explicit controls must not accidentally clear pinned scope, while governed table/card clear surfaces must still allow real user clicks on neutral whitespace to clear it. Deliberate row focus may reveal inline detail automatically, but that reveal must be drawer-aware: infrastructure and workload row toggles that already have the row in view must hand the current `.app-scroll-shell` position through `frontend-modern/src/utils/appShellScrollRestoration.ts` so the remounted shell in `frontend-modern/src/App.tsx` can reopen the inline detail without looking like a page refresh, and then still route through the shared reveal helper whenever the opened drawer would otherwise land below the fold. Same-route drawers must therefore scroll only enough to keep the row header plus the top of the inline detail visible, never hard-center the row just because the route state changed.
|
||||
6. Keep Proxmox deep-link route selection on the shared settings-navigation boundary. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` and `frontend-modern/src/components/Settings/useSettingsNavigation.ts` must treat the canonical PBS and PMG Proxmox deep links as agent-selection authority even though those URLs resolve to the shared `infrastructure-operations` tab. Reloading or remounting on a PBS or PMG deep link must not silently fall back to the PVE selector state.
|
||||
7. Keep shared storage feature presenters on canonical platform truth. When reusable storage presenters under `frontend-modern/src/features/storageBackups/` classify canonical resources for the shared storage route, API-backed virtualization datastores such as VMware must stay inventory-only datastores instead of inheriting PBS-specific backup-repository or protected-target copy from older fallback branches.
|
||||
8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest.
|
||||
9. Keep top-of-page summary interaction on shared primitives. Infrastructure, workloads, and storage summary cards must route sticky-shell behavior through `frontend-modern/src/components/shared/StickySummarySection.tsx` and route row-hover or focused-series rendering through shared chart primitives such as `frontend-modern/src/components/shared/InteractiveSparkline.tsx` and `frontend-modern/src/components/shared/DensityMap.tsx`, rather than page-local sticky wrappers or metric-card-specific hover logic. The shared summary-card contract must also own stable summary-card geometry for chart-backed cards so row hover, focus, synchronized readouts, or idle header metadata cannot ratchet the sticky summary taller across rerenders.
|
||||
10. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. `frontend-modern/src/components/Recovery/RecoverySummary.tsx` is explicitly outside this interaction dialect: recovery posture cards may share summary framing, but they must not silently grow row/group/chart hover behavior without a separate governed product decision.
|
||||
11. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states.
|
||||
12. Keep contextual row focus on the shared summary primitive. Summary surfaces and same-route table drill-ins must reuse `frontend-modern/src/components/shared/contextualFocus.ts` for interactive-series filtering, focused-name lookup, active-series derivation, local scroll preservation, and deliberate inline-detail reveal instead of rebuilding page-local `Set` filters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface.
|
||||
13. Keep summary-to-table coordination deliberate, explicit, and reversible. Shared summary hover may highlight the matching table row when it is already visible, but transient chart hover must not auto-filter tables, auto-scroll the page, or reshuffle table ordering. Pinned page/group/entity scope on workloads, infrastructure, or storage must stay row-first: the pinned row or group header is the visible scoped state, not a second strip or search-row widget. Page shells therefore must not reintroduce always-on scope banners, preview bars, page-local chips, breadcrumbs, or search/filter-row scope accessories just to explain pinned state. When the active row is off-screen, page owners must still route through `frontend-modern/src/components/shared/summaryTableFocus.ts` and surface a lightweight `Jump to row` affordance that reveals and scrolls only on explicit user action. That same shared table-focus owner now also owns reversible clearing: pinned scope may clear only from governed neutral interaction-surface space or the shared `Escape` path, with page owners binding a broader clear-surface root separately from the row-lookup table root when needed and supplying one page-level reset callback for filters plus summary-linked selections. Row cells, group headers, inline detail, summary cards, and explicit controls must not accidentally clear pinned scope, while governed table/card clear surfaces must still allow real user clicks on neutral whitespace to clear it. Deliberate row focus may reveal inline detail automatically, but that reveal must be drawer-aware: infrastructure and workload row toggles that already have the row in view must hand the current `.app-scroll-shell` position through `frontend-modern/src/utils/appShellScrollRestoration.ts` so the remounted shell in `frontend-modern/src/App.tsx` can reopen the inline detail without looking like a page refresh, and then still route through the shared reveal helper whenever the opened drawer would otherwise land below the fold. Same-route drawers must therefore scroll only enough to keep the row header plus the top of the inline detail visible, never hard-center the row just because the route state changed.
|
||||
Shared summary-linked rows and group headers must also route their preview
|
||||
semantics through
|
||||
`frontend-modern/src/components/shared/summaryInteractionA11y.ts`.
|
||||
|
|
@ -1571,6 +1572,13 @@ including the route-owned billing focus contract where
|
|||
`/settings/system/billing/plan` is the canonical settings-tab destination,
|
||||
`/settings/system/billing/usage` is a same-tab child state, and legacy billing
|
||||
base/hash links are compatibility inputs rather than primary runtime routes.
|
||||
That same route-sync owner must also preserve Proxmox platform-selection truth
|
||||
across canonical deep links such as
|
||||
`/settings/infrastructure/platforms/proxmox/pbs` and
|
||||
`/settings/infrastructure/platforms/proxmox/pmg`: even though those routes
|
||||
collapse into the shared `infrastructure-operations` tab, the selected
|
||||
platform state must still be derived from the path instead of silently
|
||||
falling back to `pve` on reload or remount.
|
||||
`useSettingsShellState.ts` owns shell-local sidebar/search/password-modal
|
||||
state, and `settingsTabSaveBehavior.ts` owns settings tab save-behavior lookup,
|
||||
`frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx` owns
|
||||
|
|
|
|||
|
|
@ -516,10 +516,12 @@ describe('Settings architecture guardrails', () => {
|
|||
"const INFRASTRUCTURE_INSTALL_PREFIX = '/settings/infrastructure/install';",
|
||||
);
|
||||
expect(settingsNavigationModelSource).toContain('export function resolveCanonicalSettingsPath');
|
||||
expect(settingsNavigationModelSource).toContain('export function isProxmoxSettingsPath');
|
||||
expect(settingsNavigationModelSource).toContain('export function settingsTabPath');
|
||||
expect(settingsNavigationModelSource).toContain('return INFRASTRUCTURE_INSTALL_PREFIX;');
|
||||
expect(settingsNavigationModelSource).toContain('SELF_HOSTED_PRO_BILLING_PLAN_ROUTE');
|
||||
expect(settingsNavigationHookSource).toContain('deriveTabFromPath');
|
||||
expect(settingsNavigationHookSource).toContain('isProxmoxSettingsPath');
|
||||
expect(settingsNavigationHookSource).toContain('resolveCanonicalSettingsPath');
|
||||
expect(settingsNavigationHookSource).toContain('resolveCanonicalSelfHostedBillingHref');
|
||||
expect(settingsNavigationHookSource).toContain('settingsTabPath');
|
||||
|
|
|
|||
|
|
@ -21,9 +21,12 @@ function renderHarness(pathname = '/settings', search = '', hash = '') {
|
|||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => navigation.setActiveTab('infrastructure-operations')}>
|
||||
open infrastructure settings
|
||||
</button>
|
||||
<div data-testid="selected-agent">{navigation.selectedAgent()}</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -58,4 +61,20 @@ describe('useSettingsNavigation', () => {
|
|||
scroll: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('syncs the selected proxmox agent from canonical deep links on initial load', async () => {
|
||||
renderHarness('/settings/infrastructure/platforms/proxmox/pbs');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('selected-agent')).toHaveTextContent('pbs');
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults the selected proxmox agent to pve on the base proxmox route', async () => {
|
||||
renderHarness('/settings/infrastructure/platforms/proxmox');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('selected-agent')).toHaveTextContent('pve');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -250,6 +250,16 @@ export function deriveAgentFromPath(path: string): AgentKey | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
export function isProxmoxSettingsPath(path: string): boolean {
|
||||
const canonicalPath = resolveCanonicalSettingsPath(path) ?? normalizeSettingsPath(path);
|
||||
return (
|
||||
canonicalPath === PLATFORM_CONNECTIONS_PREFIX ||
|
||||
canonicalPath.startsWith(PROXMOX_PREFIX) ||
|
||||
canonicalPath.startsWith(LEGACY_PROXMOX_PREFIX) ||
|
||||
canonicalPath.startsWith(LEGACY_PROXMOX_API_PREFIX)
|
||||
);
|
||||
}
|
||||
|
||||
export function settingsAgentPath(agent: AgentKey): string {
|
||||
return PROXMOX_AGENT_META[agent].path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
deriveAgentFromPath,
|
||||
deriveTabFromPath,
|
||||
deriveTabFromQuery,
|
||||
isProxmoxSettingsPath,
|
||||
resolveCanonicalSettingsPath,
|
||||
settingsAgentPath,
|
||||
settingsTabPath,
|
||||
|
|
@ -139,9 +140,11 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
|
|||
setCurrentTab(resolved);
|
||||
}
|
||||
|
||||
if (resolved === 'proxmox') {
|
||||
const agentFromPath = deriveAgentFromPath(effectivePath);
|
||||
setSelectedAgent(agentFromPath ?? 'pve');
|
||||
if (isProxmoxSettingsPath(effectivePath)) {
|
||||
const agentFromPath = deriveAgentFromPath(effectivePath) ?? 'pve';
|
||||
if (selectedAgent() !== agentFromPath) {
|
||||
setSelectedAgent(agentFromPath);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -127,6 +127,16 @@ async function expectFirstTableContains(
|
|||
}
|
||||
}
|
||||
|
||||
async function expectFirstTableExcludes(
|
||||
page: import('@playwright/test').Page,
|
||||
texts: string[],
|
||||
): Promise<void> {
|
||||
const table = page.locator('table').first();
|
||||
for (const text of texts) {
|
||||
await expect(table).not.toContainText(text);
|
||||
}
|
||||
}
|
||||
|
||||
test.describe.serial('Platform mock runtime', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
|
|
@ -219,4 +229,51 @@ test.describe.serial('Platform mock runtime', () => {
|
|||
await expectInfrastructureSource(page, source, resourceName);
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps canonical Proxmox platform routes aligned with the selected platform table', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop runtime proof');
|
||||
|
||||
await ensureMockModeEnabled(page);
|
||||
const resourceNames = await fetchMockResourceNames(page);
|
||||
|
||||
await page.goto('/settings/infrastructure/platforms/proxmox/pve', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForURL(/\/settings\/infrastructure\/platforms\/proxmox\/pve/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.getByRole('button', { name: 'Virtual Environment', exact: true })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
);
|
||||
await expectFirstTableContains(page, [resourceNames['proxmox-pve']]);
|
||||
|
||||
await page.goto('/settings/infrastructure/platforms/proxmox/pbs', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForURL(/\/settings\/infrastructure\/platforms\/proxmox\/pbs/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.getByRole('button', { name: 'Backup Server', exact: true })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
);
|
||||
await expectFirstTableContains(page, [resourceNames['proxmox-pbs']]);
|
||||
await expectFirstTableExcludes(page, [resourceNames['proxmox-pve']]);
|
||||
|
||||
await page.goto('/settings/infrastructure/platforms/proxmox/pmg', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForURL(/\/settings\/infrastructure\/platforms\/proxmox\/pmg/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.getByRole('button', { name: 'Mail Gateway', exact: true })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
);
|
||||
await expectFirstTableContains(page, [resourceNames['proxmox-pmg']]);
|
||||
await expectFirstTableExcludes(page, [resourceNames['proxmox-pve']]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue