// ─── Shared HTTP client — all API endpoints in one file ────────────────────── import type { MessageRecord, PageResult } from "@bete/shared"; const BE_API_URL = import.meta.env.VITE_BE_API_URL || "http://localhost:3001"; const BE_WS_URL = import.meta.env.VITE_BE_WS_URL || "ws://localhost:3001"; class ApiError extends Error { code: string; statusCode: number; constructor(code: string, message: string, statusCode: number) { super(message); this.name = "ApiError"; this.code = code; this.statusCode = statusCode; } } export async function request(path: string, init?: RequestInit): Promise { const password = localStorage.getItem("admin-password"); const url = path.startsWith("http") ? path : `${BE_API_URL}${path}`; const res = await fetch(url, { headers: { "Content-Type": "application/json", ...(password ? { "X-Admin-Password": password } : {}), }, ...init, }); if (!res.ok) { let message = res.statusText; let code = "REQUEST_FAILED"; try { const body = (await res.json()) as { error?: string; message?: string }; if (body.message) message = body.message; if (body.error) code = body.error; } catch { // ignore parse errors } throw new ApiError(code, message, res.status); } return res.json() as Promise; } export function getWebSocketURL(): string { return BE_WS_URL; } export function getAPIURL(): string { return BE_API_URL; } // ─── Types ─────────────────────────────────────────────────────────────────── export type { MessageRecord, PageResult }; export interface Guild { id: string; name: string; icon: string | null; } export interface Channel { id: string; name: string; type?: string; parentId?: string | null; } export interface VoiceStatus { connected: boolean; activeGuildId: string | null; activeChannelId: string | null; activeChannelName: string | null; } export interface ActiveSpeaker { id?: string; userId?: string; username: string; avatar: string; speaking: boolean; } export type MediaMode = "music" | "screen"; export interface MediaItem { id?: string; source: string; title: string; mode?: "music" | "screen"; durationMs?: number | null; thumbnailUrl?: string | null; } export interface MediaState { playing: boolean; musicVolume: number; current: MediaItem | null; queue: MediaItem[]; } export interface UIState { selectedGuild?: string; selectedVoiceGuild?: string; selectedVoiceChannel?: string; selectedTextGuild?: string; selectedTextChannel?: string; selectedAnalyticsGuild?: string; selectedAnalyticsChannel?: string; activeTab?: "live" | "messages"; isListening?: boolean; isStreaming?: boolean; } export interface AppConfig { monitorGuildId: string | null; } export interface ChatResponse { response?: string; } export type DashboardTab = "live" | "messages"; // ─── Messages ──────────────────────────────────────────────────────────────── export function listMessages(params: { guildId: string; channelId?: string; limit?: number; cursor?: string; }): Promise> { const sp = new URLSearchParams({ guildId: params.guildId, limit: String(params.limit ?? 100), ...(params.channelId && { channelId: params.channelId }), ...(params.cursor && { cursor: params.cursor }), }); return request>(`/api/messages?${sp}`); } export function listReview( params: URLSearchParams, ): Promise> { return request>(`/api/review?${params}`); } export function reanalyzeMessage(id: string): Promise { return request(`/api/messages/${id}/reanalyze`, { method: "POST" }); } export function reanalyzeErrorBatch(opts: { guildId?: string; channelId?: string; messageIds?: string[]; }): Promise<{ ok: boolean; count: number }> { return request<{ ok: boolean; count: number }>( "/api/messages/reanalyze-batch", { method: "POST", body: JSON.stringify(opts) }, ); } // ─── Guilds / Config ───────────────────────────────────────────────────────── export function getGuilds(): Promise { return request("/api/guilds"); } export function getAppConfig(): Promise { return request("/api/config"); } // ─── Voice ─────────────────────────────────────────────────────────────────── export function getVoiceChannels(guildId: string): Promise { return request(`/api/guilds/${guildId}/voice-channels`); } export function getTextChannels(guildId: string): Promise { return request(`/api/guilds/${guildId}/channels`); } export function getVoiceStatus(): Promise { return request("/api/status"); } export function connectVoice( guildId: string, channelId: string, ): Promise { return request("/api/connect", { method: "POST", body: JSON.stringify({ guildId, channelId }), }); } export function disconnectVoice(): Promise { return request("/api/disconnect", { method: "POST" }); } // ─── Media ─────────────────────────────────────────────────────────────────── export function getMediaStatus(): Promise { return request("/api/media/status"); } export function queueMedia( source: string, mode: "music" | "screen", ): Promise { return request("/api/media/queue", { method: "POST", body: JSON.stringify({ source, mode }), }); } export function skipMedia(): Promise { return request("/api/media/skip", { method: "POST" }); } export function stopMedia(): Promise { return request("/api/media/stop", { method: "POST" }); } export function setMediaVolume(volume: number): Promise { return request("/api/media/volume", { method: "POST", body: JSON.stringify({ volume }), }); } // ─── Recordings ────────────────────────────────────────────────────────────── export interface VoiceRecording { id: string; user_id: string; username: string; avatar_url: string | null; guild_id: string | null; channel_id: string | null; channel_name: string | null; filename: string; size_bytes: number; download_url: string | null; upload_status: "pending" | "uploaded" | "failed"; upload_error: string | null; created_at: number; uploaded_at: number | null; } export function listRecordings(limit = 50): Promise { return request(`/api/recordings?limit=${limit}`); } // ─── Auth ──────────────────────────────────────────────────────────────────── export function login(password: string): Promise<{ ok: boolean }> { return request<{ ok: boolean }>("/api/auth/login", { method: "POST", body: JSON.stringify({ password }), }); } // ─── UI State ──────────────────────────────────────────────────────────────── export function getUIState(): Promise { return request("/api/ui-state"); } export function updateUIState(patch: Partial): Promise { return request("/api/ui-state", { method: "POST", body: JSON.stringify(patch), }); }