feat: display platform agent status and version
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-08-06 16:01:33 +09:00
parent 4fce1c21a3
commit 4a79dab2f6
3 changed files with 35 additions and 1 deletions
+3
View File
@@ -11,6 +11,9 @@ export type PlatformDevice = {
os?: string | null; os?: string | null;
arch?: string | null; arch?: string | null;
distro?: string | null; distro?: string | null;
agentVersion?: string | null;
agentUpdatedAt?: string | null;
agentLastSeenAt?: string | null;
installedAt: string; installedAt: string;
lastProvisionedAt: string; lastProvisionedAt: string;
status: string; status: string;
@@ -38,7 +38,9 @@ type ImageTags = {
function releaseArtifacts(release?: AppRelease | null) { function releaseArtifacts(release?: AppRelease | null) {
const imageTags = release?.imageTags as ImageTags | null | undefined; const imageTags = release?.imageTags as ImageTags | null | undefined;
const entries = Object.entries(imageTags?.images ?? {}); const entries = Object.entries(imageTags?.images ?? {}).filter(([, image]) => (
!/^(postgres|mysql|mariadb|mongo)(:|@)/i.test(image.trim())
));
if (entries.length) return entries; if (entries.length) return entries;
if (release?.artifact) return [[release.app.name, release.artifact]]; if (release?.artifact) return [[release.app.name, release.artifact]];
return []; return [];
@@ -22,6 +22,8 @@ type HeadscaleNode = {
type AclRule = { id: string; action: string; sources: string[]; destinations: string[] }; type AclRule = { id: string; action: string; sources: string[]; destinations: string[] };
type PlatformRow = HeadscaleNode & { device?: PlatformDevice; controllers: string[]; no: number }; type PlatformRow = HeadscaleNode & { device?: PlatformDevice; controllers: string[]; no: number };
type DeviceKind = 'agent' | 'launcher' | 'unknown';
function normalize(value?: string | null) { function normalize(value?: string | null) {
return (value ?? '').toLowerCase().replace(/[^a-z0-9]/g, ''); return (value ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
} }
@@ -41,6 +43,17 @@ function findControllers(node: HeadscaleNode, rules: AclRule[]) {
.flatMap((rule) => rule.sources))]; .flatMap((rule) => rule.sources))];
} }
function getDeviceKind(row: PlatformRow): DeviceKind {
if (!row.device) return 'unknown';
return row.device.agentVersion || row.device.agentLastSeenAt ? 'agent' : 'launcher';
}
function formatDateTime(value?: string | null) {
if (!value) return '-';
const date = new Date(value);
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString('ko-KR', { hour12: false });
}
export function RuntimeDeviceControlPage() { export function RuntimeDeviceControlPage() {
const [rows, setRows] = useState<PlatformRow[]>([]); const [rows, setRows] = useState<PlatformRow[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -91,6 +104,19 @@ export function RuntimeDeviceControlPage() {
width: 180, width: 180,
cell: (row) => <span className="font-mono text-xs text-muted-foreground">{row.ip || '-'}</span>, cell: (row) => <span className="font-mono text-xs text-muted-foreground">{row.ip || '-'}</span>,
}, },
{
key: 'deviceKind',
header: '유형',
width: 150,
cell: (row) => {
const kind = getDeviceKind(row);
if (kind === 'agent') {
return <Badge tone="green">에이전트</Badge>;
}
if (kind === 'launcher') return <Badge tone="neutral">런처 클라이언트</Badge>;
return <Badge tone="neutral">미확인</Badge>;
},
},
{ {
key: 'controllers', key: 'controllers',
header: '제어 사용자', header: '제어 사용자',
@@ -155,6 +181,9 @@ export function RuntimeDeviceControlPage() {
<dt className="text-muted-foreground">Workspace</dt><dd className="min-w-0 break-all">{selected?.device?.workspaceRoot || '-'}</dd> <dt className="text-muted-foreground">Workspace</dt><dd className="min-w-0 break-all">{selected?.device?.workspaceRoot || '-'}</dd>
<dt className="text-muted-foreground">SSH 사용자</dt><dd>{selected?.device?.sshUser || '-'}</dd> <dt className="text-muted-foreground">SSH 사용자</dt><dd>{selected?.device?.sshUser || '-'}</dd>
<dt className="text-muted-foreground">사용 유형</dt><dd>{selected?.device?.usageType || '-'}</dd> <dt className="text-muted-foreground">사용 유형</dt><dd>{selected?.device?.usageType || '-'}</dd>
<dt className="text-muted-foreground">장비 유형</dt><dd>{selected ? ({ agent: '에이전트', launcher: '런처 클라이언트', unknown: '미확인' }[getDeviceKind(selected)]) : '-'}</dd>
<dt className="text-muted-foreground">에이전트 버전</dt><dd>{selected?.device?.agentVersion || '-'}</dd>
<dt className="text-muted-foreground">마지막 에이전트 확인</dt><dd>{formatDateTime(selected?.device?.agentLastSeenAt)}</dd>
</dl> </dl>
</Modal> </Modal>
</> </>