Files
Hellion-NewTab/docs/superpowers/plans/2026-04-16-hardening-v2.0.1.md
JonKazama-Hellion 3de1dd3b8b docs(plan): Hardening v2.0.1 Implementierungsplan erstellen
9 Tasks, 16 Dateien, Foundation-First-Strategie.
Event-System → Widget-Migration → Security → Favicons → i18n → Quality → Release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:29:14 +02:00

30 KiB

Hellion NewTab v2.0.1 Hardening — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Harden v2.0.0 with security fixes, widget event-system refactoring, i18n completeness, and code quality improvements.

Architecture: Foundation-First — build the new widget event system first, then migrate widget modules onto it, then layer security, i18n, and quality fixes. Each task touches isolated files to avoid merge conflicts.

Tech Stack: Vanilla JavaScript ES2020, CSS Custom Properties, Browser Extension Manifest V3, no build step, no npm.

Spec: docs/superpowers/specs/2026-04-16-hardening-v2.0.1-design.md

Testing: No automated test framework. Each task includes manual browser-based verification steps. Load the extension in Chrome (chrome://extensions → Developer mode → Load unpacked) after each task.


File Map

File Tasks Changes
src/js/widgets.js 1, 2 Add event system (_emitter, on, off), dispatch events in close/minimize/openWidget, replace setTimeout with transitionend
src/js/calculator.js 3 Replace monkey-patching (L692-728) with WidgetManager.on() listeners
src/js/timer.js 3 Replace monkey-patching (L723-758) with WidgetManager.on() listeners
src/js/image-ref.js 3 Replace monkey-patching (L463-498) with WidgetManager.on() listeners
src/js/settings.js 4 Add isValidBgUrl(), validate in applySettings() and file upload + URL input handlers
src/js/data.js 5 Add isSafeUrl(), immutable mapping, string length limits, Notes import via Notes.init()
src/js/state.js 6 Remove getFaviconUrl()
src/js/boards.js 6 Replace <img> favicon with local letter-div
src/css/main.css 6, 7 Replace .bm-favicon/.bm-favicon-fallback with .bm-favicon-local, add @supports not fallback, add --bg-solid-fallback per theme
newtab.html 8 Add 5x data-i18n-title, 3x data-i18n
src/js/i18n.js 8 Add 10 new keys to STRINGS.de and STRINGS.en (8 i18n + 2 bgUrl validation)
src/js/app.js 9 Store setInterval ID in variable
manifest.json 9 Version bump to 2.0.1
manifest.firefox.json 9 Version bump to 2.0.1
manifest.opera.json 9 Version bump to 2.0.1
CHANGELOG.md 9 Add v2.0.1 entry

Task 1: Widget Event-System in WidgetManager

Files:

  • Modify: src/js/widgets.js:6-10 (add emitter + on/off)

  • Modify: src/js/widgets.js:143-148 (close — dispatch event)

  • Step 1: Add event emitter and on/off methods to WidgetManager

In src/js/widgets.js, add three new properties after STORAGE_KEY: 'widgetStates', (line 10):

  /** @type {EventTarget} Internes Event-System fuer Widget-Lifecycle */
  _emitter: new EventTarget(),

  /**
   * Event-Listener registrieren
   * @param {string} event - z.B. 'widget:close', 'widget:minimize', 'widget:open'
   * @param {Function} handler
   */
  on(event, handler) {
    this._emitter.addEventListener(event, handler);
  },

  /**
   * Event-Listener entfernen
   * @param {string} event
   * @param {Function} handler
   */
  off(event, handler) {
    this._emitter.removeEventListener(event, handler);
  },
  • Step 2: Dispatch widget:close event in close()

Replace the close method (lines 143-148):

  close(id) {
    const entry = this._widgets.get(id);
    if (!entry) return;
    entry.el.remove();
    this._widgets.delete(id);
    this._emitter.dispatchEvent(new CustomEvent('widget:close', { detail: { id } }));
  },

Note: The event fires AFTER el.remove() and _widgets.delete(). Listeners must not access the widget entry.

  • Step 3: Verify event system loads without errors

Reload the extension in the browser. Open the console (F12). Verify:

  • No JavaScript errors on load

  • WidgetManager.on is a function (type WidgetManager.on in console)

  • WidgetManager._emitter is an EventTarget

  • Step 4: Commit

git add src/js/widgets.js
git commit -m "refactor(widgets): add EventTarget-based lifecycle event system

Add _emitter, on(), off() to WidgetManager. Dispatch widget:close event
after close(). Foundation for removing monkey-patching from widget modules."

Task 2: Minimize with transitionend + openWidget event dispatch

Files:

  • Modify: src/js/widgets.js:154-163 (minimize)

  • Modify: src/js/widgets.js:169-180 (openWidget)

  • Step 1: Replace setTimeout with transitionend in minimize()

Replace the minimize method (lines 154-163):

  async minimize(id) {
    const entry = this._widgets.get(id);
    if (!entry) return;
    entry.state.open = false;
    entry._minimizing = true;
    entry.el.classList.add('widget-minimized');

    entry.el.addEventListener('transitionend', function onEnd(e) {
      if (e.target !== entry.el) return;
      entry.el.removeEventListener('transitionend', onEnd);
      if (entry._minimizing) {
        entry.el.style.display = 'none';
      }
      entry._minimizing = false;
    });

    this._emitter.dispatchEvent(new CustomEvent('widget:minimize', { detail: { id } }));
    await this.save();
  },
  • Step 2: Add race-condition guard and event dispatch to openWidget()

Replace the openWidget method (lines 169-180):

  async openWidget(id) {
    const entry = this._widgets.get(id);
    if (!entry) return;
    entry._minimizing = false;
    entry.state.open = true;
    entry.el.style.display = 'flex';
    requestAnimationFrame(() => {
      entry.el.classList.remove('widget-minimized');
    });
    this.bringToFront(id);
    this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } }));
    await this.save();
  },

