godcrm/backend/scripts/setup-tickets-module.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

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);
});