Sibling refactor to the sidebar-badges cleanup. The inbox aggregates five item categories (pending approvals, failed runs, budget warnings, blocked/review tasks, unread company/incident/urgent messages) — every row is already in `ProfileSnapshot`. The server-side `getInbox` aggregation (180 lines of SQL + projection) was a redundant second path. New: `src/shared/inbox-items.ts::deriveInboxItems(snapshot, companyId)` produces the same InboxItem[] shape with the same severity ordering and 24h failed-run window. Pure function, no IPC, no state. Deleted: - database/company-crud.ts::getInbox (180-line aggregation) - database-base.ts::getInbox pass-through - ipc-core-read-handlers.ts: handler - preload/index.ts: bridge binding - shared/contracts/channels.ts: channel name - shared/contracts/desktop-api/companies.ts: API signature - state/slices/company.ts: `inboxItems` field, `setInboxItems` action - useDataLoader.ts: both getInbox call sites (initial snapshot load and section-change effect) - useCompanyController.ts: inbox reset on company switch OperationsSections now computes `inboxItems` via `useMemo` off the current snapshot + company id. The snapshot is already company-scoped at the main/database/snapshot.ts layer when `currentCompanyId` is non-null; the derivation re-filters defensively so the function works correctly even for unscoped snapshots. Tests: 603/603 green. No new tests added — the derivation is a pure function whose shape is exercised end-to-end by the existing Inbox component integration.
171 lines
7.4 KiB
TypeScript
171 lines
7.4 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { DatabaseSync } from "node:sqlite";
|
|
import type { ProfileSnapshot } from "@shared/types";
|
|
import {
|
|
type ReadAll,
|
|
type Transaction,
|
|
nowIso,
|
|
} from "./helpers";
|
|
|
|
export function buildCompanyCrudMethods(
|
|
getDb: () => DatabaseSync,
|
|
readAll: ReadAll,
|
|
transaction: Transaction,
|
|
deps: {
|
|
getSetting: (key: string) => string | null;
|
|
setSetting: (key: string, value: string) => void;
|
|
listSnapshot: (options: { companyId?: string | null }) => ProfileSnapshot;
|
|
addComment: (input: { companyId: string; taskId: string; authorAgentId: string | null; authorName: string; body: string }) => void;
|
|
},
|
|
) {
|
|
return {
|
|
saveCompany(input: {
|
|
id?: string | null;
|
|
name: string;
|
|
description: string;
|
|
status: string;
|
|
autoApproveHires?: boolean;
|
|
reviewDeliverables?: boolean;
|
|
executionMode?: string;
|
|
}): string {
|
|
const id = input.id && input.id.length > 0 ? input.id : randomUUID();
|
|
const ts = nowIso();
|
|
const autoApprove = input.autoApproveHires ? 1 : 0;
|
|
const reviewDeliverables = input.reviewDeliverables ? 1 : 0;
|
|
const executionMode = input.executionMode ?? "ai_ceo";
|
|
const existing = getDb()
|
|
.prepare("select id from companies where id = ?")
|
|
.get(id) as { id: string } | undefined;
|
|
if (existing) {
|
|
getDb()
|
|
.prepare(
|
|
"update companies set name = ?, description = ?, status = ?, auto_approve_hires = ?, review_deliverables = ?, execution_mode = ?, updated_at = ? where id = ?",
|
|
)
|
|
.run(input.name, input.description, input.status, autoApprove, reviewDeliverables, executionMode, ts, id);
|
|
} else {
|
|
getDb()
|
|
.prepare(
|
|
"insert into companies (id, name, description, status, auto_approve_hires, review_deliverables, execution_mode, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
)
|
|
.run(id, input.name, input.description, input.status, autoApprove, reviewDeliverables, executionMode, ts, ts);
|
|
if (!deps.getSetting("current_company_id")) {
|
|
deps.setSetting("current_company_id", id);
|
|
}
|
|
}
|
|
return id;
|
|
},
|
|
|
|
deleteCompany(companyId: string): void {
|
|
transaction(() => {
|
|
try {
|
|
getDb()
|
|
.prepare(
|
|
"delete from agent_message_reads where message_id in (select id from agent_messages where company_id = ?)",
|
|
)
|
|
.run(companyId);
|
|
} catch (err) {
|
|
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
|
|
}
|
|
// New table cleanup (budget_incidents before budget_policies due to FK)
|
|
getDb().prepare("delete from budget_incidents where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from budget_policies where company_id = ?").run(companyId);
|
|
// agent_skill_assignments references company_skills -- delete before skills
|
|
try {
|
|
getDb()
|
|
.prepare(
|
|
"delete from agent_skill_assignments where skill_id in (select id from company_skills where company_id = ?)",
|
|
)
|
|
.run(companyId);
|
|
} catch (err) {
|
|
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
|
|
}
|
|
getDb().prepare("delete from company_skills where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from task_sessions where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from wakeup_requests where company_id = ?").run(companyId);
|
|
// Thread runtime tables (events + participants cascade from threads via FK)
|
|
getDb().prepare("delete from thread_wait_states where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from conversation_threads where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from cost_entries where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from activity where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from task_comments where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from runs where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from approvals where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from tasks where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from projects where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from goals where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from agents where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from secrets where company_id = ?").run(companyId);
|
|
getDb().prepare("delete from workspaces where company_id = ?").run(companyId);
|
|
for (const table of [
|
|
"automation_rules",
|
|
"automation_log",
|
|
"agent_messages",
|
|
"workflow_pipelines",
|
|
"meetings",
|
|
"documents",
|
|
"knowledge_base",
|
|
"sprints",
|
|
"browser_actions",
|
|
"social_accounts",
|
|
]) {
|
|
try {
|
|
getDb().prepare(`delete from ${table} where company_id = ?`).run(companyId);
|
|
} catch (err) {
|
|
// Only tolerate "no such table" — re-throw anything else (disk error, etc.)
|
|
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
|
|
}
|
|
}
|
|
getDb().prepare("delete from companies where id = ?").run(companyId);
|
|
if (deps.getSetting("current_company_id") === companyId) {
|
|
const next = getDb()
|
|
.prepare("select id from companies order by created_at asc limit 1")
|
|
.get() as { id: string } | undefined;
|
|
if (next) {
|
|
deps.setSetting("current_company_id", next.id);
|
|
} else {
|
|
deps.setSetting("current_company_id", "");
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
getCompanyStatus(companyId: string): string | null {
|
|
const row = getDb()
|
|
.prepare("select status from companies where id = ?")
|
|
.get(companyId) as { status: string } | undefined;
|
|
return row?.status ?? null;
|
|
},
|
|
|
|
getCompanyBasic(companyId: string): { name: string; description: string } | null {
|
|
const row = getDb()
|
|
.prepare("select name, description from companies where id = ?")
|
|
.get(companyId) as { name: string; description: string } | undefined;
|
|
return row ?? null;
|
|
},
|
|
|
|
listActiveCompanyIds(): Array<{ id: string; name: string }> {
|
|
return readAll<{ id: string; name: string }>(
|
|
"select id, name from companies where status = 'active'",
|
|
);
|
|
},
|
|
|
|
getCompanyExecutionMode(companyId: string): string {
|
|
const row = getDb()
|
|
.prepare("SELECT execution_mode FROM companies WHERE id = ? LIMIT 1")
|
|
.get(companyId) as { execution_mode: string } | undefined;
|
|
return row?.execution_mode ?? "ai_ceo";
|
|
},
|
|
|
|
getCompanyApprovalSettings(companyId: string): { autoApproveHires: boolean; reviewDeliverables: boolean } | null {
|
|
const row = getDb()
|
|
.prepare("SELECT auto_approve_hires, review_deliverables FROM companies WHERE id = ? LIMIT 1")
|
|
.get(companyId) as { auto_approve_hires: number; review_deliverables: number } | undefined;
|
|
if (!row) return null;
|
|
return {
|
|
autoApproveHires: row.auto_approve_hires === 1,
|
|
reviewDeliverables: row.review_deliverables === 1,
|
|
};
|
|
},
|
|
};
|
|
}
|