WME Easy Storage Manager

Easy Storage Manager is a handy script that allows you to easily export and import local storage data for WME.

// ==UserScript==
// @name         WME Easy Storage Manager
// @namespace    https://greasyfork.org/de/users/863740-horst-wittlich
// @author       Hiwi234, DevlinDelFuego
// @version      2025.10.12
// @description  Easy Storage Manager is a handy script that allows you to easily export and import local storage data for WME.
// @match        *://*.waze.com/*editor*
// @exclude      *://*.waze.com/user/editor*
// @grant        GM_xmlhttpRequest
// @connect      api.dropboxapi.com
// @connect      content.dropboxapi.com
// @require      https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @license      MIT
// @run-at      document-start
// ==/UserScript==

(function () {
    'use strict';

  const ESM_DIAG = {
    log: (...args) => console.log('[ESM]', ...args),
    warn: (...args) => console.warn('[ESM]', ...args),
    error: (...args) => console.error('[ESM]', ...args)
  };
  window.addEventListener('error', (e) => {
    try { sessionStorage.setItem('ESM_DIAG_LAST_ERROR', `${e.message} at ${e.filename}:${e.lineno}`); } catch (err) {}
    ESM_DIAG.error('Unhandled error:', e.message);
  });
  window.addEventListener('unhandledrejection', (e) => {
    try { sessionStorage.setItem('ESM_DIAG_LAST_REJECTION', String(e.reason)); } catch (err) {}
    ESM_DIAG.error('Unhandled rejection:', e.reason);
  });
  // Compact UI styles to avoid blowing up the layout
  (function injectCompactStyles(){
    const css = `
      /* Compact buttons across all ESM variants */
      #esm-import, #esm-export,
      #esm-cloud-backup, #esm-cloud-restore,
      #esm-import-btn, #esm-export-btn,
      #esm-drive-backup-btn, #esm-drive-restore-btn {
        padding: 6px 10px !important;
        font-size: 12px !important;
        width: 100% !important;
        flex: 1 1 0 !important;
        min-width: 0 !important;
        white-space: nowrap !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
        box-sizing: border-box !important;
        margin: 0 !important;
      }
      /* Row containers: use CSS Grid for perfect equality with 4 equal columns */
      #esm-tab > div, #easy-storage-manager-tab > div {
        display: grid !important;
        width: 100% !important;
        grid-template-columns: 1fr 1fr 1fr 1fr !important;
        gap: 8px !important;
        align-items: stretch !important;
        box-sizing: border-box !important;
      }
      /* Fallback panel button container - also 4 equal columns */
      #esm-fallback-panel .btnRow,
      #esm-fallback-panel > div[style*="display: flex"] {
        display: grid !important;
        width: 100% !important;
        grid-template-columns: 1fr 1fr 1fr 1fr !important;
        gap: 8px !important;
        align-items: stretch !important;
        box-sizing: border-box !important;
        margin: 0 !important;
      }
      @media (max-width: 768px) {
        #esm-tab > div, #easy-storage-manager-tab > div,
        #esm-fallback-panel .btnRow,
        #esm-fallback-panel > div[style*="display: flex"] {
          grid-template-columns: 1fr 1fr !important;
        }
      }
      @media (max-width: 480px) {
        #esm-tab > div, #easy-storage-manager-tab > div,
        #esm-fallback-panel .btnRow,
        #esm-fallback-panel > div[style*="display: flex"] {
          grid-template-columns: 1fr !important;
        }
      }
    `;
    try {
      const style = document.createElement('style');
      style.id = 'esm-compact-styles';
      style.textContent = css;
      if (document.head) {
        document.head.appendChild(style);
      } else {
        document.addEventListener('DOMContentLoaded', function(){
          try { document.head.appendChild(style); } catch (_) {}
        });
      }
    } catch (_) {}
  })();

    let importedData; // Imported JSON data
    let applyButton; // Apply button element
    let scriptVersion = (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) ? GM_info.script.version : 'dev-local';
    const updateMessage = "<b>Changelog</b><br><br> - Full backup export/import now includes localStorage, sessionStorage, cookies, and IndexedDB. <br> - You can select which items to restore across all storage types and DB records. <br> - The page will refresh after importing to apply changes. <br> - Added Dropbox cloud backup and restore functionality. <br><br>";
    const REAPPLY_STASH_KEY = 'ESM_POST_RELOAD';
    // Sprachunterstützung (DE/EN) für UI-Texte
    const ESM_LANG = ((navigator.language || 'en').toLowerCase().startsWith('de')) ? 'de' : 'en';
    const ESM_I18N = {
      de: {
        panelTitle: 'Cloud-Backup (Dropbox) – Anleitung',
        show: 'Anzeigen',
        hide: 'Ausblenden',
        howTo: 'So aktivierst du die Online-Cloud:',
        step1: 'Bei Dropbox anmelden.',
        step2: 'App erstellen (kostenlos):',
        step3: 'In deiner App einen Generated access token erzeugen.',
        step4: 'Token unten eingeben und speichern.',
        genTokenLabel: 'Generated access token:',
        saveTokenBtn: 'Token speichern',
        clearTokenBtn: 'Abmelden',
        statusEnterToken: 'Bitte Token eingeben.',
        statusSavedValidated: 'Token gespeichert und validiert. Dropbox ist bereit.',
        statusSignedOut: 'Abgemeldet. Bitte neuen Token eingeben.',
        cloudBackup: '☁️ Cloud Sichern',
        cloudRestore: '☁️ Wiederherstellen',
        cloudBackupTitle: '☁️ Backup in Dropbox sichern',
        cloudRestoreTitle: '☁️ Aus Dropbox wiederherstellen'
      },
      en: {
        panelTitle: 'Cloud Backup (Dropbox) – Guide',
        show: 'Show',
        hide: 'Hide',
        howTo: 'How to enable cloud backup:',
        step1: 'Sign in to Dropbox.',
        step2: 'Create an app (free):',
        step3: 'Generate a personal access token in your app.',
        step4: 'Enter the token below and save it.',
        genTokenLabel: 'Generated access token:',
        saveTokenBtn: 'Save Token',
        clearTokenBtn: 'Sign out',
        statusEnterToken: 'Please enter a token.',
        statusSavedValidated: 'Token saved and validated. Dropbox is ready.',
        statusSignedOut: 'Signed out. Please enter a new token.',
        cloudBackup: '☁️ Cloud Backup',
        cloudRestore: '☁️ Restore',
        cloudBackupTitle: '☁️ Save backup to Dropbox',
        cloudRestoreTitle: '☁️ Restore from Dropbox'
      }
    };
    function t(key) {
      const langVal = (ESM_I18N[ESM_LANG] && ESM_I18N[ESM_LANG][key]) || null;
      const enVal = (ESM_I18N.en && ESM_I18N.en[key]) || null;
      const deVal = (ESM_I18N.de && ESM_I18N.de[key]) || null;
      return langVal || enVal || deVal || key;
    }
    // Extend language maps with additional UI texts for Import/Export & Drive
    try {
      Object.assign(ESM_I18N.de, {
        importExportDesc: 'Importiere eine Backup-JSON-Datei oder exportiere ein vollständiges Backup (localStorage, sessionStorage, Cookies, IndexedDB).',
        importBackup: '♻️ Wiederherstellen',
        exportBackup: '💾 Lokal Speichern',
        importBackupTitle: '♻️ Backup importieren',
        exportBackupTitle: '💾 Backup exportieren',
        driveBackupTitle: '☁️ Backup in Dropbox sichern',
        driveRestoreTitle: '☁️ Aus Dropbox wiederherstellen',
        importFunctionUnavailable: 'Import-Funktion nicht verfügbar',
        exportFunctionUnavailable: 'Export-Funktion nicht verfügbar',
        dropboxExportUnavailable: 'Dropbox Backup-Funktion nicht verfügbar',
        dropboxImportUnavailable: 'Dropbox Wiederherstellungs-Funktion nicht verfügbar',
        driveExportUnavailable: 'Dropbox Backup-Funktion nicht verfügbar',
        driveImportUnavailable: 'Dropbox Wiederherstellungs-Funktion nicht verfügbar',
        selectAll: 'Alle auswählen',
        deselectAll: 'Auswahl aufheben',
        apply: 'Anwenden',
        scriptTitle: 'Easy Storage Manager',
        fallbackDesc: 'Fallback-Panel aktiv. Importiere/Exportiere Backups und wähle Schlüssel zur Wiederherstellung.'
      });
      Object.assign(ESM_I18N.en, {
        importExportDesc: 'Import a backup JSON file or export a full backup (localStorage, sessionStorage, cookies, IndexedDB).',
        importBackup: '♻️ Restore',
        exportBackup: '💾 Save Local',
        importBackupTitle: '♻️ Import Backup',
        exportBackupTitle: '💾 Export Backup',
        driveBackupTitle: '☁️ Save backup to Dropbox',
        driveRestoreTitle: '☁️ Restore from Dropbox',
        importFunctionUnavailable: 'Import function not available',
        exportFunctionUnavailable: 'Export function not available',
        dropboxExportUnavailable: 'Dropbox export function not available',
        dropboxImportUnavailable: 'Dropbox restore function not available',
        driveExportUnavailable: 'Dropbox export function not available',
        driveImportUnavailable: 'Dropbox import function not available',
        selectAll: 'Select All',
        deselectAll: 'Deselect All',
        apply: 'Apply',
        scriptTitle: 'Easy Storage Manager',
        fallbackDesc: 'Fallback panel active. Import/export backups and choose keys to restore.'
      });
    } catch (_) {}
    // Zusätzliche Texte für vollständige UI-Übersetzung (Alerts/Labels)
    try {
      Object.assign(ESM_I18N.de, {
        dropboxSaveSuccessPrefix: '✅ Backup erfolgreich in Dropbox gespeichert!',
        dropboxSaveFailedPrefix: '❌ Dropbox Backup fehlgeschlagen:',
        dropboxNoBackups: '❌ Keine Backup-Dateien in Dropbox gefunden.',
        invalidSelection: '❌ Ungültige Auswahl.',
        foreignBackupHint: 'Hinweis: Das Backup stammt von einer anderen Quelle. Aus Sicherheitsgründen werden Cookies und Session Storage standardmäßig nicht importiert.',
        dropboxLoadSuccessPrefix: '✅ Dropbox Backup erfolgreich geladen!',
        foundEntriesLabel: 'Gefundene Einträge:',
        invalidJson: '❌ Backup-Datei konnte nicht gelesen werden: Ungültiges JSON-Format.',
        dropboxRestoreFailedPrefix: '❌ Dropbox Wiederherstellung fehlgeschlagen:',
        fileReadSuccess: 'Datei erfolgreich gelesen',
        fileReadError: 'Fehler beim Lesen der Datei. Bitte erneut versuchen.',
        noKeysSelected: 'Keine Schlüssel ausgewählt. Nichts zu importieren.',
        fileLabel: 'Datei:',
        pathLabel: 'Pfad:',
        sizeLabel: 'Größe:',
        kb: 'KB'
      });
      Object.assign(ESM_I18N.en, {
        dropboxSaveSuccessPrefix: '✅ Backup saved to Dropbox successfully!',
        dropboxSaveFailedPrefix: '❌ Dropbox backup failed:',
        dropboxNoBackups: '❌ No backup files found in Dropbox.',
        invalidSelection: '❌ Invalid selection.',
        foreignBackupHint: 'Note: The backup originates from a different source. For security reasons, cookies and session storage are not imported by default.',
        dropboxLoadSuccessPrefix: '✅ Dropbox backup loaded successfully!',
        foundEntriesLabel: 'Found entries:',
        invalidJson: '❌ Could not read backup file: Invalid JSON format.',
        dropboxRestoreFailedPrefix: '❌ Dropbox restore failed:',
        fileReadSuccess: 'File read successfully',
        fileReadError: 'Error occurred while reading the file. Please try again.',
        noKeysSelected: 'No keys selected. Nothing to import.',
        fileLabel: 'File:',
        pathLabel: 'Path:',
        sizeLabel: 'Size:',
        kb: 'KB'
      });
    } catch (_) {}
    try {
      Object.assign(ESM_I18N.de, {
        restorePrompt: 'Welche Datei möchten Sie wiederherstellen?'
      });
      Object.assign(ESM_I18N.en, {
        restorePrompt: 'Which file would you like to restore?'
      });
    } catch (_) {}

    // Dropbox API Configuration
    const DROPBOX_CONFIG = {
      APP_KEY: '9fxl4soww5di6qt',
      APP_SECRET: '46inexcd3evrqik',
      ACCESS_TOKEN: null,
      REDIRECT_URI: 'http://localhost:8080/esm_dropbox_oauth.html',
      // Use same-origin Waze Dropbox endpoint to satisfy CSP
      PROXY_BASE_URL: '/dropbox',
      API_BASE_URL: 'https://api.dropboxapi.com/2',
      CONTENT_API_URL: 'https://content.dropboxapi.com/2'
    };

    let dropboxAuth = null;
    const DROPBOX_TOKEN_KEY = 'ESM_DROPBOX_TOKEN';
    const DROPBOX_ACCOUNT_CACHE_KEY = 'ESM_DROPBOX_ACCOUNT';
    const DROPBOX_REFRESH_TOKEN_KEY = 'ESM_DROPBOX_REFRESH_TOKEN';
    const DROPBOX_TOKEN_EXPIRES_KEY = 'ESM_DROPBOX_TOKEN_EXPIRES';
const DROPBOX_REDIRECT_URI_KEY = 'ESM_DROPBOX_REDIRECT_URI';
// Hook alerts to display bilingual messages for known keys
(function(){
  const origAlert = window.alert;
  const map = {
    'Import function not available': t('importFunctionUnavailable'),
    'Export function not available': t('exportFunctionUnavailable'),
    'Dropbox Backup-Funktion nicht verfügbar': t('dropboxExportUnavailable'),
    'Dropbox Wiederherstellungs-Funktion nicht verfügbar': t('dropboxImportUnavailable'),
  };
  window.alert = function(msg){
    const key = String(msg);
    const repl = map[key] || key;
    return origAlert(repl);
  };
})();

    function getRedirectUri() {
      const saved = localStorage.getItem(DROPBOX_REDIRECT_URI_KEY);
      if (saved && typeof saved === 'string' && saved.trim()) return saved.trim();
      return DROPBOX_CONFIG.REDIRECT_URI;
    }

    function setDropboxToken(token) {
      try {
        localStorage.setItem(DROPBOX_TOKEN_KEY, token);
        dropboxAuth = token;
        ESM_DIAG.log('Dropbox token gespeichert.');
      } catch (e) {
        ESM_DIAG.warn('Konnte Dropbox-Token nicht speichern:', e);
      }
    }

    function clearDropboxToken() {
      try {
        localStorage.removeItem(DROPBOX_TOKEN_KEY);
        localStorage.removeItem(DROPBOX_REFRESH_TOKEN_KEY);
        localStorage.removeItem(DROPBOX_TOKEN_EXPIRES_KEY);
        dropboxAuth = null;
        sessionStorage.removeItem(DROPBOX_ACCOUNT_CACHE_KEY);
        ESM_DIAG.log('Dropbox-Token gelöscht.');
      } catch (e) {
        ESM_DIAG.warn('Konnte Dropbox-Token nicht löschen:', e);
      }
    }

    async function promptForDropboxToken() {
      const hint = 'Bitte persönliches Dropbox Access Token eingeben (Bearer Token).\n' +
        'Anleitung: Öffne https://www.dropbox.com/developers/apps, wähle deine App,\n' +
        'erzeuge ein Access Token und füge es hier ein.';
      const token = prompt(hint);
      if (!token) throw new Error('Kein Dropbox-Token eingegeben');
      setDropboxToken(token.trim());
      // Validierung
      await getDropboxAccount(token.trim());
      return token.trim();
    }

    async function getDropboxAccount(accessToken) {
      // Cache lesen
      try {
        const cached = sessionStorage.getItem(DROPBOX_ACCOUNT_CACHE_KEY);
        if (cached) return JSON.parse(cached);
      } catch (_) {}
      if (typeof GM_xmlhttpRequest !== 'function') {
        // Ohne GM können wir den Account nicht bequem validieren; Return minimal
        return { account_id: 'unknown', email: 'unknown' };
      }
      const res = await gmFetch(`${DROPBOX_CONFIG.API_BASE_URL}/users/get_current_account`, {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${accessToken}` },
        // Body muss leer sein
      });
      if (res.status < 200 || res.status >= 300) {
        throw new Error(`Dropbox-Token ungültig (${res.status})`);
      }
      let info;
      try { info = JSON.parse(res.responseText); } catch (_) { info = { account_id: 'unknown', email: 'unknown' }; }
      try { sessionStorage.setItem(DROPBOX_ACCOUNT_CACHE_KEY, JSON.stringify(info)); } catch (_) {}
      return info;
    }

    // Helfer global verfügbar machen
    try {
      window.ESM_setDropboxToken = setDropboxToken;
      window.ESM_clearDropboxToken = clearDropboxToken;
      window.ESM_promptDropboxToken = promptForDropboxToken;
      window.ESM_setDropboxRedirectUri = function (url) {
        try {
          if (typeof url !== 'string' || !url.trim()) throw new Error('Ungültige URL');
          const u = url.trim();
          localStorage.setItem(DROPBOX_REDIRECT_URI_KEY, u);
          DROPBOX_CONFIG.REDIRECT_URI = u; // live update
          ESM_DIAG.log('Dropbox Redirect-URI gesetzt:', u);
        } catch (e) {
          ESM_DIAG.error('Konnte Redirect-URI nicht setzen:', e);
        }
      };
      window.ESM_getDropboxRedirectUri = function () { return getRedirectUri(); };
    } catch (_) {}

    // ===== OAuth 2.0 (PKCE) Hilfsfunktionen =====
    function base64urlFromBytes(bytes) {
      let bin = '';
      for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
      return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
    }
    function generateCodeVerifier() {
      const bytes = new Uint8Array(64);
      crypto.getRandomValues(bytes);
      return base64urlFromBytes(bytes);
    }
    async function sha256Base64Url(input) {
      const data = new TextEncoder().encode(input);
      const hash = await crypto.subtle.digest('SHA-256', data);
      return base64urlFromBytes(new Uint8Array(hash));
    }
    function saveTokenResponse(token) {
      try {
        if (token.access_token) localStorage.setItem(DROPBOX_TOKEN_KEY, token.access_token);
        if (token.refresh_token) localStorage.setItem(DROPBOX_REFRESH_TOKEN_KEY, token.refresh_token);
        const expiresAt = token.expires_in ? (Date.now() + (token.expires_in * 1000)) : 0;
        localStorage.setItem(DROPBOX_TOKEN_EXPIRES_KEY, String(expiresAt));
        dropboxAuth = token.access_token || null;
      } catch (e) {
        ESM_DIAG.warn('Konnte Token-Antwort nicht speichern:', e);
      }
    }
    async function exchangeCodeForToken(code, verifier) {
      const body = new URLSearchParams({
        code,
        grant_type: 'authorization_code',
        client_id: DROPBOX_CONFIG.APP_KEY,
        code_verifier: verifier,
        redirect_uri: getRedirectUri()
      }).toString();
      const res = await gmFetch(`${DROPBOX_CONFIG.API_BASE_URL.replace('/2','')}/oauth2/token`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body
      });
      if (res.status < 200 || res.status >= 300) {
        throw new Error(`Token-Austausch fehlgeschlagen (${res.status}): ${res.responseText || ''}`);
      }
      let json; try { json = JSON.parse(res.responseText); } catch (_) { json = {}; }
      saveTokenResponse(json);
      return json;
    }
    async function refreshAccessToken() {
      const refreshToken = localStorage.getItem(DROPBOX_REFRESH_TOKEN_KEY);
      if (!refreshToken) throw new Error('Kein Refresh-Token vorhanden');
      const body = new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: DROPBOX_CONFIG.APP_KEY
      }).toString();
      const res = await gmFetch(`${DROPBOX_CONFIG.API_BASE_URL.replace('/2','')}/oauth2/token`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body
      });
      if (res.status < 200 || res.status >= 300) {
        throw new Error(`Token-Refresh fehlgeschlagen (${res.status}): ${res.responseText || ''}`);
      }
      let json; try { json = JSON.parse(res.responseText); } catch (_) { json = {}; }
      saveTokenResponse(json);
      return json;
    }
    async function ensureDropboxAccessToken() {
      const token = localStorage.getItem(DROPBOX_TOKEN_KEY);
      const exp = parseInt(localStorage.getItem(DROPBOX_TOKEN_EXPIRES_KEY) || '0', 10);
      if (token && exp && (Date.now() < (exp - 60_000))) {
        dropboxAuth = token;
        return token;
      }
      if (localStorage.getItem(DROPBOX_REFRESH_TOKEN_KEY)) {
        const refreshed = await refreshAccessToken();
        return refreshed.access_token;
      }
      return null;
    }
    async function oauthDropboxPKCE() {
      const verifier = generateCodeVerifier();
      const challenge = await sha256Base64Url(verifier);
      const state = Math.random().toString(36).slice(2);
      try { sessionStorage.setItem('ESM_OAUTH_VERIFIER', verifier); sessionStorage.setItem('ESM_OAUTH_STATE', state); } catch (_) {}
      const redirectUri = getRedirectUri();
      // Warnung ausgeben, falls die Redirect-URI offensichtlich nicht produktionsfähig ist
      try {
        const u = new URL(redirectUri);
        if (u.protocol !== 'https:' && u.hostname !== 'localhost') {
          ESM_DIAG.warn('Redirect-URI ist nicht HTTPS. Dropbox verlangt im Live-Betrieb HTTPS.');
        }
      } catch (e) {
        ESM_DIAG.warn('Ungültige Redirect-URI-Konfiguration:', redirectUri);
      }
      const params = new URLSearchParams({
        response_type: 'code',
        client_id: DROPBOX_CONFIG.APP_KEY,
        redirect_uri: redirectUri,
        code_challenge: challenge,
        code_challenge_method: 'S256',
        token_access_type: 'offline',
        state
      }).toString();
      const authUrl = `https://www.dropbox.com/oauth2/authorize?${params}`;
      const w = window.open(authUrl, 'esm_dropbox_oauth', 'width=600,height=700');
      if (!w) throw new Error('Konnte OAuth-Fenster nicht öffnen');
      return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          window.removeEventListener('message', onMsg);
          reject(new Error('OAuth Zeitüberschreitung'));
        }, 180_000);
        function onMsg(e) {
          try {
            const data = e.data || {};
            if (data && data.type === 'ESM_DROPBOX_OAUTH_CODE') {
              window.removeEventListener('message', onMsg);
              clearTimeout(timer);
              const ver = sessionStorage.getItem('ESM_OAUTH_VERIFIER') || verifier;
              const st = sessionStorage.getItem('ESM_OAUTH_STATE');
              if (st && data.state && st !== data.state) {
                reject(new Error('Ungültiger OAuth state'));
                return;
              }
              exchangeCodeForToken(data.code, ver).then(resolve).catch(reject);
            }
          } catch (err) {
            // ignore
          }
        }
        window.addEventListener('message', onMsg);
      });
    }
    try { window.ESM_startDropboxOAuth = oauthDropboxPKCE; } catch (_) {}

    // Wrapper around GM_xmlhttpRequest to bypass page CSP when available
    function gmFetch(url, opts = {}) {
      const method = opts.method || 'GET';
      const headers = opts.headers || {};
      // Only include a request body if the caller provided one.
      // This avoids sending the literal string "null" to endpoints (e.g. Dropbox files/download)
      // that require an entirely empty request body.
      const hasBody = Object.prototype.hasOwnProperty.call(opts, 'body');
      let body = null;
      if (hasBody) {
        if (typeof opts.body === 'string') body = opts.body;
        else if (opts.body != null) body = JSON.stringify(opts.body);
      }
      const responseType = opts.responseType || 'text';
      return new Promise((resolve, reject) => {
        if (typeof GM_xmlhttpRequest !== 'function') {
          reject(new Error('GM_xmlhttpRequest not available'));
          return;
        }
        try {
          const req = {
            url,
            method,
            headers,
            responseType,
            onload: (res) => resolve(res),
            onerror: (err) => reject(new Error(err && err.error ? err.error : 'GM request failed')),
            ontimeout: () => reject(new Error('GM request timeout'))
          };
          if (body != null) {
            req.data = body;
          }
          GM_xmlhttpRequest(req);
        } catch (e) {
          reject(e);
        }
      });
    }

    // Get safe, preferably native storage methods from a fresh iframe context (in case other scripts patch the prototype)
    function captureNativeStorage() {
      try {
        const iframe = document.createElement('iframe');
        iframe.style.display = 'none';
        iframe.src = 'about:blank';
        document.documentElement.appendChild(iframe);
        const win = iframe.contentWindow;
        const methods = {
          getItem: win.Storage.prototype.getItem,
          setItem: win.Storage.prototype.setItem,
          removeItem: win.Storage.prototype.removeItem,
          key: win.Storage.prototype.key
        };
        iframe.parentNode.removeChild(iframe);
        return methods;
      } catch (e) {
        // Fallback to current (possibly patched) prototype
        return {
          getItem: Storage.prototype.getItem,
          setItem: Storage.prototype.setItem,
          removeItem: Storage.prototype.removeItem,
          key: Storage.prototype.key
        };
      }
    }
    const NativeStorage = captureNativeStorage();

    // Early recovery attempt immediately after script start,
    // so that protected keys are activated before other user scripts, if possible
    try { reapplyAfterReload(); } catch (_) { /* ignore */ }

    // ===== Einfache Anleitung & Token-Input (UI) für Dropbox Cloud =====
    // parent: optional Container; wenn gesetzt, wird das Panel inline im Skripte-Tab gerendert
    function injectDropboxHelpPanel(parent) {
      try {
        if (document.getElementById('esm-dropbox-help-panel')) return;
        const inline = !!parent;
        const hiddenKey = inline ? 'ESM_DROPBOX_HELP_TAB_HIDDEN' : 'ESM_DROPBOX_HELP_HIDDEN';
        const hidden = localStorage.getItem(hiddenKey) === '1';
        const wrapper = document.createElement('div');
        wrapper.id = 'esm-dropbox-help-panel';
        wrapper.style.fontFamily = "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif";
        wrapper.style.color = '#111827';
        if (!inline) {
          wrapper.style.position = 'fixed';
          wrapper.style.bottom = '16px';
          wrapper.style.right = '16px';
          wrapper.style.zIndex = '2147483646';
        } else {
          wrapper.style.marginTop = '10px';
          wrapper.style.width = '100%';
        }

        const card = document.createElement('div');
        card.style.background = '#ffffff';
        card.style.border = '1px solid #e5e7eb';
        card.style.borderRadius = '12px';
        card.style.overflow = 'hidden';
        if (!inline) {
          card.style.boxShadow = '0 6px 18px rgba(0,0,0,0.12)';
          card.style.width = '340px';
          card.style.maxWidth = '90vw';
        } else {
          card.style.boxShadow = 'none';
          card.style.width = '100%';
          card.style.maxWidth = '100%';
        }

        const header = document.createElement('div');
        header.style.display = 'flex';
        header.style.alignItems = 'center';
        header.style.justifyContent = 'space-between';
        header.style.padding = '10px 12px';
        header.style.background = '#f9fafb';
        header.style.borderBottom = '1px solid #e5e7eb';
        const title = document.createElement('div');
        title.textContent = t('panelTitle');
        title.style.fontSize = '13px';
        title.style.fontWeight = '600';
        const controls = document.createElement('div');
        const btnHide = document.createElement('button');
        btnHide.textContent = hidden ? t('show') : t('hide');
        btnHide.style.fontSize = '12px';
        btnHide.style.border = '1px solid #d1d5db';
        btnHide.style.background = '#ffffff';
        btnHide.style.borderRadius = '8px';
        btnHide.style.padding = '4px 8px';
        btnHide.style.cursor = 'pointer';
        btnHide.addEventListener('click', () => {
          const contentVisible = content.style.display !== 'none';
          content.style.display = contentVisible ? 'none' : 'block';
          btnHide.textContent = contentVisible ? t('show') : t('hide');
          try { localStorage.setItem(hiddenKey, contentVisible ? '1' : '0'); } catch (_) {}
        });
        controls.appendChild(btnHide);
        header.appendChild(title);
        header.appendChild(controls);

        const content = document.createElement('div');
        content.style.padding = '12px';
        content.style.display = hidden ? 'none' : 'block';
        content.innerHTML = `
          <div style="font-size:12px; line-height:1.5;">
            <p style="margin:0 0 8px"><b>${t('howTo')}</b></p>
            <ol style="margin:0 0 10px 18px; padding:0;">
              <li>${t('step1')}</li>
              <li>${t('step2')} <a href="https://www.dropbox.com/developers/apps" target="_blank" rel="noopener">https://www.dropbox.com/developers/apps</a></li>
              <li>${t('step3')}</li>
              <li>${t('step4')}</li>
            </ol>
            <div style="margin:8px 0 4px">${t('genTokenLabel')}</div>
            <input id="esm-dropbox-token-input" type="text" placeholder="Dropbox Access Token" style="width:100%; box-sizing:border-box; font-size:12px; padding:6px 8px; border:1px solid #d1d5db; border-radius:8px;" />
            <div style="display:flex; gap:8px; margin-top:8px;">
              <button id="esm-dropbox-token-save" style="flex:1; font-size:12px; border:1px solid #10b981; background:#10b981; color:#fff; border-radius:8px; padding:6px 8px; cursor:pointer;">${t('saveTokenBtn')}</button>
              <button id="esm-dropbox-token-clear" style="flex:1; font-size:12px; border:1px solid #ef4444; background:#ef4444; color:#fff; border-radius:8px; padding:6px 8px; cursor:pointer;">${t('clearTokenBtn')}</button>
            </div>
            <div id="esm-dropbox-token-status" style="margin-top:8px; font-size:12px; color:#374151;"></div>
          </div>
        `;

        card.appendChild(header);
        card.appendChild(content);
        wrapper.appendChild(card);
        if (inline && parent) {
          parent.appendChild(wrapper);
        } else {
          document.documentElement.appendChild(wrapper);
        }

        // Prefill input with stored token if available
        const input = content.querySelector('#esm-dropbox-token-input');
        const saveBtn = content.querySelector('#esm-dropbox-token-save');
        const clearBtn = content.querySelector('#esm-dropbox-token-clear');
        const statusEl = content.querySelector('#esm-dropbox-token-status');
        try {
          const stored = localStorage.getItem(DROPBOX_TOKEN_KEY);
          if (stored) input.value = stored;
        } catch (_) {}

        async function setStatus(msg, type) {
          statusEl.textContent = msg || '';
          statusEl.style.color = type === 'error' ? '#b91c1c' : (type === 'success' ? '#065f46' : '#374151');
        }

        saveBtn.addEventListener('click', async () => {
          const token = String(input.value || '').trim();
          if (!token) { setStatus(t('statusEnterToken'), 'error'); return; }
          try {
            setDropboxToken(token);
            // optional: validate account
            await getDropboxAccount(token);
            setStatus(t('statusSavedValidated'), 'success');
          } catch (e) {
            setStatus(`Fehler beim Speichern/Validieren: ${e && e.message ? e.message : e}`, 'error');
          }
        });

        clearBtn.addEventListener('click', async () => {
          try {
            clearDropboxToken();
            input.value = '';
            setStatus(t('statusSignedOut'), '');
          } catch (e) {
            setStatus(`Fehler beim Abmelden: ${e && e.message ? e.message : e}`, 'error');
          }
        });
      } catch (e) {
        ESM_DIAG.warn('Konnte Dropbox-Hilfspanel nicht einfügen:', e);
      }
    }
    // Hinweis: Die Hilfe wird jetzt im Skripte-Tab inline eingefügt (siehe addScriptTab)

    // Authenticate with Dropbox (ohne OAuth, nur gespeicherter Access-Token)
    async function authenticateDropbox() {
      try {
        ESM_DIAG.log('Dropbox-Authentifizierung (nur Token) starten...');
        // Direkt gespeicherten Token verwenden
        const token = (dropboxAuth && String(dropboxAuth).trim()) || localStorage.getItem(DROPBOX_TOKEN_KEY);
        if (token && String(token).trim()) {
          try { await getDropboxAccount(token); } catch (_) {}
          dropboxAuth = String(token).trim();
          return dropboxAuth;
        }
        // Kein Token vorhanden -> Nutzer um persönlichen Access-Token bitten
        const manual = await promptForDropboxToken();
        dropboxAuth = manual;
        return manual;
      } catch (error) {
        ESM_DIAG.error('Dropbox-Authentifizierung (Token) fehlgeschlagen:', error);
        throw error;
      }
    }

    // Attempt to attach CSRF headers expected by waze.com endpoints
    function getCsrfHeaders() {
      try {
        const cookies = document.cookie.split(';').map(s => s.trim()).reduce((acc, cur) => {
          const idx = cur.indexOf('=');
          if (idx > -1) acc[cur.slice(0, idx)] = decodeURIComponent(cur.slice(idx + 1));
          return acc;
        }, {});
        const token = cookies['XSRF-TOKEN'] || cookies['xsrf-token'] || cookies['_csrf'] || cookies['csrf'] || cookies['csrf_token'] || cookies['X-CSRF-TOKEN'];
        if (!token) {
          ESM_DIAG.warn('No CSRF token cookie found for waze.com Dropbox endpoint');
          return {};
        }
        const headers = {
          'X-XSRF-TOKEN': token,
          'X-CSRF-TOKEN': token,
          'X-CSRF-Token': token
        };
        ESM_DIAG.log('Attached CSRF headers for waze.com Dropbox endpoint');
        return headers;
      } catch (e) {
        ESM_DIAG.warn('Failed to build CSRF headers:', e);
        return {};
      }
    }


    // Export backup to Dropbox
    async function exportToDropbox() {
      try {
        ESM_DIAG.log('Starting Dropbox backup...');

        // Token ermitteln (kein OAuth)
        const accessToken = await authenticateDropbox();

        // Generate backup data (same as local export)
        const backup = {
          meta: {
            exportedAt: new Date().toISOString(),
            origin: location.origin,
            scriptVersion,
            backupType: 'dropbox'
          },
          localStorage: (() => {
            const out = {};
            try {
              const len = window.localStorage.length;
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.localStorage, i);
                if (k != null) {
                  out[k] = NativeStorage.getItem.call(window.localStorage, k);
                }
              }
            } catch (e) {
              Object.keys(window.localStorage).forEach(k => {
                try { out[k] = window.localStorage.getItem(k); } catch (_) { out[k] = null; }
              });
            }
            return out;
          })(),
          sessionStorage: (() => {
            const out = {};
            try {
              const len = window.sessionStorage.length;
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.sessionStorage, i);
                if (k != null) {
                  out[k] = NativeStorage.getItem.call(window.sessionStorage, k);
                }
              }
            } catch (e) {
              Object.keys(window.sessionStorage).forEach(k => {
                try { out[k] = window.sessionStorage.getItem(k); } catch (_) { out[k] = null; }
              });
            }
            return out;
          })(),
          cookies: document.cookie
            .split(';')
            .map(c => {
              const [name, ...rest] = c.trim().split('=');
              return { name, value: rest.join('=') };
            })
            .filter(c => c.name),
          indexedDB: await backupIndexedDB()
        };

        const backupData = JSON.stringify(backup, null, 2);
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const fileName = `wme_settings_backup_${timestamp}.json`;
        const account = await getDropboxAccount(accessToken);
        const userFolder = `/WME_Backups/${account && account.account_id ? account.account_id : 'unknown'}`;

        // Upload to Dropbox
        let result;
        if (typeof GM_xmlhttpRequest === 'function') {
          // Use GM_xmlhttpRequest to bypass CSP and talk to Dropbox directly
          ESM_DIAG.log('Uploading backup to Dropbox via GM_xmlhttpRequest...');
          const gmRes = await gmFetch(`${DROPBOX_CONFIG.CONTENT_API_URL}/files/upload`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/octet-stream',
              'Authorization': `Bearer ${accessToken}`,
              'Dropbox-API-Arg': JSON.stringify({
                path: `${userFolder}/${fileName}`,
                mode: 'add',
                autorename: true
              })
            },
            body: backupData
          }).catch(err => {
            ESM_DIAG.error('Dropbox upload (GM) failed:', err);
            throw new Error(`Network error during Dropbox upload (GM): ${err.message}`);
          });
          if (gmRes.status < 200 || gmRes.status >= 300) {
            const errorText = gmRes.responseText || 'Unknown error';
            ESM_DIAG.error('Dropbox upload (GM) failed:', gmRes.status, errorText);
            throw new Error(`Upload failed: ${gmRes.status} - ${errorText}`);
          }
          try {
            result = JSON.parse(gmRes.responseText);
          } catch (err) {
            ESM_DIAG.error('Failed to parse upload response (GM):', err);
            throw new Error(`Invalid response format from Dropbox upload (GM): ${err.message}`);
          }
        } else {
          // Fallback to same-origin proxy (requires CSP + CSRF compliance in WME)
          ESM_DIAG.log('Uploading backup to Dropbox via same-origin proxy...');
          const response = await fetch(`${DROPBOX_CONFIG.PROXY_BASE_URL}/files/upload`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/octet-stream',
              'Dropbox-API-Arg': JSON.stringify({
                path: `${userFolder}/${fileName}`,
                mode: 'add',
                autorename: true
              }),
              ...getCsrfHeaders()
            },
            credentials: 'include',
            body: backupData
          }).catch(err => {
            ESM_DIAG.error('Dropbox upload fetch failed:', err);
            throw new Error(`Network error during Dropbox upload: ${err.message}`);
          });

          if (!response.ok) {
            const errorText = await response.text().catch(() => 'Unknown error');
            ESM_DIAG.error('Dropbox upload failed:', response.status, response.statusText, errorText);
            throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText}`);
          }

          result = await response.json().catch(err => {
            ESM_DIAG.error('Failed to parse upload response:', err);
            throw new Error(`Invalid response format from Dropbox upload: ${err.message}`);
          });
        }
        ESM_DIAG.log('Backup uploaded to Dropbox:', result);

        alert(`${t('dropboxSaveSuccessPrefix')}\n\n${t('fileLabel')} ${fileName}\n${t('pathLabel')} /WME_Backups/${fileName}\n${t('sizeLabel')} ${Math.round(backupData.length / 1024)} ${t('kb')}`);

      } catch (error) {
        ESM_DIAG.error('Dropbox backup failed:', error);
        alert(`${t('dropboxSaveFailedPrefix')}\n\n${error.message}`);
      }
    }

    // Import backup from Dropbox
    // Import backup from Dropbox
    async function importFromDropbox() {
      try {
        ESM_DIAG.log('Starting Dropbox restore...');

        // Token ermitteln (kein OAuth)
        let listJson;
        const accessToken = await authenticateDropbox();
        const account = await getDropboxAccount(accessToken);
        const userFolder = `/WME_Backups/${account && account.account_id ? account.account_id : 'unknown'}`;
        if (typeof GM_xmlhttpRequest === 'function') {
          ESM_DIAG.log('Listing backup files from Dropbox via GM_xmlhttpRequest...');
          const gmRes = await gmFetch(`${DROPBOX_CONFIG.API_BASE_URL}/files/list_folder`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${accessToken}`
            },
            body: { path: userFolder, recursive: false }
          }).catch(err => {
            ESM_DIAG.error('Dropbox list (GM) failed:', err);
            throw new Error(`Network error during Dropbox file listing (GM): ${err.message}`);
          });
          if (gmRes.status < 200 || gmRes.status >= 300) {
            const errorText = gmRes.responseText || 'Unknown error';
            ESM_DIAG.error('Dropbox list (GM) failed:', gmRes.status, errorText);
            throw new Error(`List request failed: ${gmRes.status} - ${errorText}`);
          }
          try {
            listJson = JSON.parse(gmRes.responseText);
          } catch (err) {
            ESM_DIAG.error('Failed to parse list response (GM):', err);
            throw new Error(`Invalid response format from Dropbox list (GM): ${err.message}`);
          }
        } else {
          ESM_DIAG.log('Listing backup files from Dropbox via same-origin proxy...');
          const listUrl = `${DROPBOX_CONFIG.PROXY_BASE_URL}/files/list_folder`;
          const listResponse = await fetch(listUrl, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              ...getCsrfHeaders()
            },
            credentials: 'include',
            body: JSON.stringify({
              path: userFolder,
              recursive: false
            })
          }).catch(err => {
            ESM_DIAG.error('Dropbox list fetch failed:', err);
            throw new Error(`Network error during Dropbox file listing: ${err.message}`);
          });
          if (!listResponse.ok) {
            const errorText = await listResponse.text().catch(() => 'Unknown error');
            ESM_DIAG.error('Dropbox list request failed:', listResponse.status, listResponse.statusText, errorText);
            throw new Error(`List request failed: ${listResponse.status} ${listResponse.statusText} - ${errorText}`);
          }
          listJson = await listResponse.json().catch(err => {
            ESM_DIAG.error('Failed to parse list response:', err);
            throw new Error(`Invalid response format from Dropbox list: ${err.message}`);
          });
        }

        const files = listJson.entries ? listJson.entries.filter(entry =>
          entry['.tag'] === 'file' && entry.name.includes('wme_settings_backup')
        ) : [];

        if (!files || files.length === 0) {
          alert(t('dropboxNoBackups'));
          return;
        }

        // Show file selection dialog
        let fileList = 'Verfügbare Backups in Dropbox:\n\n';
        files.forEach((file, index) => {
          const date = new Date(file.client_modified).toLocaleString('de-DE');
          const size = file.size ? `${Math.round(file.size / 1024)} KB` : 'Unbekannt';
          fileList += `${index + 1}. ${file.name}\n   Erstellt: ${date}\n   Größe: ${size}\n\n`;
        });

        const selection = prompt(`${fileList}${t('restorePrompt')} (1-${files.length})`);
        if (!selection) return;

        const fileIndex = parseInt(selection) - 1;
        if (fileIndex < 0 || fileIndex >= files.length) {
          alert(t('invalidSelection'));
          return;
        }

        const selectedFile = files[fileIndex];

        // Download the selected file from Dropbox
        let backupData;
        if (typeof GM_xmlhttpRequest === 'function') {
          ESM_DIAG.log('Downloading backup file from Dropbox via GM_xmlhttpRequest:', selectedFile.name);
          const gmRes = await gmFetch(`${DROPBOX_CONFIG.CONTENT_API_URL}/files/download`, {
            method: 'POST',
            headers: {
              'Authorization': `Bearer ${accessToken}`,
              'Dropbox-API-Arg': JSON.stringify({ path: selectedFile.path_lower })
            },
            responseType: 'text'
          }).catch(err => {
            ESM_DIAG.error('Dropbox download (GM) failed:', err);
            throw new Error(`Network error during Dropbox download (GM): ${err.message}`);
          });
          if (gmRes.status < 200 || gmRes.status >= 300) {
            const errorText = gmRes.responseText || 'Unknown error';
            ESM_DIAG.error('Dropbox download (GM) failed:', gmRes.status, errorText);
            throw new Error(`Download failed: ${gmRes.status} - ${errorText}`);
          }
          backupData = gmRes.responseText;
        } else {
          ESM_DIAG.log('Downloading backup file from Dropbox via same-origin proxy:', selectedFile.name);
          const downloadUrl = `${DROPBOX_CONFIG.PROXY_BASE_URL}/files/download`;
          const downloadResponse = await fetch(downloadUrl, {
            method: 'POST',
            headers: {
              'Dropbox-API-Arg': JSON.stringify({ path: selectedFile.path_lower }),
              ...getCsrfHeaders()
            },
            credentials: 'include'
          }).catch(err => {
            ESM_DIAG.error('Dropbox download fetch failed:', err);
            throw new Error(`Network error during Dropbox download: ${err.message}`);
          });
          if (!downloadResponse.ok) {
            const errorText = await downloadResponse.text().catch(() => 'Unknown error');
            ESM_DIAG.error('Dropbox download failed:', downloadResponse.status, downloadResponse.statusText, errorText);
            throw new Error(`Download failed: ${downloadResponse.status} ${downloadResponse.statusText} - ${errorText}`);
          }
          backupData = await downloadResponse.text().catch(err => {
            ESM_DIAG.error('Failed to read download response:', err);
            throw new Error(`Failed to read backup data: ${err.message}`);
          });
        }

        try {
          const parsed = JSON.parse(backupData);
          importedData = parsed;
          let keyValuePairs = [];
          const originOk = !(parsed && parsed.meta && parsed.meta.origin) || parsed.meta.origin === location.origin;

          if (parsed && typeof parsed === 'object' && (parsed.localStorage || parsed.sessionStorage || parsed.cookies || parsed.indexedDB)) {
            if (parsed.localStorage) {
              for (const [k, v] of Object.entries(parsed.localStorage)) {
                keyValuePairs.push([`localStorage:${k}`, v]);
              }
            }
            if (parsed.sessionStorage && originOk) {
              for (const [k, v] of Object.entries(parsed.sessionStorage)) {
                keyValuePairs.push([`sessionStorage:${k}`, v]);
              }
            }
            if (Array.isArray(parsed.cookies) && originOk) {
              for (const c of parsed.cookies) {
                if (c && c.name != null) {
                  keyValuePairs.push([`cookie:${c.name}`, (c && typeof c.value !== 'undefined' && c.value !== null) ? c.value : '']);
                }
              }
            }
            if (Array.isArray(parsed.indexedDB)) {
              for (const dbBackup of parsed.indexedDB) {
                const dbName = (dbBackup && dbBackup.name) ? dbBackup.name : undefined;
                if (!dbName || !Array.isArray(dbBackup.stores)) continue;
                for (const storeBackup of dbBackup.stores) {
                  const storeName = (storeBackup && storeBackup.name) ? storeBackup.name : undefined;
                  if (!storeName || !Array.isArray(storeBackup.entries)) continue;
                  for (const entry of storeBackup.entries) {
                    const keyStr = JSON.stringify(entry.key);
                    const keyLabel = `indexedDB:${dbName}/${storeName}:${keyStr}`;
                    const valueObj = {
                      db: dbName,
                      store: storeName,
                      key: entry.key,
                      value: entry.value,
                      keyPath: (storeBackup && typeof storeBackup.keyPath !== 'undefined') ? storeBackup.keyPath : null,
                      autoIncrement: !!storeBackup.autoIncrement,
                      indexes: Array.isArray(storeBackup.indexes) ? storeBackup.indexes : []
                    };
                    keyValuePairs.push([keyLabel, valueObj]);
                  }
                }
              }
            }
          } else {
            keyValuePairs = Object.entries(parsed);
          }

          displayKeyList(keyValuePairs);
          if (applyButton) applyButton.style.display = 'block';

          if (!originOk) {
            alert(t('foreignBackupHint'));
          } else {
            alert(`${t('dropboxLoadSuccessPrefix')}\n\n${t('fileLabel')} ${selectedFile.name}\n${t('foundEntriesLabel')} ${keyValuePairs.length}`);
          }

        } catch (parseError) {
          ESM_DIAG.error('Failed to parse backup data:', parseError);
          alert(t('invalidJson'));
        }

      } catch (error) {
        ESM_DIAG.error('Dropbox restore failed:', error);
        alert(`${t('dropboxRestoreFailedPrefix')}\n\n${error.message}`);
      }
    }

    // Export local storage data to a JSON file
    async function backupIndexedDB() {
      const result = [];
      let dbs = [];
      try {
        if (indexedDB.databases) {
          dbs = await indexedDB.databases();
        }
      } catch (e) {
        dbs = [];
      }
      for (const info of dbs) {
        if (!info || !info.name) continue;
        const name = info.name;
        const metaVersion = info.version;
        const backupForDb = await new Promise((resolve) => {
          const req = indexedDB.open(name);
          req.onerror = () => resolve(null);
          req.onupgradeneeded = () => {
            // No schema changes on read
          };
          req.onsuccess = () => {
            const db = req.result;
            const storeBackups = [];
            const stores = Array.from(db.objectStoreNames);
            const perStorePromises = stores.map((storeName) => new Promise((res) => {
              try {
                const tx = db.transaction([storeName], 'readonly');
                const store = tx.objectStore(storeName);
                const keyPath = store.keyPath || null;
                const autoIncrement = store.autoIncrement || false;
                const indexes = Array.from(store.indexNames).map((indexName) => {
                  const idx = store.index(indexName);
                  return { name: indexName, keyPath: idx.keyPath, unique: !!idx.unique, multiEntry: !!idx.multiEntry };
                });
                const out = { name: storeName, keyPath, autoIncrement, indexes, entries: [] };
                if (store.getAll && store.getAllKeys) {
                  const keysReq = store.getAllKeys();
                  const valsReq = store.getAll();
                  keysReq.onsuccess = () => {
                    const keys = keysReq.result || [];
                    valsReq.onsuccess = () => {
                      const vals = valsReq.result || [];
                      const len = Math.max(keys.length, vals.length);
                      for (let i = 0; i < len; i++) {
                        out.entries.push({ key: keys[i], value: vals[i] });
                      }
                      storeBackups.push(out);
                      res(true);
                    };
                    valsReq.onerror = () => { storeBackups.push(out); res(true); };
                  };
                  keysReq.onerror = () => { storeBackups.push(out); res(true); };
                } else {
                  const request = store.openCursor();
                  request.onsuccess = (e) => {
                    const cursor = e.target.result;
                    if (cursor) {
                      out.entries.push({ key: cursor.key, value: cursor.value });
                      cursor.continue();
                    } else {
                      storeBackups.push(out);
                      res(true);
                    }
                  };
                  request.onerror = () => { storeBackups.push(out); res(true); };
                }
              } catch (err) {
                res(true);
              }
            }));
            Promise.all(perStorePromises).then(() => {
              const backupObj = { name, version: db.version || metaVersion || null, stores: storeBackups };
              db.close();
              resolve(backupObj);
            });
          };
        });
        if (backupForDb) result.push(backupForDb);
      }
      return result;
    }
    async function exportLocalStorage() {
      const backup = {
        meta: {
          exportedAt: new Date().toISOString(),
          origin: location.origin,
          scriptVersion
        },
        // Robust: Determine keys and read values ​​via the native storage API
        localStorage: (() => {
          const out = {};
          try {
            const len = window.localStorage.length;
            for (let i = 0; i < len; i++) {
              const k = NativeStorage.key.call(window.localStorage, i);
              if (k != null) {
                out[k] = NativeStorage.getItem.call(window.localStorage, k);
              }
            }
          } catch (e) {
            // Fallback if another script has patched key()/getItem
            Object.keys(window.localStorage).forEach(k => {
              try { out[k] = window.localStorage.getItem(k); } catch (_) { out[k] = null; }
            });
          }
          return out;
        })(),
        sessionStorage: (() => {
          const out = {};
          try {
            const len = window.sessionStorage.length;
            for (let i = 0; i < len; i++) {
              const k = NativeStorage.key.call(window.sessionStorage, i);
              if (k != null) {
                out[k] = NativeStorage.getItem.call(window.sessionStorage, k);
              }
            }
          } catch (e) {
            Object.keys(window.sessionStorage).forEach(k => {
              try { out[k] = window.sessionStorage.getItem(k); } catch (_) { out[k] = null; }
            });
          }
          return out;
        })(),
        cookies: document.cookie
          .split(';')
          .map(c => {
            const [name, ...rest] = c.trim().split('=');
            return { name, value: rest.join('=') };
          })
          .filter(c => c.name),
        indexedDB: await backupIndexedDB()
      };
      const data = JSON.stringify(backup, null, 2);
      const file = new Blob([data], { type: 'application/json' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(file);
      const ts = new Date().toISOString().replace(/[:.]/g, '-');
      a.download = `wme_settings_backup_${ts}.json`;
      a.click();
    }

    // Import local storage data from a JSON file
    function importLocalStorage() {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = 'application/json';
      input.onchange = function (event) {
        const file = event.target.files[0];
        const reader = new FileReader();
        reader.onload = function () {
          try {
            const parsed = JSON.parse(reader.result);
            importedData = parsed;
            let keyValuePairs = [];
            const originOk = !(parsed && parsed.meta && parsed.meta.origin) || parsed.meta.origin === location.origin;
            if (parsed && typeof parsed === 'object' && (parsed.localStorage || parsed.sessionStorage || parsed.cookies || parsed.indexedDB)) {
              if (parsed.localStorage) {
                for (const [k, v] of Object.entries(parsed.localStorage)) {
                  keyValuePairs.push([`localStorage:${k}`, v]);
                }
              }
              if (parsed.sessionStorage && originOk) {
                for (const [k, v] of Object.entries(parsed.sessionStorage)) {
                  keyValuePairs.push([`sessionStorage:${k}`, v]);
                }
              }
              if (Array.isArray(parsed.cookies) && originOk) {
                for (const c of parsed.cookies) {
                  if (c && c.name != null) {
                    keyValuePairs.push([`cookie:${c.name}`, (c && typeof c.value !== 'undefined' && c.value !== null) ? c.value : '']);
                  }
                }
              }
              if (Array.isArray(parsed.indexedDB)) {
                for (const dbBackup of parsed.indexedDB) {
                  const dbName = (dbBackup && dbBackup.name) ? dbBackup.name : undefined;
                  if (!dbName || !Array.isArray(dbBackup.stores)) continue;
                  for (const storeBackup of dbBackup.stores) {
                    const storeName = (storeBackup && storeBackup.name) ? storeBackup.name : undefined;
                    if (!storeName || !Array.isArray(storeBackup.entries)) continue;
                    for (const entry of storeBackup.entries) {
                      const keyStr = JSON.stringify(entry.key);
                      const keyLabel = `indexedDB:${dbName}/${storeName}:${keyStr}`;
                      const valueObj = {
                        db: dbName,
                        store: storeName,
                        key: entry.key,
                        value: entry.value,
                        keyPath: (storeBackup && typeof storeBackup.keyPath !== 'undefined') ? storeBackup.keyPath : null,
                        autoIncrement: !!storeBackup.autoIncrement,
                        indexes: Array.isArray(storeBackup.indexes) ? storeBackup.indexes : []
                      };
                      keyValuePairs.push([keyLabel, valueObj]);
                    }
                  }
                }
              }
            } else {
              keyValuePairs = Object.entries(parsed);
            }
            displayKeyList(keyValuePairs);
            if (applyButton) applyButton.style.display = 'block';
            if (!originOk) {
              alert(t('foreignBackupHint'));
            } else {
              alert(t('fileReadSuccess'));
            }
          } catch (error) {
            // Only display the error message if the import fails
            console.error(error);
            alert(t('invalidJson'));
          };
        };
        reader.onerror = function () {
          alert(t('fileReadError'));
        };
        reader.readAsText(file);
      };
      input.click();
    }

    // Display the list of keys for selection
    function displayKeyList(keyValuePairs) {
      const container = document.getElementById('key-list-container');
      container.innerHTML = ''; // Clear existing list

      // Select All button
      const selectAllButton = document.createElement('button');
    selectAllButton.textContent = t('selectAll');
      selectAllButton.addEventListener('click', function () {
        const checkboxes = container.querySelectorAll('input[type="checkbox"]');
        checkboxes.forEach((checkbox) => {
          checkbox.checked = true;
        });
      });
      container.appendChild(selectAllButton);

      // Deselect All button
      const deselectAllButton = document.createElement('button');
    deselectAllButton.textContent = t('deselectAll');
      deselectAllButton.addEventListener('click', function () {
        const checkboxes = container.querySelectorAll('input[type="checkbox"]');
        checkboxes.forEach((checkbox) => {
          checkbox.checked = false;
        });
      });
      container.appendChild(deselectAllButton);

      container.appendChild(document.createElement('br'));

      // Key checkboxes
      keyValuePairs.forEach(([key, value]) => {
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = key;
        checkbox.value = key;
        checkbox.checked = true;
        container.appendChild(checkbox);

        const label = document.createElement('label');
        label.htmlFor = key;
        label.textContent = key;
        container.appendChild(label);

        const hiddenValue = document.createElement('input');
        hiddenValue.type = 'hidden';
        hiddenValue.value = typeof value === 'string' ? value : JSON.stringify(value);
        container.appendChild(hiddenValue);

        container.appendChild(document.createElement('br'));
      });

      // Apply button
      applyButton = document.createElement('button');
    applyButton.textContent = t('apply');
      applyButton.addEventListener('click', applyImport);
      container.appendChild(applyButton);
    }

    // Apply the selected key-value pairs from the JSON file
    async function applyImport() {
      const selectedPairs = getSelectedPairs();
      if (selectedPairs.length === 0) {
        alert('No keys selected. Nothing to import.');
        return;
      }

      const { counts, failures } = await applyPairs(selectedPairs);

      const summary = `Import Completed\n- localStorage: ${counts.local}\n- sessionStorage: ${counts.session}\n- Cookies: ${counts.cookie}\n- IndexedDB: ${counts.idb}` + (failures.length ? `\n\nError (first ${Math.min(5, failures.length)}):\n${failures.slice(0,5).join('\n')}${failures.length > 5 ? `\n... (${failures.length - 5} more)` : ''}` : '');
      alert(summary);

      // Prompt to refresh the page
      if (confirm('The import was successful. Press ok to refresh page.')) {
        try {
          // Stash for post-reload reapply to prevent other scripts from overwriting restored values during startup
          sessionStorage.setItem(REAPPLY_STASH_KEY, JSON.stringify({ origin: location.origin, items: selectedPairs }));
        } catch (e) {
          // ignore stash errors
        }
        location.reload();
      }
    }

    // Get the selected key-value pairs
    function getSelectedPairs() {
      const checkboxes = document.querySelectorAll('#key-list-container input[type="checkbox"]');
      const selectedPairs = [];
      checkboxes.forEach((checkbox) => {
        if (checkbox.checked) {
          const key = checkbox.value;
          const valueStr = checkbox.nextElementSibling.nextElementSibling.value;
          // For IndexedDB entries, the value is JSON-encoded; for all other storages, the string must remain unchanged
          if (key.startsWith('indexedDB:')) {
            let parsedValue;
            try {
              parsedValue = JSON.parse(valueStr);
            } catch (e) {
              parsedValue = valueStr; // Fallback in case something unexpected happens
            }
            selectedPairs.push([key, parsedValue]);
          } else {
            selectedPairs.push([key, valueStr]);
          }
        }
      });
      return selectedPairs;
    }

    // Helper to apply selected pairs across storage types and IndexedDB (inside IIFE)
    async function applyPairs(selectedPairs) {
      const counts = { local: 0, session: 0, cookie: 0, idb: 0 };
      const failures = [];
      const sourceOrigin = (importedData && importedData.meta && importedData.meta.origin) ? importedData.meta.origin : undefined;
      const sameOrigin = !sourceOrigin || sourceOrigin === location.origin;

      for (const [fullKey, value] of selectedPairs) {
        try {
          const colonIdx = fullKey.indexOf(':');
          if (colonIdx < 0) {
            // Fallback: without type prefix we treat it as localStorage
            localStorage.setItem(fullKey, value);
            counts.local++;
            continue;
          }
          const type = fullKey.slice(0, colonIdx);
          const rest = fullKey.slice(colonIdx + 1);

          if (type === 'localStorage') {
            try {
              NativeStorage.setItem.call(window.localStorage, rest, value);
            } catch (_) {
              window.localStorage.setItem(rest, value);
            }
            counts.local++;
          } else if (type === 'sessionStorage') {
            if (!sameOrigin) {
              failures.push(`sessionStorage:${rest} skipped (Origin differs)`);
            } else {
              try {
                NativeStorage.setItem.call(window.sessionStorage, rest, value);
              } catch (_) {
                window.sessionStorage.setItem(rest, value);
              }
              counts.session++;
            }
          } else if (type === 'cookie') {
            if (!sameOrigin) {
              failures.push(`cookie:${rest} skipped (Origin differs)`);
            } else {
              // Set a session cookie (root path). An expiration date can be added if necessary.
              document.cookie = `${rest}=${value}; path=/`;
              counts.cookie++;
            }
          } else if (type === 'indexedDB') {
            try {
              // The value is already a complete record object (db, store, key, value, keyPath, autoIncrement, indexes)
              await writeIndexedDBRecord(value);
              counts.idb++;
            } catch (e) {
              failures.push(`indexedDB:${rest} -> ${e && e.message ? e.message : e}`);
            }
          } else {
            // Unknown type: treat as localStorage
            localStorage.setItem(rest, value);
            counts.local++;
          }
        } catch (err) {
          failures.push(`${fullKey} -> ${err && err.message ? err.message : err}`);
        }
      }

      return { counts, failures };
    }

    // Create and add the tab for the script (robust multi-path)
    function addScriptTab() {
      if (typeof window !== 'undefined' && window.uiMounted) {
        ESM_DIAG.log('UI already mounted, skipping addScriptTab');
        return;
      }

      const haveWUserscripts = typeof W !== 'undefined' && W && W.userscripts && typeof W.userscripts.registerSidebarTab === 'function';
      const haveWazeWrapTab = typeof WazeWrap !== 'undefined' && WazeWrap && WazeWrap.Interface && typeof WazeWrap.Interface.Tab === 'function';

      if (haveWUserscripts) {
        try {
          const scriptId = 'easy-storage-manager-tab';
          const { tabLabel, tabPane } = W.userscripts.registerSidebarTab(scriptId);

          tabLabel.innerText = '💾';
          tabLabel.title = t('scriptTitle');

          const description = document.createElement('p');
          description.style.fontWeight = 'bold';
    description.textContent = t('scriptTitle');
          tabPane.appendChild(description);

const text = document.createElement('p');
text.textContent = t('importExportDesc');
          tabPane.appendChild(text);

const importButton = document.createElement('button');
importButton.textContent = t('importBackup');
importButton.title = t('importBackupTitle');
          importButton.addEventListener('click', function() {
            if (typeof window.importLocalStorage === 'function') {
              window.importLocalStorage();
            } else if (typeof importLocalStorage === 'function') {
              importLocalStorage();
            } else {
              try { if (window.ESM_DIAG) window.ESM_DIAG.error('Import function not available'); } catch (_) {}
              alert('Import function not available');
            }
          });
    // keep title already set
          importButton.style.backgroundImage = 'linear-gradient(180deg, #2196f3, #1976d2)';
          importButton.style.color = '#fff';
          importButton.style.border = 'none';
          importButton.style.borderRadius = '10px';
          importButton.style.padding = '8px 12px';
          importButton.style.fontWeight = '600';
          importButton.style.cursor = 'pointer';
          importButton.style.boxShadow = '0 3px 8px rgba(25, 118, 210, 0.35)';
          importButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          importButton.style.whiteSpace = 'nowrap';
          importButton.style.display = 'inline-flex';
          importButton.style.alignItems = 'center';
          importButton.style.flex = '0 0 auto';
          importButton.style.gap = '4px';
          importButton.style.fontSize = '13px';
          importButton.style.lineHeight = '18px';
          importButton.style.width = 'auto';
          importButton.addEventListener('mouseenter', () => { importButton.style.filter = 'brightness(1.08)'; importButton.style.boxShadow = '0 6px 14px rgba(25,118,210,0.45)'; });
          importButton.addEventListener('mouseleave', () => { importButton.style.filter = ''; importButton.style.boxShadow = '0 3px 8px rgba(25,118,210,0.35)'; });

const exportButton = document.createElement('button');
exportButton.textContent = t('exportBackup');
exportButton.title = t('exportBackupTitle');
          exportButton.addEventListener('click', function() {
            if (typeof window.exportLocalStorage === 'function') {
              window.exportLocalStorage();
            } else if (typeof exportLocalStorage === 'function') {
              exportLocalStorage();
            } else {
              try { if (window.ESM_DIAG) window.ESM_DIAG.error('Export function not available'); } catch (_) {}
              alert('Export function not available');
            }
          });
    // keep title already set
          exportButton.style.backgroundImage = 'linear-gradient(180deg, #43a047, #2e7d32)';
          exportButton.style.color = '#fff';
          exportButton.style.border = 'none';
          exportButton.style.borderRadius = '10px';
          exportButton.style.padding = '8px 12px';
          exportButton.style.fontWeight = '600';
          exportButton.style.cursor = 'pointer';
          exportButton.style.boxShadow = '0 3px 8px rgba(46, 125, 50, 0.35)';
          exportButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          exportButton.style.whiteSpace = 'nowrap';
          exportButton.style.display = 'inline-flex';
          exportButton.style.alignItems = 'center';
          exportButton.style.flex = '0 0 auto';
          exportButton.style.gap = '4px';
          exportButton.style.fontSize = '13px';
          exportButton.style.lineHeight = '18px';
          exportButton.style.width = 'auto';
          exportButton.addEventListener('mouseenter', () => { exportButton.style.filter = 'brightness(1.08)'; exportButton.style.boxShadow = '0 6px 14px rgba(46,125,50,0.45)'; });
          exportButton.addEventListener('mouseleave', () => { exportButton.style.filter = ''; exportButton.style.boxShadow = '0 3px 8px rgba(46,125,50,0.35)'; });

          const buttonContainer = document.createElement('div');
          buttonContainer.style.display = 'flex';
          buttonContainer.style.gap = '8px';
          buttonContainer.style.marginTop = '8px';
          buttonContainer.style.justifyContent = 'center';
          buttonContainer.style.alignItems = 'center';
          buttonContainer.style.width = '100%';
          // Reihenfolge: zuerst Lokal Speichern (Export), dann Wiederherstellen (Import)
          exportButton.style.order = '1';
          importButton.style.order = '2';
          buttonContainer.appendChild(exportButton);
          buttonContainer.appendChild(importButton);
          tabPane.appendChild(buttonContainer);

          // Dropbox Cloud Backup Buttons
const cloudBackupButton = document.createElement('button');
cloudBackupButton.textContent = t('cloudBackup');
cloudBackupButton.title = t('cloudBackupTitle');
          cloudBackupButton.addEventListener('click', function() {
            if (typeof exportToDropbox === 'function') {
          exportToDropbox();
            } else {
try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox export function not available'); } catch (_) {}
alert(t('dropboxExportUnavailable'));
            }
          });
    // title already set
          cloudBackupButton.style.backgroundImage = 'linear-gradient(180deg, #ff9800, #f57c00)';
          cloudBackupButton.style.color = '#fff';
          cloudBackupButton.style.border = 'none';
          cloudBackupButton.style.borderRadius = '10px';
          cloudBackupButton.style.padding = '8px 12px';
          cloudBackupButton.style.fontWeight = '600';
          cloudBackupButton.style.cursor = 'pointer';
          cloudBackupButton.style.boxShadow = '0 3px 8px rgba(245, 124, 0, 0.35)';
          cloudBackupButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          cloudBackupButton.style.whiteSpace = 'nowrap';
          cloudBackupButton.style.display = 'inline-flex';
          cloudBackupButton.style.alignItems = 'center';
          cloudBackupButton.style.flex = '0 0 auto';
          cloudBackupButton.style.gap = '4px';
          cloudBackupButton.style.fontSize = '13px';
          cloudBackupButton.style.lineHeight = '18px';
          cloudBackupButton.style.width = 'auto';
          cloudBackupButton.addEventListener('mouseenter', () => { cloudBackupButton.style.filter = 'brightness(1.08)'; cloudBackupButton.style.boxShadow = '0 6px 14px rgba(245,124,0,0.45)'; });
          cloudBackupButton.addEventListener('mouseleave', () => { cloudBackupButton.style.filter = ''; cloudBackupButton.style.boxShadow = '0 3px 8px rgba(245,124,0,0.35)'; });

const cloudRestoreButton = document.createElement('button');
cloudRestoreButton.textContent = t('cloudRestore');
cloudRestoreButton.title = t('cloudRestoreTitle');
          cloudRestoreButton.addEventListener('click', function() {
            if (typeof importFromDropbox === 'function') {
          importFromDropbox();
            } else {
try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox restore function not available'); } catch (_) {}
alert(t('dropboxImportUnavailable'));
            }
          });
    // title already set
          cloudRestoreButton.style.backgroundImage = 'linear-gradient(180deg, #9c27b0, #7b1fa2)';
          cloudRestoreButton.style.color = '#fff';
          cloudRestoreButton.style.border = 'none';
          cloudRestoreButton.style.borderRadius = '10px';
          cloudRestoreButton.style.padding = '8px 12px';
          cloudRestoreButton.style.fontWeight = '600';
          cloudRestoreButton.style.cursor = 'pointer';
          cloudRestoreButton.style.boxShadow = '0 3px 8px rgba(123, 31, 162, 0.35)';
          cloudRestoreButton.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
          cloudRestoreButton.style.whiteSpace = 'nowrap';
          cloudRestoreButton.style.display = 'inline-flex';
          cloudRestoreButton.style.alignItems = 'center';
          cloudRestoreButton.style.flex = '0 0 auto';
          cloudRestoreButton.style.gap = '4px';
          cloudRestoreButton.style.fontSize = '13px';
          cloudRestoreButton.style.lineHeight = '18px';
          cloudRestoreButton.style.width = 'auto';
          cloudRestoreButton.addEventListener('mouseenter', () => { cloudRestoreButton.style.filter = 'brightness(1.08)'; cloudRestoreButton.style.boxShadow = '0 6px 14px rgba(123,31,162,0.45)'; });
          cloudRestoreButton.addEventListener('mouseleave', () => { cloudRestoreButton.style.filter = ''; cloudRestoreButton.style.boxShadow = '0 3px 8px rgba(123,31,162,0.35)'; });

          const cloudButtonContainer = document.createElement('div');
          cloudButtonContainer.style.display = 'flex';
          cloudButtonContainer.style.gap = '8px';
          cloudButtonContainer.style.marginTop = '8px';
          cloudButtonContainer.style.justifyContent = 'center';
          cloudButtonContainer.style.alignItems = 'center';
          cloudButtonContainer.style.width = '100%';
          cloudButtonContainer.appendChild(cloudBackupButton);
          cloudButtonContainer.appendChild(cloudRestoreButton);
          tabPane.appendChild(cloudButtonContainer);

          // Dropbox-Hilfepanel direkt im Skripte-Tab anzeigen
          try { injectDropboxHelpPanel(tabPane); } catch (_) {}

          const keyListContainer = document.createElement('div');
          keyListContainer.id = 'key-list-container';
          keyListContainer.style.marginTop = '10px';
          tabPane.appendChild(keyListContainer);

          if (typeof window !== 'undefined') window.uiMounted = true;
          ESM_DIAG.log('UI mounted via W.userscripts.registerSidebarTab');
          return;
        } catch (e) {
          ESM_DIAG.error('Failed to mount via W.userscripts, falling back', e);
        }
      }

      if (haveWazeWrapTab) {
        try {
          const html = `
            <div id="esm-tab">
              <p id="esm-title" style="font-weight:700;margin:0 0 6px 0;">${t('scriptTitle')}</p>
              <p style="margin:0 0 8px 0;">${t('importExportDesc')}</p>
              <div style="display:flex;gap:8px;margin-bottom:8px;justify-content:center;align-items:center;width:100%;">
                <button id="esm-export">${t('exportBackup')}</button>
                <button id="esm-import">${t('importBackup')}</button>
              </div>
              <div style="display:flex;gap:8px;margin-bottom:8px;justify-content:center;align-items:center;width:100%;">
                <button id="esm-cloud-backup">${t('cloudBackup')}</button>
                <button id="esm-cloud-restore">${t('cloudRestore')}</button>
              </div>
              <div id="key-list-container" style="border:1px solid rgba(0,0,0,0.1);border-radius:8px;padding:8px;max-height:320px;overflow:auto;"></div>
            </div>`;
          new WazeWrap.Interface.Tab(t('scriptTitle'), html, () => {
const importBtn = document.getElementById('esm-import');
if (importBtn) { importBtn.textContent = t('importBackup'); importBtn.title = t('importBackupTitle'); }
const exportBtn = document.getElementById('esm-export');
if (exportBtn) { exportBtn.textContent = t('exportBackup'); exportBtn.title = t('exportBackupTitle'); }
const cloudBackupBtn = document.getElementById('esm-cloud-backup');
if (cloudBackupBtn) { cloudBackupBtn.textContent = t('cloudBackup'); cloudBackupBtn.title = t('cloudBackupTitle'); }
const cloudRestoreBtn = document.getElementById('esm-cloud-restore');
if (cloudRestoreBtn) { cloudRestoreBtn.textContent = t('cloudRestore'); cloudRestoreBtn.title = t('cloudRestoreTitle'); }
            if (cloudBackupBtn) cloudBackupBtn.textContent = t('cloudBackup');
            if (cloudRestoreBtn) cloudRestoreBtn.textContent = t('cloudRestore');

            if (importBtn) importBtn.addEventListener('click', function() { if (typeof window.importLocalStorage === 'function') { window.importLocalStorage(); } else { try { if (window.ESM_DIAG) window.ESM_DIAG.error('Import function not available'); } catch (_) {} alert(t('importFunctionUnavailable')); } });
            if (exportBtn) exportBtn.addEventListener('click', function() { if (typeof window.exportLocalStorage === 'function') { window.exportLocalStorage(); } else { try { if (window.ESM_DIAG) window.ESM_DIAG.error('Export function not available'); } catch (_) {} alert(t('exportFunctionUnavailable')); } });
            if (cloudBackupBtn) cloudBackupBtn.addEventListener('click', function() { if (typeof exportToDropbox === 'function') { exportToDropbox(); } else { try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox export function not available'); } catch (_) {} alert(t('dropboxExportUnavailable')); } });
            if (cloudRestoreBtn) cloudRestoreBtn.addEventListener('click', function() { if (typeof importFromDropbox === 'function') { importFromDropbox(); } else { try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox import function not available'); } catch (_) {} alert(t('dropboxImportUnavailable')); } });
            // Reihenfolge im Flex-Container: zuerst Lokal Speichern (Export), dann Wiederherstellen (Import)
            if (typeof exportBtn !== 'undefined' && exportBtn) exportBtn.style.order = '1';
            if (typeof importBtn !== 'undefined' && importBtn) importBtn.style.order = '2';

            // Apply compact styles for WazeWrap tab buttons
            if (importBtn) {
              importBtn.style.backgroundImage = 'linear-gradient(180deg, #2196f3, #1976d2)';
              importBtn.style.color = '#fff';
              importBtn.style.border = 'none';
              importBtn.style.borderRadius = '10px';
              importBtn.style.padding = '8px 12px';
              importBtn.style.fontWeight = '600';
              importBtn.style.cursor = 'pointer';
              importBtn.style.boxShadow = '0 3px 8px rgba(25, 118, 210, 0.35)';
              importBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              importBtn.style.whiteSpace = 'nowrap';
              importBtn.style.display = 'inline-flex';
              importBtn.style.alignItems = 'center';
              importBtn.style.gap = '4px';
              importBtn.style.fontSize = '13px';
              importBtn.style.lineHeight = '18px';
            }
            if (exportBtn) {
              exportBtn.style.backgroundImage = 'linear-gradient(180deg, #43a047, #2e7d32)';
              exportBtn.style.color = '#fff';
              exportBtn.style.border = 'none';
              exportBtn.style.borderRadius = '10px';
              exportBtn.style.padding = '8px 12px';
              exportBtn.style.fontWeight = '600';
              exportBtn.style.cursor = 'pointer';
              exportBtn.style.boxShadow = '0 3px 8px rgba(46, 125, 50, 0.35)';
              exportBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              exportBtn.style.whiteSpace = 'nowrap';
              exportBtn.style.display = 'inline-flex';
              exportBtn.style.alignItems = 'center';
              exportBtn.style.gap = '4px';
              exportBtn.style.fontSize = '13px';
              exportBtn.style.lineHeight = '18px';
            }
            if (cloudBackupBtn) {
              cloudBackupBtn.style.backgroundImage = 'linear-gradient(180deg, #ff9800, #f57c00)';
              cloudBackupBtn.style.color = '#fff';
              cloudBackupBtn.style.border = 'none';
              cloudBackupBtn.style.borderRadius = '10px';
              cloudBackupBtn.style.padding = '8px 12px';
              cloudBackupBtn.style.fontWeight = '600';
              cloudBackupBtn.style.cursor = 'pointer';
              cloudBackupBtn.style.boxShadow = '0 3px 8px rgba(245, 124, 0, 0.35)';
              cloudBackupBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              cloudBackupBtn.style.whiteSpace = 'nowrap';
              cloudBackupBtn.style.display = 'inline-flex';
              cloudBackupBtn.style.alignItems = 'center';
              cloudBackupBtn.style.gap = '4px';
              cloudBackupBtn.style.fontSize = '13px';
              cloudBackupBtn.style.lineHeight = '18px';
    cloudBackupBtn.title = t('cloudBackupTitle');
            }
            if (cloudRestoreBtn) {
              cloudRestoreBtn.style.backgroundImage = 'linear-gradient(180deg, #9c27b0, #7b1fa2)';
              cloudRestoreBtn.style.color = '#fff';
              cloudRestoreBtn.style.border = 'none';
              cloudRestoreBtn.style.borderRadius = '10px';
              cloudRestoreBtn.style.padding = '8px 12px';
              cloudRestoreBtn.style.fontWeight = '600';
              cloudRestoreBtn.style.cursor = 'pointer';
              cloudRestoreBtn.style.boxShadow = '0 3px 8px rgba(123, 31, 162, 0.35)';
              cloudRestoreBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
              cloudRestoreBtn.style.whiteSpace = 'nowrap';
              cloudRestoreBtn.style.display = 'inline-flex';
              cloudRestoreBtn.style.alignItems = 'center';
              cloudRestoreBtn.style.gap = '4px';
              cloudRestoreBtn.style.fontSize = '13px';
              cloudRestoreBtn.style.lineHeight = '18px';
    cloudRestoreBtn.title = t('cloudRestoreTitle');
            }
            if (typeof window !== 'undefined') window.uiMounted = true;
            ESM_DIAG.log('UI mounted via WazeWrap.Interface.Tab');
            // Dropbox-Hilfepanel im WazeWrap-Tab platzieren
            try { const tabRoot = document.getElementById('esm-tab') || document.body; injectDropboxHelpPanel(tabRoot); } catch (_) {}
          });
          return;
        } catch (e) {
          ESM_DIAG.error('Failed to mount via WazeWrap.Interface.Tab, falling back', e);
        }
      }

      // Final fallback: floating panel
      createFallbackPanel();
    }

     // Initialize the script
  function initialize() {
    ESM_DIAG.log('initialize() called. document.readyState=', document.readyState);

    // Check if we're in a Waze environment
    const isWazeEnvironment = window.location.hostname.includes('waze.com');

    if (isWazeEnvironment && typeof W !== 'undefined' && W && W.userscripts && W.userscripts.state && W.userscripts.state.isReady) {
      ESM_DIAG.log('W.userscripts.state.isReady is true. Initializing UI.');
      addScriptTab();
      showScriptUpdate();
      reapplyAfterReload();
    } else if (isWazeEnvironment) {
      ESM_DIAG.log('Waiting for wme-ready event...');
      document.addEventListener('wme-ready', function () {
        ESM_DIAG.log('wme-ready event received. Initializing UI.');
        addScriptTab();
        showScriptUpdate();
        reapplyAfterReload();
      }, { once: true });
    } else {
      // Non-Waze environment - use fallback panel directly
      ESM_DIAG.log('Non-Waze environment detected. Using fallback panel.');
      createFallbackPanel();
      reapplyAfterReload();
    }
  }

  // Call the initialize function
  initialize();

  // Show script update notification
  function showScriptUpdate() {
    if (typeof WazeWrap !== 'undefined' && WazeWrap && WazeWrap.Interface && typeof WazeWrap.Interface.ShowScriptUpdate === 'function') {
      WazeWrap.Interface.ShowScriptUpdate(
        t('scriptTitle'),
        (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) ? GM_info.script.version : scriptVersion,
        updateMessage,
        'https://greasyfork.org/en/scripts/466806-easy-storage-manager',
        'https://www.waze.com/forum/viewtopic.php?t=382966'
      );
    } else {
      ESM_DIAG.warn('ShowScriptUpdate skipped: WazeWrap.Interface.ShowScriptUpdate not available yet.');
    }
  }

  // Reapply after reload if a stash exists (inside IIFE)
  function reapplyAfterReload() {
    let stashStr = null;
    try {
      stashStr = sessionStorage.getItem(REAPPLY_STASH_KEY);
    } catch (e) {
      stashStr = null;
    }
    if (!stashStr) { ESM_DIAG.log('No reapply stash found; skipping.'); return; }
    sessionStorage.removeItem(REAPPLY_STASH_KEY);
    ESM_DIAG.log('Reapply stash found. Parsing...');
    try {
      const stash = JSON.parse(stashStr);
      ESM_DIAG.log('Parsed stash items count:', Array.isArray(stash.items) ? stash.items.length : 0);
      if (!(stash && stash.origin === location.origin && Array.isArray(stash.items) && stash.items.length)) { ESM_DIAG.warn('Stash invalid or empty; skipping.'); return; }

      applyPairs(stash.items).then(({ counts, failures }) => {
        ESM_DIAG.log('Initial applyPairs after reload complete.', counts, failures.slice(0, 3));
        const summary = `Restored after reload executed.\n- localStorage: ${counts.local}\n- sessionStorage: ${counts.session}\n- Cookies: ${counts.cookie}\n- IndexedDB: ${counts.idb}` + (failures.length ? `\n\nError (first ${Math.min(5, failures.length)}):\n${failures.slice(0,5).join('\n')}${failures.length > 5 ? `\n... (${failures.length - 5} more)` : ''}` : '');
        try { alert(summary); } catch (e) {}
      }).catch((ex) => { ESM_DIAG.error('applyPairs after reload failed:', ex); });

      const desiredLocal = new Map();
      for (const [fullKey, value] of stash.items) {
        const idx = fullKey.indexOf(':');
        const type = idx < 0 ? 'localStorage' : fullKey.slice(0, idx);
        const name = idx < 0 ? fullKey : fullKey.slice(idx + 1);
        if (type === 'localStorage') {
          desiredLocal.set(name, value);
        }
      }
      ESM_DIAG.log('Desired localStorage keys:', Array.from(desiredLocal.keys()));

      const scheduleRepairs = (delaysMs) => {
        ESM_DIAG.log('Scheduling repairs with delays:', delaysMs);
        delaysMs.forEach(ms => {
          setTimeout(() => {
            const repairs = [];
            desiredLocal.forEach((desired, name) => {
              try {
                const current = NativeStorage.getItem.call(window.localStorage, name);
                if (current !== desired) {
                  ESM_DIAG.log('Repair needed for key:', name, 'current=', current, 'desired=', desired);
                  repairs.push([`localStorage:${name}`, desired]);
                } else {
                  ESM_DIAG.log('Key already correct:', name);
                }
              } catch (e) {
                ESM_DIAG.warn('Error reading key during repair check:', name, e);
              }
            });
            if (repairs.length) {
              ESM_DIAG.log('Applying repairs count:', repairs.length);
              applyPairs(repairs).catch((ex) => { ESM_DIAG.error('applyPairs repairs failed:', ex); });
            }
          }, ms);
        });
      };

      const originalSetItem = localStorage.setItem.bind(localStorage);
      const originalGetItem = localStorage.getItem.bind(localStorage);
      const originalRemoveItem = localStorage.removeItem.bind(localStorage);
      const originalClear = localStorage.clear.bind(localStorage);
      const originalProtoSetItem = Storage.prototype.setItem;
      const originalProtoGetItem = Storage.prototype.getItem;
      const originalProtoRemoveItem = Storage.prototype.removeItem;
      const originalProtoClear = Storage.prototype.clear;
      const protectMs = 120000; // 120s protection
      const protectUntil = Date.now() + protectMs;
      const protectedKeys = new Set(Array.from(desiredLocal.keys()));

      try {
        localStorage.setItem = function (key, value) {
          if (protectedKeys.has(key) && Date.now() < protectUntil) {
            ESM_DIAG.log('Protected setItem intercepted for key:', key);
            try { return NativeStorage.setItem.call(window.localStorage, key, desiredLocal.get(key)); } catch (e) { return; }
          }
          return originalSetItem(key, value);
        };

        localStorage.getItem = function (key) {
          if (protectedKeys.has(key) && Date.now() < protectUntil) {
            const desired = desiredLocal.get(key);
            ESM_DIAG.log('Protected getItem intercepted for key:', key);
            return typeof desired === 'string' ? desired : desired;
          }
          return NativeStorage.getItem.call(window.localStorage, key);
        };

        localStorage.removeItem = function (key) {
          if (protectedKeys.has(key) && Date.now() < protectUntil) {
            ESM_DIAG.log('Protected removeItem blocked for key:', key);
            try { return NativeStorage.setItem.call(window.localStorage, key, desiredLocal.get(key)); } catch (_) { return; }
          }
          return originalRemoveItem(key);
        };

        localStorage.clear = function () {
          if (Date.now() < protectUntil && protectedKeys.size) {
            ESM_DIAG.log('Protected clear intercepted; preserving keys:', Array.from(protectedKeys));
            try {
              const len = window.localStorage.length;
              const toRemove = [];
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.localStorage, i);
                if (k != null && !protectedKeys.has(k)) toRemove.push(k);
              }
              toRemove.forEach(k => { try { originalRemoveItem(k); } catch (_) {} });
              return;
            } catch (_) { /* fallback */ }
          }
          return originalClear();
        };

        Storage.prototype.setItem = function (key, value) {
          const isLocal = this === window.localStorage || this === localStorage;
          if (isLocal && protectedKeys.has(key) && Date.now() < protectUntil) {
            ESM_DIAG.log('Prototype setItem intercepted for key:', key);
            try { return NativeStorage.setItem.call(window.localStorage, key, desiredLocal.get(key)); } catch (e) { return; }
          }
          return originalProtoSetItem.call(this, key, value);
        };

        Storage.prototype.getItem = function (key) {
          const isLocal = this === window.localStorage || this === localStorage;
          if (isLocal && protectedKeys.has(key) && Date.now() < protectUntil) {
            const desired = desiredLocal.get(key);
            ESM_DIAG.log('Prototype getItem intercepted for key:', key);
            return typeof desired === 'string' ? desired : desired;
          }
          return originalProtoGetItem.call(this, key);
        };

        Storage.prototype.removeItem = function (key) {
          const isLocal = this === window.localStorage || this === localStorage;
          if (isLocal && protectedKeys.has(key) && Date.now() < protectUntil) {
            ESM_DIAG.log('Prototype removeItem blocked for key:', key);
            try { return NativeStorage.setItem.call(window.localStorage, key, desiredLocal.get(key)); } catch (_) { return; }
          }
          return originalProtoRemoveItem.call(this, key);
        };

        Storage.prototype.clear = function () {
          const isLocal = this === window.localStorage || this === localStorage;
          if (isLocal && Date.now() < protectUntil && protectedKeys.size) {
            ESM_DIAG.log('Prototype clear intercepted; preserving keys:', Array.from(protectedKeys));
            try {
              const len = window.localStorage.length;
              const toRemove = [];
              for (let i = 0; i < len; i++) {
                const k = NativeStorage.key.call(window.localStorage, i);
                if (k != null && !protectedKeys.has(k)) toRemove.push(k);
              }
              toRemove.forEach(k => { try { originalRemoveItem(k); } catch (_) {} });
              return;
            } catch (_) { /* fallback */ }
          }
          return originalProtoClear.call(this);
        };

        ESM_DIAG.log('Protection overrides applied for keys:', Array.from(protectedKeys), 'until', new Date(protectUntil).toISOString());
      } catch (ex) {
        ESM_DIAG.error('Error applying protection overrides:', ex);
      }

      scheduleRepairs([500, 2000, 5000, 10000, 20000, 30000, 45000, 60000, 90000, 120000]);
    } catch (ex) {
      ESM_DIAG.error('Error parsing stash or scheduling repairs:', ex);
    }
  }

  // Export key functions to window for fallback usage
  if (typeof window !== 'undefined') {
    window.exportLocalStorage = exportLocalStorage;
    window.importLocalStorage = importLocalStorage;
  }

  // Properly close IIFE
  })();

  // Duplicate reapplyAfterReload removed; the IIFE version is used.

  async function writeIndexedDBRecord(record) {
    return new Promise((resolve, reject) => {
      const openReq = indexedDB.open(record.db);
      openReq.onerror = () => reject(openReq.error);
      openReq.onupgradeneeded = () => { /* no-op */ };
      openReq.onsuccess = () => {
        const db = openReq.result;
        const proceedWrite = () => {
          try {
            const tx = db.transaction([record.store], 'readwrite');
            const st = tx.objectStore(record.store);
            let req;
            if (st.keyPath) {
              req = st.put(record.value);
            } else {
              req = st.put(record.value, record.key);
            }
            req.onerror = () => reject(req.error);
            tx.oncomplete = () => { db.close(); resolve(true); };
            tx.onerror = () => reject(tx.error);
          } catch (err) {
            reject(err);
          }
        };
        if (!db.objectStoreNames.contains(record.store)) {
          db.close();
          const bump = indexedDB.open(record.db, (db.version || 1) + 1);
          bump.onerror = () => reject(bump.error);
          bump.onupgradeneeded = (evt) => {
            const db2 = evt.target.result;
            if (!db2.objectStoreNames.contains(record.store)) {
              const opts = {};
              if (record.keyPath) opts.keyPath = record.keyPath;
              if (record.autoIncrement) opts.autoIncrement = true;
              const newStore = db2.createObjectStore(record.store, opts);
              if (Array.isArray(record.indexes)) {
                record.indexes.forEach(ix => {
                  try {
                    newStore.createIndex(ix.name, ix.keyPath, { unique: !!ix.unique, multiEntry: !!ix.multiEntry });
                  } catch (e) { /* ignore invalid index definitions */ }
                });
              }
            }
          };
          bump.onsuccess = () => {
            const db2 = bump.result;
            try {
              const tx = db2.transaction([record.store], 'readwrite');
              const st = tx.objectStore(record.store);
              let req;
              if (st.keyPath) {
                req = st.put(record.value);
              } else {
                req = st.put(record.value, record.key);
              }
              req.onerror = () => reject(req.error);
              tx.oncomplete = () => { db2.close(); resolve(true); };
              tx.onerror = () => reject(tx.error);
            } catch (err) {
              reject(err);
            }
          };
        } else {
          proceedWrite();
        }
      };
    });
  }

  // Duplicate applyPairs removed; using the in-IIFE implementation.
  let uiMounted = false;
  function ensureDOMReady(cb) {
    if (document.readyState === 'interactive' || document.readyState === 'complete') {
      try { cb(); } catch (e) { if (window.ESM_DIAG) window.ESM_DIAG.error('ensureDOMReady callback error', e); }
    } else {
      document.addEventListener('DOMContentLoaded', () => {
        try { cb(); } catch (e) { if (window.ESM_DIAG) window.ESM_DIAG.error('ensureDOMReady DOMContentLoaded error', e); }
      }, { once: true });
    }
  }

  function createFallbackPanel() {
    ensureDOMReady(() => {
      if (window.uiMounted) return;
      const panel = document.createElement('div');
      panel.id = 'esm-fallback-panel';
      panel.style.position = 'fixed';
      panel.style.top = '72px';
      panel.style.right = '12px';
      panel.style.zIndex = '999999';
      panel.style.background = 'rgba(30, 41, 59, 0.93)';
      panel.style.backdropFilter = 'blur(4px)';
      panel.style.border = '1px solid rgba(255,255,255,0.12)';
      panel.style.borderRadius = '12px';
      panel.style.padding = '12px 14px';
      panel.style.boxShadow = '0 6px 18px rgba(0,0,0,0.35)';
      panel.style.color = '#eaeef5';
      panel.style.maxWidth = '420px';
      panel.style.fontFamily = 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif';

      const title = document.createElement('div');
    title.textContent = t('scriptTitle');
      title.style.fontWeight = '700';
      title.style.marginBottom = '6px';
      title.style.letterSpacing = '0.2px';
      panel.appendChild(title);

      const desc = document.createElement('div');
    desc.textContent = t('fallbackDesc');
      desc.style.fontSize = '12px';
      desc.style.opacity = '0.9';
      desc.style.marginBottom = '8px';
      panel.appendChild(desc);

      const btnRow = document.createElement('div');
      btnRow.className = 'btnRow';
      btnRow.style.display = 'grid';
      btnRow.style.gridTemplateColumns = '1fr 1fr 1fr 1fr';
      btnRow.style.gap = '8px';
      btnRow.style.marginBottom = '10px';
      btnRow.style.alignItems = 'stretch';
      btnRow.style.width = '100%';
      btnRow.style.boxSizing = 'border-box';
      const importBtn = document.createElement('button');
      importBtn.id = 'esm-import-btn';
    importBtn.textContent = t('importBackup');
    importBtn.title = t('importBackupTitle');
      importBtn.style.backgroundImage = 'linear-gradient(180deg, #2196f3, #1976d2)';
      importBtn.style.color = '#fff';
      importBtn.style.border = 'none';
      importBtn.style.borderRadius = '10px';
      importBtn.style.padding = '8px 12px';
      importBtn.style.fontWeight = '600';
      importBtn.style.cursor = 'pointer';
      importBtn.style.boxShadow = '0 3px 8px rgba(25, 118, 210, 0.35)';
      importBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      importBtn.style.whiteSpace = 'nowrap';
      importBtn.style.display = 'inline-flex';
      importBtn.style.alignItems = 'center';
      importBtn.style.gap = '4px';
      importBtn.style.fontSize = '13px';
      importBtn.style.lineHeight = '18px';
      importBtn.style.width = '100%';
      importBtn.style.flex = '1 1 0';
      importBtn.style.minWidth = '0';
      importBtn.style.margin = '0';
      importBtn.addEventListener('mouseenter', () => { importBtn.style.filter = 'brightness(1.08)'; importBtn.style.boxShadow = '0 6px 14px rgba(25,118,210,0.45)'; });
      importBtn.addEventListener('mouseleave', () => { importBtn.style.filter = ''; importBtn.style.boxShadow = '0 3px 8px rgba(25,118,210,0.35)'; });
      importBtn.addEventListener('click', function() {
        if (typeof window.importLocalStorage === 'function') {
          window.importLocalStorage();
        } else {
          try { if (window.ESM_DIAG) window.ESM_DIAG.error('Import function not available'); } catch (_) {}
          alert(t('importFunctionUnavailable'));
        }
      });

      const exportBtn = document.createElement('button');
      exportBtn.id = 'esm-export-btn';
    exportBtn.textContent = t('exportBackup');
    exportBtn.title = t('exportBackupTitle');
      exportBtn.style.backgroundImage = 'linear-gradient(180deg, #43a047, #2e7d32)';
      exportBtn.style.color = '#fff';
      exportBtn.style.border = 'none';
      exportBtn.style.borderRadius = '10px';
      exportBtn.style.padding = '8px 12px';
      exportBtn.style.fontWeight = '600';
      exportBtn.style.cursor = 'pointer';
      exportBtn.style.boxShadow = '0 3px 8px rgba(46, 125, 50, 0.35)';
      exportBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      exportBtn.style.whiteSpace = 'nowrap';
      exportBtn.style.display = 'inline-flex';
      exportBtn.style.alignItems = 'center';
      exportBtn.style.gap = '4px';
      exportBtn.style.fontSize = '13px';
      exportBtn.style.lineHeight = '18px';
      exportBtn.style.width = '100%';
      exportBtn.style.flex = '1 1 0';
      exportBtn.style.minWidth = '0';
      exportBtn.style.margin = '0';
      exportBtn.addEventListener('mouseenter', () => { exportBtn.style.filter = 'brightness(1.08)'; exportBtn.style.boxShadow = '0 6px 14px rgba(46,125,50,0.45)'; });
      exportBtn.addEventListener('mouseleave', () => { exportBtn.style.filter = ''; exportBtn.style.boxShadow = '0 3px 8px rgba(46,125,50,0.35)'; });
      exportBtn.addEventListener('click', function() {
        if (typeof window.exportLocalStorage === 'function') {
          window.exportLocalStorage();
        } else {
          try { if (window.ESM_DIAG) window.ESM_DIAG.error('Export function not available'); } catch (_) {}
          alert(t('exportFunctionUnavailable'));
        }
      });
      // Reihenfolge im Grid-Container: alle Buttons gleichberechtigt
      exportBtn.style.order = '1';
      importBtn.style.order = '2';
      driveBackupBtn.style.order = '3';
      driveRestoreBtn.style.order = '4';
      // Dropbox Backup Button
      const driveBackupBtn = document.createElement('button');
      driveBackupBtn.id = 'esm-drive-backup-btn';
    driveBackupBtn.textContent = t('cloudBackup');
    driveBackupBtn.title = t('cloudBackupTitle');
      driveBackupBtn.style.backgroundImage = 'linear-gradient(180deg, #4285f4, #1a73e8)';
      driveBackupBtn.style.color = '#fff';
      driveBackupBtn.style.border = 'none';
      driveBackupBtn.style.borderRadius = '10px';
      driveBackupBtn.style.padding = '8px 12px';
      driveBackupBtn.style.fontWeight = '600';
      driveBackupBtn.style.cursor = 'pointer';
      driveBackupBtn.style.boxShadow = '0 3px 8px rgba(26, 115, 232, 0.35)';
      driveBackupBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      driveBackupBtn.style.whiteSpace = 'nowrap';
      driveBackupBtn.style.display = 'inline-flex';
      driveBackupBtn.style.alignItems = 'center';
      driveBackupBtn.style.gap = '4px';
      driveBackupBtn.style.fontSize = '13px';
      driveBackupBtn.style.lineHeight = '18px';
      driveBackupBtn.style.width = '100%';
      driveBackupBtn.style.flex = '1 1 0';
      driveBackupBtn.style.minWidth = '0';
      driveBackupBtn.style.margin = '0';
      driveBackupBtn.addEventListener('mouseenter', () => { driveBackupBtn.style.filter = 'brightness(1.08)'; driveBackupBtn.style.boxShadow = '0 6px 14px rgba(26,115,232,0.45)'; });
      driveBackupBtn.addEventListener('mouseleave', () => { driveBackupBtn.style.filter = ''; driveBackupBtn.style.boxShadow = '0 3px 8px rgba(26,115,232,0.35)'; });
      driveBackupBtn.addEventListener('click', function() {
        if (typeof window.exportToDropbox === 'function') {
          window.exportToDropbox();
        } else {
          try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox backup function not available'); } catch (_) {}
          alert(t('dropboxExportUnavailable'));
        }
      });

      // Dropbox Restore Button
      const driveRestoreBtn = document.createElement('button');
      driveRestoreBtn.id = 'esm-drive-restore-btn';
    driveRestoreBtn.textContent = t('cloudRestore');
    driveRestoreBtn.title = t('cloudRestoreTitle');
      driveRestoreBtn.style.backgroundImage = 'linear-gradient(180deg, #34a853, #137333)';
      driveRestoreBtn.style.color = '#fff';
      driveRestoreBtn.style.border = 'none';
      driveRestoreBtn.style.borderRadius = '10px';
      driveRestoreBtn.style.padding = '8px 12px';
      driveRestoreBtn.style.fontWeight = '600';
      driveRestoreBtn.style.cursor = 'pointer';
      driveRestoreBtn.style.boxShadow = '0 3px 8px rgba(19, 115, 51, 0.35)';
      driveRestoreBtn.style.transition = 'transform 80ms ease, box-shadow 200ms ease, filter 200ms ease';
      driveRestoreBtn.style.whiteSpace = 'nowrap';
      driveRestoreBtn.style.display = 'inline-flex';
      driveRestoreBtn.style.alignItems = 'center';
      driveRestoreBtn.style.gap = '4px';
      driveRestoreBtn.style.fontSize = '13px';
      driveRestoreBtn.style.lineHeight = '18px';
      driveRestoreBtn.style.width = '100%';
      driveRestoreBtn.style.flex = '1 1 0';
      driveRestoreBtn.style.minWidth = '0';
      driveRestoreBtn.style.margin = '0';
      driveRestoreBtn.addEventListener('mouseenter', () => { driveRestoreBtn.style.filter = 'brightness(1.08)'; driveRestoreBtn.style.boxShadow = '0 6px 14px rgba(19,115,51,0.45)'; });
      driveRestoreBtn.addEventListener('mouseleave', () => { driveRestoreBtn.style.filter = ''; driveRestoreBtn.style.boxShadow = '0 3px 8px rgba(19,115,51,0.35)'; });
      driveRestoreBtn.addEventListener('click', function() {
        if (typeof window.importFromDropbox === 'function') {
          window.importFromDropbox();
        } else {
          try { if (window.ESM_DIAG) window.ESM_DIAG.error('Dropbox restore function not available'); } catch (_) {}
          alert(t('dropboxImportUnavailable'));
        }
      });

      btnRow.appendChild(exportBtn);
      btnRow.appendChild(importBtn);
      btnRow.appendChild(driveBackupBtn);
      btnRow.appendChild(driveRestoreBtn);
      panel.appendChild(btnRow);

      const container = document.createElement('div');
      container.id = 'key-list-container';
      container.style.background = 'rgba(255,255,255,0.05)';
      container.style.border = '1px solid rgba(255,255,255,0.10)';
      container.style.borderRadius = '10px';
      container.style.padding = '10px';
      container.style.maxHeight = '320px';
      container.style.overflow = 'auto';
      panel.appendChild(container);

      document.body.appendChild(panel);
      window.uiMounted = true;
      if (typeof window !== 'undefined' && window.ESM_DIAG) window.ESM_DIAG.log('UI mounted via floating fallback panel');
    });
  }

  // Duplicate global addScriptTab removed; using the IIFE-scoped implementation exclusively.
  // UI mounting is managed inside the IIFE via initialize(); removing duplicate global function to avoid scope issues.
  if (typeof window !== 'undefined') {
    if (typeof exportLocalStorage === 'function') {
      window.exportLocalStorage = exportLocalStorage;
    }
    if (typeof importLocalStorage === 'function') {
      window.importLocalStorage = importLocalStorage;
    }
    if (typeof exportToDropbox === 'function') {
      window.exportToDropbox = exportToDropbox;
    }
    if (typeof importFromDropbox === 'function') {
      window.importFromDropbox = importFromDropbox;
    }
  }