// BASE is the Next.js rewrite prefix defined in next.config.ts: // /api/backend/:path* → http://backend:8000/:path* // All fetch paths below start with /api/... giving the full path /api/backend/api/... // This double "api" is intentional: /api/backend is the proxy mount point, /api/... is the FastAPI route. const BASE = process.env.NEXT_PUBLIC_API_URL ?? "/api/backend" async function apiFetch(path: string, init?: RequestInit): Promise { const res = await fetch(`${BASE}${path}`, { cache: "no-store", ...init }) if (!res.ok) throw new Error(`API ${path} → ${res.status}`) if (res.status === 204) return undefined as T return res.json() } // ── Types ──────────────────────────────────────────────────────── export type KpiData = { total_power_kw: number pue: number avg_temperature: number active_alarms: number } export type PowerBucket = { bucket: string total_kw: number } export type TempBucket = { bucket: string room_id: string avg_temp: number } export type Alarm = { id: number sensor_id: string | null site_id: string room_id: string | null rack_id: string | null severity: "critical" | "warning" | "info" message: string state: string triggered_at: string } export type RoomStatus = { room_id: string avg_temp: number total_kw: number alarm_count: number status: "ok" | "warning" | "critical" } // ── Fetchers ───────────────────────────────────────────────────── export const fetchKpis = (siteId: string) => apiFetch(`/api/readings/kpis?site_id=${siteId}`) export const fetchPowerHistory = (siteId: string, hours = 1) => apiFetch(`/api/readings/site-power-history?site_id=${siteId}&hours=${hours}`) export const fetchTempHistory = (siteId: string, hours = 1) => apiFetch(`/api/readings/room-temp-history?site_id=${siteId}&hours=${hours}`) export const fetchAlarms = (siteId: string, state = "active", limit = 100) => apiFetch(`/api/alarms?site_id=${siteId}&state=${state}&limit=${limit}`) export const fetchRoomStatus = (siteId: string) => apiFetch(`/api/readings/room-status?site_id=${siteId}`) export type AlarmStats = { active: number acknowledged: number resolved: number critical: number warning: number } export const fetchAlarmStats = (siteId: string) => apiFetch(`/api/alarms/stats?site_id=${siteId}`) export const acknowledgeAlarm = async (alarmId: number): Promise => { const res = await fetch(`${BASE}/api/alarms/${alarmId}/acknowledge`, { method: "POST" }) if (!res.ok) throw new Error(`Acknowledge failed: ${res.status}`) } export const resolveAlarm = async (alarmId: number): Promise => { const res = await fetch(`${BASE}/api/alarms/${alarmId}/resolve`, { method: "POST" }) if (!res.ok) throw new Error(`Resolve failed: ${res.status}`) } export type RackAsset = { rack_id: string temp: number | null power_kw: number | null status: "ok" | "warning" | "critical" | "unknown" alarm_count: number } export type CracAsset = { crac_id: string state: "online" | "fault" | "unknown" supply_temp: number | null return_temp: number | null fan_pct: number | null } export type UpsAsset = { ups_id: string state: "online" | "battery" | "overload" | "unknown" charge_pct: number | null load_pct: number | null runtime_min: number | null voltage_v: number | null } export type UpsHistoryPoint = { bucket: string charge_pct: number | null load_pct: number | null runtime_min: number | null voltage_v: number | null } export type AssetsData = { site_id: string rooms: { room_id: string; crac: CracAsset; racks: RackAsset[] }[] ups_units: UpsAsset[] } export const fetchAssets = (siteId: string) => apiFetch(`/api/assets?site_id=${siteId}`) export type RackPower = { rack_id: string; power_kw: number | null } export type RoomPowerBreakdown = { room_id: string; racks: RackPower[] } export type PowerHistoryBucket = { bucket: string; room_id: string; total_kw: number } export const fetchRackBreakdown = (siteId: string) => apiFetch(`/api/power/rack-breakdown?site_id=${siteId}`) export const fetchRoomPowerHistory = (siteId: string, hours = 6) => apiFetch(`/api/power/room-history?site_id=${siteId}&hours=${hours}`) export const fetchUpsStatus = (siteId: string) => apiFetch(`/api/power/ups?site_id=${siteId}`) export const fetchUpsHistory = (siteId: string, upsId: string, hours = 6) => apiFetch(`/api/power/ups/history?site_id=${siteId}&ups_id=${upsId}&hours=${hours}`) export type RackEnvReading = { rack_id: string; temperature: number | null; humidity: number | null } export type RoomEnvReadings = { room_id: string; racks: RackEnvReading[] } export type HumidityBucket = { bucket: string; room_id: string; avg_humidity: number } export type CracStatus = { crac_id: string room_id: string | null state: "online" | "fault" delta: number | null rated_capacity_kw: number // Thermal supply_temp: number | null return_temp: number | null supply_humidity: number | null return_humidity: number | null airflow_cfm: number | null filter_dp_pa: number | null // Capacity cooling_capacity_kw: number | null cooling_capacity_pct: number | null cop: number | null sensible_heat_ratio: number | null // Compressor compressor_state: number | null compressor_load_pct: number | null compressor_power_kw: number | null compressor_run_hours: number | null high_pressure_bar: number | null low_pressure_bar: number | null discharge_superheat_c: number | null liquid_subcooling_c: number | null // Fan fan_pct: number | null fan_rpm: number | null fan_power_kw: number | null fan_run_hours: number | null // Electrical total_unit_power_kw: number | null input_voltage_v: number | null input_current_a: number | null power_factor: number | null } export type CracHistoryPoint = { bucket: string supply_temp: number | null return_temp: number | null delta_t: number | null capacity_kw: number | null capacity_pct: number | null cop: number | null comp_load: number | null filter_dp: number | null fan_pct: number | null } export const fetchRackEnvReadings = (siteId: string) => apiFetch(`/api/env/rack-readings?site_id=${siteId}`) export const fetchHumidityHistory = (siteId: string, hours = 6) => apiFetch(`/api/env/humidity-history?site_id=${siteId}&hours=${hours}`) export const fetchCracStatus = (siteId: string) => apiFetch(`/api/env/crac-status?site_id=${siteId}`) export type RackHistoryPoint = { bucket: string; temperature?: number; humidity?: number; power_kw?: number } export type RackHistory = { rack_id: string; site_id: string; history: RackHistoryPoint[]; alarms: Alarm[] } export const fetchRackHistory = (siteId: string, rackId: string, hours = 6) => apiFetch(`/api/env/rack-history?site_id=${siteId}&rack_id=${rackId}&hours=${hours}`) export type CracDeltaPoint = { bucket: string; delta: number } export const fetchCracDeltaHistory = (siteId: string, cracId: string, hours = 1) => apiFetch(`/api/env/crac-delta-history?site_id=${siteId}&crac_id=${cracId}&hours=${hours}`) export const fetchCracHistory = (siteId: string, cracId: string, hours = 6) => apiFetch(`/api/env/crac-history?site_id=${siteId}&crac_id=${cracId}&hours=${hours}`) export type ReportSummary = { site_id: string generated_at: string kpis: { total_power_kw: number; avg_temperature: number } alarm_stats: { active: number; acknowledged: number; resolved: number; critical: number; warning: number } crac_uptime: { crac_id: string; room_id: string; uptime_pct: number }[] ups_uptime: { ups_id: string; uptime_pct: number }[] } export const fetchReportSummary = (siteId: string) => apiFetch(`/api/reports/summary?site_id=${siteId}`) export const reportExportUrl = (type: "power" | "temperature" | "alarms", siteId: string, hours = 24) => type === "alarms" ? `${BASE}/api/reports/export/alarms?site_id=${siteId}` : `${BASE}/api/reports/export/${type}?site_id=${siteId}&hours=${hours}` export type RackCapacity = { rack_id: string room_id: string power_kw: number | null power_capacity_kw: number power_pct: number | null temp: number | null } export type RoomCapacity = { room_id: string power: { used_kw: number; capacity_kw: number; pct: number; headroom_kw: number } cooling: { load_kw: number; capacity_kw: number; pct: number; headroom_kw: number } space: { racks_total: number; racks_populated: number; pct: number } } export type CapacitySummary = { site_id: string config: { rack_power_kw: number; room_power_kw: number; crac_cooling_kw: number; rack_u_total: number } rooms: RoomCapacity[] racks: RackCapacity[] } export const fetchCapacitySummary = (siteId: string) => apiFetch(`/api/capacity/summary?site_id=${siteId}`) export type Device = { device_id: string name: string type: "server" | "switch" | "patch_panel" | "pdu" | "storage" | "firewall" | "kvm" rack_id: string room_id: string site_id: string u_start: number u_height: number ip: string serial: string model: string status: "online" | "offline" | "unknown" power_draw_w: number } export const fetchAllDevices = (siteId: string) => apiFetch(`/api/assets/devices?site_id=${siteId}`) export const fetchRackDevices = (siteId: string, rackId: string) => apiFetch(`/api/assets/rack-devices?site_id=${siteId}&rack_id=${rackId}`) // ── Generator ───────────────────────────────────────────────────── export type GeneratorStatus = { gen_id: string state: "standby" | "running" | "test" | "fault" | "unknown" fuel_pct: number | null fuel_litres: number | null fuel_rate_lph: number | null load_kw: number | null load_pct: number | null run_hours: number | null voltage_v: number | null frequency_hz: number | null engine_rpm: number | null oil_pressure_bar: number | null coolant_temp_c: number | null exhaust_temp_c: number | null alternator_temp_c: number | null power_factor: number | null battery_v: number | null } export type GeneratorHistoryPoint = { bucket: string load_pct: number | null fuel_pct: number | null coolant_temp_c: number | null exhaust_temp_c: number | null frequency_hz: number | null alternator_temp_c: number | null } export const fetchGeneratorStatus = (siteId: string) => apiFetch(`/api/generator/status?site_id=${siteId}`) export const fetchGeneratorHistory = (siteId: string, genId: string, hours = 6) => apiFetch(`/api/generator/history?site_id=${siteId}&gen_id=${genId}&hours=${hours}`) // ── ATS ─────────────────────────────────────────────────────────── export type AtsStatus = { ats_id: string state: "stable" | "transferring" active_feed: "utility-a" | "utility-b" | "generator" transfer_count: number last_transfer_ms: number | null utility_a_v: number | null utility_b_v: number | null generator_v: number | null } export const fetchAtsStatus = (siteId: string) => apiFetch(`/api/power/ats?site_id=${siteId}`) // ── Phase breakdown ─────────────────────────────────────────────── export type RackPhase = { rack_id: string room_id: string phase_a_kw: number | null phase_b_kw: number | null phase_c_kw: number | null phase_a_a: number | null phase_b_a: number | null phase_c_a: number | null imbalance_pct: number | null } export type RoomPhase = { room_id: string; racks: RackPhase[] } export const fetchPhaseBreakdown = (siteId: string) => apiFetch(`/api/power/phase?site_id=${siteId}`) // ── Power redundancy ────────────────────────────────────────────── export type PowerRedundancy = { site_id: string level: "2N" | "N+1" | "N" ups_total: number ups_online: number generator_ok: boolean ats_active_feed: string | null notes: string } export const fetchPowerRedundancy = (siteId: string) => apiFetch(`/api/power/redundancy?site_id=${siteId}`) // ── Utility power ───────────────────────────────────────────────── export type UtilityPower = { site_id: string total_kw: number tariff_sgd_kwh: number kwh_month_to_date: number cost_sgd_mtd: number kwh_annual_est: number cost_sgd_annual_est: number currency: string } export const fetchUtilityPower = (siteId: string) => apiFetch(`/api/power/utility?site_id=${siteId}`) // ── Chiller ─────────────────────────────────────────────────────── export type ChillerStatus = { chiller_id: string state: "online" | "fault" | "unknown" chw_supply_c: number | null chw_return_c: number | null chw_delta_c: number | null flow_gpm: number | null cooling_load_kw: number | null cooling_load_pct: number | null cop: number | null compressor_load_pct: number | null condenser_pressure_bar: number | null evaporator_pressure_bar: number | null cw_supply_c: number | null cw_return_c: number | null run_hours: number | null } export type ChillerHistoryPoint = { bucket: string cop: number | null load_kw: number | null load_pct: number | null chw_supply_c: number | null chw_return_c: number | null comp_load: number | null } export const fetchChillerStatus = (siteId: string) => apiFetch(`/api/cooling/status?site_id=${siteId}`) export const fetchChillerHistory = (siteId: string, chillerId: string, hours = 6) => apiFetch(`/api/cooling/history?site_id=${siteId}&chiller_id=${chillerId}&hours=${hours}`) // ── Fire / VESDA ────────────────────────────────────────────────── export type FireZoneStatus = { zone_id: string room_id: string | null level: "normal" | "alert" | "action" | "fire" obscuration_pct_m: number | null detector_1_ok: boolean detector_2_ok: boolean power_ok: boolean flow_ok: boolean } export const fetchFireStatus = (siteId: string) => apiFetch(`/api/fire/status?site_id=${siteId}`) // ── Leak sensors ────────────────────────────────────────────────── export type LeakSensorStatus = { sensor_id: string floor_zone: string under_floor: boolean near_crac: boolean room_id: string | null state: "clear" | "detected" | "unknown" recorded_at: string | null } export const fetchLeakStatus = (siteId: string) => apiFetch(`/api/leak/status?site_id=${siteId}`) // ── Energy report ───────────────────────────────────────────────── export type EnergyReport = { site_id: string period_days: number from_date: string to_date: string kwh_total: number cost_sgd: number tariff_sgd_kwh: number currency: string pue_estimated: number pue_trend: { day: string; avg_it_kw: number; pue_est: number }[] } export const fetchEnergyReport = (siteId: string, days = 30) => apiFetch(`/api/reports/energy?site_id=${siteId}&days=${days}`) // ── Network ─────────────────────────────────────────────────────── export type NetworkSwitchStatus = { switch_id: string name: string model: string room_id: string rack_id: string role: "core" | "edge" | "access" port_count: number state: "up" | "degraded" | "down" | "unknown" uptime_s: number | null active_ports: number | null bandwidth_in_mbps: number | null bandwidth_out_mbps: number | null cpu_pct: number | null mem_pct: number | null temperature_c: number | null packet_loss_pct: number | null } export const fetchNetworkStatus = (siteId: string) => apiFetch(`/api/network/status?site_id=${siteId}`) export type PduReading = { rack_id: string room_id: string total_kw: number | null phase_a_kw: number | null phase_b_kw: number | null phase_c_kw: number | null phase_a_a: number | null phase_b_a: number | null phase_c_a: number | null imbalance_pct: number | null status: "ok" | "warning" | "critical" } export const fetchPduReadings = (siteId: string) => apiFetch(`/api/assets/pdus?site_id=${siteId}`) // ── Maintenance Windows ─────────────────────────────────────────── export type MaintenanceWindow = { id: string site_id: string title: string target: string target_label: string start_dt: string end_dt: string suppress_alarms: boolean notes: string created_at: string status: "active" | "scheduled" | "expired" } export const fetchMaintenanceWindows = (siteId: string) => apiFetch(`/api/maintenance?site_id=${siteId}`) export const fetchActiveMaintenanceWindows = (siteId: string) => apiFetch(`/api/maintenance/active?site_id=${siteId}`) export const createMaintenanceWindow = (body: { site_id: string; title: string; target: string; target_label: string; start_dt: string; end_dt: string; suppress_alarms: boolean; notes: string; }) => apiFetch("/api/maintenance", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }) export const deleteMaintenanceWindow = (id: string) => apiFetch(`/api/maintenance/${id}`, { method: "DELETE" }) // ── Floor layout ────────────────────────────────────────────────── export const fetchFloorLayout = (siteId: string) => apiFetch>(`/api/floor-layout?site_id=${siteId}`) export const saveFloorLayout = (siteId: string, layout: Record) => apiFetch<{ ok: boolean }>(`/api/floor-layout?site_id=${siteId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(layout), }) export type ParticleStatus = { room_id: string particles_0_5um: number | null particles_5um: number | null iso_class: number | null } export const fetchParticleStatus = (siteId: string) => apiFetch(`/api/env/particles?site_id=${siteId}`) // ── Settings: Sensors ───────────────────────────────────────────────────────── export type DeviceType = 'ups' | 'generator' | 'crac' | 'chiller' | 'ats' | 'rack' | 'network_switch' | 'leak' | 'fire_zone' | 'custom' export type Protocol = 'mqtt' | 'modbus_tcp' | 'modbus_rtu' | 'snmp' | 'bacnet' | 'http' export type SensorDevice = { id: number site_id: string device_id: string name: string device_type: DeviceType room_id: string | null rack_id: string | null protocol: Protocol protocol_config: Record enabled: boolean created_at: string updated_at: string recent_readings?: { sensor_type: string; value: number; unit: string; recorded_at: string }[] } export type SensorCreate = { device_id: string name: string device_type: DeviceType room_id?: string | null rack_id?: string | null protocol: Protocol protocol_config: Record enabled: boolean } export type SensorUpdate = Partial> export const fetchSensors = (siteId: string, params?: { device_type?: string; room_id?: string; protocol?: string }) => { const p: Record = { site_id: siteId } if (params?.device_type) p.device_type = params.device_type if (params?.room_id) p.room_id = params.room_id if (params?.protocol) p.protocol = params.protocol return apiFetch(`/api/settings/sensors?${new URLSearchParams(p)}`) } export const fetchSensor = (id: number) => apiFetch(`/api/settings/sensors/${id}`) export const createSensor = (siteId: string, body: SensorCreate) => apiFetch(`/api/settings/sensors?site_id=${siteId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) export const updateSensor = (id: number, body: SensorUpdate) => apiFetch(`/api/settings/sensors/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) export const deleteSensor = (id: number) => apiFetch(`/api/settings/sensors/${id}`, { method: 'DELETE' }) // ── Settings: Thresholds ─────────────────────────────────────────────────────── export type AlarmThreshold = { id: number site_id: string sensor_type: string threshold_value: number direction: 'above' | 'below' severity: 'warning' | 'critical' message_template: string enabled: boolean locked: boolean created_at: string updated_at: string } export type ThresholdUpdate = { threshold_value?: number; severity?: string; enabled?: boolean } export type ThresholdCreate = { sensor_type: string; threshold_value: number; direction: string; severity: string; message_template: string } export const fetchThresholds = (siteId: string) => apiFetch(`/api/settings/thresholds?site_id=${siteId}`) export const updateThreshold = (id: number, body: ThresholdUpdate) => apiFetch(`/api/settings/thresholds/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) export const createThreshold = (siteId: string, body: ThresholdCreate) => apiFetch(`/api/settings/thresholds?site_id=${siteId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) export const deleteThreshold = (id: number) => apiFetch(`/api/settings/thresholds/${id}`, { method: 'DELETE' }) export const resetThresholds = (siteId: string) => apiFetch<{ ok: boolean; count: number }>(`/api/settings/thresholds/reset?site_id=${siteId}`, { method: 'POST' }) // ── Settings: Site / Notifications / Integrations ───────────────────────────── export type SiteSettings = { name: string; timezone: string; description: string } export type NotificationSettings = { critical_alarms: boolean; warning_alarms: boolean; generator_events: boolean; maintenance_reminders: boolean; webhook_url: string; email_recipients: string } export type IntegrationSettings = { mqtt_host: string; mqtt_port: number } export type PagePrefs = { default_time_range_hours: number; refresh_interval_seconds: number } const _settingsPut = (path: string, value: Partial) => apiFetch(`/api/settings/${path}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value }), }) export const fetchSiteSettings = (siteId: string) => apiFetch(`/api/settings/site?site_id=${siteId}`) export const updateSiteSettings = (siteId: string, v: Partial) => _settingsPut(`site?site_id=${siteId}`, v) export const fetchNotifications = (siteId: string) => apiFetch(`/api/settings/notifications?site_id=${siteId}`) export const updateNotifications = (siteId: string, v: Partial) => _settingsPut(`notifications?site_id=${siteId}`, v) export const fetchIntegrations = (siteId: string) => apiFetch(`/api/settings/integrations?site_id=${siteId}`) export const updateIntegrations = (siteId: string, v: Partial) => _settingsPut(`integrations?site_id=${siteId}`, v) export const fetchPagePrefs = (siteId: string) => apiFetch(`/api/settings/page-prefs?site_id=${siteId}`) export const updatePagePrefs = (siteId: string, v: Partial) => _settingsPut(`page-prefs?site_id=${siteId}`, v) // ── Scenarios (simulator control) ───────────────────────────────── export type ScenarioInfo = { name: string label: string description: string duration: string compound: boolean default_target: string | null targets: string[] } export const fetchScenarios = () => apiFetch("/api/scenarios") export const triggerScenario = (scenario: string, target?: string) => apiFetch<{ ok: boolean }>("/api/scenarios/trigger", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scenario, target: target ?? null }), })