Key change: entry._minimizing = false cancels any in-flight minimize transition.

  • Step 3: Verify minimize/open animation works

Reload extension. Test:

  1. Create a note → minimize it → verify it fades out and disappears
  2. Click the note in the widget toolbar to reopen → verify it appears smoothly
  3. Rapid test: minimize → immediately reopen before animation ends → verify no display glitch (the race condition fix)
  • Step 4: Commit
git add src/js/widgets.js
git commit -m "fix(widgets): replace setTimeout with transitionend in minimize

Fixes race condition where openWidget() during the 250ms timeout would
be overridden. Uses _minimizing flag to cancel in-flight transitions.
Dispatches widget:minimize and widget:open events."

Task 3: Migrate Calculator, Timer, ImageRef to Event Listeners

Files:

  • Modify: src/js/calculator.js:692-728

  • Modify: src/js/timer.js:723-758

  • Modify: src/js/image-ref.js:463-498

  • Step 1: Replace monkey-patching in calculator.js

Replace lines 692-728 (the three monkey-patching blocks in init()) with:

    // Widget-Lifecycle-Events
    const self = this;
    WidgetManager.on('widget:close', (e) => {
      if (e.detail.id === self.WIDGET_ID) {
        self.onClose();
      }
    });

    WidgetManager.on('widget:minimize', (e) => {
      if (e.detail.id === self.WIDGET_ID) {
        self._isOpen = false;
        self.save();
      }
    });

    WidgetManager.on('widget:open', (e) => {
      if (e.detail.id === self.WIDGET_ID) {
        self._isOpen = true;
        const body = WidgetManager.getBody(self.WIDGET_ID);
        if (body && body.children.length === 0) {
          self.renderBody(body);
        }
        const entry = WidgetManager._widgets.get(self.WIDGET_ID);
        if (entry) self._bindKeyboard(entry.el);
        self.save();
      }
    });
  • Step 2: Replace monkey-patching in timer.js

Replace lines 723-758 (the three monkey-patching blocks in init()) with:

    // Widget-Lifecycle-Events
    const self = this;
    WidgetManager.on('widget:close', (e) => {
      if (e.detail.id === self.WIDGET_ID) {
        self.onClose();
      }
    });

    WidgetManager.on('widget:minimize', (e) => {
      if (e.detail.id === self.WIDGET_ID) {
        self._isOpen = false;
        self.save();
      }
    });

    WidgetManager.on('widget:open', (e) => {
      if (e.detail.id === self.WIDGET_ID) {
        self._isOpen = true;
        const body = WidgetManager.getBody(self.WIDGET_ID);
        if (body && body.children.length === 0) {
          self.renderBody(body);
        }
        const entry = WidgetManager._widgets.get(self.WIDGET_ID);
        if (entry) self._bindKeyboard(entry.el);
        self.save();
      }
    });
  • Step 3: Replace monkey-patching in image-ref.js

