From f766478f5a8e069a507718e1b9cb1b736ce9ab05 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Fri, 20 Feb 2026 07:11:06 -0600 Subject: [PATCH] fix(issues): support hidden issue flows and filter hidden activity --- server/src/middleware/error-handler.ts | 14 ++++++++++++-- server/src/middleware/logger.ts | 2 +- server/src/routes/issues.ts | 5 ++++- server/src/services/activity.ts | 25 +++++++++++++++++++++++-- server/src/services/issues.ts | 1 + ui/src/pages/IssueDetail.tsx | 11 +++++++++-- 6 files changed, 50 insertions(+), 8 deletions(-) diff --git a/server/src/middleware/error-handler.ts b/server/src/middleware/error-handler.ts index 1f5433cb..35a298db 100644 --- a/server/src/middleware/error-handler.ts +++ b/server/src/middleware/error-handler.ts @@ -5,7 +5,7 @@ import { HttpError } from "../errors.js"; export function errorHandler( err: unknown, - _req: Request, + req: Request, res: Response, _next: NextFunction, ) { @@ -22,6 +22,16 @@ export function errorHandler( return; } - logger.error(err, "Unhandled error"); + const errObj = err instanceof Error + ? { message: err.message, stack: err.stack, name: err.name } + : { raw: err }; + + logger.error( + { err: errObj, method: req.method, url: req.originalUrl }, + "Unhandled error: %s %s — %s", + req.method, + req.originalUrl, + err instanceof Error ? err.message : String(err), + ); res.status(500).json({ error: "Internal server error" }); } diff --git a/server/src/middleware/logger.ts b/server/src/middleware/logger.ts index 5ccb989b..bd9ded9d 100644 --- a/server/src/middleware/logger.ts +++ b/server/src/middleware/logger.ts @@ -19,7 +19,7 @@ export const logger = pino({ targets: [ { target: "pino-pretty", - options: { ...sharedOpts, ignore: "pid,hostname,req,res", hideObject: true, colorize: true, destination: 1 }, + options: { ...sharedOpts, ignore: "pid,hostname,req,res", colorize: true, destination: 1 }, level: "info", }, { diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 48eadc13..9b7d1e13 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -186,7 +186,10 @@ export function issueRoutes(db: Db) { } assertCompanyAccess(req, existing.companyId); - const { comment: commentBody, ...updateFields } = req.body; + const { comment: commentBody, hiddenAt: hiddenAtRaw, ...updateFields } = req.body; + if (hiddenAtRaw !== undefined) { + updateFields.hiddenAt = hiddenAtRaw ? new Date(hiddenAtRaw) : null; + } const issue = await svc.update(id, updateFields); if (!issue) { res.status(404).json({ error: "Issue not found" }); diff --git a/server/src/services/activity.ts b/server/src/services/activity.ts index 4ff036c1..2d4d7067 100644 --- a/server/src/services/activity.ts +++ b/server/src/services/activity.ts @@ -1,4 +1,4 @@ -import { and, desc, eq, isNotNull, sql } from "drizzle-orm"; +import { and, desc, eq, isNotNull, isNull, or, sql } from "drizzle-orm"; import type { Db } from "@paperclip/db"; import { activityLog, heartbeatRuns, issues } from "@paperclip/db"; @@ -25,7 +25,27 @@ export function activityService(db: Db) { conditions.push(eq(activityLog.entityId, filters.entityId)); } - return db.select().from(activityLog).where(and(...conditions)).orderBy(desc(activityLog.createdAt)); + return db + .select({ activityLog }) + .from(activityLog) + .leftJoin( + issues, + and( + eq(activityLog.entityType, sql`'issue'`), + eq(activityLog.entityId, issueIdAsText), + ), + ) + .where( + and( + ...conditions, + or( + sql`${activityLog.entityType} != 'issue'`, + isNull(issues.hiddenAt), + ), + ), + ) + .orderBy(desc(activityLog.createdAt)) + .then((rows) => rows.map((r) => r.activityLog)); }, forIssue: (issueId: string) => @@ -76,6 +96,7 @@ export function activityService(db: Db) { and( eq(activityLog.runId, runId), eq(activityLog.entityType, "issue"), + isNull(issues.hiddenAt), ), ) .orderBy(issueIdAsText), diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 24bcfeb7..9594b55e 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -324,6 +324,7 @@ export function issueService(db: Db) { and( eq(issues.companyId, companyId), eq(issues.status, "in_progress"), + isNull(issues.hiddenAt), sql`${issues.startedAt} < ${cutoff.toISOString()}`, ), ) diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 61cc08d1..496ea847 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -235,6 +235,13 @@ export function IssueDetail() { )} + {issue.hiddenAt && ( +
+ + This issue is hidden +
+ )} +
- @@ -259,7 +266,7 @@ export function IssueDetail() { onClick={() => { updateIssue.mutate( { hiddenAt: new Date().toISOString() }, - { onSuccess: () => navigate("/issues") }, + { onSuccess: () => navigate("/issues/all") }, ); setMoreOpen(false); }}