mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
fix(web): wait for async plugin install to finish before auto-enabling it
Confirmed live: installing hockey-scoreboard logged "installation queued" (success) immediately followed by "enabling it failed" with a 404 "Plugin not found" from /api/v3/plugins/toggle. /api/v3/plugins/install runs the actual clone + plugin-manager discovery asynchronously via an operation queue when one is configured - the response installPlugin() was checking only means the operation was queued, not that the plugin is installed yet. Calling togglePlugin() right after that response 404s because plugin_manager hasn't discovered the new plugin. Fix: reuse the same operation-polling mechanism uninstallPlugin() already has (generalized pollOperationStatus() to take onComplete/onFailed/onTimeout callbacks instead of hardcoding uninstall behavior) so installPlugin() waits for the operation to actually complete before enabling it. Falls back to enabling immediately when no operation_id is returned (direct/synchronous install path, no queue configured).
This commit is contained in:
@@ -3334,16 +3334,28 @@ window.uninstallPlugin = function(pluginId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollOperationStatus(operationId, pluginId, pluginName, maxAttempts = 60, attempt = 0) {
|
function pollOperationStatus(operationId, pluginId, pluginName, options = {}) {
|
||||||
|
const maxAttempts = options.maxAttempts || 60;
|
||||||
|
const attempt = options.attempt || 0;
|
||||||
|
const onComplete = options.onComplete || (() => handleUninstallSuccess(pluginId));
|
||||||
|
const onFailed = options.onFailed || ((errorMsg) => {
|
||||||
|
showNotification(errorMsg || `Operation failed for ${pluginName}`, 'error');
|
||||||
|
setTimeout(() => loadInstalledPlugins(), 1000);
|
||||||
|
});
|
||||||
|
const onTimeout = options.onTimeout || (() => {
|
||||||
|
showNotification(`Operation timed out for ${pluginName}`, 'error');
|
||||||
|
setTimeout(() => loadInstalledPlugins(), 1000);
|
||||||
|
});
|
||||||
|
|
||||||
if (attempt >= maxAttempts) {
|
if (attempt >= maxAttempts) {
|
||||||
showNotification(`Uninstall operation timed out for ${pluginName}`, 'error');
|
onTimeout();
|
||||||
// Refresh plugin list to see actual state
|
|
||||||
setTimeout(() => {
|
|
||||||
loadInstalledPlugins();
|
|
||||||
}, 1000);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pollAgain = () => setTimeout(() => {
|
||||||
|
pollOperationStatus(operationId, pluginId, pluginName, { ...options, attempt: attempt + 1 });
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
fetch(`/api/v3/plugins/operation/${operationId}`)
|
fetch(`/api/v3/plugins/operation/${operationId}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -3352,32 +3364,16 @@ function pollOperationStatus(operationId, pluginId, pluginName, maxAttempts = 60
|
|||||||
const status = operation.status;
|
const status = operation.status;
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
// Operation completed successfully
|
onComplete();
|
||||||
handleUninstallSuccess(pluginId);
|
|
||||||
} else if (status === 'failed') {
|
} else if (status === 'failed') {
|
||||||
// Operation failed
|
onFailed(operation.error || operation.message);
|
||||||
const errorMsg = operation.error || operation.message || `Failed to uninstall ${pluginName}`;
|
|
||||||
showNotification(errorMsg, 'error');
|
|
||||||
// Refresh plugin list to see actual state
|
|
||||||
setTimeout(() => {
|
|
||||||
loadInstalledPlugins();
|
|
||||||
}, 1000);
|
|
||||||
} else if (status === 'pending' || status === 'in_progress') {
|
|
||||||
// Still in progress, poll again
|
|
||||||
setTimeout(() => {
|
|
||||||
pollOperationStatus(operationId, pluginId, pluginName, maxAttempts, attempt + 1);
|
|
||||||
}, 1000); // Poll every second
|
|
||||||
} else {
|
} else {
|
||||||
// Unknown status, poll again
|
// 'pending', 'in_progress', or unknown - poll again
|
||||||
setTimeout(() => {
|
pollAgain();
|
||||||
pollOperationStatus(operationId, pluginId, pluginName, maxAttempts, attempt + 1);
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Error getting operation status, try again
|
// Error getting operation status, try again
|
||||||
setTimeout(() => {
|
pollAgain();
|
||||||
pollOperationStatus(operationId, pluginId, pluginName, maxAttempts, attempt + 1);
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -3883,6 +3879,33 @@ window.installPlugin = function(pluginId, branch = null) {
|
|||||||
requestBody.branch = branch;
|
requestBody.branch = branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enableAfterInstall() {
|
||||||
|
// Enable immediately so install -> enable is one step; only nudge
|
||||||
|
// for a restart once enablement actually succeeded (persistent
|
||||||
|
// toast; duration 0 = stays until dismissed).
|
||||||
|
Promise.resolve(window.togglePlugin(pluginId, true)).then(toggleResult => {
|
||||||
|
if (toggleResult && toggleResult.status === 'success') {
|
||||||
|
showNotification(
|
||||||
|
`${pluginId} installed and enabled — restart the display to show it`,
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
duration: 0,
|
||||||
|
actionLabel: 'Restart Now',
|
||||||
|
onAction: () => restartDisplay()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showNotification(
|
||||||
|
`${pluginId} installed, but enabling it failed — use its toggle in the plugin list`,
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Refresh installed plugins list, then re-render store to update badges
|
||||||
|
loadInstalledPlugins();
|
||||||
|
setTimeout(() => applyStoreFiltersAndSort(true), 500);
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/api/v3/plugins/install', {
|
fetch('/api/v3/plugins/install', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -3891,31 +3914,23 @@ window.installPlugin = function(pluginId, branch = null) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
showNotification(data.message, data.status);
|
showNotification(data.message, data.status);
|
||||||
if (data.status === 'success') {
|
if (data.status !== 'success') return;
|
||||||
// Enable immediately so install -> enable is one step; only nudge
|
|
||||||
// for a restart once enablement actually succeeded (persistent
|
if (data.data && data.data.operation_id) {
|
||||||
// toast; duration 0 = stays until dismissed).
|
// Install runs async via the operation queue - this response only
|
||||||
Promise.resolve(window.togglePlugin(pluginId, true)).then(toggleResult => {
|
// means "queued", not "installed". Enabling immediately here would
|
||||||
if (toggleResult && toggleResult.status === 'success') {
|
// 404 with "Plugin not found" against the toggle endpoint, since
|
||||||
showNotification(
|
// the plugin manager hasn't discovered the new plugin yet (seen
|
||||||
`${pluginId} installed and enabled — restart the display to show it`,
|
// live: "installation queued" followed immediately by a failed
|
||||||
{
|
// enable). Wait for the operation to actually finish first.
|
||||||
type: 'success',
|
pollOperationStatus(data.data.operation_id, pluginId, pluginId, {
|
||||||
duration: 0,
|
onComplete: enableAfterInstall,
|
||||||
actionLabel: 'Restart Now',
|
onFailed: (errorMsg) => showNotification(errorMsg || `Failed to install ${pluginId}`, 'error'),
|
||||||
onAction: () => restartDisplay()
|
onTimeout: () => showNotification(`Install operation timed out for ${pluginId}`, 'error')
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showNotification(
|
|
||||||
`${pluginId} installed, but enabling it failed — use its toggle in the plugin list`,
|
|
||||||
'warning'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Refresh installed plugins list, then re-render store to update badges
|
} else {
|
||||||
loadInstalledPlugins();
|
// No operation queue configured - install already completed synchronously.
|
||||||
setTimeout(() => applyStoreFiltersAndSort(true), 500);
|
enableAfterInstall();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|||||||
Reference in New Issue
Block a user