mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-26 05:30:18 +00:00
a54ebcc7e2
* feat(fleet): group the Fleet tabs into Observe and Operate, with a layout setting The Fleet tab strip was ten tabs in one row, wider than a 1280px window. The strip now sets the tabs under two labelled groups on one strip: Observe (Overview, Readiness, Map, Docker Labels) and Operate (Snapshots, Blueprints, Routing, Federation, Actions, Secrets). It scrolls sideways when the window is too narrow and keeps the tab you are on in view. Every tab still opens with one click and every tab keeps its value, so deep links and URL sync are unchanged. A new account preference, Settings, Appearance, Fleet, Fleet tab layout, picks how the strip is laid out: Grouped (default), Flat (the previous single row), or Compact (Overview, Readiness, Map, Blueprints and Actions, with the rest under a More menu that names the active hidden tab). It is saved to the account and synced like the other appearance preferences, defaults to Grouped for a document that lacks it, and is cleared with the rest of the preference cache when another account signs in. * refactor(fleet): drop the Observe and Operate labels and the old flat strip The group labels only lengthened the tab row, and the previous single-row layout is no longer needed. The Fleet tab strip is now one row: Overview, Readiness, Map and Docker Labels, a vertical separator, then Snapshots, Blueprints, Routing, Federation, Actions and Secrets. It still scrolls sideways when the window is too narrow and keeps the active tab in view. The Fleet tab layout setting now offers Flat (the default, this row) and Compact (five tabs with the rest under More). The retired grouped value reads as Flat. * test(fleet): raise the cross-tab storage event without the StorageEvent constructor The code quality scan flagged the two-argument StorageEvent calls as superfluous trailing arguments. The tests now build the event from a plain Event carrying the key and the new value, which is all the hook reads.
177 lines
8.3 KiB
JavaScript
177 lines
8.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Enum parity check for the per-user preference documents.
|
|
*
|
|
* The backend Zod schemas (backend/src/routes/userPreferences.ts) and the
|
|
* frontend registries (frontend/src/hooks/use-theme.ts,
|
|
* frontend/src/hooks/use-density.ts, frontend/src/hooks/use-top-nav-mode.ts,
|
|
* frontend/src/hooks/use-top-nav-align.ts,
|
|
* frontend/src/hooks/use-log-chip-color-mode.ts,
|
|
* frontend/src/lib/navigation/appNavRegistry.ts) must accept the same value
|
|
* sets. The backend cannot import frontend sources (tsconfig rootDir is
|
|
* ./src and backend tests participate in production compilation), so this
|
|
* script parses the literal declarations from source text instead.
|
|
*
|
|
* Exits non-zero on any value-set drift. Wired as the `pretest` script of
|
|
* both backend and frontend package.json.
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(__dirname, '..');
|
|
|
|
const read = (rel) => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
|
|
|
const routeSrc = read('backend/src/routes/userPreferences.ts');
|
|
const themeSrc = read('frontend/src/hooks/use-theme.ts');
|
|
const navModeSrc = read('frontend/src/hooks/use-top-nav-mode.ts');
|
|
const navAlignSrc = read('frontend/src/hooks/use-top-nav-align.ts');
|
|
const densitySrc = read('frontend/src/hooks/use-density.ts');
|
|
const logChipSrc = read('frontend/src/hooks/use-log-chip-color-mode.ts');
|
|
const sidebarLayoutSrc = read('frontend/src/hooks/use-sidebar-layout.ts');
|
|
const anatomyLayoutSrc = read('frontend/src/hooks/use-anatomy-layout.ts');
|
|
const fleetTabLayoutSrc = read('frontend/src/hooks/use-fleet-tab-layout.ts');
|
|
const navRegistrySrc = read('frontend/src/lib/navigation/appNavRegistry.ts');
|
|
|
|
let failed = false;
|
|
const fail = (msg) => { console.error(`[preference-parity] FAIL: ${msg}`); failed = true; };
|
|
const ok = (msg) => console.log(`[preference-parity] ok: ${msg}`);
|
|
|
|
/** Extract the quoted members of a `const NAME = [...]` string-array literal. */
|
|
function extractArrayLiteral(src, name) {
|
|
const re = new RegExp(`\\bconst ${name}\\b[^=]*=\\s*\\[([^\\]]*)\\]`, 's');
|
|
const m = src.match(re);
|
|
if (!m) return null;
|
|
const values = [...m[1].matchAll(/'([^']+)'/g)].map((x) => x[1]);
|
|
return values.length > 0 ? values : null;
|
|
}
|
|
|
|
/** Extract the quoted members of a `Set(['a','b'])` literal. */
|
|
function extractSetLiteral(src, name) {
|
|
const re = new RegExp(`${name}\\s*=\\s*new Set\\(\\[([^\\]]*)\\]\\)`, 's');
|
|
const m = src.match(re);
|
|
if (!m) return null;
|
|
const values = [...m[1].matchAll(/'([^']+)'/g)].map((x) => x[1]);
|
|
return values.length > 0 ? values : null;
|
|
}
|
|
|
|
/** Extract the quoted members of a union type `type NAME = 'a' | 'b';`. */
|
|
function extractUnionType(src, name) {
|
|
const re = new RegExp(`(?:export )?type ${name}\\s*=[^;]*;`, 's');
|
|
const m = src.match(re);
|
|
if (!m) return null;
|
|
const values = [...m[0].matchAll(/'([^']+)'/g)].map((x) => x[1]);
|
|
return values.length > 0 ? values : null;
|
|
}
|
|
|
|
/** Extract the ids of registry items carrying `quickLinkEligible: true`.
|
|
* Pairs each quoted `value:` in the registry array with the
|
|
* `quickLinkEligible:` flag in the same item and counts the pairs; the
|
|
* caller fails the comparison when the pair count disagrees with the number
|
|
* of `quickLinkEligible:` flags in the source, so a refactor that renames
|
|
* either property (breaking the pairing) cannot pass vacuously. */
|
|
function extractEligibleIds(src) {
|
|
const items = [...src.matchAll(/value:\s*'([^']+)'[\s\S]*?quickLinkEligible:\s*(true|false)/g)];
|
|
const eligible = [];
|
|
for (const [, value, flag] of items) {
|
|
if (flag === 'true') eligible.push(value);
|
|
}
|
|
return { eligible: eligible.length > 0 ? eligible : null, paired: items.length };
|
|
}
|
|
|
|
function compare(label, backend, frontend) {
|
|
if (!backend || !frontend) {
|
|
fail(`${label}: could not extract declaration (backend=${JSON.stringify(backend)}, frontend=${JSON.stringify(frontend)})`);
|
|
return;
|
|
}
|
|
const b = [...backend].sort().join(',');
|
|
const f = [...frontend].sort().join(',');
|
|
if (b !== f) {
|
|
fail(`${label}: backend [${b}] != frontend [${f}]`);
|
|
return;
|
|
}
|
|
ok(`${label}: ${backend.length} values match`);
|
|
}
|
|
|
|
function compareCount(label, expected, actual) {
|
|
if (expected !== actual) {
|
|
fail(`${label}: expected ${expected} paired declarations, found ${actual} (extractor out of date?)`);
|
|
return;
|
|
}
|
|
ok(`${label}: ${actual} paired declarations extracted`);
|
|
}
|
|
|
|
// Backend declarations (string-array tables).
|
|
const bThemes = extractArrayLiteral(routeSrc, 'THEME_MODES');
|
|
const bAccents = extractArrayLiteral(routeSrc, 'ACCENT_IDS');
|
|
const bUiFonts = extractArrayLiteral(routeSrc, 'UI_FONT_IDS');
|
|
const bMonoFonts = extractArrayLiteral(routeSrc, 'MONO_FONT_IDS');
|
|
const bVisual = extractArrayLiteral(routeSrc, 'VISUAL_STYLES');
|
|
const bHeading = extractArrayLiteral(routeSrc, 'HEADING_STYLES');
|
|
const bChart = extractArrayLiteral(routeSrc, 'CHART_STYLES');
|
|
const bDensities = extractArrayLiteral(routeSrc, 'DENSITIES');
|
|
const bLogChip = extractArrayLiteral(routeSrc, 'LOG_CHIP_COLOR_MODES');
|
|
const bSidebarModes = extractArrayLiteral(routeSrc, 'SIDEBAR_MODES');
|
|
const bAnatomyModes = extractArrayLiteral(routeSrc, 'ANATOMY_MODES');
|
|
const bFleetTabLayouts = extractArrayLiteral(routeSrc, 'FLEET_TAB_LAYOUTS');
|
|
const bNavModes = extractArrayLiteral(routeSrc, 'NAV_MODES');
|
|
const bAligns = extractArrayLiteral(routeSrc, 'NAV_ALIGNS');
|
|
const bQuickLinks = extractArrayLiteral(routeSrc, 'QUICK_LINK_IDS');
|
|
|
|
// Frontend declarations (union types and Set literals).
|
|
const fThemes = extractUnionType(themeSrc, 'ThemeMode');
|
|
const fAccents = extractUnionType(themeSrc, 'AccentId');
|
|
const fUiFonts = extractUnionType(themeSrc, 'UiFont');
|
|
const fMonoFonts = extractUnionType(themeSrc, 'MonoFont');
|
|
const fVisual = extractArrayLiteral(themeSrc, 'VISUAL_STYLES');
|
|
const fHeading = extractArrayLiteral(themeSrc, 'HEADING_STYLES');
|
|
const fChart = extractArrayLiteral(themeSrc, 'CHART_STYLES');
|
|
const fDensity = extractUnionType(densitySrc, 'Density');
|
|
const fLogChip = extractUnionType(logChipSrc, 'LogChipColorMode');
|
|
const fSidebarModes = extractUnionType(sidebarLayoutSrc, 'SidebarMode');
|
|
const fAnatomyModes = extractUnionType(anatomyLayoutSrc, 'AnatomyMode');
|
|
const fFleetTabLayouts = extractUnionType(fleetTabLayoutSrc, 'FleetTabLayout');
|
|
const fNavModes = extractSetLiteral(navModeSrc, 'VALID') ?? extractUnionType(navModeSrc, 'TopNavMode');
|
|
const fAligns = extractUnionType(navAlignSrc, 'TopNavAlign');
|
|
const fEligible = extractEligibleIds(navRegistrySrc);
|
|
compare('theme modes', bThemes, fThemes);
|
|
compare('accents', bAccents, fAccents);
|
|
compare('ui fonts', bUiFonts, fUiFonts);
|
|
compare('mono fonts', bMonoFonts, fMonoFonts);
|
|
compare('visual styles', bVisual, fVisual);
|
|
compare('heading styles', bHeading, fHeading);
|
|
compare('chart styles', bChart, fChart);
|
|
compare('densities', bDensities, fDensity);
|
|
compare('log chip color modes', bLogChip, fLogChip);
|
|
compare('sidebar modes', bSidebarModes, fSidebarModes);
|
|
compare('anatomy modes', bAnatomyModes, fAnatomyModes);
|
|
compare('fleet tab layouts', bFleetTabLayouts, fFleetTabLayouts);
|
|
compare('nav modes', bNavModes, fNavModes);
|
|
compare('nav aligns', bAligns, fAligns);
|
|
|
|
// Quick links: the backend list must be a superset of the frontend's
|
|
// eligible ids (the backend rejects unknown ids; headroom allows the
|
|
// frontend cap to grow without a 400 on previously saved documents). The
|
|
// paired-declaration guard additionally proves the registry extractor still
|
|
// matches every `quickLinkEligible:` flag in the source, so a refactor that
|
|
// renames the property cannot make the check pass vacuously.
|
|
if (!bQuickLinks || !fEligible) {
|
|
fail(`quick links: could not extract declaration (backend=${bQuickLinks?.length} frontend=${fEligible?.eligible?.length})`);
|
|
} else {
|
|
compareCount('quick links pairing', (navRegistrySrc.match(/ quickLinkEligible:/g) ?? []).length, fEligible.paired);
|
|
const missing = fEligible.eligible.filter((id) => !bQuickLinks.includes(id));
|
|
if (missing.length > 0) {
|
|
fail(`quick links: frontend eligible ids missing from backend enum: ${missing.join(', ')}`);
|
|
} else {
|
|
ok(`quick links: all ${fEligible.eligible.length} frontend eligible ids present in backend enum (${bQuickLinks.length} entries)`);
|
|
}
|
|
}
|
|
|
|
if (failed) {
|
|
console.error('[preference-parity] value-set drift detected; update both sides in the same change.');
|
|
process.exit(1);
|
|
}
|
|
console.log('[preference-parity] all value sets match.');
|