Chore: rename api->camera-management-api and web->management-dashboard-web-app; update compose, ignore, README references
This commit is contained in:
565
management-dashboard-web-app/src/lib/visionApi.ts
Normal file
565
management-dashboard-web-app/src/lib/visionApi.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
// Vision System API Client
|
||||
// Base URL for the vision system API - Use environment variable or default to vision container
|
||||
// The API is accessible at vision:8000 in the current setup
|
||||
const VISION_API_BASE_URL = import.meta.env.VITE_VISION_API_URL || 'http://vision:8000'
|
||||
|
||||
// Types based on the API documentation
|
||||
export interface SystemStatus {
|
||||
system_started: boolean
|
||||
mqtt_connected: boolean
|
||||
last_mqtt_message: string
|
||||
machines: Record<string, MachineStatus>
|
||||
cameras: Record<string, CameraStatus>
|
||||
active_recordings: number
|
||||
total_recordings: number
|
||||
uptime_seconds: number
|
||||
}
|
||||
|
||||
export interface MachineStatus {
|
||||
name: string
|
||||
state: string
|
||||
last_updated: string
|
||||
last_message?: string
|
||||
mqtt_topic?: string
|
||||
}
|
||||
|
||||
export interface CameraStatus {
|
||||
name?: string
|
||||
status: string
|
||||
is_recording: boolean
|
||||
last_checked: string
|
||||
last_error?: string | null
|
||||
device_info?: {
|
||||
friendly_name?: string
|
||||
serial_number?: string
|
||||
port_type?: string
|
||||
model?: string
|
||||
firmware_version?: string
|
||||
last_checked?: number
|
||||
}
|
||||
current_recording_file?: string | null
|
||||
recording_start_time?: string | null
|
||||
last_frame_time?: string
|
||||
frame_rate?: number
|
||||
// NEW AUTO-RECORDING FIELDS
|
||||
auto_recording_enabled: boolean
|
||||
auto_recording_active: boolean
|
||||
auto_recording_failure_count: number
|
||||
auto_recording_last_attempt?: string
|
||||
auto_recording_last_error?: string
|
||||
}
|
||||
|
||||
export interface RecordingInfo {
|
||||
camera_name: string
|
||||
filename: string
|
||||
start_time: string
|
||||
state: string
|
||||
end_time?: string
|
||||
file_size_bytes?: number
|
||||
frame_count?: number
|
||||
duration_seconds?: number
|
||||
error_message?: string | null
|
||||
}
|
||||
|
||||
export interface StorageStats {
|
||||
base_path: string
|
||||
total_files: number
|
||||
total_size_bytes: number
|
||||
cameras: Record<string, {
|
||||
file_count: number
|
||||
total_size_bytes: number
|
||||
}>
|
||||
disk_usage: {
|
||||
total: number
|
||||
used: number
|
||||
free: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface RecordingFile {
|
||||
filename: string
|
||||
camera_name: string
|
||||
file_size_bytes: number
|
||||
created_date: string
|
||||
duration_seconds?: number
|
||||
}
|
||||
|
||||
export interface StartRecordingRequest {
|
||||
filename?: string
|
||||
exposure_ms?: number
|
||||
gain?: number
|
||||
fps?: number
|
||||
}
|
||||
|
||||
export interface StartRecordingResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
export interface StopRecordingResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
duration_seconds: number
|
||||
}
|
||||
|
||||
export interface StreamStartResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface StreamStopResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface CameraTestResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
camera_name: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface CameraRecoveryResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
camera_name: string
|
||||
operation: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
// Auto-Recording Response Types
|
||||
export interface AutoRecordingConfigResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
camera_name: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface AutoRecordingStatusResponse {
|
||||
running: boolean
|
||||
auto_recording_enabled: boolean
|
||||
retry_queue: Record<string, any>
|
||||
enabled_cameras: string[]
|
||||
}
|
||||
|
||||
// Camera Configuration Types
|
||||
export interface CameraConfig {
|
||||
// READ-ONLY SYSTEM FIELDS
|
||||
name: string
|
||||
machine_topic: string
|
||||
storage_path: string
|
||||
enabled: boolean
|
||||
// READ-ONLY AUTO-RECORDING FIELDS
|
||||
auto_start_recording_enabled: boolean
|
||||
auto_recording_max_retries: number
|
||||
auto_recording_retry_delay_seconds: number
|
||||
// BASIC SETTINGS (real-time configurable)
|
||||
exposure_ms: number
|
||||
gain: number
|
||||
target_fps: number
|
||||
// VIDEO RECORDING SETTINGS (restart required)
|
||||
video_format: string // 'mp4' or 'avi'
|
||||
video_codec: string // 'mp4v', 'XVID', 'MJPG'
|
||||
video_quality: number // 0-100 (higher = better quality)
|
||||
// IMAGE QUALITY SETTINGS (real-time configurable)
|
||||
sharpness: number
|
||||
contrast: number
|
||||
saturation: number
|
||||
gamma: number
|
||||
// COLOR SETTINGS (real-time configurable)
|
||||
auto_white_balance: boolean
|
||||
color_temperature_preset: number
|
||||
// WHITE BALANCE RGB GAINS (real-time configurable)
|
||||
wb_red_gain: number
|
||||
wb_green_gain: number
|
||||
wb_blue_gain: number
|
||||
// ADVANCED SETTINGS
|
||||
anti_flicker_enabled: boolean
|
||||
light_frequency: number
|
||||
// NOISE REDUCTION (restart required)
|
||||
noise_filter_enabled: boolean
|
||||
denoise_3d_enabled: boolean
|
||||
// SYSTEM SETTINGS (restart required)
|
||||
bit_depth: number
|
||||
// HDR SETTINGS (real-time configurable)
|
||||
hdr_enabled: boolean
|
||||
hdr_gain_mode: number
|
||||
}
|
||||
|
||||
export interface CameraConfigUpdate {
|
||||
// BASIC SETTINGS (real-time configurable)
|
||||
exposure_ms?: number
|
||||
gain?: number
|
||||
target_fps?: number
|
||||
// IMAGE QUALITY SETTINGS (real-time configurable)
|
||||
sharpness?: number
|
||||
contrast?: number
|
||||
saturation?: number
|
||||
gamma?: number
|
||||
// COLOR SETTINGS (real-time configurable)
|
||||
auto_white_balance?: boolean
|
||||
color_temperature_preset?: number
|
||||
// WHITE BALANCE RGB GAINS (real-time configurable)
|
||||
wb_red_gain?: number
|
||||
wb_green_gain?: number
|
||||
wb_blue_gain?: number
|
||||
// ADVANCED SETTINGS (real-time configurable)
|
||||
anti_flicker_enabled?: boolean
|
||||
light_frequency?: number
|
||||
// HDR SETTINGS (real-time configurable)
|
||||
hdr_enabled?: boolean
|
||||
hdr_gain_mode?: number
|
||||
// NOTE: Video format settings and noise reduction settings are not included
|
||||
// as they are either read-only or require restart via apply-config endpoint
|
||||
}
|
||||
|
||||
export interface CameraConfigUpdateResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
updated_settings: string[]
|
||||
}
|
||||
|
||||
export interface CameraConfigApplyResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface MqttMessage {
|
||||
timestamp: string
|
||||
topic: string
|
||||
message: string
|
||||
source: string
|
||||
}
|
||||
|
||||
export interface MqttStatus {
|
||||
connected: boolean
|
||||
broker_host: string
|
||||
broker_port: number
|
||||
subscribed_topics: string[]
|
||||
last_message_time: string
|
||||
message_count: number
|
||||
error_count: number
|
||||
uptime_seconds: number
|
||||
}
|
||||
|
||||
export interface MqttEvent {
|
||||
machine_name: string
|
||||
topic: string
|
||||
payload: string
|
||||
normalized_state: string
|
||||
timestamp: string
|
||||
message_number: number
|
||||
}
|
||||
|
||||
export interface MqttEventsResponse {
|
||||
events: MqttEvent[]
|
||||
total_events: number
|
||||
last_updated: string
|
||||
}
|
||||
|
||||
export interface FileListRequest {
|
||||
camera_name?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface FileListResponse {
|
||||
files: RecordingFile[]
|
||||
total_count: number
|
||||
}
|
||||
|
||||
export interface CleanupRequest {
|
||||
max_age_days?: number
|
||||
}
|
||||
|
||||
export interface CleanupResponse {
|
||||
files_removed: number
|
||||
bytes_freed: number
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
// API Client Class
|
||||
class VisionApiClient {
|
||||
private baseUrl: string
|
||||
|
||||
constructor(baseUrl: string = VISION_API_BASE_URL) {
|
||||
this.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// System endpoints
|
||||
async getHealth(): Promise<{ status: string; timestamp: string }> {
|
||||
return this.request('/health')
|
||||
}
|
||||
|
||||
async getSystemStatus(): Promise<SystemStatus> {
|
||||
return this.request('/system/status')
|
||||
}
|
||||
|
||||
// Machine endpoints
|
||||
async getMachines(): Promise<Record<string, MachineStatus>> {
|
||||
return this.request('/machines')
|
||||
}
|
||||
|
||||
// MQTT endpoints
|
||||
async getMqttStatus(): Promise<MqttStatus> {
|
||||
return this.request('/mqtt/status')
|
||||
}
|
||||
|
||||
async getMqttEvents(limit: number = 10): Promise<MqttEventsResponse> {
|
||||
return this.request(`/mqtt/events?limit=${limit}`)
|
||||
}
|
||||
|
||||
// Camera endpoints
|
||||
async getCameras(): Promise<Record<string, CameraStatus>> {
|
||||
return this.request('/cameras')
|
||||
}
|
||||
|
||||
async getCameraStatus(cameraName: string): Promise<CameraStatus> {
|
||||
return this.request(`/cameras/${cameraName}/status`)
|
||||
}
|
||||
|
||||
// Recording control
|
||||
async startRecording(cameraName: string, params: StartRecordingRequest = {}): Promise<StartRecordingResponse> {
|
||||
return this.request(`/cameras/${cameraName}/start-recording`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
}
|
||||
|
||||
async stopRecording(cameraName: string): Promise<StopRecordingResponse> {
|
||||
return this.request(`/cameras/${cameraName}/stop-recording`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// Streaming control
|
||||
async startStream(cameraName: string): Promise<StreamStartResponse> {
|
||||
return this.request(`/cameras/${cameraName}/start-stream`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async stopStream(cameraName: string): Promise<StreamStopResponse> {
|
||||
return this.request(`/cameras/${cameraName}/stop-stream`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
getStreamUrl(cameraName: string): string {
|
||||
return `${this.baseUrl}/cameras/${cameraName}/stream`
|
||||
}
|
||||
|
||||
// Camera diagnostics
|
||||
async testCameraConnection(cameraName: string): Promise<CameraTestResponse> {
|
||||
return this.request(`/cameras/${cameraName}/test-connection`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async reconnectCamera(cameraName: string): Promise<CameraRecoveryResponse> {
|
||||
return this.request(`/cameras/${cameraName}/reconnect`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async restartCameraGrab(cameraName: string): Promise<CameraRecoveryResponse> {
|
||||
return this.request(`/cameras/${cameraName}/restart-grab`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async resetCameraTimestamp(cameraName: string): Promise<CameraRecoveryResponse> {
|
||||
return this.request(`/cameras/${cameraName}/reset-timestamp`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async fullCameraReset(cameraName: string): Promise<CameraRecoveryResponse> {
|
||||
return this.request(`/cameras/${cameraName}/full-reset`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async reinitializeCamera(cameraName: string): Promise<CameraRecoveryResponse> {
|
||||
return this.request(`/cameras/${cameraName}/reinitialize`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// Camera configuration
|
||||
async getCameraConfig(cameraName: string): Promise<CameraConfig> {
|
||||
try {
|
||||
const config = await this.request(`/cameras/${cameraName}/config`) as any
|
||||
|
||||
// Map API field names to UI expected field names and ensure auto-recording fields have default values if missing
|
||||
return {
|
||||
...config,
|
||||
// Map auto_start_recording_enabled from API to auto_record_on_machine_start for UI
|
||||
auto_record_on_machine_start: config.auto_start_recording_enabled ?? false,
|
||||
auto_start_recording_enabled: config.auto_start_recording_enabled ?? false,
|
||||
auto_recording_max_retries: config.auto_recording_max_retries ?? 3,
|
||||
auto_recording_retry_delay_seconds: config.auto_recording_retry_delay_seconds ?? 5
|
||||
}
|
||||
} catch (error: any) {
|
||||
// If the error is related to missing auto-recording fields, try to handle it gracefully
|
||||
if (error.message?.includes('auto_start_recording_enabled') ||
|
||||
error.message?.includes('auto_recording_max_retries') ||
|
||||
error.message?.includes('auto_recording_retry_delay_seconds')) {
|
||||
|
||||
// Try to get the raw camera data and add default auto-recording fields
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/cameras/${cameraName}/config`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const rawConfig = await response.json()
|
||||
|
||||
// Add missing auto-recording fields with defaults and map field names
|
||||
return {
|
||||
...rawConfig,
|
||||
// Map auto_start_recording_enabled from API to auto_record_on_machine_start for UI
|
||||
auto_record_on_machine_start: rawConfig.auto_start_recording_enabled ?? false,
|
||||
auto_start_recording_enabled: rawConfig.auto_start_recording_enabled ?? false,
|
||||
auto_recording_max_retries: rawConfig.auto_recording_max_retries ?? 3,
|
||||
auto_recording_retry_delay_seconds: rawConfig.auto_recording_retry_delay_seconds ?? 5
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
throw new Error(`Failed to load camera configuration: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateCameraConfig(cameraName: string, config: CameraConfigUpdate): Promise<CameraConfigUpdateResponse> {
|
||||
// Map UI field names to API field names
|
||||
const apiConfig = { ...config }
|
||||
|
||||
// If auto_record_on_machine_start is present, map it to auto_start_recording_enabled for the API
|
||||
if ('auto_record_on_machine_start' in config) {
|
||||
apiConfig.auto_start_recording_enabled = config.auto_record_on_machine_start
|
||||
// Remove the UI field name to avoid confusion
|
||||
delete apiConfig.auto_record_on_machine_start
|
||||
}
|
||||
|
||||
return this.request(`/cameras/${cameraName}/config`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiConfig),
|
||||
})
|
||||
}
|
||||
|
||||
async applyCameraConfig(cameraName: string): Promise<CameraConfigApplyResponse> {
|
||||
return this.request(`/cameras/${cameraName}/apply-config`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// Auto-Recording endpoints
|
||||
async enableAutoRecording(cameraName: string): Promise<AutoRecordingConfigResponse> {
|
||||
return this.request(`/cameras/${cameraName}/auto-recording/enable`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async disableAutoRecording(cameraName: string): Promise<AutoRecordingConfigResponse> {
|
||||
return this.request(`/cameras/${cameraName}/auto-recording/disable`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async getAutoRecordingStatus(): Promise<AutoRecordingStatusResponse> {
|
||||
return this.request('/auto-recording/status')
|
||||
}
|
||||
|
||||
// Recording sessions
|
||||
async getRecordings(): Promise<Record<string, RecordingInfo>> {
|
||||
return this.request('/recordings')
|
||||
}
|
||||
|
||||
// Storage endpoints
|
||||
async getStorageStats(): Promise<StorageStats> {
|
||||
return this.request('/storage/stats')
|
||||
}
|
||||
|
||||
async getFiles(params: FileListRequest = {}): Promise<FileListResponse> {
|
||||
return this.request('/storage/files', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupStorage(params: CleanupRequest = {}): Promise<CleanupResponse> {
|
||||
return this.request('/storage/cleanup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const visionApi = new VisionApiClient()
|
||||
|
||||
// Utility functions
|
||||
export const formatBytes = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export const formatDuration = (seconds: number): string => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`
|
||||
} else {
|
||||
return `${secs}s`
|
||||
}
|
||||
}
|
||||
|
||||
export const formatUptime = (seconds: number): string => {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`
|
||||
} else {
|
||||
return `${minutes}m`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user