@inoo-ch/payload-image-optimizer 1.4.3 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,6 +5,7 @@ const POLL_INTERVAL_MS = 2000;
|
|
|
5
5
|
// With sequential processing each image takes ~4-5s, so no progress for 30s
|
|
6
6
|
// (15 polls) strongly suggests a real stall rather than slow processing.
|
|
7
7
|
const STALL_THRESHOLD = 15;
|
|
8
|
+
const SESSION_KEY = 'imageOptimizer_running';
|
|
8
9
|
export const RegenerationButton = ()=>{
|
|
9
10
|
const [isRunning, setIsRunning] = useState(false);
|
|
10
11
|
const [progress, setProgress] = useState(null);
|
|
@@ -66,6 +67,7 @@ export const RegenerationButton = ()=>{
|
|
|
66
67
|
setIsRunning(false);
|
|
67
68
|
setStalled(false);
|
|
68
69
|
stopPolling();
|
|
70
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
73
|
// Stall detection — warn but keep polling so we detect when jobs resume
|
|
@@ -90,36 +92,40 @@ export const RegenerationButton = ()=>{
|
|
|
90
92
|
collectionSlug,
|
|
91
93
|
stopPolling
|
|
92
94
|
]);
|
|
93
|
-
// On mount
|
|
95
|
+
// On mount: fetch stats for the counter display. If the user previously
|
|
96
|
+
// triggered regeneration (sessionStorage flag) and there are still pending
|
|
97
|
+
// images, resume polling so the UI reconnects after page navigation.
|
|
94
98
|
useEffect(()=>{
|
|
95
99
|
if (!collectionSlug) return;
|
|
96
100
|
let cancelled = false;
|
|
97
|
-
const
|
|
101
|
+
const loadStats = async ()=>{
|
|
98
102
|
try {
|
|
99
103
|
const res = await fetch(`/api/image-optimizer/regenerate?collection=${collectionSlug}`);
|
|
100
104
|
if (!res.ok || cancelled) return;
|
|
101
105
|
const data = await res.json();
|
|
102
|
-
// Always store stats on mount
|
|
103
106
|
setStats(data);
|
|
104
|
-
if
|
|
107
|
+
// Resume polling only if the user triggered regeneration in this session
|
|
108
|
+
const wasRunning = sessionStorage.getItem(SESSION_KEY) === collectionSlug;
|
|
109
|
+
if (wasRunning && data.pending > 0) {
|
|
105
110
|
setProgress(data);
|
|
106
111
|
setIsRunning(true);
|
|
107
112
|
setStalled(false);
|
|
108
|
-
setQueued(null);
|
|
109
113
|
stallRef.current = {
|
|
110
114
|
lastProcessed: data.complete + data.errored,
|
|
111
115
|
stallCount: 0
|
|
112
116
|
};
|
|
113
117
|
startPolling(pollProgress);
|
|
118
|
+
} else if (wasRunning && data.pending <= 0) {
|
|
119
|
+
// Jobs finished while we were away — clear the flag
|
|
120
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
114
121
|
}
|
|
115
122
|
} catch {
|
|
116
123
|
// ignore
|
|
117
124
|
}
|
|
118
125
|
};
|
|
119
|
-
|
|
126
|
+
loadStats();
|
|
120
127
|
return ()=>{
|
|
121
128
|
cancelled = true;
|
|
122
|
-
// Clear interval on effect cleanup to prevent duplicate intervals
|
|
123
129
|
stopPolling();
|
|
124
130
|
};
|
|
125
131
|
}, [
|
|
@@ -183,6 +189,8 @@ export const RegenerationButton = ()=>{
|
|
|
183
189
|
setIsRunning(false);
|
|
184
190
|
return;
|
|
185
191
|
}
|
|
192
|
+
// Persist running state so we can resume after page navigation
|
|
193
|
+
sessionStorage.setItem(SESSION_KEY, collectionSlug);
|
|
186
194
|
// Start polling
|
|
187
195
|
startPolling(pollProgress);
|
|
188
196
|
} catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/RegenerationButton.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react'\n\ntype RegenerationProgress = {\n total: number\n complete: number\n errored: number\n pending: number\n}\n\nconst POLL_INTERVAL_MS = 2000\n// With sequential processing each image takes ~4-5s, so no progress for 30s\n// (15 polls) strongly suggests a real stall rather than slow processing.\nconst STALL_THRESHOLD = 15\n\nexport const RegenerationButton: React.FC = () => {\n const [isRunning, setIsRunning] = useState(false)\n const [progress, setProgress] = useState<RegenerationProgress | null>(null)\n const [queued, setQueued] = useState<number | null>(null)\n const [force, setForce] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [stalled, setStalled] = useState(false)\n const [collectionSlug, setCollectionSlug] = useState<string | null>(null)\n const [stats, setStats] = useState<RegenerationProgress | null>(null)\n const [confirming, setConfirming] = useState(false)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const stallRef = useRef({ lastProcessed: 0, stallCount: 0 })\n const prevIsRunningRef = useRef(false)\n\n // Extract collection slug from URL after mount to avoid hydration mismatch\n useEffect(() => {\n const slug = window.location.pathname.split('/collections/')[1]?.split('/')[0] ?? null\n setCollectionSlug(slug)\n }, [])\n\n // Fetch optimization stats (independent of regeneration)\n const fetchStats = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setStats(data)\n }\n } catch {\n // ignore stats fetch errors\n }\n }, [collectionSlug])\n\n const stopPolling = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }, [])\n\n const startPolling = useCallback(\n (pollFn: () => void) => {\n // Prevent duplicate intervals\n stopPolling()\n intervalRef.current = setInterval(pollFn, POLL_INTERVAL_MS)\n },\n [stopPolling],\n )\n\n const pollProgress = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setProgress(data)\n\n // Stop polling when no more pending\n if (data.pending <= 0) {\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n return\n }\n\n // Stall detection — warn but keep polling so we detect when jobs resume\n const processed = data.complete + data.errored\n if (processed === stallRef.current.lastProcessed) {\n stallRef.current.stallCount += 1\n } else {\n stallRef.current.stallCount = 0\n stallRef.current.lastProcessed = processed\n // Clear stall warning when progress resumes\n setStalled(false)\n }\n\n if (stallRef.current.stallCount >= STALL_THRESHOLD) {\n setStalled(true)\n // Keep polling — jobs may still be running server-side\n }\n }\n } catch {\n // ignore polling errors\n }\n }, [collectionSlug, stopPolling])\n\n // On mount (once collectionSlug is known), check if there's an ongoing job and resume polling\n useEffect(() => {\n if (!collectionSlug) return\n let cancelled = false\n const checkOngoing = async () => {\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (!res.ok || cancelled) return\n const data: RegenerationProgress = await res.json()\n // Always store stats on mount\n setStats(data)\n if (data.pending > 0) {\n setProgress(data)\n setIsRunning(true)\n setStalled(false)\n setQueued(null)\n stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }\n startPolling(pollProgress)\n }\n } catch {\n // ignore\n }\n }\n checkOngoing()\n return () => {\n cancelled = true\n // Clear interval on effect cleanup to prevent duplicate intervals\n stopPolling()\n }\n }, [collectionSlug, pollProgress, startPolling, stopPolling])\n\n // Refresh stats when regeneration finishes (isRunning transitions from true to false)\n useEffect(() => {\n if (prevIsRunningRef.current && !isRunning) {\n fetchStats()\n }\n prevIsRunningRef.current = isRunning\n }, [isRunning, fetchStats])\n\n // Phase 1: Show confirmation with counts\n const handlePreflight = async () => {\n if (!collectionSlug) return\n setError(null)\n // Refresh stats to get the latest counts before confirming\n await fetchStats()\n setConfirming(true)\n }\n\n const handleCancel = () => {\n setConfirming(false)\n }\n\n // Phase 2: Actually start regeneration (after user confirms)\n const handleConfirm = async () => {\n if (!collectionSlug) return\n setConfirming(false)\n setError(null)\n setStalled(false)\n setIsRunning(true)\n setQueued(null)\n setProgress(null)\n stallRef.current = { lastProcessed: 0, stallCount: 0 }\n\n try {\n const res = await fetch('/api/image-optimizer/regenerate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ collectionSlug, force }),\n })\n\n if (!res.ok) {\n const data = await res.json()\n throw new Error(data.error || 'Failed to start regeneration')\n }\n\n const data = await res.json()\n setQueued(data.queued)\n\n if (data.queued === 0) {\n setIsRunning(false)\n return\n }\n\n // Start polling\n startPolling(pollProgress)\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err))\n setIsRunning(false)\n }\n }\n\n // Cleanup interval on unmount\n useEffect(() => {\n return () => stopPolling()\n }, [stopPolling])\n\n if (!collectionSlug) return null\n\n const progressPercent =\n progress && progress.total > 0\n ? Math.round(((progress.complete + progress.errored) / progress.total) * 100)\n : 0\n\n const showProgressBar = (isRunning && progress) || (stalled && progress)\n\n // Stats computations\n const statsPercent =\n stats && stats.total > 0\n ? Math.round((stats.complete / stats.total) * 100)\n : 0\n const allOptimized = stats && stats.total > 0 && stats.complete === stats.total\n\n return (\n <div\n style={{\n padding: '16px 24px',\n borderBottom: '1px solid #e5e7eb',\n display: 'flex',\n alignItems: 'center',\n gap: '16px',\n flexWrap: 'wrap',\n }}\n >\n {!confirming && (\n <button\n onClick={handlePreflight}\n disabled={isRunning}\n style={{\n backgroundColor: isRunning ? '#9ca3af' : '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: isRunning ? 'not-allowed' : 'pointer',\n }}\n >\n {isRunning ? 'Processing all images...' : 'Regenerate All Images'}\n </button>\n )}\n\n {confirming && stats && (\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>\n <span style={{ fontSize: '13px', color: '#374151' }}>\n {force\n ? `Re-process all ${stats.total} images across the entire collection?`\n : `Regenerate ${stats.pending} unoptimized image${stats.pending !== 1 ? 's' : ''} across the entire collection?`}\n </span>\n <button\n onClick={handleConfirm}\n style={{\n backgroundColor: '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Confirm\n </button>\n <button\n onClick={handleCancel}\n style={{\n backgroundColor: 'transparent',\n color: '#6b7280',\n border: '1px solid #d1d5db',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Cancel\n </button>\n </div>\n )}\n\n {!confirming && (\n <label\n style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}\n >\n <input\n type=\"checkbox\"\n checked={force}\n onChange={(e) => setForce(e.target.checked)}\n disabled={isRunning}\n />\n Force re-process all\n </label>\n )}\n\n {error && (\n <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>\n )}\n\n {queued !== null && queued > 0 && isRunning && !confirming && (\n <span style={{ color: '#4f46e5', fontSize: '13px' }}>\n Queued {queued} image{queued !== 1 ? 's' : ''} for processing across the entire collection\n </span>\n )}\n\n {queued === 0 && !isRunning && !stalled && !confirming && (\n <span style={{ color: '#10b981', fontSize: '13px' }}>\n All images already optimized.\n </span>\n )}\n\n {stalled && progress && (\n <span style={{ color: '#f59e0b', fontSize: '13px' }}>\n Processing appears slow — {progress.pending} image{progress.pending !== 1 ? 's' : ''} still pending.\n Jobs may still be running server-side.\n </span>\n )}\n\n {showProgressBar && (\n <div style={{ flex: 1, minWidth: '200px' }}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: '12px',\n marginBottom: '4px',\n }}\n >\n <span>\n {progress.complete} / {progress.total} complete\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>{progress.errored} errors</span>\n )}\n <span>{progressPercent}%</span>\n </div>\n <div\n style={{\n height: '6px',\n backgroundColor: '#e5e7eb',\n borderRadius: '3px',\n overflow: 'hidden',\n display: 'flex',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${progress.total > 0 ? Math.round((progress.complete / progress.total) * 100) : 0}%`,\n backgroundColor: '#10b981',\n transition: 'width 0.3s ease',\n }}\n />\n {progress.errored > 0 && (\n <div\n style={{\n height: '100%',\n width: `${progress.total > 0 ? Math.round((progress.errored / progress.total) * 100) : 0}%`,\n backgroundColor: '#ef4444',\n transition: 'width 0.3s ease',\n }}\n />\n )}\n </div>\n </div>\n )}\n\n {!isRunning && !stalled && progress && progress.complete > 0 && queued !== 0 && !confirming && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: progress.errored > 0 ? '#f59e0b' : '#10b981' }}>\n Done! {progress.complete}/{progress.total} optimized (across entire collection).\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>\n {' '}{progress.errored} failed.\n </span>\n )}\n </span>\n )}\n\n {/* Persistent optimization stats — always visible when not actively regenerating */}\n {!isRunning && !stalled && stats && stats.total > 0 && (\n <div\n style={{\n marginLeft: 'auto',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-end',\n gap: '4px',\n minWidth: '180px',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>\n {allOptimized ? (\n <span style={{ color: '#10b981' }}>\n ✓ All {stats.total} images optimized\n </span>\n ) : (\n <>\n <span style={{ color: '#6b7280' }}>\n {stats.complete}/{stats.total} optimized\n </span>\n {stats.errored > 0 && (\n <>\n <span style={{ color: '#d1d5db' }}>·</span>\n <span style={{ color: '#ef4444' }}>{stats.errored} errors</span>\n </>\n )}\n </>\n )}\n </div>\n {!allOptimized && (\n <div\n style={{\n width: '100%',\n height: '3px',\n backgroundColor: '#e5e7eb',\n borderRadius: '2px',\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${statsPercent}%`,\n backgroundColor: stats.errored > 0 ? '#f59e0b' : '#10b981',\n borderRadius: '2px',\n transition: 'width 0.3s ease',\n }}\n />\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":["React","useState","useEffect","useCallback","useRef","POLL_INTERVAL_MS","STALL_THRESHOLD","RegenerationButton","isRunning","setIsRunning","progress","setProgress","queued","setQueued","force","setForce","error","setError","stalled","setStalled","collectionSlug","setCollectionSlug","stats","setStats","confirming","setConfirming","intervalRef","stallRef","lastProcessed","stallCount","prevIsRunningRef","slug","window","location","pathname","split","fetchStats","res","fetch","ok","data","json","stopPolling","current","clearInterval","startPolling","pollFn","setInterval","pollProgress","pending","processed","complete","errored","cancelled","checkOngoing","handlePreflight","handleCancel","handleConfirm","method","headers","body","JSON","stringify","Error","err","message","String","progressPercent","total","Math","round","showProgressBar","statsPercent","allOptimized","div","style","padding","borderBottom","display","alignItems","gap","flexWrap","button","onClick","disabled","backgroundColor","color","border","borderRadius","fontSize","fontWeight","cursor","span","label","input","type","checked","onChange","e","target","flex","minWidth","justifyContent","marginBottom","height","overflow","width","transition","marginLeft","flexDirection"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/components/RegenerationButton.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react'\n\ntype RegenerationProgress = {\n total: number\n complete: number\n errored: number\n pending: number\n}\n\nconst POLL_INTERVAL_MS = 2000\n// With sequential processing each image takes ~4-5s, so no progress for 30s\n// (15 polls) strongly suggests a real stall rather than slow processing.\nconst STALL_THRESHOLD = 15\nconst SESSION_KEY = 'imageOptimizer_running'\n\nexport const RegenerationButton: React.FC = () => {\n const [isRunning, setIsRunning] = useState(false)\n const [progress, setProgress] = useState<RegenerationProgress | null>(null)\n const [queued, setQueued] = useState<number | null>(null)\n const [force, setForce] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [stalled, setStalled] = useState(false)\n const [collectionSlug, setCollectionSlug] = useState<string | null>(null)\n const [stats, setStats] = useState<RegenerationProgress | null>(null)\n const [confirming, setConfirming] = useState(false)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const stallRef = useRef({ lastProcessed: 0, stallCount: 0 })\n const prevIsRunningRef = useRef(false)\n\n // Extract collection slug from URL after mount to avoid hydration mismatch\n useEffect(() => {\n const slug = window.location.pathname.split('/collections/')[1]?.split('/')[0] ?? null\n setCollectionSlug(slug)\n }, [])\n\n // Fetch optimization stats (independent of regeneration)\n const fetchStats = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setStats(data)\n }\n } catch {\n // ignore stats fetch errors\n }\n }, [collectionSlug])\n\n const stopPolling = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }, [])\n\n const startPolling = useCallback(\n (pollFn: () => void) => {\n // Prevent duplicate intervals\n stopPolling()\n intervalRef.current = setInterval(pollFn, POLL_INTERVAL_MS)\n },\n [stopPolling],\n )\n\n const pollProgress = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setProgress(data)\n\n // Stop polling when no more pending\n if (data.pending <= 0) {\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n return\n }\n\n // Stall detection — warn but keep polling so we detect when jobs resume\n const processed = data.complete + data.errored\n if (processed === stallRef.current.lastProcessed) {\n stallRef.current.stallCount += 1\n } else {\n stallRef.current.stallCount = 0\n stallRef.current.lastProcessed = processed\n // Clear stall warning when progress resumes\n setStalled(false)\n }\n\n if (stallRef.current.stallCount >= STALL_THRESHOLD) {\n setStalled(true)\n // Keep polling — jobs may still be running server-side\n }\n }\n } catch {\n // ignore polling errors\n }\n }, [collectionSlug, stopPolling])\n\n // On mount: fetch stats for the counter display. If the user previously\n // triggered regeneration (sessionStorage flag) and there are still pending\n // images, resume polling so the UI reconnects after page navigation.\n useEffect(() => {\n if (!collectionSlug) return\n let cancelled = false\n const loadStats = async () => {\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (!res.ok || cancelled) return\n const data: RegenerationProgress = await res.json()\n setStats(data)\n\n // Resume polling only if the user triggered regeneration in this session\n const wasRunning = sessionStorage.getItem(SESSION_KEY) === collectionSlug\n if (wasRunning && data.pending > 0) {\n setProgress(data)\n setIsRunning(true)\n setStalled(false)\n stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }\n startPolling(pollProgress)\n } else if (wasRunning && data.pending <= 0) {\n // Jobs finished while we were away — clear the flag\n sessionStorage.removeItem(SESSION_KEY)\n }\n } catch {\n // ignore\n }\n }\n loadStats()\n return () => {\n cancelled = true\n stopPolling()\n }\n }, [collectionSlug, pollProgress, startPolling, stopPolling])\n\n // Refresh stats when regeneration finishes (isRunning transitions from true to false)\n useEffect(() => {\n if (prevIsRunningRef.current && !isRunning) {\n fetchStats()\n }\n prevIsRunningRef.current = isRunning\n }, [isRunning, fetchStats])\n\n // Phase 1: Show confirmation with counts\n const handlePreflight = async () => {\n if (!collectionSlug) return\n setError(null)\n // Refresh stats to get the latest counts before confirming\n await fetchStats()\n setConfirming(true)\n }\n\n const handleCancel = () => {\n setConfirming(false)\n }\n\n // Phase 2: Actually start regeneration (after user confirms)\n const handleConfirm = async () => {\n if (!collectionSlug) return\n setConfirming(false)\n setError(null)\n setStalled(false)\n setIsRunning(true)\n setQueued(null)\n setProgress(null)\n stallRef.current = { lastProcessed: 0, stallCount: 0 }\n\n try {\n const res = await fetch('/api/image-optimizer/regenerate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ collectionSlug, force }),\n })\n\n if (!res.ok) {\n const data = await res.json()\n throw new Error(data.error || 'Failed to start regeneration')\n }\n\n const data = await res.json()\n setQueued(data.queued)\n\n if (data.queued === 0) {\n setIsRunning(false)\n return\n }\n\n // Persist running state so we can resume after page navigation\n sessionStorage.setItem(SESSION_KEY, collectionSlug)\n // Start polling\n startPolling(pollProgress)\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err))\n setIsRunning(false)\n }\n }\n\n // Cleanup interval on unmount\n useEffect(() => {\n return () => stopPolling()\n }, [stopPolling])\n\n if (!collectionSlug) return null\n\n const progressPercent =\n progress && progress.total > 0\n ? Math.round(((progress.complete + progress.errored) / progress.total) * 100)\n : 0\n\n const showProgressBar = (isRunning && progress) || (stalled && progress)\n\n // Stats computations\n const statsPercent =\n stats && stats.total > 0\n ? Math.round((stats.complete / stats.total) * 100)\n : 0\n const allOptimized = stats && stats.total > 0 && stats.complete === stats.total\n\n return (\n <div\n style={{\n padding: '16px 24px',\n borderBottom: '1px solid #e5e7eb',\n display: 'flex',\n alignItems: 'center',\n gap: '16px',\n flexWrap: 'wrap',\n }}\n >\n {!confirming && (\n <button\n onClick={handlePreflight}\n disabled={isRunning}\n style={{\n backgroundColor: isRunning ? '#9ca3af' : '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: isRunning ? 'not-allowed' : 'pointer',\n }}\n >\n {isRunning ? 'Processing all images...' : 'Regenerate All Images'}\n </button>\n )}\n\n {confirming && stats && (\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>\n <span style={{ fontSize: '13px', color: '#374151' }}>\n {force\n ? `Re-process all ${stats.total} images across the entire collection?`\n : `Regenerate ${stats.pending} unoptimized image${stats.pending !== 1 ? 's' : ''} across the entire collection?`}\n </span>\n <button\n onClick={handleConfirm}\n style={{\n backgroundColor: '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Confirm\n </button>\n <button\n onClick={handleCancel}\n style={{\n backgroundColor: 'transparent',\n color: '#6b7280',\n border: '1px solid #d1d5db',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Cancel\n </button>\n </div>\n )}\n\n {!confirming && (\n <label\n style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}\n >\n <input\n type=\"checkbox\"\n checked={force}\n onChange={(e) => setForce(e.target.checked)}\n disabled={isRunning}\n />\n Force re-process all\n </label>\n )}\n\n {error && (\n <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>\n )}\n\n {queued !== null && queued > 0 && isRunning && !confirming && (\n <span style={{ color: '#4f46e5', fontSize: '13px' }}>\n Queued {queued} image{queued !== 1 ? 's' : ''} for processing across the entire collection\n </span>\n )}\n\n {queued === 0 && !isRunning && !stalled && !confirming && (\n <span style={{ color: '#10b981', fontSize: '13px' }}>\n All images already optimized.\n </span>\n )}\n\n {stalled && progress && (\n <span style={{ color: '#f59e0b', fontSize: '13px' }}>\n Processing appears slow — {progress.pending} image{progress.pending !== 1 ? 's' : ''} still pending.\n Jobs may still be running server-side.\n </span>\n )}\n\n {showProgressBar && (\n <div style={{ flex: 1, minWidth: '200px' }}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: '12px',\n marginBottom: '4px',\n }}\n >\n <span>\n {progress.complete} / {progress.total} complete\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>{progress.errored} errors</span>\n )}\n <span>{progressPercent}%</span>\n </div>\n <div\n style={{\n height: '6px',\n backgroundColor: '#e5e7eb',\n borderRadius: '3px',\n overflow: 'hidden',\n display: 'flex',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${progress.total > 0 ? Math.round((progress.complete / progress.total) * 100) : 0}%`,\n backgroundColor: '#10b981',\n transition: 'width 0.3s ease',\n }}\n />\n {progress.errored > 0 && (\n <div\n style={{\n height: '100%',\n width: `${progress.total > 0 ? Math.round((progress.errored / progress.total) * 100) : 0}%`,\n backgroundColor: '#ef4444',\n transition: 'width 0.3s ease',\n }}\n />\n )}\n </div>\n </div>\n )}\n\n {!isRunning && !stalled && progress && progress.complete > 0 && queued !== 0 && !confirming && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: progress.errored > 0 ? '#f59e0b' : '#10b981' }}>\n Done! {progress.complete}/{progress.total} optimized (across entire collection).\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>\n {' '}{progress.errored} failed.\n </span>\n )}\n </span>\n )}\n\n {/* Persistent optimization stats — always visible when not actively regenerating */}\n {!isRunning && !stalled && stats && stats.total > 0 && (\n <div\n style={{\n marginLeft: 'auto',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-end',\n gap: '4px',\n minWidth: '180px',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>\n {allOptimized ? (\n <span style={{ color: '#10b981' }}>\n ✓ All {stats.total} images optimized\n </span>\n ) : (\n <>\n <span style={{ color: '#6b7280' }}>\n {stats.complete}/{stats.total} optimized\n </span>\n {stats.errored > 0 && (\n <>\n <span style={{ color: '#d1d5db' }}>·</span>\n <span style={{ color: '#ef4444' }}>{stats.errored} errors</span>\n </>\n )}\n </>\n )}\n </div>\n {!allOptimized && (\n <div\n style={{\n width: '100%',\n height: '3px',\n backgroundColor: '#e5e7eb',\n borderRadius: '2px',\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${statsPercent}%`,\n backgroundColor: stats.errored > 0 ? '#f59e0b' : '#10b981',\n borderRadius: '2px',\n transition: 'width 0.3s ease',\n }}\n />\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":["React","useState","useEffect","useCallback","useRef","POLL_INTERVAL_MS","STALL_THRESHOLD","SESSION_KEY","RegenerationButton","isRunning","setIsRunning","progress","setProgress","queued","setQueued","force","setForce","error","setError","stalled","setStalled","collectionSlug","setCollectionSlug","stats","setStats","confirming","setConfirming","intervalRef","stallRef","lastProcessed","stallCount","prevIsRunningRef","slug","window","location","pathname","split","fetchStats","res","fetch","ok","data","json","stopPolling","current","clearInterval","startPolling","pollFn","setInterval","pollProgress","pending","sessionStorage","removeItem","processed","complete","errored","cancelled","loadStats","wasRunning","getItem","handlePreflight","handleCancel","handleConfirm","method","headers","body","JSON","stringify","Error","setItem","err","message","String","progressPercent","total","Math","round","showProgressBar","statsPercent","allOptimized","div","style","padding","borderBottom","display","alignItems","gap","flexWrap","button","onClick","disabled","backgroundColor","color","border","borderRadius","fontSize","fontWeight","cursor","span","label","input","type","checked","onChange","e","target","flex","minWidth","justifyContent","marginBottom","height","overflow","width","transition","marginLeft","flexDirection"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inoo-ch/payload-image-optimizer",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"description": "Payload CMS plugin for automatic image optimization — WebP/AVIF conversion, resize, EXIF strip, ThumbHash placeholders, and bulk regeneration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -13,6 +13,7 @@ const POLL_INTERVAL_MS = 2000
|
|
|
13
13
|
// With sequential processing each image takes ~4-5s, so no progress for 30s
|
|
14
14
|
// (15 polls) strongly suggests a real stall rather than slow processing.
|
|
15
15
|
const STALL_THRESHOLD = 15
|
|
16
|
+
const SESSION_KEY = 'imageOptimizer_running'
|
|
16
17
|
|
|
17
18
|
export const RegenerationButton: React.FC = () => {
|
|
18
19
|
const [isRunning, setIsRunning] = useState(false)
|
|
@@ -81,6 +82,7 @@ export const RegenerationButton: React.FC = () => {
|
|
|
81
82
|
setIsRunning(false)
|
|
82
83
|
setStalled(false)
|
|
83
84
|
stopPolling()
|
|
85
|
+
sessionStorage.removeItem(SESSION_KEY)
|
|
84
86
|
return
|
|
85
87
|
}
|
|
86
88
|
|
|
@@ -105,35 +107,40 @@ export const RegenerationButton: React.FC = () => {
|
|
|
105
107
|
}
|
|
106
108
|
}, [collectionSlug, stopPolling])
|
|
107
109
|
|
|
108
|
-
// On mount
|
|
110
|
+
// On mount: fetch stats for the counter display. If the user previously
|
|
111
|
+
// triggered regeneration (sessionStorage flag) and there are still pending
|
|
112
|
+
// images, resume polling so the UI reconnects after page navigation.
|
|
109
113
|
useEffect(() => {
|
|
110
114
|
if (!collectionSlug) return
|
|
111
115
|
let cancelled = false
|
|
112
|
-
const
|
|
116
|
+
const loadStats = async () => {
|
|
113
117
|
try {
|
|
114
118
|
const res = await fetch(
|
|
115
119
|
`/api/image-optimizer/regenerate?collection=${collectionSlug}`,
|
|
116
120
|
)
|
|
117
121
|
if (!res.ok || cancelled) return
|
|
118
122
|
const data: RegenerationProgress = await res.json()
|
|
119
|
-
// Always store stats on mount
|
|
120
123
|
setStats(data)
|
|
121
|
-
|
|
124
|
+
|
|
125
|
+
// Resume polling only if the user triggered regeneration in this session
|
|
126
|
+
const wasRunning = sessionStorage.getItem(SESSION_KEY) === collectionSlug
|
|
127
|
+
if (wasRunning && data.pending > 0) {
|
|
122
128
|
setProgress(data)
|
|
123
129
|
setIsRunning(true)
|
|
124
130
|
setStalled(false)
|
|
125
|
-
setQueued(null)
|
|
126
131
|
stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }
|
|
127
132
|
startPolling(pollProgress)
|
|
133
|
+
} else if (wasRunning && data.pending <= 0) {
|
|
134
|
+
// Jobs finished while we were away — clear the flag
|
|
135
|
+
sessionStorage.removeItem(SESSION_KEY)
|
|
128
136
|
}
|
|
129
137
|
} catch {
|
|
130
138
|
// ignore
|
|
131
139
|
}
|
|
132
140
|
}
|
|
133
|
-
|
|
141
|
+
loadStats()
|
|
134
142
|
return () => {
|
|
135
143
|
cancelled = true
|
|
136
|
-
// Clear interval on effect cleanup to prevent duplicate intervals
|
|
137
144
|
stopPolling()
|
|
138
145
|
}
|
|
139
146
|
}, [collectionSlug, pollProgress, startPolling, stopPolling])
|
|
@@ -190,6 +197,8 @@ export const RegenerationButton: React.FC = () => {
|
|
|
190
197
|
return
|
|
191
198
|
}
|
|
192
199
|
|
|
200
|
+
// Persist running state so we can resume after page navigation
|
|
201
|
+
sessionStorage.setItem(SESSION_KEY, collectionSlug)
|
|
193
202
|
// Start polling
|
|
194
203
|
startPolling(pollProgress)
|
|
195
204
|
} catch (err) {
|