upload ai slop

This commit is contained in:
2026-02-25 22:49:38 +02:00
parent 49ebbc34fa
commit 3dfc78cc4c
93 changed files with 7745 additions and 1732 deletions

View File

@@ -0,0 +1,188 @@
import React, { useState, useEffect, useMemo, useRef } from "react";
import { Box, Button, Menu, MenuItem, Typography } from "@mui/material";
import ListIcon from "@mui/icons-material/List";
import { useQuery } from "@tanstack/react-query";
import hljs from "highlight.js/lib/core";
import plaintext from "highlight.js/lib/languages/plaintext";
import "highlight.js/styles/github.css";
import { Client } from "api/client/AhrimanClient";
import { QueryKeys } from "api/QueryKeys";
import { formatTimestamp } from "components/common/formatTimestamp";
import CopyButton from "components/common/CopyButton";
import type { LogRecord } from "api/types/LogRecord";
import type { RepositoryId } from "api/types/RepositoryId";
hljs.registerLanguage("plaintext", plaintext);
interface LogVersion {
version: string;
processId: string;
created: number;
logs: string;
}
interface BuildLogsTabProps {
packageBase: string;
repo: RepositoryId;
refetchInterval: number | false;
}
function convertLogs(records: LogRecord[], filter?: (r: LogRecord) => boolean): string {
return records
.filter(filter || Boolean)
.map((r) => `[${new Date(r.created * 1000).toISOString()}] ${r.message}`)
.join("\n");
}
export default function BuildLogsTab({ packageBase, repo, refetchInterval }: BuildLogsTabProps): React.JSX.Element {
const [activeIndex, setActiveIndex] = useState(0);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const codeRef = useRef<HTMLElement>(null);
const preRef = useRef<HTMLElement>(null);
const initialScrollDone = useRef(false);
const { data: allLogs } = useQuery<LogRecord[]>({
queryKey: QueryKeys.logs(packageBase, repo),
queryFn: () => Client.fetchLogs(packageBase, repo),
enabled: !!packageBase,
});
// Build version selectors from all logs
const versions = useMemo<LogVersion[]>(() => {
if (!allLogs || allLogs.length === 0) {
return [];
}
const grouped: Record<string, LogRecord & { minCreated: number }> = {};
for (const record of allLogs) {
const key = `${record.version}-${record.process_id}`;
if (!grouped[key]) {
grouped[key] = { ...record, minCreated: record.created };
} else {
grouped[key].minCreated = Math.min(grouped[key].minCreated, record.created);
}
}
return Object.values(grouped)
.sort((a, b) => b.minCreated - a.minCreated)
.map((v) => ({
version: v.version,
processId: v.process_id,
created: v.minCreated,
logs: convertLogs(
allLogs,
(r) => r.version === v.version && r.process_id === v.process_id,
),
}));
}, [allLogs]);
// Reset active index when data changes
const [prevAllLogs, setPrevAllLogs] = useState(allLogs);
if (allLogs !== prevAllLogs) {
setPrevAllLogs(allLogs);
setActiveIndex(0);
initialScrollDone.current = false;
}
// Refresh active version logs when using auto-refresh
const activeVersion = versions[activeIndex];
const { data: versionLogs } = useQuery<LogRecord[]>({
queryKey: activeVersion
? QueryKeys.logsVersion(packageBase, repo, activeVersion.version, activeVersion.processId)
: ["logs-none"],
queryFn: () =>
activeVersion
? Client.fetchLogs(packageBase, repo, activeVersion.version, activeVersion.processId)
: Promise.resolve([]),
enabled: !!activeVersion && !!refetchInterval,
refetchInterval,
});
// Derive displayed logs: prefer fresh polled data when available
const displayedLogs = useMemo(() => {
if (versionLogs && versionLogs.length > 0) {
return convertLogs(versionLogs);
}
return activeVersion?.logs ?? "";
}, [versionLogs, activeVersion]);
// Highlight code
useEffect(() => {
if (codeRef.current && displayedLogs) {
codeRef.current.textContent = displayedLogs;
delete codeRef.current.dataset.highlighted;
hljs.highlightElement(codeRef.current);
}
}, [displayedLogs]);
// Auto-scroll: always scroll to bottom on initial load, then only if already near bottom
useEffect(() => {
if (preRef.current && displayedLogs) {
const el = preRef.current;
if (!initialScrollDone.current) {
el.scrollTop = el.scrollHeight;
initialScrollDone.current = true;
} else {
const isAtBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
if (isAtBottom) {
el.scrollTop = el.scrollHeight;
}
}
}
}, [displayedLogs]);
return (
<Box sx={{ display: "flex", gap: 1, mt: 1 }}>
<Box>
<Button
size="small"
startIcon={<ListIcon />}
onClick={(e) => setAnchorEl(e.currentTarget)}
/>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
{versions.map((v, idx) => (
<MenuItem
key={`${v.version}-${v.processId}`}
selected={idx === activeIndex}
onClick={() => {
setActiveIndex(idx);
setAnchorEl(null);
initialScrollDone.current = false;
}}
>
<Typography variant="body2">{formatTimestamp(v.created)}</Typography>
</MenuItem>
))}
{versions.length === 0 && (
<MenuItem disabled>No logs available</MenuItem>
)}
</Menu>
</Box>
<Box sx={{ flex: 1, position: "relative" }}>
<Box
ref={preRef}
component="pre"
sx={{
backgroundColor: "#f6f8fa",
p: 2,
borderRadius: 1,
overflow: "auto",
maxHeight: 400,
fontSize: "0.8rem",
fontFamily: "monospace",
}}
>
<code ref={codeRef} className="language-plaintext" />
</Box>
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
<CopyButton getText={() => displayedLogs} />
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,60 @@
import React, { useEffect, useRef } from "react";
import { Box } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import hljs from "highlight.js/lib/core";
import diff from "highlight.js/lib/languages/diff";
import "highlight.js/styles/github.css";
import { Client } from "api/client/AhrimanClient";
import { QueryKeys } from "api/QueryKeys";
import CopyButton from "components/common/CopyButton";
import type { Changes } from "api/types/Changes";
import type { RepositoryId } from "api/types/RepositoryId";
hljs.registerLanguage("diff", diff);
interface ChangesTabProps {
packageBase: string;
repo: RepositoryId;
}
export default function ChangesTab({ packageBase, repo }: ChangesTabProps): React.JSX.Element {
const codeRef = useRef<HTMLElement>(null);
const { data } = useQuery<Changes>({
queryKey: QueryKeys.changes(packageBase, repo),
queryFn: () => Client.fetchChanges(packageBase, repo),
enabled: !!packageBase,
});
const changesText = data?.changes ?? "";
useEffect(() => {
if (codeRef.current) {
codeRef.current.textContent = changesText;
delete codeRef.current.dataset.highlighted;
hljs.highlightElement(codeRef.current);
}
}, [changesText]);
return (
<Box sx={{ position: "relative", mt: 1 }}>
<Box
component="pre"
sx={{
backgroundColor: "#f6f8fa",
p: 2,
borderRadius: 1,
overflow: "auto",
maxHeight: 400,
fontSize: "0.8rem",
fontFamily: "monospace",
}}
>
<code ref={codeRef} className="language-diff" />
</Box>
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
<CopyButton getText={() => changesText} />
</Box>
</Box>
);
}

View File

@@ -0,0 +1,60 @@
import type React from "react";
import { Box } from "@mui/material";
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
import { useQuery } from "@tanstack/react-query";
import EventDurationLineChart from "components/charts/EventDurationLineChart";
import { Client } from "api/client/AhrimanClient";
import { QueryKeys } from "api/QueryKeys";
import { formatTimestamp } from "components/common/formatTimestamp";
import type { Event } from "api/types/Event";
import type { RepositoryId } from "api/types/RepositoryId";
interface EventsTabProps {
packageBase: string;
repo: RepositoryId;
}
interface EventRow {
id: number;
timestamp: string;
event: string;
message: string;
}
const columns: GridColDef<EventRow>[] = [
{ field: "timestamp", headerName: "date", width: 180, align: "right", headerAlign: "right" },
{ field: "event", headerName: "event", flex: 1 },
{ field: "message", headerName: "description", flex: 2 },
];
export default function EventsTab({ packageBase, repo }: EventsTabProps): React.JSX.Element {
const { data: events = [] } = useQuery<Event[]>({
queryKey: QueryKeys.events(repo, packageBase),
queryFn: () => Client.fetchEvents(repo, packageBase, 30),
enabled: !!packageBase,
});
const rows: EventRow[] = events.map((e, idx) => ({
id: idx,
timestamp: formatTimestamp(e.created),
event: e.event,
message: e.message ?? "",
}));
return (
<Box sx={{ mt: 1 }}>
<EventDurationLineChart events={events} />
<DataGrid
rows={rows}
columns={columns}
density="compact"
initialState={{
sorting: { sortModel: [{ field: "timestamp", sort: "desc" }] },
}}
pageSizeOptions={[10, 25]}
sx={{ height: 300, mt: 1 }}
disableRowSelectionOnClick
/>
</Box>
);
}