diff --git a/api/src/store/store-sync.ts b/api/src/store/store-sync.ts index 5463d11a8..4689ee97c 100644 --- a/api/src/store/store-sync.ts +++ b/api/src/store/store-sync.ts @@ -9,7 +9,6 @@ import { store } from '@app/store/index.js'; import { syncInfoApps } from '@app/store/sync/info-apps-sync.js'; import { syncRegistration } from '@app/store/sync/registration-sync.js'; import { FileLoadStatus } from '@app/store/types.js'; -import { setupConfigPathWatch } from '@app/store/watch/config-watch.js'; export const startStoreSync = async () => { // The last state is stored so we don't end up in a loop of writing -> reading -> writing @@ -45,6 +44,4 @@ export const startStoreSync = async () => { lastState = state; }); - - setupConfigPathWatch(); }; diff --git a/api/src/store/watch/config-watch.ts b/api/src/store/watch/config-watch.ts deleted file mode 100644 index fc6a80a05..000000000 --- a/api/src/store/watch/config-watch.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { existsSync, writeFileSync } from 'fs'; - -import { watch } from 'chokidar'; - -import { logger } from '@app/core/log.js'; -import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer.js'; -import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer.js'; -import { CHOKIDAR_USEPOLLING, ENVIRONMENT } from '@app/environment.js'; -import { getters, store } from '@app/store/index.js'; -import { initialState, loadConfigFile, logoutUser } from '@app/store/modules/config.js'; - -export const setupConfigPathWatch = () => { - const myServersConfigPath = getters.paths()?.['myservers-config']; - if (myServersConfigPath) { - logger.info('Watch Setup on Config Path: %s', myServersConfigPath); - if (!existsSync(myServersConfigPath)) { - const config = safelySerializeObjectToIni(getWriteableConfig(initialState, 'flash')); - writeFileSync(myServersConfigPath, config, 'utf-8'); - } - const watcher = watch(myServersConfigPath, { - persistent: true, - ignoreInitial: false, - usePolling: CHOKIDAR_USEPOLLING === true, - }) - .on('change', async (change) => { - logger.trace('Config File Changed, Reloading Config %s', change); - await store.dispatch(loadConfigFile()); - }) - .on('unlink', async () => { - const config = safelySerializeObjectToIni(getWriteableConfig(initialState, 'flash')); - await writeFileSync(myServersConfigPath, config, 'utf-8'); - watcher.close(); - setupConfigPathWatch(); - store.dispatch(logoutUser({ reason: 'Config File was Deleted' })); - }); - } else { - logger.error('[FATAL] Failed to setup watch on My Servers Config (Could Not Read Config Path)'); - } -}; diff --git a/api/src/unraid-api/cli/developer/developer.command.ts b/api/src/unraid-api/cli/developer/developer.command.ts index 9252d59e0..1224dec15 100644 --- a/api/src/unraid-api/cli/developer/developer.command.ts +++ b/api/src/unraid-api/cli/developer/developer.command.ts @@ -6,7 +6,8 @@ import { loadConfigFile, updateUserConfig } from '@app/store/modules/config.js'; import { writeConfigSync } from '@app/store/sync/config-disk-sync.js'; import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js'; import { LogService } from '@app/unraid-api/cli/log.service.js'; -import { RestartCommand } from '@app/unraid-api/cli/restart.command.js'; +import { StartCommand } from '@app/unraid-api/cli/start.command.js'; +import { StopCommand } from '@app/unraid-api/cli/stop.command.js'; interface DeveloperOptions { disclaimer: boolean; @@ -21,7 +22,8 @@ export class DeveloperCommand extends CommandRunner { constructor( private logger: LogService, private readonly inquirerService: InquirerService, - private readonly restartCommand: RestartCommand + private readonly startCommand: StartCommand, + private readonly stopCommand: StopCommand ) { super(); } @@ -33,6 +35,7 @@ export class DeveloperCommand extends CommandRunner { } const { store } = await import('@app/store/index.js'); await store.dispatch(loadConfigFile()); + await this.stopCommand.run([]); store.dispatch(updateUserConfig({ local: { sandbox: options.sandbox ? 'yes' : 'no' } })); writeConfigSync('flash'); @@ -40,6 +43,6 @@ export class DeveloperCommand extends CommandRunner { 'Updated Developer Configuration - restart the API in 5 seconds to apply them...' ); await new Promise((resolve) => setTimeout(resolve, 5000)); - await this.restartCommand.run([]); + await this.startCommand.run([], {}); } } diff --git a/api/src/unraid-api/cli/sso/add-sso-user.command.ts b/api/src/unraid-api/cli/sso/add-sso-user.command.ts index f6d843390..cb5cdbeff 100644 --- a/api/src/unraid-api/cli/sso/add-sso-user.command.ts +++ b/api/src/unraid-api/cli/sso/add-sso-user.command.ts @@ -9,6 +9,8 @@ import { writeConfigSync } from '@app/store/sync/config-disk-sync.js'; import { LogService } from '@app/unraid-api/cli/log.service.js'; import { RestartCommand } from '@app/unraid-api/cli/restart.command.js'; import { AddSSOUserQuestionSet } from '@app/unraid-api/cli/sso/add-sso-user.questions.js'; +import { StartCommand } from '@app/unraid-api/cli/start.command.js'; +import { StopCommand } from '@app/unraid-api/cli/stop.command.js'; interface AddSSOUserCommandOptions { disclaimer: string; @@ -25,7 +27,8 @@ export class AddSSOUserCommand extends CommandRunner { constructor( private readonly logger: LogService, private readonly inquirerService: InquirerService, - private readonly restartCommand: RestartCommand + private readonly startCommand: StartCommand, + private readonly stopCommand: StopCommand ) { super(); } @@ -34,16 +37,12 @@ export class AddSSOUserCommand extends CommandRunner { try { options = await this.inquirerService.prompt(AddSSOUserQuestionSet.name, options); if (options.disclaimer === 'y' && options.username) { + await this.stopCommand.run([]); await store.dispatch(loadConfigFile()); - const shouldRestart = store.getState().config.remote.ssoSubIds.length === 0; store.dispatch(addSsoUser(options.username)); writeConfigSync('flash'); - this.logger.info(`User added ${options.username}`); - if (shouldRestart) { - this.logger.info('Restarting the Unraid API in 5 seconds to enable the SSO button'); - await new Promise((resolve) => setTimeout(resolve, 5000)); - await this.restartCommand.run([]); - } + this.logger.info(`User added ${options.username}, starting the API`); + await this.startCommand.run([], {}); } } catch (e: unknown) { if (e instanceof Error) { diff --git a/api/src/unraid-api/cli/sso/remove-sso-user.command.ts b/api/src/unraid-api/cli/sso/remove-sso-user.command.ts index 10a1ab405..9d60cef5f 100644 --- a/api/src/unraid-api/cli/sso/remove-sso-user.command.ts +++ b/api/src/unraid-api/cli/sso/remove-sso-user.command.ts @@ -1,12 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { CommandRunner, InquirerService, Option, OptionChoiceFor, SubCommand } from 'nest-commander'; +import { CommandRunner, InquirerService, Option, SubCommand } from 'nest-commander'; import { store } from '@app/store/index.js'; import { loadConfigFile, removeSsoUser } from '@app/store/modules/config.js'; import { writeConfigSync } from '@app/store/sync/config-disk-sync.js'; import { LogService } from '@app/unraid-api/cli/log.service.js'; import { RemoveSSOUserQuestionSet } from '@app/unraid-api/cli/sso/remove-sso-user.questions.js'; +import { StartCommand } from '@app/unraid-api/cli/start.command.js'; +import { StopCommand } from '@app/unraid-api/cli/stop.command.js'; interface RemoveSSOUserCommandOptions { username: string; @@ -21,13 +23,17 @@ interface RemoveSSOUserCommandOptions { export class RemoveSSOUserCommand extends CommandRunner { constructor( private readonly logger: LogService, - private readonly inquirerService: InquirerService + private readonly inquirerService: InquirerService, + private readonly stopCommand: StopCommand, + private readonly startCommand: StartCommand ) { super(); } public async run(_input: string[], options: RemoveSSOUserCommandOptions): Promise { await store.dispatch(loadConfigFile()); options = await this.inquirerService.prompt(RemoveSSOUserQuestionSet.name, options); + + await this.stopCommand.run([]); store.dispatch(removeSsoUser(options.username === 'all' ? null : options.username)); if (options.username === 'all') { this.logger.info('All users removed from SSO'); @@ -35,6 +41,7 @@ export class RemoveSSOUserCommand extends CommandRunner { this.logger.info('User removed: ' + options.username); } writeConfigSync('flash'); + await this.startCommand.run([], {}); } @Option({ diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time index 9cd6b4405..3b38c58f0 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time @@ -1 +1 @@ -1742838682466 \ No newline at end of file +1743604406580 \ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php index 7fbb2469d..b1d62612b 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php @@ -105,10 +105,14 @@ if (!file_exists($notes)) file_put_contents($notes,shell_exec("$docroot/plugins/ \n"; +if (isset($myPage['Load']) && $myPage['Load'] > 0) echo "\n\n"; echo "
"; $tab = 1; $pages = []; @@ -864,7 +903,7 @@ function parseINI(msg) { // unraid animated logo var unraid_logo = ''; -var defaultPage = new NchanSubscriber('/sub/session,var',{subscriber:'websocket'}); +var defaultPage = new NchanSubscriber('/sub/session,var',{subscriber:'websocket', reconnectTimeout:5000}); defaultPage.on('message', function(msg,meta) { switch (meta.id.channel()) { case 0: @@ -933,7 +972,7 @@ function wlanSettings() { window.location = '/Settings/NetworkSettings'; } -var nchan_wlan0 = new NchanSubscriber('/sub/wlan0',{subscriber:'websocket'}); +var nchan_wlan0 = new NchanSubscriber('/sub/wlan0',{subscriber:'websocket', reconnectTimeout:5000}); nchan_wlan0.on('message', function(msg) { var wlan = JSON.parse(msg); $('#wlan0').removeClass().addClass(wlan.color).attr('title',wlan.title); @@ -941,7 +980,7 @@ nchan_wlan0.on('message', function(msg) { nchan_wlan0.start(); -var nchan_plugins = new NchanSubscriber('/sub/plugins',{subscriber:'websocket'}); +var nchan_plugins = new NchanSubscriber('/sub/plugins',{subscriber:'websocket', reconnectTimeout:5000}); nchan_plugins.on('message', function(data) { if (!data || openDone(data)) return; var box = $('pre#swaltext'); @@ -954,7 +993,7 @@ nchan_plugins.on('message', function(data) { box.html(text.join('
')).scrollTop(box[0].scrollHeight); }); -var nchan_docker = new NchanSubscriber('/sub/docker',{subscriber:'websocket'}); +var nchan_docker = new NchanSubscriber('/sub/docker',{subscriber:'websocket', reconnectTimeout:5000}); nchan_docker.on('message', function(data) { if (!data || openDone(data)) return; var box = $('pre#swaltext'); @@ -1003,7 +1042,7 @@ nchan_docker.on('message', function(data) { box.scrollTop(box[0].scrollHeight); }); -var nchan_vmaction = new NchanSubscriber('/sub/vmaction',{subscriber:'websocket'}); +var nchan_vmaction = new NchanSubscriber('/sub/vmaction',{subscriber:'websocket', reconnectTimeout:5000}); nchan_vmaction.on('message', function(data) { if (!data || openDone(data) || openError(data)) return; var box = $('pre#swaltext'); @@ -1232,6 +1271,76 @@ $('body').on('click','a,.ca_href', function(e) { } } }); + +// Start & stop live updates when window loses focus +var nchanPaused = false; +var blurTimer = false; + +$(window).focus(function() { + nchanFocusStart(); +}); + +// Stop nchan on loss of focus + +$(window).blur(function() { + blurTimer = setTimeout(function(){ + nchanFocusStop(); + },30000); +}); + + +document.addEventListener("visibilitychange", (event) => { + + if (document.hidden) { + nchanFocusStop(); + } + + if (document.hidden) { + nchanFocusStop(); + } else { + nchanFocusStart(); + } + +}); + +function nchanFocusStart() { + if ( blurTimer !== false ) { + clearTimeout(blurTimer); + blurTimer = false; + } + + if (nchanPaused !== false ) { + removeBannerWarning(nchanPaused); + nchanPaused = false; + + try { + pageFocusFunction(); + } catch(error) {} + + subscribers.forEach(function(e) { + e.start(); + }); + } +} + +function nchanFocusStop(banner=true) { + if ( subscribers.length ) { + if ( nchanPaused === false ) { + var newsub = subscribers; + subscribers.forEach(function(e) { + try { + e.stop(); + } catch(err) { + newsub.splice(newsub.indexOf(e,1)); + } + }); + subscribers = newsub; + if ( banner && subscribers.length ) { + nchanPaused = addBannerWarning("",false,true ); + } + } + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time index c371b7b34..dd6b13df7 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time @@ -1 +1 @@ -1742838681735 \ No newline at end of file +1743604406046 \ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time index 52f35376e..a2e18b78c 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time @@ -1 +1 @@ -1742838682135 \ No newline at end of file +1743604406299 \ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time index d0ea81d22..969490b50 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time @@ -1 +1 @@ -1742838682936 \ No newline at end of file +1743604406833 \ No newline at end of file diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php index a299ab856..83e0be7da 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php +++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php @@ -105,10 +105,14 @@ if (!file_exists($notes)) file_put_contents($notes,shell_exec("$docroot/plugins/ \n"; +if (isset($myPage['Load']) && $myPage['Load'] > 0) echo "\n\n"; echo "
"; $tab = 1; $pages = []; @@ -855,7 +894,7 @@ function parseINI(msg) { // unraid animated logo var unraid_logo = ''; -var defaultPage = new NchanSubscriber('/sub/session,var',{subscriber:'websocket'}); +var defaultPage = new NchanSubscriber('/sub/session,var',{subscriber:'websocket', reconnectTimeout:5000}); defaultPage.on('message', function(msg,meta) { switch (meta.id.channel()) { case 0: @@ -916,7 +955,7 @@ function wlanSettings() { window.location = '/Settings/NetworkSettings'; } -var nchan_wlan0 = new NchanSubscriber('/sub/wlan0',{subscriber:'websocket'}); +var nchan_wlan0 = new NchanSubscriber('/sub/wlan0',{subscriber:'websocket', reconnectTimeout:5000}); nchan_wlan0.on('message', function(msg) { var wlan = JSON.parse(msg); $('#wlan0').removeClass().addClass(wlan.color).attr('title',wlan.title); @@ -924,7 +963,7 @@ nchan_wlan0.on('message', function(msg) { nchan_wlan0.start(); -var nchan_plugins = new NchanSubscriber('/sub/plugins',{subscriber:'websocket'}); +var nchan_plugins = new NchanSubscriber('/sub/plugins',{subscriber:'websocket', reconnectTimeout:5000}); nchan_plugins.on('message', function(data) { if (!data || openDone(data)) return; var box = $('pre#swaltext'); @@ -937,7 +976,7 @@ nchan_plugins.on('message', function(data) { box.html(text.join('
')).scrollTop(box[0].scrollHeight); }); -var nchan_docker = new NchanSubscriber('/sub/docker',{subscriber:'websocket'}); +var nchan_docker = new NchanSubscriber('/sub/docker',{subscriber:'websocket', reconnectTimeout:5000}); nchan_docker.on('message', function(data) { if (!data || openDone(data)) return; var box = $('pre#swaltext'); @@ -986,7 +1025,7 @@ nchan_docker.on('message', function(data) { box.scrollTop(box[0].scrollHeight); }); -var nchan_vmaction = new NchanSubscriber('/sub/vmaction',{subscriber:'websocket'}); +var nchan_vmaction = new NchanSubscriber('/sub/vmaction',{subscriber:'websocket', reconnectTimeout:5000}); nchan_vmaction.on('message', function(data) { if (!data || openDone(data) || openError(data)) return; var box = $('pre#swaltext'); @@ -1215,6 +1254,76 @@ $('body').on('click','a,.ca_href', function(e) { } } }); + +// Start & stop live updates when window loses focus +var nchanPaused = false; +var blurTimer = false; + +$(window).focus(function() { + nchanFocusStart(); +}); + +// Stop nchan on loss of focus + +$(window).blur(function() { + blurTimer = setTimeout(function(){ + nchanFocusStop(); + },30000); +}); + + +document.addEventListener("visibilitychange", (event) => { + + if (document.hidden) { + nchanFocusStop(); + } + + if (document.hidden) { + nchanFocusStop(); + } else { + nchanFocusStart(); + } + +}); + +function nchanFocusStart() { + if ( blurTimer !== false ) { + clearTimeout(blurTimer); + blurTimer = false; + } + + if (nchanPaused !== false ) { + removeBannerWarning(nchanPaused); + nchanPaused = false; + + try { + pageFocusFunction(); + } catch(error) {} + + subscribers.forEach(function(e) { + e.start(); + }); + } +} + +function nchanFocusStop(banner=true) { + if ( subscribers.length ) { + if ( nchanPaused === false ) { + var newsub = subscribers; + subscribers.forEach(function(e) { + try { + e.stop(); + } catch(err) { + newsub.splice(newsub.indexOf(e,1)); + } + }); + subscribers = newsub; + if ( banner && subscribers.length ) { + nchanPaused = addBannerWarning("",false,true ); + } + } + } +} diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch index 16a3ba33a..0966cd1fa 100644 --- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch +++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch @@ -2,9 +2,9 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php =================================================================== --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php original +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php modified -@@ -556,20 +556,11 @@ - return 'three'; +@@ -591,20 +591,11 @@ } + function openNotifier() { $.post('/webGui/include/Notify.php',{cmd:'get',csrf_token:csrf_token},function(msg) { $.each($.parseJSON(msg), function(i, notify){ @@ -22,9 +22,9 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); }); } + function closeNotifier() { - $.post('/webGui/include/Notify.php',{cmd:'get',csrf_token:csrf_token},function(msg) { -@@ -698,12 +689,12 @@ +@@ -737,12 +728,12 @@ } // create list of nchan scripts to be started if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']); @@ -38,7 +38,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php foreach ($buttons as $button) { annotate($button['file']); // include page specific stylesheets (if existing) -@@ -905,26 +896,18 @@ +@@ -944,26 +935,18 @@ case 'warning': bell2++; break; case 'normal' : bell3++; break; } @@ -70,11 +70,11 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php }); -@@ -1231,7 +1214,8 @@ - }); +@@ -1340,7 +1323,8 @@ + } } } - }); + } +