feat(assistant): extend audit and import parity
This commit is contained in:
@@ -70,6 +70,13 @@ Es gibt aktuell vier Permission-/Scope-Ebenen:
|
|||||||
- `get_project_timeline_context`
|
- `get_project_timeline_context`
|
||||||
- `preview_project_shift`
|
- `preview_project_shift`
|
||||||
- basiert bereits auf denselben Timeline-Readmodels/Shift-Preview-Helfern wie die UI
|
- basiert bereits auf denselben Timeline-Readmodels/Shift-Preview-Helfern wie die UI
|
||||||
|
- Import/Export / Dispo:
|
||||||
|
- `export_resources_csv`
|
||||||
|
- `export_projects_csv`
|
||||||
|
- `import_csv_data`
|
||||||
|
- `list_dispo_import_batches`
|
||||||
|
- `get_dispo_import_batch`
|
||||||
|
- damit sind CSV-Export, CSV-Import und die Batch-Uebersicht der Dispo-Importe jetzt ueber echte Router-Pfade verfuegbar
|
||||||
- Estimates: nur Suche, Detail und Anlegen, aber kein voller Lifecycle
|
- Estimates: nur Suche, Detail und Anlegen, aber kein voller Lifecycle
|
||||||
- Reports: `run_report` ist flexibel, deckt aber nicht die spezialisierten Report-/Analyse-Readmodels ab
|
- Reports: `run_report` ist flexibel, deckt aber nicht die spezialisierten Report-/Analyse-Readmodels ab
|
||||||
- Chargeability / Transparenz:
|
- Chargeability / Transparenz:
|
||||||
@@ -77,7 +84,10 @@ Es gibt aktuell vier Permission-/Scope-Ebenen:
|
|||||||
- `get_resource_computation_graph`
|
- `get_resource_computation_graph`
|
||||||
- `get_project_computation_graph`
|
- `get_project_computation_graph`
|
||||||
- damit sind die wichtigsten tiefen Herleitungen fuer Chargeability, SAH, Feiertagsabzuege und Projektkalkulation jetzt auch im Assistant verfuegbar
|
- damit sind die wichtigsten tiefen Herleitungen fuer Chargeability, SAH, Feiertagsabzuege und Projektkalkulation jetzt auch im Assistant verfuegbar
|
||||||
- Audit/History: nur einfache History-Abfragen, keine volle Audit-API
|
- Audit/History:
|
||||||
|
- vereinfachte History-Abfragen
|
||||||
|
- echte Audit-API fuer Liste, Detail, Entity-History, Timeline und Activity Summary
|
||||||
|
- Governance-Workbench ausserhalb des Chats bleibt offen
|
||||||
- Notification/Tasking: Kernfaelle vorhanden, aber keine volle Reminder-/Task-/Notification-Paritaet
|
- Notification/Tasking: Kernfaelle vorhanden, aber keine volle Reminder-/Task-/Notification-Paritaet
|
||||||
- Country-/Location-Stammdaten: nur lesend und auch dort nur flach
|
- Country-/Location-Stammdaten: nur lesend und auch dort nur flach
|
||||||
- Insights: Summary-Ebene vorhanden, Drilldowns fehlen
|
- Insights: Summary-Ebene vorhanden, Drilldowns fehlen
|
||||||
@@ -220,12 +230,18 @@ Konsequenz:
|
|||||||
|
|
||||||
Aktuell im Assistant vorhanden:
|
Aktuell im Assistant vorhanden:
|
||||||
|
|
||||||
- vereinfachte History-Suche (`query_change_history`)
|
- vereinfachte History-Suche (`query_change_history`) jetzt auf Basis von `auditLogRouter.list`
|
||||||
- Entity-History (`get_entity_timeline`)
|
- Entity-History (`get_entity_timeline`) jetzt auf Basis von `auditLogRouter.getByEntity`
|
||||||
|
- vollstaendige Audit-Readmodel-Paritaet:
|
||||||
|
- `list_audit_log_entries`
|
||||||
|
- `get_audit_log_entry`
|
||||||
|
- `get_audit_log_timeline`
|
||||||
|
- `get_audit_activity_summary`
|
||||||
|
|
||||||
Fehlend:
|
Fehlend:
|
||||||
|
|
||||||
- die vollstaendige Governance-/Revisionstiefe der Audit-Oberflaeche
|
- dedizierte Governance-/Approval-Oberflaechen ausserhalb des Chats
|
||||||
|
- eine eigenstaendige Revisions-Workbench fuer offene Freigaben und operative Nachverfolgung
|
||||||
|
|
||||||
### Admin- und Systemkonfiguration
|
### Admin- und Systemkonfiguration
|
||||||
|
|
||||||
@@ -311,23 +327,20 @@ Fehlend:
|
|||||||
|
|
||||||
### Komplett fehlende Router-Paritaet
|
### Komplett fehlende Router-Paritaet
|
||||||
|
|
||||||
- `importExport`
|
|
||||||
- `chargeabilityReport`
|
|
||||||
- `computationGraph`
|
|
||||||
- `settings`
|
- `settings`
|
||||||
- `systemRoleConfig`
|
- `systemRoleConfig`
|
||||||
- `webhook`
|
- `webhook`
|
||||||
- `dispo`
|
|
||||||
|
|
||||||
### Deutlich unvollstaendige Router-Paritaet
|
### Deutlich unvollstaendige Router-Paritaet
|
||||||
|
|
||||||
- `timeline` (read-only Kernfaelle vorhanden, Write-Paritaet fehlt)
|
- `importExport`
|
||||||
|
- `dispo`
|
||||||
|
- `timeline` (Kern-Readmodels und wichtigste Write-Paritaet vorhanden, Spezial-Workflows fehlen)
|
||||||
- `vacation`
|
- `vacation`
|
||||||
- `estimate`
|
- `estimate`
|
||||||
- `notification`
|
- `notification`
|
||||||
- `user`
|
- `user`
|
||||||
- `country`
|
- `country`
|
||||||
- `auditLog`
|
|
||||||
- `insights`
|
- `insights`
|
||||||
- `scenario`
|
- `scenario`
|
||||||
- `resource`
|
- `resource`
|
||||||
|
|||||||
@@ -222,9 +222,25 @@ describe("assistant router tool gating", () => {
|
|||||||
PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS,
|
PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS,
|
||||||
], SystemRole.USER);
|
], SystemRole.USER);
|
||||||
|
|
||||||
|
expect(controllerNames).toContain("query_change_history");
|
||||||
|
expect(controllerNames).toContain("get_entity_timeline");
|
||||||
|
expect(controllerNames).toContain("export_resources_csv");
|
||||||
|
expect(controllerNames).toContain("export_projects_csv");
|
||||||
|
expect(controllerNames).toContain("list_audit_log_entries");
|
||||||
|
expect(controllerNames).toContain("get_audit_log_entry");
|
||||||
|
expect(controllerNames).toContain("get_audit_log_timeline");
|
||||||
|
expect(controllerNames).toContain("get_audit_activity_summary");
|
||||||
expect(controllerNames).toContain("get_chargeability_report");
|
expect(controllerNames).toContain("get_chargeability_report");
|
||||||
expect(controllerNames).toContain("get_resource_computation_graph");
|
expect(controllerNames).toContain("get_resource_computation_graph");
|
||||||
expect(controllerNames).toContain("get_project_computation_graph");
|
expect(controllerNames).toContain("get_project_computation_graph");
|
||||||
|
expect(userNames).not.toContain("query_change_history");
|
||||||
|
expect(userNames).not.toContain("get_entity_timeline");
|
||||||
|
expect(userNames).not.toContain("export_resources_csv");
|
||||||
|
expect(userNames).not.toContain("export_projects_csv");
|
||||||
|
expect(userNames).not.toContain("list_audit_log_entries");
|
||||||
|
expect(userNames).not.toContain("get_audit_log_entry");
|
||||||
|
expect(userNames).not.toContain("get_audit_log_timeline");
|
||||||
|
expect(userNames).not.toContain("get_audit_activity_summary");
|
||||||
expect(userNames).not.toContain("get_chargeability_report");
|
expect(userNames).not.toContain("get_chargeability_report");
|
||||||
expect(userNames).not.toContain("get_resource_computation_graph");
|
expect(userNames).not.toContain("get_resource_computation_graph");
|
||||||
expect(userNames).not.toContain("get_project_computation_graph");
|
expect(userNames).not.toContain("get_project_computation_graph");
|
||||||
@@ -257,6 +273,24 @@ describe("assistant router tool gating", () => {
|
|||||||
expect(missingAdvancedNames).not.toContain("quick_assign_timeline_resource");
|
expect(missingAdvancedNames).not.toContain("quick_assign_timeline_resource");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps import/dispo parity tools aligned to router roles and permissions", () => {
|
||||||
|
const managerNames = getToolNames([PermissionKey.IMPORT_DATA], SystemRole.MANAGER);
|
||||||
|
const controllerNames = getToolNames([], SystemRole.CONTROLLER);
|
||||||
|
const adminNames = getToolNames([], SystemRole.ADMIN);
|
||||||
|
const userNames = getToolNames([PermissionKey.IMPORT_DATA], SystemRole.USER);
|
||||||
|
|
||||||
|
expect(managerNames).toContain("import_csv_data");
|
||||||
|
expect(controllerNames).toContain("export_resources_csv");
|
||||||
|
expect(controllerNames).toContain("export_projects_csv");
|
||||||
|
expect(adminNames).toContain("list_dispo_import_batches");
|
||||||
|
expect(adminNames).toContain("get_dispo_import_batch");
|
||||||
|
expect(userNames).not.toContain("import_csv_data");
|
||||||
|
expect(userNames).not.toContain("export_resources_csv");
|
||||||
|
expect(userNames).not.toContain("export_projects_csv");
|
||||||
|
expect(userNames).not.toContain("list_dispo_import_batches");
|
||||||
|
expect(userNames).not.toContain("get_dispo_import_batch");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps holiday calendar mutation tools admin-only while leaving read tools available", () => {
|
it("keeps holiday calendar mutation tools admin-only while leaving read tools available", () => {
|
||||||
const adminNames = getToolNames([], SystemRole.ADMIN);
|
const adminNames = getToolNames([], SystemRole.ADMIN);
|
||||||
const managerNames = getToolNames([], SystemRole.MANAGER);
|
const managerNames = getToolNames([], SystemRole.MANAGER);
|
||||||
@@ -492,6 +526,18 @@ describe("assistant router tool gating", () => {
|
|||||||
expect(toolDescriptions.get("send_broadcast")).toContain("manageProjects");
|
expect(toolDescriptions.get("send_broadcast")).toContain("manageProjects");
|
||||||
expect(toolDescriptions.get("create_holiday_calendar")).toContain("Admin role");
|
expect(toolDescriptions.get("create_holiday_calendar")).toContain("Admin role");
|
||||||
expect(toolDescriptions.get("create_holiday_calendar_entry")).toContain("Admin role");
|
expect(toolDescriptions.get("create_holiday_calendar_entry")).toContain("Admin role");
|
||||||
|
expect(toolDescriptions.get("query_change_history")).toContain("Controller/manager/admin");
|
||||||
|
expect(toolDescriptions.get("get_entity_timeline")).toContain("Controller/manager/admin");
|
||||||
|
expect(toolDescriptions.get("export_resources_csv")).toContain("Controller/manager/admin");
|
||||||
|
expect(toolDescriptions.get("export_projects_csv")).toContain("Controller/manager/admin");
|
||||||
|
expect(toolDescriptions.get("import_csv_data")).toContain("importData");
|
||||||
|
expect(toolDescriptions.get("import_csv_data")).toContain("manager/admin");
|
||||||
|
expect(toolDescriptions.get("list_dispo_import_batches")).toContain("Admin role");
|
||||||
|
expect(toolDescriptions.get("get_dispo_import_batch")).toContain("Admin role");
|
||||||
|
expect(toolDescriptions.get("list_audit_log_entries")).toContain("Controller/manager/admin");
|
||||||
|
expect(toolDescriptions.get("get_audit_log_entry")).toContain("Controller/manager/admin");
|
||||||
|
expect(toolDescriptions.get("get_audit_log_timeline")).toContain("Controller/manager/admin");
|
||||||
|
expect(toolDescriptions.get("get_audit_activity_summary")).toContain("Controller/manager/admin");
|
||||||
expect(toolDescriptions.get("get_chargeability_report")).toContain("controller/manager/admin");
|
expect(toolDescriptions.get("get_chargeability_report")).toContain("controller/manager/admin");
|
||||||
expect(toolDescriptions.get("get_chargeability_report")).toContain("viewCosts");
|
expect(toolDescriptions.get("get_chargeability_report")).toContain("viewCosts");
|
||||||
expect(toolDescriptions.get("get_resource_computation_graph")).toContain("useAssistantAdvancedTools");
|
expect(toolDescriptions.get("get_resource_computation_graph")).toContain("useAssistantAdvancedTools");
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { SystemRole } from "@capakraken/shared";
|
||||||
|
|
||||||
|
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
|
||||||
|
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
|
||||||
|
listAssignmentBookings: vi.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { executeTool, type ToolContext } from "../router/assistant-tools.js";
|
||||||
|
|
||||||
|
function createToolContext(
|
||||||
|
db: Record<string, unknown>,
|
||||||
|
userRole: SystemRole = SystemRole.CONTROLLER,
|
||||||
|
): ToolContext {
|
||||||
|
return {
|
||||||
|
db: db as ToolContext["db"],
|
||||||
|
userId: "user_1",
|
||||||
|
userRole,
|
||||||
|
permissions: new Set(),
|
||||||
|
session: {
|
||||||
|
user: { email: "assistant@example.com", name: "Assistant User", image: null },
|
||||||
|
expires: "2026-03-29T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
dbUser: {
|
||||||
|
id: "user_1",
|
||||||
|
systemRole: userRole,
|
||||||
|
permissionOverrides: null,
|
||||||
|
},
|
||||||
|
roleDefaults: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant audit tools", () => {
|
||||||
|
it("lists audit entries through the real audit router path", async () => {
|
||||||
|
const ctx = createToolContext({
|
||||||
|
auditLog: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "audit_1",
|
||||||
|
entityType: "Project",
|
||||||
|
entityId: "project_1",
|
||||||
|
entityName: "Gelddruckmaschine",
|
||||||
|
action: "UPDATE",
|
||||||
|
userId: "user_1",
|
||||||
|
source: "ui",
|
||||||
|
summary: "Updated project dates",
|
||||||
|
createdAt: new Date("2026-03-28T10:00:00.000Z"),
|
||||||
|
user: {
|
||||||
|
id: "user_1",
|
||||||
|
name: "Larissa",
|
||||||
|
email: "larissa@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeTool(
|
||||||
|
"list_audit_log_entries",
|
||||||
|
JSON.stringify({
|
||||||
|
entityType: "Project",
|
||||||
|
search: "Gelddruckmaschine",
|
||||||
|
limit: 10,
|
||||||
|
}),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual({
|
||||||
|
filters: {
|
||||||
|
entityType: "Project",
|
||||||
|
entityId: null,
|
||||||
|
userId: null,
|
||||||
|
action: null,
|
||||||
|
source: null,
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
search: "Gelddruckmaschine",
|
||||||
|
},
|
||||||
|
itemCount: 1,
|
||||||
|
nextCursor: null,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "audit_1",
|
||||||
|
entityType: "Project",
|
||||||
|
entityId: "project_1",
|
||||||
|
entityName: "Gelddruckmaschine",
|
||||||
|
action: "UPDATE",
|
||||||
|
userId: "user_1",
|
||||||
|
source: "ui",
|
||||||
|
summary: "Updated project dates",
|
||||||
|
createdAt: "2026-03-28T10:00:00.000Z",
|
||||||
|
user: {
|
||||||
|
id: "user_1",
|
||||||
|
name: "Larissa",
|
||||||
|
email: "larissa@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces controller access for audit tools via the backing router", async () => {
|
||||||
|
const ctx = createToolContext(
|
||||||
|
{
|
||||||
|
auditLog: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SystemRole.USER,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await executeTool(
|
||||||
|
"query_change_history",
|
||||||
|
JSON.stringify({ entityType: "Project" }),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
error: expect.stringContaining("Controller access required"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||||
|
|
||||||
|
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
|
||||||
|
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
|
||||||
|
listAssignmentBookings: vi.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { executeTool, type ToolContext } from "../router/assistant-tools.js";
|
||||||
|
|
||||||
|
function createToolContext(
|
||||||
|
db: Record<string, unknown>,
|
||||||
|
options?: {
|
||||||
|
permissions?: PermissionKey[];
|
||||||
|
userRole?: SystemRole;
|
||||||
|
},
|
||||||
|
): ToolContext {
|
||||||
|
const userRole = options?.userRole ?? SystemRole.ADMIN;
|
||||||
|
return {
|
||||||
|
db: db as ToolContext["db"],
|
||||||
|
userId: "user_1",
|
||||||
|
userRole,
|
||||||
|
permissions: new Set(options?.permissions ?? []),
|
||||||
|
session: {
|
||||||
|
user: { email: "assistant@example.com", name: "Assistant User", image: null },
|
||||||
|
expires: "2026-03-29T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
dbUser: {
|
||||||
|
id: "user_1",
|
||||||
|
systemRole: userRole,
|
||||||
|
permissionOverrides: null,
|
||||||
|
},
|
||||||
|
roleDefaults: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant import/export and dispo tools", () => {
|
||||||
|
it("exports resources CSV through the real import/export router path", async () => {
|
||||||
|
const ctx = createToolContext(
|
||||||
|
{
|
||||||
|
resource: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
eid: "EMP-001",
|
||||||
|
displayName: "Carol Danvers",
|
||||||
|
email: "carol@example.com",
|
||||||
|
chapter: "Delivery",
|
||||||
|
lcrCents: 8000,
|
||||||
|
ucrCents: 12000,
|
||||||
|
currency: "EUR",
|
||||||
|
chargeabilityTarget: 0.8,
|
||||||
|
dynamicFields: {},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
blueprint: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ userRole: SystemRole.CONTROLLER },
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await executeTool("export_resources_csv", "{}", ctx);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual({
|
||||||
|
format: "csv",
|
||||||
|
lineCount: 2,
|
||||||
|
csv: "eid,displayName,email,chapter,lcrCents,ucrCents,currency,chargeabilityTarget\nEMP-001,Carol Danvers,carol@example.com,Delivery,8000,12000,EUR,0.8",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires importData permission for CSV imports", async () => {
|
||||||
|
const ctx = createToolContext(
|
||||||
|
{
|
||||||
|
auditLog: { create: vi.fn() },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userRole: SystemRole.MANAGER,
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await executeTool(
|
||||||
|
"import_csv_data",
|
||||||
|
JSON.stringify({
|
||||||
|
entityType: "resources",
|
||||||
|
rows: [{ eid: "EMP-001", displayName: "Carol Danvers" }],
|
||||||
|
dryRun: true,
|
||||||
|
}),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
error: expect.stringContaining(PermissionKey.IMPORT_DATA),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces admin access for dispo batch inspection via the backing router", async () => {
|
||||||
|
const ctx = createToolContext(
|
||||||
|
{
|
||||||
|
importBatch: {
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ userRole: SystemRole.MANAGER },
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await executeTool(
|
||||||
|
"get_dispo_import_batch",
|
||||||
|
JSON.stringify({ id: "batch_1" }),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
error: expect.stringContaining("Admin role required"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Each tool has a JSON schema (for the AI) and an execute function (for the server).
|
* Each tool has a JSON schema (for the AI) and an execute function (for the server).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { prisma, Prisma } from "@capakraken/db";
|
import { prisma, Prisma, ImportBatchStatus } from "@capakraken/db";
|
||||||
import { checkDuplicateAssignment } from "@capakraken/engine/allocation";
|
import { checkDuplicateAssignment } from "@capakraken/engine/allocation";
|
||||||
import { computeBudgetStatus } from "@capakraken/engine";
|
import { computeBudgetStatus } from "@capakraken/engine";
|
||||||
import {
|
import {
|
||||||
@@ -52,12 +52,16 @@ import {
|
|||||||
} from "../sse/event-bus.js";
|
} from "../sse/event-bus.js";
|
||||||
import { logger } from "../lib/logger.js";
|
import { logger } from "../lib/logger.js";
|
||||||
import { createCallerFactory, type TRPCContext } from "../trpc.js";
|
import { createCallerFactory, type TRPCContext } from "../trpc.js";
|
||||||
|
import { auditLogRouter } from "./audit-log.js";
|
||||||
import { chargeabilityReportRouter } from "./chargeability-report.js";
|
import { chargeabilityReportRouter } from "./chargeability-report.js";
|
||||||
import { computationGraphRouter } from "./computation-graph.js";
|
import { computationGraphRouter } from "./computation-graph.js";
|
||||||
|
import { dispoRouter } from "./dispo.js";
|
||||||
|
import { importExportRouter } from "./import-export.js";
|
||||||
|
|
||||||
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
|
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
|
||||||
|
|
||||||
export const MUTATION_TOOLS = new Set([
|
export const MUTATION_TOOLS = new Set([
|
||||||
|
"import_csv_data",
|
||||||
"create_allocation", "cancel_allocation", "update_allocation_status",
|
"create_allocation", "cancel_allocation", "update_allocation_status",
|
||||||
"update_timeline_allocation_inline", "apply_timeline_project_shift",
|
"update_timeline_allocation_inline", "apply_timeline_project_shift",
|
||||||
"quick_assign_timeline_resource", "batch_quick_assign_timeline_resources",
|
"quick_assign_timeline_resource", "batch_quick_assign_timeline_resources",
|
||||||
@@ -122,6 +126,9 @@ type ToolExecutor = (params: any, ctx: ToolContext) => Promise<unknown>;
|
|||||||
const createChargeabilityReportCaller = createCallerFactory(chargeabilityReportRouter);
|
const createChargeabilityReportCaller = createCallerFactory(chargeabilityReportRouter);
|
||||||
const createComputationGraphCaller = createCallerFactory(computationGraphRouter);
|
const createComputationGraphCaller = createCallerFactory(computationGraphRouter);
|
||||||
const createTimelineCaller = createCallerFactory(timelineRouter);
|
const createTimelineCaller = createCallerFactory(timelineRouter);
|
||||||
|
const createAuditLogCaller = createCallerFactory(auditLogRouter);
|
||||||
|
const createImportExportCaller = createCallerFactory(importExportRouter);
|
||||||
|
const createDispoCaller = createCallerFactory(dispoRouter);
|
||||||
|
|
||||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -729,6 +736,57 @@ function createScopedCallerContext(ctx: ToolContext): TRPCContext {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAuditListEntry(entry: {
|
||||||
|
id: string;
|
||||||
|
entityType: string;
|
||||||
|
entityId: string;
|
||||||
|
entityName?: string | null;
|
||||||
|
action: string;
|
||||||
|
userId?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
summary?: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
user?: { id: string; name: string | null; email: string | null } | null;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: entry.id,
|
||||||
|
entityType: entry.entityType,
|
||||||
|
entityId: entry.entityId,
|
||||||
|
entityName: entry.entityName ?? null,
|
||||||
|
action: entry.action,
|
||||||
|
userId: entry.userId ?? null,
|
||||||
|
source: entry.source ?? null,
|
||||||
|
summary: entry.summary ?? null,
|
||||||
|
createdAt: entry.createdAt.toISOString(),
|
||||||
|
user: entry.user
|
||||||
|
? {
|
||||||
|
id: entry.user.id,
|
||||||
|
name: entry.user.name,
|
||||||
|
email: entry.user.email,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAuditDetailEntry(entry: {
|
||||||
|
id: string;
|
||||||
|
entityType: string;
|
||||||
|
entityId: string;
|
||||||
|
entityName?: string | null;
|
||||||
|
action: string;
|
||||||
|
userId?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
summary?: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
changes?: unknown;
|
||||||
|
user?: { id: string; name: string | null; email: string | null } | null;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
...formatAuditListEntry(entry),
|
||||||
|
changes: entry.changes ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function filterGraphData<
|
function filterGraphData<
|
||||||
TNode extends { id: string; domain: string },
|
TNode extends { id: string; domain: string },
|
||||||
TLink extends { source: string; target: string },
|
TLink extends { source: string; target: string },
|
||||||
@@ -2623,7 +2681,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
|||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
name: "query_change_history",
|
name: "query_change_history",
|
||||||
description: "Search the activity history for changes to projects, resources, allocations, vacations, or any entity. Can filter by entity type, entity name, user, date range, or action type.",
|
description: "Search the audit history for changes to projects, resources, allocations, vacations, or any entity. Reuses the real audit log list API. Controller/manager/admin roles only.",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -2641,7 +2699,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
|||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
name: "get_entity_timeline",
|
name: "get_entity_timeline",
|
||||||
description: "Get the complete change history for a specific entity (project, resource, etc). Shows who made what changes and when.",
|
description: "Get the audit history for a specific entity (project, resource, etc.) via the real audit API. Controller/manager/admin roles only.",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -2653,6 +2711,145 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "export_resources_csv",
|
||||||
|
description: "Export the current active resource list as CSV via the real import/export router. Controller/manager/admin roles only.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "export_projects_csv",
|
||||||
|
description: "Export the current project list as CSV via the real import/export router. Controller/manager/admin roles only.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "import_csv_data",
|
||||||
|
description: "Import CSV-style row data for resources, projects, or allocations via the real import/export router. Requires manager/admin, importData permission, and confirmation.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
entityType: { type: "string", enum: ["resources", "projects", "allocations"], description: "Import target entity type." },
|
||||||
|
rows: {
|
||||||
|
type: "array",
|
||||||
|
description: "CSV rows already parsed to key/value objects.",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dryRun: { type: "boolean", description: "Validate only without persisting changes. Default: true." },
|
||||||
|
},
|
||||||
|
required: ["entityType", "rows"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_dispo_import_batches",
|
||||||
|
description: "List Dispo import batches with pagination and optional status filter via the real dispo router. Admin role required.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: { type: "string", description: "Optional batch status filter." },
|
||||||
|
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
|
||||||
|
cursor: { type: "string", description: "Optional pagination cursor." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "get_dispo_import_batch",
|
||||||
|
description: "Get one Dispo import batch including staged record counters via the real dispo router. Admin role required.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "Import batch ID." },
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_audit_log_entries",
|
||||||
|
description: "List audit log entries with full audit-router filters and cursor pagination. Controller/manager/admin roles only.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
entityType: { type: "string", description: "Optional entity type filter." },
|
||||||
|
entityId: { type: "string", description: "Optional entity ID filter." },
|
||||||
|
userId: { type: "string", description: "Optional user ID filter." },
|
||||||
|
action: { type: "string", description: "Optional action filter such as CREATE, UPDATE, DELETE, SHIFT, IMPORT." },
|
||||||
|
source: { type: "string", description: "Optional source filter such as ui or assistant." },
|
||||||
|
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
|
||||||
|
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
|
||||||
|
search: { type: "string", description: "Optional case-insensitive search across entity name, summary, and entity type." },
|
||||||
|
limit: { type: "integer", description: "Max results. Default: 50, max: 100." },
|
||||||
|
cursor: { type: "string", description: "Optional pagination cursor (last seen audit entry ID)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "get_audit_log_entry",
|
||||||
|
description: "Get one audit log entry including the full changes payload. Controller/manager/admin roles only.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "Audit log entry ID." },
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "get_audit_log_timeline",
|
||||||
|
description: "Get audit log entries grouped by day for a time window. Controller/manager/admin roles only.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
|
||||||
|
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
|
||||||
|
limit: { type: "integer", description: "Max entries. Default: 200, max: 500." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "get_audit_activity_summary",
|
||||||
|
description: "Get audit activity totals by entity type, action, and user for a date range. Controller/manager/admin roles only.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
|
||||||
|
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
@@ -8591,48 +8788,31 @@ const executors = {
|
|||||||
}, ctx: ToolContext) {
|
}, ctx: ToolContext) {
|
||||||
const limit = Math.min(params.limit ?? 20, 50);
|
const limit = Math.min(params.limit ?? 20, 50);
|
||||||
const daysBack = params.daysBack ?? 7;
|
const daysBack = params.daysBack ?? 7;
|
||||||
|
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
startDate.setDate(startDate.getDate() - daysBack);
|
startDate.setDate(startDate.getDate() - daysBack);
|
||||||
|
|
||||||
const where: Record<string, unknown> = {
|
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
|
||||||
createdAt: { gte: startDate },
|
const result = await caller.list({
|
||||||
};
|
...(params.entityType ? { entityType: params.entityType } : {}),
|
||||||
|
...(params.userId ? { userId: params.userId } : {}),
|
||||||
|
...(params.action ? { action: params.action } : {}),
|
||||||
|
...(params.search ? { search: params.search } : {}),
|
||||||
|
startDate,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
|
||||||
if (params.entityType) where.entityType = params.entityType;
|
return {
|
||||||
if (params.action) where.action = params.action;
|
filters: {
|
||||||
if (params.userId) where.userId = params.userId;
|
entityType: params.entityType ?? null,
|
||||||
|
userId: params.userId ?? null,
|
||||||
if (params.search) {
|
action: params.action ?? null,
|
||||||
where.OR = [
|
search: params.search ?? null,
|
||||||
{ entityName: { contains: params.search, mode: "insensitive" } },
|
daysBack,
|
||||||
{ summary: { contains: params.search, mode: "insensitive" } },
|
|
||||||
{ entityType: { contains: params.search, mode: "insensitive" } },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await ctx.db.auditLog.findMany({
|
|
||||||
where,
|
|
||||||
include: {
|
|
||||||
user: { select: { id: true, name: true, email: true } },
|
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "desc" },
|
itemCount: result.items.length,
|
||||||
take: limit,
|
nextCursor: result.nextCursor ?? null,
|
||||||
});
|
items: result.items.map(formatAuditListEntry),
|
||||||
|
};
|
||||||
if (entries.length === 0) {
|
|
||||||
return `No changes found in the last ${daysBack} days matching your criteria.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = entries.map((e) => {
|
|
||||||
const who = e.user?.name ?? e.user?.email ?? "System";
|
|
||||||
const when = e.createdAt.toISOString().slice(0, 16).replace("T", " ");
|
|
||||||
const name = e.entityName ? ` "${e.entityName}"` : "";
|
|
||||||
const summary = e.summary ? ` — ${e.summary}` : "";
|
|
||||||
return `[${when}] ${who}: ${e.action} ${e.entityType}${name}${summary}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return `Found ${entries.length} changes (last ${daysBack} days):\n\n${lines.join("\n")}`;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async get_entity_timeline(params: {
|
async get_entity_timeline(params: {
|
||||||
@@ -8641,50 +8821,156 @@ const executors = {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
}, ctx: ToolContext) {
|
}, ctx: ToolContext) {
|
||||||
const limit = Math.min(params.limit ?? 50, 200);
|
const limit = Math.min(params.limit ?? 50, 200);
|
||||||
|
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
|
||||||
const entries = await ctx.db.auditLog.findMany({
|
const entries = await caller.getByEntity({
|
||||||
where: {
|
entityType: params.entityType,
|
||||||
entityType: params.entityType,
|
entityId: params.entityId,
|
||||||
entityId: params.entityId,
|
limit,
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: { select: { id: true, name: true, email: true } },
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
take: limit,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (entries.length === 0) {
|
return {
|
||||||
return `No change history found for ${params.entityType} ${params.entityId}.`;
|
entityType: params.entityType,
|
||||||
}
|
entityId: params.entityId,
|
||||||
|
entityName: entries[0]?.entityName ?? null,
|
||||||
|
itemCount: entries.length,
|
||||||
|
items: entries.map(formatAuditDetailEntry),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
const entityName = entries[0]?.entityName ?? params.entityId;
|
async export_resources_csv(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
|
const caller = createImportExportCaller(createScopedCallerContext(ctx));
|
||||||
|
const csv = await caller.exportResourcesCSV();
|
||||||
|
return {
|
||||||
|
format: "csv",
|
||||||
|
lineCount: csv.length === 0 ? 0 : csv.split("\n").length,
|
||||||
|
csv,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
const lines = entries.map((e) => {
|
async export_projects_csv(_params: Record<string, never>, ctx: ToolContext) {
|
||||||
const who = e.user?.name ?? e.user?.email ?? "System";
|
const caller = createImportExportCaller(createScopedCallerContext(ctx));
|
||||||
const when = e.createdAt.toISOString().slice(0, 16).replace("T", " ");
|
const csv = await caller.exportProjectsCSV();
|
||||||
const summary = e.summary ?? e.action;
|
return {
|
||||||
const source = e.source ? ` (via ${e.source})` : "";
|
format: "csv",
|
||||||
|
lineCount: csv.length === 0 ? 0 : csv.split("\n").length,
|
||||||
|
csv,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
// Include changed fields summary for UPDATE actions
|
async import_csv_data(params: {
|
||||||
const changes = e.changes as Record<string, unknown> | null;
|
entityType: "resources" | "projects" | "allocations";
|
||||||
const diff = changes?.diff as Record<string, { old: unknown; new: unknown }> | undefined;
|
rows: Array<Record<string, string>>;
|
||||||
let diffSummary = "";
|
dryRun?: boolean;
|
||||||
if (diff && Object.keys(diff).length > 0) {
|
}, ctx: ToolContext) {
|
||||||
const fields = Object.entries(diff)
|
assertPermission(ctx, PermissionKey.IMPORT_DATA);
|
||||||
.slice(0, 3)
|
const caller = createImportExportCaller(createScopedCallerContext(ctx));
|
||||||
.map(([k, v]) => `${k}: ${JSON.stringify(v.old)} → ${JSON.stringify(v.new)}`)
|
return caller.importCSV({
|
||||||
.join("; ");
|
entityType: params.entityType,
|
||||||
diffSummary = `\n Changed: ${fields}`;
|
rows: params.rows,
|
||||||
if (Object.keys(diff).length > 3) {
|
dryRun: params.dryRun ?? true,
|
||||||
diffSummary += ` (+${Object.keys(diff).length - 3} more)`;
|
});
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
return `[${when}] ${who}${source}: ${summary}${diffSummary}`;
|
async list_dispo_import_batches(params: {
|
||||||
|
status?: ImportBatchStatus;
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
}, ctx: ToolContext) {
|
||||||
|
const caller = createDispoCaller(createScopedCallerContext(ctx));
|
||||||
|
return caller.listImportBatches({
|
||||||
|
...(params.status ? { status: params.status } : {}),
|
||||||
|
...(params.cursor ? { cursor: params.cursor } : {}),
|
||||||
|
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async get_dispo_import_batch(params: {
|
||||||
|
id: string;
|
||||||
|
}, ctx: ToolContext) {
|
||||||
|
const caller = createDispoCaller(createScopedCallerContext(ctx));
|
||||||
|
return caller.getImportBatch({ id: params.id });
|
||||||
|
},
|
||||||
|
|
||||||
|
async list_audit_log_entries(params: {
|
||||||
|
entityType?: string;
|
||||||
|
entityId?: string;
|
||||||
|
userId?: string;
|
||||||
|
action?: string;
|
||||||
|
source?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
}, ctx: ToolContext) {
|
||||||
|
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
|
||||||
|
const result = await caller.list({
|
||||||
|
...(params.entityType ? { entityType: params.entityType } : {}),
|
||||||
|
...(params.entityId ? { entityId: params.entityId } : {}),
|
||||||
|
...(params.userId ? { userId: params.userId } : {}),
|
||||||
|
...(params.action ? { action: params.action } : {}),
|
||||||
|
...(params.source ? { source: params.source } : {}),
|
||||||
|
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
|
||||||
|
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
|
||||||
|
...(params.search ? { search: params.search } : {}),
|
||||||
|
...(params.cursor ? { cursor: params.cursor } : {}),
|
||||||
|
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 100) } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return `Change history for ${params.entityType} "${entityName}" (${entries.length} entries):\n\n${lines.join("\n")}`;
|
return {
|
||||||
|
filters: {
|
||||||
|
entityType: params.entityType ?? null,
|
||||||
|
entityId: params.entityId ?? null,
|
||||||
|
userId: params.userId ?? null,
|
||||||
|
action: params.action ?? null,
|
||||||
|
source: params.source ?? null,
|
||||||
|
startDate: params.startDate ?? null,
|
||||||
|
endDate: params.endDate ?? null,
|
||||||
|
search: params.search ?? null,
|
||||||
|
},
|
||||||
|
itemCount: result.items.length,
|
||||||
|
nextCursor: result.nextCursor ?? null,
|
||||||
|
items: result.items.map(formatAuditListEntry),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async get_audit_log_entry(params: {
|
||||||
|
id: string;
|
||||||
|
}, ctx: ToolContext) {
|
||||||
|
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
|
||||||
|
const entry = await caller.getById({ id: params.id });
|
||||||
|
return formatAuditDetailEntry(entry);
|
||||||
|
},
|
||||||
|
|
||||||
|
async get_audit_log_timeline(params: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
limit?: number;
|
||||||
|
}, ctx: ToolContext) {
|
||||||
|
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
|
||||||
|
const timeline = await caller.getTimeline({
|
||||||
|
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
|
||||||
|
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
|
||||||
|
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 500) } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(timeline).map(([dateKey, entries]) => [
|
||||||
|
dateKey,
|
||||||
|
entries.map(formatAuditDetailEntry),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async get_audit_activity_summary(params: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
}, ctx: ToolContext) {
|
||||||
|
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
|
||||||
|
return caller.getActivitySummary({
|
||||||
|
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
|
||||||
|
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async get_shoring_ratio(params: { projectId: string }, ctx: ToolContext) {
|
async get_shoring_ratio(params: { projectId: string }, ctx: ToolContext) {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ const TOOL_PERMISSION_MAP: Record<string, string> = {
|
|||||||
create_estimate: "manageProjects",
|
create_estimate: "manageProjects",
|
||||||
generate_project_cover: "manageProjects",
|
generate_project_cover: "manageProjects",
|
||||||
remove_project_cover: "manageProjects",
|
remove_project_cover: "manageProjects",
|
||||||
|
import_csv_data: PermissionKey.IMPORT_DATA,
|
||||||
// Allocation management
|
// Allocation management
|
||||||
create_allocation: "manageAllocations",
|
create_allocation: "manageAllocations",
|
||||||
cancel_allocation: "manageAllocations",
|
cancel_allocation: "manageAllocations",
|
||||||
@@ -146,6 +147,14 @@ const COST_TOOLS = new Set([
|
|||||||
|
|
||||||
/** Tools that follow controllerProcedure access rules in the main API. */
|
/** Tools that follow controllerProcedure access rules in the main API. */
|
||||||
const CONTROLLER_ONLY_TOOLS = new Set([
|
const CONTROLLER_ONLY_TOOLS = new Set([
|
||||||
|
"query_change_history",
|
||||||
|
"get_entity_timeline",
|
||||||
|
"export_resources_csv",
|
||||||
|
"export_projects_csv",
|
||||||
|
"list_audit_log_entries",
|
||||||
|
"get_audit_log_entry",
|
||||||
|
"get_audit_log_timeline",
|
||||||
|
"get_audit_activity_summary",
|
||||||
"get_chargeability_report",
|
"get_chargeability_report",
|
||||||
"get_resource_computation_graph",
|
"get_resource_computation_graph",
|
||||||
"get_project_computation_graph",
|
"get_project_computation_graph",
|
||||||
@@ -153,6 +162,7 @@ const CONTROLLER_ONLY_TOOLS = new Set([
|
|||||||
|
|
||||||
/** Tools that follow managerProcedure access rules in the main API. */
|
/** Tools that follow managerProcedure access rules in the main API. */
|
||||||
const MANAGER_ONLY_TOOLS = new Set([
|
const MANAGER_ONLY_TOOLS = new Set([
|
||||||
|
"import_csv_data",
|
||||||
"update_timeline_allocation_inline",
|
"update_timeline_allocation_inline",
|
||||||
"apply_timeline_project_shift",
|
"apply_timeline_project_shift",
|
||||||
"quick_assign_timeline_resource",
|
"quick_assign_timeline_resource",
|
||||||
@@ -162,6 +172,8 @@ const MANAGER_ONLY_TOOLS = new Set([
|
|||||||
|
|
||||||
/** Tools that are intentionally limited to ADMIN because the backing routers are admin-only today. */
|
/** Tools that are intentionally limited to ADMIN because the backing routers are admin-only today. */
|
||||||
const ADMIN_ONLY_TOOLS = new Set([
|
const ADMIN_ONLY_TOOLS = new Set([
|
||||||
|
"list_dispo_import_batches",
|
||||||
|
"get_dispo_import_batch",
|
||||||
"create_country",
|
"create_country",
|
||||||
"update_country",
|
"update_country",
|
||||||
"create_metro_city",
|
"create_metro_city",
|
||||||
|
|||||||
Reference in New Issue
Block a user