mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
refactor: Update UI components for consistency and improve layout
This commit is contained in:
parent
da619171cf
commit
77be7f10d9
74 changed files with 3186 additions and 618 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue