mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 04:06:59 +00:00
feat(security): per-image scroll + retention cap in scan history (#1231)
* feat(security): per-image scroll + retention cap in scan history Long scan histories for hot images used to monopolise the Scan history sheet: a single image with dozens of scans pushed every other image off screen, and the underlying vulnerability_scans table grew without bound. Each image group's table now renders inside its own ScrollArea capped at max-h-64 (~6 rows visible) so a busy image scrolls independently while the list of images stays navigable. A new global setting scan_history_per_image_limit (default 50, min 5, max 1000) backs both a window-function query that caps the response per image_ref and a prune step that runs on the existing MonitorService cleanup tick. The response now carries cappedImageRefs + perImageLimit so the UI can render a "Capped at N · older scans pruned" hint on groups sitting at the ceiling without a second settings round-trip. Single-image deep-dive (imageRef query param) bypasses the cap so a user clicking into one image can still see its full history. The prune uses self-contained subqueries to avoid SQLITE_MAX_VARIABLE_NUMBER issues on first-run installs with large backlogs, and explicitly deletes child rows from vulnerability_details, secret_findings, and misconfig_findings inside a transaction since FK cascade is not enabled at the connection level. Settings → Developer → Data retention gains a "Scan history per image" field. * fix(security): skip searchDraft debounce on mount to stop page-reset race The searchDraft debounce useEffect fires once on initial mount with the unchanged value and, 300ms later, unconditionally calls setPage(0). When a user (or a test) paginates inside that 300ms window, the pending debounce silently undoes the page advance. CI surfaced this as a flaky 3rd fetch in the "advances offset when the user pages forward" test once the per-image cap work added enough state-update overhead to push the click past the 300ms threshold on the slower Linux jsdom run. Track searchDraft with a ref and exit the effect when the value has not actually changed, so the debounce only runs in response to real user typing.
This commit is contained in:
@@ -86,11 +86,19 @@ function scan(overrides: Partial<VulnerabilityScan> = {}): VulnerabilityScan {
|
||||
};
|
||||
}
|
||||
|
||||
function listResponse(items: VulnerabilityScan[], total?: number): Response {
|
||||
function listResponse(
|
||||
items: VulnerabilityScan[],
|
||||
opts: { total?: number; cappedImageRefs?: string[]; perImageLimit?: number } = {},
|
||||
): Response {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ items, total: total ?? items.length }),
|
||||
json: async () => ({
|
||||
items,
|
||||
total: opts.total ?? items.length,
|
||||
cappedImageRefs: opts.cappedImageRefs ?? [],
|
||||
perImageLimit: opts.perImageLimit ?? 50,
|
||||
}),
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
@@ -116,7 +124,7 @@ describe('SecurityHistoryView', () => {
|
||||
});
|
||||
|
||||
it('advances offset when the user pages forward', async () => {
|
||||
mockedFetch.mockResolvedValue(listResponse([scan()], 250));
|
||||
mockedFetch.mockResolvedValue(listResponse([scan()], { total: 250 }));
|
||||
const user = userEvent.setup();
|
||||
render(<SecurityHistoryView open onClose={vi.fn()} />);
|
||||
|
||||
@@ -228,4 +236,21 @@ describe('SecurityHistoryView', () => {
|
||||
|
||||
expect(screen.getByRole('button', { name: /Compare \(2\/2\)/ })).toBeEnabled();
|
||||
});
|
||||
|
||||
it('renders the cap hint only for images flagged in cappedImageRefs', async () => {
|
||||
mockedFetch.mockResolvedValue(
|
||||
listResponse(
|
||||
[
|
||||
scan({ id: 1, image_ref: 'hot:latest', scanned_at: 1000 }),
|
||||
scan({ id: 2, image_ref: 'cool:latest', scanned_at: 2000 }),
|
||||
],
|
||||
{ cappedImageRefs: ['hot:latest'], perImageLimit: 50 },
|
||||
),
|
||||
);
|
||||
render(<SecurityHistoryView open onClose={vi.fn()} />);
|
||||
|
||||
const cappedHint = await screen.findByText(/Capped at 50 . older scans pruned/);
|
||||
expect(cappedHint).toBeInTheDocument();
|
||||
expect(screen.queryAllByText(/Capped at 50/)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user