refactor(inbox): derive from snapshot; delete getInbox IPC chain

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.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-18 21:42:58 -04:00
1 parent 487f2456ff
commit 6abc1a823f
11 files changed
+180 -220

No files matched your search

+1 -186
View File
@@ -1,9 +1,6 @@
import { randomUUID } from "node:crypto";
import type { DatabaseSync } from "node:sqlite";
import type {
InboxItem,
ProfileSnapshot,
} from "@shared/types";
import type { ProfileSnapshot } from "@shared/types";
import {
type ReadAll,
type Transaction,
@@ -169,187 +166,5 @@ export function buildCompanyCrudMethods(
reviewDeliverables: row.review_deliverables === 1,
};
},
getInbox(companyId: string): InboxItem[] {
const items: InboxItem[] = [];
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const agentRows = readAll<{ id: string; name: string }>(
"select id, name from agents where company_id = ?",
companyId,
);
const agentNameMap = new Map(agentRows.map((a) => [a.id, a.name]));
const pendingApprovals = readAll<{
id: string; type: string; payloadSummary: string;
requestedByAgentId: string | null; createdAt: string;
}>(
`select id, type, payload_summary as payloadSummary,
requested_by_agent_id as requestedByAgentId, created_at as createdAt
from approvals where company_id = ? and state = 'pending'
order by created_at desc`,
companyId,
);
for (const approval of pendingApprovals) {
const requesterName = approval.requestedByAgentId
? agentNameMap.get(approval.requestedByAgentId) ?? null
: null;
items.push({
id: `approval-${approval.id}`,
type: "pending_approval",
title: approval.payloadSummary,
subtitle: `${approval.type.replaceAll("_", " ")} — requested by ${requesterName ?? "Unknown"}`,
severity: approval.type === "dangerous_command" ? "critical" : "high",
entityType: "approval",
entityId: approval.id,
agentId: approval.requestedByAgentId,
agentName: requesterName,
createdAt: approval.createdAt,
});
}
const failedRuns = readAll<{
id: string; agentId: string; summary: string;
errorMessage: string | null; createdAt: string;
}>(
`select id, agent_id as agentId, summary, error_message as errorMessage,
created_at as createdAt
from runs
where company_id = ?
and status in ('failed', 'timed_out')
and created_at > ?
order by created_at desc`,
companyId,
oneDayAgo,
);
for (const run of failedRuns) {
const agentName = agentNameMap.get(run.agentId) ?? null;
items.push({
id: `run-${run.id}`,
type: "failed_run",
title: run.summary || "Run failed",
subtitle: `${agentName ?? "Unknown agent"} — ${run.errorMessage?.slice(0, 100) ?? "Unknown error"}`,
severity: "high",
entityType: "run",
entityId: run.id,
agentId: run.agentId,
agentName,
createdAt: run.createdAt,
});
}
const budgetAlertRows = readAll<{
id: string; name: string; budgetMonthlyUsd: number; spentMonthlyUsd: number; updatedAt: string;
}>(
`select id, name, budget_monthly_usd as budgetMonthlyUsd,
spent_monthly_usd as spentMonthlyUsd, updated_at as updatedAt
from agents
where company_id = ?
and budget_monthly_usd > 0
and (spent_monthly_usd * 1.0 / budget_monthly_usd) > 0.8`,
companyId,
);
for (const agent of budgetAlertRows) {
const pct = Math.round((agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100);
items.push({
id: `budget-${agent.id}`,
type: "budget_warning",
title: `${agent.name} at ${pct}% budget`,
subtitle: `$${agent.spentMonthlyUsd.toFixed(2)} of $${agent.budgetMonthlyUsd.toFixed(2)} monthly limit`,
severity: pct >= 100 ? "critical" : "high",
entityType: "agent",
entityId: agent.id,
agentId: agent.id,
agentName: agent.name,
createdAt: agent.updatedAt,
});
}
const taskAlertRows = readAll<{
id: string; title: string; status: string; priority: string;
assigneeAgentId: string | null; updatedAt: string;
}>(
`select id, title, status, priority,
assignee_agent_id as assigneeAgentId, updated_at as updatedAt
from tasks
where company_id = ? and status in ('blocked', 'in_review')
order by updated_at desc`,
companyId,
);
for (const task of taskAlertRows) {
const assigneeName = task.assigneeAgentId ? agentNameMap.get(task.assigneeAgentId) ?? null : null;
if (task.status === "blocked") {
items.push({
id: `blocked-${task.id}`,
type: "blocked_task",
title: task.title,
subtitle: `Assigned to ${assigneeName ?? "Unassigned"} — blocked`,
severity: task.priority === "critical" ? "critical" : "medium",
entityType: "task",
entityId: task.id,
agentId: task.assigneeAgentId,
agentName: assigneeName,
createdAt: task.updatedAt,
});
} else {
items.push({
id: `review-${task.id}`,
type: "review_needed",
title: task.title,
subtitle: `Submitted by ${assigneeName ?? "Unknown"} — awaiting review`,
severity: task.priority === "critical" ? "high" : "medium",
entityType: "task",
entityId: task.id,
agentId: task.assigneeAgentId,
agentName: assigneeName,
createdAt: task.updatedAt,
});
}
}
const messageRows = readAll<{
id: string; fromAgentId: string; subject: string; channel: string;
priority: string; readAt: string | null; createdAt: string;
}>(
`select messages.id as id,
messages.from_agent_id as fromAgentId,
messages.subject as subject,
messages.channel as channel,
messages.priority as priority,
reads.read_at as readAt,
messages.created_at as createdAt
from agent_messages messages
left join agent_message_reads reads
on reads.message_id = messages.id and reads.agent_id = '__board__'
where messages.company_id = ?
and reads.read_at is null
and (messages.channel = 'company' or messages.channel = 'incident' or messages.priority = 'urgent')
order by messages.created_at desc`,
companyId,
);
for (const msg of messageRows) {
const senderName = agentNameMap.get(msg.fromAgentId) ?? null;
items.push({
id: `message-${msg.id}`,
type: "agent_message",
title: msg.subject,
subtitle: `${senderName ?? "Unknown"} — ${msg.channel === "incident" ? "INCIDENT" : msg.channel} ${msg.priority === "urgent" ? "(urgent)" : ""}`,
severity: msg.priority === "urgent" || msg.channel === "incident" ? "critical" : "medium",
entityType: "message",
entityId: msg.id,
agentId: msg.fromAgentId,
agentName: senderName,
createdAt: msg.createdAt,
});
}
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
items.sort(
(a, b) =>
severityOrder[a.severity] - severityOrder[b.severity] ||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
return items;
},
};
}