Replace lines 463-498 (the three monkey-patching blocks in init()) with:

    // Widget-Lifecycle-Events
    const self = this;
    WidgetManager.on('widget:close', (e) => {
      const isImage = self._images.some(img => img.id === e.detail.id);
      if (isImage) {
        self.onClose(e.detail.id);
      }
    });

    WidgetManager.on('widget:minimize', (e) => {
      const isImage = self._images.some(img => img.id === e.detail.id);
      if (isImage) {
        self.save();
      }
    });

    WidgetManager.on('widget:open', (e) => {
      const imgData = self._images.find(img => img.id === e.detail.id);
      if (imgData) {
        const body = WidgetManager.getBody(e.detail.id);
        if (body && body.children.length === 0) {
          const dataUrl = self._getSessionImage(e.detail.id);
          self.renderBody(imgData, body, dataUrl);
        }
        self.save();
      }
    });
  • Step 4: Verify all three widget types work

Reload extension. Test each widget type:

  1. Calculator: Open → type a calculation → minimize → reopen → verify history is still there → close → reopen from toolbar
  2. Timer: Open → set a time → minimize → reopen → verify time is preserved → close
  3. Image-Ref: Enable in Settings → open image widget → add an image → minimize → reopen → verify image displays → close

Check console for any errors during all operations.

  • Step 5: Commit
git add src/js/calculator.js src/js/timer.js src/js/image-ref.js
git commit -m "refactor(widgets): migrate Calculator, Timer, ImageRef to event listeners

Replace monkey-patching of WidgetManager.close/minimize/openWidget with
WidgetManager.on() event listeners. Eliminates 3-deep closure chain."

Task 4: Security — URL Validation in settings.js

Files:

  • Modify: src/js/settings.js:52-95 (applySettings)

  • Modify: src/js/settings.js:166-175 (btnApplyBg handler)

  • Modify: src/js/settings.js:181-194 (bgFileInput handler)

  • Step 1: Add isValidBgUrl() helper

Add this function at the top of settings.js, after the closeThemeModal() function (after line 24):

/**
 * Prueft ob eine Background-URL sicher fuer CSS-Einbettung ist.
 * Erlaubt nur blob: und data:image/ Protokolle (aus File Upload).
 * @param {string} url
 * @returns {boolean}
 */
function isValidBgUrl(url) {
  return typeof url === 'string' && url.length > 0 &&
    (url.startsWith('blob:') || url.startsWith('data:image/'));
}
  • Step 2: Add validation in applySettings()

Replace lines 92-94:

  if (settings.bgUrl) {
    document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`;
  }

With:

  if (settings.bgUrl && isValidBgUrl(settings.bgUrl)) {
    document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`;
  } else if (settings.bgUrl) {
    // Ungueltige URL im Storage — bereinigen
    settings.bgUrl = '';
  }
  • Step 3: Add validation in the URL-input handler (btnApplyBg)

Replace lines 169-175:

  document.getElementById('btnApplyBg').addEventListener('click', async () => {
    const url = document.getElementById('bgUrlInput').value.trim();
    settings.bgUrl = url;
    document.getElementById('bgLayer').style.backgroundImage = url ? `url('${url}')` : '';
    await saveSettings();
    document.getElementById('bgInputRow').classList.add('hidden');
  });

With:

  document.getElementById('btnApplyBg').addEventListener('click', async () => {
    const url = document.getElementById('bgUrlInput').value.trim();
    if (url && !isValidBgUrl(url)) {
      await HellionDialog.alert(t('settings.bg_invalid_url'), { type: 'danger', title: t('settings.bg_invalid_url.title') });
      return;
    }
    settings.bgUrl = url;
    document.getElementById('bgLayer').style.backgroundImage = url ? `url('${url}')` : '';
    await saveSettings();
    document.getElementById('bgInputRow').classList.add('hidden');
  });
  • Step 4: Verify the file upload handler is already safe

