Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
214 lines
8.1 KiB
JavaScript
214 lines
8.1 KiB
JavaScript
/**
|
|
* Setup Tickets Module — ADR-098: Unified Execution Ecosystem
|
|
*
|
|
* Creates or verifies the Tickets module tables and Kanban widget for a space.
|
|
* This script can be run multiple times safely (idempotent).
|
|
*
|
|
* Usage:
|
|
* node backend/scripts/setup-tickets-module.js [--space-id=N] [--project-id=N]
|
|
*
|
|
* If no space-id given, runs for the Development space.
|
|
* If no project-id given, uses the first project in the space.
|
|
*
|
|
* Creates:
|
|
* 1. Ticket States dictionary table (with 7 ADR-098 states)
|
|
* 2. Ticket Types dictionary table
|
|
* 3. Tickets main table (with all ADR-098 columns)
|
|
* 4. Kanban Board widget bound to Tickets table, grouped by state
|
|
*/
|
|
|
|
import { dbGet, dbAll, dbRun, isPostgres, safeJsonParse } from '../database/connection.js';
|
|
import { createTicketsModuleTables } from '../services/SystemTablesCreator.js';
|
|
|
|
// Parse CLI arguments
|
|
const args = Object.fromEntries(
|
|
process.argv.slice(2).map(arg => {
|
|
const [key, val] = arg.replace(/^--/, '').split('=');
|
|
return [key, val || true];
|
|
})
|
|
);
|
|
|
|
async function main() {
|
|
console.log('=== ADR-098: Tickets Module Setup ===\n');
|
|
|
|
// 1. Find target space
|
|
let spaceId = args['space-id'] ? Number(args['space-id']) : null;
|
|
|
|
if (!spaceId) {
|
|
const devSpace = await dbGet(
|
|
"SELECT id, name FROM spaces WHERE LOWER(name) = 'development' LIMIT 1"
|
|
);
|
|
if (devSpace) {
|
|
spaceId = devSpace.id;
|
|
console.log(`Using Development space: id=${spaceId} name="${devSpace.name}"`);
|
|
} else {
|
|
console.error('No Development space found. Use --space-id=N to specify.');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// 2. Find target project
|
|
let projectId = args['project-id'] ? Number(args['project-id']) : null;
|
|
|
|
if (!projectId) {
|
|
const project = await dbGet(
|
|
'SELECT id, name FROM projects WHERE space_id = ? ORDER BY id LIMIT 1',
|
|
[spaceId]
|
|
);
|
|
if (project) {
|
|
projectId = project.id;
|
|
console.log(`Using project: id=${projectId} name="${project.name}"`);
|
|
} else {
|
|
console.error(`No projects found in space ${spaceId}. Create a project first.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// 3. Check if Tickets table already exists in this project
|
|
const existingTickets = await dbGet(
|
|
"SELECT id FROM universal_tables WHERE project_id = ? AND name = 'Tickets'",
|
|
[projectId]
|
|
);
|
|
|
|
if (existingTickets) {
|
|
console.log(`\n✅ Tickets table already exists: id=${existingTickets.id}`);
|
|
|
|
// Verify related tables
|
|
const existingStates = await dbGet(
|
|
"SELECT id FROM universal_tables WHERE project_id = ? AND name = 'Ticket States'",
|
|
[projectId]
|
|
);
|
|
const existingTypes = await dbGet(
|
|
"SELECT id FROM universal_tables WHERE project_id = ? AND name = 'Ticket Types'",
|
|
[projectId]
|
|
);
|
|
|
|
console.log(` Ticket States: ${existingStates ? `id=${existingStates.id}` : '❌ MISSING'}`);
|
|
console.log(` Ticket Types: ${existingTypes ? `id=${existingTypes.id}` : '❌ MISSING'}`);
|
|
|
|
// Check Kanban widget
|
|
await ensureKanbanWidget(spaceId, projectId, existingTickets.id, existingStates?.id);
|
|
return;
|
|
}
|
|
|
|
// 4. Create Tickets module tables
|
|
console.log('\nCreating Tickets module tables...');
|
|
const result = await createTicketsModuleTables(projectId);
|
|
|
|
console.log(`\n✅ Tables created:`);
|
|
console.log(` Tickets: id=${result.ticketsTableId}`);
|
|
console.log(` Ticket States: id=${result.statesTableId} (${result.stateRowIds.length} states seeded)`);
|
|
console.log(` Ticket Types: id=${result.typesTableId}`);
|
|
|
|
// 5. Create Kanban widget
|
|
await ensureKanbanWidget(spaceId, projectId, result.ticketsTableId, result.statesTableId);
|
|
|
|
console.log('\n=== Tickets Module Setup Complete ===');
|
|
console.log('\nIMPORTANT: Update the following constants in your code if table IDs differ:');
|
|
console.log(` TICKETS_TABLE_ID = ${result.ticketsTableId} (backend/routes/v3/tickets.js)`);
|
|
console.log(` TICKETS_TABLE_ID = ${result.ticketsTableId} (backend/services/ChainHandoffService.js)`);
|
|
console.log(` TICKETS_TABLE_ID = ${result.ticketsTableId} (src/features/tickets/api/ticketsApi.ts)`);
|
|
}
|
|
|
|
/**
|
|
* Ensure a Kanban Board widget exists for the Tickets table.
|
|
* Creates it on the default dashboard if not found.
|
|
*/
|
|
async function ensureKanbanWidget(spaceId, projectId, ticketsTableId, statesTableId) {
|
|
// Find dashboard for this project
|
|
let dashboard = await dbGet(
|
|
'SELECT id FROM dashboards WHERE project_id = ? ORDER BY is_default DESC, id LIMIT 1',
|
|
[projectId]
|
|
);
|
|
|
|
if (!dashboard) {
|
|
// Create default dashboard
|
|
const dashResult = await dbRun(
|
|
isPostgres()
|
|
? `INSERT INTO dashboards (project_id, space_id, name, icon, is_default, created_at, updated_at)
|
|
VALUES ($1, $2, 'Main Dashboard', '📊', true, NOW(), NOW()) RETURNING id`
|
|
: `INSERT INTO dashboards (project_id, space_id, name, icon, is_default, created_at, updated_at)
|
|
VALUES (?, ?, 'Main Dashboard', '📊', 1, datetime('now'), datetime('now'))`,
|
|
[projectId, spaceId]
|
|
);
|
|
const dashId = dashResult.lastInsertRowid || dashResult.lastID || dashResult?.rows?.[0]?.id;
|
|
dashboard = { id: dashId };
|
|
console.log(` Created dashboard: id=${dashId}`);
|
|
}
|
|
|
|
// Check if Kanban widget already exists for this table
|
|
const existingWidget = await dbGet(
|
|
isPostgres()
|
|
? `SELECT id FROM widgets WHERE dashboard_id = $1 AND preset_name = 'kanban_board' AND config::text LIKE $2`
|
|
: `SELECT id FROM widgets WHERE dashboard_id = ? AND preset_name = 'kanban_board' AND config LIKE ?`,
|
|
[dashboard.id, `%"table_id":${ticketsTableId}%`]
|
|
);
|
|
|
|
if (existingWidget) {
|
|
console.log(`\n✅ Kanban widget already exists: id=${existingWidget.id}`);
|
|
return;
|
|
}
|
|
|
|
// Create Kanban widget bound to Tickets table
|
|
const kanbanConfig = {
|
|
table_id: ticketsTableId,
|
|
group_by_column: 'state',
|
|
card_title_column: 'what',
|
|
card_subtitle_column: 'why',
|
|
card_columns: ['assigned_to', 'priority', 'phase', 'adr_ref', 'progress'],
|
|
visible_columns: ['acceptance_criteria', 'chain_id', 'depends_on', 'scheduled_date', 'due_date'],
|
|
kanban: {
|
|
statusColumn: 'state',
|
|
titleColumn: 'what',
|
|
descriptionColumn: 'why',
|
|
scheduledDateColumn: 'scheduled_date',
|
|
dueDateColumn: 'due_date',
|
|
colorColumn: 'priority',
|
|
},
|
|
// ADR-098: Bind to Ticket States table for column headers
|
|
states_table_id: statesTableId,
|
|
};
|
|
|
|
const position = { x: 0, y: 0, w: 12, h: 8 };
|
|
|
|
const widgetResult = await dbRun(
|
|
isPostgres()
|
|
? `INSERT INTO widgets (dashboard_id, widget_type, preset_name, title, description, icon, config, position, is_visible, is_module, created_at, updated_at)
|
|
VALUES ($1, 'preset', 'kanban_board', $2, $3, '📋', $4::jsonb, $5::jsonb, true, true, NOW(), NOW()) RETURNING id`
|
|
: `INSERT INTO widgets (dashboard_id, widget_type, preset_name, title, description, icon, config, position, is_visible, is_module, created_at, updated_at)
|
|
VALUES (?, 'preset', 'kanban_board', ?, ?, '📋', ?, ?, 1, 1, datetime('now'), datetime('now'))`,
|
|
[
|
|
dashboard.id,
|
|
'Tickets Board',
|
|
'ADR-098: Development task board with 7-status workflow',
|
|
JSON.stringify(kanbanConfig),
|
|
JSON.stringify(position),
|
|
]
|
|
);
|
|
const widgetId = widgetResult.lastInsertRowid || widgetResult.lastID || widgetResult?.rows?.[0]?.id;
|
|
|
|
console.log(`\n✅ Kanban widget created: id=${widgetId}`);
|
|
console.log(` Config: table_id=${ticketsTableId}, group_by=state, states_table=${statesTableId}`);
|
|
|
|
// Create module entry for sidebar
|
|
try {
|
|
await dbRun(
|
|
isPostgres()
|
|
? `INSERT INTO modules (widget_id, space_id, sidebar_order, sidebar_icon, is_pinned, created_at, updated_at)
|
|
VALUES ($1, $2, 0, '🎫', true, NOW(), NOW())`
|
|
: `INSERT INTO modules (widget_id, space_id, sidebar_order, sidebar_icon, is_pinned, created_at, updated_at)
|
|
VALUES (?, ?, 0, '🎫', 1, datetime('now'), datetime('now'))`,
|
|
[widgetId, spaceId]
|
|
);
|
|
console.log(` Module entry created (sidebar, pinned)`);
|
|
} catch (err) {
|
|
console.log(` Module entry: skipped (${err.message})`);
|
|
}
|
|
}
|
|
|
|
main()
|
|
.then(() => process.exit(0))
|
|
.catch((err) => {
|
|
console.error('Setup failed:', err);
|
|
process.exit(1);
|
|
});
|