feat(errors): per-account error modal with persisted error list
Accounts finishing done_with_errors showed only a count and a single
last_error. This adds a modal listing every concrete error of the
account's most recent run.
- migration 0005: account_errors table (kind folder|message|account,
folder, message_ref, error, created_at; ON DELETE CASCADE; indexed)
- store: AddAccountError / ClearAccountErrors / ListAccountErrors
- copy: OnError callback captures per-message error text (previously
only counted), with a "UID N: subject" reference
- orchestrator: clear errors at run start; persist folder/message/
account errors; cap 500 rows/account/run with a suppressed-note row
- api: GET /api/tasks/{id}/accounts/{accountId}/errors
- web: AccountErrorsModal, clickable ERRORS count, api + styles
Verified: migration applies on Postgres 18; store add/list/clear and
cascade tests pass against real pg; backend build/vet/test green; web
tsc+vite build and oxlint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9Eq4JtWjyNTv5qat3B3mM
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Modal } from './Modal'
|
||||
import { listAccountErrors, type AccountError } from '../api'
|
||||
|
||||
const fmt = (iso: string) => (iso ? new Date(iso).toLocaleString() : '—')
|
||||
|
||||
export function AccountErrorsModal({
|
||||
taskId,
|
||||
account,
|
||||
onClose,
|
||||
}: {
|
||||
taskId: number
|
||||
account: { id: number; src_login: string } | null
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [errors, setErrors] = useState<AccountError[] | null>(null)
|
||||
const [failed, setFailed] = useState(false)
|
||||
const open = account !== null
|
||||
|
||||
useEffect(() => {
|
||||
if (!account) return
|
||||
setErrors(null)
|
||||
setFailed(false)
|
||||
listAccountErrors(taskId, account.id)
|
||||
.then((e) => setErrors(e ?? []))
|
||||
.catch(() => setFailed(true))
|
||||
}, [taskId, account])
|
||||
|
||||
return (
|
||||
<Modal open={open} title={account ? `Errors — ${account.src_login}` : 'Errors'} onClose={onClose} size="lg">
|
||||
<div className="tbl-wrap">
|
||||
<table className="tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Kind</th>
|
||||
<th>Folder</th>
|
||||
<th>Message</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{failed ? (
|
||||
<tr className="empty-row"><td colSpan={5}>failed to load errors</td></tr>
|
||||
) : errors === null ? (
|
||||
<tr className="empty-row"><td colSpan={5}>loading…</td></tr>
|
||||
) : errors.length === 0 ? (
|
||||
<tr className="empty-row"><td colSpan={5}>no errors recorded</td></tr>
|
||||
) : (
|
||||
errors.map((e) => (
|
||||
<tr key={e.id}>
|
||||
<td>{fmt(e.created_at)}</td>
|
||||
<td>{e.kind}</td>
|
||||
<td>{e.folder || '—'}</td>
|
||||
<td>{e.message_ref || '—'}</td>
|
||||
<td className="err-cell">{e.error}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user