Read settings.js:181-194. The FileReader.readAsDataURL(file) produces a data:image/... string, which passes isValidBgUrl(). The handler at line 186 sets settings.bgUrl = ev.target.result — this is already valid output. No change needed here.

  • Step 5: Add i18n keys for the validation error dialog

These keys will be added in Task 8 together with all other i18n keys. For now, note that we need:

  • settings.bg_invalid_url — "Nur lokale Bilder (Upload) sind als Hintergrund erlaubt." / "Only local images (upload) are allowed as background."

  • settings.bg_invalid_url.title — "Ungültige URL" / "Invalid URL"

  • Step 6: Verify background upload still works

Reload extension. Test:

  1. Open Theme Modal → upload a local image → verify it displays as background
  2. Try entering javascript:alert(1) in the URL input → verify it's rejected with a dialog
  3. Reload → verify the uploaded background persists
  • Step 7: Commit
git add src/js/settings.js
git commit -m "fix(security): validate background URL before CSS injection

Add isValidBgUrl() that only allows blob: and data:image/ protocols.
Applied in applySettings() and the manual URL input handler.
Prevents CSS injection via manipulated bgUrl storage values."

Task 5: Security + Quality — Data Import Hardening

Files:

  • Modify: src/js/data.js:33-127

  • Step 1: Add isSafeUrl() helper at top of data.js

Add after the initDataButtons function declaration (after line 6, before the function body):

Actually, add it inside the function before the event listeners, right after if (!btnExport || !btnImport) return; (after line 10):

  /**
   * Prueft ob eine URL ein sicheres Protokoll hat.
   * Blockiert javascript:, data:, vbscript: etc.
   * @param {string} url
   * @returns {boolean}
   */
  function isSafeUrl(url) {
    try {
      const u = new URL(url);
      return ['http:', 'https:', 'ftp:'].includes(u.protocol);
    } catch {
      return false;
    }
  }
  • Step 2: Replace the mutable board/bookmark filter with immutable mapping

Replace lines 41-52 (the validBoards filter block):

      const validBoards = data.boards
        .filter(b => b && typeof b.title === 'string' && Array.isArray(b.bookmarks))
        .map(b => ({
          id: b.id || uid(),
          title: String(b.title).slice(0, 100),
          blurred: !!b.blurred,
          bookmarks: b.bookmarks
            .filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
            .map(bm => ({
              id: bm.id || uid(),
              title: String(bm.title).slice(0, 200),
              url: bm.url,
              desc: String(bm.desc || '').slice(0, 500)
            }))
        }));
  • Step 3: Replace the mutable notes filter with immutable mapping

Replace lines 68-71 (the importNotes filter):

        const importNotes = data.notes
          .filter(n => n && n.id && n.template)
          .map(n => ({
            id: n.id,
            template: ['note', 'checklist'].includes(n.template) ? n.template : 'note',
            title: String(n.title || '').slice(0, 200),
            content: String(n.content || '').slice(0, 5000),
            x: typeof n.x === 'number' ? n.x : 120,
            y: typeof n.y === 'number' ? n.y : 80,
            width: typeof n.width === 'number' ? n.width : 280,
            height: typeof n.height === 'number' ? n.height : 220,
            open: n.open !== false,
            checklistItems: Array.isArray(n.checklistItems) ? n.checklistItems : []
          }));
  • Step 4: Replace direct Notes._notes mutation with Notes.init()

Replace lines 76-81:

        if (toImport.length > 0) {
          const merged = [...existingNotes, ...toImport];
          existingWidgets.notes = merged;
          Notes._notes = merged;
          notesImported = toImport.length;
        }

With:

        if (toImport.length > 0) {
          const merged = [...existingNotes, ...toImport];
          existingWidgets.notes = merged;
          notesImported = toImport.length;
        }

Then after line 113 (await Store.set('widgetStates', existingWidgets);), add:

      // Widget-Module neu aus Storage laden (kein direkter Zugriff auf Internals)
      if (notesImported > 0) await Notes.init();
      if (calcImported) await Calculator.load();
      if (timerImported) await Timer.load();

And remove the direct mutations at lines 93 and 107:

  • Remove: Calculator._history = existingWidgets.calculator.history; (line 93)

  • Remove: Timer._presets = existingWidgets.timer.presets; (line 107)

  • Step 5: Verify import functionality

