plant/frontend/src/api.ts
jtricerolph 4f048a9629 Add configurable water-softener diagram to Dashboard
Per-asset diagram_config (JSONB) maps live MQTT field_keys to brine/resin
tank roles, editable on the Assets page with dropdowns populated from that
asset's actual telemetry. Dashboard renders brine + 1-2 resin tanks with
color-coded fill levels, an "in service" tank indicator, and an animated
flow/regen indicator when regeneration is active.
2026-07-30 09:02:12 +00:00

115 lines
4.4 KiB
TypeScript

import type {
PlantAsset, AssetStatus, AssetPhoto, AlertRule, PlantAlert, AppSetting, PhotoType, TelemetryField,
} from './types'
const BASE = '/plant/api'
async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...opts.headers },
...opts,
})
if (res.status === 401) {
;(window.top ?? window).location.href = '/login'
throw new Error('Unauthenticated')
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || `Request failed: ${res.status}`)
}
return res.json()
}
// Assets
export function fetchAssets(): Promise<PlantAsset[]> {
return request('/assets')
}
export function createAsset(body: Partial<PlantAsset>): Promise<PlantAsset> {
return request('/assets', { method: 'POST', body: JSON.stringify(body) })
}
export function updateAsset(id: number, body: Partial<PlantAsset>): Promise<PlantAsset> {
return request(`/assets/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function fetchAssetLatest(id: number): Promise<TelemetryField[]> {
return request(`/assets/${id}/latest`)
}
// Asset photos — multipart, so no JSON content-type header
export async function uploadAssetPhoto(assetId: number, file: File, photoType: PhotoType): Promise<AssetPhoto> {
const form = new FormData()
form.append('photo_type', photoType)
form.append('file', file)
const res = await fetch(`${BASE}/assets/${assetId}/photos`, { method: 'POST', credentials: 'include', body: form })
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || `Upload failed: ${res.status}`)
}
return res.json()
}
export function fetchAssetPhotos(assetId: number): Promise<AssetPhoto[]> {
return request(`/assets/${assetId}/photos`)
}
export function deleteAssetPhoto(id: number): Promise<{ ok: boolean }> {
return request(`/photos/${id}`, { method: 'DELETE' })
}
export function photoUrl(filePath: string): string {
return `${BASE}/uploads${filePath}`
}
// Status (dashboard)
export function fetchStatus(): Promise<{ open_alerts: number; mqtt_connected: boolean; assets: AssetStatus[] }> {
return request('/status')
}
// Alerts
export function fetchAlerts(filters: { status?: string; severity?: string } = {}): Promise<PlantAlert[]> {
const params = new URLSearchParams()
if (filters.status) params.set('status', filters.status)
if (filters.severity) params.set('severity', filters.severity)
const qs = params.toString()
return request(`/alerts${qs ? `?${qs}` : ''}`)
}
export function acknowledgeAlert(id: number): Promise<PlantAlert> {
return request(`/alerts/${id}`, { method: 'PATCH', body: JSON.stringify({ action: 'acknowledge' }) })
}
export function resolveAlert(id: number): Promise<PlantAlert> {
return request(`/alerts/${id}`, { method: 'PATCH', body: JSON.stringify({ action: 'resolve' }) })
}
// Alert rules — threshold/asset_id/asset_type are write-side numbers/nulls,
// distinct enough from AlertRule's read-side (string threshold) shape that a
// plain object type is simpler than fighting Partial<AlertRule> here.
export interface AlertRuleInput {
asset_id?: number | null
asset_type?: AlertRule['asset_type']
field_key?: string
condition?: AlertRule['condition']
threshold?: number
severity?: AlertRule['severity']
active?: boolean
}
export function fetchAlertRules(): Promise<AlertRule[]> {
return request('/alert-rules')
}
export function createAlertRule(body: AlertRuleInput): Promise<AlertRule> {
return request('/alert-rules', { method: 'POST', body: JSON.stringify(body) })
}
export function updateAlertRule(id: number, body: AlertRuleInput): Promise<AlertRule> {
return request(`/alert-rules/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteAlertRule(id: number): Promise<{ ok: boolean }> {
return request(`/alert-rules/${id}`, { method: 'DELETE' })
}
// Settings
export function getSettings(): Promise<{ settings: AppSetting[] }> {
return request('/settings')
}
export function saveSettings(settings: { key: string; value: string }[]): Promise<{ ok: boolean }> {
return request('/settings', { method: 'PUT', body: JSON.stringify({ settings }) })
}
export function fetchMqttStatus(): Promise<{ connected: boolean; note: string }> {
return request('/settings/mqtt-status')
}