From ccde43ce37a42538381dbc2c14190cf4c7885e68 Mon Sep 17 00:00:00 2001 From: zgaetano Date: Tue, 31 Mar 2026 15:29:52 -0400 Subject: [PATCH] Add server.js --- server.js | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 server.js diff --git a/server.js b/server.js new file mode 100644 index 0000000..51736ca --- /dev/null +++ b/server.js @@ -0,0 +1,166 @@ +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}`); +});