Reload extension. Test:

  1. Export current data as JSON
  2. Edit the exported JSON: add a bookmark with javascript:alert(1) URL → import → verify the bad bookmark is silently skipped
  3. Import a normal JSON backup → verify boards, notes, calculator history, timer presets all appear correctly
  4. Verify no console errors
  • Step 6: Commit
git add src/js/data.js
git commit -m "fix(security): harden JSON import with URL validation and immutable mapping

Add isSafeUrl() to block javascript:/data: URLs in imported bookmarks.
Replace mutable object mutation with immutable .map() and string length limits.
Use Notes.init()/Calculator.load()/Timer.load() instead of direct _notes/_history
mutation after import."

Task 6: Remove Google Favicons — Local Letter Icons

Files:

  • Modify: src/js/state.js:36-43 (remove getFaviconUrl)

  • Modify: src/js/boards.js:218-230 (replace favicon rendering)

  • Modify: src/css/main.css:565-571 (replace CSS classes)

  • Step 1: Remove getFaviconUrl() from state.js

Delete lines 36-43 in src/js/state.js:

function getFaviconUrl(url) {
  try {
    const u = new URL(url);
    return `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=16`;
  } catch {
    return '';
  }
}
  • Step 2: Replace favicon rendering in boards.js

Replace lines 218-230 in src/js/boards.js:

  const favicon = document.createElement('img');
  favicon.className = 'bm-favicon';
  favicon.width = 14;
  favicon.height = 14;
  favicon.src = getFaviconUrl(bm.url);
  favicon.addEventListener('error', function() {
    this.classList.add('hidden');
    this.nextElementSibling.classList.remove('hidden');
  });

  const fallback = document.createElement('div');
  fallback.className = 'bm-favicon-fallback hidden';
  fallback.textContent = bm.title.charAt(0).toUpperCase();

With:

  const favicon = document.createElement('div');
  favicon.className = 'bm-favicon-local';
  favicon.textContent = bm.title.charAt(0).toUpperCase();
  const hue = (bm.title.charCodeAt(0) * 137) % 360;
  favicon.style.backgroundColor = `hsl(${hue}, 45%, 35%)`;

Also update the appendChild calls below. The old code appends both favicon and fallback:

Find the line that appends the fallback (should be near line 243-244):

  li.append(favicon, fallback, textDiv, deleteBtn);

Replace with:

  li.append(favicon, textDiv, deleteBtn);
  • Step 3: Replace CSS classes in main.css

Replace lines 565-571:

.bm-favicon { width: 14px; height: 14px; flex-shrink: 0; border-radius: 2px; opacity: 0.85; }
.bm-favicon-fallback {
  width: 14px; height: 14px; flex-shrink: 0;
  background: var(--accent-dim); border-radius: 2px;
  display: flex; align-items: center; justify-content: center;
  font-size: 8px; color: var(--accent);
}

With:

.bm-favicon-local {
  width: 16px; height: 16px; flex-shrink: 0;
  border-radius: 3px;
  display: flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 600;
  color: #fff;
  line-height: 1;
}
  • Step 4: Verify favicons display correctly

Reload extension. Check:

  1. All bookmarks show a colored letter icon
  2. Different bookmark titles produce different colors
  3. The icons are aligned and properly sized in all themes
  4. No network requests to google.com in the Network tab (F12 → Network)
  5. No console errors about getFaviconUrl
  • Step 5: Commit
git add src/js/state.js src/js/boards.js src/css/main.css
git commit -m "feat(privacy): replace Google Favicons with local letter icons

Remove getFaviconUrl() and all external network requests. Bookmarks now
show a colored letter icon with deterministic hue based on title.
Eliminates privacy leak and Brave Shields compatibility issues."

Task 7: backdrop-filter Fallback for Brave Shields

Files:

  • Modify: src/css/main.css (add --bg-solid-fallback per theme + @supports not block)

  • Step 1: Add --bg-solid-fallback to each theme

Add the variable to each theme's [data-theme] block. The value is an opaque version of --bg-board:

Theme Line --bg-solid-fallback value
nebula ~82 #0a060e
crescent ~108 #0c0b08
event-horizon ~137 #06040f
merchantman ~163 #040d0d
julia-jin ~189 #080c12
sc-sunset ~216 #0e0808
hellion-hud ~245 #04080c
hellion-energy ~278 #040a08
satisfactory ~310 #060a0c
avorion ~341 #040c0a
hellion-stealth ~371 #060a0e

