delete Task Logs
A complete read path over a table nothing could write to. task-logger.ts exported createTaskLog, appendToLog and finalizeLog, and none of the three was called anywhere in the tree — so `task_logs` could never gain a row, and the screen was permanently empty for everyone. The read half was fully wired: mounted router, capability claim, dock icon, two routes and a page-title rule. That is why it looked alive. Gone, in the order it was reached: Screens/Dashboard/TaskLogs/ the screen App.tsx /task-logs and /task-logs/:id Dashboard/index.tsx the export Layout/Dock.tsx the 'Logs' icon, and ScrollText with it state/usePageTitle.ts the title rule api/task-logs/task-logs.ts the router, and its mount in hono.ts api/task-logger.ts 101 lines of orphaned writer officer_db/src/operations/ the directory officer_db/src/schema.ts the export line officer_db/src/types.ts TaskLogSelect / TaskLogInsert capabilities/registry.ts loses '/task-logs' from the `tasks` capability's `api` AND `routes`. The api half is not optional: assertCapabilityTotality check 2 refuses to boot on a capability claiming a prefix nothing mounts, so unmounting the router while leaving the claim would have stopped the server starting. db:push now creates 21 tables, down from 43 at the start of the evening. officer_db/src/operations was one of the two lopsided directories the schema/queries merge exposed. integrations/ is the remaining one, and it is legitimate — it spans server and user-data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -91,8 +91,6 @@ export function App() {
|
|||||||
<Route path="/tasks/:dirName" element={<Dashboard.Tasks />} />
|
<Route path="/tasks/:dirName" element={<Dashboard.Tasks />} />
|
||||||
<Route path="/processes" element={<Dashboard.Processes />} />
|
<Route path="/processes" element={<Dashboard.Processes />} />
|
||||||
<Route path="/processes/:dirName" element={<Dashboard.Processes />} />
|
<Route path="/processes/:dirName" element={<Dashboard.Processes />} />
|
||||||
<Route path="/task-logs" element={<Dashboard.TaskLogs />} />
|
|
||||||
<Route path="/task-logs/:id" element={<Dashboard.TaskLogs />} />
|
|
||||||
<Route path="/jobs" element={<Dashboard.JobsPage />} />
|
<Route path="/jobs" element={<Dashboard.JobsPage />} />
|
||||||
<Route path="/jobs/:id" element={<Dashboard.JobsPage />} />
|
<Route path="/jobs/:id" element={<Dashboard.JobsPage />} />
|
||||||
<Route path="/dashboards" element={<Dashboard.DashboardsScreen />} />
|
<Route path="/dashboards" element={<Dashboard.DashboardsScreen />} />
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ import {
|
|||||||
FolderOpen,
|
FolderOpen,
|
||||||
Code,
|
Code,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
ScrollText,
|
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
Monitor,
|
Monitor,
|
||||||
Mail,
|
Mail,
|
||||||
@@ -170,7 +169,6 @@ export const CORE_DOCK_ITEMS: DockItem[] = [
|
|||||||
{ label: 'Gitea', to: '/gitea', icon: GitBranch, color: '#609926' },
|
{ label: 'Gitea', to: '/gitea', icon: GitBranch, color: '#609926' },
|
||||||
{ label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
|
{ label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
|
||||||
{ label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' },
|
{ label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' },
|
||||||
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
|
|
||||||
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
|
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
|
||||||
{ label: 'Browser', to: '/browser', icon: Globe, color: '#06b6d4' },
|
{ label: 'Browser', to: '/browser', icon: Globe, color: '#06b6d4' },
|
||||||
{ label: 'Monitor', to: '/system-monitor', icon: Activity, color: '#0ea5e9' },
|
{ label: 'Monitor', to: '/system-monitor', icon: Activity, color: '#0ea5e9' },
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Link, useParams } from 'react-router';
|
|
||||||
import { Search, AlertCircle, CheckCircle2, Clock, ArrowLeft } from 'lucide-react';
|
|
||||||
import { useClient } from 'hooks/useClient';
|
|
||||||
import { Card } from '@/components/Card';
|
|
||||||
import { MessageBubble, type ChatMessage } from 'officerdev';
|
|
||||||
|
|
||||||
type LogMetadata = {
|
|
||||||
id: number;
|
|
||||||
taskName: string;
|
|
||||||
taskDirName: string;
|
|
||||||
entryName: string;
|
|
||||||
entryType: string;
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
isError: boolean;
|
|
||||||
startedAt: string;
|
|
||||||
completedAt: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FullLog = LogMetadata & {
|
|
||||||
messages: ChatMessage[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (iso: string) => {
|
|
||||||
const d = new Date(iso);
|
|
||||||
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProviderBadge = ({ provider }: { provider: string }) => (
|
|
||||||
<span
|
|
||||||
className={`text-[10px] font-medium px-1.5 py-0.5 rounded-full ${provider === 'claude' ? 'bg-orange-100 dark:bg-orange-900/40 text-orange-700 dark:text-orange-300' : 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300'}`}
|
|
||||||
>
|
|
||||||
{provider}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Which run is open is `/task-logs/:id`. No redirect guard — the bare route is the list with nothing
|
|
||||||
// open, and an id that no longer exists gets the empty pane rather than a rewritten address.
|
|
||||||
export const TaskLogs = () => {
|
|
||||||
const client = useClient();
|
|
||||||
const [logs, setLogs] = useState<LogMetadata[]>([]);
|
|
||||||
const selectedId = useParams<{ id: string }>().id ?? null;
|
|
||||||
const [selectedLog, setSelectedLog] = useState<FullLog | null>(null);
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client
|
|
||||||
.get<LogMetadata[]>('/task-logs')
|
|
||||||
.then((data) => {
|
|
||||||
setLogs(data);
|
|
||||||
setIsLoading(false);
|
|
||||||
})
|
|
||||||
.catch(() => setIsLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedId) {
|
|
||||||
setSelectedLog(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
client
|
|
||||||
.get<FullLog>(`/task-logs/${selectedId}`)
|
|
||||||
.then(setSelectedLog)
|
|
||||||
.catch(() => setSelectedLog(null));
|
|
||||||
}, [selectedId]);
|
|
||||||
|
|
||||||
const filtered = search
|
|
||||||
? logs.filter((l) => {
|
|
||||||
const q = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
l.taskName.toLowerCase().includes(q) ||
|
|
||||||
l.entryName.toLowerCase().includes(q) ||
|
|
||||||
l.provider.toLowerCase().includes(q)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: logs;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full p-3 md:p-6 gap-4">
|
|
||||||
{/* Left panel: list */}
|
|
||||||
<Card
|
|
||||||
className={`md:w-80 shrink-0 flex flex-col overflow-hidden ${selectedId ? 'hidden md:flex' : 'flex-1 md:flex-none'}`}
|
|
||||||
>
|
|
||||||
<div className="p-3 border-b border-duck-dark/10">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-duck-dark/40" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search logs..."
|
|
||||||
value={search}
|
|
||||||
onChange={(ev) => setSearch(ev.target.value)}
|
|
||||||
className="w-full pl-8 pr-3 py-1.5 text-sm rounded-md border border-duck-dark/15 bg-background/60 text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
{isLoading && (
|
|
||||||
<div className="flex items-center justify-center h-32 text-duck-dark/30 text-sm">Loading...</div>
|
|
||||||
)}
|
|
||||||
{!isLoading && filtered.length === 0 && (
|
|
||||||
<div className="flex items-center justify-center h-32 text-duck-dark/30 text-sm">No logs found</div>
|
|
||||||
)}
|
|
||||||
{filtered.map((log) => (
|
|
||||||
<Link
|
|
||||||
key={log.id}
|
|
||||||
to={`/task-logs/${log.id}`}
|
|
||||||
className={`block w-full text-left px-3 py-2.5 border-b border-duck-dark/5 hover:bg-duck-dark/5 transition-colors cursor-pointer ${selectedId === String(log.id) ? 'bg-duck-teal/10' : ''}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mb-0.5">
|
|
||||||
{log.isError ? (
|
|
||||||
<AlertCircle className="h-3.5 w-3.5 text-red-500 shrink-0" />
|
|
||||||
) : log.completedAt ? (
|
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
|
||||||
) : (
|
|
||||||
<Clock className="h-3.5 w-3.5 text-amber-500 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium text-duck-dark truncate">{log.taskName}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 ml-5.5">
|
|
||||||
<span className="text-xs text-duck-dark/50 truncate">{log.entryName}</span>
|
|
||||||
<ProviderBadge provider={log.provider} />
|
|
||||||
</div>
|
|
||||||
<div className="text-[10px] text-duck-dark/40 ml-5.5 mt-0.5">{formatDate(log.startedAt)}</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Right panel: log viewer */}
|
|
||||||
<Card className={`flex-1 min-w-0 flex flex-col overflow-hidden ${selectedId ? 'flex' : 'hidden md:flex'}`}>
|
|
||||||
{!selectedLog && (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full text-duck-dark/30 text-sm gap-2">
|
|
||||||
Select a log to view
|
|
||||||
<Link to="/task-logs" className="md:hidden text-duck-teal text-xs cursor-pointer">
|
|
||||||
<ArrowLeft className="h-4 w-4 inline mr-1" />
|
|
||||||
Back to list
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedLog && (
|
|
||||||
<>
|
|
||||||
<div className="shrink-0 px-4 py-3 border-b border-duck-dark/10 flex items-center gap-3">
|
|
||||||
<Link to="/task-logs" className="md:hidden p-1 -ml-1 rounded hover:bg-duck-dark/10 cursor-pointer">
|
|
||||||
<ArrowLeft className="h-4 w-4 text-duck-dark/60" />
|
|
||||||
</Link>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm font-semibold text-duck-dark">{selectedLog.taskName}</span>
|
|
||||||
<ProviderBadge provider={selectedLog.provider} />
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-duck-dark/50 mt-0.5">
|
|
||||||
{selectedLog.entryName} · {selectedLog.model} · {formatDate(selectedLog.startedAt)}
|
|
||||||
{selectedLog.completedAt && ` — ${formatDate(selectedLog.completedAt)}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{selectedLog.isError && (
|
|
||||||
<span className="text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/50 px-2 py-0.5 rounded-full">
|
|
||||||
Error
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
|
||||||
{selectedLog.messages.map((msg, i) => (
|
|
||||||
<MessageBubble key={i} message={msg} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -6,7 +6,6 @@ export * from './Processes';
|
|||||||
export * from './CapabilityPage';
|
export * from './CapabilityPage';
|
||||||
export * from './Settings';
|
export * from './Settings';
|
||||||
export * from './Skills';
|
export * from './Skills';
|
||||||
export * from './TaskLogs';
|
|
||||||
export * from './Tasks';
|
export * from './Tasks';
|
||||||
|
|
||||||
export * from './Files';
|
export * from './Files';
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const RULES: TitleRule[] = [
|
|||||||
{ match: (p) => p.startsWith('/qr-transfer'), title: 'QR Transfer' },
|
{ match: (p) => p.startsWith('/qr-transfer'), title: 'QR Transfer' },
|
||||||
{ match: (p) => p.startsWith('/activity'), title: 'Activity' },
|
{ match: (p) => p.startsWith('/activity'), title: 'Activity' },
|
||||||
{ match: (p) => p.startsWith('/code-editor'), title: 'Code Editor' },
|
{ match: (p) => p.startsWith('/code-editor'), title: 'Code Editor' },
|
||||||
{ match: (p) => p.startsWith('/task-logs'), title: 'Task Logs' },
|
|
||||||
{ match: (p) => p.startsWith('/tasks'), title: 'Tasks' },
|
{ match: (p) => p.startsWith('/tasks'), title: 'Tasks' },
|
||||||
{ match: (p) => p.startsWith('/jobs'), title: 'Jobs' },
|
{ match: (p) => p.startsWith('/jobs'), title: 'Jobs' },
|
||||||
{ match: (p) => p.startsWith('/skills'), title: 'Skills' },
|
{ match: (p) => p.startsWith('/skills'), title: 'Skills' },
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ features listed twice, `db` and `schema` buried at line 270 with three feature b
|
|||||||
(`app-store`/`sidecar-installs`, `email`/`email-accounts`, `server`/`server-config`), `operations` had no
|
(`app-store`/`sidecar-installs`, `email`/`email-accounts`, `server`/`server-config`), `operations` had no
|
||||||
query file at all, and `integrations` had no schema file.
|
query file at all, and `integrations` had no schema file.
|
||||||
|
|
||||||
Two directories are still lopsided and say so by their contents: `operations/` has only a schema (its
|
One directory is still lopsided and says so by its contents: `integrations/` has only queries, because it
|
||||||
`task_logs` is reached directly from `src/servers/`, bypassing this package), and `integrations/` has only
|
spans `server` and `user-data`. (`operations/` was the other, and was deleted on 2026-08-13 along with the
|
||||||
queries, because it spans `server` and `user-data`.
|
Task Logs feature — see below.)
|
||||||
|
|
||||||
**`src/schema.ts` is drizzle-kit's view, not the runtime's.** `drizzle.config.ts` points at it, so a
|
**`src/schema.ts` is drizzle-kit's view, not the runtime's.** `drizzle.config.ts` points at it, so a
|
||||||
commented line there removes a table from the DATABASE without removing a line of code — every query
|
commented line there removes a table from the DATABASE without removing a line of code — every query
|
||||||
|
|||||||
@@ -40,6 +40,3 @@ export * from './user-data';
|
|||||||
export * from './vault';
|
export * from './vault';
|
||||||
export * from './wallet';
|
export * from './wallet';
|
||||||
|
|
||||||
// `operations` is deliberately absent: it has a schema and no queries. Its one table, `task_logs`, is
|
|
||||||
// reached as `schema.taskLogs` from src/servers/api/task-logger.ts, which reaches past this package's
|
|
||||||
// own boundary. Give it a queries.ts and it earns a line here.
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
// Task run logs.
|
|
||||||
//
|
|
||||||
// This file held two other tables until 2026-08-13, both dead and both removed:
|
|
||||||
//
|
|
||||||
// queue_jobs the background job engine's state, from when it kept it in Postgres. It works
|
|
||||||
// on files now (src/servers/queue/storage.ts) and had not read this table since.
|
|
||||||
// terminal_containers a docker id and port per user, from the architecture where every account ran
|
|
||||||
// inside its own container. That is gone — see data-path.ts on getHomeDir.
|
|
||||||
//
|
|
||||||
// Neither had a single reader or writer anywhere in the tree. They were created on every fresh install
|
|
||||||
// by db:push and then never touched.
|
|
||||||
//
|
|
||||||
// What is left is one table with no queries.ts beside it: task_logs is reached as `schema.taskLogs`
|
|
||||||
// from src/servers/api/task-logger.ts, which reaches past this package's boundary. That is the next
|
|
||||||
// thing to fix here.
|
|
||||||
|
|
||||||
import { pgTable, serial, text, integer, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
|
|
||||||
import { users } from '../auth/schema';
|
|
||||||
|
|
||||||
export const taskLogs = pgTable('task_logs', {
|
|
||||||
id: serial('id').primaryKey(),
|
|
||||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
||||||
taskName: text('task_name').notNull(),
|
|
||||||
taskDirName: text('task_dir_name').notNull(),
|
|
||||||
entryName: text('entry_name').notNull(),
|
|
||||||
entryType: text('entry_type').notNull(),
|
|
||||||
provider: text('provider').notNull(),
|
|
||||||
model: text('model').notNull(),
|
|
||||||
isError: boolean('is_error').notNull().default(false),
|
|
||||||
messages: jsonb('messages').notNull().default([]),
|
|
||||||
startedAt: timestamp('started_at', { withTimezone: true }).notNull(),
|
|
||||||
completedAt: timestamp('completed_at', { withTimezone: true }),
|
|
||||||
}, (table) => [
|
|
||||||
index('idx_task_logs_user_started').on(table.userId, table.startedAt),
|
|
||||||
]);
|
|
||||||
@@ -27,7 +27,6 @@ export * from './api-keys/schema'; // api_keys
|
|||||||
export * from './user-data/schema'; // user_settings, user_state, user_integrations, dock_configs
|
export * from './user-data/schema'; // user_settings, user_state, user_integrations, dock_configs
|
||||||
export * from './dashboards/schema'; // dashboards, screens, dashboard_defaults
|
export * from './dashboards/schema'; // dashboards, screens, dashboard_defaults
|
||||||
export * from './server/schema'; // server_config (SMTP lives here), server_integrations
|
export * from './server/schema'; // server_config (SMTP lives here), server_integrations
|
||||||
export * from './operations/schema'; // task_logs
|
|
||||||
export * from './pipeline-jobs/schema'; // pipeline_jobs
|
export * from './pipeline-jobs/schema'; // pipeline_jobs
|
||||||
export * from './chat-events/schema'; // chat_session_events
|
export * from './chat-events/schema'; // chat_session_events
|
||||||
export * from './agent-panels/schema'; // agent_panels
|
export * from './agent-panels/schema'; // agent_panels
|
||||||
|
|||||||
@@ -44,10 +44,6 @@ export type DashboardInsert = typeof Schema.dashboards.$inferInsert;
|
|||||||
export type ScreenSelect = typeof Schema.screens.$inferSelect;
|
export type ScreenSelect = typeof Schema.screens.$inferSelect;
|
||||||
export type ScreenInsert = typeof Schema.screens.$inferInsert;
|
export type ScreenInsert = typeof Schema.screens.$inferInsert;
|
||||||
|
|
||||||
// ── Operations ──
|
|
||||||
|
|
||||||
export type TaskLogSelect = typeof Schema.taskLogs.$inferSelect;
|
|
||||||
export type TaskLogInsert = typeof Schema.taskLogs.$inferInsert;
|
|
||||||
|
|
||||||
// ── Email ──
|
// ── Email ──
|
||||||
|
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import { db, schema } from 'officerdb';
|
|
||||||
import type { TaskInfo } from '@@/api/chat-types';
|
|
||||||
|
|
||||||
type ChatMessage =
|
|
||||||
| { role: 'user'; text: string }
|
|
||||||
| { role: 'assistant'; text: string }
|
|
||||||
| {
|
|
||||||
role: 'tool';
|
|
||||||
toolName: string;
|
|
||||||
toolInput: Record<string, unknown>;
|
|
||||||
toolUseId: string;
|
|
||||||
output?: string;
|
|
||||||
isError?: boolean;
|
|
||||||
}
|
|
||||||
| { role: 'result'; costUsd: number; durationMs: number; numTurns: number; isError: boolean }
|
|
||||||
| { role: 'error'; text: string };
|
|
||||||
|
|
||||||
type TaskLog = {
|
|
||||||
userId: number;
|
|
||||||
taskName: string;
|
|
||||||
taskDirName: string;
|
|
||||||
entryName: string;
|
|
||||||
entryType: 'file' | 'directory';
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
startedAt: Date;
|
|
||||||
messages: ChatMessage[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type LogEntry = {
|
|
||||||
userId: number;
|
|
||||||
log: TaskLog;
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeLogs = new Map<string, LogEntry>();
|
|
||||||
let logCounter = 0;
|
|
||||||
|
|
||||||
export function createTaskLog(userId: number, taskInfo: TaskInfo, provider: string, model: string): string {
|
|
||||||
const logId = `log_${Date.now()}_${++logCounter}`;
|
|
||||||
|
|
||||||
const log: TaskLog = {
|
|
||||||
userId,
|
|
||||||
taskName: taskInfo.taskName,
|
|
||||||
taskDirName: taskInfo.taskDirName,
|
|
||||||
entryName: taskInfo.entryName,
|
|
||||||
entryType: taskInfo.entryType,
|
|
||||||
provider,
|
|
||||||
model,
|
|
||||||
startedAt: new Date(),
|
|
||||||
messages: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
activeLogs.set(logId, { userId, log });
|
|
||||||
return logId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function appendToLog(logId: string, message: ChatMessage) {
|
|
||||||
const entry = activeLogs.get(logId);
|
|
||||||
if (!entry) return;
|
|
||||||
|
|
||||||
// For tool results, update the existing tool message instead of appending
|
|
||||||
if (message.role === 'tool' && message.output !== undefined) {
|
|
||||||
const existing = entry.log.messages.find((m) => m.role === 'tool' && m.toolUseId === message.toolUseId);
|
|
||||||
if (existing && existing.role === 'tool') {
|
|
||||||
existing.output = message.output;
|
|
||||||
existing.isError = message.isError;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.log.messages.push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function finalizeLog(logId: string) {
|
|
||||||
const entry = activeLogs.get(logId);
|
|
||||||
if (!entry) return;
|
|
||||||
|
|
||||||
const { log } = entry;
|
|
||||||
const lastMessage = log.messages[log.messages.length - 1];
|
|
||||||
const isError = lastMessage?.role === 'result' ? lastMessage.isError : lastMessage?.role === 'error';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.insert(schema.taskLogs).values({
|
|
||||||
userId: log.userId,
|
|
||||||
taskName: log.taskName,
|
|
||||||
taskDirName: log.taskDirName,
|
|
||||||
entryName: log.entryName,
|
|
||||||
entryType: log.entryType,
|
|
||||||
provider: log.provider,
|
|
||||||
model: log.model,
|
|
||||||
isError: !!isError,
|
|
||||||
messages: log.messages,
|
|
||||||
startedAt: log.startedAt,
|
|
||||||
completedAt: new Date(),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[task-logger] Failed to write log:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
activeLogs.delete(logId);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { desc, eq } from 'drizzle-orm';
|
|
||||||
import { db, schema } from 'officerdb';
|
|
||||||
import { createRouter } from '../../create-router';
|
|
||||||
|
|
||||||
export const taskLogsRouter = createRouter();
|
|
||||||
|
|
||||||
// GET / — list all logs (metadata only, no messages)
|
|
||||||
taskLogsRouter.get('/', async (ctx) => {
|
|
||||||
const userId = ctx.get('user').id;
|
|
||||||
|
|
||||||
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 /: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 (Number.isNaN(id)) {
|
|
||||||
return ctx.text('Invalid id', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [log] = await db.select().from(schema.taskLogs).where(eq(schema.taskLogs.id, id));
|
|
||||||
|
|
||||||
if (!log || log.userId !== userId) {
|
|
||||||
return ctx.text('Not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.json(log);
|
|
||||||
});
|
|
||||||
@@ -307,9 +307,9 @@ export const CAPABILITIES: Capability[] = [
|
|||||||
label: 'Tasks and jobs',
|
label: 'Tasks and jobs',
|
||||||
description: 'Running capabilities, pipelines and background jobs',
|
description: 'Running capabilities, pipelines and background jobs',
|
||||||
kind: 'execution',
|
kind: 'execution',
|
||||||
api: ['/tasks', '/jobs', '/pipeline-jobs', '/task-logs', '/queue'],
|
api: ['/tasks', '/jobs', '/pipeline-jobs', '/queue'],
|
||||||
ws: ['task-runner', 'pipeline'],
|
ws: ['task-runner', 'pipeline'],
|
||||||
routes: ['/jobs', '/task-logs'],
|
routes: ['/jobs'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'items',
|
key: 'items',
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { scrapeRouter } from './api/scrape/scrape';
|
|||||||
import { uploadRouter } from './api/upload/upload';
|
import { uploadRouter } from './api/upload/upload';
|
||||||
import { settingsRouter } from './api/settings/settings';
|
import { settingsRouter } from './api/settings/settings';
|
||||||
import { dashboardsRouter } from './api/dashboards';
|
import { dashboardsRouter } from './api/dashboards';
|
||||||
import { taskLogsRouter } from './api/task-logs/task-logs';
|
|
||||||
import { router as fileBrowserRouter } from './api/file-browser/router';
|
import { router as fileBrowserRouter } from './api/file-browser/router';
|
||||||
import { musicRouter } from './api/music/router';
|
import { musicRouter } from './api/music/router';
|
||||||
import { vaultRouter } from './api/vault/router';
|
import { vaultRouter } from './api/vault/router';
|
||||||
@@ -199,7 +198,6 @@ const PROTECTED_MOUNTS: [prefix: string, router: ReturnType<typeof createRouter>
|
|||||||
['/user', settingsRouter],
|
['/user', settingsRouter],
|
||||||
['/api-keys', apiKeysRouter], // your own keys; the `account` core capability covers it
|
['/api-keys', apiKeysRouter], // your own keys; the `account` core capability covers it
|
||||||
['/dashboards', dashboardsRouter],
|
['/dashboards', dashboardsRouter],
|
||||||
['/task-logs', taskLogsRouter],
|
|
||||||
['/file-browser', fileBrowserRouter],
|
['/file-browser', fileBrowserRouter],
|
||||||
['/music', musicRouter],
|
['/music', musicRouter],
|
||||||
['/slskd', slskdRouter],
|
['/slskd', slskdRouter],
|
||||||
|
|||||||
Reference in New Issue
Block a user