import { req, setupHeaders, getActiveAccount, HttpError, setActiveAccount, clearActiveAccount, setUnauthorizedHandler, } from "./api/core"; // Re-export the shared HTTP machinery so consumers keep importing these from "./api" unchanged. export { HttpError, getActiveAccount, setActiveAccount, clearActiveAccount, setUnauthorizedHandler, }; // Domain modules split out of this file. Their TYPES are re-exported so consumers keep importing // them from "./api" unchanged; their method slices stay internal (`export type *`, not `export *`) // and reach consumers only through the `api` façade they're spread into below. Runtime value // exports a domain genuinely owns (e.g. plex's EMPTY_PLEX_FILTERS/plexFilterCount) are re-exported // explicitly. import { feedApi } from "./api/feed"; export type * from "./api/feed"; import { channelsApi } from "./api/channels"; export type * from "./api/channels"; import { syncApi } from "./api/sync"; export type * from "./api/sync"; import { playlistsApi } from "./api/playlists"; export type * from "./api/playlists"; import { tagsApi } from "./api/tags"; export type * from "./api/tags"; import { notificationsApi } from "./api/notifications"; export type * from "./api/notifications"; import { savedViewsApi } from "./api/savedViews"; export type * from "./api/savedViews"; import { quotaApi } from "./api/quota"; export type * from "./api/quota"; import { downloadsApi } from "./api/downloads"; export type * from "./api/downloads"; import { plexApi } from "./api/plex"; export type * from "./api/plex"; export { EMPTY_PLEX_FILTERS, plexFilterCount } from "./api/plex"; import { messagingApi } from "./api/messaging"; export type * from "./api/messaging"; // --------------------------------------------------------------------------- // The domains still living inline here — account, auth, setup, admin — are // extracted in R7.5 S2. Their types and methods stay together in this file // until then. // --------------------------------------------------------------------------- export interface Me { id: number; email: string; display_name: string | null; avatar_url: string | null; role: string; is_demo: boolean; has_google: boolean; has_password: boolean; google_enabled: boolean; plex_enabled: boolean; can_read: boolean; can_write: boolean; pending_invites: number; preferences: Record; } export interface DemoWhitelistEntry { id: number; email: string; note: string | null; added_by: string | null; created_at: string | null; } export interface Invite { id: number; email: string; status: "pending" | "approved" | "denied"; note: string | null; requested_at: string | null; decided_at: string | null; decided_by: string | null; } export interface VersionInfo { app_version: string; git_sha: string; build_date: string | null; db_revision: string | null; } export interface Account { id: number; email: string; display_name: string | null; avatar_url: string | null; active: boolean; } // Admin Configuration page: one DB-overridable setting (registry entry on the backend). export interface ConfigItem { key: string; type: "int" | "str" | "bool"; group: string; secret: boolean; min: number | null; max: number | null; is_set: boolean; // a DB override row exists (else using the env/config default) allow_empty?: boolean; // if true, a blank field stores "" (explicit empty) rather than resetting value?: string | number | boolean; // non-secret: effective value default?: string | number | boolean; // non-secret: env/config default default_is_set?: boolean; // secret: whether an env default exists } export interface SystemConfigData { groups: Record; secrets_manageable: boolean; } // Admin Users & roles page. export interface AdminUserRow { id: number; email: string; display_name: string | null; role: string; // "user" | "admin" is_active: boolean; is_suspended: boolean; email_verified: boolean; is_demo: boolean; has_password: boolean; has_google: boolean; created_at: string | null; } export interface AuditRow { id: number; created_at: string | null; actor: string | null; // email of the acting admin; null = System (scheduler/background) action: string; // canonical key, localized on the client via auditActions. target_type: string | null; target_id: string | null; summary: string | null; before: Record | null; after: Record | null; } export interface AuditListResult { items: AuditRow[]; total: number; // total rows in the log (may exceed `items` if capped) returned: number; } export interface SchedulerJob { id: string; interval_minutes: number; next_run: string | null; running: boolean; status: "ok" | "error" | "skipped" | null; last_started: string | null; last_finished: string | null; last_result: string | null; last_error: string | null; // Live progress while running (null when idle or for jobs that don't report it). // total is null for indeterminate progress (e.g. enrichment drains an unknown queue). progress: { current: number; total: number | null; phase: string | null } | null; } export interface SchedulerStatus { running_here: boolean; enabled: boolean; paused: boolean; jobs: SchedulerJob[]; queue: { channels_recent_pending: number; channels_deep_pending: number; deep_eta_seconds: number; videos_pending_enrich: number; videos_pending_shorts: number; videos_live_refresh: number; }; quota: { used_today: number; remaining_today: number; daily_budget: number; backfill_reserve: number; }; maintenance: { revalidate_batch: number; revalidate_batch_default: number; min: number; max: number; }; } export const api = { // --- account & session --- // Bootstrap probe: 200 always. Returns the user when signed in, or null when logged out // (the endpoint answers `{authenticated:false}` rather than 401, so the public landing never // logs a failed request to the console). React Query treats the null as data, not an error. me: async (): Promise => { // Always 200: `{ authenticated: false }` when logged out, else the full Me + `authenticated`. const r = await req<{ authenticated: boolean } & Partial>("/api/me"); return r.authenticated ? (r as Me) : null; }, accounts: (): Promise => req("/api/me/accounts"), deleteAccount: (): Promise<{ deleted: boolean }> => req("/api/me/account", { method: "DELETE" }), switchAccount: (userId: number): Promise<{ ok: boolean; user_id: number }> => req("/api/me/switch", { method: "POST", body: JSON.stringify({ user_id: userId }) }), // Signs the current tab's active account out of the browser wallet (per-tab aware via the // X-Siftlode-Account header that req() attaches). logout: (): Promise<{ ok: boolean; switched: boolean }> => req("/auth/logout", { method: "POST" }), version: (): Promise => req("/api/version"), // Overwriting preferences is idempotent, so let it retry on a transient gateway blip. savePrefs: (p: Record) => req("/api/me/preferences", { method: "PUT", body: JSON.stringify(p) }, { idempotent: true }), // --- admin: discovery / system configuration --- // Admin: reclaim all un-kept discovery content (search results + explored channels) now. purgeDiscovery: (): Promise> => req("/api/admin/purge-discovery", { method: "POST" }), adminConfig: (): Promise => req("/api/admin/config"), setConfig: (key: string, value: string | number | boolean): Promise => req(`/api/admin/config/${key}`, { method: "PATCH", body: JSON.stringify({ value }) }), resetConfig: (key: string): Promise => req(`/api/admin/config/${key}`, { method: "DELETE" }), testEmail: (): Promise<{ sent: boolean; to: string }> => req("/api/admin/config/test-email", { method: "POST" }), // --- admin: users & roles --- adminAudit: (limit = 500): Promise => req(`/api/admin/audit?limit=${limit}`), clearAudit: (): Promise<{ cleared: number }> => req("/api/admin/audit", { method: "DELETE" }), adminUsers: (): Promise => req("/api/admin/users"), setUserRole: (id: number, role: "user" | "admin"): Promise => req(`/api/admin/users/${id}/role`, { method: "PATCH", body: JSON.stringify({ role }) }), setUserSuspended: (id: number, suspended: boolean): Promise => req(`/api/admin/users/${id}/suspend`, { method: "PATCH", body: JSON.stringify({ suspended }) }), adminDeleteUser: (id: number): Promise<{ deleted: number }> => req(`/api/admin/users/${id}`, { method: "DELETE" }), schedulerStatus: (): Promise => req("/api/admin/scheduler"), updateSchedulerJob: ( jobId: string, intervalMinutes: number, ): Promise<{ id: string; interval_minutes: number }> => req(`/api/admin/scheduler/jobs/${jobId}`, { method: "PATCH", body: JSON.stringify({ interval_minutes: intervalMinutes }), }), runSchedulerJob: (jobId: string): Promise<{ id: string; started: boolean }> => req(`/api/admin/scheduler/jobs/${jobId}/run`, { method: "POST" }), runAllSchedulerJobs: (): Promise<{ started: string[] }> => req("/api/admin/scheduler/run-all", { method: "POST" }), updateMaintenanceBatch: (revalidateBatch: number): Promise<{ revalidate_batch: number }> => req("/api/admin/scheduler/maintenance", { method: "PATCH", body: JSON.stringify({ revalidate_batch: revalidateBatch }), }), // --- auth: public config --- // Public: which sign-in options this instance offers (e.g. hide Google when not configured). authConfig: (): Promise<{ google_enabled: boolean; allow_registration: boolean; operator_contact: string | null; }> => req("/auth/config"), // --- first-run install wizard (token from the setup URL printed to the container logs) --- setupStatus: (): Promise<{ configured: boolean }> => req("/api/setup/status"), setupInfo: (token: string): Promise<{ secrets_manageable: boolean }> => req("/api/setup/info", { headers: setupHeaders(token) }, { quiet: true }), setupAdmin: (token: string, email: string, password: string): Promise<{ ok: boolean }> => req( "/api/setup/admin", { method: "POST", headers: setupHeaders(token), body: JSON.stringify({ email, password }) }, { quiet: true }, ), setupConfig: (token: string, values: Record): Promise<{ saved: string[] }> => req( "/api/setup/config", { method: "POST", headers: setupHeaders(token), body: JSON.stringify(values) }, { quiet: true }, ), setupTestEmail: (token: string, to: string): Promise<{ ok: boolean }> => req( "/api/setup/test-email", { method: "POST", headers: setupHeaders(token), body: JSON.stringify({ to }) }, { quiet: true }, ), setupFinish: (token: string): Promise<{ ok: boolean }> => req("/api/setup/finish", { method: "POST", headers: setupHeaders(token) }, { quiet: true }), // --- auth: email + password (errors handled inline via quiet) --- register: (email: string, password: string): Promise<{ status: string }> => req( "/auth/register", { method: "POST", body: JSON.stringify({ email, password }) }, { quiet: true }, ), passwordLogin: (email: string, password: string): Promise<{ ok: boolean }> => req( "/auth/password-login", { method: "POST", body: JSON.stringify({ email, password }) }, { quiet: true }, ), requestPasswordReset: (email: string): Promise<{ status: string }> => req( "/auth/password-reset/request", { method: "POST", body: JSON.stringify({ email }) }, { quiet: true }, ), confirmPasswordReset: (token: string, password: string): Promise<{ ok: boolean }> => req( "/auth/password-reset/confirm", { method: "POST", body: JSON.stringify({ token, password }) }, { quiet: true }, ), // Confirm an email-verification token the SPA read from the URL fragment (SB3). verifyEmail: (token: string): Promise<{ ok: boolean }> => req("/auth/verify", { method: "POST", body: JSON.stringify({ token }) }, { quiet: true }), // Re-issue a verification link (the first one expired, or was lost). Uniform response — never // reveals whether the email has a pending account. resendVerification: (email: string): Promise<{ status: string }> => req( "/auth/verify/resend", { method: "POST", body: JSON.stringify({ email }) }, { quiet: true }, ), // SA4: sign every OTHER session for this account out (keeps the current one). logoutOthers: (): Promise<{ ok: boolean }> => req("/auth/logout-others", { method: "POST" }), // Set or change the signed-in account's password (errors shown inline via quiet). setPassword: (password: string, currentPassword?: string): Promise<{ ok: boolean }> => req( "/auth/set-password", { method: "POST", body: JSON.stringify({ password, current_password: currentPassword }) }, { quiet: true }, ), // --- onboarding / admin: access requests, demo --- requestAccess: (email: string): Promise<{ status: string }> => req("/auth/request-access", { method: "POST", body: JSON.stringify({ email }) }), // Hidden demo entry: a whitelisted email logs into the shared demo account, no Google // sign-in. Returns whether a session was established. demoLogin: (email: string): Promise<{ authenticated: boolean }> => req("/auth/demo", { method: "POST", body: JSON.stringify({ email }) }), adminDemoWhitelist: (): Promise => req("/api/admin/demo/whitelist"), addDemoWhitelist: (email: string): Promise => req("/api/admin/demo/whitelist", { method: "POST", body: JSON.stringify({ email }) }), removeDemoWhitelist: (id: number) => req(`/api/admin/demo/whitelist/${id}`, { method: "DELETE" }), resetDemo: (): Promise<{ reset: boolean; playlists_seeded: number }> => req("/api/admin/demo/reset", { method: "POST" }), adminInvites: (status?: string): Promise => req(`/api/admin/invites${status ? `?status=${status}` : ""}`), approveInvite: (id: number) => req(`/api/admin/invites/${id}/approve`, { method: "POST" }), denyInvite: (id: number) => req(`/api/admin/invites/${id}/deny`, { method: "POST" }), addInvite: (email: string) => req("/api/admin/invites", { method: "POST", body: JSON.stringify({ email }) }), // --- extracted domain slices (their methods reach consumers through this façade) --- ...feedApi, ...channelsApi, ...syncApi, ...playlistsApi, ...tagsApi, ...notificationsApi, ...savedViewsApi, ...quotaApi, ...plexApi, ...messagingApi, ...downloadsApi, };