From 63cef590d6d3b5100168a20c1bacdfb765e5a611 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Thu, 26 Feb 2026 04:21:29 +0000 Subject: [PATCH] Refactor network API calls to use centralized authentication context and improve cache handling - Removed redundant getApiToken function and replaced its usage with getAuthContext in network-api.js, network-events.js, and network-init.js. - Updated cache handling in network-events.js and network-init.js to use CACHE_KEYS constants for better maintainability. - Introduced cache.js for centralized cache management functions and constants, including cache initialization and retrieval. - Added app-init.js for application lifecycle management, including cache orchestration and initialization checks. - Created app_config.php to securely fetch API token and GraphQL port from configuration. - Improved error handling and logging throughout the codebase for better debugging and maintenance. --- front/deviceDetails.php | 33 +- front/deviceDetailsTools.php | 8 +- front/js/app-init.js | 281 ++++++++++++++ front/js/cache.js | 538 ++++++++++++++++++++++++++ front/js/common.js | 646 +------------------------------- front/js/network-api.js | 25 +- front/js/network-events.js | 8 +- front/js/network-init.js | 11 +- front/multiEditCore.php | 4 +- front/php/server/app_config.php | 38 ++ front/php/templates/header.php | 3 + front/php/templates/version.php | 2 +- 12 files changed, 915 insertions(+), 682 deletions(-) create mode 100644 front/js/app-init.js create mode 100644 front/js/cache.js create mode 100644 front/php/server/app_config.php diff --git a/front/deviceDetails.php b/front/deviceDetails.php index 7c9f3e2d..aa89349b 100755 --- a/front/deviceDetails.php +++ b/front/deviceDetails.php @@ -419,7 +419,12 @@ async function renderSmallBoxes() { const apiToken = getSetting("API_TOKEN"); const apiBaseUrl = getApiBase(); - const url = `${apiBaseUrl}/device/${getMac()}?period=${encodeURIComponent(period)}`; + // Ensure period is a string, not an element + let periodValue = period; + if (typeof period === 'object' && period !== null && 'value' in period) { + periodValue = period.value; + } + const url = `${apiBaseUrl}/device/${getMac()}?period=${encodeURIComponent(periodValue)}`; const response = await fetch(url, { method: "GET", @@ -553,20 +558,24 @@ function updateDevicePageName(mac) { //----------------------------------------------------------------------------------- -// Call renderSmallBoxes, then main -(async () => { - await renderSmallBoxes(); - main(); - })(); -window.onload = function async() -{ - mac = getMac() - // initializeTabs(); - updateChevrons(mac); - updateDevicePageName(mac); +window.onload = function() { + // Always trigger app-init bootstrap + if (typeof executeOnce === 'function') { + executeOnce(); + } + + mac = getMac(); + + // Wait for app initialization (cache populated) before using cached data + callAfterAppInitialized(async () => { + updateDevicePageName(mac); + updateChevrons(mac); + await renderSmallBoxes(); + main(); + }); } diff --git a/front/deviceDetailsTools.php b/front/deviceDetailsTools.php index 404c8349..cbe6ebf2 100755 --- a/front/deviceDetailsTools.php +++ b/front/deviceDetailsTools.php @@ -526,13 +526,7 @@ function getVisibleDevicesList() { // Read cache (skip cookie expiry check) - devicesList = getCache('devicesListAll_JSON', true); - - if (devicesList != '') { - devicesList = JSON.parse (devicesList); - } else { - devicesList = []; - } + devicesList = parseDeviceCache(getCache('devicesListAll_JSON', true)); // only loop thru the filtered down list visibleDevices = getCache("ntx_visible_macs") diff --git a/front/js/app-init.js b/front/js/app-init.js new file mode 100644 index 00000000..5da09560 --- /dev/null +++ b/front/js/app-init.js @@ -0,0 +1,281 @@ +/* ----------------------------------------------------------------------------- + * NetAlertX + * Open Source Network Guard / WIFI & LAN intrusion detector + * + * app-init.js - Front module. Application lifecycle: initialization, + * cache orchestration, and startup sequencing. + * Loaded AFTER common.js — depends on showSpinner(), isEmpty(), + * mergeUniqueArrays(), getSetting(), getString(), getCache(), + * setCache(), and all cache* functions from cache.js. + *------------------------------------------------------------------------------- + # jokob@duck.com GNU GPLv3 + ----------------------------------------------------------------------------- */ + +// ----------------------------------------------------------------------------- +// initialize +// ----------------------------------------------------------------------------- + +var completedCalls = [] +var completedCalls_final = ['cacheApiConfig', 'cacheSettings', 'cacheStrings', 'cacheDevices']; +var lang_completedCalls = 0; + + +// ----------------------------------------------------------------------------- +// Clearing all the caches +function clearCache() { + showSpinner(); + sessionStorage.clear(); + localStorage.clear(); + // Wait for spinner to show and cache to clear, then reload + setTimeout(() => { + console.warn("clearCache called"); + window.location.reload(); + }, 100); +} + +// =================================================================== +// DEPRECATED: checkSettingChanges() - Replaced by SSE-based manager +// Settings changes are now handled via SSE events +// Kept for backward compatibility, will be removed in future version +// =================================================================== +function checkSettingChanges() { + // SSE manager handles settings_changed events now + if (typeof netAlertXStateManager !== 'undefined' && netAlertXStateManager.initialized) { + return; // SSE handles this now + } + + // Fallback for backward compatibility + $.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now() }, function(appState) { + const importedMilliseconds = parseInt(appState["settingsImported"] * 1000); + const lastReloaded = parseInt(getCache(CACHE_KEYS.INIT_TIMESTAMP)); + + if (importedMilliseconds > lastReloaded) { + console.log("Cache needs to be refreshed because of setting changes"); + setTimeout(() => { + clearCache(); + }, 500); + } + }); +} + +// =================================================================== +// Display spinner and reload page if not yet initialized +async function handleFirstLoad(callback) { + if (!isAppInitialized()) { + await new Promise(resolve => setTimeout(resolve, 1000)); + callback(); + } +} + +// =================================================================== +// Execute callback once the app is initialized and GraphQL server is running +async function callAfterAppInitialized(callback) { + if (!isAppInitialized() || !(await isGraphQLServerRunning())) { + setTimeout(() => { + callAfterAppInitialized(callback); + }, 500); + } else { + callback(); + } +} + +// =================================================================== +// Polling function to repeatedly check if the server is running +async function waitForGraphQLServer() { + const pollInterval = 2000; // 2 seconds between each check + let serverRunning = false; + + while (!serverRunning) { + serverRunning = await isGraphQLServerRunning(); + if (!serverRunning) { + console.log("GraphQL server not running, retrying in 2 seconds..."); + await new Promise(resolve => setTimeout(resolve, pollInterval)); + } + } + + console.log("GraphQL server is now running."); +} + +// ----------------------------------------------------------------------------- +// Returns 1 if running, 0 otherwise +async function isGraphQLServerRunning() { + try { + const response = await $.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now()}); + console.log("graphQLServerStarted: " + response["graphQLServerStarted"]); + setCache(CACHE_KEYS.GRAPHQL_STARTED, response["graphQLServerStarted"]); + return response["graphQLServerStarted"]; + } catch (error) { + console.error("Failed to check GraphQL server status:", error); + return false; + } +} + +// Throttle isAppInitialized logging so the console isn't spammed on every poll. +let _isAppInit_lastLogTime = 0; +function _isAppInitLog(msg) { + const now = Date.now(); + if (now - _isAppInit_lastLogTime > 5000) { // log at most once per 5s + console.log(msg); + _isAppInit_lastLogTime = now; + } +} + +// ----------------------------------------------------------------------------- +// Check if the code has been executed before by checking localStorage +function isAppInitialized() { + + lang_shouldBeCompletedCalls = getLangCode() == 'en_us' ? 1 : 2; + + // check if each ajax call completed succesfully + for (const call_name of completedCalls_final) { + if (getCache(CACHE_KEYS.initFlag(call_name)) != "true") { + _isAppInitLog(`[isAppInitialized] waiting on ${call_name} (value: ${getCache(CACHE_KEYS.initFlag(call_name))})`); + return false; + } + } + + // check if all required languages chached + if(parseInt(getCache(CACHE_KEYS.STRINGS_COUNT)) != lang_shouldBeCompletedCalls) + { + _isAppInitLog(`[isAppInitialized] waiting on cacheStrings: ${getCache(CACHE_KEYS.STRINGS_COUNT)} of ${lang_shouldBeCompletedCalls}`); + return false; + } + + return true; +} + +// Retry a single async init step up to maxAttempts times with a delay. +async function retryStep(name, fn, maxAttempts = 3, delayMs = 1500) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await fn(); + return; // success + } catch (err) { + console.warn(`[executeOnce] ${name} failed (attempt ${attempt}/${maxAttempts}):`, err); + if (attempt < maxAttempts) { + await new Promise(r => setTimeout(r, delayMs)); + } else { + console.error(`[executeOnce] ${name} permanently failed after ${maxAttempts} attempts.`); + } + } + } +} + +// ----------------------------------------------------------------------------- +// Main execution logic +let _executeOnceRunning = false; +async function executeOnce() { + if (_executeOnceRunning) { + console.log('[executeOnce] Already running — skipping duplicate call.'); + return; + } + _executeOnceRunning = true; + showSpinner(); + + // Auto-bust stale cache if code version has changed since last init. + // Clears localStorage in-place so the subsequent init runs fresh without + // requiring a page reload. + if (getCache(CACHE_KEYS.CACHE_VERSION) !== NAX_CACHE_VERSION) { + console.log(`[executeOnce] Cache version mismatch (stored: "${getCache(CACHE_KEYS.CACHE_VERSION)}", expected: "${NAX_CACHE_VERSION}"). Clearing cache.`); + localStorage.clear(); + sessionStorage.clear(); + } + + if (!isAppInitialized()) { + try { + await waitForGraphQLServer(); // Wait for the server to start + + await retryStep('cacheApiConfig', cacheApiConfig); // Bootstrap: API_TOKEN + GRAPHQL_PORT from app.conf + await retryStep('cacheDevices', cacheDevices); + await retryStep('cacheSettings', cacheSettings); + await retryStep('cacheStrings', cacheStrings); + + console.log("All AJAX callbacks have completed"); + onAllCallsComplete(); + } finally { + _executeOnceRunning = false; + } + } else { + _executeOnceRunning = false; + } +} + + +// ----------------------------------------------------------------------------- +// Function to handle successful completion of an AJAX call +const handleSuccess = (callName) => { + console.log(`AJAX call successful: ${callName}`); + + if(callName.includes("cacheStrings")) + { + completed_tmp = getCache(CACHE_KEYS.STRINGS_COUNT); + completed_tmp == "" ? completed_tmp = 0 : completed_tmp = completed_tmp; + completed_tmp++; + setCache(CACHE_KEYS.STRINGS_COUNT, completed_tmp); + } + + setCache(CACHE_KEYS.initFlag(callName), true) +}; + +// ----------------------------------------------------------------------------- +// Function to handle failure of an AJAX call +const handleFailure = (callName, callback) => { + msg = `AJAX call ${callName} failed` + console.error(msg); + if (typeof callback === 'function') { + callback(new Error(msg)); + } +}; + +// ----------------------------------------------------------------------------- +// Function to execute when all AJAX calls have completed +const onAllCallsComplete = () => { + completedCalls = mergeUniqueArrays(getCache(CACHE_KEYS.COMPLETED_CALLS).split(','), completedCalls); + setCache(CACHE_KEYS.COMPLETED_CALLS, completedCalls); + + // Check if all necessary strings are initialized + if (areAllStringsInitialized()) { + const millisecondsNow = Date.now(); + setCache(CACHE_KEYS.INIT_TIMESTAMP, millisecondsNow); + setCache(CACHE_KEYS.CACHE_VERSION, NAX_CACHE_VERSION); + + console.log('✔ Cache initialized'); + + } else { + // If not all strings are initialized, retry initialization + console.log('❌ Not all strings are initialized. Retrying...'); + executeOnce(); + return; + } + + // Call any other initialization functions here if needed + +}; + +// Function to check if all necessary strings are initialized +const areAllStringsInitialized = () => { + // Implement logic to check if all necessary strings are initialized + // Return true if all strings are initialized, false otherwise + return getString('UI_LANG_name') != "" +}; + +// Call the function to execute the code +executeOnce(); + +// Set timer for regular UI refresh if enabled +setTimeout(() => { + + // page refresh if configured + const refreshTime = getSetting("UI_REFRESH"); + if (refreshTime && refreshTime !== "0" && refreshTime !== "") { + console.log("Refreshing page becasue UI_REFRESH setting enabled."); + newTimerRefreshData(clearCache, parseInt(refreshTime)*1000); + } + + // Check if page needs to refresh due to setting changes + checkSettingChanges() + +}, 10000); + + +console.log("init app-init.js"); diff --git a/front/js/cache.js b/front/js/cache.js new file mode 100644 index 00000000..84ce9b6e --- /dev/null +++ b/front/js/cache.js @@ -0,0 +1,538 @@ +/* ----------------------------------------------------------------------------- + * NetAlertX + * Open Source Network Guard / WIFI & LAN intrusion detector + * + * cache.js - Front module. Cache primitives, settings, strings, and device + * data caching. Loaded FIRST — no dependencies on other NAX files. + * All cross-file calls (handleSuccess, showSpinner, etc.) are + * call-time dependencies resolved after page load. + *------------------------------------------------------------------------------- + # jokob@duck.com GNU GPLv3 + ----------------------------------------------------------------------------- */ + +// Cache version stamp — injected by header.php from the app's .VERSION file. +// Changes automatically on every release, busting stale localStorage caches. +// Falls back to a build-time constant so local dev without PHP still works. +const NAX_CACHE_VERSION = (typeof window.NAX_APP_VERSION !== 'undefined') + ? window.NAX_APP_VERSION + : 'dev'; + +// ----------------------------------------------------------------------------- +// Central registry of all localStorage cache keys. +// Use these constants (and the helper functions for dynamic keys) everywhere +// instead of bare string literals to prevent silent typo bugs. +// ----------------------------------------------------------------------------- +const CACHE_KEYS = { + // --- Init flags (dynamic) --- + // Stores "true" when an AJAX init call completes. Use initFlag(name) below. + initFlag: (name) => `${name}_completed`, + + // --- Settings --- + // Stores the value of a setting by its setKey. nax_set_ + setting: (key) => `nax_set_${key}`, + // Stores the resolved options array for a setting. nax_set_opt_ + settingOpts: (key) => `nax_set_opt_${key}`, + + // --- Language strings --- + // Stores a translated string. pia_lang__ + langString: (key, langCode) => `pia_lang_${key}_${langCode}`, + LANG_FALLBACK: 'en_us', // fallback language code + + // --- Devices --- + DEVICES_ALL: 'devicesListAll_JSON', // full device list from table_devices.json + DEVICES_TOPOLOGY: 'devicesListNew', // filtered/sorted list for network topology + + // --- UI state --- + VISIBLE_MACS: 'ntx_visible_macs', // comma-separated MACs visible in current view + SHOW_ARCHIVED: 'showArchived', // topology show-archived toggle (network page) + SHOW_OFFLINE: 'showOffline', // topology show-offline toggle (network page) + + // --- Internal init tracking --- + GRAPHQL_STARTED: 'graphQLServerStarted', // set when GraphQL server responds + STRINGS_COUNT: 'cacheStringsCountCompleted', // count of language packs loaded + COMPLETED_CALLS: 'completedCalls', // comma-joined list of completed init calls + INIT_TIMESTAMP: 'nax_init_timestamp', // ms timestamp of last successful cache init + CACHE_VERSION: 'nax_cache_version', // version stamp for auto-bust on deploy +}; + + +// ----------------------------------------------------------------------------- +// localStorage cache helpers +// ----------------------------------------------------------------------------- +function getCache(key) +{ + // check cache + cachedValue = localStorage.getItem(key) + + if(cachedValue) + { + return cachedValue; + } + + return ""; +} + +// ----------------------------------------------------------------------------- +function setCache(key, data) +{ + localStorage.setItem(key, data); +} + +// ----------------------------------------------------------------------------- +// Fetch data from a server-generated JSON file via query_json.php. +// Returns a Promise resolving with the "data" array from the response. +// ----------------------------------------------------------------------------- +function fetchJson(file) { + return new Promise((resolve, reject) => { + $.get('php/server/query_json.php', { file: file, nocache: Date.now() }) + .done((res) => resolve(res['data'] || [])) + .fail((err) => reject(err)); + }); +} + +// ----------------------------------------------------------------------------- +// Safely parse and normalize device cache data. +// Handles both direct array format and { data: [...] } format. +// Returns an array, or empty array on failure. +function parseDeviceCache(cachedStr) { + if (!cachedStr || cachedStr === "") { + return []; + } + + let parsed; + try { + parsed = JSON.parse(cachedStr); + } catch (err) { + console.error('[parseDeviceCache] Failed to parse:', err); + return []; + } + + // If result is an object with a .data property, extract it (handles legacy format) + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray(parsed.data)) { + console.warn('[parseDeviceCache] Extracting .data property from wrapper object'); + parsed = parsed.data; + } + + // Ensure result is an array + if (!Array.isArray(parsed)) { + console.error('[parseDeviceCache] Result is not an array:', parsed); + return []; + } + + return parsed; +} + +// ----------------------------------------------------------------------------- +// Returns the API token, base URL, and a ready-to-use Authorization header +// object for all backend API calls. Centralises the repeated +// getSetting("API_TOKEN") + getApiBase() pattern. +// ----------------------------------------------------------------------------- +function getAuthContext() { + const token = getSetting('API_TOKEN'); + const apiBase = getApiBase(); + return { + token, + apiBase, + authHeader: { 'Authorization': 'Bearer ' + token }, + }; +} + + +// ----------------------------------------------------------------------------- +// Get settings from the .json file generated by the python backend +// and cache them, if available, with options +// ----------------------------------------------------------------------------- + +// ----------------------------------------------------------------------------- +// Bootstrap: fetch API_TOKEN and GRAPHQL_PORT directly from app.conf via the +// PHP helper endpoint. Runs before cacheSettings so that API calls made during +// or after init always have a token available — even if table_settings.json +// hasn't been generated yet. Writes values into the setting() namespace so +// getSetting("API_TOKEN") and getSetting("GRAPHQL_PORT") work immediately. +// ----------------------------------------------------------------------------- +function cacheApiConfig() { + return new Promise((resolve, reject) => { + if (getCache(CACHE_KEYS.initFlag('cacheApiConfig')) === 'true') { + resolve(); + return; + } + + $.get('php/server/app_config.php', { nocache: Date.now() }) + .done((res) => { + if (res && res.api_token) { + setCache(CACHE_KEYS.setting('API_TOKEN'), res.api_token); + setCache(CACHE_KEYS.setting('GRAPHQL_PORT'), String(res.graphql_port || 20212)); + handleSuccess('cacheApiConfig'); + resolve(); + } else { + console.warn('[cacheApiConfig] Response missing api_token — will rely on cacheSettings fallback'); + resolve(); // non-fatal: cacheSettings will still populate these + } + }) + .fail((err) => { + console.warn('[cacheApiConfig] Failed to reach app_config.php:', err); + resolve(); // non-fatal fallback + }); + }); +} + +function cacheSettings() +{ + return new Promise((resolve, reject) => { + if(getCache(CACHE_KEYS.initFlag('cacheSettings')) === "true") + { + resolve(); + return; + } + + // plugins.json may not exist on first boot — treat its absence as non-fatal + Promise.all([fetchJson('table_settings.json'), fetchJson('plugins.json').catch(() => [])]) + .then(([settingsArr, pluginsArr]) => { + pluginsData = pluginsArr; + settingsData = settingsArr; + + // Defensive: Accept either array or object with .data property + // for both settings and plugins + if (!Array.isArray(settingsData)) { + if (settingsData && Array.isArray(settingsData.data)) { + settingsData = settingsData.data; + } else { + console.error('[cacheSettings] settingsData is not an array:', settingsData); + reject(new Error('settingsData is not an array')); + return; + } + } + + // Normalize plugins array too (may have { data: [...] } format) + if (!Array.isArray(pluginsData)) { + if (pluginsData && Array.isArray(pluginsData.data)) { + pluginsData = pluginsData.data; + } else { + console.warn('[cacheSettings] pluginsData is not an array, treating as empty'); + pluginsData = []; + } + } + + settingsData.forEach((set) => { + resolvedOptions = createArray(set.setOptions) + resolvedOptionsOld = resolvedOptions + setPlugObj = {}; + options_params = []; + resolved = "" + + // proceed only if first option item contains something to resolve + if( !set.setKey.includes("__metadata") && + resolvedOptions.length != 0 && + resolvedOptions[0].includes("{value}")) + { + // get setting definition from the plugin config if available + setPlugObj = getPluginSettingObject(pluginsData, set.setKey) + + // check if options contains parameters and resolve + if(setPlugObj != {} && setPlugObj["options_params"]) + { + // get option_params for {value} resolution + options_params = setPlugObj["options_params"] + + if(options_params != []) + { + // handles only strings of length == 1 + + resolved = resolveParams(options_params, resolvedOptions[0]) + + if(resolved.includes('"')) // check if list of strings + { + resolvedOptions = `[${resolved}]` + } else // one value only + { + resolvedOptions = `["${resolved}"]` + } + } + } + } + + setCache(CACHE_KEYS.setting(set.setKey), set.setValue) + setCache(CACHE_KEYS.settingOpts(set.setKey), resolvedOptions) + }); + + handleSuccess('cacheSettings'); + resolve(); + }) + .catch((err) => { handleFailure('cacheSettings'); reject(err); }); + }); +} + +// ----------------------------------------------------------------------------- +// Get a setting options value by key +function getSettingOptions (key) { + + result = getCache(CACHE_KEYS.settingOpts(key)); + + if (result == "") + { + result = [] + } + + return result; +} + +// ----------------------------------------------------------------------------- +// Get a setting value by key +function getSetting (key) { + + result = getCache(CACHE_KEYS.setting(key)); + + return result; +} + +// ----------------------------------------------------------------------------- +// Get language string +// ----------------------------------------------------------------------------- +function cacheStrings() { + return new Promise((resolve, reject) => { + if(getCache(CACHE_KEYS.initFlag('cacheStrings')) === "true") + { + resolve(); + return; + } + + // Create a promise for each language (include en_us by default as fallback) + languagesToLoad = ['en_us'] + + additionalLanguage = getLangCode() + + if(additionalLanguage != 'en_us') + { + languagesToLoad.push(additionalLanguage) + } + + console.log(languagesToLoad); + + const languagePromises = languagesToLoad.map((language_code) => { + return new Promise((resolveLang, rejectLang) => { + // Fetch core strings and translations + + $.get(`php/templates/language/${language_code}.json?nocache=${Date.now()}`) + .done((res) => { + // Iterate over each key-value pair and store the translations + Object.entries(res).forEach(([key, value]) => { + setCache(CACHE_KEYS.langString(key, language_code), value); + }); + + // Fetch strings and translations from plugins (non-fatal — file may + // not exist on first boot or immediately after a cache clear) + fetchJson('table_plugins_language_strings.json') + .catch((pluginError) => { + console.warn('[cacheStrings] Plugin language strings unavailable (non-fatal):', pluginError); + return []; // treat as empty list + }) + .then((data) => { + // Defensive: ensure data is an array (fetchJson may return + // an object, undefined, or empty string on edge cases) + if (!Array.isArray(data)) { data = []; } + // Store plugin translations + data.forEach((langString) => { + setCache(CACHE_KEYS.langString(langString.String_Key, langString.Language_Code), langString.String_Value); + }); + + // Handle successful completion of language processing + handleSuccess('cacheStrings'); + resolveLang(); + }); + }) + .fail((error) => { + // Handle failure in core strings fetching + rejectLang(error); + }); + }); + }); + + // Wait for all language promises to complete + Promise.all(languagePromises) + .then(() => { + // All languages processed successfully + resolve(); + }) + .catch((error) => { + // Handle failure in any of the language processing + handleFailure('cacheStrings'); + reject(error); + }); + + }); +} + +// ----------------------------------------------------------------------------- +// Get translated language string +function getString(key) { + + function fetchString(key) { + + lang_code = getLangCode(); + + let result = getCache(CACHE_KEYS.langString(key, lang_code)); + + if (isEmpty(result)) { + result = getCache(CACHE_KEYS.langString(key, CACHE_KEYS.LANG_FALLBACK)); + } + + return result; + } + + if (isAppInitialized()) { + return fetchString(key); + } else { + callAfterAppInitialized(() => fetchString(key)); + } +} + +// ----------------------------------------------------------------------------- +// Get current language ISO code +// below has to match exactly the values in /front/php/templates/language/lang.php & /front/js/common.js +function getLangCode() { + + UI_LANG = getSetting("UI_LANG"); + + let lang_code = 'en_us'; + + switch (UI_LANG) { + case 'English (en_us)': + lang_code = 'en_us'; + break; + case 'Spanish (es_es)': + lang_code = 'es_es'; + break; + case 'German (de_de)': + lang_code = 'de_de'; + break; + case 'Farsi (fa_fa)': + lang_code = 'fa_fa'; + break; + case 'French (fr_fr)': + lang_code = 'fr_fr'; + break; + case 'Norwegian (nb_no)': + lang_code = 'nb_no'; + break; + case 'Polish (pl_pl)': + lang_code = 'pl_pl'; + break; + case 'Portuguese (pt_br)': + lang_code = 'pt_br'; + break; + case 'Portuguese (pt_pt)': + lang_code = 'pt_pt'; + break; + case 'Turkish (tr_tr)': + lang_code = 'tr_tr'; + break; + case 'Swedish (sv_sv)': + lang_code = 'sv_sv'; + break; + case 'Italian (it_it)': + lang_code = 'it_it'; + break; + case 'Japanese (ja_jp)': + lang_code = 'ja_jp'; + break; + case 'Russian (ru_ru)': + lang_code = 'ru_ru'; + break; + case 'Chinese (zh_cn)': + lang_code = 'zh_cn'; + break; + case 'Czech (cs_cz)': + lang_code = 'cs_cz'; + break; + case 'Arabic (ar_ar)': + lang_code = 'ar_ar'; + break; + case 'Catalan (ca_ca)': + lang_code = 'ca_ca'; + break; + case 'Ukrainian (uk_uk)': + lang_code = 'uk_ua'; + break; + case 'Vietnamese (vi_vn)': + lang_code = 'vi_vn'; + break; + } + + return lang_code; +} + +// ----------------------------------------------------------------------------- +// A function to get a device property using the mac address as key and DB column name as parameter +// for the value to be returned +function getDevDataByMac(macAddress, dbColumn) { + + const sessionDataKey = CACHE_KEYS.DEVICES_ALL; + const devicesCache = getCache(sessionDataKey); + + if (!devicesCache || devicesCache == "") { + console.warn(`[getDevDataByMac] Cache key "${sessionDataKey}" is empty — cache may not be initialized yet.`); + return null; + } + + const devices = parseDeviceCache(devicesCache); + + if (devices.length === 0) { + return null; + } + + for (const device of devices) { + if (device["devMac"].toLowerCase() === macAddress.toLowerCase()) { + + if(dbColumn) + { + return device[dbColumn]; + } + else + { + return device + } + } + } + + console.error("⚠ Device with MAC not found:" + macAddress) + return null; // Return a default value if MAC address is not found +} + +// ----------------------------------------------------------------------------- +// Cache the devices as one JSON +function cacheDevices() +{ + return new Promise((resolve, reject) => { + if(getCache(CACHE_KEYS.initFlag('cacheDevices')) === "true") + { + resolve(); + return; + } + + fetchJson('table_devices.json') + .then((arr) => { + + devicesListAll_JSON = arr; + + devicesListAll_JSON_str = JSON.stringify(devicesListAll_JSON) + + if(devicesListAll_JSON_str == "") + { + showSpinner() + + setTimeout(() => { + cacheDevices() + }, 1000); + } + + setCache(CACHE_KEYS.DEVICES_ALL, devicesListAll_JSON_str) + + handleSuccess('cacheDevices'); + resolve(); + }) + .catch((err) => { handleFailure('cacheDevices'); reject(err); }); + } + ); +} + +var devicesListAll_JSON = []; // this will contain a list off all devices diff --git a/front/js/common.js b/front/js/common.js index c5d109de..205d4986 100755 --- a/front/js/common.js +++ b/front/js/common.js @@ -19,40 +19,10 @@ const allLanguages = ["ar_ar","ca_ca","cs_cz","de_de", "tr_tr","uk_ua","vi_vn","zh_cn"]; // needs to be same as in lang.php var settingsJSON = {} +// NAX_CACHE_VERSION and CACHE_KEYS moved to cache.js -// ----------------------------------------------------------------------------- -// Simple session cache withe expiration managed via cookies -// ----------------------------------------------------------------------------- -function getCache(key, noCookie = false) -{ - // check cache - cachedValue = localStorage.getItem(key) - // console.log(cachedValue); - - if(cachedValue) - { - // // check if not expired - // if(noCookie || getCookie(key + '_session_expiry') != "") - // { - return cachedValue; - // } - } - - return ""; -} - -// ----------------------------------------------------------------------------- -function setCache(key, data, expirationMinutes='') -{ - localStorage.setItem(key, data); - - // // create cookie if expiration set to handle refresh of data - // if (expirationMinutes != '') - // { - // setCookie ('cache_session_expiry', 'OK', 1) - // } -} +// getCache, setCache, fetchJson, getAuthContext moved to cache.js // ----------------------------------------------------------------------------- @@ -93,288 +63,13 @@ function deleteCookie (cookie) { document.cookie = cookie + '=;expires=Thu, 01 Jan 1970 00:00:00 UTC'; } -// ----------------------------------------------------------------------------- -function deleteAllCookies() { - // Array of cookies - var allCookies = document.cookie.split(";"); - - // For each cookie - for (var i = 0; i < allCookies.length; i++) { - var cookie = allCookies[i].trim(); - var eqPos = cookie.indexOf("="); - var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; - document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 UTC"; - } -} +// cacheApiConfig, cacheSettings, getSettingOptions, getSetting moved to cache.js // ----------------------------------------------------------------------------- -// Get settings from the .json file generated by the python backend -// and cache them, if available, with options -// ----------------------------------------------------------------------------- -function cacheSettings() -{ - return new Promise((resolve, reject) => { - if(!getCache('cacheSettings_completed') === true) - { - $.get('php/server/query_json.php', { file: 'table_settings.json', nocache: Date.now() }, function(resSet) { - - $.get('php/server/query_json.php', { file: 'plugins.json', nocache: Date.now() }, function(resPlug) { - - pluginsData = resPlug["data"]; - settingsData = resSet["data"]; - - settingsData.forEach((set) => { - - resolvedOptions = createArray(set.setOptions) - resolvedOptionsOld = resolvedOptions - setPlugObj = {}; - options_params = []; - resolved = "" - - // proceed only if first option item contains something to resolve - if( !set.setKey.includes("__metadata") && - resolvedOptions.length != 0 && - resolvedOptions[0].includes("{value}")) - { - // get setting definition from the plugin config if available - setPlugObj = getPluginSettingObject(pluginsData, set.setKey) - - // check if options contains parameters and resolve - if(setPlugObj != {} && setPlugObj["options_params"]) - { - // get option_params for {value} resolution - options_params = setPlugObj["options_params"] - - if(options_params != []) - { - // handles only strings of length == 1 - - resolved = resolveParams(options_params, resolvedOptions[0]) - - if(resolved.includes('"')) // check if list of strings - { - resolvedOptions = `[${resolved}]` - } else // one value only - { - resolvedOptions = `["${resolved}"]` - } - } - } - } - - setCache(`nax_set_${set.setKey}`, set.setValue) - setCache(`nax_set_opt_${set.setKey}`, resolvedOptions) - }); - }).then(() => handleSuccess('cacheSettings', resolve())).catch(() => handleFailure('cacheSettings', reject("cacheSettings already completed"))); // handle AJAX synchronization - }) - } - }); -} - -// ----------------------------------------------------------------------------- -// Get a setting options value by key -function getSettingOptions (key) { - - // handle initial load to make sure everything is set-up and cached - // handleFirstLoad() - - result = getCache(`nax_set_opt_${key}`, true); - - if (result == "") - { - // console.log(`Setting options with key "${key}" not found`) - result = [] - } - - return result; -} - -// ----------------------------------------------------------------------------- -// Get a setting value by key -function getSetting (key) { - - // handle initial load to make sure everything is set-up and cached - // handleFirstLoad() - - result = getCache(`nax_set_${key}`, true); - - // if (result == "") - // { - // console.log(`Setting with key "${key}" not found`) - // } - - return result; -} - -// ----------------------------------------------------------------------------- -// Get language string -// ----------------------------------------------------------------------------- -function cacheStrings() { - return new Promise((resolve, reject) => { - - // Create a promise for each language (include en_us by default as fallback) - languagesToLoad = ['en_us'] - - additionalLanguage = getLangCode() - - if(additionalLanguage != 'en_us') - { - languagesToLoad.push(additionalLanguage) - } - - console.log(languagesToLoad); - - const languagePromises = languagesToLoad.map((language_code) => { - return new Promise((resolveLang, rejectLang) => { - // Fetch core strings and translations - - $.get(`php/templates/language/${language_code}.json?nocache=${Date.now()}`) - .done((res) => { - // Iterate over each key-value pair and store the translations - Object.entries(res).forEach(([key, value]) => { - setCache(`pia_lang_${key}_${language_code}`, value); - }); - - // Fetch strings and translations from plugins - $.get('php/server/query_json.php', { file: 'table_plugins_language_strings.json', nocache: Date.now() }) - .done((pluginRes) => { - const data = pluginRes["data"]; - - // Store plugin translations - data.forEach((langString) => { - setCache(`pia_lang_${langString.String_Key}_${langString.Language_Code}`, langString.String_Value); - }); - - // Handle successful completion of language processing - handleSuccess(`cacheStrings`, resolveLang); - }) - .fail((pluginError) => { - // Handle failure in plugin strings fetching - rejectLang(pluginError); - }); - }) - .fail((error) => { - // Handle failure in core strings fetching - rejectLang(error); - }); - }); - }); - - // Wait for all language promises to complete - Promise.all(languagePromises) - .then(() => { - // All languages processed successfully - resolve(); - }) - .catch((error) => { - // Handle failure in any of the language processing - handleFailure('cacheStrings', reject); - }); - - }); -} - -// ----------------------------------------------------------------------------- -// Get translated language string -function getString(key) { - - function fetchString(key) { - - lang_code = getLangCode(); - - let result = getCache(`pia_lang_${key}_${lang_code}`, true); - - if (isEmpty(result)) { - result = getCache(`pia_lang_${key}_en_us`, true); - } - - return result; - } - - if (isAppInitialized()) { - return fetchString(key); - } else { - callAfterAppInitialized(() => fetchString(key)); - } -} - -// ----------------------------------------------------------------------------- -// Get current language ISO code -// below has to match exactly the values in /front/php/templates/language/lang.php & /front/js/common.js -function getLangCode() { - - UI_LANG = getSetting("UI_LANG"); - - let lang_code = 'en_us'; - - switch (UI_LANG) { - case 'English (en_us)': - lang_code = 'en_us'; - break; - case 'Spanish (es_es)': - lang_code = 'es_es'; - break; - case 'German (de_de)': - lang_code = 'de_de'; - break; - case 'Farsi (fa_fa)': - lang_code = 'fa_fa'; - break; - case 'French (fr_fr)': - lang_code = 'fr_fr'; - break; - case 'Norwegian (nb_no)': - lang_code = 'nb_no'; - break; - case 'Polish (pl_pl)': - lang_code = 'pl_pl'; - break; - case 'Portuguese (pt_br)': - lang_code = 'pt_br'; - break; - case 'Portuguese (pt_pt)': - lang_code = 'pt_pt'; - break; - case 'Turkish (tr_tr)': - lang_code = 'tr_tr'; - break; - case 'Swedish (sv_sv)': - lang_code = 'sv_sv'; - break; - case 'Italian (it_it)': - lang_code = 'it_it'; - break; - case 'Japanese (ja_jp)': - lang_code = 'ja_jp'; - break; - case 'Russian (ru_ru)': - lang_code = 'ru_ru'; - break; - case 'Chinese (zh_cn)': - lang_code = 'zh_cn'; - break; - case 'Czech (cs_cz)': - lang_code = 'cs_cz'; - break; - case 'Arabic (ar_ar)': - lang_code = 'ar_ar'; - break; - case 'Catalan (ca_ca)': - lang_code = 'ca_ca'; - break; - case 'Ukrainian (uk_uk)': - lang_code = 'uk_ua'; - break; - case 'Vietnamese (vi_vn)': - lang_code = 'vi_vn'; - break; - } - - return lang_code; -} +// cacheStrings, getString, getLangCode moved to cache.js const tz = getSetting("TIMEZONE") || 'Europe/Berlin'; const LOCALE = getSetting('UI_LOCALE') || 'en-GB'; @@ -718,14 +413,13 @@ function numberArrayFromString(data) // ----------------------------------------------------------------------------- // Update network parent/child relationship (network tree) function updateNetworkLeaf(leafMac, parentMac) { - const apiBase = getApiBase(); - const apiToken = getSetting("API_TOKEN"); + const { apiBase, authHeader } = getAuthContext(); const url = `${apiBase}/device/${leafMac}/update-column`; $.ajax({ method: "POST", url: url, - headers: { "Authorization": `Bearer ${apiToken}` }, + headers: authHeader, data: JSON.stringify({ columnName: "devParentMAC", columnValue: parentMac }), contentType: "application/json", success: function(response) { @@ -1157,72 +851,7 @@ function isRandomMAC(mac) return options; } -// ----------------------------------------------------------------------------- -// A function to get a device property using the mac address as key and DB column nakme as parameter -// for the value to be returned -function getDevDataByMac(macAddress, dbColumn) { - - const sessionDataKey = 'devicesListAll_JSON'; - const devicesCache = getCache(sessionDataKey); - - if (!devicesCache || devicesCache == "") { - console.error(`Session variable "${sessionDataKey}" not found.`); - return null; - } - - const devices = JSON.parse(devicesCache); - - for (const device of devices) { - if (device["devMac"].toLowerCase() === macAddress.toLowerCase()) { - - if(dbColumn) - { - return device[dbColumn]; - } - else - { - return device - } - } - } - - console.error("⚠ Device with MAC not found:" + macAddress) - return null; // Return a default value if MAC address is not found -} - -// ----------------------------------------------------------------------------- -// Cache the devices as one JSON -function cacheDevices() -{ - return new Promise((resolve, reject) => { - - $.get('php/server/query_json.php', { file: 'table_devices.json', nocache: Date.now() }, function(data) { - - // console.log(data) - - devicesListAll_JSON = data["data"] - - devicesListAll_JSON_str = JSON.stringify(devicesListAll_JSON) - - if(devicesListAll_JSON_str == "") - { - showSpinner() - - setTimeout(() => { - cacheDevices() - }, 1000); - } - // console.log(devicesListAll_JSON_str); - - setCache('devicesListAll_JSON', devicesListAll_JSON_str) - - // console.log(getCache('devicesListAll_JSON')) - }).then(() => handleSuccess('cacheDevices', resolve())).catch(() => handleFailure('cacheDevices', reject("cacheDevices already completed"))); // handle AJAX synchronization - } - ); -} - -var devicesListAll_JSON = []; // this will contain a list off all devices +// getDevDataByMac, cacheDevices, devicesListAll_JSON moved to cache.js // ----------------------------------------------------------------------------- function isEmpty(value) @@ -1369,18 +998,13 @@ function updateApi(apiEndpoints) // value has to be in format event|param. e.g. run|ARPSCAN action = `${getGuid()}|update_api|${apiEndpoints}` - // Get data from the server - const apiToken = getSetting("API_TOKEN"); - const apiBaseUrl = getApiBase(); + const { token: apiToken, apiBase: apiBaseUrl, authHeader } = getAuthContext(); const url = `${apiBaseUrl}/logs/add-to-execution-queue`; $.ajax({ method: "POST", url: url, - headers: { - "Authorization": "Bearer " + apiToken, - "Content-Type": "application/json" - }, + headers: { ...authHeader, "Content-Type": "application/json" }, data: JSON.stringify({ action: action }), success: function(data, textStatus) { console.log(data) @@ -1548,16 +1172,11 @@ function hideUIelements(setKey) { function getDevicesList() { // Read cache (skip cookie expiry check) - devicesList = getCache('devicesListAll_JSON', true); - - if (devicesList != '') { - devicesList = JSON.parse (devicesList); - } else { - devicesList = []; - } + const cached = getCache(CACHE_KEYS.DEVICES_ALL); + let devicesList = parseDeviceCache(cached); // only loop thru the filtered down list - visibleDevices = getCache("ntx_visible_macs") + visibleDevices = getCache(CACHE_KEYS.VISIBLE_MACS) if(visibleDevices != "") { visibleDevicesMACs = visibleDevices.split(','); @@ -1616,18 +1235,14 @@ function restartBackend() { modalEventStatusId = 'modal-message-front-event' - const apiToken = getSetting("API_TOKEN"); - const apiBaseUrl = getApiBase(); + const { token: apiToken, apiBase: apiBaseUrl, authHeader } = getAuthContext(); const url = `${apiBaseUrl}/logs/add-to-execution-queue`; // Execute $.ajax({ method: "POST", url: url, - headers: { - "Authorization": "Bearer " + apiToken, - "Content-Type": "application/json" - }, + headers: { ...authHeader, "Content-Type": "application/json" }, data: JSON.stringify({ action: `cron_restart_backend` }), success: function(data, textStatus) { // showModalOk ('Result', data ); @@ -1642,237 +1257,8 @@ function restartBackend() { }) } -// ----------------------------------------------------------------------------- -// initialize -// ----------------------------------------------------------------------------- -// ----------------------------------------------------------------------------- - -// Define a unique key for storing the flag in sessionStorage -const sessionStorageKey = "myScriptExecuted_common_js"; -var completedCalls = [] -var completedCalls_final = ['cacheSettings', 'cacheStrings', 'cacheDevices']; -var lang_completedCalls = 0; - - -// ----------------------------------------------------------------------------- -// Clearing all the caches -function clearCache() { - showSpinner(); - sessionStorage.clear(); - localStorage.clear(); - setTimeout(() => { - console.warn("clearChache called"); - window.location.reload(); - }, 500); -} - -// =================================================================== -// DEPRECATED: checkSettingChanges() - Replaced by SSE-based manager -// Settings changes are now handled via SSE events -// Kept for backward compatibility, will be removed in future version -// =================================================================== -function checkSettingChanges() { - // SSE manager handles settings_changed events now - if (typeof netAlertXStateManager !== 'undefined' && netAlertXStateManager.initialized) { - return; // SSE handles this now - } - - // Fallback for backward compatibility - $.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now() }, function(appState) { - const importedMilliseconds = parseInt(appState["settingsImported"] * 1000); - const lastReloaded = parseInt(sessionStorage.getItem(sessionStorageKey + '_time')); - - if (importedMilliseconds > lastReloaded) { - console.log("Cache needs to be refreshed because of setting changes"); - setTimeout(() => { - clearCache(); - }, 500); - } - }); -} - -// =================================================================== -// Display spinner and reload page if not yet initialized -async function handleFirstLoad(callback) { - if (!isAppInitialized()) { - await new Promise(resolve => setTimeout(resolve, 1000)); - callback(); - } -} - -// =================================================================== -// Execute callback once the app is initialized and GraphQL server is running -async function callAfterAppInitialized(callback) { - if (!isAppInitialized() || !(await isGraphQLServerRunning())) { - setTimeout(() => { - callAfterAppInitialized(callback); - }, 500); - } else { - callback(); - } -} - -// =================================================================== -// Polling function to repeatedly check if the server is running -async function waitForGraphQLServer() { - const pollInterval = 2000; // 2 seconds between each check - let serverRunning = false; - - while (!serverRunning) { - serverRunning = await isGraphQLServerRunning(); - if (!serverRunning) { - console.log("GraphQL server not running, retrying in 2 seconds..."); - await new Promise(resolve => setTimeout(resolve, pollInterval)); - } - } - - console.log("GraphQL server is now running."); -} - -// ----------------------------------------------------------------------------- -// Returns 1 if running, 0 otherwise -async function isGraphQLServerRunning() { - try { - const response = await $.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now()}); - console.log("graphQLServerStarted: " + response["graphQLServerStarted"]); - setCache("graphQLServerStarted", response["graphQLServerStarted"]); - return response["graphQLServerStarted"]; - } catch (error) { - console.error("Failed to check GraphQL server status:", error); - return false; - } -} - -// ----------------------------------------------------------------------------- -// Check if the code has been executed before by checking sessionStorage -function isAppInitialized() { - - lang_shouldBeCompletedCalls = getLangCode() == 'en_us' ? 1 : 2; - - // check if each ajax call completed succesfully - $.each(completedCalls_final, function(index, call_name){ - - if(getCache(call_name + "_completed") != "true") - { - console.log(`[isAppInitialized] AJAX call ${call_name} unsuccesful: ${getCache(call_name + "_completed")}`) - return false; - } - - }); - - // check if all required languages chached - if(parseInt(getCache("cacheStringsCountCompleted")) != lang_shouldBeCompletedCalls) - { - console.log(`[isAppInitialized] AJAX call cacheStrings unsuccesful: ${getCache("cacheStringsCountCompleted")} out of ${lang_shouldBeCompletedCalls}`) - return false; - } - - return true; -} - -// ----------------------------------------------------------------------------- -// Main execution logic -async function executeOnce() { - showSpinner(); - - if (!isAppInitialized()) { - try { - - await waitForGraphQLServer(); // Wait for the server to start - - await cacheDevices(); - await cacheSettings(); - await cacheStrings(); - - console.log("All AJAX callbacks have completed"); - onAllCallsComplete(); - } catch (error) { - console.error("Error:", error); - } - } -} - - -// ----------------------------------------------------------------------------- -// Function to handle successful completion of an AJAX call -const handleSuccess = (callName) => { - console.log(`AJAX call successful: ${callName}`); - - if(callName.includes("cacheStrings")) - { - completed_tmp = getCache("cacheStringsCountCompleted"); - completed_tmp == "" ? completed_tmp = 0 : completed_tmp = completed_tmp; - completed_tmp++; - setCache("cacheStringsCountCompleted", completed_tmp); - } - - setCache(callName + "_completed", true) -}; - -// ----------------------------------------------------------------------------- -// Function to handle failure of an AJAX call -const handleFailure = (callName, callback) => { - msg = `AJAX call ${callName} failed` - console.error(msg); - // Implement retry logic here if needed - // write_notification(msg, 'interrupt') -}; - -// ----------------------------------------------------------------------------- -// Function to execute when all AJAX calls have completed -const onAllCallsComplete = () => { - completedCalls = mergeUniqueArrays(getCache('completedCalls').split(','), completedCalls); - setCache('completedCalls', completedCalls); - - // Check if all necessary strings are initialized - if (areAllStringsInitialized()) { - sessionStorage.setItem(sessionStorageKey, "true"); - const millisecondsNow = Date.now(); - sessionStorage.setItem(sessionStorageKey + '_time', millisecondsNow); - - console.log('✔ Cache initialized'); - // setTimeout(() => { - // location.reload() - // }, 10); - - } else { - // If not all strings are initialized, retry initialization - console.log('❌ Not all strings are initialized. Retrying...'); - executeOnce(); - return; - } - - // Call any other initialization functions here if needed - -}; - -// Function to check if all necessary strings are initialized -const areAllStringsInitialized = () => { - // Implement logic to check if all necessary strings are initialized - // Return true if all strings are initialized, false otherwise - return getString('UI_LANG_name') != "" -}; - -// Call the function to execute the code -executeOnce(); - -// Set timer for regular UI refresh if enabled -setTimeout(() => { - - // page refresh if configured - const refreshTime = getSetting("UI_REFRESH"); - if (refreshTime && refreshTime !== "0" && refreshTime !== "") { - console.log("Refreshing page becasue UI_REFRESH setting enabled."); - newTimerRefreshData(clearCache, parseInt(refreshTime)*1000); - } - - // Check if page needs to refresh due to setting changes - checkSettingChanges() - -}, 10000); - - -console.log("init common.js"); +// App lifecycle (completedCalls, executeOnce, handleSuccess, clearCache, etc.) moved to app-init.js + diff --git a/front/js/network-api.js b/front/js/network-api.js index df6135b9..712f1024 100644 --- a/front/js/network-api.js +++ b/front/js/network-api.js @@ -1,21 +1,6 @@ // network-api.js // API calls and data loading functions for network topology -/** - * Get API token, waiting if necessary for settings to load - * @returns {string} The API token - */ -function getApiToken() { - let token = getSetting("API_TOKEN"); - - // If token is not yet available, log warning - if (!token || token.trim() === '') { - console.warn("API_TOKEN not yet loaded from settings"); - } - - return token; -} - /** * Load network nodes (network device types) * Creates top-level tabs for each network device @@ -52,8 +37,7 @@ function loadNetworkNodes() { ORDER BY parent.devName; `; - const apiBase = getApiBase(); - const apiToken = getApiToken(); + const { token: apiToken, apiBase, authHeader } = getAuthContext(); // Verify token is available if (!apiToken || apiToken.trim() === '') { @@ -66,7 +50,7 @@ function loadNetworkNodes() { $.ajax({ url, method: "POST", - headers: { "Authorization": `Bearer ${apiToken}` }, + headers: { ...authHeader, "Content-Type": "application/json" }, data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(rawSql))) }), contentType: "application/json", success: function(data) { @@ -95,8 +79,7 @@ function loadNetworkNodes() { * @param {boolean} options.assignMode - Whether to show assign/unassign buttons */ function loadDeviceTable({ sql, containerSelector, tableId, wrapperHtml = null, assignMode = true }) { - const apiBase = getApiBase(); - const apiToken = getApiToken(); + const { token: apiToken, apiBase, authHeader } = getAuthContext(); // Verify token is available if (!apiToken || apiToken.trim() === '') { @@ -109,7 +92,7 @@ function loadDeviceTable({ sql, containerSelector, tableId, wrapperHtml = null, $.ajax({ url, method: "POST", - headers: { "Authorization": `Bearer ${apiToken}` }, + headers: { ...authHeader, "Content-Type": "application/json" }, data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(sql))) }), contentType: "application/json", success: function(data) { diff --git a/front/js/network-events.js b/front/js/network-events.js index 4241fc10..9b97c8b6 100644 --- a/front/js/network-events.js +++ b/front/js/network-events.js @@ -84,12 +84,12 @@ $(window).on('resize', function () { */ $(document).ready(function () { // Restore cached values on load - const cachedOffline = getCache('showOffline'); + const cachedOffline = getCache(CACHE_KEYS.SHOW_OFFLINE); if (cachedOffline !== null) { $('input[name="showOffline"]').prop('checked', cachedOffline === 'true'); } - const cachedArchived = getCache('showArchived'); + const cachedArchived = getCache(CACHE_KEYS.SHOW_ARCHIVED); if (cachedArchived !== null) { $('input[name="showArchived"]').prop('checked', cachedArchived === 'true'); } @@ -102,7 +102,7 @@ $(document).ready(function () { if (!isOfflineChecked) { archivedToggle.prop('checked', false); archivedToggle.prop('disabled', true); - setCache('showArchived', false); + setCache(CACHE_KEYS.SHOW_ARCHIVED, false); } else { archivedToggle.prop('disabled', false); } @@ -115,6 +115,8 @@ $(document).ready(function () { $('input[name="showOffline"], input[name="showArchived"]').on('change', function () { const name = $(this).attr('name'); const value = $(this).is(':checked'); + // setCache(name, value) works because CACHE_KEYS.SHOW_OFFLINE === 'showOffline' + // and CACHE_KEYS.SHOW_ARCHIVED === 'showArchived' — matches the DOM input name attr. setCache(name, value); // Update state of showArchived if showOffline changed diff --git a/front/js/network-init.js b/front/js/network-init.js index bfca39e5..b23364e4 100644 --- a/front/js/network-init.js +++ b/front/js/network-init.js @@ -12,8 +12,8 @@ var showOffline = false; */ function initNetworkTopology() { networkDeviceTypes = getSetting("NETWORK_DEVICE_TYPES").replace("[", "").replace("]", ""); - showArchived = getCache('showArchived') === "true"; - showOffline = getCache('showOffline') === "true"; + showArchived = getCache(CACHE_KEYS.SHOW_ARCHIVED) === "true"; + showOffline = getCache(CACHE_KEYS.SHOW_OFFLINE) === "true"; console.log('showArchived:', showArchived); console.log('showOffline:', showOffline); @@ -33,8 +33,7 @@ function initNetworkTopology() { FROM Devices a `; - const apiBase = getApiBase(); - const apiToken = getApiToken(); + const { token: apiToken, apiBase, authHeader } = getAuthContext(); // Verify token is available before making API call if (!apiToken || apiToken.trim() === '') { @@ -51,7 +50,7 @@ function initNetworkTopology() { $.ajax({ url, method: "POST", - headers: { "Authorization": `Bearer ${apiToken}` }, + headers: { ...authHeader, "Content-Type": "application/json" }, data: JSON.stringify({ rawSql: btoa(unescape(encodeURIComponent(rawSql))) }), contentType: "application/json", success: function(data) { @@ -121,7 +120,7 @@ function initNetworkTopology() { } }); - setCache('devicesListNew', JSON.stringify(devicesSorted)); + setCache(CACHE_KEYS.DEVICES_TOPOLOGY, JSON.stringify(devicesSorted)); deviceListGlobal = devicesSorted; // Render filtered result diff --git a/front/multiEditCore.php b/front/multiEditCore.php index 4484ff8a..accf5e2e 100755 --- a/front/multiEditCore.php +++ b/front/multiEditCore.php @@ -240,8 +240,8 @@ // Initialize device selectors / pickers fields function initDeviceSelectors() { - // Parse device list - devicesList = JSON.parse(getCache('devicesListAll_JSON')); + // Parse device list using the shared helper + devicesList = parseDeviceCache(getCache('devicesListAll_JSON')); // Check if the device list exists and is an array if (Array.isArray(devicesList)) { diff --git a/front/php/server/app_config.php b/front/php/server/app_config.php new file mode 100644 index 00000000..7de94cc7 --- /dev/null +++ b/front/php/server/app_config.php @@ -0,0 +1,38 @@ + 'Method not allowed']); + exit; +} + +// API_TOKEN: security.php extracts it from app.conf but the value is empty until Python +// initialise.py runs. Fall back to table_settings.json (runtime source of truth). +$resolved_token = !empty($api_token) ? $api_token : getSettingValue('API_TOKEN'); + +// GRAPHQL_PORT: format in app.conf is bare integer — GRAPHQL_PORT=20212 (no quotes) +$graphql_port_raw = getConfigLine('/^GRAPHQL_PORT\s*=/', $configLines); +$graphql_port = isset($graphql_port_raw[1]) ? (int) trim($graphql_port_raw[1]) : 20212; + +// Validate we have something useful before returning +if (empty($resolved_token) || str_starts_with($resolved_token, 'Could not')) { + http_response_code(500); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Could not read API_TOKEN from configuration']); + exit; +} + +header('Content-Type: application/json'); +echo json_encode([ + 'api_token' => $resolved_token, + 'graphql_port' => $graphql_port, +]); diff --git a/front/php/templates/header.php b/front/php/templates/header.php index e32b0efb..8060abe9 100755 --- a/front/php/templates/header.php +++ b/front/php/templates/header.php @@ -43,7 +43,10 @@ + + + diff --git a/front/php/templates/version.php b/front/php/templates/version.php index e58e7aab..a9cf10e6 100755 --- a/front/php/templates/version.php +++ b/front/php/templates/version.php @@ -18,7 +18,7 @@ if(file_exists($filename)) { if(trim($fileContents) === 'Dev') { echo date('H:i:s') . " - " . $fileContents; } else { - echo $fileContents; + echo trim($fileContents); } } else {