task logs: migrate from filesystem to postgresql; refactor sidecars into submodules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 11:49:39 +00:00
co-authored by Claude Opus 4.6
parent 5129f7827f
commit d88fe3cac7
22 changed files with 621 additions and 545 deletions
+30 -62
View File
@@ -1,79 +1,47 @@
import { readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { desc, eq } from 'drizzle-orm';
import { db, schema } from 'officerdb';
import { createRouter } from '../../create-router';
import { getTaskLogsDir } from '../../data-path';
type LogMetadata = {
filename: string;
taskName: string;
taskDirName: string;
entryName: string;
entryType: 'file' | 'directory';
provider: string;
model: string;
startedAt: string;
completedAt: string | null;
isError: boolean;
};
export const taskLogsRouter = createRouter();
// GET / — list all log files (metadata only, no messages)
// GET / — list all logs (metadata only, no messages)
taskLogsRouter.get('/', async (ctx) => {
const email = ctx.get('user').email;
const dir = getTaskLogsDir(email);
const userId = ctx.get('user').id;
let files: string[];
try {
files = (await readdir(dir)).filter((f) => f.endsWith('.json'));
} catch {
return ctx.json([]);
}
// Sort by filename descending (newest first since filenames start with timestamp)
files.sort((a, b) => b.localeCompare(a));
const logs: LogMetadata[] = [];
for (const filename of files) {
try {
const raw = await Bun.file(join(dir, filename)).json();
const lastMessage = Array.isArray(raw.messages) ? raw.messages[raw.messages.length - 1] : null;
const isError = lastMessage?.role === 'result' ? lastMessage.isError : lastMessage?.role === 'error';
logs.push({
filename,
taskName: raw.taskName ?? '',
taskDirName: raw.taskDirName ?? '',
entryName: raw.entryName ?? '',
entryType: raw.entryType ?? 'file',
provider: raw.provider ?? '',
model: raw.model ?? '',
startedAt: raw.startedAt ?? '',
completedAt: raw.completedAt ?? null,
isError: !!isError,
});
} catch {
// Skip unreadable files
}
}
const logs = await db
.select({
id: schema.taskLogs.id,
taskName: schema.taskLogs.taskName,
taskDirName: schema.taskLogs.taskDirName,
entryName: schema.taskLogs.entryName,
entryType: schema.taskLogs.entryType,
provider: schema.taskLogs.provider,
model: schema.taskLogs.model,
isError: schema.taskLogs.isError,
startedAt: schema.taskLogs.startedAt,
completedAt: schema.taskLogs.completedAt,
})
.from(schema.taskLogs)
.where(eq(schema.taskLogs.userId, userId))
.orderBy(desc(schema.taskLogs.startedAt));
return ctx.json(logs);
});
// GET /:filename — return full log file content
taskLogsRouter.get('/:filename', async (ctx) => {
const email = ctx.get('user').email;
const filename = ctx.req.param('filename');
// GET /:id — return full log with messages
taskLogsRouter.get('/:id', async (ctx) => {
const userId = ctx.get('user').id;
const id = Number(ctx.req.param('id'));
if (!filename.endsWith('.json') || filename.includes('/') || filename.includes('..')) {
return ctx.text('Invalid filename', 400);
if (Number.isNaN(id)) {
return ctx.text('Invalid id', 400);
}
const filePath = join(getTaskLogsDir(email), filename);
const [log] = await db.select().from(schema.taskLogs).where(eq(schema.taskLogs.id, id));
try {
const data = await Bun.file(filePath).json();
return ctx.json(data);
} catch {
if (!log || log.userId !== userId) {
return ctx.text('Not found', 404);
}
return ctx.json(log);
});