(() => { const perf = window.AssetPerf; perf?.start('browser-state:gesamtes Modul'); const persistent = document.body?.dataset.persistBrowserState === 'true'; const fallbackStorage = persistent ? window.localStorage : window.sessionStorage; const alternateStorage = persistent ? window.sessionStorage : window.localStorage; const prefix = 'assetmanager:'; let serverBacked = false; let serverState = {}; let pendingUpdates = {}; let pendingRemovals = new Set(); let saveTimer = null; let saveChain = Promise.resolve(); function browserGet(storage, key) { try { return storage.getItem(prefix + key); } catch (_) { return null; } } function browserSet(storage, key, value) { try { storage.setItem(prefix + key, value); } catch (_) {} } function browserRemove(storage, key) { try { storage.removeItem(prefix + key); } catch (_) {} } function collectBrowserState() { const result = {}; const collect = storage => { try { for (let index = 0; index < storage.length; index += 1) { const fullKey = storage.key(index); if (!fullKey || !fullKey.startsWith(prefix)) continue; const key = fullKey.slice(prefix.length); if (!key || Object.prototype.hasOwnProperty.call(result, key)) continue; const value = storage.getItem(fullKey); if (value !== null) result[key] = value; } } catch (_) {} }; // Prefer the storage mode that was active for this user before the update. collect(fallbackStorage); collect(alternateStorage); return result; } async function postState(updates = {}, removals = []) { const response = await fetch('/api/user-ui-state', { method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({updates, remove: removals}), keepalive: true, }); if (!response.ok) throw new Error(`UI state save failed (${response.status})`); } function flushPending() { if (!serverBacked) return Promise.resolve(); if (saveTimer !== null) { clearTimeout(saveTimer); saveTimer = null; } const updates = pendingUpdates; const removals = [...pendingRemovals]; pendingUpdates = {}; pendingRemovals = new Set(); if (!Object.keys(updates).length && !removals.length) return saveChain; saveChain = saveChain .catch(() => {}) .then(() => postState(updates, removals)) .catch(error => console.warn('[AssetManager] Benutzerbezogener Tabellenzustand konnte nicht gespeichert werden.', error)); return saveChain; } function scheduleSave() { if (!serverBacked) return; if (saveTimer !== null) clearTimeout(saveTimer); saveTimer = window.setTimeout(flushPending, 350); } const api = { persistent, serverBacked: false, ready: Promise.resolve(), get(key) { const normalized = String(key || ''); if (!normalized) return null; if (serverBacked) { const value = serverState[normalized]; return value === undefined || value === null ? null : String(value); } return browserGet(fallbackStorage, normalized); }, set(key, value) { const normalized = String(key || ''); if (!normalized) return; const serialized = String(value); if (serverBacked) { serverState[normalized] = serialized; pendingUpdates[normalized] = serialized; pendingRemovals.delete(normalized); scheduleSave(); return; } browserSet(fallbackStorage, normalized, serialized); }, remove(key) { const normalized = String(key || ''); if (!normalized) return; if (serverBacked) { delete serverState[normalized]; delete pendingUpdates[normalized]; pendingRemovals.add(normalized); scheduleSave(); return; } browserRemove(fallbackStorage, normalized); }, flush: flushPending, }; window.AssetBrowserState = api; async function loadUserState() { try { const response = await fetch('/api/user-ui-state', {credentials: 'same-origin', cache: 'no-store'}); if (!response.ok) return; const payload = await response.json(); if (!payload?.authenticated) return; serverBacked = true; api.serverBacked = true; serverState = payload.state && typeof payload.state === 'object' && !Array.isArray(payload.state) ? {...payload.state} : {}; // One-time/non-destructive migration: existing values from this browser are // copied only when the account does not already have a value for the key. const browserState = collectBrowserState(); const migration = {}; Object.entries(browserState).forEach(([key, value]) => { if (!Object.prototype.hasOwnProperty.call(serverState, key)) { serverState[key] = value; migration[key] = value; } }); if (Object.keys(migration).length) { try { await postState(migration, []); } catch (error) { console.warn('[AssetManager] Vorhandener Browserzustand konnte nicht in das Benutzerkonto übernommen werden.', error); } } } catch (error) { // Browser storage remains the safe fallback if the account-state endpoint // is temporarily unavailable. console.warn('[AssetManager] Benutzerbezogener Tabellenzustand nicht verfügbar; Browser-Fallback wird verwendet.', error); } } api.ready = loadUserState(); function tableKey(table, index) { return table.dataset.storageKey || table.id || `table-${location.pathname}-${index}`; } function filterControls(table) { return [...table.querySelectorAll('thead input, thead select, thead textarea')].filter(control => { const type = (control.type || '').toLowerCase(); return !['checkbox', 'radio', 'button', 'submit'].includes(type); }); } function controlKey(control, index) { const cell = control.closest('th'); return control.name || control.dataset.field || cell?.dataset.field || `filter-${index}`; } function restoreSort(table, index, force = false) { if (!force && table.dataset.browserSortRestored === '1') return; if (typeof table.assetApplySort !== 'function') return; const key = `${tableKey(table, index)}:sort`; let state = null; try { state = JSON.parse(api.get(key) || 'null'); } catch (_) {} if (state?.field && (state.direction === 'asc' || state.direction === 'desc')) { table.assetApplySort(state.field, state.direction, {silent: true}); } else if (table.dataset.defaultSortField) { table.assetApplySort( table.dataset.defaultSortField, table.dataset.defaultSortDirection === 'desc' ? 'desc' : 'asc', {silent: true} ); } else { table.assetUpdateRowNumbers?.(); } table.dataset.browserSortRestored = '1'; } function restoreTable(table, index) { if (table.dataset.browserStateReady === '1') return; const restoreStarted = performance.now(); const controls = filterControls(table); if (!controls.length) { table.dataset.browserStateReady = '1'; restoreSort(table, index); return; } table.dataset.browserStateReady = '1'; const key = `${tableKey(table, index)}:filters`; let values = {}; try { values = JSON.parse(api.get(key) || '{}') || {}; } catch (_) {} let serverValuesChanged = false; controls.forEach((control, controlIndex) => { const field = controlKey(control, controlIndex); if (Object.prototype.hasOwnProperty.call(values, field)) { if (control.value !== values[field]) serverValuesChanged = true; control.value = values[field]; } const save = () => { const current = {}; filterControls(table).forEach((item, itemIndex) => { current[controlKey(item, itemIndex)] = item.value; }); api.set(key, JSON.stringify(current)); }; control.addEventListener('input', save); control.addEventListener('change', save); }); if (typeof table.assetApplyFilters === 'function') table.assetApplyFilters(); if (table.dataset.serverFilters === '1' && serverValuesChanged && typeof table.assetSubmitServerFilters === 'function') { table.assetSubmitServerFilters(true); } restoreSort(table, index); table.dataset.browserStateRestored = '1'; window.dispatchEvent(new CustomEvent('asset-table-state-ready', { detail: { table } })); const duration = performance.now() - restoreStarted; const logger = duration >= 50 ? console.warn : console.debug; logger('[AssetManager][Performance]', 'browser-state:restoreTable', { dauer_ms: Math.round(duration * 10) / 10, filter: controls.length, gespeicherte_werte: Object.keys(values).length }); } async function initializeTables() { await api.ready; document.querySelectorAll('table').forEach(restoreTable); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeTables, { once: true }); } else { initializeTables(); } window.addEventListener('asset-table-structure-ready', initializeTables); window.addEventListener('asset-table-filters-cleared', event => { const table = event.detail?.table; if (!(table instanceof HTMLTableElement)) return; const tables = [...document.querySelectorAll('table')]; const index = tables.indexOf(table); api.remove(`${tableKey(table, index >= 0 ? index : 0)}:filters`); }); window.addEventListener('asset-table-sort-changed', event => { const table = event.detail?.table; if (!(table instanceof HTMLTableElement)) return; const tables = [...document.querySelectorAll('table')]; const index = tables.indexOf(table); api.set(`${tableKey(table, index >= 0 ? index : 0)}:sort`, JSON.stringify({ field: event.detail?.field || '', direction: event.detail?.direction || 'asc' })); }); window.addEventListener('asset-table-content-reloaded', event => { const table = event.detail?.table; if (!(table instanceof HTMLTableElement)) return; const tables = [...document.querySelectorAll('table')]; const index = tables.indexOf(table); restoreSort(table, index >= 0 ? index : 0, true); }); window.addEventListener('pagehide', () => { flushPending(); }); perf?.end('browser-state:gesamtes Modul'); })();