Sanitized mirror from private repository - 2026-04-18 11:19:59 UTC
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m14s
Documentation / Deploy to GitHub Pages (push) Has been skipped

This commit is contained in:
Gitea Mirror Bot
2026-04-18 11:19:59 +00:00
commit fb00a325d1
1418 changed files with 359990 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { usePoll } from "@/lib/use-poll";
import { fetchAPI } from "@/lib/api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { CardSkeleton } from "@/components/skeleton";
interface LogFile {
name: string;
filename?: string;
size?: string;
size_bytes?: number;
modified?: number;
}
export default function LogsPage() {
const { data: logsRaw } = usePoll<LogFile[] | { files: LogFile[] }>("/api/logs", 60000);
const logFiles = Array.isArray(logsRaw) ? logsRaw : (logsRaw?.files ?? []);
const [selected, setSelected] = useState<string | null>(null);
const [content, setContent] = useState<string>("");
const [loadingContent, setLoadingContent] = useState(false);
const [search, setSearch] = useState("");
useEffect(() => {
if (!selected) {
setContent("");
return;
}
let cancelled = false;
setLoadingContent(true);
fetchAPI<{ lines?: string[]; content?: string } | string>(`/api/logs/${encodeURIComponent(selected)}?tail=200`)
.then((data) => {
if (cancelled) return;
if (typeof data === "string") setContent(data);
else if (Array.isArray((data as Record<string,unknown>).lines)) setContent(((data as Record<string,unknown>).lines as string[]).join("\n"));
else if ((data as Record<string,unknown>).content) setContent(String((data as Record<string,unknown>).content));
else setContent(JSON.stringify(data, null, 2));
})
.catch((err) => {
if (cancelled) return;
setContent(`Error loading log: ${err}`);
})
.finally(() => {
if (!cancelled) setLoadingContent(false);
});
return () => { cancelled = true; };
}, [selected]);
const filteredLines = useMemo(() => {
if (!content) return [];
const lines = content.split("\n");
if (!search.trim()) return lines;
const lower = search.toLowerCase();
return lines.filter(line => line.toLowerCase().includes(lower));
}, [content, search]);
return (
<div className="space-y-8">
<h1 className="text-lg font-semibold">Logs</h1>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-5" style={{ minHeight: "500px" }}>
{/* Left sidebar: log file list */}
<Card className="lg:col-span-1">
<CardHeader className="pb-2">
<CardTitle className="text-base font-semibold">Log Files</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-[460px]">
{logFiles.length === 0 ? (
<CardSkeleton lines={8} />
) : (
<div className="space-y-1">
{logFiles.map((file) => (
<button
key={file.name}
onClick={() => setSelected(file.name)}
className={cn(
"w-full text-left rounded-lg px-3 py-2 text-sm transition-all duration-200",
selected === file.name
? "bg-white/[0.06] text-foreground"
: "text-muted-foreground hover:bg-white/[0.03] hover:text-foreground"
)}
>
<p className="font-medium truncate">{file.name}</p>
{(file.size || file.size_bytes != null) && (
<p className="text-[10px] text-muted-foreground/60">
{file.size ?? (file.size_bytes != null ? `${(file.size_bytes / 1024).toFixed(0)} KB` : "")}
</p>
)}
</button>
))}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
{/* Right: log content viewer */}
<Card className="lg:col-span-3">
<CardHeader className="pb-2 flex flex-row items-center justify-between gap-4">
<CardTitle className="text-base font-semibold">
{selected ?? "Select a log file"}
</CardTitle>
{selected && (
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter lines..."
className="rounded-lg glass-input px-3 py-1.5 text-xs w-48"
/>
)}
</CardHeader>
<CardContent>
<ScrollArea className="h-[460px]">
{!selected ? (
<p className="text-xs text-muted-foreground/60 py-4 text-center">
Select a log file from the sidebar
</p>
) : loadingContent ? (
<CardSkeleton lines={10} />
) : (
<pre className="text-[11px] font-mono text-foreground whitespace-pre-wrap break-all leading-relaxed">
{filteredLines.join("\n") || "No matching lines"}
</pre>
)}
</ScrollArea>
</CardContent>
</Card>
</div>
</div>
);
}