feat(system): background image update checker with stack badges

Adds an ImageUpdateService that queries OCI registry manifest endpoints
every 6 hours to compare remote digests against local RepoDigests.
Results are cached in a new stack_update_status SQLite table. A pulsing
blue dot badge appears in the stack sidebar for stacks with updates
available. Manual refresh available via POST /api/image-updates/refresh
with a 10-minute rate limit.
This commit is contained in:
SaelixCode
2026-03-19 21:28:48 -04:00
parent b7748b4d17
commit d64d23fc50
5 changed files with 375 additions and 0 deletions
+21
View File
@@ -96,6 +96,9 @@ export default function EditorLayout() {
const [logContainer, setLogContainer] = useState<{ id: string; name: string } | null>(null);
// Image update checker state
const [stackUpdates, setStackUpdates] = useState<Record<string, boolean>>({});
// Notifications & Settings state
const [notifications, setNotifications] = useState<any[]>([]);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
@@ -172,6 +175,7 @@ export default function EditorLayout() {
setIsEditing(false);
setActiveView('dashboard');
refreshStacks();
fetchImageUpdates();
}, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps
const fetchNotifications = async () => {
@@ -184,6 +188,16 @@ export default function EditorLayout() {
} catch (e) { }
};
const fetchImageUpdates = async () => {
try {
const res = await apiFetch('/image-updates');
if (res.ok) {
const data = await res.json();
setStackUpdates(data);
}
} catch (e) { }
};
const markAllRead = async () => {
try {
await apiFetch('/notifications/read', { method: 'POST' });
@@ -769,6 +783,13 @@ export default function EditorLayout() {
/>
<span className="flex-1 truncate">{getDisplayName(file)}</span>
{stackUpdates[file] && (
<span
className="w-2 h-2 rounded-full bg-blue-400 animate-pulse shrink-0"
title="Update available"
/>
)}
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>