import type { DashboardPermissions, DashboardRole, DashboardUser, StoredDashboardUser, } from "@/lib/permissions"; export type { DashboardUser, DashboardPermissions, StoredDashboardUser, DashboardRole }; const REQUEST_TIMEOUT_MS = 25_000; async function request( path: string, options: RequestInit = {}, ): Promise { const headers = new Headers(options.headers); if (!headers.has("Content-Type") && options.body) { headers.set("Content-Type", "application/json"); } const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); let res: Response; try { res = await fetch(path, { ...options, headers, credentials: "include", signal: controller.signal, }); } catch (err) { if (err instanceof Error && err.name === "AbortError") { throw new Error( "Request timed out. The API may still be starting or the bot is restarting — try again.", ); } throw new Error( err instanceof Error ? err.message : "Could not reach the API. Is the bot process running?", ); } finally { window.clearTimeout(timeout); } const body = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(body.error || `Request failed (${res.status})`); } return body as T; } export type LogType = "Output" | "Warn" | "Error"; export interface LogEntry { type: LogType; timestamp: string | null; message: string; } export interface LogsPayload { entries: LogEntry[]; cursor: Record; } export interface BotRestartResult { success: boolean; error?: string; commands?: number; } export interface ApiResponse { success: boolean; data?: T; error?: string; message?: string; restartRequired?: boolean; reloaded?: string[]; botRestart?: BotRestartResult; } export interface CatalogAddon { id: string; name: string; description: string; repositoryUrl: string; jsFiles: string[]; configFiles: string[]; fileCount: number; installed?: boolean; } export interface UpdateCheckResult { current: string; latest: string; updateAvailable: boolean; repository: string; branch: string; zipUrl: string; } export type NotificationType = "bot" | "update" | "info" | "error"; export interface DashboardNotification { id: string; type: NotificationType; title: string; message: string; href: string | null; createdAt: number; read: boolean; } export interface NotificationsPayload { items: DashboardNotification[]; unreadCount: number; } export type ModuleSetupStatus = "todo" | "progress" | "done"; export interface ModuleStatusInfo { status: ModuleSetupStatus; subtitle: string; passed?: number; total?: number; } export interface GuildSummary { id: string; name: string; memberCount: number | null; icon: string | null; } export type GuildHealthStatus = | "ok" | "missing_config" | "not_in_guild" | "extra_guilds" | "offline"; export interface GuildHealth { status: GuildHealthStatus; configuredGuildId: string | null; message: string | null; configuredGuild: GuildSummary | null; extraGuilds: GuildSummary[]; } export type DashboardAlertSeverity = "error" | "warning" | "info"; export interface DashboardAlertPayload { id: string; severity: DashboardAlertSeverity; category: string; title: string; message: string; action?: { label: string; href: string }; meta?: Record; } export interface GuildEmojiResource { id: string; name: string; animated: boolean; url: string | null; } export interface GuildResources { guildId: string; guildName: string; roles: { id: string; name: string; color: string | null }[]; channels: { id: string; name: string; type: number }[]; categories: { id: string; name: string }[]; emojis: GuildEmojiResource[]; } export interface BotStats { bot: { username: string; id: string; avatar: string; inviteUrl?: string; version: string; ping: number; uptime: number; }; servers: number; users: number; tickets: { total: number; open: number; closed: number }; modules?: Record; hosting: { ram_used: string; ram_total: string; ram_percent: number; cpu_load: string; uptime: string; }; } export function maskConfigSecrets(content: string, filename: string): string { if (filename !== "supportbot") return content; return content.replace( /^(\s*Token:\s*)["'][^"']*["']/m, '$1"BOT_TOKEN"', ); } export const api = { me: () => request>("/api/auth/me"), logout: () => request>("/api/auth/logout", { method: "POST" }), health: () => request< ApiResponse<{ dashboard: boolean; botReady: boolean; oauthEnabled?: boolean; }> >("/api/health"), stats: () => request>("/api/stats"), getAlerts: () => request>("/api/alerts"), getGuildStatus: () => request>("/api/guild/status"), getGuildResources: (refresh = false) => request>( `/api/guild/resources${refresh ? "?refresh=1" : ""}`, ), leaveGuild: (guildId: string) => request>(`/api/guild/${guildId}/leave`, { method: "POST", }), listConfigs: () => [ "supportbot", "ticket-panel", "commands", "messages", "supportbot-ai", ] as const, getConfigRaw: (file: string) => request>(`/api/configs/${file}`), saveConfigRaw: (filename: string, content: string) => request>("/api/configs/raw", { method: "PUT", body: JSON.stringify({ filename, content }), }), getConfigJson: (file: string) => request>>(`/api/configs/json/${file}`), updateConfigFields: (updates: Record) => request>("/api/configs/update-fields/multi", { method: "POST", body: JSON.stringify(updates), }), checkForUpdates: () => request>("/api/system/update-check"), runUpdate: (version: string) => request>("/api/system/update", { method: "POST", body: JSON.stringify({ version }), }), reloadConfigs: (file?: string) => request>("/api/system/reload-configs", { method: "POST", body: JSON.stringify(file ? { file } : {}), }), getLogsMeta: () => request< ApiResponse<{ types: LogType[]; files: { type: LogType; date: string; file: string | null; size: number }[]; }> >("/api/system/logs/meta"), getLogs: (params: { types?: LogType[]; cursor?: Record; tail?: number; }) => { const q = new URLSearchParams(); if (params.types?.length) q.set("types", params.types.join(",")); if (params.cursor && Object.keys(params.cursor).length > 0) { q.set("cursor", JSON.stringify(params.cursor)); } if (params.tail != null) q.set("tail", String(params.tail)); const qs = q.toString(); return request>( `/api/system/logs${qs ? `?${qs}` : ""}`, ); }, listTranscripts: () => request< ApiResponse< { id: string; filename: string; createdAt: string; size?: number; ticketName?: string | null; }[] > >("/api/system/transcripts"), getTranscript: (id: string) => request>(`/api/system/transcripts/${id}`), getTranscriptSettings: () => request< ApiResponse<{ autoDelete: { enabled: boolean; afterDays: number }; publicAccess: { enabled: boolean }; publicBasePath?: string; }> >("/api/system/transcript-settings"), updateTranscriptSettings: (body: { autoDelete?: { enabled?: boolean; afterDays?: number }; publicAccess?: { enabled?: boolean }; }) => request< ApiResponse<{ autoDelete: { enabled: boolean; afterDays: number }; publicAccess: { enabled: boolean }; publicBasePath?: string; }> >("/api/system/transcript-settings", { method: "PUT", body: JSON.stringify(body), }), deleteTranscript: (id: string) => request>(`/api/system/transcripts/${encodeURIComponent(id)}`, { method: "DELETE", }), purgeTranscripts: () => request< ApiResponse<{ deleted: number; skipped: boolean }> >("/api/system/transcripts/purge", { method: "POST" }), getBranding: () => request< ApiResponse<{ title: string; pageTitle: string; hasFavicon: boolean; faviconUrl: string | null; updatedAt: number | null; }> >("/api/branding"), updateBranding: (body: { title?: string; pageTitle?: string }) => request< ApiResponse<{ title: string; pageTitle: string; hasFavicon: boolean; faviconUrl: string | null; updatedAt: number | null; }> >("/api/branding", { method: "PUT", body: JSON.stringify(body), }), uploadBrandingFavicon: (dataUrl: string) => request< ApiResponse<{ title: string; pageTitle: string; hasFavicon: boolean; faviconUrl: string | null; updatedAt: number | null; }> >("/api/branding/favicon", { method: "POST", body: JSON.stringify({ dataUrl }), }), removeBrandingFavicon: () => request< ApiResponse<{ title: string; pageTitle: string; hasFavicon: boolean; faviconUrl: string | null; updatedAt: number | null; }> >("/api/branding/favicon", { method: "DELETE" }), resetBranding: () => request< ApiResponse<{ title: string; pageTitle: string; hasFavicon: boolean; faviconUrl: string | null; updatedAt: number | null; }> >("/api/branding/reset", { method: "POST" }), getTranscriptTemplate: () => request>>( "/api/system/transcript-template", ), saveTranscriptTemplate: (data: Record) => request>>( "/api/system/transcript-template", { method: "PUT", body: JSON.stringify(data), }, ), previewTranscriptTemplate: async ( data: Record, ): Promise => { const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { const res = await fetch("/api/system/transcript-template/preview", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(data), signal: controller.signal, }); if (!res.ok) { throw new Error(`Preview failed (${res.status})`); } return await res.text(); } finally { window.clearTimeout(timeout); } }, getNotifications: () => request>("/api/notifications"), markNotificationsRead: (ids: string[] | "all") => request>("/api/notifications/read", { method: "POST", body: JSON.stringify({ ids }), }), dismissNotification: (id: string) => request>(`/api/notifications/${id}`, { method: "DELETE", }), listDashboardUsers: () => request< ApiResponse<{ users: StoredDashboardUser[]; roles: DashboardRole[]; configFiles: string[]; }> >("/api/dashboard-users"), addDashboardUser: (body: { id: string; role: DashboardRole; username?: string; globalName?: string; permissions?: DashboardPermissions; enabled?: boolean; }) => request>("/api/dashboard-users", { method: "POST", body: JSON.stringify(body), }), updateDashboardUser: ( id: string, body: { role?: DashboardRole; enabled?: boolean; username?: string; globalName?: string; permissions?: DashboardPermissions; }, ) => request>(`/api/dashboard-users/${id}`, { method: "PATCH", body: JSON.stringify(body), }), removeDashboardUser: (id: string) => request>(`/api/dashboard-users/${id}`, { method: "DELETE", }), getAddonsCatalog: () => request< ApiResponse<{ repository: string; addons: CatalogAddon[]; }> >("/api/addons/catalog"), getAddonsInstalled: () => request< ApiResponse<{ installed: { id: string; name: string; jsFiles: string[]; configFiles: string[] }[]; configs: string[]; }> >("/api/addons/installed"), listAddonConfigs: () => request>("/api/addons/configs"), installAddon: (name: string) => request>("/api/addons/install", { method: "POST", body: JSON.stringify({ name }), }), getAddonConfigJson: (file: string) => request>>( `/api/addons/configs/json/${encodeURIComponent(file)}`, ), saveAddonConfig: (filename: string, data: Record) => request>("/api/addons/configs", { method: "PUT", body: JSON.stringify({ filename, data }), }), getSetupStatus: () => request>("/api/setup/status"), validateSetup: (body: Record) => request>( "/api/setup/validate", { method: "POST", body: JSON.stringify(body) }, ), completeSetup: (body: Record) => request< ApiResponse<{ setupComplete: boolean; botRestart?: BotRestartResult }> >("/api/setup/complete", { method: "POST", body: JSON.stringify(body), }), }; export function displayDiscordName( user: Pick, ): string { return user.globalName || user.username; } export function formatUptime(seconds: number): string { const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); const m = Math.floor((seconds % 3600) / 60); if (d > 0) return `${d}d ${h}h`; if (h > 0) return `${h}h ${m}m`; return `${m}m`; }