From 14e82d76f9d9464e98923a669080ff5452a9c9e7 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 12 Mar 2026 18:19:06 +0100 Subject: [PATCH] chore(ui): improve errors and reporting during model installation (#8979) Signed-off-by: Ettore Di Giacinto --- core/http/react-ui/src/App.css | 6 ++- .../react-ui/src/components/OperationsBar.jsx | 43 ++++++++++++++----- core/http/react-ui/src/hooks/useOperations.js | 28 ++++++++++-- core/http/react-ui/src/pages/Backends.jsx | 2 - core/http/react-ui/src/pages/Models.jsx | 20 ++++++++- core/http/react-ui/src/utils/api.js | 1 + core/http/react-ui/src/utils/config.js | 1 + core/http/routes/ui_api.go | 26 +++++++++-- 8 files changed, 104 insertions(+), 23 deletions(-) diff --git a/core/http/react-ui/src/App.css b/core/http/react-ui/src/App.css index e7bbdffe9..2ff91d98b 100644 --- a/core/http/react-ui/src/App.css +++ b/core/http/react-ui/src/App.css @@ -365,6 +365,7 @@ align-items: center; gap: var(--spacing-sm); flex: 1; + min-width: 0; } .operation-spinner { @@ -379,6 +380,8 @@ .operation-text { font-size: 0.8125rem; color: var(--color-text-secondary); + overflow: hidden; + text-overflow: ellipsis; } .operation-progress { @@ -403,11 +406,12 @@ } .operation-cancel { + flex-shrink: 0; background: none; border: none; color: var(--color-text-muted); cursor: pointer; - padding: 2px; + padding: 4px 6px; font-size: 0.875rem; } .operation-cancel:hover { diff --git a/core/http/react-ui/src/components/OperationsBar.jsx b/core/http/react-ui/src/components/OperationsBar.jsx index de52b1c4e..ae25712db 100644 --- a/core/http/react-ui/src/components/OperationsBar.jsx +++ b/core/http/react-ui/src/components/OperationsBar.jsx @@ -1,7 +1,7 @@ import { useOperations } from '../hooks/useOperations' export default function OperationsBar() { - const { operations, cancelOperation } = useOperations() + const { operations, cancelOperation, dismissFailedOp } = useOperations() if (operations.length === 0) return null @@ -10,7 +10,9 @@ export default function OperationsBar() { {operations.map(op => (
- {op.isCancelled ? ( + {op.error ? ( + + ) : op.isCancelled ? ( ) : op.isDeletion ? ( @@ -18,34 +20,53 @@ export default function OperationsBar() {
)} - {op.isDeletion ? 'Removing' : 'Installing'}{' '} - {op.isBackend ? 'backend' : 'model'}: {op.name || op.id} + {op.error ? ( + <> + Failed to install {op.isBackend ? 'backend' : 'model'}: {op.name || op.id} + + ({op.error}) + + + ) : ( + <> + {op.isDeletion ? 'Removing' : 'Installing'}{' '} + {op.isBackend ? 'backend' : 'model'}: {op.name || op.id} + + )} - {op.isQueued && ( + {!op.error && op.isQueued && ( (Queued) )} - {op.isCancelled && ( + {!op.error && op.isCancelled && ( Cancelling... )} - {op.message && !op.isQueued && !op.isCancelled && ( + {!op.error && op.message && !op.isQueued && !op.isCancelled && ( {op.message} )} - {op.progress !== undefined && op.progress > 0 && ( + {!op.error && op.progress !== undefined && op.progress > 0 && ( {Math.round(op.progress)}% )}
- {op.progress !== undefined && op.progress > 0 && ( + {!op.error && op.progress !== undefined && op.progress > 0 && (
)} - {op.cancellable && !op.isCancelled && ( + {op.error ? ( + + ) : op.cancellable && !op.isCancelled ? ( - )} + ) : null}
))}
diff --git a/core/http/react-ui/src/hooks/useOperations.js b/core/http/react-ui/src/hooks/useOperations.js index e4e23eebc..bb0a57670 100644 --- a/core/http/react-ui/src/hooks/useOperations.js +++ b/core/http/react-ui/src/hooks/useOperations.js @@ -14,11 +14,18 @@ export function useOperations(pollInterval = 1000) { const data = await operationsApi.list() const ops = data?.operations || (Array.isArray(data) ? data : []) setOperations(ops) - // Auto-refresh the page when all operations complete (mirrors original behavior) - if (previousCountRef.current > 0 && ops.length === 0) { + + // Separate active (non-failed) operations from failed ones + const activeOps = ops.filter(op => !op.error) + const failedOps = ops.filter(op => op.error) + + // Auto-refresh the page when all active operations complete (mirrors original behavior) + // but not when there are still failed operations being shown + if (previousCountRef.current > 0 && activeOps.length === 0 && failedOps.length === 0) { setTimeout(() => window.location.reload(), 1000) } - previousCountRef.current = ops.length + previousCountRef.current = activeOps.length + setError(null) } catch (err) { setError(err.message) @@ -36,6 +43,19 @@ export function useOperations(pollInterval = 1000) { } }, [fetchOperations]) + // Dismiss a failed operation (acknowledge the error and remove it) + const dismissFailedOp = useCallback(async (opId) => { + try { + const op = operations.find(o => o.id === opId) + if (op?.jobID) { + await operationsApi.dismiss(op.jobID) + await fetchOperations() + } + } catch { + // Ignore dismiss errors + } + }, [operations, fetchOperations]) + useEffect(() => { fetchOperations() intervalRef.current = setInterval(fetchOperations, pollInterval) @@ -44,5 +64,5 @@ export function useOperations(pollInterval = 1000) { } }, [fetchOperations, pollInterval]) - return { operations, loading, error, cancelOperation, refetch: fetchOperations } + return { operations, loading, error, cancelOperation, dismissFailedOp, refetch: fetchOperations } } diff --git a/core/http/react-ui/src/pages/Backends.jsx b/core/http/react-ui/src/pages/Backends.jsx index 3723c7ca0..24c2a5fcf 100644 --- a/core/http/react-ui/src/pages/Backends.jsx +++ b/core/http/react-ui/src/pages/Backends.jsx @@ -87,7 +87,6 @@ export default function Backends() { const handleInstall = async (id) => { try { await backendsApi.install(id) - addToast(`Installing backend ${id}...`, 'info') } catch (err) { addToast(`Install failed: ${err.message}`, 'error') } @@ -112,7 +111,6 @@ export default function Backends() { if (manualName.trim()) body.name = manualName.trim() if (manualAlias.trim()) body.alias = manualAlias.trim() await backendsApi.installExternal(body) - addToast('Installing backend...', 'info') setManualUri('') setManualName('') setManualAlias('') diff --git a/core/http/react-ui/src/pages/Models.jsx b/core/http/react-ui/src/pages/Models.jsx index 9bb698d80..dd112cc32 100644 --- a/core/http/react-ui/src/pages/Models.jsx +++ b/core/http/react-ui/src/pages/Models.jsx @@ -198,7 +198,6 @@ export default function Models() { try { setInstalling(prev => new Set(prev).add(modelId)) await modelsApi.install(modelId) - addToast(`Installing ${modelId}...`, 'info') } catch (err) { addToast(`Failed to install: ${err.message}`, 'error') } @@ -215,6 +214,25 @@ export default function Models() { } } + // Clear local installing flags when operations finish (success or error) + useEffect(() => { + if (installing.size === 0) return + setInstalling(prev => { + const next = new Set(prev) + let changed = false + for (const modelId of prev) { + const hasActiveOp = operations.some(op => + op.name === modelId && !op.completed && !op.error + ) + if (!hasActiveOp) { + next.delete(modelId) + changed = true + } + } + return changed ? next : prev + }) + }, [operations, installing.size]) + const isInstalling = (modelId) => { return installing.has(modelId) || operations.some(op => op.name === modelId && !op.completed && !op.error diff --git a/core/http/react-ui/src/utils/api.js b/core/http/react-ui/src/utils/api.js index 3011bfd6d..260dd0b9f 100644 --- a/core/http/react-ui/src/utils/api.js +++ b/core/http/react-ui/src/utils/api.js @@ -130,6 +130,7 @@ export const resourcesApi = { export const operationsApi = { list: () => fetchJSON(API_CONFIG.endpoints.operations), cancel: (jobID) => postJSON(API_CONFIG.endpoints.cancelOperation(jobID), {}), + dismiss: (jobID) => postJSON(API_CONFIG.endpoints.dismissOperation(jobID), {}), } // Settings API diff --git a/core/http/react-ui/src/utils/config.js b/core/http/react-ui/src/utils/config.js index c82dfc24b..5bc0cbd5e 100644 --- a/core/http/react-ui/src/utils/config.js +++ b/core/http/react-ui/src/utils/config.js @@ -3,6 +3,7 @@ export const API_CONFIG = { // Operations operations: '/api/operations', cancelOperation: (jobID) => `/api/operations/${jobID}/cancel`, + dismissOperation: (jobID) => `/api/operations/${jobID}/dismiss`, // Models gallery models: '/api/models', diff --git a/core/http/routes/ui_api.go b/core/http/routes/ui_api.go index d70010a5a..1cd5c2af6 100644 --- a/core/http/routes/ui_api.go +++ b/core/http/routes/ui_api.go @@ -80,8 +80,8 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model message := "" if status != nil { - // Skip completed operations (unless cancelled and not yet cleaned up) - if status.Processed && !status.Cancelled { + // Skip successfully completed operations + if status.Processed && !status.Cancelled && status.Error == nil { continue } // Skip cancelled operations that are processed (they're done, no need to show) @@ -131,7 +131,7 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model } } - operations = append(operations, map[string]interface{}{ + opData := map[string]interface{}{ "id": galleryID, "name": displayName, "fullName": galleryID, @@ -144,7 +144,11 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model "isCancelled": isCancelled, "cancellable": isCancellable, "message": message, - }) + } + if status != nil && status.Error != nil { + opData["error"] = status.Error.Error() + } + operations = append(operations, opData) } // Sort operations by progress (ascending), then by ID for stable display order @@ -188,6 +192,20 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model }) }) + // Dismiss a failed operation (acknowledge the error and remove it from the list) + app.POST("/api/operations/:jobID/dismiss", func(c echo.Context) error { + jobID := c.Param("jobID") + xlog.Debug("API request to dismiss operation", "jobID", jobID) + + // Remove the operation from the opcache so it no longer appears + opcache.DeleteUUID(jobID) + + return c.JSON(200, map[string]interface{}{ + "success": true, + "message": "Operation dismissed", + }) + }) + // Model Gallery APIs app.GET("/api/models", func(c echo.Context) error { term := c.QueryParam("term")