feat: add server-sent events for logging and activity tracking

- Implemented a custom tracing layer (`AppLogLayer`) to capture log events and broadcast them to SSE clients.
- Created admin routes for streaming server logs and listing recent activity logs.
- Added an activity log repository interface and SQLite implementation for persisting activity events.
- Integrated activity logging into user authentication and channel CRUD operations.
- Developed frontend components for displaying server logs and activity logs in the admin panel.
- Enhanced the video player with a stats overlay for monitoring streaming metrics.
This commit is contained in:
2026-03-16 02:21:40 +01:00
parent 4df6522952
commit e805028d46
28 changed files with 893 additions and 8 deletions

View File

@@ -0,0 +1,73 @@
"use client";
import type { ActivityEvent } from "@/lib/types";
const eventColors: Record<string, string> = {
channel_created: "bg-green-900/40 text-green-400",
channel_updated: "bg-blue-900/40 text-blue-400",
channel_deleted: "bg-red-900/40 text-red-400",
schedule_generated: "bg-violet-900/40 text-violet-400",
user_login: "bg-zinc-800 text-zinc-400",
};
function fmtTs(ts: string) {
try {
const d = new Date(ts);
return d.toLocaleTimeString(undefined, { hour12: false });
} catch {
return ts;
}
}
interface ActivityLogPanelProps {
events: ActivityEvent[];
isLoading: boolean;
}
export function ActivityLogPanel({ events, isLoading }: ActivityLogPanelProps) {
return (
<div className="flex h-full flex-col">
<div className="border-b border-zinc-800 px-4 py-2.5">
<span className="text-xs font-semibold uppercase tracking-widest text-violet-400">
Activity
</span>
</div>
<div className="flex-1 overflow-y-auto p-3">
{isLoading && events.length === 0 ? (
<p className="mt-8 text-center text-xs text-zinc-600">Loading</p>
) : events.length === 0 ? (
<p className="mt-8 text-center text-xs text-zinc-600">No activity yet.</p>
) : (
<div className="flex flex-col gap-2">
{events.map((event) => (
<div
key={event.id}
className="rounded-md border border-zinc-800 bg-zinc-900 p-3"
>
<div className="mb-1 flex items-center gap-2">
<span
className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${
eventColors[event.event_type] ?? "bg-zinc-800 text-zinc-400"
}`}
>
{event.event_type.replace(/_/g, " ")}
</span>
<span className="ml-auto font-mono text-[10px] text-zinc-600">
{fmtTs(event.timestamp)}
</span>
</div>
<p className="text-xs text-zinc-300">{event.detail}</p>
{event.channel_id && (
<p className="mt-0.5 font-mono text-[10px] text-zinc-600">
ch: {event.channel_id.slice(0, 8)}
</p>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}