diff --git a/backend/routes/api.js b/backend/routes/api.js index 7cbe0f5..a3e7ebb 100644 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -909,6 +909,83 @@ router.post("/setTaskSettings", async (req, res) => { } }); +// Get Activity Monitor Polling Settings +router.get("/getActivityMonitorSettings", async (req, res) => { + try { + const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows); + + if (settingsjson.length > 0) { + const settings = settingsjson[0].settings || {}; + console.log(settings); + const pollingSettings = settings.ActivityMonitorPolling || { + activeSessionsInterval: 1000, + idleInterval: 5000 + }; + res.send(pollingSettings); + } else { + res.status(404); + res.send({ error: "Settings Not Found" }); + } + } catch (error) { + res.status(503); + res.send({ error: "Error: " + error }); + } +}); + +// Set Activity Monitor Polling Settings +router.post("/setActivityMonitorSettings", async (req, res) => { + const { activeSessionsInterval, idleInterval } = req.body; + + if (activeSessionsInterval === undefined || idleInterval === undefined) { + res.status(400); + res.send("activeSessionsInterval and idleInterval are required"); + return; + } + + if (!Number.isInteger(activeSessionsInterval) || activeSessionsInterval <= 0) { + res.status(400); + res.send("A valid activeSessionsInterval(int) which is > 0 milliseconds is required"); + return; + } + + if (!Number.isInteger(idleInterval) || idleInterval <= 0) { + res.status(400); + res.send("A valid idleInterval(int) which is > 0 milliseconds is required"); + return; + } + + if (activeSessionsInterval > idleInterval) { + res.status(400); + res.send("activeSessionsInterval should be <= idleInterval for optimal performance"); + return; + } + + try { + const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows); + + if (settingsjson.length > 0) { + const settings = settingsjson[0].settings || {}; + + settings.ActivityMonitorPolling = { + activeSessionsInterval: activeSessionsInterval, + idleInterval: idleInterval + }; + + let query = 'UPDATE app_config SET settings=$1 where "ID"=1'; + await db.query(query, [settings]); + + res.status(200); + res.send(settings.ActivityMonitorPolling); + } else { + res.status(404); + res.send({ error: "Settings Not Found" }); + } + } catch (error) { + res.status(503); + res.send({ error: "Error: " + error }); + } +}); + //Jellystat functions router.get("/CheckForUpdates", async (req, res) => { try { diff --git a/backend/tasks/ActivityMonitor.js b/backend/tasks/ActivityMonitor.js index 43f1969..62c903b 100644 --- a/backend/tasks/ActivityMonitor.js +++ b/backend/tasks/ActivityMonitor.js @@ -114,20 +114,70 @@ function getWatchDogNotInSessions(SessionData, WatchdogData) { return removedData; } -async function ActivityMonitor(interval) { - // console.log("Activity Interval: " + interval); +let currentIntervalId = null; +let lastHadActiveSessions = false; +let cachedPollingSettings = { + activeSessionsInterval: 1000, + idleInterval: 5000 +}; - setInterval(async () => { +async function ActivityMonitor(defaultInterval) { + // console.log("Activity Monitor started with default interval: " + defaultInterval); + + const runMonitoring = async () => { try { const config = await new configClass().getConfig(); if (config.error || config.state !== 2) { return; } + + // Get adaptive polling settings from config + const pollingSettings = config.settings?.ActivityMonitorPolling || { + activeSessionsInterval: 1000, + idleInterval: 5000 + }; + + // Check if polling settings have changed + const settingsChanged = + cachedPollingSettings.activeSessionsInterval !== pollingSettings.activeSessionsInterval || + cachedPollingSettings.idleInterval !== pollingSettings.idleInterval; + + if (settingsChanged) { + console.log('[ActivityMonitor] Polling settings changed, updating intervals'); + console.log('Old settings:', cachedPollingSettings); + console.log('New settings:', pollingSettings); + cachedPollingSettings = { ...pollingSettings }; + } + const ExcludedUsers = config.settings?.ExcludedUsers || []; const apiSessionData = await API.getSessions(); const SessionData = apiSessionData.filter((row) => row.NowPlayingItem !== undefined && !ExcludedUsers.includes(row.UserId)); sendUpdate("sessions", apiSessionData); + + const hasActiveSessions = SessionData.length > 0; + + // Determine current appropriate interval + const currentInterval = hasActiveSessions ? pollingSettings.activeSessionsInterval : pollingSettings.idleInterval; + + // Check if we need to change the interval (either due to session state change OR settings change) + if (hasActiveSessions !== lastHadActiveSessions || settingsChanged) { + if (hasActiveSessions !== lastHadActiveSessions) { + console.log(`[ActivityMonitor] Switching to ${hasActiveSessions ? 'active' : 'idle'} polling mode (${currentInterval}ms)`); + lastHadActiveSessions = hasActiveSessions; + } + if (settingsChanged) { + console.log(`[ActivityMonitor] Applying new ${hasActiveSessions ? 'active' : 'idle'} interval: ${currentInterval}ms`); + } + + // Clear current interval and restart with new timing + if (currentIntervalId) { + clearInterval(currentIntervalId); + } + currentIntervalId = setInterval(runMonitoring, currentInterval); + return; // Let the new interval handle the next execution + } + /////get data from jf_activity_monitor const WatchdogData = await db.query("SELECT * FROM jf_activity_watchdog").then((res) => res.rows); @@ -258,7 +308,50 @@ async function ActivityMonitor(interval) { } return []; } - }, interval); + }; + + // Get initial configuration to start with the correct interval + const initConfig = async () => { + try { + const config = await new configClass().getConfig(); + + if (config.error || config.state !== 2) { + console.log("[ActivityMonitor] Config not ready, starting with default interval:", defaultInterval + "ms"); + currentIntervalId = setInterval(runMonitoring, defaultInterval); + return; + } + + // Get adaptive polling settings from config + const pollingSettings = config.settings?.ActivityMonitorPolling || { + activeSessionsInterval: 1000, + idleInterval: 5000 + }; + + // Initialize cached settings + cachedPollingSettings = { ...pollingSettings }; + + // Start with idle interval since there are likely no active sessions at startup + const initialInterval = pollingSettings.idleInterval; + console.log("[ActivityMonitor] Starting adaptive polling with idle interval:", initialInterval + "ms"); + console.log("[ActivityMonitor] Loaded settings:", pollingSettings); + currentIntervalId = setInterval(runMonitoring, initialInterval); + + } catch (error) { + console.log("[ActivityMonitor] Error loading config, using default interval:", defaultInterval + "ms"); + currentIntervalId = setInterval(runMonitoring, defaultInterval); + } + }; + + // Initialize with proper configuration + await initConfig(); + + // Return a cleanup function + return () => { + if (currentIntervalId) { + clearInterval(currentIntervalId); + currentIntervalId = null; + } + }; } module.exports = { diff --git a/public/locales/en-UK/translation.json b/public/locales/en-UK/translation.json index 4b9357b..e524200 100644 --- a/public/locales/en-UK/translation.json +++ b/public/locales/en-UK/translation.json @@ -211,6 +211,15 @@ "1_DAY": "1 Day", "1_WEEK": "1 Week" }, + "ACTIVITY_MONITOR": "Activity Monitor", + "ACTIVE_SESSIONS_INTERVAL": "Active Sessions Interval (ms)", + "ACTIVE_SESSIONS_HELP": "How often to check when users are watching content (recommended: 1000ms)", + "IDLE_INTERVAL": "Idle Interval (ms)", + "IDLE_HELP": "How often to check when no active sessions (recommended: 5000ms)", + "POLLING_INFO_TITLE": "Smart Polling", + "POLLING_INFO": "The system automatically adapts monitoring frequency: fast when users are watching content, slower when the server is idle. This reduces CPU load on your Jellyfin server.", + "INTERVAL_WARNING": "Active sessions interval should not be greater than idle interval", + "REALTIME_UPDATE_INFO": "Changes are applied in real-time without server restart.", "SELECT_LIBRARIES_TO_IMPORT": "Select Libraries to Import", "SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "Activity for Items within these libraries are still Tracked - Even when not imported.", "DATE_ADDED": "Date Added" diff --git a/public/locales/fr-FR/translation.json b/public/locales/fr-FR/translation.json index 78929ef..6047ca7 100644 --- a/public/locales/fr-FR/translation.json +++ b/public/locales/fr-FR/translation.json @@ -211,6 +211,15 @@ "1_DAY": "1 Jour", "1_WEEK": "1 Semaine" }, + "ACTIVITY_MONITOR": "Surveillance d'activité", + "ACTIVE_SESSIONS_INTERVAL": "Intervalle avec sessions actives (ms)", + "ACTIVE_SESSIONS_HELP": "Fréquence de vérification quand des utilisateurs regardent du contenu (recommandé: 1000ms)", + "IDLE_INTERVAL": "Intervalle en veille (ms)", + "IDLE_HELP": "Fréquence de vérification quand aucune session active (recommandé: 5000ms)", + "POLLING_INFO_TITLE": "Polling intelligent", + "POLLING_INFO": "Le système adapte automatiquement la fréquence de surveillance : rapide quand des utilisateurs regardent du contenu, plus lent quand le serveur est inactif. Cela réduit la charge CPU sur votre serveur Jellyfin.", + "INTERVAL_WARNING": "L'intervalle actif ne devrait pas être supérieur à l'intervalle de veille", + "REALTIME_UPDATE_INFO": "Les modifications sont appliquées en temps réel sans redémarrage du serveur.", "SELECT_LIBRARIES_TO_IMPORT": "Sélectionner les médiathèques à importer", "SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "L'activité du contenu de ces médiathèques est toujours suivie, même s'ils ne sont pas importés.", "DATE_ADDED": "Date d'ajout" diff --git a/src/pages/components/settings/ActivityMonitorSettings.jsx b/src/pages/components/settings/ActivityMonitorSettings.jsx new file mode 100644 index 0000000..7ad3992 --- /dev/null +++ b/src/pages/components/settings/ActivityMonitorSettings.jsx @@ -0,0 +1,175 @@ +import { useState, useEffect } from "react"; +import axios from "../../../lib/axios_instance"; +import { Row, Col, Form, Button, Alert } from "react-bootstrap"; +import { Trans } from "react-i18next"; +import Loading from "../general/loading"; + +export default function ActivityMonitorSettings() { + const [settings, setSettings] = useState({ + activeSessionsInterval: 1000, + idleInterval: 5000 + }); + const [isSubmitted, setIsSubmitted] = useState(""); + const [submissionMessage, setSubmissionMessage] = useState(""); + const [loading, setLoading] = useState(true); + const token = localStorage.getItem("token"); + + useEffect(() => { + fetchSettings(); + }, []); + + const fetchSettings = async () => { + try { + const response = await axios.get("/api/getActivityMonitorSettings", { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + setSettings(response.data); + setLoading(false); + } catch (error) { + console.error("Error fetching Activity Monitor settings:", error); + setLoading(false); + } + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setSettings(prev => ({ + ...prev, + [name]: parseInt(value, 10) + })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setIsSubmitted(""); + setSubmissionMessage(""); + + try { + await axios.post("/api/setActivityMonitorSettings", settings, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + setIsSubmitted("Success"); + setSubmissionMessage("Paramètres de surveillance mis à jour avec succès - Appliqués en temps réel"); + } catch (error) { + console.error("Error updating Activity Monitor settings:", error); + setIsSubmitted("Failed"); + setSubmissionMessage( + error.response?.data || "Erreur lors de la mise à jour des paramètres" + ); + } + }; + + if (loading) { + return ; + } + + return ( +
+

+ +

+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + {settings.activeSessionsInterval > settings.idleInterval && ( + + + + )} + + {isSubmitted !== "" ? ( + isSubmitted === "Failed" ? ( + + {submissionMessage} + + ) : ( + + {submissionMessage} + + ) + ) : ( + <> + )} + +
+ +
+
+
+ ); +} diff --git a/src/pages/settings.jsx b/src/pages/settings.jsx index d96a224..03b39d5 100644 --- a/src/pages/settings.jsx +++ b/src/pages/settings.jsx @@ -6,6 +6,7 @@ import Tasks from "./components/settings/Tasks"; import SecuritySettings from "./components/settings/security"; import ApiKeys from "./components/settings/apiKeys"; import LibrarySelector from "./library_selector"; +import ActivityMonitorSettings from "./components/settings/ActivityMonitorSettings"; import Logs from "./components/settings/logs"; @@ -32,6 +33,7 @@ export default function Settings() { > +