mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-26 01:44:04 +00:00
feat: optimize observations table — 478MB → 141MB
Schema v3 migration:
- Replace observer_id TEXT (64-char hex) with observer_idx INTEGER FK
- Drop redundant hash, observer_name, created_at columns
- Store timestamp as epoch integer instead of ISO string
- In-memory dedup Set replaces expensive unique index lookups
- Auto-migration on startup with timestamped backup (never overwrites)
- Detects already-migrated DBs via pragma user_version + column inspection
Fixes:
- disambiguateHops: restore 'known' field dropped during refactor (5dd0727)
- Skip MQTT connections when NODE_ENV=test
- e2e test: encodeURIComponent for # channel hashes in URLs
- VACUUM + TRUNCATE checkpoint after migration (not just VACUUM)
- Daily TRUNCATE checkpoint at 2:00 AM UTC to reclaim WAL space
Observability:
- SQLite stats in /api/perf (DB size, WAL size, freelist, row counts, busy pages)
- Rendered in perf dashboard with color-coded thresholds
Tests: 839 pass (89 db + 30 migration + 70 helpers + 200 routes + 34 packet-store + 52 decoder + 255 decoder-spec + 62 filter + 47 e2e)
This commit is contained in:
@@ -67,21 +67,37 @@ db.exec(`
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_transmissions_hash ON transmissions(hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_transmissions_first_seen ON transmissions(first_seen);
|
||||
CREATE INDEX IF NOT EXISTS idx_transmissions_payload_type ON transmissions(payload_type);
|
||||
`);
|
||||
|
||||
// --- Determine schema version ---
|
||||
let schemaVersion = 0;
|
||||
try {
|
||||
const row = db.prepare('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1').get();
|
||||
if (row) schemaVersion = row.version;
|
||||
} catch {}
|
||||
let schemaVersion = db.pragma('user_version', { simple: true }) || 0;
|
||||
|
||||
// Migrate from old schema_version table to pragma user_version
|
||||
if (schemaVersion === 0) {
|
||||
try {
|
||||
const row = db.prepare('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1').get();
|
||||
if (row && row.version >= 3) {
|
||||
db.pragma(`user_version = ${row.version}`);
|
||||
schemaVersion = row.version;
|
||||
db.exec('DROP TABLE IF EXISTS schema_version');
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Detect v3 schema by column presence (handles crash between migration and version write)
|
||||
if (schemaVersion === 0) {
|
||||
try {
|
||||
const cols = db.pragma('table_info(observations)').map(c => c.name);
|
||||
if (cols.includes('observer_idx') && !cols.includes('observer_id')) {
|
||||
db.pragma('user_version = 3');
|
||||
schemaVersion = 3;
|
||||
console.log('[migration-v3] Detected already-migrated schema, set user_version = 3');
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// --- v3 migration: lean observations table ---
|
||||
function needsV3Migration() {
|
||||
@@ -98,7 +114,7 @@ function runV3Migration() {
|
||||
console.log('[migration-v3] Starting observations table optimization...');
|
||||
|
||||
// a. Backup DB
|
||||
const backupPath = dbPath + '.pre-v3-backup';
|
||||
const backupPath = dbPath + `.pre-v3-backup-${Date.now()}`;
|
||||
try {
|
||||
console.log(`[migration-v3] Backing up DB to ${backupPath}...`);
|
||||
fs.copyFileSync(dbPath, backupPath);
|
||||
@@ -137,15 +153,12 @@ function runV3Migration() {
|
||||
`).run();
|
||||
console.log(`[migration-v3] Migrated ${result.changes} rows (${Date.now() - stepStart}ms)`);
|
||||
|
||||
// d. Drop old table
|
||||
// d. Drop view, old table, rename
|
||||
stepStart = Date.now();
|
||||
db.exec('DROP VIEW IF EXISTS packets_v');
|
||||
db.exec('DROP TABLE observations');
|
||||
console.log(`[migration-v3] Dropped old observations table (${Date.now() - stepStart}ms)`);
|
||||
|
||||
// e. Rename
|
||||
stepStart = Date.now();
|
||||
db.exec('ALTER TABLE observations_v3 RENAME TO observations');
|
||||
console.log(`[migration-v3] Renamed observations_v3 → observations (${Date.now() - stepStart}ms)`);
|
||||
console.log(`[migration-v3] Replaced observations table (${Date.now() - stepStart}ms)`);
|
||||
|
||||
// f. Create indexes
|
||||
stepStart = Date.now();
|
||||
@@ -158,22 +171,23 @@ function runV3Migration() {
|
||||
console.log(`[migration-v3] Created indexes (${Date.now() - stepStart}ms)`);
|
||||
|
||||
// g. Set schema version
|
||||
db.exec('DELETE FROM schema_version');
|
||||
db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(3);
|
||||
|
||||
db.pragma('user_version = 3');
|
||||
schemaVersion = 3;
|
||||
|
||||
// h. Rebuild view (done below in common code)
|
||||
|
||||
// i. VACUUM
|
||||
// i. VACUUM + checkpoint
|
||||
stepStart = Date.now();
|
||||
db.exec('VACUUM');
|
||||
console.log(`[migration-v3] VACUUM complete (${Date.now() - stepStart}ms)`);
|
||||
db.pragma('wal_checkpoint(TRUNCATE)');
|
||||
console.log(`[migration-v3] VACUUM + checkpoint complete (${Date.now() - stepStart}ms)`);
|
||||
|
||||
console.log(`[migration-v3] Migration complete! Total time: ${Date.now() - startTime}ms`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(`[migration-v3] Migration failed: ${e.message}`);
|
||||
console.error('[migration-v3] Old data should still be intact if observations table was not yet dropped');
|
||||
console.error('[migration-v3] Restore from backup if needed: ' + dbPath + '.pre-v3-backup');
|
||||
// Try to clean up v3 table if it exists
|
||||
try { db.exec('DROP TABLE IF EXISTS observations_v3'); } catch {}
|
||||
return false;
|
||||
@@ -186,7 +200,7 @@ if (!isV3 && needsV3Migration()) {
|
||||
runV3Migration();
|
||||
}
|
||||
|
||||
// If schema_version < 3 and no migration happened (fresh DB or migration skipped), create old-style table
|
||||
// If user_version < 3 and no migration happened (fresh DB or migration skipped), create old-style table
|
||||
if (schemaVersion < 3) {
|
||||
const obsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='observations'").get();
|
||||
if (!obsExists) {
|
||||
@@ -208,8 +222,8 @@ if (schemaVersion < 3) {
|
||||
CREATE INDEX idx_observations_timestamp ON observations(timestamp);
|
||||
CREATE UNIQUE INDEX idx_observations_dedup ON observations(transmission_id, observer_idx, COALESCE(path_json, ''));
|
||||
`);
|
||||
db.exec('DELETE FROM schema_version');
|
||||
db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(3);
|
||||
|
||||
db.pragma('user_version = 3');
|
||||
schemaVersion = 3;
|
||||
} else {
|
||||
// Old-style observations table exists but migration wasn't run (or failed)
|
||||
|
||||
+26
-26
@@ -22,9 +22,9 @@
|
||||
<meta name="twitter:title" content="MeshCore Analyzer">
|
||||
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
|
||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1774418278">
|
||||
<link rel="stylesheet" href="home.css?v=1774418278">
|
||||
<link rel="stylesheet" href="live.css?v=1774418278">
|
||||
<link rel="stylesheet" href="style.css?v=1774477855">
|
||||
<link rel="stylesheet" href="home.css?v=1774477855">
|
||||
<link rel="stylesheet" href="live.css?v=1774477855">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="anonymous">
|
||||
@@ -81,28 +81,28 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1774418278"></script>
|
||||
<script src="customize.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774418278"></script>
|
||||
<script src="hop-resolver.js?v=1774418278"></script>
|
||||
<script src="hop-display.js?v=1774418278"></script>
|
||||
<script src="app.js?v=1774418278"></script>
|
||||
<script src="home.js?v=1774418278"></script>
|
||||
<script src="packet-filter.js?v=1774418278"></script>
|
||||
<script src="packets.js?v=1774418278"></script>
|
||||
<script src="map.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774418278" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1774477855"></script>
|
||||
<script src="customize.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774477855"></script>
|
||||
<script src="hop-resolver.js?v=1774477855"></script>
|
||||
<script src="hop-display.js?v=1774477855"></script>
|
||||
<script src="app.js?v=1774477855"></script>
|
||||
<script src="home.js?v=1774477855"></script>
|
||||
<script src="packet-filter.js?v=1774477855"></script>
|
||||
<script src="packets.js?v=1774477855"></script>
|
||||
<script src="map.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774477855" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -84,6 +84,25 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// SQLite stats
|
||||
if (server.sqlite && !server.sqlite.error) {
|
||||
const sq = server.sqlite;
|
||||
const walColor = sq.walSizeMB > 50 ? 'var(--status-red)' : sq.walSizeMB > 10 ? 'var(--status-yellow)' : 'var(--status-green)';
|
||||
const freelistColor = sq.freelistMB > 10 ? 'var(--status-yellow)' : 'var(--status-green)';
|
||||
html += `<h3>SQLite</h3><div style="display:flex;gap:16px;flex-wrap:wrap;margin:8px 0;">
|
||||
<div class="perf-card"><div class="perf-num">${sq.dbSizeMB}MB</div><div class="perf-label">DB Size</div></div>
|
||||
<div class="perf-card"><div class="perf-num" style="color:${walColor}">${sq.walSizeMB}MB</div><div class="perf-label">WAL Size</div></div>
|
||||
<div class="perf-card"><div class="perf-num" style="color:${freelistColor}">${sq.freelistMB}MB</div><div class="perf-label">Freelist</div></div>
|
||||
<div class="perf-card"><div class="perf-num">${(sq.rows.transmissions || 0).toLocaleString()}</div><div class="perf-label">Transmissions</div></div>
|
||||
<div class="perf-card"><div class="perf-num">${(sq.rows.observations || 0).toLocaleString()}</div><div class="perf-label">Observations</div></div>
|
||||
<div class="perf-card"><div class="perf-num">${sq.rows.nodes || 0}</div><div class="perf-label">Nodes</div></div>
|
||||
<div class="perf-card"><div class="perf-num">${sq.rows.observers || 0}</div><div class="perf-label">Observers</div></div>`;
|
||||
if (sq.walPages) {
|
||||
html += `<div class="perf-card"><div class="perf-num">${sq.walPages.busy}</div><div class="perf-label">WAL Busy Pages</div></div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
// Server endpoints table
|
||||
const eps = Object.entries(server.endpoints);
|
||||
if (eps.length) {
|
||||
|
||||
+1
-1
@@ -210,7 +210,7 @@ function disambiguateHops(hops, allNodes, maxHopDist) {
|
||||
else if (!prev && next && dNext > MAX_HOP_DIST) { r.unreliable = true; r.lat = null; r.lon = null; }
|
||||
}
|
||||
|
||||
return resolved.map(r => ({ hop: r.hop, name: r.name, lat: r.lat, lon: r.lon, pubkey: r.pubkey, ambiguous: !!r.candidates, unreliable: !!r.unreliable }));
|
||||
return resolved.map(r => ({ hop: r.hop, name: r.name, lat: r.lat, lon: r.lon, pubkey: r.pubkey, known: !!r.known, ambiguous: !!r.candidates, unreliable: !!r.unreliable }));
|
||||
}
|
||||
|
||||
// Update hash_size maps for a single packet
|
||||
|
||||
@@ -395,6 +395,28 @@ app.get('/api/perf', (req, res) => {
|
||||
slowQueries: perfStats.slowQueries.slice(-20),
|
||||
cache: { size: cache.size, hits: cache.hits, misses: cache.misses, staleHits: cache.staleHits, recomputes: cache.recomputes, hitRate: cache.hits + cache.misses > 0 ? Math.round(cache.hits / (cache.hits + cache.misses) * 1000) / 10 : 0 },
|
||||
packetStore: pktStore.getStats(),
|
||||
sqlite: (() => {
|
||||
try {
|
||||
const walInfo = db.db.pragma('wal_checkpoint(PASSIVE)');
|
||||
const pageSize = db.db.pragma('page_size', { simple: true });
|
||||
const pageCount = db.db.pragma('page_count', { simple: true });
|
||||
const freelistCount = db.db.pragma('freelist_count', { simple: true });
|
||||
const dbSizeMB = Math.round(pageSize * pageCount / 1048576 * 10) / 10;
|
||||
const freelistMB = Math.round(pageSize * freelistCount / 1048576 * 10) / 10;
|
||||
const fs = require('fs');
|
||||
const dbPath = process.env.DB_PATH || require('path').join(__dirname, 'data', 'meshcore.db');
|
||||
let walSizeMB = 0;
|
||||
try { walSizeMB = Math.round(fs.statSync(dbPath + '-wal').size / 1048576 * 10) / 10; } catch {}
|
||||
const stats = db.getStats();
|
||||
return {
|
||||
dbSizeMB,
|
||||
walSizeMB,
|
||||
freelistMB,
|
||||
walPages: walInfo[0] ? { total: walInfo[0].busy + walInfo[0].checkpointed, checkpointed: walInfo[0].checkpointed, busy: walInfo[0].busy } : null,
|
||||
rows: { transmissions: stats.totalTransmissions, observations: stats.totalObservations, nodes: stats.totalNodes, observers: stats.totalObservers },
|
||||
};
|
||||
} catch (e) { return { error: e.message }; }
|
||||
})(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -425,6 +447,19 @@ setInterval(() => {
|
||||
} catch (e) { console.error('[wal] checkpoint error:', e.message); }
|
||||
}, 300000).unref();
|
||||
|
||||
// Daily TRUNCATE checkpoint at 2:00 AM UTC — reclaims WAL file space
|
||||
setInterval(() => {
|
||||
const h = new Date().getUTCHours();
|
||||
const m = new Date().getUTCMinutes();
|
||||
if (h === 2 && m === 0) {
|
||||
try {
|
||||
const t0 = Date.now();
|
||||
db.db.pragma('wal_checkpoint(TRUNCATE)');
|
||||
console.log(`[wal] daily TRUNCATE checkpoint: ${Date.now() - t0}ms`);
|
||||
} catch (e) { console.error('[wal] TRUNCATE checkpoint error:', e.message); }
|
||||
}
|
||||
}, 60000).unref();
|
||||
|
||||
// --- Health / Telemetry Endpoint ---
|
||||
app.get('/api/health', (req, res) => {
|
||||
const mem = process.memoryUsage();
|
||||
@@ -522,6 +557,9 @@ if (config.mqttSources && Array.isArray(config.mqttSources)) {
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
console.log('[mqtt] Skipping MQTT connections in test mode');
|
||||
} else {
|
||||
for (const source of mqttSources) {
|
||||
try {
|
||||
const opts = { reconnectPeriod: 5000 };
|
||||
@@ -800,6 +838,7 @@ for (const source of mqttSources) {
|
||||
console.error(`MQTT [${source.name || source.broker}] connection failed (non-fatal):`, e.message);
|
||||
}
|
||||
}
|
||||
} // end NODE_ENV !== 'test'
|
||||
|
||||
// --- Express ---
|
||||
app.use(express.json());
|
||||
|
||||
+16
-14
@@ -126,13 +126,13 @@ function runDbModule(dbPath) {
|
||||
process.env.DB_PATH = ${JSON.stringify(dbPath)};
|
||||
const db = require(${JSON.stringify(path.resolve(__dirname, 'db'))});
|
||||
const cols = db.db.pragma('table_info(observations)').map(c => c.name);
|
||||
const sv = db.db.prepare('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1').get();
|
||||
const sv = db.db.pragma('user_version', { simple: true });
|
||||
const obsCount = db.db.prepare('SELECT COUNT(*) as c FROM observations').get().c;
|
||||
const viewRows = db.db.prepare('SELECT * FROM packets_v ORDER BY id').all();
|
||||
const rawObs = db.db.prepare('SELECT * FROM observations ORDER BY id').all();
|
||||
console.log(JSON.stringify({
|
||||
columns: cols,
|
||||
schemaVersion: sv ? sv.version : 0,
|
||||
schemaVersion: sv || 0,
|
||||
obsCount,
|
||||
viewRows,
|
||||
rawObs
|
||||
@@ -193,7 +193,8 @@ console.log('Migration from old schema:');
|
||||
assert(vr2.path_json === null, 'null path_json preserved');
|
||||
|
||||
// Verify backup file created
|
||||
assert(fs.existsSync(dbPath + '.pre-v3-backup'), 'backup file exists');
|
||||
const backups1 = fs.readdirSync(tmpDir).filter(f => f.includes('.pre-v3-backup-'));
|
||||
assert(backups1.length === 1, 'backup file exists');
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true });
|
||||
}
|
||||
@@ -211,7 +212,8 @@ console.log('\nMigration idempotency:');
|
||||
assert(info.schemaVersion === 3, 'first run migrates to v3');
|
||||
|
||||
// Second run — should NOT re-run migration (no backup overwrite, same data)
|
||||
const backupMtime = fs.statSync(dbPath + '.pre-v3-backup').mtimeMs;
|
||||
const backups2pre = fs.readdirSync(tmpDir).filter(f => f.includes('.pre-v3-backup-'));
|
||||
const backupMtime = fs.statSync(path.join(tmpDir, backups2pre[0])).mtimeMs;
|
||||
info = runDbModule(dbPath);
|
||||
assert(info.schemaVersion === 3, 'second run still v3');
|
||||
assert(info.obsCount === 3, 'rows still intact');
|
||||
@@ -219,24 +221,24 @@ console.log('\nMigration idempotency:');
|
||||
fs.rmSync(tmpDir, { recursive: true });
|
||||
}
|
||||
|
||||
// --- Test 3: Backup failure aborts migration ---
|
||||
console.log('\nBackup failure aborts migration:');
|
||||
// --- Test 3: Each migration creates a unique backup ---
|
||||
console.log('\nUnique backup per migration:');
|
||||
{
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'meshcore-mig-test3-'));
|
||||
const dbPath = path.join(tmpDir, 'test-mig3.db');
|
||||
|
||||
createOldSchemaDB(dbPath);
|
||||
|
||||
// Create backup path as a directory so copyFileSync fails
|
||||
fs.mkdirSync(dbPath + '.pre-v3-backup');
|
||||
|
||||
// Run db.js — migration should abort, old schema preserved
|
||||
const info = runDbModule(dbPath);
|
||||
|
||||
// Old schema should be preserved
|
||||
assert(info.columns.includes('observer_id'), 'old observer_id column preserved');
|
||||
assert(info.schemaVersion < 3, 'schema version not updated');
|
||||
assert(info.obsCount === 3, 'old rows still intact');
|
||||
// Migration should have completed
|
||||
assert(info.columns.includes('observer_idx'), 'migration completed');
|
||||
assert(info.schemaVersion === 3, 'schema version is 3');
|
||||
|
||||
// A timestamped backup should exist
|
||||
const backups = fs.readdirSync(tmpDir).filter(f => f.includes('.pre-v3-backup-'));
|
||||
assert(backups.length === 1, 'exactly one backup created');
|
||||
assert(fs.statSync(path.join(tmpDir, backups[0])).size > 0, 'backup is non-empty');
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true });
|
||||
}
|
||||
|
||||
+3
-3
@@ -288,9 +288,9 @@ console.log('\nv3 schema:');
|
||||
assert(typeof viewRow.timestamp === 'string', 'packets_v timestamp is ISO string');
|
||||
}
|
||||
|
||||
// schema_version table exists with version 3
|
||||
const sv = db.db.prepare('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1').get();
|
||||
assert(sv && sv.version === 3, 'schema_version table has version 3');
|
||||
// user_version is 3
|
||||
const sv = db.db.pragma('user_version', { simple: true });
|
||||
assert(sv === 3, 'user_version is 3');
|
||||
}
|
||||
|
||||
// --- v3 ingestion: observer resolved via observer_idx ---
|
||||
|
||||
+1
-1
@@ -375,7 +375,7 @@ async function main() {
|
||||
if (chList.length > 0) {
|
||||
const someCh = chList[0];
|
||||
assert(someCh.messageCount > 0, `channel has messages (${someCh.messageCount})`);
|
||||
const msgResp = (await get(`/api/channels/${someCh.hash}/messages`)).data;
|
||||
const msgResp = (await get(`/api/channels/${encodeURIComponent(someCh.hash)}/messages`)).data;
|
||||
assert(msgResp.messages.length > 0, 'channel has message list');
|
||||
assert(msgResp.messages[0].sender !== undefined, 'message has sender');
|
||||
console.log(` ✓ Channels: ${chList.length} channels\n`);
|
||||
|
||||
Reference in New Issue
Block a user