Merge release-0.3.21 826dbf76 into release-0.3.22 (the 0.3.21 app tree as shipped over c4459193)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
41caea6a89
12 changed files with 156 additions and 11 deletions
12
.github/workflows/windows.yml
vendored
12
.github/workflows/windows.yml
vendored
|
|
@ -216,11 +216,13 @@ jobs:
|
|||
run: |
|
||||
$payload = Resolve-Path 'packaging\windows\igneum-windows-app'
|
||||
function Run-Capture([string]$exe, [string]$flag) {
|
||||
$out = Join-Path $env:RUNNER_TEMP ('smoke-' + [IO.Path]::GetRandomFileName() + '.txt')
|
||||
$p = Start-Process -FilePath $exe -ArgumentList $flag -Wait -NoNewWindow -PassThru -RedirectStandardOutput $out
|
||||
$text = if (Test-Path $out) { (Get-Content $out -Raw) } else { '' }
|
||||
Write-Host ("{0} {1} -> exit {2}: {3}" -f (Split-Path -Leaf $exe), $flag, $p.ExitCode, $text.Trim())
|
||||
if ($p.ExitCode -ne 0) { throw "$exe $flag exited $($p.ExitCode)" }
|
||||
# a direct call, not Start-Process -Wait -PassThru: a program that prints and exits at once raced the cmdlet
|
||||
# ("Cannot process request because the process has exited", 7 October 2026, the 0.3.21 run) and the read-back
|
||||
# was lost; the host program is a windows-subsystem exe, so its text comes through the same pipe
|
||||
$text = (& $exe $flag 2>&1 | Out-String)
|
||||
$code = $LASTEXITCODE
|
||||
Write-Host ("{0} {1} -> exit {2}: {3}" -f (Split-Path -Leaf $exe), $flag, $code, $text.Trim())
|
||||
if ($code -ne 0) { throw "$exe $flag exited $code" }
|
||||
return $text
|
||||
}
|
||||
$v = $env:APP_VERSION
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -1 +1 @@
|
|||
96161037f375a092b47ad998f663bac5733c776d
|
||||
c4459193ff5ada38df21f900c91026d3f07c963f
|
||||
|
|
|
|||
|
|
@ -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