"use client"; /** * `` — surfaces the in-process wide-event ring buffer * (`src/lib/logging/in-memory-buffer.ts`) so admins can drill into the most * recent ~500 structured events without standing up a Loki stack just to * read the last hour. * * Limitations (covered by the section header): * - Per-process. Under split web/worker deployments, only the web buffer * is visible here. Worker logs ship to Loki when configured. * - Volatile. Restart drops the buffer. * * Privacy: every event passes through `redactSecrets()` server-side before * reaching this component; storage stays raw so the diagnostic value * survives shipping to Loki. */ import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { AlertCircle, AlertTriangle, CheckCircle2, FileText, Info, Loader2, RefreshCw, X, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { formatDateTime } from "@/lib/format"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import type { WideEvent, LogLevel } from "@/lib/logging/types"; import { apiGet } from "@/lib/api/api-fetch"; type RangePreset = "15m" | "1h" | "6h" | "all"; interface AppLogsResponse { events: WideEvent[]; meta: { total: number; bufferMax: number; }; } function rangeToSince(range: RangePreset): string | undefined { if (range === "all") return undefined; const now = Date.now(); const minutes = range === "15m" ? 15 : range === "1h" ? 60 : 6 * 60; return new Date(now - minutes * 60 * 1000).toISOString(); } function levelIcon(level: LogLevel) { switch (level) { case "error": return ; case "warn": return ; case "debug": return ; case "info": default: return ; } } export function AppLogPreviewSection() { const { t } = useTranslations(); const [traceId, setTraceId] = useState(""); const [actionFilter, setActionFilter] = useState(""); const [level, setLevel] = useState("__all__"); const [range, setRange] = useState("1h"); const [selected, setSelected] = useState(null); const params = useMemo(() => { const p = new URLSearchParams(); if (traceId.trim()) p.set("traceId", traceId.trim()); if (actionFilter.trim()) p.set("action", actionFilter.trim()); if (level !== "__all__") p.set("level", level); const since = rangeToSince(range); if (since) p.set("since", since); return p; }, [traceId, actionFilter, level, range]); const { data, isLoading, refetch, isFetching } = useQuery({ queryKey: queryKeys.adminAppLogs(traceId, actionFilter, level, range), queryFn: async () => { return apiGet( `/api/admin/app-logs?${params.toString()}`, ); }, refetchInterval: 30_000, }); const events = data?.events ?? []; return ( refetch()} disabled={isFetching} aria-label={t("admin.section.app-logs.refresh")} > } />
setTraceId(e.target.value)} aria-label={t("admin.section.app-logs.filterTraceId")} /> setActionFilter(e.target.value)} aria-label={t("admin.section.app-logs.filterAction")} />
{isLoading ? (
) : events.length === 0 ? ( } title={t("admin.section.app-logs.empty")} description={t("admin.section.app-logs.emptyDescription")} /> ) : ( // v1.4.27 MB5 — keep the table scroll container, but lift the // "showing X of Y" summary line out into a sibling `
` so // the meta info stays reachable on narrow viewports even when // the table is scrolled horizontally. The refresh control was // already in the header above the table wrapper. <>
{/* `app-log-rows` names the read events. The branch above it is a spinner and the one below an empty state, so the marker exists only once the buffer has actually answered. */} {events.map((event, i) => { const actionLabel = event.action?.name ?? (`${event.http?.method ?? ""} ${event.http?.path ?? ""}`.trim() || event.kind); return ( setSelected(event)} > ); })}
{t("admin.section.app-logs.colLevel")} {t("admin.section.app-logs.colTimestamp")} {t("admin.section.app-logs.colAction")} {t("admin.section.app-logs.colDuration")} {t("admin.section.app-logs.colTraceId")}
{levelIcon(event.level)} {formatDateTime(event.timestamp)} {event.duration_ms} ms {event.trace_id.slice(0, 8)}…
{t("admin.section.app-logs.showing", { count: events.length, bufferMax: data?.meta.bufferMax ?? 500, })}
)} !open && setSelected(null)} > {selected && levelIcon(selected.level)} {selected?.action?.name ?? selected?.http?.path ?? t("admin.section.app-logs.eventDetails")}
{selected && (
                {JSON.stringify(selected, null, 2)}
              
)}
); }