Scaling (the project lead, 7 October 2026: "the miner needs some scaling so more is visible in the initial window"): the window opens at about 80 percent of the screen's work area, capped at 1440 by 900 on a display up to 1920 wide and scaled up with the display beyond that, remembered per machine once the user resizes (the Mac window's frame autosave name; the Windows host's HKCU Software/Igneum/Miner WindowW and WindowH, written on WM_EXITSIZEMOVE and used while they fit the work area); the Overview's vertical rhythm tightened so the rate band, the big button, the ladder strip, the chain card and the node card's first row sit above the fold at 1280 by 800 (gaps, paddings and the scene's height 220 to 184 px; no type below 13 px; the live scene and the GPU marks keep their look); fold.test.mjs measures the Overview's key elements inside the viewport at 1280 by 800, 1440 by 900 and 1920 by 1080 and the first object of Cards, Earnings and Prove, with Playwright on build-2 (known-failed on 0.3.21: the chain card ended at 836 and the node row at 900 of 800), skipping where there is no Playwright env; tools/ui-mock/fold-measure.cjs prints the numbers; captures at 1280 by 800 and 1920 by 1080
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
eda005504c
commit
54f2599a36
10 changed files with 148 additions and 5 deletions
|
|
@ -854,3 +854,22 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
|||
#bc-card{display:flex;align-items:center;min-width:0}
|
||||
#bc-card>span{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
||||
.help.ask-cur{color:var(--ember)}
|
||||
|
||||
/* scaling-21 (the project lead, 7 October 2026: "the miner needs some scaling so more is visible in the initial window"): the
|
||||
Overview's vertical rhythm tightened so the rate band, the big button, the ladder strip, the chain card and the node
|
||||
card's first row sit above the fold at 1280 by 800 (fold.test.mjs measures it on build-2 at three viewports). Same
|
||||
proportions, no type below 13 px, the live scene and the GPU marks keep their look; only paddings, gaps and the
|
||||
scene's height move. The other pages already fit their first object at 1280 by 800 and stay as they are. */
|
||||
#page-overview{gap:var(--s-3)}
|
||||
#page-overview .hero-row{gap:var(--s-2)}
|
||||
#page-overview .totals{padding:16px 22px 14px}
|
||||
#page-overview .tot .k{margin-top:2px}
|
||||
#page-overview .toggle-big{min-height:54px;padding:9px 20px}
|
||||
#page-overview .ladder-strip{padding:10px 16px}
|
||||
#page-overview .ls-rungs{margin-bottom:6px}
|
||||
#page-overview .dag-card{padding:16px 18px 14px}
|
||||
#page-overview .dag-card .card-head{margin-bottom:8px}
|
||||
#page-overview #dag-live{height:184px}
|
||||
#page-overview .legend{margin-top:8px}
|
||||
#page-overview .wait-card{padding:12px 20px}
|
||||
#page-overview .node-card{padding-top:18px}
|
||||
|
|
|
|||
52
app/igneum-app/ui/fold.test.mjs
Normal file
52
app/igneum-app/ui/fold.test.mjs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// node --test app/igneum-app/ui/fold.test.mjs (scaling-21, the project lead, 7 October 2026: "the miner needs some scaling so
|
||||
// more is visible in the initial window"). Measures the key elements of each page inside the viewport on the mock with
|
||||
// Playwright's Chromium, at 1280 by 800, 1440 by 900 and 1920 by 1080: the Overview's rate band, big button, ladder
|
||||
// strip and chain card must end above the fold, and the node card's first row must start above it; Cards shows its
|
||||
// first card row, Earnings its headline card, Prove its switch. Runs where the Playwright env is set (build-2:
|
||||
// `. /srv/builds/_bin/overlap/env.sh`), skips elsewhere: the Mac runs no Playwright suite (CLAUDE.md, 7 October 2026).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
const here = dirname(fileURLToPath(import.meta.url)), root = join(here, '../../..');
|
||||
const require = createRequire(import.meta.url);
|
||||
let chromium = null;
|
||||
try { if (process.env.IGNEUM_PLAYWRIGHT_DIR) chromium = require(join(process.env.IGNEUM_PLAYWRIGHT_DIR, 'node_modules/playwright')).chromium; } catch (e) { chromium = null; }
|
||||
const SIZES = [[1280, 800], [1440, 900], [1920, 1080]];
|
||||
const ROW = 48; // the first row of a card: its head line with the padding above it
|
||||
|
||||
test('the first screen: the Overview above the fold at 1280 by 800, 1440 by 900 and 1920 by 1080; Cards, Earnings and Prove show their first object', { skip: chromium ? false : 'no Playwright env (IGNEUM_PLAYWRIGHT_DIR); runs on build-2' }, async () => {
|
||||
const port = 4480 + Math.floor(Math.random() * 400);
|
||||
const mock = spawn(process.execPath, [join(root, 'tools/ui-mock/server.mjs'), String(port)], { stdio: 'ignore' });
|
||||
try {
|
||||
for (let i = 0; i < 40; i++) { try { const r = await fetch(`http://127.0.0.1:${port}/t/mock/api/state`); if (r.ok) break; } catch (e) { } await new Promise((r) => setTimeout(r, 250)); }
|
||||
const browser = await chromium.launch();
|
||||
const bad = [];
|
||||
const rect = (page, sel) => page.evaluate((s) => { const el = document.querySelector(s); if (!el) return null; const r = el.getBoundingClientRect(); return { top: Math.round(r.top), bottom: Math.round(r.bottom), inner: window.innerHeight }; }, sel);
|
||||
for (const [w, h] of SIZES) {
|
||||
const page = await browser.newPage({ viewport: { width: w, height: h }, reducedMotion: 'reduce', colorScheme: 'dark' });
|
||||
const open = async (scenario, p) => { await page.goto(`http://127.0.0.1:${port}/t/mock/?scenario=${scenario}&page=${p}&theme=dark`, { waitUntil: 'load' }); await page.waitForTimeout(1200); };
|
||||
const fits = async (sel, label, rowOnly) => { const r = await rect(page, sel); if (!r) { bad.push(`${w}x${h} ${label}: not found`); return; } const end = rowOnly ? r.top + ROW : r.bottom; if (!(r.top >= 0 && end <= r.inner)) bad.push(`${w}x${h} ${label}: ${rowOnly ? 'row ends at ' + end : 'ends at ' + r.bottom} of ${r.inner}`); };
|
||||
await open('live', 'overview');
|
||||
await fits('#totals', 'Overview rate band'); await fits('#btn-toggle', 'Overview big button'); await fits('#ladder-strip', 'Overview ladder strip'); await fits('#dag-card', 'Overview chain card'); await fits('#node-card', 'Overview node card first row', true);
|
||||
if (h >= 1080) await fits('#node-card', 'Overview node card whole at 1080');
|
||||
await open('rig', 'cards'); await fits('#tune-card', 'Cards tune strip'); await fits('#d-cards .gpu-line:first-child', 'Cards first card row');
|
||||
await open('ladder', 'earnings'); await fits('.earn', 'Earnings headline card');
|
||||
await open('ladder', 'prove'); await fits('#s-prove', 'Prove switch');
|
||||
await page.close();
|
||||
}
|
||||
await browser.close();
|
||||
assert.deepEqual(bad, [], 'below the fold:\n' + bad.join('\n'));
|
||||
} finally { mock.kill(); }
|
||||
});
|
||||
|
||||
test('the type never drops below 13 px on the Overview density pass: no font-size under 13px inside the #page-overview rules', async () => {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const css = readFileSync(join(here, 'app.css'), 'utf8');
|
||||
const at = css.indexOf('/* scaling-21');
|
||||
assert.ok(at >= 0, 'the density block exists');
|
||||
const block = css.slice(at);
|
||||
for (const m of block.matchAll(/font-size:\s*(\d+(?:\.\d+)?)px/g)) assert.ok(parseFloat(m[1]) >= 13, 'font-size ' + m[1] + 'px in the density block');
|
||||
});
|
||||
|
|
@ -109,7 +109,13 @@ final class App: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDele
|
|||
}
|
||||
|
||||
func buildWindow() {
|
||||
let size = snapshotPath == nil ? NSSize(width: 1120, height: 780) : snapshotSize
|
||||
// scaling-21 (7 October 2026): the window opens at about 80 percent of the screen's work area, capped at 1440 by 900
|
||||
// on a display up to 1920 wide and scaled up with the display beyond that; once the user resizes, the frame is
|
||||
// remembered per machine (the frame autosave name) and the computed size is only the first-run default
|
||||
let work = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1440, height: 900)
|
||||
let scale = max(1.0, work.width / 1920.0)
|
||||
let opening = NSSize(width: max(900, min(work.width * 0.8, 1440 * scale)), height: max(600, min(work.height * 0.8, 900 * scale)))
|
||||
let size = snapshotPath == nil ? opening : snapshotSize
|
||||
window = NSWindow(contentRect: NSRect(origin: .zero, size: size), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false)
|
||||
window.title = "Igneum Miner"
|
||||
window.titlebarAppearsTransparent = true
|
||||
|
|
@ -117,7 +123,10 @@ final class App: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDele
|
|||
window.isMovableByWindowBackground = true
|
||||
window.backgroundColor = obsidian
|
||||
window.minSize = NSSize(width: 900, height: 600)
|
||||
window.center()
|
||||
if snapshotPath == nil {
|
||||
if !window.setFrameUsingName("IgneumMinerMain") { window.center() }
|
||||
window.setFrameAutosaveName("IgneumMinerMain")
|
||||
} else { window.center() }
|
||||
window.delegate = self
|
||||
window.isReleasedWhenClosed = false
|
||||
window.appearance = NSAppearance(named: .darkAqua)
|
||||
|
|
|
|||
|
|
@ -341,11 +341,49 @@ static void beginQuit() {
|
|||
}
|
||||
}
|
||||
|
||||
// scaling-21 (7 October 2026, the project lead: "the miner needs some scaling so more is visible in the initial window"): the window
|
||||
// opens at about 80 percent of the work area, capped at 1440 by 900 on a display up to 1920 wide and scaled up with the
|
||||
// display beyond that; once the user resizes, the size is remembered per machine under HKCU\Software\Igneum\Miner
|
||||
// (WindowW, WindowH) and used on the next start while it still fits the work area. The Mac window does the same
|
||||
// through its frame autosave name (app/mac/IgneumMiner.swift).
|
||||
static const wchar_t* kSizeKey = L"Software\\Igneum\\Miner";
|
||||
static void saveWindowSize(HWND hwnd) {
|
||||
if (IsIconic(hwnd) || IsZoomed(hwnd)) return;
|
||||
RECT r; if (!GetWindowRect(hwnd, &r)) return;
|
||||
DWORD w = (DWORD)(r.right - r.left), h = (DWORD)(r.bottom - r.top);
|
||||
if (w < 900 || h < 600) return;
|
||||
HKEY k; if (RegCreateKeyExW(HKEY_CURRENT_USER, kSizeKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &k, nullptr) != ERROR_SUCCESS) return;
|
||||
RegSetValueExW(k, L"WindowW", 0, REG_DWORD, (const BYTE*)&w, sizeof(w));
|
||||
RegSetValueExW(k, L"WindowH", 0, REG_DWORD, (const BYTE*)&h, sizeof(h));
|
||||
RegCloseKey(k);
|
||||
}
|
||||
static void openingSize(int& w, int& h, int& sx, int& sy) {
|
||||
RECT work = { 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN) };
|
||||
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
|
||||
int ww = work.right - work.left, wh = work.bottom - work.top;
|
||||
double scale = ww > 1920 ? ww / 1920.0 : 1.0;
|
||||
double cw = 1440.0 * scale, ch = 900.0 * scale, aw = ww * 0.8, ah = wh * 0.8;
|
||||
w = (int)(aw < cw ? aw : cw); h = (int)(ah < ch ? ah : ch);
|
||||
if (w < 900) w = ww < 900 ? ww : 900;
|
||||
if (h < 600) h = wh < 600 ? wh : 600;
|
||||
HKEY k; DWORD sw = 0, sh = 0, n = sizeof(DWORD);
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, kSizeKey, 0, KEY_QUERY_VALUE, &k) == ERROR_SUCCESS) {
|
||||
RegQueryValueExW(k, L"WindowW", nullptr, nullptr, (LPBYTE)&sw, &n); n = sizeof(DWORD);
|
||||
RegQueryValueExW(k, L"WindowH", nullptr, nullptr, (LPBYTE)&sh, &n);
|
||||
RegCloseKey(k);
|
||||
if (sw >= 900 && sh >= 600 && (int)sw <= ww && (int)sh <= wh) { w = (int)sw; h = (int)sh; }
|
||||
}
|
||||
sx = work.left + (ww - w) / 2; sy = work.top + (wh - h) / 2;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
switch (msg) {
|
||||
case WM_SIZE:
|
||||
if (g_controller) { RECT rc; GetClientRect(hwnd, &rc); g_controller->put_Bounds(rc); }
|
||||
return 0;
|
||||
case WM_EXITSIZEMOVE:
|
||||
saveWindowSize(hwnd);
|
||||
return 0;
|
||||
case WM_GETMINMAXINFO: // the dashboard lays out from 900 x 600 up (app/igneum-app/ui); the Mac window says the same
|
||||
((MINMAXINFO*)lp)->ptMinTrackSize.x = 900; ((MINMAXINFO*)lp)->ptMinTrackSize.y = 600;
|
||||
return 0;
|
||||
|
|
@ -512,8 +550,7 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) {
|
|||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
wc.hbrBackground = CreateSolidBrush(RGB(12, 12, 14));
|
||||
RegisterClassW(&wc);
|
||||
int w = 1120, h = 820;
|
||||
int sx = (GetSystemMetrics(SM_CXSCREEN) - w) / 2, sy = (GetSystemMetrics(SM_CYSCREEN) - h) / 2;
|
||||
int w, h, sx, sy; openingSize(w, h, sx, sy);
|
||||
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"Igneum Miner", WS_OVERLAPPEDWINDOW, sx, sy, w, h, nullptr, nullptr, hInst, nullptr);
|
||||
ShowWindow(g_hwnd, SW_SHOW);
|
||||
|
||||
|
|
|
|||
3
docs/plans/scaling-shots.txt
Normal file
3
docs/plans/scaling-shots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
01-overview-1280x800-dark.png scenario=live&page=overview&theme=dark 800
|
||||
02-overview-1920x1080-dark.png scenario=live&page=overview&theme=dark 1080
|
||||
03-cards-1280x800-dark.png scenario=rig&page=cards&theme=dark 800
|
||||
BIN
docs/plans/scaling-shots/01-overview-1280x800-dark.png
Normal file
BIN
docs/plans/scaling-shots/01-overview-1280x800-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 454 KiB |
BIN
docs/plans/scaling-shots/02-overview-1920x1080-dark.png
Normal file
BIN
docs/plans/scaling-shots/02-overview-1920x1080-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 558 KiB |
BIN
docs/plans/scaling-shots/03-cards-1280x800-dark.png
Normal file
BIN
docs/plans/scaling-shots/03-cards-1280x800-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
|
|
@ -106,7 +106,7 @@ tree_checks() {
|
|||
run "interface bundle publisher self-test (pack, and sign when the signer is built)" node tools/ui-ota/publish.mjs --self-test
|
||||
run "heat gate reader self-test (a known hold passes, a known drift fails)" node tools/heat-gate.mjs --self-test
|
||||
run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs
|
||||
run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs app/igneum-app/ui/ui-ota.test.mjs
|
||||
run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs app/igneum-app/ui/ui-ota.test.mjs app/igneum-app/ui/fold.test.mjs
|
||||
run "the Intel rotate-fold rewrite: Intel gets the shift form, AMD and NVIDIA untouched (proto-opencl/test_intel_rotr.c)" bash -c 'cc -std=c99 -Wall -Wextra -o "${TMPDIR:-/tmp}/test_intel_rotr.$$" proto-opencl/test_intel_rotr.c && "${TMPDIR:-/tmp}/test_intel_rotr.$$"; s=$?; rm -f "${TMPDIR:-/tmp}/test_intel_rotr.$$"; exit $s'
|
||||
run "no secret file names and no 64-hex secrets in the tree" bash -c 'bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh'
|
||||
run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs
|
||||
|
|
|
|||
23
tools/ui-mock/fold-measure.cjs
Normal file
23
tools/ui-mock/fold-measure.cjs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Measures where the key elements of a page end inside the viewport, on the mock, with Playwright's Chromium (build-2's
|
||||
// overlap env: NODE_PATH and IGNEUM_PLAYWRIGHT_DIR from /srv/builds/_bin/overlap/env.sh). Prints one JSON line per
|
||||
// viewport and page: { w, h, page, items: { id: { top, bottom, fits } } }.
|
||||
// node tools/ui-mock/fold-measure.cjs <mock base url> [pages...]
|
||||
const base = process.argv[2]; const pages = process.argv.slice(3).length ? process.argv.slice(3) : ['overview', 'cards', 'earnings', 'prove'];
|
||||
const { chromium } = require('playwright');
|
||||
const SIZES = [[1280, 800], [1440, 900], [1920, 1080]];
|
||||
const KEYS = { overview: ['#totals', '#btn-toggle', '#ladder-strip', '#dag-card', '#node-card .card-head, #node-card h3, #node-card'], cards: ['#tune-card, .tune-strip', '#d-cards .gpu-line:first-child'], earnings: ['.earn', '#ladder-card .card-head'], prove: ['#page-prove .card:first-child', '#s-prove'] };
|
||||
(async () => {
|
||||
const dir = process.env.IGNEUM_PLAYWRIGHT_DIR; const exe = process.env.PLAYWRIGHT_CHROMIUM;
|
||||
const browser = await chromium.launch(exe ? { executablePath: exe } : {});
|
||||
for (const [w, h] of SIZES) {
|
||||
const page = await browser.newPage({ viewport: { width: w, height: h }, reducedMotion: 'reduce', colorScheme: 'dark' });
|
||||
for (const p of pages) {
|
||||
await page.goto(`${base}/t/mock/?scenario=${p === 'cards' ? 'rig' : 'live'}&page=${p}&theme=dark`, { waitUntil: 'load' });
|
||||
await page.waitForTimeout(1500);
|
||||
const items = await page.evaluate((keys) => { const out = {}; for (const k of keys) { const el = document.querySelector(k); if (!el) { out[k] = null; continue; } const r = el.getBoundingClientRect(); out[k] = { top: Math.round(r.top), bottom: Math.round(r.bottom), fits: r.bottom <= window.innerHeight && r.top >= 0 }; } out.scrollHeight = document.querySelector('main').scrollHeight; out.inner = window.innerHeight; return out; }, KEYS[p]);
|
||||
console.log(JSON.stringify({ w, h, page: p, items }));
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
await browser.close();
|
||||
})().catch((e) => { console.error(e); process.exit(1); });
|
||||
Loading…
Reference in a new issue