Add --bg-solid-fallback: <value>; as the last variable in each theme block.

  • Step 2: Add @supports not block at the end of the general layout section

Add after the existing board/widget styles, before the theme-specific sections (around line 75, before the first [data-theme] block):

/* Fallback fuer Browser die backdrop-filter blockieren (z.B. Brave Shields) */
@supports not (backdrop-filter: blur(1px)) {
  .board,
  .widget,
  .settings-panel,
  .dialog-box,
  .theme-modal,
  .search-bar {
    background-color: var(--bg-solid-fallback, var(--bg-primary));
  }
}
  • Step 3: Verify fallback works

Test in Brave with Shields set to aggressive. Or test by temporarily adding this CSS rule:

.board { backdrop-filter: none !important; }

Verify that boards still have a visible background (opaque, not transparent).

  • Step 4: Commit
git add src/css/main.css
git commit -m "fix(compat): add backdrop-filter fallback for Brave Shields

Add --bg-solid-fallback CSS variable to all 11 themes and a
@supports not (backdrop-filter) block. UI remains usable when
Brave Shields or strict fingerprinting settings block backdrop-filter."

Task 8: Complete i18n Coverage

Files:

  • Modify: newtab.html:26-42 (add data-i18n-title to 5 header buttons)

  • Modify: newtab.html:198, 215, 374 (add data-i18n to 3 setting buttons)

  • Modify: src/js/i18n.js (add 10 new keys — 8 from spec + 2 from Task 4)

  • Step 1: Add data-i18n-title to header buttons in newtab.html

Line 26 — change:

<button class="btn-icon" id="btnImport" title="Bookmarks importieren (HTML)">

To:

<button class="btn-icon" id="btnImport" title="Bookmarks importieren (HTML)" data-i18n-title="header.import_title">

Line 30 — change:

<button class="btn-icon" id="btnAddBoard" title="Neues Board hinzufügen">

To:

<button class="btn-icon" id="btnAddBoard" title="Neues Board hinzufügen" data-i18n-title="header.board_title">

Line 34 — change:

<button class="btn-icon" id="btnNote" title="Schnellnotiz">

To:

<button class="btn-icon" id="btnNote" title="Schnellnotiz" data-i18n-title="header.note_title">

Line 38 — change:

<button class="btn-icon" id="btnTheme" title="Darstellung & Theme">

To:

<button class="btn-icon" id="btnTheme" title="Darstellung & Theme" data-i18n-title="header.theme_title">

Line 42 — change:

<button class="btn-icon" id="btnSettings" title="Einstellungen">

To:

<button class="btn-icon" id="btnSettings" title="Einstellungen" data-i18n-title="header.settings_title">
  • Step 2: Add data-i18n to settings buttons in newtab.html

Line 198 — change:

<button class="btn-small" id="btnRestartOnboarding">Start</button>

To:

<button class="btn-small" id="btnRestartOnboarding" data-i18n="settings.onboarding_btn">Start</button>

Line 215 — change:

<button class="btn-danger" id="btnResetAll">Reset</button>

To:

<button class="btn-danger" id="btnResetAll" data-i18n="settings.reset_btn">Reset</button>

Line 374 — change:

<button class="btn-small" id="btnBgFile">Upload</button>

To:

<button class="btn-small" id="btnBgFile" data-i18n="settings.bg_upload_btn">Upload</button>
  • Step 3: Add new keys to STRINGS.de in i18n.js

Add these keys to the STRINGS.de object, in the appropriate sections:

In the Header section:

    'header.import_title':       'Bookmarks importieren (HTML)',
    'header.board_title':        'Neues Board hinzufügen',
    'header.note_title':         'Schnellnotiz',
    'header.theme_title':        'Darstellung & Theme',
    'header.settings_title':     'Einstellungen',

In the Settings section:

    'settings.onboarding_btn':   'Start',
    'settings.reset_btn':        'Reset',
    'settings.bg_upload_btn':    'Upload',
    'settings.bg_invalid_url':   'Nur lokale Bilder (Upload) sind als Hintergrund erlaubt.',
    'settings.bg_invalid_url.title': 'Ungültige URL',
  • Step 4: Add new keys to STRINGS.en in i18n.js

