diff --git a/client/src/api.js b/client/src/api.js
index cc58bb5..29c9244 100644
--- a/client/src/api.js
+++ b/client/src/api.js
@@ -125,4 +125,8 @@ export function testNotify(url) {
return post('/notify/test', url ? { url } : {});
}
+export function getChangelog(name) {
+ return get(`/changelog/${encodeURIComponent(name)}`);
+}
+
export { ApiError };
diff --git a/client/src/components/UpdateCard.jsx b/client/src/components/UpdateCard.jsx
index aebdbba..b95ac3d 100644
--- a/client/src/components/UpdateCard.jsx
+++ b/client/src/components/UpdateCard.jsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
-import { pin, unpin } from '../api.js';
+import { pin, unpin, getChangelog } from '../api.js';
import { useUpdateRunner } from '../hooks/useUpdateRunner.js';
import StatusMessage from './StatusMessage.jsx';
import StreamLog from './StreamLog.jsx';
@@ -57,10 +57,61 @@ const ExternalIcon = () => (
);
+// Renders a resolved changelog payload (GitHub release notes, a link-out, or
+// nothing). Release bodies render as plain text (React escapes — no XSS).
+function ChangelogContent({ data }) {
+ if (data.type === 'github') {
+ if (!data.releases.length) {
+ return (
+
+ No newer release notes found.{' '}
+
+ View releases
+
+
+ );
+ }
+ return (
+
+ );
+ }
+ if (data.type === 'link') {
+ return (
+
+ {data.note ? `${data.note} ` : ''}
+
+ {data.label || 'Open'}
+
+
+ );
+ }
+ return No changelog source available for this image.
;
+}
+
/**
- * A single container's card: identity, version, source/changelog link, pin +
- * hide controls, update button, and an expandable live log for the in-flight
- * (or most recent) update.
+ * A single container's card: identity, version, source/changelog link, pin
+ * control, update button, an expandable "What's changed" panel, and an
+ * expandable live log for the in-flight (or most recent) update.
*
* props:
* - container: item shape from GET /api/containers
@@ -75,6 +126,11 @@ export default function UpdateCard({ container, onSettled, onPinChange, register
const [pinBusy, setPinBusy] = useState(false);
const [actionError, setActionError] = useState('');
+ const [clOpen, setClOpen] = useState(false);
+ const [clLoading, setClLoading] = useState(false);
+ const [clData, setClData] = useState(null);
+ const [clError, setClError] = useState('');
+
const { run, busy, startError, status, lines } = useUpdateRunner(name, onSettled);
useEffect(() => {
@@ -106,6 +162,23 @@ export default function UpdateCard({ container, onSettled, onPinChange, register
}
}, [pinned, image, onPinChange]);
+ const toggleChangelog = useCallback(async () => {
+ const next = !clOpen;
+ setClOpen(next);
+ if (next && !clData && !clLoading) {
+ setClLoading(true);
+ setClError('');
+ try {
+ const d = await getChangelog(name);
+ setClData(d);
+ } catch (err) {
+ setClError(err.message || 'Failed to load changelog');
+ } finally {
+ setClLoading(false);
+ }
+ }
+ }, [clOpen, clData, clLoading, name]);
+
const showUpdateAvailable = updateAvailable && !pinned;
const link = sourceLink(sourceUrl);
@@ -170,6 +243,11 @@ export default function UpdateCard({ container, onSettled, onPinChange, register
)}
+ {showUpdateAvailable && (
+
+ )}