mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-19 08:33:56 -06:00
TWO CHANGES, together because they touch the same packager hunks. 1. THE DRIVER. The BrightSign package used to be MANUFACTURED. scripts/build-server-zip.sh dropped better-sqlite3 from package.json and then installed db/sqlite-compat.js into node_modules under that name, so every require resolved to the façade. It worked — and it shipped a database layer that no test had ever executed. That is the same shape as the TELEMETRY_COLLECTOR TDZ crash that took production down while 1676 tests and four CI jobs were green: a build-time rewrite cannot be tested by the build that performs it. db/sqlite-driver.js now decides at runtime: the native driver when it loads, the node:sqlite façade otherwise. One artifact, one code path, and — the point — both branches reachable from a test. ST_SQLITE_DRIVER=node runs the entire suite the way a player runs it, and a new CI job does exactly that on Node 24 with --omit=optional so the fallback is reached the same way it is on hardware, not by an env var alone. better-sqlite3 becomes an optionalDependency, so a host with no compiler installs cleanly and falls back rather than failing. preflight-deps stops trying to rebuild a native module on a host that has no toolchain and a working built-in driver — on a player that was a five-minute node-gyp failure ending in a server that never started. Asking for the native driver BY NAME (ST_SQLITE_DRIVER=better-sqlite3) still fails loudly, because a production box that has lost its native module is broken and should say so rather than quietly running something else. ⚠️ NODE 24 IN PRACTICE. node:sqlite is unflagged only from 23.4; on the 22.x line it needs --experimental-sqlite and on 20.x it does not exist. So the code probes rather than comparing versions, the player package pins engines >=24, and the built-in cases skip on the Node 20 CI job rather than failing there. Verified on Node 24, both drivers, full suite: better-sqlite3 1762 pass / 0 fail node:sqlite 1762 pass / 0 fail and the built payload resolves node:sqlite with no better-sqlite3 present at all. 2. THE LICENCE. The ffprobe/ffmpeg binaries added in the previous commit are LGPL 2.1 and statically linked, so the licence text has to travel WITH them — a link on a website is not the copy the licence asks to accompany the work. The packager now copies COPYING.LGPLv2.1 and a build README into bin/, and refuses to build if the licence is missing. legal/third-party.html gains an LGPL section with the written offer required by section 6 for static linking, and the exact configure line. It also drops Sharp, which that page still listed although #263 removed it, and names what actually does the image work now (jimp, @jsquash/webp, @jsquash/avif). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56
101 lines
4.2 KiB
JavaScript
101 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Phase 1 multitenancy parity check.
|
|
//
|
|
// Compares per-user resource counts between the pre-migration snapshot and
|
|
// the current DB. Every row must end up in exactly one workspace owned by
|
|
// the original user. Drift = bug.
|
|
//
|
|
// Usage:
|
|
// node scripts/parity-multitenancy.js
|
|
//
|
|
// Exits non-zero on any drift.
|
|
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
const SERVER_DIR = path.resolve(__dirname, '..', 'server');
|
|
process.chdir(SERVER_DIR);
|
|
const { Database } = require(path.join(SERVER_DIR, 'db', 'sqlite-driver.js'));
|
|
const config = require(path.join(SERVER_DIR, 'config'));
|
|
|
|
const PRE = path.resolve(SERVER_DIR, 'db', 'remote_display.pre-multitenancy.db');
|
|
const POST = config.dbPath;
|
|
|
|
console.log(`[parity] pre = ${PRE}`);
|
|
console.log(`[parity] post = ${POST}`);
|
|
|
|
const pre = new Database(PRE, { readonly: true });
|
|
const post = new Database(POST, { readonly: true });
|
|
|
|
const TABLES = ['devices','content','playlists','layouts','widgets','schedules','video_walls','device_groups','white_labels','kiosk_pages','alert_configs'];
|
|
const users = pre.prepare('SELECT id, email FROM users').all();
|
|
let pass = 0, fail = 0;
|
|
|
|
console.log('\n--- per-user, per-table row counts ---');
|
|
for (const u of users) {
|
|
for (const t of TABLES) {
|
|
const preN = pre.prepare(`SELECT COUNT(*) AS n FROM ${t} WHERE user_id = ?`).get(u.id).n;
|
|
// Post: rows belonging to any workspace owned by an org owned by this user.
|
|
let postN;
|
|
try {
|
|
postN = post.prepare(`
|
|
SELECT COUNT(*) AS n FROM ${t} r
|
|
WHERE r.workspace_id IN (
|
|
SELECT w.id FROM workspaces w
|
|
JOIN organizations o ON w.organization_id = o.id
|
|
WHERE o.owner_user_id = ?
|
|
)
|
|
`).get(u.id).n;
|
|
} catch (e) {
|
|
postN = '<no workspace_id col>';
|
|
}
|
|
const ok = preN === postN;
|
|
if (preN === 0 && postN === 0) continue;
|
|
console.log(` ${u.email.padEnd(30)} ${t.padEnd(16)} pre=${String(preN).padStart(4)} post=${String(postN).padStart(4)} ${ok ? 'PASS' : 'FAIL'}`);
|
|
if (ok) pass++; else fail++;
|
|
}
|
|
}
|
|
|
|
console.log('\n--- platform-wide totals ---');
|
|
const totalsPre = {};
|
|
const totalsPost = {};
|
|
for (const t of TABLES) {
|
|
totalsPre[t] = pre .prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
|
|
totalsPost[t] = post.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
|
|
const ok = totalsPre[t] === totalsPost[t];
|
|
console.log(` ${t.padEnd(16)} pre=${String(totalsPre[t]).padStart(4)} post=${String(totalsPost[t]).padStart(4)} ${ok ? 'PASS' : 'FAIL'}`);
|
|
if (ok) pass++; else fail++;
|
|
}
|
|
|
|
console.log('\n--- new tables populated ---');
|
|
const newTables = ['organizations', 'organization_members', 'workspaces', 'workspace_members'];
|
|
for (const t of newTables) {
|
|
const n = post.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
|
|
console.log(` ${t.padEnd(24)} rows=${n}`);
|
|
}
|
|
|
|
console.log('\n--- orphan check: rows with NON-NULL user_id but NULL workspace_id ---');
|
|
console.log('(rows with NULL user_id are unclaimed devices or platform templates - expected NULL workspace_id)');
|
|
for (const t of TABLES) {
|
|
try {
|
|
const realOrphans = post.prepare(`SELECT COUNT(*) AS n FROM ${t} WHERE user_id IS NOT NULL AND workspace_id IS NULL`).get().n;
|
|
const expectedNulls = post.prepare(`SELECT COUNT(*) AS n FROM ${t} WHERE user_id IS NULL`).get().n;
|
|
const tag = realOrphans > 0 ? 'FAIL' : 'PASS';
|
|
console.log(` ${t.padEnd(16)} bug_orphans=${realOrphans} expected_nulls(user_id IS NULL)=${expectedNulls} ${tag}`);
|
|
if (realOrphans > 0) fail++; else pass++;
|
|
} catch { /* no column */ }
|
|
}
|
|
|
|
console.log('\n--- role migration ---');
|
|
const sa = post.prepare(`SELECT COUNT(*) AS n FROM users WHERE role = 'superadmin'`).get().n;
|
|
const pa = post.prepare(`SELECT COUNT(*) AS n FROM users WHERE role = 'platform_admin'`).get().n;
|
|
const adm = post.prepare(`SELECT COUNT(*) AS n FROM users WHERE role = 'admin'`).get().n;
|
|
console.log(` superadmin (should be 0) : ${sa}`);
|
|
console.log(` platform_admin (should be > 0 if any pre had superadmin): ${pa}`);
|
|
console.log(` admin (should be 0) : ${adm}`);
|
|
if (sa === 0 && adm === 0) pass++; else fail++;
|
|
|
|
console.log(`\n--- summary: ${pass} pass, ${fail} fail ---`);
|
|
pre.close(); post.close();
|
|
process.exit(fail === 0 ? 0 : 1);
|