diff --git a/tests/browser/workflows.spec.mjs b/tests/browser/workflows.spec.mjs
index 633c0a00..a3007d54 100644
--- a/tests/browser/workflows.spec.mjs
+++ b/tests/browser/workflows.spec.mjs
@@ -99,6 +99,34 @@ for (const leave of ['beforeunload', 'pagehide']) {
});
}
+// #222: the gallery's Clear cache clears the server's thumbnails by its own request and keeps this browser's
+// preferences and other tools' storage; nothing of Prompt Manager's is cached in a browser, and the toast says so.
+test('the gallery cache clear keeps preferences and other tools caches', async ({ page }) => {
+ await page.addInitScript(() => {
+ if (sessionStorage.getItem('seeded')) return;
+ sessionStorage.setItem('seeded', '1');
+ localStorage.setItem('pm_workspace_theme', 'light');
+ localStorage.setItem('other-tool-setting', 'keep');
+ localStorage.setItem('pm_other_tool', 'keep');
+ });
+ await page.goto('/prompt_manager/gallery');
+ await expect(page.locator('#galleryGrid .image-item')).toHaveCount(2);
+ await page.evaluate(async () => { await (await caches.open('other-tool-gallery-thumbnails')).put('/x', new Response('x')); });
+ const clears = [];
+ page.on('request', req => { if (req.url().includes('/clear-thumbnails')) clears.push(req.method()); });
+ page.once('dialog', dialog => dialog.accept());
+ await page.locator('#settingsBtn').click();
+ await page.locator('#clearCacheBtn').click();
+ await expect(page.getByText("Thumbnails cleared; original images untouched. Nothing of Prompt Manager's is cached in this browser."))
+ .toBeVisible();
+ expect(clears).toEqual(['POST']);
+ expect(await page.evaluate(async () => ({
+ theme: localStorage.getItem('pm_workspace_theme'), other: localStorage.getItem('other-tool-setting'),
+ prefixed: localStorage.getItem('pm_other_tool'), notice: localStorage.getItem('pm_update_notice_dismissed'),
+ caches: await caches.keys(),
+ }))).toEqual({ theme: 'light', other: 'keep', prefixed: 'keep', notice: '3.2.0', caches: ['other-tool-gallery-thumbnails'] });
+});
+
test('actual gallery review generates and persists tags for the selected root', async ({ page, request }) => {
await page.goto('/prompt_manager/gallery');
await expect(page.locator('#galleryGrid .image-item')).toHaveCount(2);
diff --git a/tests/js/cache-ownership.test.js b/tests/js/cache-ownership.test.js
new file mode 100644
index 00000000..b70fac5f
--- /dev/null
+++ b/tests/js/cache-ownership.test.js
@@ -0,0 +1,154 @@
+// #222: what "clear the cache" may remove from this browser. Only entries Prompt Manager itself writes as a cache count,
+// from an inventory of the real writers (never a guessed prefix): preferences (the workspace theme, the update notice,
+// the display defaults) and other tools' storage and caches on the shared ComfyUI origin always stay.
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const CacheOwnership = require('../../web/js/cache-ownership.js');
+
+const WEB = path.join(__dirname, '../../web');
+
+function memoryStorage(entries) {
+ const map = new Map(Object.entries(entries));
+ return {
+ map,
+ get length() { return map.size; },
+ key: index => [...map.keys()][index] ?? null,
+ getItem: key => (map.has(key) ? map.get(key) : null),
+ setItem: (key, value) => map.set(key, String(value)),
+ removeItem: key => map.delete(key),
+ };
+}
+
+function memoryCaches(names, { failDelete = new Set() } = {}) {
+ const set = new Set(names);
+ return {
+ set,
+ keys: async () => [...set],
+ delete: async name => {
+ if (failDelete.has(name)) throw new Error('denied');
+ return set.delete(name);
+ },
+ };
+}
+
+const PREFERENCES = { gallerySettings: '{"viewMode":"grid"}', pm_workspace_theme: 'light', pm_update_notice_dismissed: '3.3.0' };
+const OWNED = Object.freeze({ storageKeys: ['pm-owned-test-entry'], cacheNames: ['pm-owned-test-cache'] });
+
+test('keeps pm_workspace_theme, pm_update_notice_dismissed and gallerySettings', async () => {
+ const storage = memoryStorage({ ...PREFERENCES, 'pm-owned-test-entry': 'x', pm_cached: '1', promptManager_x: '1' });
+ const result = await CacheOwnership.clearOwned({ storage, cacheStorage: null, owned: OWNED });
+ assert.deepEqual(result.removed, ['pm-owned-test-entry']);
+ for (const [key, value] of Object.entries(PREFERENCES)) assert.equal(storage.getItem(key), value, key);
+ // The old prefix rule's matches are not Prompt Manager caches either: nothing writes them.
+ assert.equal(storage.getItem('pm_cached'), '1');
+ assert.equal(storage.getItem('promptManager_x'), '1');
+ for (const key of Object.keys(PREFERENCES)) assert.ok(CacheOwnership.PREFERENCE_KEYS.includes(key), key);
+});
+
+test("keeps other tools' CacheStorage named gallery/thumbnail", async () => {
+ const caches = memoryCaches(['other-gallery', 'thumbnail-cache', 'my-gallery-thumbnails', 'pm-owned-test-cache']);
+ const result = await CacheOwnership.clearOwned({ storage: memoryStorage({}), cacheStorage: caches, owned: OWNED });
+ assert.deepEqual([...caches.set].sort(), ['my-gallery-thumbnails', 'other-gallery', 'thumbnail-cache']);
+ assert.deepEqual(result.removed, ['pm-owned-test-cache']);
+});
+
+test('removes only inventoried owned keys', async () => {
+ // The shipped inventory: no Prompt Manager writer stores a cache entry in this browser.
+ assert.deepEqual(CacheOwnership.OWNED, { storageKeys: [], cacheNames: [] });
+ assert.ok(Object.isFrozen(CacheOwnership.OWNED.storageKeys) && Object.isFrozen(CacheOwnership.OWNED.cacheNames));
+ const storage = memoryStorage({ ...PREFERENCES, gallery_cache_1: 'a', metadata_cache_2: 'b', other: 'c' });
+ const caches = memoryCaches(['gallery', 'thumbnail']);
+ const result = await CacheOwnership.clearOwned({ storage, cacheStorage: caches });
+ assert.deepEqual(result, { removed: [], failed: [], denied: false, partial: false });
+ assert.equal(storage.map.size, 6);
+ assert.equal(caches.set.size, 2);
+ assert.equal(CacheOwnership.clearMessage(result), CacheOwnership.NOTHING_CACHED);
+});
+
+test('the inventory covers every browser storage writer in web/', () => {
+ // Every key a Prompt Manager page writes is a preference or an inventoried cache entry; a new writer must be
+ // classified here before a cache clear may remove it.
+ const files = [];
+ const walk = dir => {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) { if (!['lib', 'toolkit'].includes(entry.name)) walk(full); }
+ else if (/\.(m?js|html)$/.test(entry.name)) files.push(full);
+ }
+ };
+ walk(WEB);
+ const written = new Set();
+ const constants = {};
+ for (const file of files) {
+ const text = fs.readFileSync(file, 'utf8');
+ for (const match of text.matchAll(/(?:const|export const)\s+([A-Z_]+_KEY)\s*=\s*'([^']+)'/g)) constants[match[1]] = match[2];
+ assert.equal(/caches\.open\(/.test(text), false, `${path.relative(WEB, file)} opens a CacheStorage cache`);
+ assert.equal(/indexedDB\.open\(/.test(text), false, `${path.relative(WEB, file)} opens IndexedDB`);
+ for (const match of text.matchAll(/\.setItem\(\s*([^,)]+)/g)) {
+ const literal = /^(?:'([^']+)'|"([^"]+)")$/.exec(match[1].trim());
+ const constant = /^[A-Z_]+_KEY$/.test(match[1].trim()) ? `$${match[1].trim()}` : null;
+ // A key computed at run time could be anything: it must be a literal or a named key constant.
+ assert.ok(literal || constant, `${path.relative(WEB, file)} writes a computed key: ${match[1]}`);
+ written.add(literal ? literal[1] ?? literal[2] : constant);
+ }
+ }
+ const keys = [...written].map(key => (key.startsWith('$') ? constants[key.slice(1)] : key));
+ assert.ok(keys.length >= 3, 'the writers were found');
+ for (const key of keys) {
+ assert.ok(key, 'every written key resolves');
+ assert.ok(CacheOwnership.PREFERENCE_KEYS.includes(key) || CacheOwnership.OWNED.storageKeys.includes(key), key);
+ }
+});
+
+test('storage denial reports partial', async () => {
+ const denied = { get length() { throw new Error('SecurityError'); }, key() { throw new Error('SecurityError'); },
+ getItem() { throw new Error('SecurityError'); }, removeItem() { throw new Error('SecurityError'); } };
+ const result = await CacheOwnership.clearOwned({ storage: denied, cacheStorage: null, owned: OWNED });
+ assert.equal(result.denied, true);
+ assert.equal(result.partial, true);
+ assert.equal(CacheOwnership.clearMessage(result), CacheOwnership.STORAGE_DENIED);
+
+ const caches = memoryCaches(['pm-owned-test-cache'], { failDelete: new Set(['pm-owned-test-cache']) });
+ const storage = memoryStorage({ 'pm-owned-test-entry': 'x' });
+ const partial = await CacheOwnership.clearOwned({ storage, cacheStorage: caches, owned: OWNED });
+ assert.deepEqual(partial.removed, ['pm-owned-test-entry']);
+ assert.deepEqual(partial.failed, ['pm-owned-test-cache']);
+ assert.equal(partial.partial, true);
+ assert.equal(CacheOwnership.clearMessage(partial), 'Cleared 1 of 2 cached entries; this browser refused the rest.');
+
+ const keysDenied = { keys: async () => { throw new Error('denied'); } };
+ const unreadable = await CacheOwnership.clearOwned({ storage: memoryStorage({}), cacheStorage: keysDenied, owned: OWNED });
+ assert.equal(unreadable.denied, true);
+});
+
+test('server thumbnail clear stays a separate explicit call', async () => {
+ const calls = [];
+ const original = globalThis.fetch;
+ globalThis.fetch = async (...args) => { calls.push(args); return { ok: true }; };
+ try {
+ const storage = memoryStorage({ 'pm-owned-test-entry': 'x' });
+ const result = await CacheOwnership.clearOwned({ storage, cacheStorage: memoryCaches([]), owned: OWNED });
+ assert.deepEqual(result.removed, ['pm-owned-test-entry']);
+ assert.equal(CacheOwnership.clearMessage(result), 'Cleared 1 cached entry from this browser.');
+ } finally {
+ globalThis.fetch = original;
+ }
+ assert.deepEqual(calls, []);
+ assert.equal(/fetch|clear-thumbnails|ApiClient/.test(fs.readFileSync(path.join(WEB, 'js/cache-ownership.js'), 'utf8')), false);
+});
+
+test('ownedEntries lists what a clear would remove, reading nothing it cannot', async () => {
+ const storage = memoryStorage({ ...PREFERENCES, 'pm-owned-test-entry': 'x' });
+ const caches = memoryCaches(['pm-owned-test-cache', 'gallery']);
+ assert.deepEqual(await CacheOwnership.ownedEntries({ storage, cacheStorage: caches, owned: OWNED }),
+ { keys: ['pm-owned-test-entry'], caches: ['pm-owned-test-cache'], denied: false });
+ assert.deepEqual(await CacheOwnership.ownedEntries({ storage, cacheStorage: caches }),
+ { keys: [], caches: [], denied: false });
+ assert.deepEqual(await CacheOwnership.ownedEntries({ storage: null, cacheStorage: undefined, owned: OWNED }),
+ { keys: [], caches: [], denied: false });
+ assert.equal(CacheOwnership.clearMessage({ removed: ['a', 'b'], failed: [], denied: false, partial: false }),
+ 'Cleared 2 cached entries from this browser.');
+});
diff --git a/tests/js/legacy-refusals.test.js b/tests/js/legacy-refusals.test.js
index 6daec1c7..c06daedc 100644
--- a/tests/js/legacy-refusals.test.js
+++ b/tests/js/legacy-refusals.test.js
@@ -32,8 +32,11 @@ function sseResponse(frames) {
function load(file, className, fetchUrl, extra = {}, dom = elements()) {
const timers = [];
const notifications = [];
- const storage = new Map([['pm_cached', '1'], ['userData', 'keep']]);
+ const storage = new Map([['pm_cached', '1'], ['userData', 'keep'], ['pm_workspace_theme', 'light'],
+ ['pm_update_notice_dismissed', '3.3.0']]);
const localStorage = {
+ get length() { return storage.size; },
+ key: index => [...storage.keys()][index] ?? null,
getItem: key => storage.get(key) ?? null,
setItem: (key, value) => storage.set(key, String(value)),
removeItem: key => storage.delete(key),
@@ -45,11 +48,12 @@ function load(file, className, fetchUrl, extra = {}, dom = elements()) {
setTimeout: callback => timers.push(callback),
confirm: () => true,
localStorage,
- Object: { ...Object, keys: target => (target === localStorage ? [...storage.keys()] : Object.keys(target)) },
TextDecoder, TextEncoder, JSON,
ApiClient: { fetchUrl },
...extra,
});
+ // gallery.html loads cache-ownership.js before gallery.js (#222).
+ vm.runInContext(fs.readFileSync(path.join(__dirname, '../../web/js/cache-ownership.js'), 'utf8'), context);
const source = fs.readFileSync(path.join(__dirname, `../../web/js/${file}`), 'utf8');
const Page = vm.runInContext(`${source}\n${className};`, context, { filename: path.join(__dirname, `../../web/js/${file}`) });
const page = Object.create(Page.prototype);
@@ -78,18 +82,68 @@ test('gallery cache clear reports a refused server clear and never claims succes
assert.equal(storage.get('gallerySettings'), undefined); // thumbnailsGenerated is not reset
});
-test('gallery cache clear still says so when the server clears', async () => {
- const { page, dom, notifications, storage } = load('gallery.js', 'GalleryManager',
- async () => jsonResponse(200, { success: true, cleared_count: 3 }));
+test('gallery cache clear says what it cleared and keeps preferences and other data (#222)', async () => {
+ const calls = [];
+ const { page, dom, notifications, storage } = load('gallery.js', 'GalleryManager', async url => {
+ calls.push(url);
+ return jsonResponse(200, { success: true, cleared_count: 3 });
+ });
page.getSettings = () => ({ thumbnailsGenerated: true });
await page.clearCache();
- assert.deepEqual(notifications, [['success', 'PromptManager cache cleared successfully! Original images untouched.']]);
+ // The server's thumbnails are cleared by their own explicit call; nothing Prompt Manager owns is cached in this
+ // browser, so no browser entry is removed and the toast says exactly that.
+ assert.deepEqual(calls, ['/prompt_manager/images/clear-thumbnails']);
+ assert.deepEqual(notifications, [['success',
+ "Thumbnails cleared; original images untouched. Nothing of Prompt Manager's is cached in this browser."]]);
assert.equal(dom.nodes.thumbnailStatus.textContent, 'Not generated');
assert.equal(JSON.parse(storage.get('gallerySettings')).thumbnailsGenerated, false);
assert.equal(storage.get('userData'), 'keep');
- assert.equal(storage.has('pm_cached'), false);
+ assert.equal(storage.get('pm_cached'), '1');
+ assert.equal(storage.get('pm_workspace_theme'), 'light');
+ assert.equal(storage.get('pm_update_notice_dismissed'), '3.3.0');
+});
+
+test("gallery cache clear never deletes another tool's gallery or thumbnail caches (#222)", async () => {
+ const names = new Set(['comfy-gallery', 'thumbnail-cache']);
+ const caches = { keys: async () => [...names], delete: async name => names.delete(name) };
+ const { page, notifications } = load('gallery.js', 'GalleryManager',
+ async () => jsonResponse(200, { success: true }), { caches, window: { caches } });
+ page.getSettings = () => ({});
+
+ await page.clearCache();
+
+ assert.deepEqual([...names], ['comfy-gallery', 'thumbnail-cache']);
+ assert.equal(notifications.at(-1)[0], 'success');
+});
+
+test('gallery cache clear reports a lost server clear instead of success', async () => {
+ const { page, dom, notifications, storage } = load('gallery.js', 'GalleryManager', async () => {
+ throw new TypeError('Failed to fetch');
+ });
+ page.getSettings = () => ({ thumbnailsGenerated: true });
+ dom.getElementById('thumbnailStatus').textContent = 'Generated';
+
+ await page.clearCache();
+
+ assert.deepEqual(notifications, [['error', 'Thumbnails were not cleared: the server could not be reached.']]);
+ assert.equal(dom.nodes.thumbnailStatus.textContent, 'Generated');
+ assert.equal(storage.get('gallerySettings'), undefined);
+});
+
+test('gallery cache clear says so when browser storage is denied', async () => {
+ const denied = { get length() { throw new Error('SecurityError'); }, key() { throw new Error('SecurityError'); },
+ getItem() { throw new Error('SecurityError'); }, setItem() { throw new Error('SecurityError'); },
+ removeItem() { throw new Error('SecurityError'); } };
+ const { page, notifications } = load('gallery.js', 'GalleryManager',
+ async () => jsonResponse(200, { success: true }), { localStorage: denied });
+ page.getSettings = () => ({});
+
+ await page.clearCache();
+
+ assert.deepEqual(notifications, [['warning',
+ 'Thumbnails cleared; original images untouched. This browser refused access to its storage, so nothing there was cleared.']]);
});
test('admin LoRA import shows a refusal frame and closes its modal', async () => {
diff --git a/tools/vendor/frontend-coverage-baseline.json b/tools/vendor/frontend-coverage-baseline.json
index 49f91a58..59f1afa1 100644
--- a/tools/vendor/frontend-coverage-baseline.json
+++ b/tools/vendor/frontend-coverage-baseline.json
@@ -11,6 +11,12 @@
"functions": 98,
"statements": 98
},
+ "cache-ownership.js": {
+ "lines": 80,
+ "branches": 80,
+ "functions": 80,
+ "statements": 80
+ },
"classic-node.js": {
"lines": 98,
"branches": 79,
diff --git a/web/gallery.html b/web/gallery.html
index 36543893..ce1c8547 100644
--- a/web/gallery.html
+++ b/web/gallery.html
@@ -8,6 +8,7 @@
+
diff --git a/web/js/cache-ownership.js b/web/js/cache-ownership.js
new file mode 100644
index 00000000..804d1dae
--- /dev/null
+++ b/web/js/cache-ownership.js
@@ -0,0 +1,119 @@
+/**
+ * What "clear the cache" may remove from this browser (#222).
+ *
+ * ComfyUI serves every custom node from one origin, so a browser's localStorage and CacheStorage are shared with other
+ * tools. A cache clear removes only the entries Prompt Manager itself writes as a cache, listed by name in `OWNED` from
+ * an inventory of the real writers, never by a guessed prefix. The inventory (2026-10-24, enforced by
+ * tests/js/cache-ownership.test.js): every key a Prompt Manager page writes is a preference (`PREFERENCE_KEYS`: the
+ * display defaults, the workspace theme, the dismissed update notice), and no page opens a CacheStorage cache or
+ * IndexedDB. So `OWNED` is empty: nothing of Prompt Manager's is cached in this browser, and a clear says so instead of
+ * claiming it removed something.
+ *
+ * Server thumbnails are not browser data: clearing them is the caller's own, separate request. This module never makes
+ * a request.
+ *
+ * Loadable in the browser (window.CacheOwnership) and in Node for tests.
+ */
+(function (root) {
+ "use strict";
+
+ const PREFERENCE_KEYS = Object.freeze(["gallerySettings", "pm_workspace_theme", "pm_update_notice_dismissed"]);
+ const OWNED = Object.freeze({ storageKeys: Object.freeze([]), cacheNames: Object.freeze([]) });
+ const NOTHING_CACHED = "Nothing of Prompt Manager's is cached in this browser.";
+ const STORAGE_DENIED = "This browser refused access to its storage, so nothing there was cleared.";
+
+ // What a storage getter that threw (the browser refused storage) resolves to.
+ const REFUSED = Object.freeze({});
+
+ /** `storage`, or what its getter returns; `REFUSED` when the getter throws. */
+ function resolveStorage(storage) {
+ if (typeof storage !== "function") return storage;
+ try {
+ return storage() ?? null;
+ } catch {
+ return REFUSED;
+ }
+ }
+
+ /** The keys `storage` holds, or null when the browser refuses to say. */
+ function storageKeys(storage) {
+ if (storage === REFUSED) return null;
+ if (!storage) return [];
+ try {
+ const keys = [];
+ for (let index = 0; index < storage.length; index += 1) keys.push(storage.key(index));
+ return keys;
+ } catch {
+ return null;
+ }
+ }
+
+ async function cacheNames(cacheStorage) {
+ if (!cacheStorage || typeof cacheStorage.keys !== "function") return [];
+ try {
+ return await cacheStorage.keys();
+ } catch {
+ return null;
+ }
+ }
+
+ /**
+ * The owned entries present now: `{keys, caches, denied}`; `denied` when the browser refused to list its storage or
+ * caches.
+ */
+ async function ownedEntries({ storage, cacheStorage, owned = OWNED } = {}) {
+ const present = storageKeys(resolveStorage(storage));
+ const names = await cacheNames(cacheStorage);
+ return {
+ keys: (present ?? []).filter(key => owned.storageKeys.includes(key)),
+ caches: (names ?? []).filter(name => owned.cacheNames.includes(name)),
+ denied: present === null || names === null,
+ };
+ }
+
+ /**
+ * Remove the owned entries present now. `storage` is a Storage or a getter for one (`() => localStorage`, which a
+ * browser refusing storage makes throw). Returns `{removed, failed, denied, partial}`: what was removed and what the
+ * browser refused to remove, by name; `partial` when anything was refused, including listing the storage.
+ */
+ async function clearOwned({ storage, cacheStorage, owned = OWNED } = {}) {
+ storage = resolveStorage(storage);
+ const { keys, caches, denied } = await ownedEntries({ storage, cacheStorage, owned });
+ const removed = [];
+ const failed = [];
+ for (const key of keys) {
+ try {
+ storage.removeItem(key);
+ removed.push(key);
+ } catch {
+ failed.push(key);
+ }
+ }
+ for (const name of caches) {
+ try {
+ await cacheStorage.delete(name);
+ removed.push(name);
+ } catch {
+ failed.push(name);
+ }
+ }
+ return { removed, failed, denied, partial: denied || failed.length > 0 };
+ }
+
+ const entries = count => `${count.toLocaleString("en-US")} cached ${count === 1 ? "entry" : "entries"}`;
+
+ /** What a clear's result says, truthfully. */
+ function clearMessage(result) {
+ const total = result.removed.length + result.failed.length;
+ if (result.failed.length) {
+ return `Cleared ${result.removed.length.toLocaleString("en-US")} of ${entries(total)}; this browser refused the rest.`;
+ }
+ if (result.denied) return STORAGE_DENIED;
+ if (!total) return NOTHING_CACHED;
+ return `Cleared ${entries(total)} from this browser.`;
+ }
+
+ const api = { PREFERENCE_KEYS, OWNED, NOTHING_CACHED, STORAGE_DENIED, ownedEntries, clearOwned, clearMessage };
+ if (typeof module !== "undefined" && module.exports) module.exports = api;
+ else root.CacheOwnership = api;
+})(typeof window !== "undefined" ? window : globalThis);
diff --git a/web/js/gallery.js b/web/js/gallery.js
index 77781e5a..97834b04 100644
--- a/web/js/gallery.js
+++ b/web/js/gallery.js
@@ -2166,61 +2166,44 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
}
+ /**
+ * Clear cache (#222): the server's thumbnails by their own request, then only the browser entries Prompt
+ * Manager owns as a cache (cache-ownership.js; preferences and other tools' storage stay). Nothing is
+ * claimed that did not happen: a refused or lost server clear changes nothing here, and the toast says what
+ * the browser part removed, or that nothing of Prompt Manager's is cached, or that storage was refused.
+ */
async clearCache() {
- if (confirm('Clear PromptManager cache and thumbnails? This will NOT affect your original images.')) {
- try {
- // Clear only PromptManager-related browser caches (never touch user images)
- if ('caches' in window) {
- const cacheNames = await caches.keys();
- // Only clear caches that are specifically ours
- const ourCaches = cacheNames.filter(name =>
- name.includes('prompt-manager') ||
- name.includes('gallery') ||
- name.includes('thumbnail')
- );
- await Promise.all(ourCaches.map(name => caches.delete(name)));
- }
-
- // Clear only our localStorage entries (never touch user data)
- Object.keys(localStorage).forEach(key => {
- if (key.startsWith('gallery_cache_') ||
- key.startsWith('metadata_cache_') ||
- key.startsWith('promptManager_') ||
- key.startsWith('pm_')) {
- localStorage.removeItem(key);
- }
- });
-
- // Call backend to clear server-side thumbnails (only our generated thumbnails)
- try {
- const response = await ApiClient.fetchUrl('/prompt_manager/images/clear-thumbnails', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- if (!response.ok) {
- // Refused (a running job holds the thumbnails): say why and claim nothing.
- const data = await response.json().catch(() => ({}));
- this.showNotification(`Thumbnails were not cleared: ${data.error || `HTTP ${response.status}`}`, 'error');
- return;
- }
- } catch (e) {
- console.warn('Could not clear server thumbnails:', e);
- // Not a critical error - continue with local cleanup
- }
-
- // Update thumbnail status
- const settings = this.getSettings();
- settings.thumbnailsGenerated = false;
- localStorage.setItem('gallerySettings', JSON.stringify(settings));
- document.getElementById('thumbnailStatus').textContent = 'Not generated';
-
- this.showNotification('PromptManager cache cleared successfully! Original images untouched.', 'success');
- } catch (error) {
- console.error('Clear cache error:', error);
- this.showNotification('Failed to clear cache', 'error');
- }
+ if (!confirm('Clear PromptManager cache and thumbnails? This will NOT affect your original images.')) return;
+ let response;
+ try {
+ response = await ApiClient.fetchUrl('/prompt_manager/images/clear-thumbnails', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.warn('Could not clear server thumbnails:', error);
+ this.showNotification('Thumbnails were not cleared: the server could not be reached.', 'error');
+ return;
}
+ if (!response.ok) {
+ // Refused (a running job holds the thumbnails): say why and claim nothing.
+ const data = await response.json().catch(() => ({}));
+ this.showNotification(`Thumbnails were not cleared: ${data.error || `HTTP ${response.status}`}`, 'error');
+ return;
+ }
+ // Read through a getter: a browser that refuses storage throws on `localStorage` itself.
+ const browser = await window.CacheOwnership.clearOwned({ storage: () => localStorage,
+ cacheStorage: 'caches' in window ? window.caches : null });
+ try {
+ const settings = this.getSettings();
+ settings.thumbnailsGenerated = false;
+ localStorage.setItem('gallerySettings', JSON.stringify(settings));
+ } catch (error) {
+ console.warn('Could not record the cleared thumbnails:', error);
+ }
+ document.getElementById('thumbnailStatus').textContent = 'Not generated';
+ this.showNotification(`Thumbnails cleared; original images untouched. ${window.CacheOwnership.clearMessage(browser)}`,
+ browser.partial ? 'warning' : 'success');
}
async rescanFolder() {