@inoo-ch/payload-image-optimizer 1.4.2 → 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.
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
-
const
|
|
4
|
+
const POLL_INTERVAL_MS = 2000;
|
|
5
|
+
// With sequential processing each image takes ~4-5s, so no progress for 30s
|
|
6
|
+
// (15 polls) strongly suggests a real stall rather than slow processing.
|
|
7
|
+
const STALL_THRESHOLD = 15;
|
|
8
|
+
const SESSION_KEY = 'imageOptimizer_running';
|
|
5
9
|
export const RegenerationButton = ()=>{
|
|
6
10
|
const [isRunning, setIsRunning] = useState(false);
|
|
7
11
|
const [progress, setProgress] = useState(null);
|
|
@@ -44,6 +48,13 @@ export const RegenerationButton = ()=>{
|
|
|
44
48
|
intervalRef.current = null;
|
|
45
49
|
}
|
|
46
50
|
}, []);
|
|
51
|
+
const startPolling = useCallback((pollFn)=>{
|
|
52
|
+
// Prevent duplicate intervals
|
|
53
|
+
stopPolling();
|
|
54
|
+
intervalRef.current = setInterval(pollFn, POLL_INTERVAL_MS);
|
|
55
|
+
}, [
|
|
56
|
+
stopPolling
|
|
57
|
+
]);
|
|
47
58
|
const pollProgress = useCallback(async ()=>{
|
|
48
59
|
if (!collectionSlug) return;
|
|
49
60
|
try {
|
|
@@ -54,21 +65,24 @@ export const RegenerationButton = ()=>{
|
|
|
54
65
|
// Stop polling when no more pending
|
|
55
66
|
if (data.pending <= 0) {
|
|
56
67
|
setIsRunning(false);
|
|
68
|
+
setStalled(false);
|
|
57
69
|
stopPolling();
|
|
70
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
58
71
|
return;
|
|
59
72
|
}
|
|
60
|
-
// Stall detection
|
|
73
|
+
// Stall detection — warn but keep polling so we detect when jobs resume
|
|
61
74
|
const processed = data.complete + data.errored;
|
|
62
75
|
if (processed === stallRef.current.lastProcessed) {
|
|
63
76
|
stallRef.current.stallCount += 1;
|
|
64
77
|
} else {
|
|
65
78
|
stallRef.current.stallCount = 0;
|
|
66
79
|
stallRef.current.lastProcessed = processed;
|
|
80
|
+
// Clear stall warning when progress resumes
|
|
81
|
+
setStalled(false);
|
|
67
82
|
}
|
|
68
83
|
if (stallRef.current.stallCount >= STALL_THRESHOLD) {
|
|
69
|
-
stopPolling();
|
|
70
|
-
setIsRunning(false);
|
|
71
84
|
setStalled(true);
|
|
85
|
+
// Keep polling — jobs may still be running server-side
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
} catch {
|
|
@@ -78,39 +92,47 @@ export const RegenerationButton = ()=>{
|
|
|
78
92
|
collectionSlug,
|
|
79
93
|
stopPolling
|
|
80
94
|
]);
|
|
81
|
-
// 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.
|
|
82
98
|
useEffect(()=>{
|
|
83
99
|
if (!collectionSlug) return;
|
|
84
100
|
let cancelled = false;
|
|
85
|
-
const
|
|
101
|
+
const loadStats = async ()=>{
|
|
86
102
|
try {
|
|
87
103
|
const res = await fetch(`/api/image-optimizer/regenerate?collection=${collectionSlug}`);
|
|
88
104
|
if (!res.ok || cancelled) return;
|
|
89
105
|
const data = await res.json();
|
|
90
|
-
// Always store stats on mount
|
|
91
106
|
setStats(data);
|
|
92
|
-
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) {
|
|
93
110
|
setProgress(data);
|
|
94
111
|
setIsRunning(true);
|
|
95
112
|
setStalled(false);
|
|
96
|
-
setQueued(null);
|
|
97
113
|
stallRef.current = {
|
|
98
114
|
lastProcessed: data.complete + data.errored,
|
|
99
115
|
stallCount: 0
|
|
100
116
|
};
|
|
101
|
-
|
|
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);
|
|
102
121
|
}
|
|
103
122
|
} catch {
|
|
104
123
|
// ignore
|
|
105
124
|
}
|
|
106
125
|
};
|
|
107
|
-
|
|
126
|
+
loadStats();
|
|
108
127
|
return ()=>{
|
|
109
128
|
cancelled = true;
|
|
129
|
+
stopPolling();
|
|
110
130
|
};
|
|
111
131
|
}, [
|
|
112
132
|
collectionSlug,
|
|
113
|
-
pollProgress
|
|
133
|
+
pollProgress,
|
|
134
|
+
startPolling,
|
|
135
|
+
stopPolling
|
|
114
136
|
]);
|
|
115
137
|
// Refresh stats when regeneration finishes (isRunning transitions from true to false)
|
|
116
138
|
useEffect(()=>{
|
|
@@ -167,8 +189,10 @@ export const RegenerationButton = ()=>{
|
|
|
167
189
|
setIsRunning(false);
|
|
168
190
|
return;
|
|
169
191
|
}
|
|
192
|
+
// Persist running state so we can resume after page navigation
|
|
193
|
+
sessionStorage.setItem(SESSION_KEY, collectionSlug);
|
|
170
194
|
// Start polling
|
|
171
|
-
|
|
195
|
+
startPolling(pollProgress);
|
|
172
196
|
} catch (err) {
|
|
173
197
|
setError(err instanceof Error ? err.message : String(err));
|
|
174
198
|
setIsRunning(false);
|
|
@@ -176,10 +200,10 @@ export const RegenerationButton = ()=>{
|
|
|
176
200
|
};
|
|
177
201
|
// Cleanup interval on unmount
|
|
178
202
|
useEffect(()=>{
|
|
179
|
-
return ()=>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
return ()=>stopPolling();
|
|
204
|
+
}, [
|
|
205
|
+
stopPolling
|
|
206
|
+
]);
|
|
183
207
|
if (!collectionSlug) return null;
|
|
184
208
|
const progressPercent = progress && progress.total > 0 ? Math.round((progress.complete + progress.errored) / progress.total * 100) : 0;
|
|
185
209
|
const showProgressBar = isRunning && progress || stalled && progress;
|
|
@@ -305,13 +329,11 @@ export const RegenerationButton = ()=>{
|
|
|
305
329
|
fontSize: '13px'
|
|
306
330
|
},
|
|
307
331
|
children: [
|
|
308
|
-
"Processing
|
|
309
|
-
progress.
|
|
332
|
+
"Processing appears slow — ",
|
|
333
|
+
progress.pending,
|
|
310
334
|
" image",
|
|
311
|
-
progress.
|
|
312
|
-
"
|
|
313
|
-
progress.pending > 0 ? ` (${progress.pending} stuck)` : '',
|
|
314
|
-
". Re-run to retry."
|
|
335
|
+
progress.pending !== 1 ? 's' : '',
|
|
336
|
+
" still pending. Jobs may still be running server-side."
|
|
315
337
|
]
|
|
316
338
|
}),
|
|
317
339
|
showProgressBar && /*#__PURE__*/ _jsxs("div", {
|
|
@@ -382,14 +404,14 @@ export const RegenerationButton = ()=>{
|
|
|
382
404
|
})
|
|
383
405
|
]
|
|
384
406
|
}),
|
|
385
|
-
!isRunning && progress && progress.complete > 0 && queued !== 0 && !confirming && /*#__PURE__*/ _jsxs("span", {
|
|
407
|
+
!isRunning && !stalled && progress && progress.complete > 0 && queued !== 0 && !confirming && /*#__PURE__*/ _jsxs("span", {
|
|
386
408
|
style: {
|
|
387
409
|
fontSize: '13px'
|
|
388
410
|
},
|
|
389
411
|
children: [
|
|
390
412
|
/*#__PURE__*/ _jsxs("span", {
|
|
391
413
|
style: {
|
|
392
|
-
color: progress.errored > 0
|
|
414
|
+
color: progress.errored > 0 ? '#f59e0b' : '#10b981'
|
|
393
415
|
},
|
|
394
416
|
children: [
|
|
395
417
|
"Done! ",
|
|
@@ -399,19 +421,19 @@ export const RegenerationButton = ()=>{
|
|
|
399
421
|
" optimized (across entire collection)."
|
|
400
422
|
]
|
|
401
423
|
}),
|
|
402
|
-
|
|
424
|
+
progress.errored > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
403
425
|
style: {
|
|
404
426
|
color: '#ef4444'
|
|
405
427
|
},
|
|
406
428
|
children: [
|
|
407
429
|
' ',
|
|
408
|
-
progress.errored
|
|
430
|
+
progress.errored,
|
|
409
431
|
" failed."
|
|
410
432
|
]
|
|
411
433
|
})
|
|
412
434
|
]
|
|
413
435
|
}),
|
|
414
|
-
!isRunning && stats && stats.total > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
436
|
+
!isRunning && !stalled && stats && stats.total > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
415
437
|
style: {
|
|
416
438
|
marginLeft: 'auto',
|
|
417
439
|
display: 'flex',
|
|
@@ -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 STALL_THRESHOLD = 5\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 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 stopPolling()\n return\n }\n\n // Stall detection\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 }\n\n if (stallRef.current.stallCount >= STALL_THRESHOLD) {\n stopPolling()\n setIsRunning(false)\n setStalled(true)\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 intervalRef.current = setInterval(pollProgress, 2000)\n }\n } catch {\n // ignore\n }\n }\n checkOngoing()\n return () => {\n cancelled = true\n }\n }, [collectionSlug, pollProgress])\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 intervalRef.current = setInterval(pollProgress, 2000)\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 () => {\n if (intervalRef.current) clearInterval(intervalRef.current)\n }\n }, [])\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 finished with issues. {progress.errored + progress.pending} image\n {progress.errored + progress.pending !== 1 ? 's' : ''} failed\n {progress.pending > 0 ? ` (${progress.pending} stuck)` : ''}.\n Re-run to retry.\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 && progress && progress.complete > 0 && queued !== 0 && !confirming && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: progress.errored > 0 || stalled ? '#f59e0b' : '#10b981' }}>\n Done! {progress.complete}/{progress.total} optimized (across entire collection).\n </span>\n {(progress.errored > 0 || (stalled && progress.pending > 0)) && (\n <span style={{ color: '#ef4444' }}>\n {' '}{progress.errored + (stalled ? progress.pending : 0)} failed.\n </span>\n )}\n </span>\n )}\n\n {/* Persistent optimization stats — always visible when not actively regenerating */}\n {!isRunning && 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","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","pollProgress","pending","processed","complete","errored","cancelled","checkOngoing","setInterval","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": [
|
|
@@ -9,7 +9,11 @@ type RegenerationProgress = {
|
|
|
9
9
|
pending: number
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const POLL_INTERVAL_MS = 2000
|
|
13
|
+
// With sequential processing each image takes ~4-5s, so no progress for 30s
|
|
14
|
+
// (15 polls) strongly suggests a real stall rather than slow processing.
|
|
15
|
+
const STALL_THRESHOLD = 15
|
|
16
|
+
const SESSION_KEY = 'imageOptimizer_running'
|
|
13
17
|
|
|
14
18
|
export const RegenerationButton: React.FC = () => {
|
|
15
19
|
const [isRunning, setIsRunning] = useState(false)
|
|
@@ -54,6 +58,15 @@ export const RegenerationButton: React.FC = () => {
|
|
|
54
58
|
}
|
|
55
59
|
}, [])
|
|
56
60
|
|
|
61
|
+
const startPolling = useCallback(
|
|
62
|
+
(pollFn: () => void) => {
|
|
63
|
+
// Prevent duplicate intervals
|
|
64
|
+
stopPolling()
|
|
65
|
+
intervalRef.current = setInterval(pollFn, POLL_INTERVAL_MS)
|
|
66
|
+
},
|
|
67
|
+
[stopPolling],
|
|
68
|
+
)
|
|
69
|
+
|
|
57
70
|
const pollProgress = useCallback(async () => {
|
|
58
71
|
if (!collectionSlug) return
|
|
59
72
|
try {
|
|
@@ -67,23 +80,26 @@ export const RegenerationButton: React.FC = () => {
|
|
|
67
80
|
// Stop polling when no more pending
|
|
68
81
|
if (data.pending <= 0) {
|
|
69
82
|
setIsRunning(false)
|
|
83
|
+
setStalled(false)
|
|
70
84
|
stopPolling()
|
|
85
|
+
sessionStorage.removeItem(SESSION_KEY)
|
|
71
86
|
return
|
|
72
87
|
}
|
|
73
88
|
|
|
74
|
-
// Stall detection
|
|
89
|
+
// Stall detection — warn but keep polling so we detect when jobs resume
|
|
75
90
|
const processed = data.complete + data.errored
|
|
76
91
|
if (processed === stallRef.current.lastProcessed) {
|
|
77
92
|
stallRef.current.stallCount += 1
|
|
78
93
|
} else {
|
|
79
94
|
stallRef.current.stallCount = 0
|
|
80
95
|
stallRef.current.lastProcessed = processed
|
|
96
|
+
// Clear stall warning when progress resumes
|
|
97
|
+
setStalled(false)
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
if (stallRef.current.stallCount >= STALL_THRESHOLD) {
|
|
84
|
-
stopPolling()
|
|
85
|
-
setIsRunning(false)
|
|
86
101
|
setStalled(true)
|
|
102
|
+
// Keep polling — jobs may still be running server-side
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
} catch {
|
|
@@ -91,36 +107,43 @@ export const RegenerationButton: React.FC = () => {
|
|
|
91
107
|
}
|
|
92
108
|
}, [collectionSlug, stopPolling])
|
|
93
109
|
|
|
94
|
-
// 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.
|
|
95
113
|
useEffect(() => {
|
|
96
114
|
if (!collectionSlug) return
|
|
97
115
|
let cancelled = false
|
|
98
|
-
const
|
|
116
|
+
const loadStats = async () => {
|
|
99
117
|
try {
|
|
100
118
|
const res = await fetch(
|
|
101
119
|
`/api/image-optimizer/regenerate?collection=${collectionSlug}`,
|
|
102
120
|
)
|
|
103
121
|
if (!res.ok || cancelled) return
|
|
104
122
|
const data: RegenerationProgress = await res.json()
|
|
105
|
-
// Always store stats on mount
|
|
106
123
|
setStats(data)
|
|
107
|
-
|
|
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) {
|
|
108
128
|
setProgress(data)
|
|
109
129
|
setIsRunning(true)
|
|
110
130
|
setStalled(false)
|
|
111
|
-
setQueued(null)
|
|
112
131
|
stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }
|
|
113
|
-
|
|
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)
|
|
114
136
|
}
|
|
115
137
|
} catch {
|
|
116
138
|
// ignore
|
|
117
139
|
}
|
|
118
140
|
}
|
|
119
|
-
|
|
141
|
+
loadStats()
|
|
120
142
|
return () => {
|
|
121
143
|
cancelled = true
|
|
144
|
+
stopPolling()
|
|
122
145
|
}
|
|
123
|
-
}, [collectionSlug, pollProgress])
|
|
146
|
+
}, [collectionSlug, pollProgress, startPolling, stopPolling])
|
|
124
147
|
|
|
125
148
|
// Refresh stats when regeneration finishes (isRunning transitions from true to false)
|
|
126
149
|
useEffect(() => {
|
|
@@ -174,8 +197,10 @@ export const RegenerationButton: React.FC = () => {
|
|
|
174
197
|
return
|
|
175
198
|
}
|
|
176
199
|
|
|
200
|
+
// Persist running state so we can resume after page navigation
|
|
201
|
+
sessionStorage.setItem(SESSION_KEY, collectionSlug)
|
|
177
202
|
// Start polling
|
|
178
|
-
|
|
203
|
+
startPolling(pollProgress)
|
|
179
204
|
} catch (err) {
|
|
180
205
|
setError(err instanceof Error ? err.message : String(err))
|
|
181
206
|
setIsRunning(false)
|
|
@@ -184,10 +209,8 @@ export const RegenerationButton: React.FC = () => {
|
|
|
184
209
|
|
|
185
210
|
// Cleanup interval on unmount
|
|
186
211
|
useEffect(() => {
|
|
187
|
-
return () =>
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
}, [])
|
|
212
|
+
return () => stopPolling()
|
|
213
|
+
}, [stopPolling])
|
|
191
214
|
|
|
192
215
|
if (!collectionSlug) return null
|
|
193
216
|
|
|
@@ -307,10 +330,8 @@ export const RegenerationButton: React.FC = () => {
|
|
|
307
330
|
|
|
308
331
|
{stalled && progress && (
|
|
309
332
|
<span style={{ color: '#f59e0b', fontSize: '13px' }}>
|
|
310
|
-
Processing
|
|
311
|
-
|
|
312
|
-
{progress.pending > 0 ? ` (${progress.pending} stuck)` : ''}.
|
|
313
|
-
Re-run to retry.
|
|
333
|
+
Processing appears slow — {progress.pending} image{progress.pending !== 1 ? 's' : ''} still pending.
|
|
334
|
+
Jobs may still be running server-side.
|
|
314
335
|
</span>
|
|
315
336
|
)}
|
|
316
337
|
|
|
@@ -363,21 +384,21 @@ export const RegenerationButton: React.FC = () => {
|
|
|
363
384
|
</div>
|
|
364
385
|
)}
|
|
365
386
|
|
|
366
|
-
{!isRunning && progress && progress.complete > 0 && queued !== 0 && !confirming && (
|
|
387
|
+
{!isRunning && !stalled && progress && progress.complete > 0 && queued !== 0 && !confirming && (
|
|
367
388
|
<span style={{ fontSize: '13px' }}>
|
|
368
|
-
<span style={{ color: progress.errored > 0
|
|
389
|
+
<span style={{ color: progress.errored > 0 ? '#f59e0b' : '#10b981' }}>
|
|
369
390
|
Done! {progress.complete}/{progress.total} optimized (across entire collection).
|
|
370
391
|
</span>
|
|
371
|
-
{
|
|
392
|
+
{progress.errored > 0 && (
|
|
372
393
|
<span style={{ color: '#ef4444' }}>
|
|
373
|
-
{' '}{progress.errored
|
|
394
|
+
{' '}{progress.errored} failed.
|
|
374
395
|
</span>
|
|
375
396
|
)}
|
|
376
397
|
</span>
|
|
377
398
|
)}
|
|
378
399
|
|
|
379
400
|
{/* Persistent optimization stats — always visible when not actively regenerating */}
|
|
380
|
-
{!isRunning && stats && stats.total > 0 && (
|
|
401
|
+
{!isRunning && !stalled && stats && stats.total > 0 && (
|
|
381
402
|
<div
|
|
382
403
|
style={{
|
|
383
404
|
marginLeft: 'auto',
|