mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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={[]} />);
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user