Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel, Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame (DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale. Design only: behaviour, texts and roles are unchanged. Tests that asserted the old DOM were updated; the tests now format numbers in English wherever they run. docs/reui-migration-plan.md records the plan and its decisions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
127 lines
5.6 KiB
JavaScript
127 lines
5.6 KiB
JavaScript
/*
|
|
Which of upstream's sortable columns are still sortable here.
|
|
|
|
## Why this file exists
|
|
|
|
`check-parity-controls.mjs` counts three things (destinations, help texts and
|
|
examples), and it has been green throughout. A sortable column header is none of
|
|
the three, so it walks straight past it, and that blind spot has now cost twice:
|
|
|
|
· About (2026-09-04): the three links of the update modal were `href="#"` filled
|
|
in by JavaScript, so "all 28 destinations present" was true and useless.
|
|
· Apps (2026-09-07): upstream's `Store Apps` header is a sort link and this
|
|
console had dropped it. Found by reading upstream by hand, which is not a
|
|
method.
|
|
|
|
A green tool is not a contract. This one turns the remaining half of that lesson
|
|
into something that fails on its own.
|
|
|
|
## How it measures
|
|
|
|
Upstream declares every sortable column as `sortTable('<tbodyId>', <n>)` inside
|
|
the `<a>` of its `<th>`, so the census is exact and comes from the source rather
|
|
than from a list somebody typed. Ours are `<Th field=…>`, plus the handful of
|
|
sort affordances that are not table headers.
|
|
|
|
The comparison is per TABLE and by count, not by column name: upstream's labels
|
|
live in the HTML and ours in TSX, and matching them by string would be a second
|
|
inventory to keep in sync, which is the thing that rots.
|
|
|
|
Every gap must be DECLARED below with its reason. An undeclared gap is a finding;
|
|
a declared one that has closed is also a finding, because a reason nobody needs
|
|
any more is a reason nobody rereads.
|
|
|
|
Exit code is the number of findings.
|
|
*/
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
const ROOT = path.resolve(HERE, '..')
|
|
// UPSTREAM_HTML supplies an explicit snapshot when the sibling checkout is absent.
|
|
const UPSTREAM = process.env.UPSTREAM_HTML
|
|
? path.resolve(process.env.UPSTREAM_HTML)
|
|
: path.resolve(ROOT, '../technitium-ui/DnsServerCore/www/index.html')
|
|
|
|
/**
|
|
* Upstream table id -> where it lives here, how many of its columns we made
|
|
* sortable, and the reason for any gap.
|
|
*
|
|
* `ours` counts the sort affordances the surface really offers, whether they are
|
|
* `<Th field>` or not: the store's is a button over a list, and it counts.
|
|
*/
|
|
const MAP = [
|
|
{ id: 'tableZonesBody', file: 'src/screens/zones/ZoneList.tsx', ours: 7,
|
|
gap: 'The `#` column. Sorting by the row number the sort itself has just handed out leads nowhere, and it sorts as text: 1, 10, 11, 2.' },
|
|
{ id: 'tableEditZoneBody', file: 'src/screens/zones/ZoneRecords.tsx', ours: 4,
|
|
gap: 'The `#` column, for the same reason. Documented at the head of the file.' },
|
|
{ id: 'tableDnssecPropertiesPrivateKeysBody', file: 'src/screens/zones/modals/DnssecProperties.tsx', ours: 6 },
|
|
{ id: 'tbodyAdminUsers', file: 'src/screens/admin/Users.tsx', ours: 7 },
|
|
{ id: 'tbodyAdminGroups', file: 'src/screens/admin/Groups.tsx', ours: 2 },
|
|
{ id: 'tbodyAdminSessions', file: 'src/screens/admin/Sessions.tsx', ours: 5 },
|
|
{ id: 'tbodyAdminCluster', file: 'src/screens/admin/Cluster.tsx', ours: 8 },
|
|
{ id: 'tbodyAdminPermissions', file: 'src/screens/admin/Permissions.tsx', ours: 1 },
|
|
{ id: 'tbodyUserDetailsActiveSessions', file: 'src/screens/admin/UserDetails.tsx', ours: 4 },
|
|
{ id: 'tbodyMyProfileActiveSessions', file: 'src/screens/modals/MyProfile.tsx', ours: 4 },
|
|
{ id: 'tbodyMyProfileMemberOf', file: 'src/screens/modals/MyProfile.tsx', ours: 1 },
|
|
{ id: 'tableDhcpScopesBody', file: 'src/screens/dhcp/Scopes.tsx', ours: 4 },
|
|
{ id: 'tableDhcpLeasesBody', file: 'src/screens/dhcp/Leases.tsx', ours: 7 },
|
|
{ id: 'tableStoreAppsBody', file: 'src/screens/apps/StoreApps.tsx', ours: 1 },
|
|
{ id: 'tableAppsBody', file: 'src/screens/apps/Apps.tsx', ours: 1 },
|
|
{ id: 'tbodyEditPermissionsUser', file: 'src/screens/admin/Permissions.tsx', ours: 1 },
|
|
{ id: 'tbodyEditPermissionsGroup', file: 'src/screens/admin/Permissions.tsx', ours: 1 },
|
|
]
|
|
|
|
if (!fs.existsSync(UPSTREAM)) {
|
|
console.error(`\n SORT PARITY FAILED: upstream not found at ${UPSTREAM}.\n` +
|
|
' Check out technitium-ui or set UPSTREAM_HTML to its www/index.html snapshot.\n')
|
|
process.exit(1)
|
|
}
|
|
|
|
const html = fs.readFileSync(UPSTREAM, 'utf8')
|
|
const upstream = new Map()
|
|
for (const m of html.matchAll(/sortTable\('([^']+)',\s*(\d+)\)/g)) {
|
|
const [, id, col] = m
|
|
if (!upstream.has(id)) upstream.set(id, new Set())
|
|
upstream.get(id).add(Number(col))
|
|
}
|
|
|
|
const findings = []
|
|
const seen = new Set()
|
|
|
|
for (const row of MAP) {
|
|
seen.add(row.id)
|
|
const theirs = upstream.get(row.id)?.size
|
|
if (theirs == null) {
|
|
findings.push(`${row.id}: declared here but upstream no longer sorts it`)
|
|
continue
|
|
}
|
|
const gap = theirs - row.ours
|
|
if (gap > 0 && row.gap == null) {
|
|
findings.push(`${row.id} (${row.file}): upstream sorts ${theirs} columns, this console ${row.ours}, ${gap} undeclared`)
|
|
}
|
|
if (gap <= 0 && row.gap != null) {
|
|
findings.push(`${row.id}: the gap is declared and no longer exists: remove the reason`)
|
|
}
|
|
}
|
|
|
|
for (const id of upstream.keys()) {
|
|
if (!seen.has(id)) findings.push(`${id}: upstream sorts it and this file does not say where it lives here`)
|
|
}
|
|
|
|
const theirTotal = [...upstream.values()].reduce((n, s) => n + s.size, 0)
|
|
const ourTotal = MAP.reduce((n, r) => n + r.ours, 0)
|
|
|
|
if (findings.length === 0) {
|
|
console.log(
|
|
`\n SORT PARITY: ${ourTotal} of upstream's ${theirTotal} sortable columns, across ${MAP.length} tables.\n` +
|
|
` The ${theirTotal - ourTotal} missing are declared, with a reason each.\n`,
|
|
)
|
|
} else {
|
|
console.log('')
|
|
for (const f of findings) console.log(` ${f}`)
|
|
console.log(`\n ${findings.length} findings\n`)
|
|
}
|
|
process.exit(Math.min(findings.length, 250))
|