-
-// Build page menus
-echo "";
-foreach ($buttons as $button) {
- annotate($button['file']);
- // include page specific stylesheets (if existing)
- $css = "/{$button['root']}/sheets/{$button['name']}";
- $css_stock = "$css.css";
- $css_theme = "$css-$theme.css";
- if (is_file($docroot.$css_stock)) echo '
',"\n";
- // create page content
- eval('?>'.parse_text($button['text']));
-}
-unset($buttons,$button);
-
-// Build page content
-// Reload page every X minutes during extended viewing?
-if (isset($myPage['Load']) && $myPage['Load'] > 0) {
- ?>
-
-
-}
-echo "
";
-$tab = 1;
-$pages = [];
+$taskPages = find_pages('Tasks');
+$buttonPages = find_pages('Buttons');
+$pages = []; // finds subpages
if (!empty($myPage['text'])) $pages[$myPage['name']] = $myPage;
if (_var($myPage,'Type')=='xmenu') $pages = array_merge($pages, find_pages($myPage['name']));
-if (isset($myPage['Tabs'])) $display['tabs'] = strtolower($myPage['Tabs'])=='true' ? 0 : 1;
-$tabbed = $display['tabs']==0 && count($pages)>1;
-foreach ($pages as $page) {
- $close = false;
- if (isset($page['Title'])) {
- eval("\$title=\"".htmlspecialchars($page['Title'])."\";");
- if ($tabbed) {
- echo "
";
- echo tab_title($title,$page['root'],_var($page,'Tag',false));
- echo " ";
- $close = true;
- } else {
- if ($tab==1) echo "
";
- echo "
";
- echo tab_title($title,$page['root'],_var($page,'Tag',false));
- echo "
";
- }
- $tab++;
- }
- if (isset($page['Type']) && $page['Type']=='menu') {
- $pgs = find_pages($page['name']);
- foreach ($pgs as $pg) {
- @eval("\$title=\"".htmlspecialchars($pg['Title'])."\";");
- $icon = _var($pg,'Icon',"
");
- if (substr($icon,-4)=='.png') {
- $root = $pg['root'];
- if (file_exists("$docroot/$root/images/$icon")) {
- $icon = "
";
- } elseif (file_exists("$docroot/$root/$icon")) {
- $icon = "
";
- } else {
- $icon = "
";
- }
- } elseif (substr($icon,0,5)=='icon-') {
- $icon = "
";
- } elseif ($icon[0]!='<') {
- if (substr($icon,0,3)!='fa-') $icon = "fa-$icon";
- $icon = "
";
- }
- echo "
";
- }
- }
- // create list of nchan scripts to be started
+// nchan related actions
+$nchan = ['webGui/nchan/notify_poller','webGui/nchan/session_check'];
+if ($wlan0) $nchan[] = 'webGui/nchan/wlan0';
+// build nchan scripts from found pages
+$allPages = array_merge($taskPages, $buttonPages, $pages);
+foreach ($allPages as $page) {
if (isset($page['Nchan'])) nchan_merge($page['root'], $page['Nchan']);
- annotate($page['file']);
- // include page specific stylesheets (if existing)
- $css = "/{$page['root']}/sheets/{$page['name']}";
- $css_stock = "$css.css";
- $css_theme = "$css-$theme.css";
- if (is_file($docroot.$css_stock)) echo '
',"\n";
- if (is_file($docroot.$css_theme)) echo '
',"\n";
- // create page content
- empty($page['Markdown']) || $page['Markdown']=='true' ? eval('?>'.Markdown(parse_text($page['text']))) : eval('?>'.parse_text($page['text']));
- if ($close) echo "
";
}
+// act on nchan scripts
if (count($pages)) {
$running = file_exists($nchan_pid) ? file($nchan_pid,FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) : [];
$start = array_diff($nchan, $running); // returns any new scripts to be started
@@ -874,496 +71,84 @@ if (count($pages)) {
}
if (count($running)) file_put_contents($nchan_pid,implode("\n",$running)."\n"); else @unlink($nchan_pid);
}
-unset($pages,$page,$pgs,$pg,$icon,$nchan,$running,$start,$stop,$row,$script,$opt,$nchan_run);
?>
-
-
-
-
+
+lang="=strtok($locale, '_') ?: 'en'?>" class="= $themeHelper->getThemeHtmlClass() ?>">
+
+
=_var($var, 'NAME')?>/=_var($myPage, 'name')?>
+
+
+
+
+
+
+
+
+
">
+
">
+
">
+
">
+
">
+
">
- require_once "$docroot/webGui/include/DefaultPageLayout/Footer.php"; ?>
+
">
+
">
+
">
+
">
+
">
-
+
-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');
- data = data.split('\0');
- switch (data[0]) {
- case 'addLog':
- var rows = document.getElementsByClassName('logLine');
- if (rows.length) {
- var row = rows[rows.length-1];
- row.innerHTML += data[1]+'
';
- }
- break;
- case 'progress':
- var rows = document.getElementsByClassName('progress-'+data[1]);
- if (rows.length) {
- rows[rows.length-1].textContent = data[2];
- }
- break;
- case 'addToID':
- var rows = document.getElementById(data[1]);
- if (rows === null) {
- rows = document.getElementsByClassName('logLine');
- if (rows.length) {
- var row = rows[rows.length-1];
- row.innerHTML += '
'+data[1]+': '+data[2]+' . ';
- }
- } else {
- var rows_content = rows.getElementsByClassName('content');
- if (!rows_content.length || rows_content[rows_content.length-1].textContent != data[2]) {
- rows.innerHTML += '
'+data[2]+' .';
- }
- }
- break;
- case 'show_Wait':
- progress_span[data[1]] = document.getElementById('wait-'+data[1]);
- progress_dots[data[1]] = setInterval(function(){if (((progress_span[data[1]].innerHTML += '.').match(/\./g)||[]).length > 9) progress_span[data[1]].innerHTML = progress_span[data[1]].innerHTML.replace(/\.+$/,'');},500);
- break;
- case 'stop_Wait':
- clearInterval(progress_dots[data[1]]);
- progress_span[data[1]].innerHTML = '';
- break;
- default:
- box.html(box.html()+data[0]);
- break;
- }
- box.scrollTop(box[0].scrollHeight);
-});
+ require_once "$docroot/webGui/include/DefaultPageLayout/HeadInlineJS.php"; ?>
-const scrollDuration = 500;
-$(window).scroll(function() {
- if ($(this).scrollTop() > 0) {
- $('.back_to_top').fadeIn(scrollDuration);
- } else {
- $('.back_to_top').fadeOut(scrollDuration);
- }
-
- var top = $('div#header').height()-1; // header height has 1 extra pixel to cover overlap
- $('div#menu').css($(this).scrollTop() > top ? {position:'fixed',top:'0'} : {position:'absolute',top:top+'px'});
- // banner
- $('div.upgrade_notice').css($(this).scrollTop() > 24 ? {position:'fixed',top:'0'} : {position:'absolute',top:'24px'});
-
-});
+'.parse_text($button['text']));
+}
-$('.move_to_end').click(function(event) {
- event.preventDefault();
- $('html,body').animate({scrollTop:$(document).height()},scrollDuration);
- return false;
-});
-
-$('.back_to_top').click(function(event) {
- event.preventDefault();
- $('html,body').animate({scrollTop:0},scrollDuration);
- return false;
-});
-
-
-$.post('/webGui/include/Notify.php',{cmd:'init',csrf_token:csrf_token});
-
-$(function() {
- defaultPage.start();
- $('div.spinner.fixed').html(unraid_logo);
- setTimeout(function(){$('div.spinner').not('.fixed').each(function(){$(this).html(unraid_logo);});},500); // display animation if page loading takes longer than 0.5s
- shortcut.add('F1',function(){HelpButton();});
-
- $('#licensetype').addClass('orange-text');
-
- $('#licensetype').addClass('red-text');
-
- $('input[value="=_("Apply")?>"],input[value="Apply"],input[name="cmdEditShare"],input[name="cmdUserEdit"]').prop('disabled',true);
- $('form').find('select,input[type=text],input[type=number],input[type=password],input[type=checkbox],input[type=radio],input[type=file],textarea').not('.lock').each(function(){$(this).on('input change',function() {
- var form = $(this).parentsUntil('form').parent();
- form.find('input[value="=_("Apply")?>"],input[value="Apply"],input[name="cmdEditShare"],input[name="cmdUserEdit"]').not('input.lock').prop('disabled',false);
- form.find('input[value="=_("Done")?>"],input[value="Done"]').not('input.lock').val("=_('Reset')?>").prop('onclick',null).off('click').click(function(){formHasUnsavedChanges=false;refresh(form.offset().top);});
- });});
- // add leave confirmation when form has changed without applying (opt-in function)
- if ($('form.js-confirm-leave').length>0) {
- $('form.js-confirm-leave').on('change',function(e){formHasUnsavedChanges=true;}).on('submit',function(e){formHasUnsavedChanges=false;});
- $(window).on('beforeunload',function(e){if (formHasUnsavedChanges) return '';}); // note: the browser creates its own popup window and warning message
- }
- // form parser: add escapeQuotes protection
- $('form').each(function(){
- var action = $(this).prop('action').actionName();
- if (action=='update.htm' || action=='update.php') {
- var onsubmit = $(this).attr('onsubmit')||'';
- $(this).attr('onsubmit','clearTimeout(timers.flashReport);escapeQuotes(this);'+onsubmit);
- }
- });
- var top = ($.cookie('top')||0) - $('.tabs').offset().top - 75;
- if (top>0) {$('html,body').scrollTop(top);}
- $.removeCookie('top');
- if ($.cookie('addAlert') != null) bannerAlert(addAlert.text,addAlert.cmd,addAlert.plg,addAlert.func);
-
- showNotice("=_('System running in')?>
=('safe mode')?> ");
-
-
- addBannerWarning("=_('System notifications are')?>
=_('disabled')?> . =_('Click')?>
=_('here')?> =_('to change notification settings')?>.",true,true);
-
-
- var opts = [];
- context.settings({above:false});
- opts.push({header:"=_('Notifications')?>"});
- opts.push({text:"=_('View')?>",icon:'fa-folder-open-o',action:function(e){e.preventDefault();openNotifier();}});
- opts.push({text:"=_('History')?>",icon:'fa-file-text-o',action:function(e){e.preventDefault();viewHistory();}});
- opts.push({text:"=_('Acknowledge')?>",icon:'fa-check-square-o',action:function(e){e.preventDefault();closeNotifier();}});
- context.attach('#board',opts);
- if (location.pathname.search(/\/(AddVM|UpdateVM|AddContainer|UpdateContainer)/)==-1) {
- $('blockquote.inline_help').each(function(i) {
- $(this).attr('id','helpinfo'+i);
- var pin = $(this).prev();
- if (!pin.prop('nodeName')) pin = $(this).parent().prev();
- while (pin.prop('nodeName') && pin.prop('nodeName').search(/(table|dl)/i)==-1) pin = pin.prev();
- pin.find('tr:first,dt:last').each(function() {
- var node = $(this);
- var name = node.prop('nodeName').toLowerCase();
- if (name=='dt') {
- while (!node.html() || node.html().search(/(
=0 || name!='dt') {
- if (name=='dt' && node.is(':first-of-type')) break;
- node = node.prev();
- name = node.prop('nodeName').toLowerCase();
- }
- node.css('cursor','help').click(function(){$('#helpinfo'+i).toggle('slow');});
- } else {
- if (node.html() && (name!='tr' || node.children('td:first').html())) node.css('cursor','help').click(function(){$('#helpinfo'+i).toggle('slow');});
- }
- });
- });
- }
- $('form').append($('
').attr({type:'hidden', name:'csrf_token', value:csrf_token}));
- setInterval(function(){if ($(document).height() > $(window).height()) $('.move_to_end').fadeIn(scrollDuration); else $('.move_to_end').fadeOut(scrollDuration);},250);
-});
-
-var gui_pages_available = [];
-
- $gui_pages = glob("/usr/local/emhttp/plugins/*/*.page");
- array_walk($gui_pages,function($value,$key){ ?>
- gui_pages_available.push('=basename($value,".page")?>');
- });
+foreach ($pages as $page) {
+ annotate($page['file']);
+ includePageStylesheets($page);
+}
?>
-function isValidURL(url) {
- try {
- var ret = new URL(url);
- return ret;
- } catch (err) {
- return false;
- }
-}
-
-$('body').on('click','a,.ca_href', function(e) {
- if ($(this).hasClass('ca_href')) {
- var ca_href = true;
- var href=$(this).attr('data-href');
- var target=$(this).attr('data-target');
- } else {
- var ca_href = false;
- var href = $(this).attr('href');
- var target = $(this).attr('target');
- }
- if (href) {
- href = href.trim();
- // Sanitize href to prevent XSS
- href = href.replace(/[<>"]/g, '');
- if (href.match('https?://[^\.]*.(my)?unraid.net/') || href.indexOf('https://unraid.net/') == 0 || href == 'https://unraid.net' || href.indexOf('http://lime-technology.com') == 0) {
- if (ca_href) window.open(href,target);
- return;
- }
- if (href !== '#' && href.indexOf('javascript') !== 0) {
- var dom = isValidURL(href);
- if (dom == false) {
- if (href.indexOf('/') == 0) return; // all internal links start with "/"
- var baseURLpage = href.split('/');
- if (gui_pages_available.includes(baseURLpage[0])) return;
- }
- if ($(this).hasClass('localURL')) return;
- try {
- var domainsAllowed = JSON.parse($.cookie('allowedDomains'));
- } catch(e) {
- var domainsAllowed = new Object();
- }
- $.cookie('allowedDomains',JSON.stringify(domainsAllowed),{expires:3650}); // rewrite cookie to further extend expiration by 400 days
- if (domainsAllowed[dom.hostname]) return;
- e.preventDefault();
- swal({
- title: "=_('External Link')?>",
- text: "
=_('Clicking OK will take you to a 3rd party website not associated with Lime Technology')?>"+href+" =_('Always Allow')?> "+dom.hostname+" ",
- html: true,
- animation: 'none',
- type: 'warning',
- showCancelButton: true,
- showConfirmButton: true,
- cancelButtonText: "=_('Cancel')?>",
- confirmButtonText: "=_('OK')?>"
- },function(isConfirm) {
- if (isConfirm) {
- if ($('#Link_Always_Allow').is(':checked')) {
- domainsAllowed[dom.hostname] = true;
- $.cookie('allowedDomains',JSON.stringify(domainsAllowed),{expires:3650});
- }
- var popupOpen = window.open(href,target);
- if (!popupOpen || popupOpen.closed || typeof popupOpen == 'undefined') {
- var popupWarning = addBannerWarning("=_('Popup Blocked');?>");
- setTimeout(function(){removeBannerWarning(popupWarning);},10000);
- }
- }
- });
- }
- }
-});
-
-// Only include window focus/blur event handlers when live updates are disabled
-// to prevent unnecessary page reloads when live updates are already handling data refreshes
-// nchanPaused / blurTimer used elsewhere so need to always be defined
-
-var nchanPaused = false;
-var blurTimer = false;
-
- if ( $display['liveUpdate'] == "no" ):?>
-$(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();
- } else {
- if (isset($myPage['Load']) && $myPage['Load'] > 0):?>
- if ( dialogOpen() ) {
- clearInterval(timers.reload);
- setTimerReload();
- nchanFocusStart();
- } else {
- window.location.reload();
- }
-
- 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("=_('Live Updates Paused');?>",false,true );
- }
- }
- }
-}
-
-
+
+
+
+ include "$docroot/webGui/include/DefaultPageLayout/Header.php"; ?>
+ include "$docroot/webGui/include/DefaultPageLayout/Navigation/Main.php"; ?>
+ include "$docroot/webGui/include/DefaultPageLayout/MainContent.php"; ?>
+ include "$docroot/webGui/include/DefaultPageLayout/Footer.php"; ?>
+ include "$docroot/webGui/include/DefaultPageLayout/MiscElements.php"; ?>
+ include "$docroot/webGui/include/DefaultPageLayout/BodyInlineJS.php"; ?>
diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout/BodyInlineJS.php b/emhttp/plugins/dynamix/include/DefaultPageLayout/BodyInlineJS.php
new file mode 100644
index 000000000..5b9359601
--- /dev/null
+++ b/emhttp/plugins/dynamix/include/DefaultPageLayout/BodyInlineJS.php
@@ -0,0 +1,485 @@
+
+
diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout/HeadInlineJS.php b/emhttp/plugins/dynamix/include/DefaultPageLayout/HeadInlineJS.php
new file mode 100644
index 000000000..f191553d6
--- /dev/null
+++ b/emhttp/plugins/dynamix/include/DefaultPageLayout/HeadInlineJS.php
@@ -0,0 +1,607 @@
+
+
diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout/Header.php b/emhttp/plugins/dynamix/include/DefaultPageLayout/Header.php
new file mode 100644
index 000000000..4a33aef78
--- /dev/null
+++ b/emhttp/plugins/dynamix/include/DefaultPageLayout/Header.php
@@ -0,0 +1,13 @@
+
diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout/MainContent.php b/emhttp/plugins/dynamix/include/DefaultPageLayout/MainContent.php
new file mode 100644
index 000000000..0364b470d
--- /dev/null
+++ b/emhttp/plugins/dynamix/include/DefaultPageLayout/MainContent.php
@@ -0,0 +1,100 @@
+
+/**
+ * Main content template for the Unraid web interface.
+ * Handles the rendering of tabs and page content.
+ */
+
+$defaultIcon = "
";
+// Helper function to process icon
+function process_icon($icon, $docroot, $root) {
+ global $defaultIcon;
+ if (substr($icon, -4) == '.png') {
+ if (file_exists("$docroot/$root/images/$icon")) {
+ return "
";
+ } elseif (file_exists("$docroot/$root/$icon")) {
+ return "
";
+ }
+ return $defaultIcon;
+ } elseif (substr($icon, 0, 5) == 'icon-') {
+ return "
";
+ } elseif ($icon[0] != '<') {
+ if (substr($icon, 0, 3) != 'fa-') {
+ $icon = "fa-$icon";
+ }
+ return "
";
+ }
+ return $icon;
+}
+
+$tab = 1;
+// even if DisplaySettings is not enabled for tabs, pages with Tabs="true" will use tabs
+$display['tabs'] = isset($myPage['Tabs']) ? (strtolower($myPage['Tabs']) == 'true' ? 0 : 1) : 1;
+$tabbed = $display['tabs'] == 0 && count($pages) > 1;
+?>
+
+
+ foreach ($pages as $page):
+ $close = false;
+ if (isset($page['Title'])):
+ $title = htmlspecialchars($page['Title']) ?? '';
+ if ($tabbed): ?>
+
+
+
+ = tab_title($title, $page['root'], _var($page, 'Tag', false)) ?>
+
+
+ $close = true;
+ else:
+ if ($tab == 1): ?>
+
+
+
+ endif; ?>
+
+
+ = tab_title($title, $page['root'], _var($page, 'Tag', false)) ?>
+
+
+ endif;
+ $tab++;
+ endif;
+
+ // Handle menu type pages
+ if (isset($page['Type']) && $page['Type'] == 'menu'):
+ $pgs = find_pages($page['name']);
+ foreach ($pgs as $pg):
+ // Set title variable with proper escaping (suppress errors)
+ @$title = htmlspecialchars($pg['Title']);
+ $icon = _var($pg, 'Icon', $defaultIcon);
+ $icon = process_icon($icon, $docroot, $pg['root']); ?>
+
+ endforeach;
+ endif;
+
+ // Annotate with HTML comment
+ annotate($page['file']);
+
+ // Create page content
+ if (empty($page['Markdown']) || $page['Markdown'] == 'true'):
+ eval('?>'.Markdown(parse_text($page['text'])));
+ else:
+ eval('?>'.parse_text($page['text']));
+ endif;
+
+ if ($close): ?>
+
+
+ endif;
+ endforeach; ?>
+
+
+
+// Clean up variables
+unset($pages, $page, $pgs, $pg, $icon, $nchan, $running, $start, $stop, $row, $script, $opt, $nchan_run);
+?>
diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout/MiscElements.php b/emhttp/plugins/dynamix/include/DefaultPageLayout/MiscElements.php
new file mode 100644
index 000000000..099e87f50
--- /dev/null
+++ b/emhttp/plugins/dynamix/include/DefaultPageLayout/MiscElements.php
@@ -0,0 +1,24 @@
+
+/**
+ * Global elements are elements that are used on all pages.
+ * Describe the purpose of each element here.
+ *
+ * - upgrade_notice: Display a notice to the user that they need to upgrade their browser.
+ * - move_to_end: Move to the end of the page.
+ * - back_to_top: Move to the top of the page.
+ * - spinner: Display the Unraid loader icon.
+ * - rebootNow: Global form to POST to /webGui/include/Boot.php?cmd=reboot to trigger a system reboot.
+ * - progressFrame: Display a frame to show the progress of the reboot.
+ */
+?>
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout/Navigation/Main.php b/emhttp/plugins/dynamix/include/DefaultPageLayout/Navigation/Main.php
new file mode 100644
index 000000000..2b248b534
--- /dev/null
+++ b/emhttp/plugins/dynamix/include/DefaultPageLayout/Navigation/Main.php
@@ -0,0 +1,84 @@
+
+/**
+ * Template is used for both top navigation and sidebar navigation.
+ *
+ * @var $themeHelper ThemeHelper
+ * @var $taskPages array
+ * @var $buttonPages array
+ * @var $display array
+ * @var $task string
+ */
+?>
+
diff --git a/emhttp/plugins/dynamix/include/Helpers.php b/emhttp/plugins/dynamix/include/Helpers.php
index 68474afcf..f22723a31 100644
--- a/emhttp/plugins/dynamix/include/Helpers.php
+++ b/emhttp/plugins/dynamix/include/Helpers.php
@@ -416,4 +416,156 @@ function device_exists($name)
global $disks,$devs;
return (array_key_exists($name, $disks) && !str_contains(_var($disks[$name],'status'),'_NP')) || (array_key_exists($name, $devs));
}
+
+
+# Check for process Core Types.
+function parse_cpu_ranges($file) {
+ if (!is_file($file)) return null;
+ $ranges = file_get_contents($file);
+ $ranges = trim($ranges);
+ $cores = [];
+ foreach (explode(',', $ranges) as $range) {
+ if (strpos($range, '-') !== false) {
+ list($start, $end) = explode('-', $range);
+ $cores = array_merge($cores, range((int)$start, (int)$end));
+ } else {
+ $cores[] = (int)$range;
+ }
+ }
+ return $cores;
+}
+
+function get_intel_core_types() {
+ $core_types = array();
+ $cpu_core_file = "/sys/devices/cpu_core/cpus";
+ $cpu_atom_file = "/sys/devices/cpu_atom/cpus";
+ $p_cores = parse_cpu_ranges($cpu_core_file);
+ $e_cores = parse_cpu_ranges($cpu_atom_file);
+ if ($p_cores) {
+ foreach ($p_cores as $core) {
+ $core_types[$core] = _("P-Core");
+ }
+ }
+ if ($e_cores) {
+ foreach ($e_cores as $core) {
+ $core_types[$core] = _("E-Core");
+ }
+ }
+ return $core_types;
+}
+
+function dmidecode($key,$n,$all=true) {
+ $entries = array_filter(explode($key,shell_exec("dmidecode -qt$n")??""));
+ $properties = [];
+ foreach ($entries as $entry) {
+ $property = [];
+ foreach (explode("\n",$entry) as $line) if (strpos($line,': ')!==false) {
+ [$key,$value] = my_explode(': ',trim($line));
+ $property[$key] = $value;
+ }
+ $properties[] = $property;
+ }
+ return $all ? $properties : $properties[0]??null;
+}
+
+function is_intel_cpu() {
+ $cpu = dmidecode('Processor Information','4',0);
+ $cpu_vendor = $cpu['Manufacturer'] ?? "";
+ $is_intel_cpu = stripos($cpu_vendor, "intel") !== false ? true : false;
+ return $is_intel_cpu;
+}
+// Load saved PCI data
+function loadSavedData($filename) {
+ if (file_exists($filename)) {
+ $saveddata = file_get_contents($filename);
+ } else $saveddata = "";
+
+ return json_decode($saveddata, true);
+}
+
+// Run lspci -Dmn to get the current devices
+function loadCurrentPCIData() {
+ $output = shell_exec('lspci -Dmn');
+ $devices = [];
+
+ if (file_exists("/boot/config/current.json")){
+ $devices = loadSavedData("/boot/config/current.json");
+ } else {
+ foreach (explode("\n", trim($output)) as $line) {
+ $parts = explode(" ", $line);
+
+ if (count($parts) < 6) continue; // Skip malformed lines
+
+ $description_str = shell_exec(("lspci -s ".$parts[0]));
+ $description = preg_replace('/^\S+\s+/', '', $description_str);
+
+ $device = [
+ 'class' => trim($parts[1], '"'),
+ 'vendor_id' => trim($parts[2], '"'),
+ 'device_id' => trim($parts[3], '"'),
+ 'description' => trim($description,'"'),
+ ];
+
+ $devices[$parts[0]] = $device;
+ }
+ }
+ return $devices;
+}
+
+// Compare the saved and current data
+function comparePCIData() {
+
+ $changes = [];
+ $saved = loadSavedData("/boot/config/savedpcidata.json");
+ if (!$saved) return [];
+ $current = loadCurrentPCIData();
+
+ // Compare saved devices with current devices
+ foreach ($saved as $pci_id => $saved_device) {
+ if (!isset($current[$pci_id])) {
+ // Device has been removed
+ $changes[$pci_id] = [
+ 'status' => 'removed',
+ 'device' => $saved_device
+ ];
+ } else {
+ // Device exists in both, check for modifications
+ $current_device = $current[$pci_id];
+ $differences = [];
+
+ // Compare fields
+ foreach (['vendor_id', 'device_id', 'class'] as $field) {
+ if (isset($saved_device[$field]) && isset($current_device[$field]) && $saved_device[$field] !== $current_device[$field]) {
+ $differences[$field] = [
+ 'old' => $saved_device[$field],
+ 'new' => $current_device[$field]
+ ];
+ }
+ }
+
+ if (!empty($differences)) {
+ $changes[$pci_id] = [
+ 'status' => 'changed',
+ 'device' => $current_device,
+ 'differences' => $differences
+ ];
+ }
+ }
+ }
+
+ // Check for added devices
+ foreach ($current as $pci_id => $current_device) {
+ if (!isset($saved[$pci_id])) {
+ // Device has been added
+ $changes[$pci_id] = [
+ 'status' => 'added',
+ 'device' => $current_device
+ ];
+ }
+ }
+ return $changes;
+}
+
+
+
?>
diff --git a/emhttp/plugins/dynamix/include/PCIUpdate.php b/emhttp/plugins/dynamix/include/PCIUpdate.php
new file mode 100644
index 000000000..2e6f195cd
--- /dev/null
+++ b/emhttp/plugins/dynamix/include/PCIUpdate.php
@@ -0,0 +1,96 @@
+
+
+$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
+require_once "$docroot/webGui/include/Helpers.php";
+require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
+
+
+function process_action($pciaddr,$action)
+{
+ global $saved,$current;
+ switch ($action) {
+ case 'removed':
+ unset($saved[$pciaddr]);
+ break;
+ case 'changed':
+ case 'added':
+ $saved[$pciaddr] = $current[$pciaddr];
+ break;
+ }
+}
+
+function build_pci_vm_map() {
+ global $lv;
+ $pci_device_changes = comparePCIData();
+ $vms = $lv->get_domains();
+ foreach ($vms as $vm) {
+ $vmpciids = $lv->domain_get_vm_pciids($vm);
+ foreach($vmpciids as $pciid => $pcidetail) {
+ if (isset($pci_device_changes["0000:".$pciid])) {
+ $pcitovm["0000:".$pciid][$vm] = $vm;
+ }
+ }
+ }
+ return $pcitovm;
+}
+
+$savedfile = "/boot/config/savedpcidata.json";
+$saved = loadSavedData($savedfile);
+if (!$saved) {echo "ERROR"; return;};
+$current = loadCurrentPCIData();
+$pciaddr = $_POST['pciid'];
+$action = $_POST['action']??'';
+
+switch($action) {
+case "all":
+ $pciaddrs = explode(";", $pciaddr);
+ foreach ($pciaddrs as $pciaddraction){
+ if ($pciaddraction == "") continue;
+ $values = explode(',',$pciaddraction);
+ process_action($values[0],$values[1]);
+ }
+ file_put_contents($savedfile,json_encode($saved,JSON_PRETTY_PRINT));
+ break;
+case "removed":
+case "added":
+case "changed":
+ process_action($pciaddr,$action);
+ file_put_contents($savedfile,json_encode($saved,JSON_PRETTY_PRINT));
+ break;
+case "getvm":
+ $pcimap = build_pci_vm_map();
+ $pciaddrs = explode(";", $pciaddr);
+ $vmact =[];
+
+ foreach ($pciaddrs as $pcidev) {
+ if ($pcidev == "") continue;
+ if (strpos($pcidev,",")) {
+ $values = explode(',',$pcidev);
+ $pcidev = $values[0];
+ }
+ foreach ($pcimap[$pcidev] as $key => $vmname) {
+ $vmact[$vmname]= $vmname;
+ }
+ }
+ $ret = implode(";",$vmact);
+
+ echo $ret;
+ exit;
+
+}
+
+file_put_contents($savedfile,json_encode($saved,JSON_PRETTY_PRINT));
+echo "OK";
+?>
diff --git a/emhttp/plugins/dynamix/include/PageBuilder.php b/emhttp/plugins/dynamix/include/PageBuilder.php
index 0bb42f052..8218e4578 100644
--- a/emhttp/plugins/dynamix/include/PageBuilder.php
+++ b/emhttp/plugins/dynamix/include/PageBuilder.php
@@ -97,6 +97,49 @@ function tab_title($title,$path,$tag) {
}
}
+/**
+ * Generate CSS for sidebar icons
+ *
+ * @param array $tasks Array of task pages
+ * @param array $buttons Array of button pages
+ * @return string CSS for sidebar icons
+ */
+function generate_sidebar_icon_css($tasks, $buttons) {
+ $css = '';
+
+ // Generate CSS for task icons
+ foreach ($tasks as $button) {
+ if (isset($button['Code'])) {
+ $css .= ".nav-item a[href='/{$button['name']}']:before{content:'\\{$button['Code']}'}\n";
+ }
+ }
+
+ // Add lock button icon
+ $css .= ".nav-item.LockButton a:before{content:'\\e955'}\n";
+
+ // Generate CSS for utility button icons
+ foreach ($buttons as $button) {
+ if (isset($button['Code'])) {
+ $css .= ".nav-item.{$button['name']} a:before{content:'\\{$button['Code']}'}\n";
+ }
+ }
+
+ return $css;
+}
+
+function includePageStylesheets($page) {
+ global $docroot, $theme;
+ $css = "/{$page['root']}/sheets/{$page['name']}";
+ $css_stock = "$css.css";
+ $css_theme = "$css-$theme.css"; // @todo add syslog for deprecation notice
+ if (is_file($docroot.$css_stock)) echo '
',"\n";
+ if (is_file($docroot.$css_theme)) echo '
',"\n";
+}
+
+function annotate($text) {
+ echo "\n\n";
+}
+
// hack to embed function output in a quoted string (e.g., in a page Title)
// see: http://stackoverflow.com/questions/6219972/why-embedding-functions-inside-of-strings-is-different-than-variables
function _func($x) {return $x;}
diff --git a/emhttp/plugins/dynamix/include/SysDevs.php b/emhttp/plugins/dynamix/include/SysDevs.php
index 32fbddb22..1feee62d0 100644
--- a/emhttp/plugins/dynamix/include/SysDevs.php
+++ b/emhttp/plugins/dynamix/include/SysDevs.php
@@ -18,6 +18,8 @@ require_once "$docroot/webGui/include/Helpers.php";
$_SERVER['REQUEST_URI'] = 'tools';
require_once "$docroot/webGui/include/Translations.php";
+$pci_device_diffs = comparePCIData();
+
function usb_physical_port($usbbusdev) {
if (preg_match('/^Bus (?P
\S+) Device (?P\S+): ID (?P\S+)(?P.*)$/', $usbbusdev, $usbMatch)) {
//udevadm info -a --name=/dev/bus/usb/003/002 | grep KERNEL==
@@ -115,6 +117,15 @@ case 't1':
$iommuinuse[] = (strpos($string,'/')) ? strstr($string, '/', true) : $string;
}
exec('lsscsi -s',$lsscsi);
+ // Filter for 'removed' devices
+ $removedArr = array_filter($pci_device_diffs, function($entry) {
+ return isset($entry['status']) && $entry['status'] === 'removed';
+ });
+ foreach ($removedArr as $removedpci => $removeddata) {
+ $groups[] = "IOMMU "._("Removed");
+ $groups[] = "\tR[{$removeddata['device']['vendor_id']}:{$removeddata['device']['device_id']}] ".str_replace("0000:","",$removedpci)." ".trim($removeddata['device']['description'],"\n");
+ }
+ $ackparm = "";
foreach ($groups as $line) {
if (!$line) continue;
if ($line[0]=='I') {
@@ -126,6 +137,8 @@ case 't1':
$line = preg_replace("/^\t/","",$line);
$vd = trim(explode(" ", $line)[0], "[]");
$pciaddress = explode(" ", $line)[1];
+ $removed = $line[0]=='R' ? true : false;
+ if ($removed) $line=preg_replace('/R/', '', $line, 1);
if (preg_match($BDF_REGEX, $pciaddress)) {
// By default lspci does not output the when the only domain in the system is 0000. Add it back.
$pciaddress = "0000:".$pciaddress;
@@ -140,13 +153,32 @@ case 't1':
if ((strpos($line, 'Host bridge') === false) && (strpos($line, 'PCI bridge') === false)) {
if (file_exists('/sys/kernel/iommu_groups/'.$iommu.'/devices/'.$pciaddress.'/reset')) echo " ";
echo "";
+ if (!$removed) {
echo in_array($iommu, $iommuinuse) ? ' | or just
echo (in_array($pciaddress."|".$vd, $vfio_cfg_devices) || in_array($pciaddress, $vfio_cfg_devices)) ? " checked>" : ">";
+ }
} else { echo " "; }
echo ' ',$line,' ';
+ if (array_key_exists($pciaddress,$pci_device_diffs)) {
+ echo "";
+ echo " ";
+ echo _("PCI Device change");
+ echo " "._("Action").":".ucfirst(_($pci_device_diffs[$pciaddress]['status']))." ";
+ $ackparm .= $pciaddress.",".$pci_device_diffs[$pciaddress]['status'].";";
+ if ($pci_device_diffs[$pciaddress]['status']!="removed") echo $pci_device_diffs[$pciaddress]['device']['description'];
+ echo " ";
+ if ($pci_device_diffs[$pciaddress]['status']=="changed") {
+ echo "";
+ echo _("Differences");
+ foreach($pci_device_diffs[$pciaddress]['differences'] as $key => $changes){
+ echo " $key "._("before").":{$changes['old']} "._("after").":{$changes['new']} ";
+ }
+ echo " ";
+ }
+ }
unset($outputvfio);
switch (true) {
case (strpos($line, 'USB controller') !== false):
@@ -194,8 +226,10 @@ case 't1':
if (file_exists("/var/log/vfio-pci") && filesize("/var/log/vfio-pci")) {
echo ' ';
}
+ if ($ackparm == "") $ackdisable =" disabled "; else $ackdisable = "";
echo ' ';
echo ' ';
+ echo ' ';
echo '';
echo <<
@@ -211,11 +245,19 @@ EOT;
}
break;
case 't2':
+ $is_intel_cpu = is_intel_cpu();
+ $core_types = $is_intel_cpu ? get_intel_core_types() : [];
exec('cat /sys/devices/system/cpu/*/topology/thread_siblings_list|sort -nu',$pairs);
$i = 1;
foreach ($pairs as $line) {
+ $line2 = $line;
$line = preg_replace(['/(\d+)[-,](\d+)/','/(\d+)\b/'],['$1 / $2','cpu $1'],$line);
- echo "".(strpos($line,'/')===false?"Single":"Pair ".$i++).": $line ";
+ if ($is_intel_cpu && count($core_types) > 0) {
+ [$cpu1, $cpu2] = my_preg_split('/[,-]/',$line2);
+ $core = $cpu1;
+ $core_type = "({$core_types[$core]})";
+ } else $core_type = "";
+ echo "".(strpos($line,'/')===false?"Single":"Pair ".$i++).": $line $core_type ";
}
break;
case 't3':
diff --git a/emhttp/plugins/dynamix/include/ThemeHelper.php b/emhttp/plugins/dynamix/include/ThemeHelper.php
new file mode 100644
index 000000000..13bd5ad1f
--- /dev/null
+++ b/emhttp/plugins/dynamix/include/ThemeHelper.php
@@ -0,0 +1,153 @@
+ self::COLOR_BLACK,
+ self::THEME_WHITE => self::COLOR_BLACK,
+ self::THEME_GRAY => self::COLOR_WHITE,
+ self::THEME_AZURE => self::COLOR_WHITE
+ ];
+
+ private const INIT_ERROR = 'ThemeHelper not initialized. Call initWithCurrentThemeSetting() first.';
+
+ private string $themeName;
+ private bool $topNavTheme;
+ private bool $sidebarTheme;
+ private bool $darkTheme;
+ private bool $lightTheme;
+ private string $fgcolor;
+ private bool $unlimitedWidth = false;
+
+ /**
+ * Constructor for ThemeHelper
+ *
+ * @param string|null $theme The theme name (optional)
+ * @param '1'|null $width The width of the theme (optional)
+ */
+ public function __construct(?string $theme = null, ?string $width = null) {
+ if ($theme === null) {
+ throw new \RuntimeException(self::INIT_ERROR);
+ }
+
+ $this->themeName = strtok($theme, '-');
+
+ $this->topNavTheme = in_array($this->themeName, self::TOP_NAV_THEMES);
+ $this->sidebarTheme = in_array($this->themeName, self::SIDEBAR_THEMES);
+ $this->darkTheme = in_array($this->themeName, self::DARK_THEMES);
+ $this->lightTheme = in_array($this->themeName, self::LIGHT_THEMES);
+ $this->fgcolor = self::FGCOLORS[$this->themeName] ?? self::COLOR_BLACK;
+
+ if ($width !== null) {
+ $this->setWidth($width);
+ }
+ }
+
+ /**
+ * Set the width setting
+ *
+ * @param string $width The width setting ('1' for unlimited, empty string for boxed)
+ * @return void
+ */
+ public function setWidth(string $width): void {
+ $this->unlimitedWidth = ($width === '1');
+ }
+
+ /**
+ * Check if unlimited width is enabled
+ *
+ * @return bool
+ */
+ public function isUnlimitedWidth(): bool {
+ return $this->unlimitedWidth;
+ }
+
+ public function getThemeName(): string {
+ return $this->themeName;
+ }
+
+ public function isTopNavTheme(): bool {
+ return $this->topNavTheme;
+ }
+
+ public function isSidebarTheme(): bool {
+ return $this->sidebarTheme;
+ }
+
+ public function isDarkTheme(): bool {
+ return $this->darkTheme;
+ }
+
+ public function isLightTheme(): bool {
+ return $this->lightTheme;
+ }
+
+ /**
+ * Get the theme HTML class string
+ *
+ * @return string
+ */
+ public function getThemeHtmlClass(): string {
+
+ $classes = ["Theme--{$this->themeName}"];
+
+ if ($this->sidebarTheme) {
+ $classes[] = "Theme--sidebar";
+ }
+
+ if ($this->topNavTheme) {
+ $classes[] = "Theme--nav-top";
+ }
+
+ $classes[] = $this->unlimitedWidth ? "Theme--width-unlimited" : "Theme--width-boxed";
+
+ return implode(' ', $classes);
+ }
+
+ public function getFgColor(): string {
+ return $this->fgcolor;
+ }
+
+ public function updateDockerLogColor(string $docroot): void {
+ exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:{$this->fgcolor};/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &");
+ }
+
+ /**
+ * Get all available theme names from the themes directory
+ *
+ * @param string $docroot The document root path
+ * @return array Array of theme names
+ */
+ public static function getThemesFromFileSystem(string $docroot): array {
+ $themes = [];
+ $themePath = "$docroot/webGui/styles/themes";
+ if (!is_dir($themePath)) {
+ error_log("Theme directory not found: $themePath");
+ return $themes;
+ }
+
+ $themeFiles = glob("$themePath/*.css");
+
+ foreach ($themeFiles as $themeFile) {
+ $themeName = basename($themeFile, '.css');
+ $themes[] = $themeName;
+ }
+
+ return $themes;
+ }
+}
diff --git a/emhttp/plugins/dynamix/styles/default-base.css b/emhttp/plugins/dynamix/styles/default-base.css
index f60aaffe1..2b8c91faf 100644
--- a/emhttp/plugins/dynamix/styles/default-base.css
+++ b/emhttp/plugins/dynamix/styles/default-base.css
@@ -1,6 +1,6 @@
html {
font-family: clear-sans, sans-serif;
- font-size: 62.5%;
+ font-size: var(--custom-font-size, 62.5%);
height: 100%;
}
body {
@@ -366,10 +366,15 @@ textarea {
z-index: 102;
margin: 0;
color: var(--inverse-text-color);
- background-color: var(--header-color);
+ background-color: var(--header-background-color);
background-size: 100% 90px;
background-repeat: no-repeat;
}
+
+#header.image { /* .image is conditionally added by DefaultPageLayout.php */
+ background-image: var(--customer-header-background-image);
+}
+
#header .logo {
float: left;
margin-left: 10px;
@@ -442,12 +447,12 @@ div.title.shift {
.nav-tile {
height: 4rem;
line-height: 4rem;
- display: block;
+ display: flex;
padding: 0;
margin: 0;
font-size: 1.2rem;
letter-spacing: 1.8px;
- background-color: var(--header-color);
+ background-color: var(--header-background-color);
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
@@ -458,11 +463,13 @@ div.title.shift {
}
.nav-tile.right {
text-align: right;
+ justify-content: flex-end;
}
.nav-item,
.nav-user {
position: relative;
- display: inline-block;
+ display: flex;
+ align-items: center;
text-align: center;
margin: 0;
}
@@ -477,7 +484,7 @@ div.title.shift {
padding-bottom: 2px;
}
.nav-item a {
- color: var(--inverse-text-color);
+ color: var(--header-text-color);
background-color: transparent;
text-transform: uppercase;
font-weight: bold;
@@ -1704,7 +1711,6 @@ span#wlan0 {
position: fixed;
height: 90px;
z-index: 100;
- background-color: var(--mild-background-color);
border-bottom: 1px solid var(--gray-600);
box-sizing: border-box;
padding-left: 80px;
@@ -2218,3 +2224,63 @@ span#wlan0 {
background-color: var(--brand-orange);
}
}
+
+/* Theme width styles */
+.Theme--width-boxed #displaybox {
+ min-width: 1280px;
+ max-width: 1280px;
+ margin: 0;
+}
+
+.Theme--width-unlimited #displaybox {
+ min-width: 1280px;
+ margin: 0;
+}
+
+/* Media queries for different screen sizes */
+@media (max-width: 1280px) {
+ .Theme--width-boxed #displaybox {
+ min-width: 1280px;
+ max-width: 1280px;
+ margin: 0;
+ }
+
+ .Theme--width-unlimited #displaybox {
+ min-width: 1280px;
+ margin: 0;
+ }
+}
+
+@media (min-width: 1281px) {
+ .Theme--width-boxed #displaybox {
+ min-width: 1280px;
+ max-width: 1920px;
+ margin: 0 auto;
+ }
+
+ .Theme--width-unlimited #displaybox {
+ min-width: 1280px;
+ margin: 0 auto;
+ }
+
+ .Theme--nav-top.Theme--width-unlimited #displaybox {
+ margin: 0 10px;
+ }
+}
+
+@media (min-width: 1921px) {
+ .Theme--width-boxed #displaybox {
+ min-width: 1280px;
+ max-width: 1920px;
+ margin: 0 auto;
+ }
+
+ .Theme--width-unlimited #displaybox {
+ min-width: 1280px;
+ margin: 0 auto;
+ }
+
+ .Theme--nav-top.Theme--width-unlimited #displaybox {
+ margin: 0 20px;
+ }
+}
diff --git a/emhttp/plugins/dynamix/styles/themes/README.md b/emhttp/plugins/dynamix/styles/themes/README.md
index 18646dbca..f832cc690 100644
--- a/emhttp/plugins/dynamix/styles/themes/README.md
+++ b/emhttp/plugins/dynamix/styles/themes/README.md
@@ -114,7 +114,7 @@ Files like `UserList.css`, `ShareList.css` that provide reusable styles:
### Theme Classes
- Main theme: `.Theme--{themename}`
-- Layout variants: `.Theme--sidebar`, `.Theme--topnav`
+- Layout variants: `.Theme--sidebar`, `.Theme--nav-top`
## Best Practices
diff --git a/emhttp/plugins/dynamix/styles/themes/azure.css b/emhttp/plugins/dynamix/styles/themes/azure.css
index 5aaca08f5..3c38e360a 100644
--- a/emhttp/plugins/dynamix/styles/themes/azure.css
+++ b/emhttp/plugins/dynamix/styles/themes/azure.css
@@ -64,7 +64,8 @@
--brand-orange: var(--orange-500);
--brand-red: var(--red-800);
- --header-color: var(--gray-100);
+ --header-text-color: var(--customer-header-text-color, var(--inverse-text-color));
+ --header-background-color: var(--custom-header-background-color, var(--gray-100));
--title-header-background-color: var(--mild-background-color);
diff --git a/emhttp/plugins/dynamix/styles/themes/black.css b/emhttp/plugins/dynamix/styles/themes/black.css
index 2fc8b5f2a..17d57889f 100644
--- a/emhttp/plugins/dynamix/styles/themes/black.css
+++ b/emhttp/plugins/dynamix/styles/themes/black.css
@@ -46,7 +46,8 @@
--brand-orange: var(--orange-500);
--brand-red: var(--red-800);
- --header-color: var(--gray-100);
+ --header-text-color: var(--customer-header-text-color, var(--inverse-text-color));
+ --header-background-color: var(--custom-header-background-color, var(--gray-100));
--title-header-background-color: var(--mild-background-color);
diff --git a/emhttp/plugins/dynamix/styles/themes/gray.css b/emhttp/plugins/dynamix/styles/themes/gray.css
index 16fe612b6..172c86323 100644
--- a/emhttp/plugins/dynamix/styles/themes/gray.css
+++ b/emhttp/plugins/dynamix/styles/themes/gray.css
@@ -58,7 +58,8 @@
--brand-orange: var(--orange-500);
--brand-red: var(--red-800);
- --header-color: var(--gray-100);
+ --header-text-color: var(--customer-header-text-color, var(--inverse-text-color));
+ --header-background-color: var(--custom-header-background-color, var(--gray-100));
--title-header-background-color: var(--mild-background-color);
diff --git a/emhttp/plugins/dynamix/styles/themes/white.css b/emhttp/plugins/dynamix/styles/themes/white.css
index 4384ececa..b834ccab3 100644
--- a/emhttp/plugins/dynamix/styles/themes/white.css
+++ b/emhttp/plugins/dynamix/styles/themes/white.css
@@ -47,7 +47,8 @@
--brand-orange: var(--orange-500);
--brand-red: var(--red-800);
- --header-color: var(--gray-900);
+ --header-text-color: var(--customer-header-text-color, var(--inverse-text-color));
+ --header-background-color: var(--custom-header-background-color, var(--gray-900));
--title-header-background-color: var(--gray-150);
diff --git a/etc/rc.d/rc.6 b/etc/rc.d/rc.6
index 8368d0f93..be06262b1 100755
--- a/etc/rc.d/rc.6
+++ b/etc/rc.d/rc.6
@@ -76,6 +76,10 @@ if [[ -x /sbin/hwclock ]]; then
fi
fi
+# Save PCI data to validate system changes at system start.
+log "Save PCI Configuration."
+/usr/local/sbin/savepcidata
+
# Run any local shutdown scripts:
if [[ -x /etc/rc.d/rc.local_shutdown ]]; then
/etc/rc.d/rc.local_shutdown stop
diff --git a/sbin/qemu b/sbin/qemu
index 905e464af..da3c4945f 100755
--- a/sbin/qemu
+++ b/sbin/qemu
@@ -6,4 +6,11 @@ if [ $DISABLE == "yes" ]
printf '\n%s\n' "Start/autostart is disabled in VM settings." >&2 ## Send message to stderr.
exit 1 ;
fi
+PCI="no"
+PCI=$(/usr/local/emhttp/plugins/dynamix.vm.manager/scripts/pcicheck.php "$@");
+if [ $PCI == "yes" ]
+ then
+ printf '\n%s\n' "Start/autostart is disabled PCI Change detected." >&2 ## Send message to stderr.
+ exit 1 ;
+fi
eval exec /usr/bin/qemu-system-x86_64 $(/usr/local/emhttp/plugins/dynamix.vm.manager/scripts/qemu.php "$@")
\ No newline at end of file
diff --git a/sbin/savepcidata b/sbin/savepcidata
new file mode 100755
index 000000000..23b2a918a
--- /dev/null
+++ b/sbin/savepcidata
@@ -0,0 +1,26 @@
+#!/usr/bin/php
+ trim($parts[1], '"'),
+ 'vendor_id' => trim($parts[2], '"'),
+ 'device_id' => trim($parts[3], '"'),
+ 'description' => trim($description,'"'),
+ ];
+
+ $devices[$parts[0]] = $device;
+ }
+
+ file_put_contents("/boot/config/savedpcidata.json",json_encode($devices,JSON_PRETTY_PRINT));
+?>
\ No newline at end of file