chore(roadmap): add idempotent prod roadmap sync script
scripts/sync-roadmap.js takes its own online backup of the target DB, then upserts the 10 canonical roadmap items matched by display_order — updating title/description/status/target/features in place so ids, attributions and shipped_at are preserved. Inserts only when a display_order is missing; never deletes. Run on the box after deploy with BIFROST_DB_PATH set.
This commit is contained in:
parent
ec24694a6d
commit
e4accb614b
1 changed files with 150 additions and 0 deletions
150
scripts/sync-roadmap.js
Normal file
150
scripts/sync-roadmap.js
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
//
|
||||||
|
// One-off, idempotent sync of the canonical roadmap content into a target DB.
|
||||||
|
//
|
||||||
|
// BIFROST_DB_PATH=/opt/fenja/data/bifrost-portal/bifrost.db node scripts/sync-roadmap.js
|
||||||
|
//
|
||||||
|
// What it does, in order:
|
||||||
|
// 1. Takes its OWN online backup of the target DB to
|
||||||
|
// <db>.pre-roadmap-sync-<timestamp>.bak (safe while the app is running).
|
||||||
|
// 2. Upserts the 10 canonical items MATCHED BY display_order — it UPDATEs
|
||||||
|
// title / description / status / target / features in place, so row ids,
|
||||||
|
// roadmap_attributions, created_at and shipped_at are all preserved.
|
||||||
|
// 3. Inserts a canonical item only if no row exists at that display_order.
|
||||||
|
// 4. Leaves any extra prod rows (display_order outside 1..N) untouched and
|
||||||
|
// logs them — it never deletes.
|
||||||
|
//
|
||||||
|
// Safe to re-run. Does NOT wipe the table (unlike scripts/seed-demo.js, which
|
||||||
|
// must never be run against prod).
|
||||||
|
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
|
||||||
|
// ── Canonical roadmap (content only; no demo dates/attributions) ───────────
|
||||||
|
const ROADMAP = [
|
||||||
|
{ display_order: 1, title: 'MCP Usage', status: 'shipping', target: 'July 2026',
|
||||||
|
description: 'Connect Fenja to external MCP servers — databases, internal tools, and other software — so it can read from and act across your systems.',
|
||||||
|
features: ['Connect external MCP servers', 'Reach databases, internal tools & software', 'Per-connection credentials & scoping'] },
|
||||||
|
{ display_order: 2, title: 'Extended Logging', status: 'shipping', target: 'July 2026',
|
||||||
|
description: 'Granular logging and audit controls, capturing usage down to the individual user, request, and document.',
|
||||||
|
features: ['Granular usage & audit logging', 'Per-user, per-request, per-document trails', 'Exportable audit records'] },
|
||||||
|
{ display_order: 3, title: 'Wiki 2.0', status: 'in_beta', target: 'July 2026',
|
||||||
|
description: 'A major wiki upgrade: version history with diffs between revisions, admin-locked pages, and a richer page structure.',
|
||||||
|
features: ['Version history with revision diffs', 'Admin-locked pages', 'Richer page structure'] },
|
||||||
|
{ display_order: 4, title: 'Interview 2.0', status: 'planned', target: 'July 2026',
|
||||||
|
description: 'A major Interviews upgrade: build and manage interview scripts, send invites, and track responses in one place.',
|
||||||
|
features: ['Build & manage interview scripts', 'Send invites & track responses', 'Centralised results view'] },
|
||||||
|
{ display_order: 5, title: 'Fenja Analyze', status: 'planned', target: 'August 2026',
|
||||||
|
description: 'The first release of Fenja Analyze: ask a question in plain language, run it as a query against your data, and get an answer with sources.',
|
||||||
|
features: ['Plain-language questions → queries', 'Answers grounded in your data', 'Source citations'] },
|
||||||
|
{ display_order: 6, title: 'Fenja Agentic', status: 'planned', target: 'September 2026',
|
||||||
|
description: 'The first release of Agents: create and orchestrate agents from an admin panel, wiring them to the tools and data they need.',
|
||||||
|
features: ['Create agents from an admin panel', 'Orchestrate multi-agent workflows', 'Wire agents to tools & data'] },
|
||||||
|
{ display_order: 7, title: 'Fenja Dev', status: 'planned', target: 'September 2026',
|
||||||
|
description: 'The first release of Fenja Dev: a sovereign IDE with terminal and git integration, running entirely inside your environment.',
|
||||||
|
features: ['Sovereign in-environment IDE', 'Integrated terminal', 'Git integration'] },
|
||||||
|
{ display_order: 8, title: 'HTML Reports', status: 'planned', target: 'September 2026',
|
||||||
|
description: 'Structured reporting for Fenja Analyze: admins define report templates so generated analyses render as clean, shareable HTML.',
|
||||||
|
features: ['Admin-defined report templates', 'Analyses rendered as clean HTML', 'Shareable, structured output'] },
|
||||||
|
{ display_order: 9, title: 'Self-Service Agents', status: 'planned', target: 'October 2026',
|
||||||
|
description: 'A major expansion of the agent experience, letting users create and run their own agents without admin involvement.',
|
||||||
|
features: ['User-created agents (no admin needed)', 'Run & manage your own agents', 'Reuse across tasks'] },
|
||||||
|
{ display_order: 10, title: 'Self-service Routines & Skills', status: 'exploring', target: 'October 2026',
|
||||||
|
description: "Personal and domain-level routines and skills: users build reusable, tailored workflows for their own and their team's recurring tasks.",
|
||||||
|
features: ['Personal & domain-level routines', 'Reusable, tailored workflows', 'Built for recurring team tasks'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const dbPath = process.env.BIFROST_DB_PATH;
|
||||||
|
if (!dbPath) {
|
||||||
|
console.error('!! BIFROST_DB_PATH is not set. Refusing to guess the DB location.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!existsSync(dbPath)) {
|
||||||
|
console.error(`!! No database at ${dbPath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
db.pragma('foreign_keys = ON');
|
||||||
|
|
||||||
|
// Guard: the features column must exist (migration 0009). Deploy runs
|
||||||
|
// migrate.js, so run this AFTER deploying.
|
||||||
|
const cols = db.prepare('PRAGMA table_info(roadmap_items)').all().map((c) => c.name);
|
||||||
|
if (!cols.includes('features')) {
|
||||||
|
console.error('!! roadmap_items.features column is missing — run `node scripts/migrate.js` (deploy) first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Online backup (safe while the app is running). Timestamp from the OS.
|
||||||
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const backupPath = `${dbPath}.pre-roadmap-sync-${ts}.bak`;
|
||||||
|
console.log(`==> Backing up current DB -> ${backupPath}`);
|
||||||
|
await db.backup(backupPath);
|
||||||
|
|
||||||
|
// Snapshot current titles for the diff log.
|
||||||
|
const before = db.prepare('SELECT display_order, title FROM roadmap_items ORDER BY display_order').all();
|
||||||
|
console.log(`==> Current roadmap (${before.length} rows):`);
|
||||||
|
before.forEach((r) => console.log(` ${r.display_order}. ${r.title}`));
|
||||||
|
|
||||||
|
// 2/3. Upsert by display_order, inside a transaction.
|
||||||
|
const findByOrder = db.prepare('SELECT id FROM roadmap_items WHERE display_order = ?');
|
||||||
|
const update = db.prepare(`
|
||||||
|
UPDATE roadmap_items
|
||||||
|
SET title = @title, description = @description, status = @status,
|
||||||
|
target = @target, features = @features, updated_at = datetime('now')
|
||||||
|
WHERE id = @id
|
||||||
|
`);
|
||||||
|
const insert = db.prepare(`
|
||||||
|
INSERT INTO roadmap_items (title, description, status, target, display_order, shipped_at, metadata_text, features)
|
||||||
|
VALUES (@title, @description, @status, @target, @display_order, NULL, NULL, @features)
|
||||||
|
`);
|
||||||
|
|
||||||
|
let updated = 0;
|
||||||
|
let inserted = 0;
|
||||||
|
const run = db.transaction(() => {
|
||||||
|
for (const item of ROADMAP) {
|
||||||
|
const row = findByOrder.get(item.display_order);
|
||||||
|
const params = {
|
||||||
|
title: item.title,
|
||||||
|
description: item.description,
|
||||||
|
status: item.status,
|
||||||
|
target: item.target,
|
||||||
|
features: JSON.stringify(item.features),
|
||||||
|
display_order: item.display_order,
|
||||||
|
};
|
||||||
|
if (row) {
|
||||||
|
update.run({ ...params, id: row.id });
|
||||||
|
updated += 1;
|
||||||
|
} else {
|
||||||
|
insert.run(params);
|
||||||
|
inserted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
run();
|
||||||
|
|
||||||
|
// 4. Report extras that were left untouched (never deleted).
|
||||||
|
const maxOrder = ROADMAP.length;
|
||||||
|
const extras = db
|
||||||
|
.prepare('SELECT display_order, title FROM roadmap_items WHERE display_order > ? ORDER BY display_order')
|
||||||
|
.all(maxOrder);
|
||||||
|
|
||||||
|
console.log(`==> Synced: ${updated} updated in place, ${inserted} inserted.`);
|
||||||
|
if (extras.length > 0) {
|
||||||
|
console.log(`==> Left untouched (display_order > ${maxOrder}) — review in /admin if these should go:`);
|
||||||
|
extras.forEach((r) => console.log(` ${r.display_order}. ${r.title}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const after = db.prepare('SELECT display_order, title, status, target FROM roadmap_items ORDER BY display_order').all();
|
||||||
|
console.log('==> Roadmap now:');
|
||||||
|
after.forEach((r) => console.log(` ${r.display_order}. ${r.title} [${r.status} · ${r.target}]`));
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
console.log(`==> Done. Backup kept at ${backupPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue