This commit is contained in:
Mick Grove 2026-01-02 12:49:58 -08:00
commit 08cccfd6ef
8 changed files with 135 additions and 42 deletions

View file

@ -23,7 +23,6 @@ jobs:
toolchain: ${{ env.RUST_TOOLCHAIN }}
profile: minimal
override: true
- uses: swatinem/rust-cache@v2
- name: Build (Makefile linux-arm64)
run: make ubuntu-arm64
- name: Run tests

View file

@ -79,8 +79,6 @@ jobs:
profile: minimal
override: true
- uses: swatinem/rust-cache@v2
- name: Install packaging tools
run: cargo install cargo-deb cargo-generate-rpm

View file

@ -66,7 +66,10 @@ Explore Kingfishers built-in report viewer and its `--access-map`, which can
kingfisher scan /path/to/scan --access-map --view-report
```
![alt text](docs/kingfisher-usage-access-map.gif)
![alt text](docs/kingfisher-usage-access-map-01.gif)
**Click to view video**
[![Demo](docs/demos/findings-thumbnail.png)](docs/kingfisher-usage-access-map-02.mp4)
# Table of Contents

View file

@ -137,3 +137,6 @@ dist
# SvelteKit build / generate output
.svelte-kit
pw-out/*

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

View file

@ -1,28 +0,0 @@
GIF_IN="../kingfisher-usage-access-map-01.gif"
WEBM_IN="pw-out/066d10b5ae5d3603dacd69417a8227c6.webm"
OUT_GIF="../kingfisher-usage-access-map-01+accessmap.gif"
# 1) Normalize GIF -> MP4 (H.264, fixed fps/size)
ffmpeg -y -i "$GIF_IN" \
-vf "fps=12,scale=960:-2:flags=lanczos" \
-an -c:v libx264 -pix_fmt yuv420p -crf 18 -preset veryfast \
gif_part.mp4
# 2) Normalize WEBM -> MP4 (same settings)
ffmpeg -y -i "$WEBM_IN" \
-vf "fps=12,scale=960:-2:flags=lanczos" \
-an -c:v libx264 -pix_fmt yuv420p -crf 18 -preset veryfast \
webm_part.mp4
# 3) Concatenate via filter (video-only; reliable)
ffmpeg -y -i gif_part.mp4 -i webm_part.mp4 \
-filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[v]" \
-map "[v]" -c:v libx264 -pix_fmt yuv420p -crf 18 -preset veryfast \
combined.mp4
# 4) Convert combined MP4 -> GIF (single palette across whole thing)
ffmpeg -y -i combined.mp4 \
-vf "fps=12,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=256[p];[s1][p]paletteuse=dither=bayer" \
"$OUT_GIF"
echo "Wrote: $OUT_GIF"

View file

@ -6,24 +6,142 @@ fs.mkdirSync(outDir, { recursive: true });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const cursorOverlayScript = `
(() => {
const style = document.createElement('style');
style.textContent = \`
#__pw_cursor {
position: fixed;
top: 0; left: 0;
width: 18px; height: 18px;
transform: translate(-100px, -100px);
z-index: 2147483647;
pointer-events: none;
}
#__pw_cursor svg { width: 18px; height: 18px; }
#__pw_cursor .dot {
fill: rgba(255,255,255,0.9);
stroke: rgba(0,0,0,0.85);
stroke-width: 2;
}
#__pw_click {
position: fixed;
width: 8px; height: 8px;
border-radius: 50%;
transform: translate(-100px, -100px);
z-index: 2147483646;
pointer-events: none;
opacity: 0;
border: 2px solid rgba(0,0,0,0.6);
}
\`;
document.documentElement.appendChild(style);
const cursor = document.createElement('div');
cursor.id = '__pw_cursor';
cursor.innerHTML = \`
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle class="dot" cx="12" cy="12" r="6"></circle>
</svg>
\`;
document.documentElement.appendChild(cursor);
const click = document.createElement('div');
click.id = '__pw_click';
document.documentElement.appendChild(click);
let x = -100, y = -100;
const move = (nx, ny) => {
x = nx; y = ny;
cursor.style.transform = \`translate(\${x}px, \${y}px)\`;
click.style.transform = \`translate(\${x}px, \${y}px)\`;
};
window.addEventListener('pointermove', (e) => move(e.clientX, e.clientY), { passive: true });
window.addEventListener('mousemove', (e) => move(e.clientX, e.clientY), { passive: true });
window.addEventListener('pointerdown', () => {
click.style.transition = 'none';
click.style.opacity = '0.9';
click.style.width = '8px';
click.style.height = '8px';
requestAnimationFrame(() => {
click.style.transition = 'all 250ms ease-out';
click.style.opacity = '0';
click.style.width = '28px';
click.style.height = '28px';
});
}, { passive: true });
})();
`;
// Converts native <select> dropdowns into in-page listboxes while focused/clicked,
// so the “dropdown” actually appears in the recorded page surface.
const selectListboxScript = `
(() => {
const enhance = (sel) => {
if (sel.__pwEnhanced) return;
sel.__pwEnhanced = true;
const originalSize = sel.getAttribute('size');
const open = () => {
// Prevent the native popup and instead expand in-page
const n = Math.min(Math.max(sel.options.length, 2), 12);
sel.setAttribute('size', String(n));
sel.style.background = sel.style.background || 'white';
};
const close = () => {
if (originalSize === null) sel.removeAttribute('size');
else sel.setAttribute('size', originalSize);
};
sel.addEventListener('mousedown', (e) => {
// Stop native dropdown UI
e.preventDefault();
open();
sel.focus();
});
sel.addEventListener('blur', () => close());
sel.addEventListener('change', () => close());
sel.addEventListener('keydown', (e) => {
if (e.key === 'Escape' || e.key === 'Enter') close();
});
};
const scan = () => document.querySelectorAll('select').forEach(enhance);
// initial + dynamic pages
scan();
new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true });
})();
`;
(async () => {
const browser = await chromium.launch({ headless: false });
const browser = await chromium.launch({
headless: false,
// Optional: keeps it from throttling when not focused
args: ["--disable-renderer-backgrounding", "--disable-background-timer-throttling"],
});
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
recordVideo: { dir: outDir, size: { width: 1280, height: 720 } },
viewport: { width: 1280, height: 1024 },
recordVideo: { dir: outDir, size: { width: 1280, height: 1024 } },
});
// Make sure overlays exist before any page scripts run
await context.addInitScript(cursorOverlayScript);
await context.addInitScript(selectListboxScript);
const page = await context.newPage();
await page.goto("http://127.0.0.1:7890", { waitUntil: "networkidle" });
await sleep(25000);
// networkidle is often a trap for SPAs; use domcontentloaded for recording
await page.goto("http://127.0.0.1:7890", { waitUntil: "domcontentloaded" });
// await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
// await sleep(800);
// await page.evaluate(() => window.scrollTo(0, 0));
// await sleep(800);
// Give yourself time to interact manually
await sleep(45000);
await context.close(); // finalizes video
await browser.close();

Binary file not shown.