feat(agent-office/LogTab): source 뱃지 + access 메타데이터 표시 + 5초 폴링

This commit is contained in:
2026-05-28 02:48:56 +09:00
parent d29fdac4a0
commit 86f020182a
2 changed files with 49 additions and 9 deletions

View File

@@ -371,6 +371,18 @@
.ao-log-level { min-width: 48px; font-weight: bold; } .ao-log-level { min-width: 48px; font-weight: bold; }
.ao-log-msg { color: #ccc; word-break: break-all; } .ao-log-msg { color: #ccc; word-break: break-all; }
.ao-log-source {
margin-left: 6px;
font-size: 0.75em;
font-weight: 600;
letter-spacing: 0.5px;
}
.ao-log-meta {
color: #6b7280;
font-size: 0.85em;
}
/* ===== Common ===== */ /* ===== Common ===== */
.ao-empty { .ao-empty {
color: #94a3b8; color: #94a3b8;

View File

@@ -5,19 +5,37 @@ import { getAgentLogs } from '../../../api';
const LEVEL_STYLE = { const LEVEL_STYLE = {
info: { color: '#60a5fa' }, info: { color: '#60a5fa' },
warning: { color: '#fbbf24' }, warning: { color: '#fbbf24' },
error: { color: '#ef4444' } error: { color: '#ef4444' },
}; };
const SOURCE_STYLE = {
agent: { color: '#9ca3af', label: 'AGENT' },
access: { color: '#5eead4', label: 'ACCESS' },
log: { color: '#a78bfa', label: 'LOG' },
};
function formatTime(iso) {
if (!iso) return '';
return new Date(iso).toLocaleTimeString('ko-KR', {
hour: '2-digit', minute: '2-digit', second: '2-digit',
});
}
export default function LogTab({ agentId, refreshTrigger }) { export default function LogTab({ agentId, refreshTrigger }) {
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const scrollRef = useRef(null); const scrollRef = useRef(null);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
getAgentLogs(agentId, 50).then(data => { const fetchLogs = () => {
if (!cancelled) setLogs(Array.isArray(data) ? data : (data?.logs || [])); getAgentLogs(agentId, 100).then(data => {
}); if (cancelled) return;
return () => { cancelled = true; }; setLogs(Array.isArray(data) ? data : (data?.logs || []));
}).catch(() => {});
};
fetchLogs();
const interval = setInterval(fetchLogs, 5000); // 5초 폴링
return () => { cancelled = true; clearInterval(interval); };
}, [agentId, refreshTrigger]); }, [agentId, refreshTrigger]);
useEffect(() => { useEffect(() => {
@@ -30,13 +48,23 @@ export default function LogTab({ agentId, refreshTrigger }) {
<div className="ao-log-tab" ref={scrollRef}> <div className="ao-log-tab" ref={scrollRef}>
{logs.length === 0 && <div className="ao-empty">No logs yet</div>} {logs.length === 0 && <div className="ao-empty">No logs yet</div>}
{logs.map((log, i) => { {logs.map((log, i) => {
const style = LEVEL_STYLE[log.level] || LEVEL_STYLE.info; const source = log.source || 'agent';
const time = new Date(log.created_at).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const sourceMeta = SOURCE_STYLE[source] || SOURCE_STYLE.agent;
const levelStyle = LEVEL_STYLE[log.level] || LEVEL_STYLE.info;
const time = formatTime(log.ts || log.created_at);
return ( return (
<div key={log.id || i} className="ao-log-item"> <div key={log.id || `${source}-${i}-${time}`} className="ao-log-item">
<span className="ao-log-time">{time}</span> <span className="ao-log-time">{time}</span>
<span className="ao-log-level" style={style}>[{log.level}]</span> <span className="ao-log-source" style={{ color: sourceMeta.color }}>
[{sourceMeta.label}]
</span>
<span className="ao-log-level" style={levelStyle}>[{log.level}]</span>
<span className="ao-log-msg">{log.message}</span> <span className="ao-log-msg">{log.message}</span>
{source === 'access' && (
<span className="ao-log-meta">
{' '}({log.status} · {log.ms}ms)
</span>
)}
</div> </div>
); );
})} })}