This commit is contained in:
lpf
2026-03-03 10:36:53 +08:00
parent 35b0ad1bfd
commit bd93c12edc
30 changed files with 1311 additions and 262 deletions

View File

@@ -64,7 +64,7 @@ const Chat: React.FC = () => {
const ur = await fetch(`/webui/api/upload${q}`, { method: 'POST', body: fd });
const uj = await ur.json(); media = uj.path || '';
} catch (e) {
console.error('Upload failed', e);
console.error('L0053', e);
}
}

View File

@@ -103,7 +103,7 @@ const Cron: React.FC = () => {
});
}
} catch (e) {
console.error('Failed to fetch job details', e);
console.error('L0068', e);
}
} else {
setEditingCron(null);

View File

@@ -0,0 +1,81 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
type CodeItem = {
code: number;
text: string;
};
const LogCodes: React.FC = () => {
const { t } = useTranslation();
const { q } = useAppContext();
const [items, setItems] = useState<CodeItem[]>([]);
const [kw, setKw] = useState('');
useEffect(() => {
const load = async () => {
try {
const paths = [`/webui/log-codes.json${q}`, '/log-codes.json'];
for (const p of paths) {
const r = await fetch(p);
if (!r.ok) continue;
const j = await r.json();
if (Array.isArray(j?.items)) {
setItems(j.items);
return;
}
}
} catch {
setItems([]);
}
};
load();
}, [q]);
const filtered = useMemo(() => {
const k = kw.trim().toLowerCase();
if (!k) return items;
return items.filter((it) => String(it.code).includes(k) || it.text.toLowerCase().includes(k));
}, [items, kw]);
return (
<div className="p-8 max-w-7xl mx-auto space-y-6">
<div className="flex items-center justify-between gap-3">
<h1 className="text-2xl font-semibold tracking-tight">{t('logCodes')}</h1>
<input
value={kw}
onChange={(e) => setKw(e.target.value)}
placeholder="Search code/text"
className="w-72 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500"
/>
</div>
<div className="bg-zinc-950 border border-zinc-800 rounded-2xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-zinc-900/90 border-b border-zinc-800">
<tr className="text-zinc-400">
<th className="text-left p-3 font-medium w-40">Code</th>
<th className="text-left p-3 font-medium">Template</th>
</tr>
</thead>
<tbody>
{filtered.map((it) => (
<tr key={it.code} className="border-b border-zinc-900 hover:bg-zinc-900/40">
<td className="p-3 font-mono text-indigo-300">{it.code}</td>
<td className="p-3 text-zinc-200 break-all">{it.text}</td>
</tr>
))}
{filtered.length === 0 && (
<tr>
<td className="p-6 text-zinc-500" colSpan={2}>No codes</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
export default LogCodes;

View File

@@ -8,6 +8,7 @@ const Logs: React.FC = () => {
const { t } = useTranslation();
const { q } = useAppContext();
const [logs, setLogs] = useState<LogEntry[]>([]);
const [codeMap, setCodeMap] = useState<Record<number, string>>({});
const [isStreaming, setIsStreaming] = useState(true);
const [showRaw, setShowRaw] = useState(false);
const logEndRef = useRef<HTMLDivElement>(null);
@@ -22,7 +23,7 @@ const Logs: React.FC = () => {
setLogs(j.logs.map(normalizeLog));
}
} catch (e) {
console.error('load recent logs failed', e);
console.error('L0096', e);
}
};
@@ -59,11 +60,38 @@ const Logs: React.FC = () => {
}
} catch (e: any) {
if (e.name !== 'AbortError') {
console.error('Log stream error:', e);
console.error('L0097', e);
}
}
};
const loadCodeMap = async () => {
try {
const paths = [`/webui/log-codes.json${q}`, '/log-codes.json'];
for (const p of paths) {
const r = await fetch(p);
if (!r.ok) continue;
const j = await r.json();
if (Array.isArray(j?.items)) {
const m: Record<number, string> = {};
j.items.forEach((it: any) => {
if (typeof it?.code === 'number' && typeof it?.text === 'string') {
m[it.code] = it.text;
}
});
setCodeMap(m);
return;
}
}
} catch {
setCodeMap({});
}
};
useEffect(() => {
loadCodeMap();
}, [q]);
useEffect(() => {
loadRecent();
if (isStreaming) {
@@ -84,13 +112,29 @@ const Logs: React.FC = () => {
const clearLogs = () => setLogs([]);
const normalizeLog = (v: any): LogEntry => ({
time: typeof v?.time === 'string' && v.time ? v.time : new Date().toISOString(),
time: typeof v?.time === 'string' && v.time ? v.time : (typeof v?.timestamp === 'string' && v.timestamp ? v.timestamp : new Date().toISOString()),
level: typeof v?.level === 'string' && v.level ? v.level : 'INFO',
msg: typeof v?.msg === 'string' ? v.msg : JSON.stringify(v),
code: typeof v?.code === 'number' ? v.code : undefined,
msg: typeof v?.msg === 'string' ? v.msg : (typeof v?.message === 'string' ? v.message : JSON.stringify(v)),
__raw: JSON.stringify(v),
...v,
});
const toCode = (v: any): number | undefined => {
if (typeof v === 'number' && Number.isFinite(v) && v > 0) return v;
if (typeof v === 'string') {
if (/^L\d{4}$/.test(v)) return Number(v.slice(1));
const n = Number(v);
if (Number.isFinite(n) && n > 0) return n;
}
return undefined;
};
const decode = (v: any) => {
const c = toCode(v);
if (!c) return v;
return codeMap[c] || v;
};
const formatTime = (raw: string) => {
try {
if (!raw) return '--:--:--';
@@ -189,16 +233,19 @@ const Logs: React.FC = () => {
<tbody>
{logs.map((log, i) => {
const lvl = (log.level || 'INFO').toUpperCase();
const errText = (log as any).error || (lvl === 'ERROR' ? log.msg : '');
const message = lvl === 'ERROR' ? ((log as any).message || log.msg || '') : ((log as any).message || log.msg || '');
const rawCode = (log as any).code ?? (log as any).message ?? log.msg ?? '';
const message = String(decode(rawCode) || '');
const errRaw = (log as any).message || (log as any).error || (lvl === 'ERROR' ? rawCode : '');
const errText = String(decode(errRaw) || '');
const caller = (log as any).caller || (log as any).source || '';
const code = toCode(rawCode);
return (
<tr key={i} className="border-b border-zinc-900 hover:bg-zinc-900/40 align-top">
<td className="p-2 text-zinc-500 whitespace-nowrap">{formatTime(log.time)}</td>
<td className={`p-2 font-semibold whitespace-nowrap ${getLevelColor(lvl)}`}>{lvl}</td>
<td className="p-2 text-zinc-200 break-all">{message}</td>
<td className="p-2 text-red-300 break-all">{errText}</td>
<td className="p-2 text-zinc-500 break-all">{caller}</td>
<td className="p-2 text-zinc-500 break-all">{code ? `${code} | ${caller}` : caller}</td>
</tr>
);
})}