Add the matching English keys to STRINGS.en:

In the Header section:

    'header.import_title':       'Import bookmarks (HTML)',
    'header.board_title':        'Add new board',
    'header.note_title':         'Quick note',
    'header.theme_title':        'Appearance & Theme',
    'header.settings_title':     'Settings',

In the Settings section:

    'settings.onboarding_btn':   'Start',
    'settings.reset_btn':        'Reset',
    'settings.bg_upload_btn':    'Upload',
    'settings.bg_invalid_url':   'Only local images (upload) are allowed as background.',
    'settings.bg_invalid_url.title': 'Invalid URL',
  • Step 5: Verify translations

Reload extension. Test:

  1. Set language to English → hover over header buttons → verify English tooltips
  2. Set language to German → hover → verify German tooltips
  3. Open Settings → verify "Start", "Reset", "Upload" buttons have data-i18n attributes (inspect in DevTools)
  • Step 6: Commit
git add newtab.html src/js/i18n.js
git commit -m "fix(i18n): complete missing translations for toolbar tooltips and button texts

Add data-i18n-title to 5 header buttons, data-i18n to 3 settings buttons.
Add 10 new keys to STRINGS.de and STRINGS.en including background URL
validation error messages."

Task 9: Version Bump, Changelog, Clock Cleanup

Files:

  • Modify: src/js/app.js:135

  • Modify: manifest.json:5

  • Modify: manifest.firefox.json (version field)

  • Modify: manifest.opera.json (version field)

  • Modify: CHANGELOG.md

  • Step 1: Store clock interval ID in app.js

Replace line 135 in src/js/app.js:

  setInterval(tick, 1000);

With:

  const clockInterval = setInterval(tick, 1000);
  • Step 2: Bump version in all three manifests

In manifest.json, manifest.firefox.json, and manifest.opera.json, change:

"version": "2.0.0",

To:

"version": "2.0.1",
  • Step 3: Add CHANGELOG entry

Add this block at the top of CHANGELOG.md, after the header and before the v2.0.0 entry:

### v2.0.1 — 16.04.2026

#### Security

- **Background URL validation** — Only `blob:` and `data:image/` protocols allowed in CSS `backgroundImage` (prevents CSS injection via manipulated storage)
- **Import URL validation** — `javascript:`, `data:`, and other unsafe protocols are blocked during JSON import
- **Immutable import mapping** — Imported boards, bookmarks, and notes are sanitized with explicit field selection and string length limits

#### Fixed

- **Widget minimize race condition** — Replaced `setTimeout` with `transitionend` event; `openWidget()` during animation no longer causes display glitch
- **Notes import mutation** — Import now uses `Notes.init()` instead of directly setting `Notes._notes`
- **Complete i18n coverage** — 5 header button tooltips and 3 settings button texts now have `data-i18n` attributes (10 new translation keys)

#### Changed

- **Widget event system** — `WidgetManager` now dispatches `widget:close`, `widget:minimize`, `widget:open` CustomEvents via `EventTarget`. Calculator, Timer, and ImageRef use `WidgetManager.on()` instead of monkey-patching
- **Local favicon icons** — Replaced Google Favicons API with local colored letter icons (deterministic hue per title). Zero external network requests, Brave Shields compatible
- **backdrop-filter fallback** — `@supports not (backdrop-filter)` block with `--bg-solid-fallback` per theme for Brave Shields compatibility
- **Clock interval cleanup** — `setInterval` ID stored in variable

---

  • Step 4: Verify everything

Full manual test:

  1. Reload extension
  2. Verify version in chrome://extensions shows 2.0.1
  3. Open/close/minimize/reopen widgets of all types
  4. Switch language DE/EN — all tooltips translate
  5. Import/export JSON data
  6. Upload background image
  7. Check Network tab — zero external requests
  8. Check Console — zero errors
  • Step 5: Commit
git add src/js/app.js manifest.json manifest.firefox.json manifest.opera.json CHANGELOG.md
git commit -m "chore(release): bump version to v2.0.1 — hardening release

Security fixes, widget event system, local favicons, i18n completeness,
backdrop-filter fallback, code quality improvements. See CHANGELOG.md."