Files
paperclip/server/src/routes/activity.ts
Forgotten fe63c10d69 Include issue identifier in all activity log details for notifications
Activity log events for issue.created and issue.updated were missing
the identifier field in their details, causing toast notifications to
fall back to showing a truncated UUID hash instead of the shortname
(e.g. PAP-47). Also includes checkout lock adoption and activity
query improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 08:23:44 -06:00

97 lines
3.0 KiB
TypeScript

import { Router } from "express";
import { z } from "zod";
import type { Db } from "@paperclip/db";
import { validate } from "../middleware/validate.js";
import { activityService } from "../services/activity.js";
import { assertBoard, assertCompanyAccess } from "./authz.js";
import { issueService } from "../services/index.js";
import { sanitizeRecord } from "../redaction.js";
const createActivitySchema = z.object({
actorType: z.enum(["agent", "user", "system"]).optional().default("system"),
actorId: z.string().min(1),
action: z.string().min(1),
entityType: z.string().min(1),
entityId: z.string().min(1),
agentId: z.string().uuid().optional().nullable(),
details: z.record(z.unknown()).optional().nullable(),
});
export function activityRoutes(db: Db) {
const router = Router();
const svc = activityService(db);
const issueSvc = issueService(db);
router.get("/companies/:companyId/activity", async (req, res) => {
const companyId = req.params.companyId as string;
assertCompanyAccess(req, companyId);
const filters = {
companyId,
agentId: req.query.agentId as string | undefined,
entityType: req.query.entityType as string | undefined,
entityId: req.query.entityId as string | undefined,
};
const result = await svc.list(filters);
res.json(result);
});
router.post("/companies/:companyId/activity", validate(createActivitySchema), async (req, res) => {
assertBoard(req);
const companyId = req.params.companyId as string;
const event = await svc.create({
companyId,
...req.body,
details: req.body.details ? sanitizeRecord(req.body.details) : null,
});
res.status(201).json(event);
});
// Resolve issue identifiers (e.g. "PAP-39") to UUIDs
router.param("id", async (req, res, next, rawId) => {
try {
if (/^[A-Z]+-\d+$/i.test(rawId)) {
const issue = await issueSvc.getByIdentifier(rawId);
if (issue) {
req.params.id = issue.id;
}
}
next();
} catch (err) {
next(err);
}
});
router.get("/issues/:id/activity", async (req, res) => {
const id = req.params.id as string;
const issue = await issueSvc.getById(id);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
}
assertCompanyAccess(req, issue.companyId);
const result = await svc.forIssue(id);
res.json(result);
});
router.get("/issues/:id/runs", async (req, res) => {
const id = req.params.id as string;
const issue = await issueSvc.getById(id);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
}
assertCompanyAccess(req, issue.companyId);
const result = await svc.runsForIssue(issue.companyId, id);
res.json(result);
});
router.get("/heartbeat-runs/:runId/issues", async (req, res) => {
const runId = req.params.runId as string;
const result = await svc.issuesForRun(runId);
res.json(result);
});
return router;
}