Spaces:
Sleeping
Sleeping
Update static/js/app.js
Browse files- static/js/app.js +36 -45
static/js/app.js
CHANGED
|
@@ -84,7 +84,7 @@ DOMElements.contexts = DOMElements.canvases.map(canvas =>
|
|
| 84 |
function updateDateTime() {
|
| 85 |
if (!DOMElements.datetime) return;
|
| 86 |
|
| 87 |
-
DOMElements.datetime.textContent = new Date().toLocaleString('
|
| 88 |
dateStyle: 'full',
|
| 89 |
timeStyle: 'short'
|
| 90 |
});
|
|
@@ -93,7 +93,7 @@ function updateDateTime() {
|
|
| 93 |
function addLog(message, type = 'info') {
|
| 94 |
if (!DOMElements.systemLog) return;
|
| 95 |
|
| 96 |
-
const timestamp = new Date().toLocaleTimeString('
|
| 97 |
const typeClass = {
|
| 98 |
info: 'text-slate-500',
|
| 99 |
warning: 'text-amber-600',
|
|
@@ -160,12 +160,12 @@ function displayAudioResults(data) {
|
|
| 160 |
}
|
| 161 |
|
| 162 |
if (data.status === "analyzing") {
|
| 163 |
-
DOMElements.vocalizationContent.innerHTML = `<p class="text-sm text-amber-600">
|
| 164 |
return;
|
| 165 |
}
|
| 166 |
|
| 167 |
if (data.status === "no_data" || data.prediction === null) {
|
| 168 |
-
DOMElements.vocalizationContent.innerHTML = `<p class="text-sm text-slate-500">
|
| 169 |
return;
|
| 170 |
}
|
| 171 |
|
|
@@ -176,9 +176,9 @@ function displayAudioResults(data) {
|
|
| 176 |
|
| 177 |
const probabilities = data.probabilities || {};
|
| 178 |
const statusMap = {
|
| 179 |
-
'Healthy': { text: '
|
| 180 |
-
'Unhealthy': { text: '
|
| 181 |
-
'Noise': { text: '
|
| 182 |
};
|
| 183 |
|
| 184 |
const dominantStatus = statusMap[data.prediction] || {
|
|
@@ -186,14 +186,10 @@ function displayAudioResults(data) {
|
|
| 186 |
color: 'slate'
|
| 187 |
};
|
| 188 |
|
| 189 |
-
if(
|
| 190 |
-
(
|
| 191 |
-
state.lastVocalization !== "Unhealthy"
|
| 192 |
-
){
|
| 193 |
-
addLog("Status Vokal: <strong>Tidak Sehat Terdeteksi</strong>", "danger");
|
| 194 |
state.lastVocalization = "Unhealthy";
|
| 195 |
-
}
|
| 196 |
-
else if (data.prediction === "Healthy"){
|
| 197 |
state.lastVocalization = "Healthy";
|
| 198 |
}
|
| 199 |
|
|
@@ -217,13 +213,11 @@ function displayAudioResults(data) {
|
|
| 217 |
|
| 218 |
DOMElements.vocalizationContent.innerHTML = `
|
| 219 |
<div class="flex items-center justify-between mb-3">
|
| 220 |
-
<span class="text-slate-500 text-xs">Status
|
| 221 |
<span class="font-bold text-base text-${dominantStatus.color}-600">${dominantStatus.text}</span>
|
| 222 |
</div>
|
| 223 |
<div class="space-y-2">${barsHtml}</div>
|
| 224 |
`;
|
| 225 |
-
|
| 226 |
-
|
| 227 |
}
|
| 228 |
|
| 229 |
// --- Modal function ---
|
|
@@ -265,7 +259,7 @@ function loadSettingsFromStorage() {
|
|
| 265 |
if (savedAudioUrl) {
|
| 266 |
state.audioUrl = savedAudioUrl;
|
| 267 |
}
|
| 268 |
-
addLog('Settings
|
| 269 |
}
|
| 270 |
|
| 271 |
// --- Websocket Streaming functions ---
|
|
@@ -286,7 +280,7 @@ function connectWebSocket(cameraIndex) {
|
|
| 286 |
state.cameraWebSockets[cameraIndex] = ws;
|
| 287 |
|
| 288 |
ws.onopen = () => {
|
| 289 |
-
addLog(`
|
| 290 |
|
| 291 |
ws.send(JSON.stringify({
|
| 292 |
type: 'start_stream',
|
|
@@ -310,7 +304,7 @@ function connectWebSocket(cameraIndex) {
|
|
| 310 |
};
|
| 311 |
|
| 312 |
ws.onclose = () => {
|
| 313 |
-
addLog(`
|
| 314 |
|
| 315 |
const ctx = DOMElements.contexts[cameraIndex];
|
| 316 |
if (ctx) ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
@@ -321,17 +315,17 @@ function connectWebSocket(cameraIndex) {
|
|
| 321 |
|
| 322 |
state.isStreaming = state.cameraWebSockets.some(ws => ws !== null);
|
| 323 |
if (!state.isStreaming) {
|
| 324 |
-
addLog("
|
| 325 |
}
|
| 326 |
};
|
| 327 |
|
| 328 |
ws.onerror = (error) => {
|
| 329 |
console.error(`WebSocket Error for Camera ${cameraIndex + 1}:`, error);
|
| 330 |
-
addLog(`
|
| 331 |
};
|
| 332 |
} catch (error) {
|
| 333 |
console.error(`Failed to create WebSocket for Camera ${cameraIndex + 1}:`, error);
|
| 334 |
-
addLog(`
|
| 335 |
}
|
| 336 |
}
|
| 337 |
|
|
@@ -380,8 +374,7 @@ function handleTextMessage(data, cameraIndex) {
|
|
| 380 |
const hasAlerted = state.lastInactiveAlert[cameraId];
|
| 381 |
|
| 382 |
if (isAboveThreshold && !hasAlerted){
|
| 383 |
-
const message = `Camera ${cameraId}: <strong>
|
| 384 |
-
|
| 385 |
addLog(message, "danger");
|
| 386 |
state.lastInactiveAlert[cameraId] = true;
|
| 387 |
}
|
|
@@ -403,7 +396,7 @@ function handleTextMessage(data, cameraIndex) {
|
|
| 403 |
}
|
| 404 |
|
| 405 |
function startAllStreams() {
|
| 406 |
-
addLog("
|
| 407 |
|
| 408 |
for (let i = 0; i < 4; i++) {
|
| 409 |
setTimeout(() => connectWebSocket(i), i * 100);
|
|
@@ -419,7 +412,7 @@ function startAllStreams() {
|
|
| 419 |
}
|
| 420 |
|
| 421 |
function stopAllStreams() {
|
| 422 |
-
addLog("
|
| 423 |
|
| 424 |
state.cameraWebSockets.forEach((ws, index) => {
|
| 425 |
if (ws) {
|
|
@@ -459,20 +452,19 @@ async function fetchLatestAudioResult() {
|
|
| 459 |
state.lastAudioResultKey = resultKey;
|
| 460 |
|
| 461 |
if (wasAnalyzing){
|
| 462 |
-
addLog("
|
| 463 |
state.isAnalyzingAudio = false;
|
| 464 |
state.lastAnalysisTimestamp = new Date();
|
| 465 |
}else{
|
| 466 |
-
addLog("
|
| 467 |
}
|
| 468 |
|
| 469 |
-
|
| 470 |
displayAudioResults(audioData);
|
| 471 |
} catch (error) {
|
| 472 |
console.error("Error fetching audio result:", error);
|
| 473 |
|
| 474 |
if (state.isAnalyzingAudio){
|
| 475 |
-
addLog("
|
| 476 |
state.isAnalyzingAudio = false;
|
| 477 |
}
|
| 478 |
}
|
|
@@ -484,7 +476,7 @@ async function downloadCSV() {
|
|
| 484 |
const cameraId = DOMElements.cameraSelect.value;
|
| 485 |
|
| 486 |
if (!start || !end) {
|
| 487 |
-
addLog("
|
| 488 |
return;
|
| 489 |
}
|
| 490 |
|
|
@@ -536,13 +528,14 @@ function handleToggleControlClick(e) {
|
|
| 536 |
updateToggleButtons();
|
| 537 |
|
| 538 |
const labelMap = {
|
| 539 |
-
show_detected: '
|
| 540 |
-
show_density: '
|
| 541 |
-
show_inactive: '
|
| 542 |
};
|
| 543 |
|
| 544 |
const displayName = labelMap[control] || control;
|
| 545 |
-
|
|
|
|
| 546 |
|
| 547 |
const payload = JSON.stringify({
|
| 548 |
type: 'display_settings_update',
|
|
@@ -559,7 +552,7 @@ function handleToggleControlClick(e) {
|
|
| 559 |
}
|
| 560 |
|
| 561 |
function handleSettingsButtonClick() {
|
| 562 |
-
addLog('Tip: Double-click
|
| 563 |
}
|
| 564 |
|
| 565 |
function handleSettingsButtonDoubleClick() {
|
|
@@ -577,13 +570,13 @@ function handleSaveSettingsClick() {
|
|
| 577 |
try {
|
| 578 |
localStorage.setItem('chickSenseCameraUrls', JSON.stringify(state.cameraUrls));
|
| 579 |
localStorage.setItem('chickSenseAudioUrl', state.audioUrl);
|
| 580 |
-
addLog('
|
| 581 |
} catch (e) {
|
| 582 |
console.error("Failed to save settings to local storage:", e);
|
| 583 |
-
addLog('
|
| 584 |
}
|
| 585 |
|
| 586 |
-
addLog('
|
| 587 |
closeModal(DOMElements.settingsModal);
|
| 588 |
|
| 589 |
stopAllStreams();
|
|
@@ -596,7 +589,7 @@ function handleStopStreamsClick() {
|
|
| 596 |
}
|
| 597 |
|
| 598 |
function handleExportButtonClick() {
|
| 599 |
-
addLog('Tip: Double-click
|
| 600 |
}
|
| 601 |
|
| 602 |
function handleExportButtonDoubleClick() {
|
|
@@ -669,7 +662,7 @@ function initialize() {
|
|
| 669 |
|
| 670 |
setInterval(updateDateTime, 30000);
|
| 671 |
|
| 672 |
-
addLog('
|
| 673 |
startAllStreams();
|
| 674 |
}
|
| 675 |
|
|
@@ -677,6 +670,4 @@ if (document.readyState === 'loading') {
|
|
| 677 |
document.addEventListener('DOMContentLoaded', initialize);
|
| 678 |
} else {
|
| 679 |
initialize();
|
| 680 |
-
}
|
| 681 |
-
|
| 682 |
-
|
|
|
|
| 84 |
function updateDateTime() {
|
| 85 |
if (!DOMElements.datetime) return;
|
| 86 |
|
| 87 |
+
DOMElements.datetime.textContent = new Date().toLocaleString('en-US', {
|
| 88 |
dateStyle: 'full',
|
| 89 |
timeStyle: 'short'
|
| 90 |
});
|
|
|
|
| 93 |
function addLog(message, type = 'info') {
|
| 94 |
if (!DOMElements.systemLog) return;
|
| 95 |
|
| 96 |
+
const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
|
| 97 |
const typeClass = {
|
| 98 |
info: 'text-slate-500',
|
| 99 |
warning: 'text-amber-600',
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
if (data.status === "analyzing") {
|
| 163 |
+
DOMElements.vocalizationContent.innerHTML = `<p class="text-sm text-amber-600">Vocalization analysis in progress...</p>`;
|
| 164 |
return;
|
| 165 |
}
|
| 166 |
|
| 167 |
if (data.status === "no_data" || data.prediction === null) {
|
| 168 |
+
DOMElements.vocalizationContent.innerHTML = `<p class="text-sm text-slate-500">Waiting for vocalization analysis to run...</p>`;
|
| 169 |
return;
|
| 170 |
}
|
| 171 |
|
|
|
|
| 176 |
|
| 177 |
const probabilities = data.probabilities || {};
|
| 178 |
const statusMap = {
|
| 179 |
+
'Healthy': { text: 'Healthy', color: 'green' },
|
| 180 |
+
'Unhealthy': { text: 'Unhealthy', color: 'red' },
|
| 181 |
+
'Noise': { text: 'Noise', color: 'amber' },
|
| 182 |
};
|
| 183 |
|
| 184 |
const dominantStatus = statusMap[data.prediction] || {
|
|
|
|
| 186 |
color: 'slate'
|
| 187 |
};
|
| 188 |
|
| 189 |
+
if (data.prediction === "Unhealthy" && state.lastVocalization !== "Unhealthy") {
|
| 190 |
+
addLog("Vocalization Status: <strong>Unhealthy Detected</strong>", "danger");
|
|
|
|
|
|
|
|
|
|
| 191 |
state.lastVocalization = "Unhealthy";
|
| 192 |
+
} else if (data.prediction === "Healthy") {
|
|
|
|
| 193 |
state.lastVocalization = "Healthy";
|
| 194 |
}
|
| 195 |
|
|
|
|
| 213 |
|
| 214 |
DOMElements.vocalizationContent.innerHTML = `
|
| 215 |
<div class="flex items-center justify-between mb-3">
|
| 216 |
+
<span class="text-slate-500 text-xs">Dominant Status:</span>
|
| 217 |
<span class="font-bold text-base text-${dominantStatus.color}-600">${dominantStatus.text}</span>
|
| 218 |
</div>
|
| 219 |
<div class="space-y-2">${barsHtml}</div>
|
| 220 |
`;
|
|
|
|
|
|
|
| 221 |
}
|
| 222 |
|
| 223 |
// --- Modal function ---
|
|
|
|
| 259 |
if (savedAudioUrl) {
|
| 260 |
state.audioUrl = savedAudioUrl;
|
| 261 |
}
|
| 262 |
+
addLog('Settings loaded from browser storage', 'info');
|
| 263 |
}
|
| 264 |
|
| 265 |
// --- Websocket Streaming functions ---
|
|
|
|
| 280 |
state.cameraWebSockets[cameraIndex] = ws;
|
| 281 |
|
| 282 |
ws.onopen = () => {
|
| 283 |
+
addLog(`Connecting to Camera ${cameraIndex + 1}`, 'info');
|
| 284 |
|
| 285 |
ws.send(JSON.stringify({
|
| 286 |
type: 'start_stream',
|
|
|
|
| 304 |
};
|
| 305 |
|
| 306 |
ws.onclose = () => {
|
| 307 |
+
addLog(`Camera ${cameraIndex + 1} disconnected.`, 'warning');
|
| 308 |
|
| 309 |
const ctx = DOMElements.contexts[cameraIndex];
|
| 310 |
if (ctx) ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
|
|
| 315 |
|
| 316 |
state.isStreaming = state.cameraWebSockets.some(ws => ws !== null);
|
| 317 |
if (!state.isStreaming) {
|
| 318 |
+
addLog("All streams disconnected.", 'danger');
|
| 319 |
}
|
| 320 |
};
|
| 321 |
|
| 322 |
ws.onerror = (error) => {
|
| 323 |
console.error(`WebSocket Error for Camera ${cameraIndex + 1}:`, error);
|
| 324 |
+
addLog(`Connection failed for Camera ${cameraIndex + 1}.`, 'danger');
|
| 325 |
};
|
| 326 |
} catch (error) {
|
| 327 |
console.error(`Failed to create WebSocket for Camera ${cameraIndex + 1}:`, error);
|
| 328 |
+
addLog(`Failed to connect to Camera ${cameraIndex + 1}.`, 'danger');
|
| 329 |
}
|
| 330 |
}
|
| 331 |
|
|
|
|
| 374 |
const hasAlerted = state.lastInactiveAlert[cameraId];
|
| 375 |
|
| 376 |
if (isAboveThreshold && !hasAlerted){
|
| 377 |
+
const message = `Camera ${cameraId}: <strong> Inactive chicken percentage is high (${percent}%) </strong>`;
|
|
|
|
| 378 |
addLog(message, "danger");
|
| 379 |
state.lastInactiveAlert[cameraId] = true;
|
| 380 |
}
|
|
|
|
| 396 |
}
|
| 397 |
|
| 398 |
function startAllStreams() {
|
| 399 |
+
addLog("Attempting to start all configured streams...", 'info');
|
| 400 |
|
| 401 |
for (let i = 0; i < 4; i++) {
|
| 402 |
setTimeout(() => connectWebSocket(i), i * 100);
|
|
|
|
| 412 |
}
|
| 413 |
|
| 414 |
function stopAllStreams() {
|
| 415 |
+
addLog("Stopping all streams...", 'warning');
|
| 416 |
|
| 417 |
state.cameraWebSockets.forEach((ws, index) => {
|
| 418 |
if (ws) {
|
|
|
|
| 452 |
state.lastAudioResultKey = resultKey;
|
| 453 |
|
| 454 |
if (wasAnalyzing){
|
| 455 |
+
addLog("Vocalization results updated", "info");
|
| 456 |
state.isAnalyzingAudio = false;
|
| 457 |
state.lastAnalysisTimestamp = new Date();
|
| 458 |
}else{
|
| 459 |
+
addLog("Vocalization results available", "info");
|
| 460 |
}
|
| 461 |
|
|
|
|
| 462 |
displayAudioResults(audioData);
|
| 463 |
} catch (error) {
|
| 464 |
console.error("Error fetching audio result:", error);
|
| 465 |
|
| 466 |
if (state.isAnalyzingAudio){
|
| 467 |
+
addLog("Audio analysis failed", "danger");
|
| 468 |
state.isAnalyzingAudio = false;
|
| 469 |
}
|
| 470 |
}
|
|
|
|
| 476 |
const cameraId = DOMElements.cameraSelect.value;
|
| 477 |
|
| 478 |
if (!start || !end) {
|
| 479 |
+
addLog("Please select a start and end date for export.", 'warning');
|
| 480 |
return;
|
| 481 |
}
|
| 482 |
|
|
|
|
| 528 |
updateToggleButtons();
|
| 529 |
|
| 530 |
const labelMap = {
|
| 531 |
+
show_detected: 'Detection',
|
| 532 |
+
show_density: 'Density',
|
| 533 |
+
show_inactive: 'Inactivity'
|
| 534 |
};
|
| 535 |
|
| 536 |
const displayName = labelMap[control] || control;
|
| 537 |
+
const status = state[control] ? 'enabled' : 'disabled';
|
| 538 |
+
addLog(`View updated: ${displayName} ${status}`, 'info');
|
| 539 |
|
| 540 |
const payload = JSON.stringify({
|
| 541 |
type: 'display_settings_update',
|
|
|
|
| 552 |
}
|
| 553 |
|
| 554 |
function handleSettingsButtonClick() {
|
| 555 |
+
addLog('Tip: Double-click the gear icon to open camera settings.', 'info');
|
| 556 |
}
|
| 557 |
|
| 558 |
function handleSettingsButtonDoubleClick() {
|
|
|
|
| 570 |
try {
|
| 571 |
localStorage.setItem('chickSenseCameraUrls', JSON.stringify(state.cameraUrls));
|
| 572 |
localStorage.setItem('chickSenseAudioUrl', state.audioUrl);
|
| 573 |
+
addLog('Settings saved to browser storage.', 'info');
|
| 574 |
} catch (e) {
|
| 575 |
console.error("Failed to save settings to local storage:", e);
|
| 576 |
+
addLog('Could not save settings.', 'danger');
|
| 577 |
}
|
| 578 |
|
| 579 |
+
addLog('Settings saved. Restarting streams...', 'info');
|
| 580 |
closeModal(DOMElements.settingsModal);
|
| 581 |
|
| 582 |
stopAllStreams();
|
|
|
|
| 589 |
}
|
| 590 |
|
| 591 |
function handleExportButtonClick() {
|
| 592 |
+
addLog('Tip: Double-click "Export CSV" to export metrics to a CSV file.', 'info');
|
| 593 |
}
|
| 594 |
|
| 595 |
function handleExportButtonDoubleClick() {
|
|
|
|
| 662 |
|
| 663 |
setInterval(updateDateTime, 30000);
|
| 664 |
|
| 665 |
+
addLog('System initialized. Welcome! IKGC', 'info');
|
| 666 |
startAllStreams();
|
| 667 |
}
|
| 668 |
|
|
|
|
| 670 |
document.addEventListener('DOMContentLoaded', initialize);
|
| 671 |
} else {
|
| 672 |
initialize();
|
| 673 |
+
}
|
|
|
|
|
|