diff --git a/services/web-ui/public/js/api.js b/services/web-ui/public/js/api.js new file mode 100644 index 0000000..ec002e7 --- /dev/null +++ b/services/web-ui/public/js/api.js @@ -0,0 +1,348 @@ +// Wild Dragon MAM - API Helper Module + +const API_BASE = '/api/v1'; +const CAPTURE_BASE = '/capture'; + +/** + * Wrapper around fetch with JSON parsing and error handling + */ +async function api(path, options = {}) { + const url = `${API_BASE}${path}`; + const config = { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }; + + try { + const response = await fetch(url, config); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: response.statusText })); + throw new Error(error.message || `API Error: ${response.status}`); + } + + const data = await response.json(); + return { success: true, data }; + } catch (error) { + console.error(`API Error on ${path}:`, error); + return { success: false, error: error.message }; + } +} + +/** + * Wrapper around fetch for capture endpoints + */ +async function captureApi(path, options = {}) { + const url = `${CAPTURE_BASE}${path}`; + const config = { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }; + + try { + const response = await fetch(url, config); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: response.statusText })); + throw new Error(error.message || `Capture API Error: ${response.status}`); + } + + const data = await response.json(); + return { success: true, data }; + } catch (error) { + console.error(`Capture API Error on ${path}:`, error); + return { success: false, error: error.message }; + } +} + +// ============================================================ +// ASSET API CALLS +// ============================================================ + +/** + * Get all assets with optional filtering + */ +async function getAssets(filters = {}) { + const params = new URLSearchParams(filters); + const path = `/assets${params.toString() ? '?' + params.toString() : ''}`; + return api(path); +} + +/** + * Get a single asset by ID + */ +async function getAsset(assetId) { + return api(`/assets/${assetId}`); +} + +/** + * Get signed streaming URL for an asset + */ +async function getAssetStreamUrl(assetId) { + return api(`/assets/${assetId}/stream`); +} + +/** + * Update asset metadata (tags, notes, etc) + */ +async function updateAsset(assetId, data) { + return api(`/assets/${assetId}`, { + method: 'PATCH', + body: JSON.stringify(data), + }); +} + +/** + * Search assets by query + */ +async function searchAssets(query) { + return api(`/assets/search?q=${encodeURIComponent(query)}`); +} + +/** + * Get asset status and metadata + */ +async function getAssetMetadata(assetId) { + return api(`/assets/${assetId}/metadata`); +} + +// ============================================================ +// PROJECT API CALLS +// ============================================================ + +/** + * Get all projects + */ +async function getProjects() { + return api('/projects'); +} + +/** + * Get a single project by ID + */ +async function getProject(projectId) { + return api(`/projects/${projectId}`); +} + +/** + * Create a new project + */ +async function createProject(name, description = '') { + return api('/projects', { + method: 'POST', + body: JSON.stringify({ name, description }), + }); +} + +/** + * Update project metadata + */ +async function updateProject(projectId, data) { + return api(`/projects/${projectId}`, { + method: 'PATCH', + body: JSON.stringify(data), + }); +} + +/** + * Delete a project + */ +async function deleteProject(projectId) { + return api(`/projects/${projectId}`, { + method: 'DELETE', + }); +} + +// ============================================================ +// BIN API CALLS +// ============================================================ + +/** + * Get all bins for a project + */ +async function getBins(projectId) { + return api(`/projects/${projectId}/bins`); +} + +/** + * Get a single bin by ID + */ +async function getBin(projectId, binId) { + return api(`/projects/${projectId}/bins/${binId}`); +} + +/** + * Create a new bin in a project + */ +async function createBin(projectId, name, description = '') { + return api(`/projects/${projectId}/bins`, { + method: 'POST', + body: JSON.stringify({ name, description }), + }); +} + +/** + * Update bin metadata + */ +async function updateBin(projectId, binId, data) { + return api(`/projects/${projectId}/bins/${binId}`, { + method: 'PATCH', + body: JSON.stringify(data), + }); +} + +/** + * Delete a bin + */ +async function deleteBin(projectId, binId) { + return api(`/projects/${projectId}/bins/${binId}`, { + method: 'DELETE', + }); +} + +// ============================================================ +// CAPTURE API CALLS +// ============================================================ + +/** + * Get list of available capture devices + */ +async function getCaptureDevices() { + return captureApi('/devices'); +} + +/** + * Get capture status + */ +async function getCaptureStatus() { + return captureApi('/status'); +} + +/** + * Get current recording state + */ +async function getRecordingStatus() { + return captureApi('/recording/status'); +} + +/** + * Start recording + */ +async function startRecording(deviceId, projectId, binId, clipName) { + return captureApi('/recording/start', { + method: 'POST', + body: JSON.stringify({ + device_id: deviceId, + project_id: projectId, + bin_id: binId, + clip_name: clipName, + }), + }); +} + +/** + * Stop recording + */ +async function stopRecording() { + return captureApi('/recording/stop', { + method: 'POST', + }); +} + +/** + * Get recent capture sessions + */ +async function getRecentCaptures(limit = 10) { + return captureApi(`/sessions?limit=${limit}`); +} + +/** + * Get timecode for current recording + */ +async function getRecordingTimecode() { + return captureApi('/recording/timecode'); +} + +// ============================================================ +// UTILITY FUNCTIONS +// ============================================================ + +/** + * Format bytes to human-readable size + */ +function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; +} + +/** + * Format duration in seconds to HH:MM:SS + */ +function formatDuration(seconds) { + if (!seconds || seconds < 0) return '00:00:00'; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + return [hours, minutes, secs] + .map(v => v.toString().padStart(2, '0')) + .join(':'); +} + +/** + * Get status badge text from status code + */ +function getStatusLabel(status) { + const labels = { + ingesting: 'Ingesting', + processing: 'Processing', + ready: 'Ready', + error: 'Error', + archived: 'Archived', + }; + return labels[status] || status; +} + +/** + * Get status badge CSS class from status code + */ +function getStatusBadgeClass(status) { + return `badge-${status}`; +} + +/** + * Debounce function for search + */ +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +/** + * Throttle function for scroll/resize events + */ +function throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +}