refactor(decnet_web/SwarmHosts): extract types + helpers with tests

This commit is contained in:
2026-05-09 06:05:19 -04:00
parent 31f4c54c32
commit 3a8519b2a1
3 changed files with 110 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest';
import {
AGENT_NAME_RE, bundleSecondsLeft, formatMmSs, shortFp,
} from './helpers';
describe('shortFp', () => {
it('truncates long fingerprints with an ellipsis', () => {
expect(shortFp('a'.repeat(64))).toBe('aaaaaaaaaaaaaaaa…');
});
it('returns em-dash for empty input', () => {
expect(shortFp('')).toBe('—');
});
});
describe('AGENT_NAME_RE', () => {
it('accepts lowercase / digits / dashes within 1..63 chars', () => {
expect(AGENT_NAME_RE.test('agent-1')).toBe(true);
expect(AGENT_NAME_RE.test('a')).toBe(true);
expect(AGENT_NAME_RE.test('1')).toBe(true);
});
it('rejects uppercase, leading dash, and oversized names', () => {
expect(AGENT_NAME_RE.test('Agent-1')).toBe(false);
expect(AGENT_NAME_RE.test('-leading')).toBe(false);
expect(AGENT_NAME_RE.test('a'.repeat(64))).toBe(false);
expect(AGENT_NAME_RE.test('')).toBe(false);
});
});
describe('bundleSecondsLeft', () => {
it('returns the floor of (expires - now) / 1000 in seconds', () => {
const now = new Date('2026-05-09T08:00:00Z').getTime();
const exp = new Date('2026-05-09T08:04:30Z').toISOString();
expect(bundleSecondsLeft(exp, now)).toBe(270);
});
it('clamps to zero when already expired', () => {
const now = new Date('2026-05-09T08:10:00Z').getTime();
expect(bundleSecondsLeft('2026-05-09T08:05:00Z', now)).toBe(0);
});
it('returns 0 for unparseable timestamps', () => {
expect(bundleSecondsLeft('not-a-date', Date.now())).toBe(0);
});
});
describe('formatMmSs', () => {
it('zero-pads minutes and seconds', () => {
expect(formatMmSs(75)).toEqual({ mm: '01', ss: '15' });
expect(formatMmSs(9)).toEqual({ mm: '00', ss: '09' });
expect(formatMmSs(0)).toEqual({ mm: '00', ss: '00' });
});
});

View File

@@ -0,0 +1,29 @@
import type { ApiError } from '../../utils/api';
export const AGENT_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
export const shortFp = (fp: string): string =>
(fp ? fp.slice(0, 16) + '…' : '—');
export function extractErrorDetail(err: unknown, fallback: string): string {
const e = err as ApiError;
if (e?.response?.data?.detail) return e.response.data.detail;
if (e?.response?.status === 403) return 'Insufficient permissions (admin only)';
if (e?.response?.status === 401) return 'Session expired — please log in again';
if (e?.message) return e.message;
return fallback;
}
/** Seconds remaining until the bundle expires, clamped to >= 0. */
export function bundleSecondsLeft(expiresAt: string, now: number): number {
const t = new Date(expiresAt).getTime();
if (Number.isNaN(t)) return 0;
return Math.max(0, Math.floor((t - now) / 1000));
}
export function formatMmSs(seconds: number): { mm: string; ss: string } {
return {
mm: Math.floor(seconds / 60).toString().padStart(2, '0'),
ss: (seconds % 60).toString().padStart(2, '0'),
};
}

View File

@@ -0,0 +1,27 @@
export interface SwarmHost {
uuid: string;
name: string;
address: string;
agent_port: number;
status: string;
last_heartbeat: string | null;
client_cert_fingerprint: string;
updater_cert_fingerprint: string | null;
enrolled_at: string;
notes: string | null;
}
export interface BundleResult {
token: string;
host_uuid: string;
command: string;
expires_at: string;
}
export interface BundleRequest {
master_host: string;
agent_name: string;
with_updater: boolean;
use_ipvlan: boolean;
services_ini: string | null;
}