Files
hanarang-dashboard/frontend/app/admin/page.tsx

90 lines
5.1 KiB
TypeScript

'use client';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import StatusBadge from '@/components/common/StatusBadge';
import ConfirmModal from '@/components/admin/ConfirmModal';
import { API_URL } from '@/lib/config';
import { adminFetch } from '@/lib/adminFetch';
import { SISTER_DISPLAY_NAMES } from '@/lib/sisters';
import SisterAvatar from '@/components/common/SisterAvatar';
type Status = 'online' | 'offline' | 'working' | 'unknown';
interface ActionResult {
success: boolean;
output: string;
error: string | null;
}
interface SisterAdminItem {
id?: number;
name: string;
status: Status;
}
const Table = styled.div`display:flex;flex-direction:column;gap:8px;user-select:none;`;
const Row = styled.div`background:var(--bg-surface);border:1px solid var(--border-color);border-radius:10px;padding:16px 20px;display:flex;align-items:center;gap:16px;`;
const SisterInfo = styled.div`display:flex;align-items:center;gap:10px;flex:1;`;
const Name = styled.span`font-size:15px;font-weight:600;color:var(--text-primary);min-width:70px;`;
const Actions = styled.div`display:flex;gap:8px;`;
const ActionBtn = styled.button<{ $danger?: boolean }>`padding:6px 14px;border-radius:7px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid;transition:all .15s;user-select:none;${({ $danger }) => $danger ? `background: rgba(255,23,68,0.08); border-color: rgba(255,23,68,0.3); color: #FF1744; &:hover { background: rgba(255,23,68,0.18); }` : `background: rgba(88,166,255,0.08); border-color: rgba(88,166,255,0.3); color: #58A6FF; &:hover { background: rgba(88,166,255,0.18); }`}`;
const ResultBanner = styled.div<{ $success: boolean }>`margin-top:8px;padding:8px 12px;background:${({ $success }) => $success ? 'rgba(0,230,118,0.08)' : 'rgba(255,23,68,0.08)'};border:1px solid ${({ $success }) => $success ? 'rgba(0,230,118,0.3)' : 'rgba(255,23,68,0.3)'};border-radius:6px;font-size:12px;color:${({ $success }) => $success ? '#00FF00' : '#FF1744'};font-family:monospace;white-space:pre-wrap;`;
export default function AdminSistersPage() {
const [sisters, setSisters] = useState<SisterAdminItem[]>([]);
const [modal, setModal] = useState<{ action: 'restart' | 'reset'; name: string } | null>(null);
const [results, setResults] = useState<Record<string, ActionResult>>({});
const [loading, setLoading] = useState<Record<string, boolean>>({});
useEffect(() => {
fetch(`${API_URL}/api/sisters`).then((r) => r.json()).then(setSisters).catch(() => {});
const iv = setInterval(() => { fetch(`${API_URL}/api/sisters`).then((r) => r.json()).then(setSisters).catch(() => {}); }, 15000);
return () => clearInterval(iv);
}, []);
const doAction = async (action: 'restart' | 'reset', name: string) => {
setModal(null);
setLoading((p) => ({ ...p, [`${action}:${name}`]: true }));
try {
const res = await adminFetch(`/api/admin/sisters/${name}/${action}`, { method: 'POST' });
const data: ActionResult = await res.json();
setResults((p) => ({ ...p, [`${action}:${name}`]: data }));
} catch (e) {
setResults((p) => ({ ...p, [`${action}:${name}`]: { success: false, output: '', error: (e as Error).message } }));
} finally {
setLoading((p) => ({ ...p, [`${action}:${name}`]: false }));
}
};
return (
<>
<Table>
{sisters.map((s) => {
const restartResult = results[`restart:${s.name}`];
const resetResult = results[`reset:${s.name}`];
return (
<div key={s.id}>
<Row>
<SisterInfo>
<SisterAvatar name={s.name} size={28} />
<Name>{SISTER_DISPLAY_NAMES[s.name] ?? s.name}</Name>
<StatusBadge status={s.status as Status} />
</SisterInfo>
<Actions>
<ActionBtn onClick={() => setModal({ action: 'restart', name: s.name })} disabled={loading[`restart:${s.name}`]}>{loading[`restart:${s.name}`] ? '...' : '재시작'}</ActionBtn>
<ActionBtn $danger onClick={() => setModal({ action: 'reset', name: s.name })} disabled={loading[`reset:${s.name}`]}>{loading[`reset:${s.name}`] ? '...' : '리셋'}</ActionBtn>
</Actions>
</Row>
{restartResult && <ResultBanner $success={restartResult.success}>: {restartResult.success ? '✅ 성공' : `${restartResult.error}`}{restartResult.output && `\n${restartResult.output}`}</ResultBanner>}
{resetResult && <ResultBanner $success={resetResult.success}>: {resetResult.success ? '✅ 성공' : `${resetResult.error}`}</ResultBanner>}
</div>
);
})}
</Table>
{modal && <ConfirmModal title={modal.action === 'restart' ? '게이트웨이 재시작' : '세션 리셋'} message={`${SISTER_DISPLAY_NAMES[modal.name] ?? modal.name}${modal.action === 'restart' ? 'OpenClaw Gateway를 재시작' : '메인 세션을 초기화'}하시겠어요?`} confirmLabel={modal.action === 'restart' ? '재시작' : '리셋'} danger={modal.action === 'reset'} onConfirm={() => doAction(modal.action, modal.name)} onCancel={() => setModal(null)} />}
</>
);
}