fix(ui): keep disk mounts in the parent scrolling flow

The nested 140px mount list relies on a thin scrollbar to reveal additional disks, which #2051 reports as invisible in Firefox. Remove that nested scroll boundary so all mounts participate in the existing outer scroll flow without changing data or global scrollbar styling.

Validate short and long lists in light/dark Firefox and Chromium fixtures, including keyboard reachability, and retain aggregate/empty-state component coverage. This is synthetic browser proof, not reporter installation acceptance.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-11 11:23:25 +01:00
parent 72809bc1cf
commit bfae4ce0c8
9 changed files with 209 additions and 36 deletions
@@ -20,6 +20,17 @@
## Purpose
### Disk mount scrolling
DisksCard keeps every supplied mount in its parent's scrolling flow. It must not
cap the mount list or create a nested scroll target whose only overflow cue is
a platform scrollbar. Large lists intentionally increase card height; aggregate
usage, individual mount data and empty-state behaviour remain unchanged. This
boundary is local to DisksCard, not a global scrollbar styling requirement.
SharedPrimitives.guardrails.test.ts protects this composition; component tests
preserve mount counts and totals. The disk-mounts qualification fixture checks
short/long lists, themes and keyboard reachability with production CSS.
### Ollama credential editing
The provider panel exposes the existing Basic Auth configuration. Saved passwords
are represented by presence text, never a placeholder secret or input value.
+17 -35
View File
@@ -1,55 +1,37 @@
{
"version": 1,
"base_sha": "a3eab37bcd36a0a087680ece938ca862c6f4b4d9",
"verified_at": "2026-09-10T20:31:10.676001Z",
"base_sha": "72809bc1cf71e2493d1f0c492b926110a969bdb4",
"verified_at": "2026-09-11T10:26:20.316799Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx",
"frontend-modern/src/components/Settings/aiSettingsModel.ts",
"frontend-modern/src/components/Settings/useAISettingsState.ts",
"frontend-modern/src/types/ai.ts"
"frontend-modern/src/components/shared/cards/DisksCard.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx": "76f3ed93e95cbe9f24f5cd55d49aeae7397f739e6520665f1b8de074ccaeaa96",
"frontend-modern/src/components/Settings/aiSettingsModel.ts": "9dee0d8b8bd013e870732c3893fa4742e40914ec4931a044b1698bc90f598c22",
"frontend-modern/src/components/Settings/useAISettingsState.ts": "d4bdd6ce19279e5e243b69e9ca769436c3d88a50f243bd8241c7aeffafeb2613",
"frontend-modern/src/types/ai.ts": "bdad2e3926976f761b2a75db03ed4473f67e73cdc60e6327f15acc6626225507"
"frontend-modern/src/components/shared/cards/DisksCard.tsx": "cdda554f93b7aecb925d803b0192260ef1a445065c501468a0412b824dc08028"
},
"routes": [
"/settings/pulse-intelligence/provider",
"/settings/system-updates",
"/proxmox"
"/qualification/disks/?theme=light&count=2",
"/qualification/disks/?theme=dark&count=24"
],
"viewports": [
{
"width": 1440,
"height": 900
"width": 600,
"height": 500
},
{
"width": 390,
"height": 900
"width": 1200,
"height": 500
}
],
"states": [
"Saved Ollama password with blank masked input",
"Unrelated save preserves credentials",
"Loading disables save and credential controls",
"Failed save retains replacement draft, successful retry preserves whitespace and clears draft",
"Explicit password and username clearing persists across reload",
"Source build clears cached release offers and disables release updates",
"Stable and preview builds retain manual update checks",
"Live Proxmox agent identity and CPU with no source-build agent upgrade target"
"2 and 24 mounts in light and dark themes",
"No nested clipping before scrolling; all mounts remain in outer flow"
],
"interactions": [
"Expand Ollama accordion, focus password and Tab",
"Save unrelated option with keyboard Enter",
"Fail replacement save then retry",
"Clear saved credentials and reload",
"Hover and focus disabled source update check",
"Keyboard manual stable and preview update checks",
"Expand and collapse Proxmox details with keyboard, reload",
"Inspect desktop and narrow screenshots, no horizontal overflow in provider form"
"Focus Before disks, keyboard End then Tab",
"Verify final mount in viewport and After disks focused",
"Inspect full-page Firefox dark narrow and Chromium light desktop screenshots"
],
"command": "node merged-ollama-browser.cjs && node merged-source-build-browser.cjs",
"notes": "Fresh verification of merged source on isolated Vite port 5174 at 1440x900 and 390x900. Playwright uses synthetic provider, version, update and agent HTTP responses over the managed local dev backend. Credential writes are intercepted. Actual pixels inspected for provider saved, clear and reloaded states, source update controls and Proxmox detail. Evidence scripts, interaction matrix, logs and screenshots retained in machine-local issue-1913-upgrade task directory. Earlier locator failures are retained and not counted as passes. No release qualification is claimed."
"command": "pulse-heavy-run -- node tests/qualification/disk-mounts/check.mjs",
"notes": "16 synthetic production-card/CSS cases passed in Firefox142.0.1 and Chromium141.0.7390.37. Screenshots retained in lane outcome screenshots directory. No full installed drawer or reporter Firefox140 ESR reproduction claimed. Production runtime unchanged during completion repair."
}
@@ -0,0 +1,7 @@
<!doctype html>
<html>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
@@ -0,0 +1,22 @@
import { render } from 'solid-js/web';
import { DisksCard } from '../../src/components/shared/cards/DisksCard';
import '../../src/index.css';
const params = new URLSearchParams(location.search);
document.documentElement.classList.toggle('dark', params.get('theme') === 'dark');
const disks = Array.from({ length: Number(params.get('count') ?? 24) }, (_, i) => ({
mountpoint: `/mnt/disk-${i}`,
total: 1000000000,
used: 500000000,
free: 500000000,
usage: 0.5,
}));
render(
() => (
<main class="bg-surface text-base-content p-4" style={{ width: '440px' }}>
<button>Before disks</button>
<DisksCard disks={disks} />
<button>After disks</button>
</main>
),
document.getElementById('root')!,
);
@@ -344,6 +344,14 @@ const frontendIndexCssSource = readFileSync(join(process.cwd(), 'src/index.css')
const readFrontendSource = (path: string) => readFileSync(join(process.cwd(), path), 'utf8');
describe('shared primitive guardrails', () => {
it('keeps disk mounts in the parent scrolling flow rather than a hidden nested list', () => {
const source = readFrontendSource('src/components/shared/cards/DisksCard.tsx');
expect(source).toContain('data-testid="disks-card-mounts"');
expect(source).not.toMatch(/max-h-|overflow-y-|custom-scrollbar/);
expect(source).toContain('<For each={props.disks}>');
expect(source).toContain('<StackedDiskBar');
});
it('keeps one canonical agent-host metric history group catalog', () => {
expect(HOST_METRICS_HISTORY_GROUPS.map((group) => group.id)).toEqual([
'utilization',
@@ -47,7 +47,8 @@ export const DisksCard: Component<DisksCardProps> = (props) => {
</div>
)}
</Show>
<div class="max-h-[140px] overflow-y-auto custom-scrollbar space-y-2">
{/* Keep mounts in the parent scroll flow: overlay scrollbars can hide a nested list. */}
<div class="space-y-2" data-testid="disks-card-mounts">
<For each={props.disks}>
{(disk) => {
const total = disk.total ?? 0;
@@ -55,6 +55,21 @@ describe('DisksCard', () => {
expect(screen.getByText('/data')).toBeInTheDocument();
});
it.each([2, 24])('keeps all %i mounts in the parent scroll flow', (count) => {
const disks = Array.from({ length: count }, (_, i) => ({
mountpoint: `/mnt/disk-${i}`,
total: 100,
used: 50,
free: 50,
usage: 0.5,
}));
render(() => <DisksCard disks={disks} />);
const mounts = screen.getByTestId('disks-card-mounts');
expect(mounts.children).toHaveLength(count);
expect(mounts.className).not.toMatch(/max-h-|overflow-|custom-scrollbar/);
expect(screen.getByTitle(`/mnt/disk-${count - 1}`)).toBeInTheDocument();
});
it('renders nothing when no disks are available', () => {
const { container } = render(() => <DisksCard disks={[]} />);
+49
View File
@@ -0,0 +1,49 @@
# Disk mount visibility (#2051)
The Disks card previously clipped its mount list to 140px. Its thin nested
scrollbar was the only indication of additional mounts. The repair keeps all
mounts in the outer scrolling flow; aggregate usage and individual mount data
are unchanged. Large lists make the card taller rather than introducing another
scroll target. This deliberately avoids a global scrollbar-style change.
## Reproduce
Install locked root and frontend-modern npm dependencies and pinned Playwright
Firefox/Chromium browsers, then from the repository root:
```sh
pulse-heavy-run -- node tests/qualification/disk-mounts/check.mjs
```
The isolated Vite fixture imports the production card and stylesheet. It uses
synthetic mount data, no backend, credentials or customer installation. It checks
2 and 24 mounts in light/dark themes, list clipping and keyboard End/Tab access
to the final mount and following control. Vite is shut down after the run.
The qualification HTML is not an application entry or production build input.
## Evidence — 11 September 2026
Baseline 72809bc1cf71, Firefox 142.0.1: short list 54/54px; long list
clientHeight=140, scrollHeight=736, overflow=auto, maxHeight=140px. The no-clipping
assertion fails. After repair all eight Firefox 142.0.1 / Chromium 141.0.7390.37
cases pass: short 54/54px, long 736/736px, overflow=visible, maxHeight=none.
Keyboard checks pass in each case. Component coverage additionally preserves
aggregate usage, all mounts and the empty state.
This demonstrates the nested clipping and its removal with production CSS. It
does not establish why Firefox 140.15 ESR on the reporter's Debian desktop hides
its native scrollbar, nor reproduce their full drawer/estate. Reporter retest
and release availability remain distinct from this source/browser proof.
The reporter's subsequent before/after-scroll crops in comment
https://github.com/rcourtman/Pulse/issues/2051#issuecomment-5632905322 were also
inspected: no evident pre-scroll thumb, then a narrow pale mark beside the disk
bars after scrolling. Removing the nested overflow eliminates that card's native
track/thumb entirely, rather than claiming to repair Firefox painting. The
fixture proves there is no hidden card overflow before keyboard scrolling;
outer-page navigation still reaches every mount. Exact desktop painting remains
unreproduced.
Completion verification adds 600x500 and 1200x500 viewports: all 16 cases pass.
Full-page narrow Firefox dark and desktop Chromium light images were inspected;
all mounts and the focused following control remain visible without nested clipping.
+78
View File
@@ -0,0 +1,78 @@
import { firefox, chromium } from "@playwright/test";
import { createServer } from "../../../frontend-modern/node_modules/vite/dist/node/index.js";
import assert from "node:assert/strict";
process.chdir("frontend-modern");
const server = await createServer({
root: ".",
configFile: "vite.config.ts",
server: { port: 18791 },
});
await server.listen();
try {
for (const engine of [firefox, chromium]) {
const browser = await engine.launch();
try {
for (const width of [600, 1200])
for (const theme of ["light", "dark"])
for (const count of [2, 24]) {
const page = await browser.newPage({
viewport: { width, height: 500 },
});
await page.goto(
`http://127.0.0.1:18791/qualification/disks/?theme=${theme}&count=${count}`,
);
const last = page.getByTitle(`/mnt/disk-${count - 1}`, {
exact: true,
});
await last.waitFor();
const geometry = await last.evaluate((el) => {
const list = el.parentElement.parentElement.parentElement;
const style = getComputedStyle(list);
return {
height: list.clientHeight,
scrollHeight: list.scrollHeight,
overflow: style.overflowY,
maxHeight: style.maxHeight,
};
});
console.log(
JSON.stringify({
browser: browser.version(),
theme,
width,
count,
...geometry,
}),
);
assert.equal(
geometry.height,
geometry.scrollHeight,
"mounts must not be clipped inside a nested scroller",
);
await page.getByRole("button", { name: "Before disks" }).focus();
await page.keyboard.press("End");
await page.waitForTimeout(300);
await page.keyboard.press("Tab");
assert.equal(
await page
.getByRole("button", { name: "After disks" })
.evaluate((el) => el === document.activeElement),
true,
);
const box = await last.boundingBox();
assert.ok(
box && box.y >= 0 && box.y + box.height <= 500,
"last mount reachable through outer scrolling",
);
if (process.env.DISK_SCREENSHOT_DIR && count === 24) {
await page.screenshot({path: `${process.env.DISK_SCREENSHOT_DIR}/${engine.name()}-${theme}-${width}.png`, fullPage:true});
}
await page.close();
}
} finally {
await browser.close();
}
}
} finally {
await server.close();
}