From e4accb614b2a4533cca76267d206cd08a26661bf Mon Sep 17 00:00:00 2001 From: Arlind Date: Thu, 18 Jun 2026 16:29:14 +0200 Subject: [PATCH] chore(roadmap): add idempotent prod roadmap sync script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scripts/sync-roadmap.js | 150 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 scripts/sync-roadmap.js diff --git a/scripts/sync-roadmap.js b/scripts/sync-roadmap.js new file mode 100644 index 0000000..be5a4b7 --- /dev/null +++ b/scripts/sync-roadmap.js @@ -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 +// .pre-roadmap-sync-.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); +});