diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index 94b4b46c..920ad6f7 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -198,6 +198,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { ipcMain.handle(IPC_CHANNELS.createMigrationReport, (_event, input) => mutate(() => migrations.createReport(input))); ipcMain.handle(IPC_CHANNELS.createRollbackSnapshot, (_event, input) => mutate(() => rollback.createSnapshot(input))); ipcMain.handle(IPC_CHANNELS.setRollbackSnapshotActive, (_event, input) => mutate(() => rollback.setActive(input))); + ipcMain.handle(IPC_CHANNELS.resolveMigrationReconciliation, (_event, input) => mutate(() => rollback.resolveReconciliationItem(input))); ipcMain.handle(IPC_CHANNELS.setFeatureFlag, (_event, input) => mutate(() => operations.setFeatureFlag(input))); ipcMain.handle(IPC_CHANNELS.runChaosDrill, (_event, input) => mutate(() => operations.runChaosDrill(input))); ipcMain.handle(IPC_CHANNELS.runSdlcReview, (_event, input) => mutate(() => sdlc.runReview(input))); diff --git a/AgentCompany-New/src/main/services/rollback-service.ts b/AgentCompany-New/src/main/services/rollback-service.ts index 6deb8c6e..944f4afa 100644 --- a/AgentCompany-New/src/main/services/rollback-service.ts +++ b/AgentCompany-New/src/main/services/rollback-service.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { closeSync, existsSync, openSync, readSync, realpathSync } from "node:fs"; import type { DatabaseSync } from "node:sqlite"; -import type { CreateRollbackSnapshotInput, SetRollbackSnapshotActiveInput } from "../../shared/commands"; +import type { CreateRollbackSnapshotInput, ResolveMigrationReconciliationInput, SetRollbackSnapshotActiveInput } from "../../shared/commands"; import { createId } from "../../shared/ids"; import { AuditService } from "./audit-service"; import { MigrationCutoverJournalService, type CutoverDeltaEvent } from "./migration-cutover-journal-service"; @@ -39,6 +39,15 @@ interface RollbackSnapshotRow { delta_journal_json: string; } +interface MigrationReconciliationItemRow { + id: string; + migration_report_id: string; + rollback_snapshot_id: string | null; + target_type: string; + target_id: string; + state: string; +} + interface FileRow { id: string; name: string; @@ -175,6 +184,43 @@ export class RollbackService { }); } + resolveReconciliationItem(input: ResolveMigrationReconciliationInput): void { + this.requireEmployee(input.orgId, input.employeeId); + const item = this.db.prepare(` + select id, migration_report_id, rollback_snapshot_id, target_type, target_id, state + from migration_reconciliation_items + where id = ? and org_id = ? + `).get(input.reconciliationItemId, input.orgId) as MigrationReconciliationItemRow | undefined; + if (!item) throw new Error("Migration reconciliation item was not found."); + if (item.state === "resolved") throw new Error("Migration reconciliation item is already resolved."); + const now = new Date().toISOString(); + const result = this.db.prepare(` + update migration_reconciliation_items + set state = ?, resolved_at = ?, resolved_by_employee_id = ? + where id = ? and org_id = ? and state = ? + `).run("resolved", now, input.employeeId, item.id, input.orgId, "open"); + if (result.changes === 0) throw new Error("Migration reconciliation item could not be resolved."); + this.audit.record({ + orgId: input.orgId, + entityType: "migration_reconciliation_item", + entityId: item.id, + actorType: "employee", + actorId: input.employeeId, + action: "resolved", + payload: { targetType: item.target_type, targetId: item.target_id, rollbackSnapshotId: item.rollback_snapshot_id }, + }); + this.cutoverJournal.recordEvent({ + orgId: input.orgId, + migrationReportId: item.migration_report_id, + rollbackSnapshotId: item.rollback_snapshot_id, + eventType: "migration_reconciliation_resolved", + actorType: "employee", + actorId: input.employeeId, + payload: { reconciliationItemId: item.id, targetType: item.target_type, targetId: item.target_id }, + createdAt: now, + }); + } + private requireReport(orgId: string, reportId: string): MigrationReportRow { const report = this.db.prepare("select id, source_ref, report_json, created_at from migration_reports where id = ? and org_id = ?") .get(reportId, orgId) as MigrationReportRow | undefined; diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index 0676cf5c..cb0e6c68 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -46,6 +46,7 @@ const api: DesktopApi = { createMigrationReport: (input) => invoke(IPC_CHANNELS.createMigrationReport, input), createRollbackSnapshot: (input) => invoke(IPC_CHANNELS.createRollbackSnapshot, input), setRollbackSnapshotActive: (input) => invoke(IPC_CHANNELS.setRollbackSnapshotActive, input), + resolveMigrationReconciliation: (input) => invoke(IPC_CHANNELS.resolveMigrationReconciliation, input), setFeatureFlag: (input) => invoke(IPC_CHANNELS.setFeatureFlag, input), runChaosDrill: (input) => invoke(IPC_CHANNELS.runChaosDrill, input), runSdlcReview: (input) => invoke(IPC_CHANNELS.runSdlcReview, input), diff --git a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx index 88ad0ff2..540d8153 100644 --- a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx @@ -209,6 +209,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) { employeeId={employee.id} reports={migrationReports} rollbackSnapshots={rollbackSnapshots} + reconciliationItems={snapshot.migrationReconciliationItems.filter((item) => item.orgId === orgId)} actions={actions} /> )} diff --git a/AgentCompany-New/src/renderer/src/pages/settings/MigrationPanel.tsx b/AgentCompany-New/src/renderer/src/pages/settings/MigrationPanel.tsx index a6a83e19..8d21529d 100644 --- a/AgentCompany-New/src/renderer/src/pages/settings/MigrationPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/settings/MigrationPanel.tsx @@ -1,7 +1,7 @@ import { useState, type FormEvent } from "react"; import { Button, Field, Icon, Pill } from "../../shared/ui"; import { Dialog } from "../../shared/Dialog"; -import type { MigrationReportRecord, RollbackSnapshotRecord } from "../../../../shared/domain"; +import type { MigrationReconciliationItemRecord, MigrationReportRecord, RollbackSnapshotRecord } from "../../../../shared/domain"; import type { SnapshotActions } from "../../app/useSnapshot"; import { formatRelative } from "../types"; @@ -18,12 +18,14 @@ export function MigrationPanel({ employeeId, reports, rollbackSnapshots, + reconciliationItems, actions, }: { orgId: string; employeeId: string; reports: MigrationReportRecord[]; rollbackSnapshots: RollbackSnapshotRecord[]; + reconciliationItems: MigrationReconciliationItemRecord[]; actions: SnapshotActions; }) { const [open, setOpen] = useState(false); @@ -78,6 +80,22 @@ export function MigrationPanel({ /> )) :