const express = require('express'); const cors = require('cors'); const axios = require('axios'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; app.use(cors()); app.use(express.json()); app.use(express.static('public')); // Proxy endpoint for getting token app.post('/api/token', async (req, res) => { try { const { apiKey, baseUrl } = req.body; const response = await axios.post( `${baseUrl}/identity/connect/token`, 'grant_type=client_credentials&scope=platform', { headers: { 'Authorization': `Basic ${apiKey}`, 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 } ); res.json(response.data); } catch (error) { console.error('Token error:', error.message); res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // Proxy endpoint for fetching RUNNING recorders // Strategy: use events as the primary source (they reflect what's actually running), // then enrich with channel data for editing (source:text, destinationId:int) app.get('/api/channels', async (req, res) => { try { const { token, baseUrl } = req.query; if (!token || !baseUrl) { return res.status(400).json({ error: 'Missing token or baseUrl' }); } const headers = { 'Authorization': `Bearer ${token}` }; // Fetch channels and events in parallel const [channelsRes, eventsRes] = await Promise.allSettled([ axios.get(`${baseUrl}/api/store/channel/v1/channels`, { headers, timeout: 10000 }), axios.get(`${baseUrl}/api/v2/store/schedule/events`, { headers, timeout: 10000 }) ]); const channelsData = channelsRes.status === 'fulfilled' ? (Array.isArray(channelsRes.value.data) ? channelsRes.value.data : []) : []; const eventsData = eventsRes.status === 'fulfilled' ? (Array.isArray(eventsRes.value.data) ? eventsRes.value.data : []) : []; console.log(`Fetched ${channelsData.length} channels, ${eventsData.length} events`); if (eventsData.length > 0) { console.log('Sample event keys:', Object.keys(eventsData[0])); console.log('Sample event:', JSON.stringify(eventsData[0], null, 2).substring(0, 500)); } // Build a channel lookup map by URI const channelByUri = {}; channelsData.forEach(ch => { if (ch['uri:text']) channelByUri[ch['uri:text']] = ch; if (ch.uri) channelByUri[ch.uri] = ch; }); // Filter events to only those currently running const runningEvents = eventsData.filter(event => { const status = (event['status:enum'] || '').toLowerCase(); return status === 'running'; }); console.log(`Running events: ${runningEvents.length}`); // Enrich each running event with its channel data const result = runningEvents.map(event => { const channelUri = event['source']?.['channel:text']; const channel = channelUri ? channelByUri[channelUri] : null; return { // Clean up name - remove leading "undefined" or "null" prefixes const rawName = event['name:text'] || channel?.['name:text'] || 'Unknown'; const cleanName = rawName.replace(/^(undefined|null)\s*/i, '').trim() || rawName; // Event fields _eventId: event['scheduleEvent:id'] || event['id'], _health: event['health:enum'], _status: event['status:enum'], _healthCondition: event['healthCondition:enum'], _channelUri: channelUri, // Channel fields (for editing) 'channel:id': channel?.['channel:id'] || null, 'name:text': cleanName, 'source:text': channel?.['source:text'] || null, 'destinationId:int': channel?.['destinationId:int'] || null, 'elasticRecorder': channel?.['elasticRecorder'] || null, }; }); res.json(result); } catch (error) { console.error('Channels error:', error.message); res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // Proxy endpoint for updating channel app.patch('/api/channels/:id', async (req, res) => { try { const { token, baseUrl } = req.query; const { id } = req.params; const data = req.body; if (!token || !baseUrl) { return res.status(400).json({ error: 'Missing token or baseUrl' }); } const response = await axios.patch( `${baseUrl}/api/store/channel/v1/channels/${id}`, data, { headers: { 'Authorization': `Bearer ${token}` }, timeout: 10000 } ); res.json(response.data); } catch (error) { console.error('Update error:', error.message); res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // Health check app.get('/health', (req, res) => { res.json({ status: 'ok' }); }); // Serve index app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.listen(PORT, () => { console.log(`✅ Dashboard running on http://localhost:${PORT}`); });