From 2ca7004b7092072a68af4de422f7fa4cbdba12df Mon Sep 17 00:00:00 2001 From: rdkartono Date: Wed, 15 Oct 2025 16:13:44 +0700 Subject: [PATCH] commit 15/10/2025 Overview menu belum beres --- html/webpage/assets/js/overview.js | 349 +++++++++++++++++++++++++++++ html/webpage/assets/js/script.js | 147 ++++++------ html/webpage/overview.html | 1 + src/web/StreamerOutputData.kt | 9 + src/web/WebApp.kt | 3 +- 5 files changed, 444 insertions(+), 65 deletions(-) create mode 100644 html/webpage/assets/js/overview.js create mode 100644 src/web/StreamerOutputData.kt diff --git a/html/webpage/assets/js/overview.js b/html/webpage/assets/js/overview.js new file mode 100644 index 0000000..2ea3069 --- /dev/null +++ b/html/webpage/assets/js/overview.js @@ -0,0 +1,349 @@ +/** + * @typedef {Object} StreamerOutputData + * @property {number} index - The index of the Barix connection. + * @property {string} channel - The channel name of the Barix connection. + * @property {string} ipaddress - The IP address of the Barix connection. + * @property {number} bufferRemain - The remaining buffer size of the Barix connection. + * @property {boolean} isPlaying - true = playback started, false = playback stopped + * @property {number} vu - The VU level of the Barix connection, 0 to 100. + */ + + +/** + * @typedef {Object} PagingQueue + * @property {number} index - The index of the paging queue item. + * @property {string} Date_Time - The date and time of the paging queue item. + * @property {string} Source - The source of the paging queue item. + * @property {string} Type - The type of the paging queue item. + * @property {string} Message - The message of the paging queue item. + * @property {string} BroadcastZones - The broadcast zones of the paging queue item. + */ + +/** + * @typedef {Object} StreamerCard + * @property {JQuery | null} title - The jQuery result should be

element. + * @property {JQuery | null} ip - The jQuery result should be
element. + * @property {JQuery | null} buffer - The jQuery result should be
element. + * @property {JQuery | null} status - The jQuery result should be

element. + * @property {JQuery | null} vu - The jQuery result should be element. + */ + +function getCardByIndex(index) { + let obj = { + // title is

element wiht id `streamertitle${index}`, with index as two digit number, e.g. 01, 02, 03 + title: $(`#streamertitle${index.toString().padStart(2, '0')}`), + // ip is

element with id `streamerip${index}`, with index as two digit number, e.g. 01, 02, 03 + ip: $(`#streamerip${index.toString().padStart(2, '0')}`), + // buffer is
element with id `streamerbuffer${index}`, with index as two digit number, e.g. 01, 02, 03 + buffer: $(`#streamerbuffer${index.toString().padStart(2, '0')}`), + // status is

element with id `streamerstatus${index}`, with index as two digit number, e.g. 01, 02, 03 + status: $(`#streamerstatus${index.toString().padStart(2, '0')}`), + // vu is element with id `streamervu${index}`, with index as two digit number, e.g. 01, 02, 03 + vu: $(`#streamervu${index.toString().padStart(2, '0')} .progress-bar`), + } + return obj; +} + +/** + * Updates the streamer card with the provided values. + * @param {StreamerOutputData[]} values + */ +function UpdateStreamerCard(values) { + if (!Array.isArray(values) || values.length === 0) return; + function setProgress($bar, value, max = 100) { + const v = Number(value ?? 0); + const pct = Math.max(0, Math.min(100, Math.round((v / max) * 100))); + $bar + .attr('aria-valuenow', v) // semantic value + .css('width', pct + '%') // visual width + .text(pct); // optional label + } + for (let i = 1; i <= 64; i++) { + let vv = values.find(v => v.index === i); + let card = getCardByIndex(i); + if (vv) { + // there is value for this index + if (card.title) card.title.text(vv.channel ? vv.channel : `Channel ${i.toString().padStart(2, '0')}`); + if (card.ip) card.ip.text(`IP Address: ${vv.ipaddress ? vv.ipaddress : 'N/A'}`); + if (card.buffer) card.buffer.text(`Buffer: ${vv.bufferRemain !== undefined && vv.bufferRemain !== null ? vv.bufferRemain.toString() : 'N/A'}`); + if (card.status) card.status.text(`Status: ${vv.isPlaying ? 'Playing' : 'Stopped'}`); + if (card.vu) { + setProgress(card.vu, vv.vu, 100); + } + } else { + // no value for this index, disable the card + if (card.title) card.title.text(`Channel ${i.toString().padStart(2, '0')}`); + if (card.ip) card.ip.text(`IP Address: N/A`); + if (card.buffer) card.buffer.text(`Buffer: N/A`); + if (card.status) card.status.text(`Status: Disconnected`); + if (card.vu) { + setProgress(card.vu, 0, 100); + } + } + } +} + +/** + * @type {PagingQueue[]} + */ +window.PagingQueue = []; +/** + * @type {JQuery | null} + */ +window.selectedpagingrow = null; + +/** + * @typedef {Object} QueueTable + * @property {number} index - The index of the automatic queue item. + * @property {string} Date_Time - The date and time of the automatic queue item. + * @property {string} Source - The source of the automatic queue item. + * @property {string} Type - The type of the automatic queue item. + * @property {string} Message - The message of the automatic queue item. + * @property {string} SB_TAGS - The SB_TAGS of the automatic queue item. + * @property {string} BroadcastZones - The broadcast zones of the automatic queue item. + * @property {number} Repeat - The repeat count of the automatic queue item. + * @property {string} Language - The language of the automatic queue item. + */ + +/** + * @type {QueueTable[]} + */ +window.QueueTable = []; +/** + * @type {JQuery | null} + */ +window.selectedautomaticrow = null; + +/** + * Fills the paging queue table body with the provided data. + * @param {PagingQueue[]} vv array of PagingQueue objects + * @returns + */ +function fill_pagingqueuetablebody(vv) { + $('#pagingqueuetable').empty(); + if (!Array.isArray(vv) || vv.length === 0) return; + vv.forEach(item => { + // fill index and description columns using item properties + let description = `${item.Date_Time}_${item.Source}_${item.Type}_${item.Message}_${item.BroadcastZones}`; + $('#pagingqueuetable').append(` + ${item.index} + ${description} + `); + let $addedrow = $('#pagingqueuetable tr:last'); + $addedrow.off('click').on('click', function () { + if (window.selectedpagingrow) { + window.selectedpagingrow.find('td').css('background-color', ''); + if (window.selectedpagingrow.is($(this))) { + window.selectedpagingrow = null; + $('#removepagingqueue').prop('disabled', true); + return; + } + } + window.selectedpagingrow = $(this); + window.selectedpagingrow.find('td').css('background-color', 'lightblue'); + $('#removepagingqueue').prop('disabled', false); + }); + }); +} + +/** + * Fills the automatic queue table body with the provided data. + * @param {QueueTable[]} vv array of QueueTable objects + * @returns + */ +function fill_automaticqueuetablebody(vv) { + $('#automaticqueuetable').empty(); + if (!Array.isArray(vv) || vv.length === 0) return; + vv.forEach(item => { + // fill index and description columns using item properties + let description = `${item.Date_Time}_${item.Source}_${item.Type}_${item.Message}_${item.BroadcastZones}`; + $('#automaticqueuetable').append(` + ${item.index} + ${description} + `); + let $addedrow = $('#automaticqueuetable tr:last'); + $addedrow.off('click').on('click', function () { + if (window.selectedautomaticrow) { + window.selectedautomaticrow.find('td').css('background-color', ''); + if (window.selectedautomaticrow.is($(this))) { + window.selectedautomaticrow = null; + $('#removeautomatictable').prop('disabled', true); + return; + } + } + window.selectedautomaticrow = $(this); + window.selectedautomaticrow.find('td').css('background-color', 'lightblue'); + $('#removeautomatictable').prop('disabled', false); + }); + }); +} + +function reloadPagingQueue(APIURL = "QueuePaging/") { + window.PagingQueue = []; + fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => { + if (Array.isArray(okdata) && okdata.length > 0) { + window.PagingQueue.push(...okdata); + fill_pagingqueuetablebody(window.PagingQueue); + } else { + console.log("reloadPagingQueue: okdata is not array"); + } + }, (errdata) => { + console.log("reloadPagingQueue: errdata", errdata); + }); +} + +function reloadAutomaticQueue(APIURL = "QueueTable/") { + window.QueueTable = []; + fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => { + if (Array.isArray(okdata) && okdata.length > 0) { + window.QueueTable.push(...okdata); + fill_automaticqueuetablebody(window.QueueTable); + } else { + console.log("reloadAutomaticQueue: okdata is not array"); + } + }, (errdata) => { + console.log("reloadAutomaticQueue: errdata", errdata); + }); +} + +function RemovePagingQueueByIndex(index, APIURL = "QueuePaging/") { + fetchAPI(APIURL + "DeleteByIndex/" + index, "DELETE", {}, null, (okdata) => { + console.log("RemovePagingQueueByIndex: okdata", okdata); + reloadPagingQueue(APIURL); + }, (errdata) => { + console.log("RemovePagingQueueByIndex: errdata", errdata); + }); +} + +function RemoveAutomaticQueueByIndex(index, APIURL = "QueueTable/") { + fetchAPI(APIURL + "DeleteByIndex/" + index, "DELETE", {}, null, (okdata) => { + console.log("RemoveAutomaticQueueByIndex: okdata", okdata); + reloadAutomaticQueue(APIURL); + }, (errdata) => { + console.log("RemoveAutomaticQueueByIndex: errdata", errdata); + }); +} + +$(document).ready(function () { + console.log("overview.js loaded"); + + + + $('#clearpagingqueue').off('click').on('click', function () { + DoClear("QueuePaging/", "Paging Queue", (okdata) => { + reloadPagingQueue(); + alert("Success clear Paging Queue: " + okdata.message); + }, (errdata) => { + alert("Error clear Paging Queue: " + errdata.message); + }); + }); + $('#removepagingqueue').off('click').on('click', function () { + if (window.selectedpagingrow) { + let cells = window.selectedpagingrow.find('td'); + let index = Number(cells.eq(0).text()); + let description = cells.eq(1).text(); + if (!isNaN(index) && description && description.length > 0) { + if (confirm(`Are you sure to remove Paging Queue Index: ${index} Description: ${description} ?`)) { + RemovePagingQueueByIndex(index); + window.selectedpagingrow = null; + $('#removepagingqueue').prop('disabled', true); + } + } + } + }); + + + + $('#clearautomatictable').off('click').on('click', function () { + DoClear("QueueTable/", "Automatic Queue", (okdata) => { + reloadAutomaticQueue(); + alert("Success clear Automatic Queue: " + okdata.message); + }, (errdata) => { + alert("Error clear Automatic Queue: " + errdata.message); + }); + }); + $('#removeautomatictable').off('click').on('click', function () { + if (window.selectedautomaticrow) { + let cells = window.selectedautomaticrow.find('td'); + let index = Number(cells.eq(0).text()); + let description = cells.eq(1).text(); + if (!isNaN(index) && description && description.length > 0) { + if (confirm(`Are you sure to remove Automatic Queue Index: ${index} Description: ${description} ?`)) { + RemoveAutomaticQueueByIndex(index); + window.selectedautomaticrow = null; + $('#removeautomatictable').prop('disabled', true); + } + } + } + }); + + let intervaljob = null; + function runIntervalJob() { + if (intervaljob) clearInterval(intervaljob); + intervaljob = setInterval(() => { + sendCommand("getPagingQueue", ""); + sendCommand("getAASQueue", ""); + sendCommand("getStreamerOutputs", ""); + }, 1000); + console.log("overview.js interval job started"); + } + + runIntervalJob(); + + window.addEventListener('ws_connected', () => { + console.log("overview.js ws_connected event triggered"); + runIntervalJob(); + }); + + window.addEventListener('ws_disconnected', () => { + console.log("overview.js ws_disconnected event triggered"); + if (intervaljob) clearInterval(intervaljob); + intervaljob = null; + }); + window.addEventListener('ws_message', (event) => { + let rep = event.detail; + let cmd = rep.reply; + let data = rep.data; + if (cmd && cmd.length > 0) { + switch (cmd) { + case "getPagingQueue": + let pq = JSON.parse(data); + //console.log("getPagingQueue:", pq); + if (Array.isArray(pq) && pq.length > 0) { + window.PagingQueue = []; + window.PagingQueue.push(...pq); + console.log(`PagingQueue length: ${window.PagingQueue.length}`); + fill_pagingqueuetablebody(window.PagingQueue); + } + break; + case "getAASQueue": + let aq = JSON.parse(data); + //console.log("getAASQueue:", aq); + if (Array.isArray(aq) && aq.length > 0) { + window.QueueTable = []; + window.QueueTable.push(...aq); + console.log(`QueueTable length: ${window.QueueTable.length}`); + fill_automaticqueuetablebody(window.QueueTable); + } + break; + case "getStreamerOutputs": + /** + * @type {StreamerOutputData[]} + */ + let so = JSON.parse(data); + UpdateStreamerCard(so); + break; + } + } + }); + + + + + + $(window).on('beforeunload', function () { + console.log("overview.js beforeunload event triggered"); + clearInterval(intervaljob); + intervaljob = null; + }); +}); diff --git a/html/webpage/assets/js/script.js b/html/webpage/assets/js/script.js index d293996..cb299eb 100644 --- a/html/webpage/assets/js/script.js +++ b/html/webpage/assets/js/script.js @@ -69,10 +69,10 @@ function fetchAPI(endpoint, method, headers = {}, body = null, cbOK, cbError) { } } fetch(url, options) - .then(async(response) => { + .then(async (response) => { if (!response.ok) { - let msg ; - try{ + let msg; + try { let _xxx = await response.json(); msg = _xxx.message || response.statusText; } catch { @@ -281,10 +281,10 @@ window.redcircle = null; */ $(document).ready(function () { document.title = "Automatic Announcement System" - if (window.greencircle === null){ + if (window.greencircle === null) { fetchImg('green_circle.png', (url) => { window.greencircle = url; }, (err) => { console.error("Error loading green_circle.png : ", err); }); } - if (window.redcircle === null){ + if (window.redcircle === null) { fetchImg('red_circle.png', (url) => { window.redcircle = url; }, (err) => { console.error("Error loading red_circle.png : ", err); }); } const wsURL = window.location.pathname + '/ws' @@ -292,8 +292,8 @@ $(document).ready(function () { alert("Runtime error: " + chrome.runtime.lastError.message); return; } - - + + // reset status indicators function resetStatusIndicators() { @@ -311,64 +311,83 @@ $(document).ready(function () { getCategories(); getLanguages(); getScheduledDays(); - - // Initialize WebSocket connection - window.ws = new WebSocket(wsURL); - window.ws.onopen = () => { - console.log('WebSocket connection established'); - $('#onlineindicator').attr('src', window.greencircle); - }; - window.ws.onmessage = (event) => { - if ($('#onlineindicator').attr('src') !== window.greencircle) { + // reconnect handle + let ws_reconnect; + + function reconnect() { + if (window.ws && window.ws.readyState === WebSocket.OPEN) return; + const s = new WebSocket(wsURL); + s.addEventListener('open', () => { + console.log('WebSocket connection established'); $('#onlineindicator').attr('src', window.greencircle); - } - let rep = JSON.parse(event.data); - let cmd = rep.reply - let data = rep.data; - if (cmd && cmd.length > 0) { - switch (cmd) { - case "getCPUStatus": - $('#cpustatus').text("CPU : " + data) - break; - case "getMemoryStatus": - $('#ramstatus').text("RAM : " + data) - break; - case "getDiskStatus": - $('#diskstatus').text("Disk : " + data) - break; - case "getNetworkStatus": - //console.log("Network status: ", data); - let result = ""; - let json = JSON.parse(data); - if (Array.isArray(json) && json.length> 0){ - json.forEach((net)=>{ - //console.log("Network interface: ", net); - if (result.length>0) result+="\n" - result+=`${net.displayName} (${net.ipV4addr.join(";")}) TX:${(net.txSpeed/1024).toFixed(1)} KB/s RX:${(net.rxSpeed/1024).toFixed(1)} KB/s; ` - }) - } else result = "N/A"; - $('#networkstatus').text(result) - break; - case "getSystemTime": - $('#datetimetext').text(data) - break; + if (ws_reconnect) { + // stop reconnect attempts + clearTimeout(ws_reconnect); + ws_reconnect = null; } + window.dispatchEvent(new Event('ws_connected')); + }); + s.addEventListener('close', () => { + console.log('WebSocket connection closed'); + window.dispatchEvent(new Event('ws_disconnected')); + resetStatusIndicators(); + if (!ws_reconnect) { + clearTimeout(ws_reconnect); + ws_reconnect = null; + } + ws_reconnect = setTimeout(reconnect, 5000); // try to reconnect every 5 seconds + }); + s.addEventListener('message', (event) => { + if ($('#onlineindicator').attr('src') !== window.greencircle) { + $('#onlineindicator').attr('src', window.greencircle); + } + let rep = JSON.parse(event.data); + window.dispatchEvent(new CustomEvent('ws_message', { detail: rep })); + let cmd = rep.reply + let data = rep.data; + if (cmd && cmd.length > 0) { + switch (cmd) { + case "getCPUStatus": + $('#cpustatus').text("CPU : " + data) + break; + case "getMemoryStatus": + $('#ramstatus').text("RAM : " + data) + break; + case "getDiskStatus": + $('#diskstatus').text("Disk : " + data) + break; + case "getNetworkStatus": + let result = ""; + let json = JSON.parse(data); + if (Array.isArray(json) && json.length > 0) { + json.forEach((net) => { + if (result.length > 0) result += "\n" + result += `${net.displayName} (${net.ipV4addr.join(";")}) TX:${(net.txSpeed / 1024).toFixed(1)} KB/s RX:${(net.rxSpeed / 1024).toFixed(1)} KB/s` + }) + } else result = "N/A"; + $('#networkstatus').text(result) + break; + case "getSystemTime": + $('#datetimetext').text(data) + break; + + } + } + }); + window.ws = s; + } + + reconnect(); + window.addEventListener('beforeunload', () => { + try{ + window.ws?.close(1000, "Client closed connection"); + } catch (error) { + console.error("Error closing WebSocket connection:", error); } - }; - window.ws.onclose = () => { - console.log('WebSocket connection closed'); - resetStatusIndicators(); - - }; - - // window.ws.onerror = (error) => { - // console.error('WebSocket error:', error); - // }; - - + }); setInterval(() => { sendCommand("getCPUStatus", "") @@ -395,7 +414,7 @@ $(document).ready(function () { if (status === "success") { console.log("Soundbank content loaded successfully"); // pindah soundbank.js - + } else { console.error("Error loading soundbank content : ", xhr.status, xhr.statusText); } @@ -408,7 +427,7 @@ $(document).ready(function () { if (status === "success") { console.log("Messagebank content loaded successfully"); // pindah messagebank.js - + } else { console.error("Error loading messagebank content : ", xhr.status, xhr.statusText); } @@ -421,7 +440,7 @@ $(document).ready(function () { if (status === "success") { console.log("Language content loaded successfully"); // pindah languagelink.js - + } else { console.error("Error loading language content : ", xhr.status, xhr.statusText); } @@ -445,7 +464,7 @@ $(document).ready(function () { if (status === "success") { console.log("Timer content loaded successfully"); // pindah ke schedulebank.js - + } else { console.error("Error loading timer content : ", xhr.status, xhr.statusText); } @@ -469,7 +488,7 @@ $(document).ready(function () { if (status === "success") { console.log("User Management content loaded successfully"); // pindah ke usermanagement.js - + } else { console.error("Error loading user management content:", xhr.status, xhr.statusText); } diff --git a/html/webpage/overview.html b/html/webpage/overview.html index ff8c347..ab5c9b1 100644 --- a/html/webpage/overview.html +++ b/html/webpage/overview.html @@ -1324,6 +1324,7 @@ + \ No newline at end of file diff --git a/src/web/StreamerOutputData.kt b/src/web/StreamerOutputData.kt new file mode 100644 index 0000000..7294854 --- /dev/null +++ b/src/web/StreamerOutputData.kt @@ -0,0 +1,9 @@ +package web + +class StreamerOutputData(val index: UInt, val channel: String, val ipaddress: String, val vu: Int, val bufferRemain: Int, var isPlaying: Boolean) { + companion object{ + fun fromBarixConnection(bc: barix.BarixConnection): StreamerOutputData { + return StreamerOutputData(bc.index, bc.channel, bc.ipaddress, bc.vu, bc.bufferRemain, bc.isPlaying()) + } + } +} \ No newline at end of file diff --git a/src/web/WebApp.kt b/src/web/WebApp.kt index 1d4cb5a..56b32b6 100644 --- a/src/web/WebApp.kt +++ b/src/web/WebApp.kt @@ -143,7 +143,8 @@ class WebApp(val listenPort: Int, val userlist: List>) { } "getStreamerOutputs" -> { - SendReply(wsMessageContext, cmd.command, objectmapper.writeValueAsString(StreamerOutputs.values.toList())) + val reply : List = StreamerOutputs.map { so -> StreamerOutputData.fromBarixConnection(so.value) } + SendReply(wsMessageContext, cmd.command, objectmapper.writeValueAsString(reply)) } else -> {