mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 03:30:39 +09:00
Persist non-issue inbox dismissals
This commit is contained in:
parent
1de5fb9316
commit
5640d29ab0
23 changed files with 13623 additions and 54 deletions
69
server/src/routes/inbox-dismissals.ts
Normal file
69
server/src/routes/inbox-dismissals.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
import { inboxDismissalService, logActivity } from "../services/index.js";
|
||||
|
||||
const inboxDismissalSchema = z.object({
|
||||
itemKey: z.string().trim().min(1).regex(/^(approval|join|run):.+$/, "Unsupported inbox item key"),
|
||||
});
|
||||
|
||||
export function inboxDismissalRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const svc = inboxDismissalService(db);
|
||||
|
||||
router.get("/companies/:companyId/inbox-dismissals", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
if (req.actor.type !== "board") {
|
||||
res.status(403).json({ error: "Board authentication required" });
|
||||
return;
|
||||
}
|
||||
if (!req.actor.userId) {
|
||||
res.status(403).json({ error: "Board user context required" });
|
||||
return;
|
||||
}
|
||||
const dismissals = await svc.list(companyId, req.actor.userId);
|
||||
res.json(dismissals);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/companies/:companyId/inbox-dismissals",
|
||||
validate(inboxDismissalSchema),
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
if (req.actor.type !== "board") {
|
||||
res.status(403).json({ error: "Board authentication required" });
|
||||
return;
|
||||
}
|
||||
if (!req.actor.userId) {
|
||||
res.status(403).json({ error: "Board user context required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const dismissal = await svc.dismiss(companyId, req.actor.userId, req.body.itemKey, new Date());
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "inbox.dismissed",
|
||||
entityType: "company",
|
||||
entityId: companyId,
|
||||
details: {
|
||||
userId: req.actor.userId,
|
||||
itemKey: dismissal.itemKey,
|
||||
dismissedAt: dismissal.dismissedAt,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json(dismissal);
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ export { costRoutes } from "./costs.js";
|
|||
export { activityRoutes } from "./activity.js";
|
||||
export { dashboardRoutes } from "./dashboard.js";
|
||||
export { sidebarBadgeRoutes } from "./sidebar-badges.js";
|
||||
export { inboxDismissalRoutes } from "./inbox-dismissals.js";
|
||||
export { llmRoutes } from "./llms.js";
|
||||
export { accessRoutes } from "./access.js";
|
||||
export { instanceSettingsRoutes } from "./instance-settings.js";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
import { Router } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { joinRequests } from "@paperclipai/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { inboxDismissals, joinRequests } from "@paperclipai/db";
|
||||
import { sidebarBadgeService } from "../services/sidebar-badges.js";
|
||||
import { accessService } from "../services/access.js";
|
||||
import { dashboardService } from "../services/dashboard.js";
|
||||
import { assertCompanyAccess } from "./authz.js";
|
||||
|
||||
function buildDismissedAtByKey(
|
||||
dismissals: Array<{ itemKey: string; dismissedAt: Date | string }>,
|
||||
): Map<string, number> {
|
||||
return new Map(
|
||||
dismissals.map((dismissal) => [dismissal.itemKey, new Date(dismissal.dismissedAt).getTime()]),
|
||||
);
|
||||
}
|
||||
|
||||
export function sidebarBadgeRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const svc = sidebarBadgeService(db);
|
||||
|
|
@ -26,23 +34,36 @@ export function sidebarBadgeRoutes(db: Db) {
|
|||
canApproveJoins = await access.hasPermission(companyId, "agent", req.actor.agentId, "joins:approve");
|
||||
}
|
||||
|
||||
const joinRequestCount = canApproveJoins
|
||||
const visibleJoinRequests = canApproveJoins
|
||||
? await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.select({
|
||||
id: joinRequests.id,
|
||||
updatedAt: joinRequests.updatedAt,
|
||||
createdAt: joinRequests.createdAt,
|
||||
})
|
||||
.from(joinRequests)
|
||||
.where(and(eq(joinRequests.companyId, companyId), eq(joinRequests.status, "pending_approval")))
|
||||
.then((rows) => Number(rows[0]?.count ?? 0))
|
||||
: 0;
|
||||
: [];
|
||||
|
||||
const dismissedAtByKey =
|
||||
req.actor.type === "board" && req.actor.userId
|
||||
? await db
|
||||
.select({ itemKey: inboxDismissals.itemKey, dismissedAt: inboxDismissals.dismissedAt })
|
||||
.from(inboxDismissals)
|
||||
.where(and(eq(inboxDismissals.companyId, companyId), eq(inboxDismissals.userId, req.actor.userId)))
|
||||
.then(buildDismissedAtByKey)
|
||||
: new Map<string, number>();
|
||||
|
||||
const badges = await svc.get(companyId, {
|
||||
joinRequests: joinRequestCount,
|
||||
dismissals: dismissedAtByKey,
|
||||
joinRequests: visibleJoinRequests,
|
||||
});
|
||||
const summary = await dashboard.summary(companyId);
|
||||
const hasFailedRuns = badges.failedRuns > 0;
|
||||
const alertsCount =
|
||||
(summary.agents.error > 0 && !hasFailedRuns ? 1 : 0) +
|
||||
(summary.costs.monthBudgetCents > 0 && summary.costs.monthUtilizationPercent >= 80 ? 1 : 0);
|
||||
badges.inbox = badges.failedRuns + alertsCount + joinRequestCount + badges.approvals;
|
||||
badges.inbox = badges.failedRuns + alertsCount + badges.joinRequests + badges.approvals;
|
||||
|
||||
res.json(badges);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue