import React, { useEffect, useMemo, useState } from 'react'; import { Download, RefreshCw, Scissors, Trash2 } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { useAppContext } from '../context/AppContext'; import ArtifactPreviewCard from '../components/data-display/ArtifactPreviewCard'; import { FixedButton, FixedLinkButton, LinkButton } from '../components/ui/Button'; import CodeBlockPanel from '../components/data-display/CodeBlockPanel'; import EmptyState from '../components/data-display/EmptyState'; import { SelectField, TextField } from '../components/ui/FormControls'; import ListPanel from '../components/layout/ListPanel'; import SummaryListItem from '../components/list/SummaryListItem'; import { dataUrlForArtifact, formatArtifactBytes } from '../utils/artifacts'; import { formatLocalDateTime } from '../utils/time'; const NodeArtifacts: React.FC = () => { const { t } = useTranslation(); const { q } = useAppContext(); const [searchParams, setSearchParams] = useSearchParams(); const [items, setItems] = useState([]); const [retentionSummary, setRetentionSummary] = useState>({}); const [selectedID, setSelectedID] = useState(''); const [loading, setLoading] = useState(false); const [prunePending, setPrunePending] = useState(false); const [nodeFilter, setNodeFilter] = useState(searchParams.get('node') || 'all'); const [actionFilter, setActionFilter] = useState(searchParams.get('action') || 'all'); const [kindFilter, setKindFilter] = useState(searchParams.get('kind') || 'all'); const [keepLatest, setKeepLatest] = useState('20'); const apiQuery = useMemo(() => { const params = new URLSearchParams(); params.set('limit', '400'); if (nodeFilter !== 'all') params.set('node', nodeFilter); if (actionFilter !== 'all') params.set('action', actionFilter); if (kindFilter !== 'all') params.set('kind', kindFilter); return params.toString(); }, [nodeFilter, actionFilter, kindFilter]); useEffect(() => { const next = new URLSearchParams(); if (nodeFilter !== 'all') next.set('node', nodeFilter); if (actionFilter !== 'all') next.set('action', actionFilter); if (kindFilter !== 'all') next.set('kind', kindFilter); setSearchParams(next, { replace: true }); }, [nodeFilter, actionFilter, kindFilter, setSearchParams]); const loadArtifacts = async () => { setLoading(true); try { const query = q ? `${q}&${apiQuery}` : `?${apiQuery}`; const r = await fetch(`/webui/api/node_artifacts${query}`); if (!r.ok) throw new Error(await r.text()); const j = await r.json(); const next = Array.isArray(j.items) ? j.items : []; setRetentionSummary(j.artifact_retention && typeof j.artifact_retention === 'object' ? j.artifact_retention : {}); setItems(next); if (next.length === 0) { setSelectedID(''); } else if (!next.some((item: any) => String(item?.id || '') === selectedID)) { setSelectedID(String(next[0]?.id || '')); } } catch (err) { console.error(err); setItems([]); setSelectedID(''); } finally { setLoading(false); } }; useEffect(() => { loadArtifacts(); }, [q, apiQuery]); const nodes = useMemo(() => Array.from(new Set(items.map((item) => String(item?.node || '')).filter(Boolean))).sort(), [items]); const actions = useMemo(() => Array.from(new Set(items.map((item) => String(item?.action || '')).filter(Boolean))).sort(), [items]); const kinds = useMemo(() => Array.from(new Set(items.map((item) => String(item?.kind || '')).filter(Boolean))).sort(), [items]); const filteredItems = items; const selected = useMemo(() => { return filteredItems.find((item) => String(item?.id || '') === selectedID) || filteredItems[0] || null; }, [filteredItems, selectedID]); async function deleteArtifact(id: string) { const r = await fetch(`/webui/api/node_artifacts/delete${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); if (!r.ok) throw new Error(await r.text()); await loadArtifacts(); } async function pruneArtifacts() { setPrunePending(true); try { const keep = Math.max(0, Number.parseInt(keepLatest || '0', 10) || 0); const r = await fetch(`/webui/api/node_artifacts/prune${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ node: nodeFilter === 'all' ? '' : nodeFilter, action: actionFilter === 'all' ? '' : actionFilter, kind: kindFilter === 'all' ? '' : kindFilter, keep_latest: keep, limit: 1000, }), }); if (!r.ok) throw new Error(await r.text()); const j = await r.json(); setRetentionSummary(j && typeof j === 'object' ? { ...retentionSummary, manual_pruned: j.pruned, manual_deleted_files: j.deleted_files, last_run_at: new Date().toISOString() } : retentionSummary); await loadArtifacts(); } catch (err) { console.error(err); } finally { setPrunePending(false); } } function downloadURL(id: string) { return `/webui/api/node_artifacts/download${q ? `${q}&id=${encodeURIComponent(id)}` : `?id=${encodeURIComponent(id)}`}`; } function exportURL() { const query = q ? `${q}&${apiQuery}` : `?${apiQuery}`; return `/webui/api/node_artifacts/export${query}`; } return (

{t('nodeArtifacts')}

{t('nodeArtifactsHint')}
{t('nodeArtifactsRetention')}
{t('nodeArtifactsRetentionKeepLatest')}: {Number(retentionSummary?.keep_latest || 0) || '-'}
{t('nodeArtifactsRetentionRetainDays')}: {Number(retentionSummary?.retain_days || 0)}
{t('nodeArtifactsRetentionPruned')}: {Number(retentionSummary?.pruned || retentionSummary?.manual_pruned || 0)}
{t('nodeArtifactsRetentionRemaining')}: {Number(retentionSummary?.remaining || filteredItems.length || 0)}
setNodeFilter(e.target.value)}> {nodes.map((node) => )} setActionFilter(e.target.value)}> {actions.map((action) => )} setKindFilter(e.target.value)}> {kinds.map((kind) => )}
setKeepLatest(e.target.value)} inputMode="numeric" placeholder={t('nodeArtifactsKeepLatest')} dense />
{filteredItems.length === 0 ? ( ) : filteredItems.map((item, index) => { const active = String(selected?.id || '') === String(item?.id || ''); return ( setSelectedID(String(item?.id || ''))} active={active} className="border-b py-3" title={String(item?.name || item?.source_path || `artifact-${index + 1}`)} subtitle={`${String(item?.node || '-')} · ${String(item?.action || '-')} · ${String(item?.kind || '-')}`} meta={formatLocalDateTime(item?.time)} /> ); })}
{t('nodeArtifactDetail')}
}>
{!selected ? ( ) : ( <>
{String(selected?.name || selected?.source_path || 'artifact')}
{String(selected?.node || '-')} · {String(selected?.action || '-')} · {formatLocalDateTime(selected?.time)}
{t('download')} deleteArtifact(String(selected?.id || ''))} variant="danger" label={t('delete')}>
{t('node')}
{String(selected?.node || '-')}
{t('action')}
{String(selected?.action || '-')}
{t('kind')}
{String(selected?.kind || '-')}
{t('size')}
{formatArtifactBytes(selected?.size_bytes)}
{String(selected?.source_path || selected?.path || selected?.url || '-')}
{String(selected?.content_text || '').trim() === '' && !String(selected?.content_base64 || '').trim() ? ( ) : null} {JSON.stringify(selected, null, 2)} )}
); }; export default NodeArtifacts;