feat(web): plugin install auto-enables + persistent restart nudge

Getting a plugin onto the display used to take three disconnected manual
steps: install from the store, flip its enable toggle, then restart the
display service - with no in-UI hint that steps 2 and 3 were needed (only
docs/GETTING_STARTED.md mentions it).

- installPlugin() now enables the plugin immediately on successful install
  (owner-confirmed behavior change: always auto-enable, no opt-out; users
  who don't want it running toggle it off as before), then shows a
  persistent toast ("... restart the display to show it") with an inline
  "Restart Now" button wired to the existing restartDisplay() - the same
  function the three existing Restart Display buttons call.
- notification.js: show() accepts optional { actionLabel, onAction } to
  render one inline action button per toast. Callbacks are stored per
  notification id and cleaned up on dismiss; a new triggerAction() public
  method runs the callback and dismisses. The global showNotification()
  shorthand now forwards a full options object as its second argument
  (legacy type-string calls unchanged).

Scope note: applies to the plugin store's install path (window.installPlugin).
The custom-registry install path keeps its existing behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-16 10:28:41 -04:00
co-authored by Claude Sonnet 5
parent af991d4cca
commit 242cd2a943
2 changed files with 46 additions and 2 deletions
@@ -58,6 +58,9 @@
// Track active notifications
let activeNotifications = [];
// onAction callbacks for notifications with an inline action button,
// keyed by notification id (cleaned up on dismiss).
const actionCallbacks = {};
let notificationCounter = 0;
/**
@@ -113,6 +116,7 @@
// Remove from tracking array
activeNotifications = activeNotifications.filter(id => id !== notificationId);
delete actionCallbacks[notificationId];
}
/**
@@ -158,6 +162,20 @@
html += `<span class="flex-1 text-sm">${escapeHtml(message)}</span>`;
// Optional inline action button (e.g. "Restart Now" on a restart nudge).
// The callback is stored by id and invoked via triggerAction, which
// also dismisses the notification.
if (options.actionLabel && typeof options.onAction === 'function') {
actionCallbacks[notificationId] = options.onAction;
html += `
<button type="button"
onclick="window.LEDMatrixWidgets.get('notification').triggerAction('${notificationId}')"
class="flex-shrink-0 ml-2 px-3 py-1 text-xs font-semibold rounded-md bg-white bg-opacity-20 hover:bg-opacity-30 transition-colors duration-150">
${escapeHtml(options.actionLabel)}
</button>
`;
}
if (dismissible) {
html += `
<button type="button"
@@ -227,6 +245,17 @@
removeNotification(notificationId);
},
/**
* Invoke a notification's onAction callback (see options.actionLabel /
* options.onAction on show) and dismiss it.
* @param {string} notificationId - Notification ID whose action to run
*/
triggerAction: function(notificationId) {
const cb = actionCallbacks[notificationId];
removeNotification(notificationId);
if (cb) cb();
},
/**
* Clear all notifications
*/
@@ -262,9 +291,11 @@
}
});
// Global shorthand function (backwards compatible with existing code)
// Global shorthand function (backwards compatible with existing code).
// Accepts either the legacy type string or a full options object
// ({ type, duration, actionLabel, onAction, ... }).
window.showNotification = function(message, type = 'info') {
return showNotification(message, { type: type });
return showNotification(message, typeof type === 'string' ? { type: type } : (type || {}));
};
// Initialize container on load
@@ -3888,6 +3888,19 @@ window.installPlugin = function(pluginId, branch = null) {
.then(data => {
showNotification(data.message, data.status);
if (data.status === 'success') {
// Enable immediately so install -> enable is one step, then nudge
// for the display restart that's needed before it shows on the
// matrix (persistent toast; duration 0 = stays until dismissed).
window.togglePlugin(pluginId, true);
showNotification(
`${pluginId} installed and enabled — restart the display to show it`,
{
type: 'success',
duration: 0,
actionLabel: 'Restart Now',
onAction: () => restartDisplay()
}
);
// Refresh installed plugins list, then re-render store to update badges
loadInstalledPlugins();
setTimeout(() => applyStoreFiltersAndSort(true), 500);