import * as z from 'zod'; // Shared types and validation for /start (the owner's dashboard / new-tab page). export const linkInput = z.object({ title: z.string().trim().min(1, 'Give the link a title').max(80, 'Title is too long'), url: z .string() .trim() .transform((u) => (/^[a-z][a-z0-9+.-]*:/i.test(u) ? u : `https://${u}`)) .pipe( z .string() .max(2000) .regex(/^https?:\/\/[^\s]+$/i, 'Use an http(s) URL') ), section: z .string() .trim() .max(40) .transform((s) => s.toLowerCase() || 'links') .default('links') }); export type Container = { name: string; image: string; state: string | null; health: string | null; restarts: number; exitCode: number | null; startedAt: string | null; cpu: string | null; mem: string | null; }; export type TimerState = { label: string; unit: string; scope: 'user' | 'system'; last: number | null; next: number | null; running: boolean; result: string | null; exitStatus: number; }; export type ServiceCheck = { name: string; url: string; ok: boolean | null; code: number | null; ms: number | null; }; export type HostSnapshot = { hostname: string; cpus: number; cpuPct: number; load: number[]; uptimeS: number; mem: { total: number; used: number; available: number }; swap: { total: number; used: number }; disks: { mount: string; total: number; used: number; free: number; pct: number }[]; containers: Container[]; services: ServiceCheck[]; timers: TimerState[]; b2: Record | null; }; export type NtfyMessage = { id: string; topic: string; time: number; title: string; message: string; priority: number; tags: string[]; click: string | null; }; export type NtfySnapshot = { messages: NtfyMessage[]; topics: string[]; errors: string[] }; /** A timer's health: failed if the last run failed, late if it should have run by now. */ export function timerStatus(t: TimerState, nowS: number): 'ok' | 'fail' | 'late' | 'running' { if (t.running) return 'running'; if (t.result && t.result !== 'success') return 'fail'; if (t.next && nowS - t.next > 15 * 60) return 'late'; return 'ok'; } export function bytes(n: number): string { const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return `${n >= 100 || i === 0 ? Math.round(n) : n.toFixed(1)} ${u[i]}`; } /** Compact relative time: "now", "5m", "3h", "2d" (past) or "in 5m" (future). */ export function ago(ms: number, now: number): string { const d = Math.round((now - ms) / 1000); const a = Math.abs(d); const s = a < 45 ? 'now' : a < 3600 ? `${Math.round(a / 60)}m` : a < 86400 * 2 ? `${Math.round(a / 3600)}h` : `${Math.round(a / 86400)}d`; if (s === 'now') return s; return d < 0 ? `in ${s}` : `${s} ago`; } export function duration(seconds: number): string { const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); return d ? `${d}d ${h}h` : `${h}h ${Math.floor((seconds % 3600) / 60)}m`; }