Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 25 additions & 29 deletions .github/scripts/generate-pr-plugin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -252,40 +252,33 @@ Link='nav-user'
---
<script>
$(function() {
// Check for updates (non-dismissible)
caPluginUpdateCheck("webgui-pr-PR_PLACEHOLDER.plg", {noDismiss: true},function(result){
// Surface the active PR test build as a persistent, keyed notification in the
// bell (not a banner): keyed so it dedupes across reloads, links to the
// Plugins page to remove it, and is cleared by the plugin's removal script.
function raisePrNotice() {
$.post('/webGui/include/Notify.php', {
cmd: 'add',
i: 'warning',
e: 'PR test build installed',
s: 'Modified GUI via webgui-pr-PR_PLACEHOLDER',
d: 'A pull-request test build is active. Remove it from Plugins before applying production updates.',
l: '/Plugins',
p: '1',
k: 'webgui-pr-PR_PLACEHOLDER'
});
}
// If the PR has been merged/removed upstream, clear the notice; otherwise raise it.
caPluginUpdateCheck("webgui-pr-PR_PLACEHOLDER.plg", {noDismiss: true}, function(result){
try {
let json = JSON.parse(result);
if ( ! json.version ) {
addBannerWarning("Note: webgui-pr-PR_PLACEHOLDER has either been merged or removed");
if (!json.version) {
$.post('/webGui/include/Notify.php', {cmd:'clear', k:'webgui-pr-PR_PLACEHOLDER'});
return;
}
} catch(e) {}
raisePrNotice();
});

// Create banner with uninstall link (nondismissible)
let bannerMessage = "Modified GUI installed via <b>webgui-pr-PR_PLACEHOLDER</b> plugin. " +
"<a onclick='uninstallPRPlugin()' style='cursor: pointer; text-decoration: underline;'>Click here to uninstall</a>";

// true = warning style, true = non-dismissible
addBannerWarning(bannerMessage, true, true);

// Define uninstall function
window.uninstallPRPlugin = function() {
swal({
title: "Uninstall PR Test Plugin?",
text: "This will reverse all of this PR's changes and remove the test plugin.",
type: "warning",
showCancelButton: true,
confirmButtonText: "Yes, uninstall",
cancelButtonText: "Cancel",
closeOnConfirm: false,
showLoaderOnConfirm: true
}, function(isConfirm) {
if (isConfirm) {
openPlugin("plugin remove webgui-pr-PR_PLACEHOLDER.plg", "Removing PR Test Plugin", "", "refresh");
}
});
};
raisePrNotice();
});
</script>
]]>
Expand Down Expand Up @@ -352,6 +345,9 @@ echo "Cleaning up plugin files..."
rm -rf "/usr/local/emhttp/plugins/webgui-pr-PR_PLACEHOLDER"
rm -rf "$PLUGIN_DIR"

# Clear the persistent "PR test build installed" notification raised by the Banner page.
/usr/local/emhttp/webGui/scripts/notify clear -k "webgui-pr-PR_PLACEHOLDER" 2>/dev/null || true

echo ""
echo "✅ Plugin removed successfully"
echo "⚠️ A reboot may be required to fully clear all changes"
Expand Down
6 changes: 6 additions & 0 deletions emhttp/plugins/dynamix.plugin.manager/scripts/PluginAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,19 @@ function download_url($url, $path = "") {
$existing = (array)@file("/tmp/reboot_notifications",FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$existing[] = $message;
file_put_contents("/tmp/reboot_notifications",implode("\n",array_unique($existing)));
// Surface "reboot required" as a persistent notification (sticky in the bell,
// keyed so repeated notices update rather than stack). It clears on reboot
// (RAM-backed /tmp is wiped) or via removeRebootNotice below.
exec("/usr/local/emhttp/webGui/scripts/notify -p -k reboot-required -i alert -e ".escapeshellarg("Reboot required")." -s ".escapeshellarg("Reboot required")." -d ".escapeshellarg($message)." &>/dev/null");
break;

case 'removeRebootNotice':
$message = htmlspecialchars(trim($_POST['message']));
$existing = file_get_contents("/tmp/reboot_notifications");
$newReboots = str_replace($message,"",$existing);
file_put_contents("/tmp/reboot_notifications",$newReboots);
// Once no reboot reasons remain, resolve the persistent notification.
if (trim($newReboots)==="") exec("/usr/local/emhttp/webGui/scripts/notify clear -k reboot-required &>/dev/null");
break;
}
?>
123 changes: 93 additions & 30 deletions emhttp/plugins/dynamix/include/DefaultPageLayout/HeadInlineJS.php
Original file line number Diff line number Diff line change
Expand Up @@ -379,35 +379,103 @@ function escapeQuotes(form) {
var osUpgradeWarning = false;
var forcedBanner = false;

// The legacy fixed yellow ".upgrade_notice" bar is retired. addBannerWarning now
// routes by intent:
// - Persistent conditions (noDismiss, not a transient "forced" in-progress
// banner) -> the notification bell, as a keyed (idempotent, deduped,
// header-reserved) notification that survives reloads and clears when
// resolved. Re-raising the same condition just updates the one entry.
// - Everything else -> a transient toast that auto-dismisses.
// removeBannerWarning(id) clears the bell notification or dismisses the toast.
var bannerToastSeq = 0;
var bannerRegistry = {}; // id -> { persistent: bool, key?: string }
// Per-page-load generation stamp. Legacy banners (CA, boot checks, ...) have no
// explicit clear: they re-render every load while active and simply stop when
// resolved. Each banner -> bell notification is stamped with this generation.
// Exposed on window so the notification drawer can hand it to the API, which
// authoritatively clears any 'banner-' notification not re-raised this load
// (see reconcileBannerNotifications).
var bannerGen = window.bannerGen = String(Date.now());

function bannerStableKey(text) {
var h = 0;
for (var i=0;i<text.length;i++) h = ((h<<5)-h+text.charCodeAt(i))|0;
return 'banner-' + (h>>>0).toString(36);
}

// Split a banner's HTML blob into plain text + a single action link.
function parseBanner(html) {
var tmp = document.createElement('div');
tmp.innerHTML = html;
var a = tmp.querySelector('a');
var link = null;
if (a) {
link = { href: a.getAttribute('href'), onclick: a.getAttribute('onclick'), label: (a.textContent||'').trim() };
a.remove();
}
return { text: (tmp.textContent||'').replace(/\s+/g,' ').trim(), link: link };
}

function addBannerWarning(text, warning=true, noDismiss=false, forced=false) {
var cookieText = text.replace(/[^a-z0-9]/gi,'');
if ($.cookie(cookieText) == "true") return false;
if (warning) text = "<i class='fa fa-warning fa-fw' style='float:initial'></i> "+text;
if (bannerWarnings.indexOf(text) < 0) {
if (forced) {
var arrayEntry = 0; bannerWarnings = []; clearTimeout(timers.bannerWarning); timers.bannerWarning = null; forcedBanner = true;
} else {
var arrayEntry = bannerWarnings.push("placeholder") - 1;
}
if (!noDismiss) text += "<a class='bannerDismiss' onclick='dismissBannerWarning("+arrayEntry+",&quot;"+cookieText+"&quot;)'></a>";
bannerWarnings[arrayEntry] = text;
} else {
return bannerWarnings.indexOf(text);
var id = "banner-" + (++bannerToastSeq);
var parsed = parseBanner(text);
var importance = (warning || noDismiss) ? "warning" : "info";

if (noDismiss && !forced) {
// Persistent condition -> notification bell (keyed for idempotent re-raise).
var key = bannerStableKey(parsed.text);
bannerRegistry[id] = { persistent: true, key: key };
$.post('/webGui/include/Notify.php', {
cmd: 'add',
i: importance === "warning" ? "warning" : "normal",
e: parsed.text,
s: '<?=_('System notice')?>',
d: '', // no description: the "Active" badge already conveys persistence
l: (parsed.link && parsed.link.href) ? parsed.link.href : '',
p: '1',
k: key,
g: bannerGen
});
return id;
}
if (timers.bannerWarning==null) showBannerWarnings();
return arrayEntry;

// Transient (or forced in-progress) -> toast.
bannerRegistry[id] = { persistent: false };
showBannerToast(parsed, importance, !!noDismiss, id, 0);
return id;
}

function showBannerToast(parsed, importance, persist, id, attempt) {
// globalThis.toast is registered when the toaster web component mounts; a
// banner may fire before then, so retry briefly.
if (!window.toast || !window.toast.warning) {
if (attempt < 40) setTimeout(function(){ showBannerToast(parsed, importance, persist, id, attempt+1); }, 150);
return;
}
var opts = { id: id, duration: persist ? Infinity : 8000, closeButton: true };
if (parsed.link) {
var l = parsed.link;
opts.action = { label: l.label || 'Open', onClick: function() {
if (l.href) window.location.href = l.href;
else if (l.onclick) { try { (new Function(l.onclick)).call(window); } catch(e) {} }
}};
}
var fn = window.toast[importance] || window.toast;
fn(parsed.text, opts);
}

function dismissBannerWarning(entry,cookieText) {
$.cookie(cookieText,"true",{expires:30}); // reset after 1 month
removeBannerWarning(entry);
}

function removeBannerWarning(entry) {
if (forcedBanner) return;
bannerWarnings[entry] = false;
clearTimeout(timers.bannerWarning);
showBannerWarnings();
var info = bannerRegistry[entry];
if (info && info.persistent && info.key) {
$.post('/webGui/include/Notify.php', { cmd: 'clear', k: info.key });
} else if (window.toast && window.toast.dismiss) {
window.toast.dismiss(entry);
}
delete bannerRegistry[entry];
}

function bannerFilterArray(array) {
Expand All @@ -432,14 +500,12 @@ function showBannerWarnings() {
}

function addRebootNotice(message="<?=_('You must reboot for changes to take effect')?>") {
addBannerWarning("<i class='fa fa-warning' style='float:initial;'></i> "+message,false,true);
// PluginAPI raises a persistent, keyed "reboot-required" bell notification
// (and persists it server-side), so no separate client banner is needed.
$.post("/plugins/dynamix.plugin.manager/scripts/PluginAPI.php",{action:'addRebootNotice',message:message});
}

function removeRebootNotice(message="<?=_('You must reboot for changes to take effect')?>") {
var bannerIndex = bannerWarnings.indexOf("<i class='fa fa-warning' style='float:initial;'></i> "+message);
if (bannerIndex < 0) return;
removeBannerWarning(bannerIndex);
$.post("/plugins/dynamix.plugin.manager/scripts/PluginAPI.php",{action:'removeRebootNotice',message:message});
}

Expand Down Expand Up @@ -493,7 +559,7 @@ function digits(number) {

function flashReport() {
$.post('/webGui/include/Report.php',{cmd:'config'},function(check){
if (check>0) addBannerWarning("<?=_('Your boot drive is corrupted or offline').'. '._('Post your diagnostics in the forum for help').'.'?> <a target='_blank' href='https://docs.unraid.net/go/changing-the-flash-device/'><?=_('See also here')?></a>");
if (check>0) addBannerWarning("<?=_('Your boot drive is corrupted or offline').'. '._('Post your diagnostics in the forum for help').'.'?> <a target='_blank' href='https://docs.unraid.net/go/changing-the-flash-device/'><?=_('See also here')?></a>",true,true);
});
}

Expand Down Expand Up @@ -522,11 +588,8 @@ function flashReport() {
updateTime();

Shadowbox.setup('a.sb-enable', {modal:true});
// add any pre-existing reboot notices
$.post('/webGui/include/Report.php',{cmd:'notice'},function(notices){
notices = notices.split('\n');
for (var i=0,notice; notice=notices[i]; i++) addBannerWarning("<i class='fa fa-warning' style='float:initial;'></i> "+notice,false,true);
});
// Pre-existing reboot notices now live in the notification bell (raised by
// PluginAPI when the reboot reason was added), so no client-side re-display.
// check for boot device offline / corrupted (delayed).
timers.flashReport = setTimeout(flashReport,6000);
});
Expand Down
10 changes: 10 additions & 0 deletions emhttp/plugins/dynamix/include/Notify.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,26 @@
case 'd':
case 'i':
case 'm':
case 'k':
case 'l':
case 'g':
$notify .= " -{$option} ".escapeshellarg($value);
break;
case 'x':
case 't':
case 'p':
$notify .= " -{$option}";
break;
}
}
shell_exec("$notify add");
break;

case 'clear':
// Resolve a keyed (condition-style / persistent) notification.
$key = $_POST['k']??'';
if ($key !== '') shell_exec("$notify clear -k ".escapeshellarg($key));
break;
case 'get':
echo shell_exec("$notify get");
break;
Expand Down
Loading
Loading