From 82d5feef5906e387b67f01017e781bd40d96d62e Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 3 Dec 2025 15:26:29 +0530 Subject: [PATCH 01/12] added input field types for refresh time --- edge-apps/powerbi/screenly.yml | 14 ++++++++++++++ edge-apps/powerbi/screenly_qc.yml | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/edge-apps/powerbi/screenly.yml b/edge-apps/powerbi/screenly.yml index a7f64c284..caab8269d 100644 --- a/edge-apps/powerbi/screenly.yml +++ b/edge-apps/powerbi/screenly.yml @@ -36,3 +36,17 @@ settings: help_text: The URL of the Power BI dashboard or report to display. type: oauth:power_bi:embed_url schema_version: 1 + refresh_interval: + type: string + default_value: '30' + title: Refresh Interval + optional: true + help_text: + properties: + advanced: true + help_text: | + Set the interval (in minutes) for periodically refreshing the Power BI dashboard or report. + The default value is 30 minutes, the minimum allowed is 1 minute. + Note: The embed token will be refreshed each time the dashboard or report is refreshed. + type: number + schema_version: 1 diff --git a/edge-apps/powerbi/screenly_qc.yml b/edge-apps/powerbi/screenly_qc.yml index f5470a997..a4751e204 100644 --- a/edge-apps/powerbi/screenly_qc.yml +++ b/edge-apps/powerbi/screenly_qc.yml @@ -36,3 +36,17 @@ settings: help_text: The URL of the Power BI dashboard or report to display. type: oauth:power_bi:embed_url schema_version: 1 + refresh_interval: + type: string + default_value: '30' + title: Refresh Interval + optional: true + help_text: + schema_version: 1 + properties: + advanced: true + help_text: | + Set the interval (in minutes) for periodically refreshing the Power BI dashboard or report. + The default value is 30 minutes, the minimum allowed is 1 minute. + Note: The embed token will be refreshed each time the dashboard or report is refreshed. + type: number From 855c1ab9e0fda291fb0ff1f1d36c2be5d9e3ee8a Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 3 Dec 2025 15:39:44 +0530 Subject: [PATCH 02/12] added dashboard refresh --- edge-apps/powerbi/static/js/main.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index e697706ca..be9591554 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -1,7 +1,17 @@ /* global screenly, panic */ (function () { - const DEFAULT_TOKEN_REFRESH_SEC = 30 * 60; // refresh token every 30 minutes + const MIN_TOKEN_REFRESH_MIN = 1; // minimum 1 minute + const DEFAULT_TOKEN_REFRESH_MIN = 30; // default 30 minutes + + function getTokenRefreshInterval() { + // User provides interval in minutes, convert to seconds + var intervalMinutes = parseInt(screenly.settings.refresh_interval, 10); + if (isNaN(intervalMinutes) || intervalMinutes < MIN_TOKEN_REFRESH_MIN) { + return DEFAULT_TOKEN_REFRESH_MIN * 60; // convert to seconds + } + return intervalMinutes * 60; // convert minutes to seconds + } function getEmbedTypeFromUrl(url) { switch (true) { @@ -33,9 +43,10 @@ var currentErrorStep = 0; var initErrorDelaySec = 15; var maxErrorStep = 7; + var tokenRefreshInterval = getTokenRefreshInterval(); async function run() { - var nextTimeout = DEFAULT_TOKEN_REFRESH_SEC; + var nextTimeout = tokenRefreshInterval; try { var newToken = await getEmbedToken(); await report.setAccessToken(newToken); @@ -50,7 +61,7 @@ setTimeout(run, nextTimeout * 1000); } - setTimeout(run, DEFAULT_TOKEN_REFRESH_SEC * 1000); + setTimeout(run, tokenRefreshInterval * 1000); } async function initializePowerBI() { @@ -92,4 +103,4 @@ } initializePowerBI(); -})(); +})(); \ No newline at end of file From c09e3dcb434f3d00cfa2067765030415feb4ca52 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 3 Dec 2025 15:53:21 +0530 Subject: [PATCH 03/12] fix js lint --- edge-apps/powerbi/static/js/main.js | 72 +++++++++++++++++------------ 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index be9591554..05aedfca3 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -15,10 +15,10 @@ function getEmbedTypeFromUrl(url) { switch (true) { - case url.indexOf('/dashboard') !== -1: - return 'dashboard'; + case url.indexOf("/dashboard") !== -1: + return "dashboard"; default: - return 'report'; + return "report"; } } @@ -27,13 +27,16 @@ return screenly.settings.embed_token; } - var response = await fetch(screenly.settings.screenly_oauth_tokens_url + 'embed_token/', { - method: 'GET', - headers: { - Accept: 'application/json', - Authorization: `Bearer ${screenly.settings.screenly_app_auth_token}`, + var response = await fetch( + screenly.settings.screenly_oauth_tokens_url + "embed_token/", + { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${screenly.settings.screenly_app_auth_token}`, + }, }, - }); + ); const { token } = await response.json(); return token; @@ -52,7 +55,10 @@ await report.setAccessToken(newToken); currentErrorStep = 0; } catch { - nextTimeout = Math.min(initErrorDelaySec * Math.pow(2, currentErrorStep), nextTimeout); + nextTimeout = Math.min( + initErrorDelaySec * Math.pow(2, currentErrorStep), + nextTimeout, + ); if (currentErrorStep >= maxErrorStep) { return; } @@ -65,26 +71,29 @@ } async function initializePowerBI() { - const models = window['powerbi-client'].models; + const models = window["powerbi-client"].models; const embedUrl = screenly.settings.embed_url; const resourceType = getEmbedTypeFromUrl(embedUrl); - const report = window.powerbi.embed(document.getElementById('embed-container'), { - embedUrl: embedUrl, - accessToken: await getEmbedToken(), - type: resourceType, - tokenType: models.TokenType.Embed, - permissions: models.Permissions.All, - settings: { - filterPaneEnabled: false, - navContentPaneEnabled: false, + const report = window.powerbi.embed( + document.getElementById("embed-container"), + { + embedUrl: embedUrl, + accessToken: await getEmbedToken(), + type: resourceType, + tokenType: models.TokenType.Embed, + permissions: models.Permissions.All, + settings: { + filterPaneEnabled: false, + navContentPaneEnabled: false, + }, }, - }); + ); - if (resourceType === 'report') { - report.on('rendered', screenly.signalReadyForRendering); - } else if (resourceType === 'dashboard') { - report.on('loaded', () => { + if (resourceType === "report") { + report.on("rendered", screenly.signalReadyForRendering); + } else if (resourceType === "dashboard") { + report.on("loaded", () => { setTimeout(screenly.signalReadyForRendering, 1000); }); } @@ -95,12 +104,15 @@ } panic.configure({ - handleErrors: screenly.settings.display_errors == 'true' || false, + handleErrors: screenly.settings.display_errors == "true" || false, }); - if (screenly.settings.display_errors == 'true') { - window.addEventListener('error', screenly.signalReadyForRendering); - window.addEventListener('unhandledrejection', screenly.signalReadyForRendering); + if (screenly.settings.display_errors == "true") { + window.addEventListener("error", screenly.signalReadyForRendering); + window.addEventListener( + "unhandledrejection", + screenly.signalReadyForRendering, + ); } initializePowerBI(); -})(); \ No newline at end of file +})(); From 751406b4b937a60d5168c78e2edf8973a4f73944 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 3 Dec 2025 15:59:12 +0530 Subject: [PATCH 04/12] fix js lint as per custom config --- edge-apps/powerbi/static/js/main.js | 98 ++++++++++++++--------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index 05aedfca3..766bcab7a 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -1,82 +1,82 @@ /* global screenly, panic */ -(function () { - const MIN_TOKEN_REFRESH_MIN = 1; // minimum 1 minute - const DEFAULT_TOKEN_REFRESH_MIN = 30; // default 30 minutes +;(function () { + const MIN_TOKEN_REFRESH_MIN = 1 // minimum 1 minute + const DEFAULT_TOKEN_REFRESH_MIN = 30 // default 30 minutes function getTokenRefreshInterval() { // User provides interval in minutes, convert to seconds - var intervalMinutes = parseInt(screenly.settings.refresh_interval, 10); + var intervalMinutes = parseInt(screenly.settings.refresh_interval, 10) if (isNaN(intervalMinutes) || intervalMinutes < MIN_TOKEN_REFRESH_MIN) { - return DEFAULT_TOKEN_REFRESH_MIN * 60; // convert to seconds + return DEFAULT_TOKEN_REFRESH_MIN * 60 // convert to seconds } - return intervalMinutes * 60; // convert minutes to seconds + return intervalMinutes * 60 // convert minutes to seconds } function getEmbedTypeFromUrl(url) { switch (true) { - case url.indexOf("/dashboard") !== -1: - return "dashboard"; + case url.indexOf('/dashboard') !== -1: + return 'dashboard' default: - return "report"; + return 'report' } } async function getEmbedToken() { if (screenly.settings.embed_token) { - return screenly.settings.embed_token; + return screenly.settings.embed_token } var response = await fetch( - screenly.settings.screenly_oauth_tokens_url + "embed_token/", + screenly.settings.screenly_oauth_tokens_url + 'embed_token/', { - method: "GET", + method: 'GET', headers: { - Accept: "application/json", + Accept: 'application/json', Authorization: `Bearer ${screenly.settings.screenly_app_auth_token}`, }, }, - ); + ) - const { token } = await response.json(); - return token; + const { token } = await response.json() + return token } function initTokenRefreshLoop(report) { - var currentErrorStep = 0; - var initErrorDelaySec = 15; - var maxErrorStep = 7; - var tokenRefreshInterval = getTokenRefreshInterval(); + var currentErrorStep = 0 + var initErrorDelaySec = 15 + var maxErrorStep = 7 + var tokenRefreshInterval = getTokenRefreshInterval() async function run() { - var nextTimeout = tokenRefreshInterval; + var nextTimeout = tokenRefreshInterval try { - var newToken = await getEmbedToken(); - await report.setAccessToken(newToken); - currentErrorStep = 0; + var newToken = await getEmbedToken() + await report.setAccessToken(newToken) + currentErrorStep = 0 } catch { nextTimeout = Math.min( initErrorDelaySec * Math.pow(2, currentErrorStep), nextTimeout, - ); + ) if (currentErrorStep >= maxErrorStep) { - return; + return } - currentErrorStep += 1; + currentErrorStep += 1 } - setTimeout(run, nextTimeout * 1000); + setTimeout(run, nextTimeout * 1000) } - setTimeout(run, tokenRefreshInterval * 1000); + setTimeout(run, tokenRefreshInterval * 1000) } async function initializePowerBI() { - const models = window["powerbi-client"].models; - const embedUrl = screenly.settings.embed_url; - const resourceType = getEmbedTypeFromUrl(embedUrl); + const models = window['powerbi-client'].models + const embedUrl = screenly.settings.embed_url + const resourceType = getEmbedTypeFromUrl(embedUrl) const report = window.powerbi.embed( - document.getElementById("embed-container"), + document.getElementById('embed-container'), { embedUrl: embedUrl, accessToken: await getEmbedToken(), @@ -88,31 +88,31 @@ navContentPaneEnabled: false, }, }, - ); + ) - if (resourceType === "report") { - report.on("rendered", screenly.signalReadyForRendering); - } else if (resourceType === "dashboard") { - report.on("loaded", () => { - setTimeout(screenly.signalReadyForRendering, 1000); - }); + if (resourceType === 'report') { + report.on('rendered', screenly.signalReadyForRendering) + } else if (resourceType === 'dashboard') { + report.on('loaded', () => { + setTimeout(screenly.signalReadyForRendering, 1000) + }) } if (!screenly.settings.embed_token) { - initTokenRefreshLoop(report); + initTokenRefreshLoop(report) } } panic.configure({ - handleErrors: screenly.settings.display_errors == "true" || false, - }); - if (screenly.settings.display_errors == "true") { - window.addEventListener("error", screenly.signalReadyForRendering); + handleErrors: screenly.settings.display_errors == 'true' || false, + }) + if (screenly.settings.display_errors == 'true') { + window.addEventListener('error', screenly.signalReadyForRendering) window.addEventListener( - "unhandledrejection", + 'unhandledrejection', screenly.signalReadyForRendering, - ); + ) } - initializePowerBI(); -})(); + initializePowerBI() +})() From 09da5e00c1b2c233a43a89c46b18083a4eb9b8f7 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Thu, 4 Dec 2025 17:52:07 +0530 Subject: [PATCH 05/12] update logic on refresh --- edge-apps/powerbi/static/js/main.js | 240 ++++++++++++++++++++-------- 1 file changed, 172 insertions(+), 68 deletions(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index 766bcab7a..613fb7f31 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -1,118 +1,222 @@ /* global screenly, panic */ -;(function () { - const MIN_TOKEN_REFRESH_MIN = 1 // minimum 1 minute - const DEFAULT_TOKEN_REFRESH_MIN = 30 // default 30 minutes - - function getTokenRefreshInterval() { - // User provides interval in minutes, convert to seconds - var intervalMinutes = parseInt(screenly.settings.refresh_interval, 10) - if (isNaN(intervalMinutes) || intervalMinutes < MIN_TOKEN_REFRESH_MIN) { - return DEFAULT_TOKEN_REFRESH_MIN * 60 // convert to seconds - } - return intervalMinutes * 60 // convert minutes to seconds - } +(function () { + const DEFAULT_TOKEN_REFRESH_SEC = 30 * 60; + const TOKEN_STORAGE_KEY = 'powerbi_embed_token'; + const TOKEN_EXPIRY_STORAGE_KEY = 'powerbi_embed_token_expiry'; + const TOKEN_EXPIRY_BUFFER_SEC = 5 * 60; // Refresh 5 minutes before expiry + const DEFAULT_APP_REFRESH_MIN = 30; + const MIN_APP_REFRESH_MIN = 1; function getEmbedTypeFromUrl(url) { switch (true) { case url.indexOf('/dashboard') !== -1: - return 'dashboard' + return 'dashboard'; default: - return 'report' + return 'report'; + } + } + + function clearStoredToken() { + try { + localStorage.removeItem(TOKEN_STORAGE_KEY); + localStorage.removeItem(TOKEN_EXPIRY_STORAGE_KEY); + } catch (error) { + console.warn('Could not clear token from localStorage:', error); } } async function getEmbedToken() { + // Use static token from settings if provided if (screenly.settings.embed_token) { - return screenly.settings.embed_token + return screenly.settings.embed_token; + } + + // Check localStorage for cached token to avoid unnecessary API calls + try { + const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY); + const storedExpiry = localStorage.getItem(TOKEN_EXPIRY_STORAGE_KEY); + + if (storedToken && storedExpiry) { + const expiryTime = parseInt(storedExpiry, 10); + const now = Date.now(); + const timeUntilExpiry = expiryTime - now; + const bufferMs = TOKEN_EXPIRY_BUFFER_SEC * 1000; + + // Reuse token if it's still valid (more than 5 minutes until expiry) + if (timeUntilExpiry > bufferMs) { + return storedToken; + } + clearStoredToken(); + } + } catch (error) { + console.warn('Could not access localStorage for token cache:', error); } - var response = await fetch( - screenly.settings.screenly_oauth_tokens_url + 'embed_token/', - { + // Fetch new token from API + try { + var response = await fetch(screenly.settings.screenly_oauth_tokens_url + 'embed_token/', { method: 'GET', headers: { Accept: 'application/json', Authorization: `Bearer ${screenly.settings.screenly_app_auth_token}`, }, - }, - ) + }); + + if (!response.ok) { + throw new Error(`Failed to fetch embed token: ${response.status} ${response.statusText}`); + } - const { token } = await response.json() - return token + const { token } = await response.json(); + + if (!token) { + throw new Error('No token received from API'); + } + + // Store token with 1 hour expiry (Power BI tokens expire after 1 hour) + const actualExpiryTime = Date.now() + (60 * 60 * 1000); + + try { + localStorage.setItem(TOKEN_STORAGE_KEY, token); + localStorage.setItem(TOKEN_EXPIRY_STORAGE_KEY, actualExpiryTime.toString()); + } catch (storageError) { + console.warn('Could not store token in localStorage:', storageError); + } + + return token; + } catch (error) { + console.error('Error fetching embed token:', error); + throw error; + } } function initTokenRefreshLoop(report) { - var currentErrorStep = 0 - var initErrorDelaySec = 15 - var maxErrorStep = 7 - var tokenRefreshInterval = getTokenRefreshInterval() + // Token refresh loop: proactively refreshes token to keep it valid + var currentErrorStep = 0; + var initErrorDelaySec = 15; + var maxErrorStep = 7; + + // Use app_refresh_interval from settings to configure token refresh frequency + var tokenRefreshIntervalSec = DEFAULT_TOKEN_REFRESH_SEC; + if (screenly.settings.app_refresh_interval) { + var intervalMinutes = parseInt(screenly.settings.app_refresh_interval, 10); + if (!isNaN(intervalMinutes) && intervalMinutes >= MIN_APP_REFRESH_MIN) { + tokenRefreshIntervalSec = intervalMinutes * 60; + } + } async function run() { - var nextTimeout = tokenRefreshInterval + var nextTimeout = tokenRefreshIntervalSec; try { - var newToken = await getEmbedToken() - await report.setAccessToken(newToken) - currentErrorStep = 0 - } catch { - nextTimeout = Math.min( - initErrorDelaySec * Math.pow(2, currentErrorStep), - nextTimeout, - ) + var newToken = await getEmbedToken(); + await report.setAccessToken(newToken); + currentErrorStep = 0; + } catch (error) { + console.error('Error refreshing token:', error); + clearStoredToken(); + // Exponential backoff on errors + nextTimeout = Math.min(initErrorDelaySec * Math.pow(2, currentErrorStep), tokenRefreshIntervalSec); if (currentErrorStep >= maxErrorStep) { - return + console.error('Max token refresh errors reached, stopping token refresh loop'); + return; } - currentErrorStep += 1 + currentErrorStep += 1; } - setTimeout(run, nextTimeout * 1000) + setTimeout(run, nextTimeout * 1000); } - setTimeout(run, tokenRefreshInterval * 1000) + setTimeout(run, tokenRefreshIntervalSec * 1000); + } + + function initAppRefreshLoop(report) { + // App refresh loop: refreshes report data at user-defined intervals + // This is separate from token refresh - only refreshes data, not the token + var refreshIntervalMin = DEFAULT_APP_REFRESH_MIN; + if (screenly.settings.app_refresh_interval) { + var parsed = parseInt(screenly.settings.app_refresh_interval, 10); + if (!isNaN(parsed) && parsed >= MIN_APP_REFRESH_MIN) { + refreshIntervalMin = parsed; + } else if (!isNaN(parsed) && parsed > 0 && parsed < MIN_APP_REFRESH_MIN) { + console.warn('App refresh interval must be at least 1 minute. Using minimum value of 1 minute.'); + refreshIntervalMin = MIN_APP_REFRESH_MIN; + } else { + console.warn('Invalid app refresh interval, using default of', DEFAULT_APP_REFRESH_MIN, 'minutes'); + } + } + + var refreshIntervalSec = refreshIntervalMin * 60; + + function refreshApp() { + try { + report.refresh(); + } catch (error) { + console.error('Error refreshing Power BI report:', error); + } + setTimeout(refreshApp, refreshIntervalSec * 1000); + } + + setTimeout(refreshApp, refreshIntervalSec * 1000); } async function initializePowerBI() { - const models = window['powerbi-client'].models - const embedUrl = screenly.settings.embed_url - const resourceType = getEmbedTypeFromUrl(embedUrl) - - const report = window.powerbi.embed( - document.getElementById('embed-container'), - { - embedUrl: embedUrl, - accessToken: await getEmbedToken(), - type: resourceType, - tokenType: models.TokenType.Embed, - permissions: models.Permissions.All, - settings: { - filterPaneEnabled: false, - navContentPaneEnabled: false, - }, + const models = window['powerbi-client'].models; + const embedUrl = screenly.settings.embed_url; + const resourceType = getEmbedTypeFromUrl(embedUrl); + + const report = window.powerbi.embed(document.getElementById('embed-container'), { + embedUrl: embedUrl, + accessToken: await getEmbedToken(), + type: resourceType, + tokenType: models.TokenType.Embed, + permissions: models.Permissions.All, + settings: { + filterPaneEnabled: false, + navContentPaneEnabled: false, }, - ) + }); + + // Listen for Power BI errors to detect invalid tokens + report.on('error', function(event) { + if (event.detail) { + var errorCode = event.detail.errorCode; + var errorMessage = event.detail.message || ''; + + // Detect token-related errors and clear cached token + if (errorCode === 'TokenExpired' || + errorCode === 'InvalidToken' || + errorCode === 'Unauthorized' || + errorMessage.toLowerCase().indexOf('token') !== -1 || + errorMessage.toLowerCase().indexOf('unauthorized') !== -1 || + errorMessage.toLowerCase().indexOf('expired') !== -1) { + console.warn('Power BI reported token error:', errorCode, errorMessage); + clearStoredToken(); + } + } + }); if (resourceType === 'report') { - report.on('rendered', screenly.signalReadyForRendering) + report.on('rendered', screenly.signalReadyForRendering); } else if (resourceType === 'dashboard') { report.on('loaded', () => { - setTimeout(screenly.signalReadyForRendering, 1000) - }) + setTimeout(screenly.signalReadyForRendering, 1000); + }); } if (!screenly.settings.embed_token) { - initTokenRefreshLoop(report) + initTokenRefreshLoop(report); } + + initAppRefreshLoop(report); + return report; } panic.configure({ handleErrors: screenly.settings.display_errors == 'true' || false, - }) + }); if (screenly.settings.display_errors == 'true') { - window.addEventListener('error', screenly.signalReadyForRendering) - window.addEventListener( - 'unhandledrejection', - screenly.signalReadyForRendering, - ) + window.addEventListener('error', screenly.signalReadyForRendering); + window.addEventListener('unhandledrejection', screenly.signalReadyForRendering); } - initializePowerBI() -})() + initializePowerBI(); +})(); From da880a076a940163f6e7ee03cec3b174b6e081b8 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Thu, 4 Dec 2025 17:52:42 +0530 Subject: [PATCH 06/12] update setting name --- edge-apps/powerbi/screenly.yml | 13 +++++-------- edge-apps/powerbi/screenly_qc.yml | 13 +++++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/edge-apps/powerbi/screenly.yml b/edge-apps/powerbi/screenly.yml index caab8269d..41b2f0212 100644 --- a/edge-apps/powerbi/screenly.yml +++ b/edge-apps/powerbi/screenly.yml @@ -36,17 +36,14 @@ settings: help_text: The URL of the Power BI dashboard or report to display. type: oauth:power_bi:embed_url schema_version: 1 - refresh_interval: +app_refresh_interval: type: string - default_value: '30' - title: Refresh Interval + title: App Refresh Interval (minutes) optional: true + default_value: '30' help_text: properties: advanced: true - help_text: | - Set the interval (in minutes) for periodically refreshing the Power BI dashboard or report. - The default value is 30 minutes, the minimum allowed is 1 minute. - Note: The embed token will be refreshed each time the dashboard or report is refreshed. + help_text: How often to refresh the Power BI report/dashboard (in minutes). Default is 30 minutes. Minimum is 1 minute. type: number - schema_version: 1 + schema_version: 1 \ No newline at end of file diff --git a/edge-apps/powerbi/screenly_qc.yml b/edge-apps/powerbi/screenly_qc.yml index a4751e204..98c97695e 100644 --- a/edge-apps/powerbi/screenly_qc.yml +++ b/edge-apps/powerbi/screenly_qc.yml @@ -36,17 +36,14 @@ settings: help_text: The URL of the Power BI dashboard or report to display. type: oauth:power_bi:embed_url schema_version: 1 - refresh_interval: +app_refresh_interval: type: string - default_value: '30' - title: Refresh Interval + title: App Refresh Interval (minutes) optional: true + default_value: '30' help_text: - schema_version: 1 properties: advanced: true - help_text: | - Set the interval (in minutes) for periodically refreshing the Power BI dashboard or report. - The default value is 30 minutes, the minimum allowed is 1 minute. - Note: The embed token will be refreshed each time the dashboard or report is refreshed. + help_text: How often to refresh the Power BI report/dashboard (in minutes). Default is 30 minutes. Minimum is 1 minute. type: number + schema_version: 1 From 46f311ca8085bc42f1410699b980fe0876d02708 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Fri, 5 Dec 2025 14:03:06 +0530 Subject: [PATCH 07/12] Revert "fix js lint as per custom config" This reverts commit 751406b4b937a60d5168c78e2edf8973a4f73944. : --- edge-apps/powerbi/static/js/main.js | 212 ++++++++-------------------- 1 file changed, 56 insertions(+), 156 deletions(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index 613fb7f31..10c61e45f 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -1,28 +1,24 @@ /* global screenly, panic */ (function () { - const DEFAULT_TOKEN_REFRESH_SEC = 30 * 60; - const TOKEN_STORAGE_KEY = 'powerbi_embed_token'; - const TOKEN_EXPIRY_STORAGE_KEY = 'powerbi_embed_token_expiry'; - const TOKEN_EXPIRY_BUFFER_SEC = 5 * 60; // Refresh 5 minutes before expiry - const DEFAULT_APP_REFRESH_MIN = 30; - const MIN_APP_REFRESH_MIN = 1; + const MIN_TOKEN_REFRESH_MIN = 1; // minimum 1 minute + const DEFAULT_TOKEN_REFRESH_MIN = 30; // default 30 minutes + + function getTokenRefreshInterval() { + // User provides interval in minutes, convert to seconds + var intervalMinutes = parseInt(screenly.settings.app_refresh_interval, 10); + if (isNaN(intervalMinutes) || intervalMinutes < MIN_TOKEN_REFRESH_MIN) { + return DEFAULT_TOKEN_REFRESH_MIN * 60; // convert to seconds + } + return intervalMinutes * 60; // convert minutes to seconds + } function getEmbedTypeFromUrl(url) { switch (true) { - case url.indexOf('/dashboard') !== -1: - return 'dashboard'; + case url.indexOf("/dashboard") !== -1: + return "dashboard"; default: - return 'report'; - } - } - - function clearStoredToken() { - try { - localStorage.removeItem(TOKEN_STORAGE_KEY); - localStorage.removeItem(TOKEN_EXPIRY_STORAGE_KEY); - } catch (error) { - console.warn('Could not clear token from localStorage:', error); + return "report"; } } @@ -32,92 +28,39 @@ return screenly.settings.embed_token; } - // Check localStorage for cached token to avoid unnecessary API calls - try { - const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY); - const storedExpiry = localStorage.getItem(TOKEN_EXPIRY_STORAGE_KEY); - - if (storedToken && storedExpiry) { - const expiryTime = parseInt(storedExpiry, 10); - const now = Date.now(); - const timeUntilExpiry = expiryTime - now; - const bufferMs = TOKEN_EXPIRY_BUFFER_SEC * 1000; - - // Reuse token if it's still valid (more than 5 minutes until expiry) - if (timeUntilExpiry > bufferMs) { - return storedToken; - } - clearStoredToken(); - } - } catch (error) { - console.warn('Could not access localStorage for token cache:', error); - } - - // Fetch new token from API - try { - var response = await fetch(screenly.settings.screenly_oauth_tokens_url + 'embed_token/', { - method: 'GET', + var response = await fetch( + screenly.settings.screenly_oauth_tokens_url + "embed_token/", + { + method: "GET", headers: { - Accept: 'application/json', + Accept: "application/json", Authorization: `Bearer ${screenly.settings.screenly_app_auth_token}`, }, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch embed token: ${response.status} ${response.statusText}`); - } + }, + ); - const { token } = await response.json(); - - if (!token) { - throw new Error('No token received from API'); - } - - // Store token with 1 hour expiry (Power BI tokens expire after 1 hour) - const actualExpiryTime = Date.now() + (60 * 60 * 1000); - - try { - localStorage.setItem(TOKEN_STORAGE_KEY, token); - localStorage.setItem(TOKEN_EXPIRY_STORAGE_KEY, actualExpiryTime.toString()); - } catch (storageError) { - console.warn('Could not store token in localStorage:', storageError); - } - - return token; - } catch (error) { - console.error('Error fetching embed token:', error); - throw error; - } + const { token } = await response.json(); + return token; } function initTokenRefreshLoop(report) { - // Token refresh loop: proactively refreshes token to keep it valid var currentErrorStep = 0; var initErrorDelaySec = 15; var maxErrorStep = 7; - - // Use app_refresh_interval from settings to configure token refresh frequency - var tokenRefreshIntervalSec = DEFAULT_TOKEN_REFRESH_SEC; - if (screenly.settings.app_refresh_interval) { - var intervalMinutes = parseInt(screenly.settings.app_refresh_interval, 10); - if (!isNaN(intervalMinutes) && intervalMinutes >= MIN_APP_REFRESH_MIN) { - tokenRefreshIntervalSec = intervalMinutes * 60; - } - } + var tokenRefreshInterval = getTokenRefreshInterval(); async function run() { - var nextTimeout = tokenRefreshIntervalSec; + var nextTimeout = tokenRefreshInterval; try { var newToken = await getEmbedToken(); await report.setAccessToken(newToken); currentErrorStep = 0; - } catch (error) { - console.error('Error refreshing token:', error); - clearStoredToken(); - // Exponential backoff on errors - nextTimeout = Math.min(initErrorDelaySec * Math.pow(2, currentErrorStep), tokenRefreshIntervalSec); + } catch { + nextTimeout = Math.min( + initErrorDelaySec * Math.pow(2, currentErrorStep), + nextTimeout, + ); if (currentErrorStep >= maxErrorStep) { - console.error('Max token refresh errors reached, stopping token refresh loop'); return; } currentErrorStep += 1; @@ -125,79 +68,33 @@ setTimeout(run, nextTimeout * 1000); } - setTimeout(run, tokenRefreshIntervalSec * 1000); - } - - function initAppRefreshLoop(report) { - // App refresh loop: refreshes report data at user-defined intervals - // This is separate from token refresh - only refreshes data, not the token - var refreshIntervalMin = DEFAULT_APP_REFRESH_MIN; - if (screenly.settings.app_refresh_interval) { - var parsed = parseInt(screenly.settings.app_refresh_interval, 10); - if (!isNaN(parsed) && parsed >= MIN_APP_REFRESH_MIN) { - refreshIntervalMin = parsed; - } else if (!isNaN(parsed) && parsed > 0 && parsed < MIN_APP_REFRESH_MIN) { - console.warn('App refresh interval must be at least 1 minute. Using minimum value of 1 minute.'); - refreshIntervalMin = MIN_APP_REFRESH_MIN; - } else { - console.warn('Invalid app refresh interval, using default of', DEFAULT_APP_REFRESH_MIN, 'minutes'); - } - } - - var refreshIntervalSec = refreshIntervalMin * 60; - - function refreshApp() { - try { - report.refresh(); - } catch (error) { - console.error('Error refreshing Power BI report:', error); - } - setTimeout(refreshApp, refreshIntervalSec * 1000); - } - - setTimeout(refreshApp, refreshIntervalSec * 1000); + setTimeout(run, tokenRefreshInterval * 1000); } async function initializePowerBI() { - const models = window['powerbi-client'].models; + const models = window["powerbi-client"].models; const embedUrl = screenly.settings.embed_url; const resourceType = getEmbedTypeFromUrl(embedUrl); - const report = window.powerbi.embed(document.getElementById('embed-container'), { - embedUrl: embedUrl, - accessToken: await getEmbedToken(), - type: resourceType, - tokenType: models.TokenType.Embed, - permissions: models.Permissions.All, - settings: { - filterPaneEnabled: false, - navContentPaneEnabled: false, + const report = window.powerbi.embed( + document.getElementById("embed-container"), + { + embedUrl: embedUrl, + accessToken: await getEmbedToken(), + type: resourceType, + tokenType: models.TokenType.Embed, + permissions: models.Permissions.All, + settings: { + filterPaneEnabled: false, + navContentPaneEnabled: false, + }, }, - }); - - // Listen for Power BI errors to detect invalid tokens - report.on('error', function(event) { - if (event.detail) { - var errorCode = event.detail.errorCode; - var errorMessage = event.detail.message || ''; - - // Detect token-related errors and clear cached token - if (errorCode === 'TokenExpired' || - errorCode === 'InvalidToken' || - errorCode === 'Unauthorized' || - errorMessage.toLowerCase().indexOf('token') !== -1 || - errorMessage.toLowerCase().indexOf('unauthorized') !== -1 || - errorMessage.toLowerCase().indexOf('expired') !== -1) { - console.warn('Power BI reported token error:', errorCode, errorMessage); - clearStoredToken(); - } - } - }); + ); - if (resourceType === 'report') { - report.on('rendered', screenly.signalReadyForRendering); - } else if (resourceType === 'dashboard') { - report.on('loaded', () => { + if (resourceType === "report") { + report.on("rendered", screenly.signalReadyForRendering); + } else if (resourceType === "dashboard") { + report.on("loaded", () => { setTimeout(screenly.signalReadyForRendering, 1000); }); } @@ -211,11 +108,14 @@ } panic.configure({ - handleErrors: screenly.settings.display_errors == 'true' || false, + handleErrors: screenly.settings.display_errors == "true" || false, }); - if (screenly.settings.display_errors == 'true') { - window.addEventListener('error', screenly.signalReadyForRendering); - window.addEventListener('unhandledrejection', screenly.signalReadyForRendering); + if (screenly.settings.display_errors == "true") { + window.addEventListener("error", screenly.signalReadyForRendering); + window.addEventListener( + "unhandledrejection", + screenly.signalReadyForRendering, + ); } initializePowerBI(); From eca677989c9c97bf3114cec971c2d699f3234bed Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Fri, 5 Dec 2025 14:29:26 +0530 Subject: [PATCH 08/12] fix yml lint --- edge-apps/powerbi/screenly.yml | 20 ++++++++++---------- edge-apps/powerbi/screenly_qc.yml | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/edge-apps/powerbi/screenly.yml b/edge-apps/powerbi/screenly.yml index 41b2f0212..49400d2e8 100644 --- a/edge-apps/powerbi/screenly.yml +++ b/edge-apps/powerbi/screenly.yml @@ -37,13 +37,13 @@ settings: type: oauth:power_bi:embed_url schema_version: 1 app_refresh_interval: - type: string - title: App Refresh Interval (minutes) - optional: true - default_value: '30' - help_text: - properties: - advanced: true - help_text: How often to refresh the Power BI report/dashboard (in minutes). Default is 30 minutes. Minimum is 1 minute. - type: number - schema_version: 1 \ No newline at end of file + type: string + title: App Refresh Interval (minutes) + optional: true + default_value: '30' + help_text: + properties: + advanced: true + help_text: How often to refresh the Power BI report/dashboard (in minutes). Default is 30 minutes. Minimum is 1 minute. + type: number + schema_version: 1 diff --git a/edge-apps/powerbi/screenly_qc.yml b/edge-apps/powerbi/screenly_qc.yml index 98c97695e..ebe628034 100644 --- a/edge-apps/powerbi/screenly_qc.yml +++ b/edge-apps/powerbi/screenly_qc.yml @@ -37,13 +37,13 @@ settings: type: oauth:power_bi:embed_url schema_version: 1 app_refresh_interval: - type: string - title: App Refresh Interval (minutes) - optional: true - default_value: '30' - help_text: - properties: - advanced: true - help_text: How often to refresh the Power BI report/dashboard (in minutes). Default is 30 minutes. Minimum is 1 minute. - type: number - schema_version: 1 + type: string + title: App Refresh Interval (minutes) + optional: true + default_value: '30' + help_text: + properties: + advanced: true + help_text: How often to refresh the Power BI report/dashboard (in minutes). Default is 30 minutes. Minimum is 1 minute. + type: number + schema_version: 1 From 5d5715f12aefa34b7d6de269a70b4bae1d2efc35 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Fri, 12 Dec 2025 15:16:14 +0530 Subject: [PATCH 09/12] Update edge-apps/powerbi/static/js/main.js Co-authored-by: rusko124 --- edge-apps/powerbi/static/js/main.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index 10c61e45f..58cb2cfae 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -5,12 +5,11 @@ const DEFAULT_TOKEN_REFRESH_MIN = 30; // default 30 minutes function getTokenRefreshInterval() { - // User provides interval in minutes, convert to seconds var intervalMinutes = parseInt(screenly.settings.app_refresh_interval, 10); if (isNaN(intervalMinutes) || intervalMinutes < MIN_TOKEN_REFRESH_MIN) { - return DEFAULT_TOKEN_REFRESH_MIN * 60; // convert to seconds + return DEFAULT_TOKEN_REFRESH_MIN * 60; } - return intervalMinutes * 60; // convert minutes to seconds + return intervalMinutes * 60; } function getEmbedTypeFromUrl(url) { From 4aa151ca264e934d6cacf7c07c39d5d8e46bb437 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Fri, 12 Dec 2025 15:16:24 +0530 Subject: [PATCH 10/12] Update edge-apps/powerbi/static/js/main.js Co-authored-by: rusko124 --- edge-apps/powerbi/static/js/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index 58cb2cfae..8d4aae243 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -1,8 +1,8 @@ /* global screenly, panic */ (function () { - const MIN_TOKEN_REFRESH_MIN = 1; // minimum 1 minute - const DEFAULT_TOKEN_REFRESH_MIN = 30; // default 30 minutes + const MIN_TOKEN_REFRESH_MIN = 1; + const DEFAULT_TOKEN_REFRESH_MIN = 30; function getTokenRefreshInterval() { var intervalMinutes = parseInt(screenly.settings.app_refresh_interval, 10); From c717064df8dbabae7edbb9e0c01e0c7f8cfe181d Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Fri, 12 Dec 2025 16:10:47 +0530 Subject: [PATCH 11/12] Update edge-apps/powerbi/static/js/main.js Co-authored-by: rusko124 --- edge-apps/powerbi/static/js/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index 8d4aae243..da8cca6d5 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -22,7 +22,6 @@ } async function getEmbedToken() { - // Use static token from settings if provided if (screenly.settings.embed_token) { return screenly.settings.embed_token; } From c9dac306a50e657c07e9e9b98d1de56f90d3b76e Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Fri, 12 Dec 2025 16:11:38 +0530 Subject: [PATCH 12/12] remove undef function call --- edge-apps/powerbi/static/js/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/edge-apps/powerbi/static/js/main.js b/edge-apps/powerbi/static/js/main.js index da8cca6d5..075243a57 100644 --- a/edge-apps/powerbi/static/js/main.js +++ b/edge-apps/powerbi/static/js/main.js @@ -101,7 +101,6 @@ initTokenRefreshLoop(report); } - initAppRefreshLoop(report); return report; }