Compare commits

..

3 Commits

Author SHA1 Message Date
b5eb553d74 Merge pull request 'Add richer metadata to logs and expandable detail panel' (#14) from probablycorey/richer-log-metadata into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #14
2026-03-10 17:34:00 +00:00
926b9b17c9 Merge branch 'main' into probablycorey/richer-log-metadata
Some checks failed
CI / test (pull_request) Has been cancelled
2026-03-10 17:33:30 +00:00
6555be012a Add richer metadata to logs and expandable detail panel
Some checks failed
CI / test (pull_request) Has been cancelled
Enriches webhook logs with sender and ref for better visibility into who triggered events and what they affected (e.g. which branch). PR logs now include the title, comment logs include the first 200 characters of the comment body.

Adds an expandable detail row in the log viewer UI that shows all metadata as key-value pairs when clicked, keeping the summary line clean.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 10:25:47 -07:00
4 changed files with 60 additions and 16 deletions

View File

@ -60,7 +60,7 @@ class PRHandler {
await this.handleStateChange(pullRequest, repository)
}
log({ type: "pr", action, pr: payload.number, repo: repository.full_name, user: pullRequest.user.login })
log({ type: "pr", action, pr: payload.number, repo: repository.full_name, user: pullRequest.user.login, title: pullRequest.title })
}
static async handleOpened(pullRequest: Gitea.PullRequest, repository: Gitea.Repository) {
@ -153,7 +153,7 @@ class CommentHandler {
await this.handleDeleted(comment)
}
log({ type: "comment", action, pr: issue.number, repo: repository.full_name, user: comment.user.login })
log({ type: "comment", action, pr: issue.number, repo: repository.full_name, user: comment.user.login, body: comment.body.slice(0, 200) })
}
static async handleCreated(issue: Gitea.Issue, comment: Gitea.Comment, repo: string) {

View File

@ -3,10 +3,10 @@ import { basename } from "node:path"
import { getConfig } from "./config"
export type LogEvent =
| { type: "webhook"; eventType: string; repo: string }
| { type: "webhook"; eventType: string; repo: string; sender?: string; ref?: string }
| { type: "webhook-ignored"; eventType: string; repo: string }
| { type: "pr"; action: string; pr: number; repo: string; user: string }
| { type: "comment"; action: string; pr: number; repo: string; user: string }
| { type: "pr"; action: string; pr: number; repo: string; user: string; title?: string }
| { type: "comment"; action: string; pr: number; repo: string; user: string; body?: string }
| { type: "comment-skipped"; pr: number; repo: string; commentId: number }
| { type: "review"; action: string; pr: number; repo: string; user: string }
| { type: "thread-auto-created"; pr: number; repo: string }
@ -96,6 +96,9 @@ onLog((event) => {
if ("repo" in event) parts.push(dim(event.repo))
if ("pr" in event) parts.push(cyan(`#${event.pr}`))
if ("user" in event) parts.push(dim(event.user))
if ("sender" in event && event.sender) parts.push(dim(event.sender))
if ("ref" in event && event.ref) parts.push(dim(event.ref))
if ("title" in event && event.title) parts.push(event.title)
if ("detail" in event) parts.push(event.detail)
if ("command" in event) parts.push(event.command)
if ("eventType" in event) parts.push(event.eventType)

View File

@ -18,7 +18,9 @@ const server = serve({
const payload = await req.json()
const eventType = req.headers.get("X-Gitea-Event") || "unknown"
const repo = (payload as any)?.repository?.full_name || "unknown"
log({ type: "webhook", eventType, repo })
const sender = (payload as any)?.sender?.login as string | undefined
const ref = (payload as any)?.ref as string | undefined
log({ type: "webhook", eventType, repo, sender, ref })
try {
await handleGiteaWebhook(payload, eventType as any)

View File

@ -17,7 +17,19 @@ const typeColors: Record<string, string> = {
error: "#f87171",
}
function EventRow({ event }: { event: StoredLogEvent }) {
const summaryFields = ["type", "ts"] as const
function getEventMeta(event: StoredLogEvent): Record<string, string> {
const meta: Record<string, string> = {}
for (const [key, value] of Object.entries(event)) {
if (summaryFields.includes(key as any)) continue
if (value === undefined) continue
meta[key] = typeof value === "string" ? value : String(value)
}
return meta
}
function EventRow({ event, index }: { event: StoredLogEvent; index: number }) {
const color = typeColors[event.type] || "#9ca3af"
const time = event.ts.slice(11, 19)
@ -35,14 +47,41 @@ function EventRow({ event }: { event: StoredLogEvent }) {
details.push(msg)
}
const meta = getEventMeta(event)
const hasExtra = Object.keys(meta).length > 0
return (
<tr style="border-bottom: 1px solid #2a2a2a">
<td data-ts={event.ts} style="padding: 6px 12px; color: #6b7280; white-space: nowrap; font-size: 13px">{time}</td>
<td style={`padding: 6px 12px; color: ${color}; white-space: nowrap; font-weight: 600; font-size: 13px`}>
{event.type}
</td>
<td style="padding: 6px 12px; color: #d1d5db; font-size: 13px">{details.join(" ")}</td>
</tr>
<>
<tr
style={`border-bottom: 1px solid #2a2a2a${hasExtra ? "; cursor: pointer" : ""}`}
data-row={index}
onclick={hasExtra ? `(function(e){var d=document.getElementById('detail-${index}');if(d){d.style.display=d.style.display==='none'?'table-row':'none';e.currentTarget.querySelector('.expand-icon').textContent=d.style.display==='none'?'+':'-'}})(event)` : undefined}
>
<td style="padding: 6px 4px 6px 12px; color: #4a4a4a; font-size: 11px; width: 16px; user-select: none">
{hasExtra && <span class="expand-icon">+</span>}
</td>
<td data-ts={event.ts} style="padding: 6px 12px; color: #6b7280; white-space: nowrap; font-size: 13px">{time}</td>
<td style={`padding: 6px 12px; color: ${color}; white-space: nowrap; font-weight: 600; font-size: 13px`}>
{event.type}
</td>
<td style="padding: 6px 12px; color: #d1d5db; font-size: 13px">{details.join(" ")}</td>
</tr>
{hasExtra && (
<tr id={`detail-${index}`} style="display: none; background: #141414">
<td></td>
<td colspan="3" style="padding: 8px 12px 12px">
<div style="display: flex; flex-wrap: wrap; gap: 6px 20px; font-size: 12px">
{Object.entries(meta).map(([key, value]) => (
<div style="display: flex; gap: 6px">
<span style="color: #6b7280">{key}</span>
<span style="color: #d1d5db; max-width: 500px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">{value}</span>
</div>
))}
</div>
</td>
</tr>
)}
</>
)
}
@ -154,8 +193,8 @@ export function LogsPage(req: Request) {
{/* Events table */}
<table style="width: 100%; border-collapse: collapse">
<tbody>
{events.map((event) => (
<EventRow event={event} />
{events.map((event, i) => (
<EventRow event={event} index={i} />
))}
</tbody>
</table>