first commit

This commit is contained in:
mega 2026-03-19 11:32:17 +00:00
commit 4b98219bf7
144 changed files with 31561 additions and 0 deletions

711
frontend/lib/api.ts Normal file
View file

@ -0,0 +1,711 @@
// 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<T>(path: string, init?: RequestInit): Promise<T> {
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<KpiData>(`/api/readings/kpis?site_id=${siteId}`)
export const fetchPowerHistory = (siteId: string, hours = 1) =>
apiFetch<PowerBucket[]>(`/api/readings/site-power-history?site_id=${siteId}&hours=${hours}`)
export const fetchTempHistory = (siteId: string, hours = 1) =>
apiFetch<TempBucket[]>(`/api/readings/room-temp-history?site_id=${siteId}&hours=${hours}`)
export const fetchAlarms = (siteId: string, state = "active", limit = 100) =>
apiFetch<Alarm[]>(`/api/alarms?site_id=${siteId}&state=${state}&limit=${limit}`)
export const fetchRoomStatus = (siteId: string) =>
apiFetch<RoomStatus[]>(`/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<AlarmStats>(`/api/alarms/stats?site_id=${siteId}`)
export const acknowledgeAlarm = async (alarmId: number): Promise<void> => {
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<void> => {
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<AssetsData>(`/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<RoomPowerBreakdown[]>(`/api/power/rack-breakdown?site_id=${siteId}`)
export const fetchRoomPowerHistory = (siteId: string, hours = 6) =>
apiFetch<PowerHistoryBucket[]>(`/api/power/room-history?site_id=${siteId}&hours=${hours}`)
export const fetchUpsStatus = (siteId: string) =>
apiFetch<UpsAsset[]>(`/api/power/ups?site_id=${siteId}`)
export const fetchUpsHistory = (siteId: string, upsId: string, hours = 6) =>
apiFetch<UpsHistoryPoint[]>(`/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<RoomEnvReadings[]>(`/api/env/rack-readings?site_id=${siteId}`)
export const fetchHumidityHistory = (siteId: string, hours = 6) =>
apiFetch<HumidityBucket[]>(`/api/env/humidity-history?site_id=${siteId}&hours=${hours}`)
export const fetchCracStatus = (siteId: string) =>
apiFetch<CracStatus[]>(`/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<RackHistory>(`/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<CracDeltaPoint[]>(`/api/env/crac-delta-history?site_id=${siteId}&crac_id=${cracId}&hours=${hours}`)
export const fetchCracHistory = (siteId: string, cracId: string, hours = 6) =>
apiFetch<CracHistoryPoint[]>(`/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<ReportSummary>(`/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<CapacitySummary>(`/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<Device[]>(`/api/assets/devices?site_id=${siteId}`)
export const fetchRackDevices = (siteId: string, rackId: string) =>
apiFetch<Device[]>(`/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<GeneratorStatus[]>(`/api/generator/status?site_id=${siteId}`)
export const fetchGeneratorHistory = (siteId: string, genId: string, hours = 6) =>
apiFetch<GeneratorHistoryPoint[]>(`/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<AtsStatus[]>(`/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<RoomPhase[]>(`/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<PowerRedundancy>(`/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<UtilityPower>(`/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<ChillerStatus[]>(`/api/cooling/status?site_id=${siteId}`)
export const fetchChillerHistory = (siteId: string, chillerId: string, hours = 6) =>
apiFetch<ChillerHistoryPoint[]>(`/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<FireZoneStatus[]>(`/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<LeakSensorStatus[]>(`/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<EnergyReport>(`/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<NetworkSwitchStatus[]>(`/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<PduReading[]>(`/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<MaintenanceWindow[]>(`/api/maintenance?site_id=${siteId}`)
export const fetchActiveMaintenanceWindows = (siteId: string) =>
apiFetch<MaintenanceWindow[]>(`/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<MaintenanceWindow>("/api/maintenance", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })
export const deleteMaintenanceWindow = (id: string) =>
apiFetch<void>(`/api/maintenance/${id}`, { method: "DELETE" })
// ── Floor layout ──────────────────────────────────────────────────
export const fetchFloorLayout = (siteId: string) =>
apiFetch<Record<string, unknown>>(`/api/floor-layout?site_id=${siteId}`)
export const saveFloorLayout = (siteId: string, layout: Record<string, unknown>) =>
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<ParticleStatus[]>(`/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<string, unknown>
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<string, unknown>
enabled: boolean
}
export type SensorUpdate = Partial<Omit<SensorCreate, 'device_id'>>
export const fetchSensors = (siteId: string, params?: { device_type?: string; room_id?: string; protocol?: string }) => {
const p: Record<string, string> = { 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<SensorDevice[]>(`/api/settings/sensors?${new URLSearchParams(p)}`)
}
export const fetchSensor = (id: number) =>
apiFetch<SensorDevice>(`/api/settings/sensors/${id}`)
export const createSensor = (siteId: string, body: SensorCreate) =>
apiFetch<SensorDevice>(`/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<SensorDevice>(`/api/settings/sensors/${id}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
})
export const deleteSensor = (id: number) =>
apiFetch<void>(`/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<AlarmThreshold[]>(`/api/settings/thresholds?site_id=${siteId}`)
export const updateThreshold = (id: number, body: ThresholdUpdate) =>
apiFetch<AlarmThreshold>(`/api/settings/thresholds/${id}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
})
export const createThreshold = (siteId: string, body: ThresholdCreate) =>
apiFetch<AlarmThreshold>(`/api/settings/thresholds?site_id=${siteId}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
})
export const deleteThreshold = (id: number) =>
apiFetch<void>(`/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 = <T>(path: string, value: Partial<T>) =>
apiFetch<T>(`/api/settings/${path}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value }),
})
export const fetchSiteSettings = (siteId: string) => apiFetch<SiteSettings>(`/api/settings/site?site_id=${siteId}`)
export const updateSiteSettings = (siteId: string, v: Partial<SiteSettings>) => _settingsPut<SiteSettings>(`site?site_id=${siteId}`, v)
export const fetchNotifications = (siteId: string) => apiFetch<NotificationSettings>(`/api/settings/notifications?site_id=${siteId}`)
export const updateNotifications = (siteId: string, v: Partial<NotificationSettings>) => _settingsPut<NotificationSettings>(`notifications?site_id=${siteId}`, v)
export const fetchIntegrations = (siteId: string) => apiFetch<IntegrationSettings>(`/api/settings/integrations?site_id=${siteId}`)
export const updateIntegrations = (siteId: string, v: Partial<IntegrationSettings>) => _settingsPut<IntegrationSettings>(`integrations?site_id=${siteId}`, v)
export const fetchPagePrefs = (siteId: string) => apiFetch<PagePrefs>(`/api/settings/page-prefs?site_id=${siteId}`)
export const updatePagePrefs = (siteId: string, v: Partial<PagePrefs>) => _settingsPut<PagePrefs>(`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<ScenarioInfo[]>("/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 }),
})