refactor: Update UI components for consistency and improve layout

This commit is contained in:
elipeter 2026-05-06 04:38:04 -04:00
parent da619171cf
commit 77be7f10d9
74 changed files with 3186 additions and 618 deletions

View file

@ -53,11 +53,12 @@
* docs/serve-rules.png
* docs/serve-config.png
*/
import { execFileSync } from 'node:child_process';
import { execFileSync, spawn } from 'node:child_process';
import {
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
rmSync,
unlinkSync,
writeFileSync,
@ -73,15 +74,16 @@ const NYX_BIN = process.env.NYX_BIN || '/Users/elipeter/nyx/target/release/ny
// Sibling marketing site that mirrors a small subset of these assets.
// Set NYXSCAN_DIR=skip to disable the mirror step.
const NYXSCAN_DIR = process.env.NYXSCAN_DIR || '/Users/elipeter/nyxscan.dev/assets/screenshots';
const VIEW = { width: 1440, height: 900 };
const VIEW = { width: 1280, height: 960 };
const COLOR_SCHEME = 'light';
const args = new Set(process.argv.slice(2));
const wantStills = args.has('--stills') || args.has('--all');
const wantGif = args.has('--gif') || args.has('--all');
const wantCli = args.has('--cli') || args.has('--all');
if (!wantStills && !wantGif && !wantCli) {
console.error('usage: capture-screenshots.mjs [--stills|--gif|--cli|--all]');
const wantCombo = args.has('--combo') || args.has('--all');
if (!wantStills && !wantGif && !wantCli && !wantCombo) {
console.error('usage: capture-screenshots.mjs [--stills|--gif|--cli|--combo|--all]');
process.exit(2);
}
@ -337,9 +339,9 @@ async function captureGifFrames(page) {
.waitForSelector('.health-score-card, [class*="health"]', { timeout: 10_000 })
.catch(() => {});
await sleep(1800);
await page.evaluate(() => window.scrollBy({ top: 360, behavior: 'smooth' }));
await page.evaluate(() => window.scrollBy({ top: 480, behavior: 'smooth' }));
await sleep(1500);
await page.evaluate(() => window.scrollBy({ top: 360, behavior: 'smooth' }));
await page.evaluate(() => window.scrollBy({ top: 480, behavior: 'smooth' }));
await sleep(1500);
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'smooth' }));
await sleep(800);
@ -394,18 +396,61 @@ async function captureGifFrames(page) {
await sleep(1500);
}
// Combo GIF browser storyboard — data already present from VHS scan phase -----
async function captureGifFramesCombo(page) {
console.error('[combo/gif] scene 1: overview with scan data');
await page.goto(URL_BASE + '/');
await page
.waitForSelector('.health-score-card, [class*="health"]', { timeout: 15_000 })
.catch(() => {});
await sleep(2200);
await page.evaluate(() => window.scrollBy({ top: 480, behavior: 'smooth' }));
await sleep(1500);
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'smooth' }));
await sleep(900);
console.error('[combo/gif] scene 2: findings list');
await page.click('a.nav-link:has-text("Findings"), .sidebar a:has-text("Findings")');
await page.waitForURL('**/findings', { timeout: 10_000 });
await page.waitForSelector('tbody tr', { timeout: 10_000 });
await sleep(1500);
console.error('[combo/gif] scene 3: 5-hop taint finding detail');
const taintRow = await findFirstTaintRow(page);
await taintRow.click();
await page.waitForURL(/\/findings\/\d+/, { timeout: 10_000 });
await sleep(2500);
await page.evaluate(() => window.scrollBy({ top: 480, behavior: 'smooth' }));
await sleep(1600);
await page.evaluate(() => window.scrollBy({ top: 360, behavior: 'smooth' }));
await sleep(1600);
console.error('[combo/gif] scene 4: open Evidence + Analysis Notes');
for (const title of ['Evidence', 'Analysis Notes']) {
const toggle = page.locator(`.section-toggle:has-text("${title}")`).first();
if (await toggle.count()) {
await toggle.scrollIntoViewIfNeeded();
await sleep(500);
await toggle.click();
await sleep(1000);
}
}
await sleep(1200);
}
async function convertWebmToGif(webm, gifOut) {
const palette = '/tmp/nyx-demo-palette.png';
console.error('[gif] generating palette');
execFileSync('ffmpeg', [
'-y', '-ss', '1.0', '-i', webm,
'-vf', 'fps=15,scale=1440:-1:flags=lanczos,palettegen',
'-vf', 'fps=15,scale=1280:-1:flags=lanczos,palettegen',
palette,
], { stdio: 'inherit' });
console.error('[gif] palette → gif');
execFileSync('ffmpeg', [
'-y', '-ss', '1.0', '-i', webm, '-i', palette,
'-lavfi', 'fps=15,scale=1440:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle',
'-lavfi', 'fps=15,scale=1280:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle',
gifOut,
], { stdio: 'inherit' });
}
@ -538,6 +583,143 @@ function captureCli() {
// without a much larger fixture; the existing image is left alone.
}
// Combo GIF ------------------------------------------------------------------
// Single GIF: CLI scan (VHS terminal) → hard cut → serve UI (Playwright).
// The VHS portion is a visual recording only — nyx scan (standalone CLI)
// writes to a separate store that nyx serve does not read. After VHS we
// wipe state and trigger a real scan through the serve API so Playwright
// has live data to explore.
async function captureComboGif() {
function wipeState() {
rmSync(join(SCAN_ROOT, '.nyx'), { recursive: true, force: true });
const homeDir = process.env.HOME || '/Users/elipeter';
const sysDbBase = join(homeDir, 'Library/Application Support/nyx/nyx-demo-app.sqlite');
for (const suffix of ['', '-wal', '-shm']) {
try { unlinkSync(sysDbBase + suffix); } catch {}
}
}
// 1. Clean state + write demo.
try { execFileSync('pkill', ['-f', 'nyx serve'], { stdio: 'ignore' }); } catch {}
await sleep(800);
wipeState();
writeDemo('v1');
// 2. VHS: scan → results pause → type nyx serve → see it start.
const cliGifPath = '/tmp/nyx-combo-cli.gif';
const tapePath = '/tmp/nyx-combo.tape';
const tape = [
`Output "${cliGifPath}"`,
'',
'Set Shell "bash"',
'Set FontSize 22',
'Set Width 1280',
'Set Height 960',
'Set Framerate 15',
'Env CLICOLOR_FORCE "1"',
'',
'Sleep 500ms',
`Type "${NYX_BIN} scan ${SCAN_ROOT}"`,
'Sleep 300ms',
'Enter',
'Sleep 1500ms',
`Type "${NYX_BIN} serve --port 9876 --no-browser ${SCAN_ROOT}"`,
'Sleep 300ms',
'Enter',
'Sleep 2000ms',
].join('\n');
writeFileSync(tapePath, tape);
console.error('[combo] recording CLI portion with vhs');
execFileSync(VHS_BIN, [tapePath], { stdio: 'inherit' });
// 3. Wipe state again and start a fresh host serve. The VHS scan wrote
// to standalone storage that nyx serve doesn't read, so we drive a
// real scan through the serve API to populate the browser session.
try { execFileSync('pkill', ['-f', 'nyx serve'], { stdio: 'ignore' }); } catch {}
await sleep(800);
wipeState();
const serveProc = spawn(NYX_BIN, [
'serve', '--port', '9876', '--no-browser', SCAN_ROOT,
], { detached: false, stdio: 'ignore' });
serveProc.unref();
await waitForServer();
const comboToken = await csrfToken();
const comboBefore = await currentScanId();
await startScanViaApi(comboToken);
await waitForScanComplete(comboBefore);
// 4. Playwright: record browser walkthrough against the live scan data.
const videoDir = '/tmp/nyx-combo-video';
if (existsSync(videoDir)) rmSync(videoDir, { recursive: true });
mkdirSync(videoDir, { recursive: true });
const { chromium } = await import('playwright');
const browser = await chromium.launch({ headless: true });
try {
const ctx = await browser.newContext({
viewport: VIEW,
colorScheme: COLOR_SCHEME,
recordVideo: { dir: videoDir, size: VIEW },
});
await ctx.addInitScript(() => {
try { localStorage.setItem('theme', 'light'); } catch {}
});
const page = await ctx.newPage();
await captureGifFramesCombo(page);
await page.close();
await ctx.close();
} finally {
await browser.close();
}
try { execFileSync('pkill', ['-f', 'nyx serve'], { stdio: 'ignore' }); } catch {}
// 5. Find Playwright webm.
const webms = readdirSync(videoDir).filter((f) => f.endsWith('.webm'));
if (!webms.length) throw new Error('[combo] no webm captured for browser portion');
const webmPath = join(videoDir, webms[0]);
// 6. ffmpeg: three-step to avoid OOM from single-pass concat+palettegen.
// Step A: concat VHS gif + browser webm → intermediate webm.
// Step B: generate global palette from intermediate.
// Step C: palette → final GIF.
const comboOut = join(OUT_DIR, 'demo-combo.gif');
const comboIntermediate = '/tmp/nyx-combo-intermediate.mp4';
const comboPalette = '/tmp/nyx-combo-palette.png';
console.error('[combo] step A: concat → intermediate webm');
execFileSync('ffmpeg', [
'-y',
'-ignore_loop', '1', '-r', '15', '-i', cliGifPath,
'-ss', '1.0', '-r', '15', '-i', webmPath,
'-filter_complex',
'[0:v]scale=1280:960:flags=lanczos,fps=15[cli];' +
'[1:v]scale=1280:960:flags=lanczos,fps=15[bro];' +
'[cli][bro]concat=n=2:v=1:a=0[out]',
'-map', '[out]',
'-c:v', 'libx264', '-crf', '28', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p',
comboIntermediate,
], { stdio: 'inherit' });
console.error('[combo] step B: generate palette');
execFileSync('ffmpeg', [
'-y', '-i', comboIntermediate,
'-vf', 'fps=15,palettegen',
'-update', '1', '-frames:v', '1',
comboPalette,
], { stdio: 'inherit' });
console.error('[combo] step C: palette → gif');
execFileSync('ffmpeg', [
'-y', '-i', comboIntermediate, '-i', comboPalette,
'-lavfi', 'fps=15 [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle',
comboOut,
], { stdio: 'inherit' });
console.error(`[combo] wrote ${comboOut}`);
}
// Frame phase ----------------------------------------------------------------
const STILLS_PNGS = [
@ -604,6 +786,7 @@ const NYXSCAN_MIRROR = [
['docs/serve-overview_raw.png', 'overview.png'],
['docs/serve-finding-detail_raw.png', 'finding-detail.png'],
['cli-scan_raw.gif', 'cli-scan.gif'],
['demo-combo.gif', 'demo-combo.gif'],
];
function syncNyxscanDev() {
if (NYXSCAN_DIR === 'skip') return;
@ -711,7 +894,11 @@ async function main() {
captureCli();
}
if (wantStills || wantCli || wantGif) {
if (wantCombo) {
await captureComboGif();
}
if (wantStills || wantCli || wantGif || wantCombo) {
// Frame phase — only frame what was captured this run so that
// already-framed PNGs from prior runs aren't framed again.
// Stills and the GIF use the fixed 1600x992 inner; CLI captures