Server: migration prompts, structured logging, heartbeat reaping, and issue-run tracking
Replace auto-migrate-if-empty with interactive migration flow that inspects pending migrations and prompts before applying. Add pino-pretty for structured console + file logging. Add reapOrphanedRuns to clean up stuck heartbeat runs on startup and periodically. Track runId through auth middleware, activity logs, and all mutation routes. Add issue-run cross-reference queries, live-run and active-run endpoints for issues, issue identifier lookup, reopen-via-comment flow, and done/cancelled -> todo status transitions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq, isNotNull, sql } from "drizzle-orm";
|
||||
import type { Db } from "@paperclip/db";
|
||||
import { activityLog } from "@paperclip/db";
|
||||
import { activityLog, heartbeatRuns, issues } from "@paperclip/db";
|
||||
|
||||
export interface ActivityFilters {
|
||||
companyId: string;
|
||||
@@ -10,6 +10,7 @@ export interface ActivityFilters {
|
||||
}
|
||||
|
||||
export function activityService(db: Db) {
|
||||
const issueIdAsText = sql<string>`${issues.id}::text`;
|
||||
return {
|
||||
list: (filters: ActivityFilters) => {
|
||||
const conditions = [eq(activityLog.companyId, filters.companyId)];
|
||||
@@ -27,6 +28,58 @@ export function activityService(db: Db) {
|
||||
return db.select().from(activityLog).where(and(...conditions)).orderBy(desc(activityLog.createdAt));
|
||||
},
|
||||
|
||||
forIssue: (issueId: string) =>
|
||||
db
|
||||
.select()
|
||||
.from(activityLog)
|
||||
.where(
|
||||
and(
|
||||
eq(activityLog.entityType, "issue"),
|
||||
eq(activityLog.entityId, issueId),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(activityLog.createdAt)),
|
||||
|
||||
runsForIssue: (issueId: string) =>
|
||||
db
|
||||
.selectDistinctOn([activityLog.runId], {
|
||||
runId: activityLog.runId,
|
||||
status: heartbeatRuns.status,
|
||||
agentId: heartbeatRuns.agentId,
|
||||
startedAt: heartbeatRuns.startedAt,
|
||||
finishedAt: heartbeatRuns.finishedAt,
|
||||
createdAt: heartbeatRuns.createdAt,
|
||||
invocationSource: heartbeatRuns.invocationSource,
|
||||
})
|
||||
.from(activityLog)
|
||||
.innerJoin(heartbeatRuns, eq(activityLog.runId, heartbeatRuns.id))
|
||||
.where(
|
||||
and(
|
||||
eq(activityLog.entityType, "issue"),
|
||||
eq(activityLog.entityId, issueId),
|
||||
isNotNull(activityLog.runId),
|
||||
),
|
||||
)
|
||||
.orderBy(activityLog.runId, desc(heartbeatRuns.createdAt)),
|
||||
|
||||
issuesForRun: (runId: string) =>
|
||||
db
|
||||
.selectDistinctOn([issueIdAsText], {
|
||||
issueId: issues.id,
|
||||
title: issues.title,
|
||||
status: issues.status,
|
||||
priority: issues.priority,
|
||||
})
|
||||
.from(activityLog)
|
||||
.innerJoin(issues, eq(activityLog.entityId, issueIdAsText))
|
||||
.where(
|
||||
and(
|
||||
eq(activityLog.runId, runId),
|
||||
eq(activityLog.entityType, "issue"),
|
||||
),
|
||||
)
|
||||
.orderBy(issueIdAsText),
|
||||
|
||||
create: (data: typeof activityLog.$inferInsert) =>
|
||||
db
|
||||
.insert(activityLog)
|
||||
|
||||
Reference in New Issue
Block a user