From 7cda3019c82b5dcd6dc302ef4e7f290ad0333a7e Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:07:04 +0200 Subject: [PATCH 01/13] 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. --- src/js/widgets.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/js/widgets.js b/src/js/widgets.js index 47ea24d..502e5f9 100644 --- a/src/js/widgets.js +++ b/src/js/widgets.js @@ -9,6 +9,27 @@ const WidgetManager = { _topZ: 100, STORAGE_KEY: 'widgetStates', + /** @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); + }, + /** * Widget erstellen und in DOM einfuegen * @param {string} type - 'note' @@ -145,6 +166,7 @@ const WidgetManager = { if (!entry) return; entry.el.remove(); this._widgets.delete(id); + this._emitter.dispatchEvent(new CustomEvent('widget:close', { detail: { id } })); }, /** From fde1fdd002b0bb84283bd0157014fa55c5c09f55 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:09:43 +0200 Subject: [PATCH 02/13] fix(widgets): dispatch close event before registry cleanup Move widget:close dispatch before _widgets.delete() so handlers can still query WidgetManager for the widget's state during the event. Co-Authored-By: Claude Opus 4.6 --- src/js/widgets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/widgets.js b/src/js/widgets.js index 502e5f9..301f194 100644 --- a/src/js/widgets.js +++ b/src/js/widgets.js @@ -165,8 +165,8 @@ const WidgetManager = { 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 } })); + this._widgets.delete(id); }, /** From b92ea5a1a45c1d5eb6851c9f1c6eb1d212946775 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:10:30 +0200 Subject: [PATCH 03/13] 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. --- src/js/widgets.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/js/widgets.js b/src/js/widgets.js index 301f194..36a9a14 100644 --- a/src/js/widgets.js +++ b/src/js/widgets.js @@ -177,10 +177,19 @@ const WidgetManager = { const entry = this._widgets.get(id); if (!entry) return; entry.state.open = false; + entry._minimizing = true; entry.el.classList.add('widget-minimized'); - setTimeout(() => { - entry.el.style.display = 'none'; - }, 250); + + 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(); }, @@ -191,13 +200,14 @@ const WidgetManager = { async openWidget(id) { const entry = this._widgets.get(id); if (!entry) return; + entry._minimizing = false; entry.state.open = true; entry.el.style.display = 'flex'; - // Naechster Frame fuer Animation requestAnimationFrame(() => { entry.el.classList.remove('widget-minimized'); }); this.bringToFront(id); + this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } })); await this.save(); }, From 30df93a4cc13ae9b838f5f2e3b8b661aadad4cf6 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:13:47 +0200 Subject: [PATCH 04/13] fix(widgets): harden minimize transitionend with fallback and property filter - Filter transitionend by propertyName=opacity to prevent double-fire - Add 350ms fallback setTimeout for prefers-reduced-motion / zero-duration - Initialize _minimizing: false in create() for clean state - Dispatch events after save() for consistent state in listeners Co-Authored-By: Claude Opus 4.6 --- src/js/widgets.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/js/widgets.js b/src/js/widgets.js index 36a9a14..b16ac56 100644 --- a/src/js/widgets.js +++ b/src/js/widgets.js @@ -52,7 +52,7 @@ const WidgetManager = { const el = this._buildDOM(state); document.body.appendChild(el); - this._widgets.set(id, { el, type, state }); + this._widgets.set(id, { el, type, state, _minimizing: false }); this._initDrag(el); this._initResize(el); this.bringToFront(id); @@ -170,7 +170,9 @@ const WidgetManager = { }, /** - * Widget minimieren (aus DOM verstecken, bleibt im Notebook) + * Widget minimieren (aus DOM verstecken, bleibt im Notebook). + * Nutzt transitionend statt setTimeout — _minimizing Flag verhindert Race Condition + * mit openWidget(). Fallback-Timer fuer prefers-reduced-motion / fehlende Transition. * @param {string} id */ async minimize(id) { @@ -180,17 +182,30 @@ const WidgetManager = { entry._minimizing = true; entry.el.classList.add('widget-minimized'); - entry.el.addEventListener('transitionend', function onEnd(e) { - if (e.target !== entry.el) return; + const MINIMIZE_FALLBACK_MS = 350; + + function onEnd(e) { + if (e.target !== entry.el || e.propertyName !== 'opacity') return; + clearTimeout(fallbackTimer); entry.el.removeEventListener('transitionend', onEnd); if (entry._minimizing) { entry.el.style.display = 'none'; } entry._minimizing = false; - }); + } + + entry.el.addEventListener('transitionend', onEnd); + + const fallbackTimer = setTimeout(() => { + entry.el.removeEventListener('transitionend', onEnd); + if (entry._minimizing) { + entry.el.style.display = 'none'; + entry._minimizing = false; + } + }, MINIMIZE_FALLBACK_MS); - this._emitter.dispatchEvent(new CustomEvent('widget:minimize', { detail: { id } })); await this.save(); + this._emitter.dispatchEvent(new CustomEvent('widget:minimize', { detail: { id } })); }, /** @@ -207,8 +222,8 @@ const WidgetManager = { entry.el.classList.remove('widget-minimized'); }); this.bringToFront(id); - this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } })); await this.save(); + this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } })); }, /** From 2430d65e3aeefed7fd95fd0004f9406e62d4357b Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:15:08 +0200 Subject: [PATCH 05/13] 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. --- src/js/calculator.js | 33 ++++++++++++--------------------- src/js/image-ref.js | 39 +++++++++++++++------------------------ src/js/timer.js | 33 ++++++++++++--------------------- 3 files changed, 39 insertions(+), 66 deletions(-) diff --git a/src/js/calculator.js b/src/js/calculator.js index b2f2f5b..f8090ed 100644 --- a/src/js/calculator.js +++ b/src/js/calculator.js @@ -689,41 +689,32 @@ const Calculator = { await this.open(); } - // Close-Event abfangen: WidgetManager.close() ueberschreiben - const origClose = WidgetManager.close.bind(WidgetManager); + // Widget-Lifecycle-Events const self = this; - WidgetManager.close = function(id) { - origClose(id); - if (id === self.WIDGET_ID) { + WidgetManager.on('widget:close', (e) => { + if (e.detail.id === self.WIDGET_ID) { self.onClose(); } - }; + }); - // Minimize-Event abfangen - const origMinimize = WidgetManager.minimize.bind(WidgetManager); - WidgetManager.minimize = async function(id) { - await origMinimize(id); - if (id === self.WIDGET_ID) { + WidgetManager.on('widget:minimize', (e) => { + if (e.detail.id === self.WIDGET_ID) { self._isOpen = false; - await self.save(); + self.save(); } - }; + }); - // Open-Event abfangen - const origOpen = WidgetManager.openWidget.bind(WidgetManager); - WidgetManager.openWidget = async function(id) { - await origOpen(id); - if (id === self.WIDGET_ID) { + WidgetManager.on('widget:open', (e) => { + if (e.detail.id === self.WIDGET_ID) { self._isOpen = true; - // Body neu rendern (war durch minimize entfernt) 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); - await self.save(); + self.save(); } - }; + }); } }; diff --git a/src/js/image-ref.js b/src/js/image-ref.js index 93a2211..2f52433 100644 --- a/src/js/image-ref.js +++ b/src/js/image-ref.js @@ -460,41 +460,32 @@ const ImageRef = { }); } - // Close-Event abfangen + // Widget-Lifecycle-Events const self = this; - const prevClose = WidgetManager.close; - WidgetManager.close = function(id) { - prevClose.call(WidgetManager, id); - // Pruefen ob es ein Image-Widget ist - const isImage = self._images.some(img => img.id === id); + WidgetManager.on('widget:close', (e) => { + const isImage = self._images.some(img => img.id === e.detail.id); if (isImage) { - self.onClose(id); + self.onClose(e.detail.id); } - }; + }); - // Minimize-Event abfangen - const prevMinimize = WidgetManager.minimize; - WidgetManager.minimize = async function(id) { - await prevMinimize.call(WidgetManager, id); - const isImage = self._images.some(img => img.id === id); + WidgetManager.on('widget:minimize', (e) => { + const isImage = self._images.some(img => img.id === e.detail.id); if (isImage) { - await self.save(); + self.save(); } - }; + }); - // Open-Event abfangen - const prevOpen = WidgetManager.openWidget; - WidgetManager.openWidget = async function(id) { - await prevOpen.call(WidgetManager, id); - const imgData = self._images.find(img => img.id === id); + WidgetManager.on('widget:open', (e) => { + const imgData = self._images.find(img => img.id === e.detail.id); if (imgData) { - const body = WidgetManager.getBody(id); + const body = WidgetManager.getBody(e.detail.id); if (body && body.children.length === 0) { - const dataUrl = self._getSessionImage(id); + const dataUrl = self._getSessionImage(e.detail.id); self.renderBody(imgData, body, dataUrl); } - await self.save(); + self.save(); } - }; + }); } }; diff --git a/src/js/timer.js b/src/js/timer.js index 34ab404..8cb1616 100644 --- a/src/js/timer.js +++ b/src/js/timer.js @@ -720,32 +720,23 @@ const Timer = { await this.open(); } - // Close-Event abfangen - const origClose = WidgetManager.close.bind(WidgetManager); + // Widget-Lifecycle-Events const self = this; - const prevClose = WidgetManager.close; - WidgetManager.close = function(id) { - prevClose.call(WidgetManager, id); - if (id === self.WIDGET_ID) { + WidgetManager.on('widget:close', (e) => { + if (e.detail.id === self.WIDGET_ID) { self.onClose(); } - }; + }); - // Minimize-Event abfangen - const prevMinimize = WidgetManager.minimize; - WidgetManager.minimize = async function(id) { - await prevMinimize.call(WidgetManager, id); - if (id === self.WIDGET_ID) { + WidgetManager.on('widget:minimize', (e) => { + if (e.detail.id === self.WIDGET_ID) { self._isOpen = false; - await self.save(); + self.save(); } - }; + }); - // Open-Event abfangen - const prevOpen = WidgetManager.openWidget; - WidgetManager.openWidget = async function(id) { - await prevOpen.call(WidgetManager, id); - if (id === self.WIDGET_ID) { + 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) { @@ -753,8 +744,8 @@ const Timer = { } const entry = WidgetManager._widgets.get(self.WIDGET_ID); if (entry) self._bindKeyboard(entry.el); - await self.save(); + self.save(); } - }; + }); } }; From 82dd6e026a8f639d27997161692b0439831adaba Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:18:42 +0200 Subject: [PATCH 06/13] 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. --- src/js/settings.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/js/settings.js b/src/js/settings.js index 29408e2..9a9d50d 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -23,6 +23,17 @@ function closeThemeModal() { overlay.classList.remove('active'); } +/** + * 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/')); +} + // ---- ACCORDION ---- function initAccordion() { const defaultOpen = new Set(['widgets']); @@ -89,8 +100,10 @@ function applySettings() { applyTheme(settings.theme || 'nebula', !!settings.bgUrl); - if (settings.bgUrl) { + if (settings.bgUrl && isValidBgUrl(settings.bgUrl)) { document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`; + } else if (settings.bgUrl) { + settings.bgUrl = ''; } } @@ -168,6 +181,10 @@ function bindSettingsEvents() { }); 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(); From a3e21a760f39954db406cf2bda7b1d07993a3d65 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:20:38 +0200 Subject: [PATCH 07/13] 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 internal mutation after import. --- src/js/data.js | 69 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/src/js/data.js b/src/js/data.js index 4ccfb83..46ff12c 100644 --- a/src/js/data.js +++ b/src/js/data.js @@ -9,6 +9,21 @@ function initDataButtons() { const jsonInput = document.getElementById('jsonImportInput'); if (!btnExport || !btnImport) return; + /** + * 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; + } + } + // Export (inkl. Notes) btnExport.addEventListener('click', async () => { const widgetData = await Store.get('widgetStates'); @@ -38,18 +53,21 @@ function initDataButtons() { try { const data = JSON.parse(await file.text()); if (!Array.isArray(data.boards)) throw new Error(t('data.invalid_format')); - const validBoards = data.boards.filter(b => { - if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false; - b.id = b.id || uid(); - b.blurred = !!b.blurred; - b.bookmarks = b.bookmarks.filter(bm => { - if (!bm || typeof bm.title !== 'string' || typeof bm.url !== 'string') return false; - bm.id = bm.id || uid(); - bm.desc = bm.desc || ''; - return true; - }); - return true; - }); + 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) + })) + })); if (validBoards.length === 0) throw new Error(t('data.no_boards')); const ok = await HellionDialog.confirm( t('data.import_confirm', { count: validBoards.length }), @@ -65,18 +83,26 @@ function initDataButtons() { const existingWidgets = await Store.get('widgetStates') || {}; if (Array.isArray(data.notes) && data.notes.length > 0) { const existingNotes = Array.isArray(existingWidgets.notes) ? existingWidgets.notes : []; - const importNotes = data.notes.filter(n => { - if (!n || !n.id || !n.template) return false; - n.checklistItems = Array.isArray(n.checklistItems) ? n.checklistItems : []; - return true; - }); + 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 : [] + })); // Limit beachten const spaceLeft = Notes.MAX_NOTES - existingNotes.length; const toImport = importNotes.slice(0, spaceLeft); if (toImport.length > 0) { const merged = [...existingNotes, ...toImport]; existingWidgets.notes = merged; - Notes._notes = merged; notesImported = toImport.length; } } @@ -90,7 +116,6 @@ function initDataButtons() { existingWidgets.calculator = { x: 400, y: 120, width: 280, height: 400, open: false, history: [] }; } existingWidgets.calculator.history = calcHistory.slice(0, Calculator.MAX_HISTORY); - Calculator._history = existingWidgets.calculator.history; calcImported = true; } } @@ -104,7 +129,6 @@ function initDataButtons() { existingWidgets.timer = { x: 600, y: 80, width: 260, height: 360, open: false, presets: [] }; } existingWidgets.timer.presets = validPresets.slice(0, Timer.MAX_PRESETS); - Timer._presets = existingWidgets.timer.presets; timerImported = true; } } @@ -112,6 +136,11 @@ function initDataButtons() { // Gemeinsam speichern await Store.set('widgetStates', existingWidgets); + // 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(); + const noteMsg = notesImported > 0 ? t('data.notes_suffix', { count: notesImported }) : ''; const calcMsg = calcImported ? t('data.calc_suffix') : ''; const timerMsg = timerImported ? t('data.timer_suffix') : ''; From 6704f4c9553c939f48759e222c69b2c36c3ea807 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:22:18 +0200 Subject: [PATCH 08/13] 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. --- src/css/main.css | 11 ++++++----- src/js/boards.js | 19 +++++-------------- src/js/state.js | 9 --------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/css/main.css b/src/css/main.css index 33f0f3f..419569d 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -562,12 +562,13 @@ html, body { body.compact .bm-item { padding: var(--spacing-compact) 10px; } -.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; +.bm-favicon-local { + width: 16px; height: 16px; flex-shrink: 0; + border-radius: 3px; display: flex; align-items: center; justify-content: center; - font-size: 8px; color: var(--accent); + font-size: 9px; font-weight: 600; + color: #fff; + line-height: 1; } .bm-text { flex: 1; min-width: 0; } .bm-title { font-size: 12px; font-weight: 400; color: var(--text-primary); line-height: 1.3; } diff --git a/src/js/boards.js b/src/js/boards.js index 7e7d5fc..44aaa82 100644 --- a/src/js/boards.js +++ b/src/js/boards.js @@ -215,19 +215,11 @@ function createBmEl(bm) { li.dataset.bmUrl = bm.url; li.draggable = true; - 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(); + 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%)`; const textDiv = document.createElement('div'); textDiv.className = 'bm-text'; @@ -247,7 +239,6 @@ function createBmEl(bm) { deleteBtn.textContent = '✕'; li.appendChild(favicon); - li.appendChild(fallback); li.appendChild(textDiv); li.appendChild(deleteBtn); diff --git a/src/js/state.js b/src/js/state.js index d387e47..d2db23e 100644 --- a/src/js/state.js +++ b/src/js/state.js @@ -33,15 +33,6 @@ function escHtml(str) { .replace(/"/g, '"'); } -function getFaviconUrl(url) { - try { - const u = new URL(url); - return `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=16`; - } catch { - return ''; - } -} - function getDefaultBoards() { return [ { From b6d347cd15864e6eea5b3c2fee24561cc6fce4f8 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:25:10 +0200 Subject: [PATCH 09/13] 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. --- src/css/main.css | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/css/main.css b/src/css/main.css index 419569d..9f0045d 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -68,6 +68,19 @@ --board-hover-border: rgba(179,89,255,0.18); --toggle-on-bg: rgba(214,92,255,0.22); --logo-shadow: rgba(179,89,255,0.35); + --bg-solid-fallback: #0a060e; +} + +/* 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)); + } } /* ============================================ @@ -91,6 +104,7 @@ --board-hover-border: rgba(179,89,255,0.18); --toggle-on-bg: rgba(214,92,255,0.22); --logo-shadow: rgba(179,89,255,0.35); + --bg-solid-fallback: #0a060e; } [data-theme="nebula"] .board { border-color: rgba(214,92,255,0.10); } [data-theme="nebula"] .bm-item:hover { background: rgba(214,92,255,0.05); } @@ -116,6 +130,7 @@ --board-hover-border: rgba(212, 189, 138, 0.20); --toggle-on-bg: rgba(200,168,74,0.22); --logo-shadow: rgba(212, 189, 138, 0.40); + --bg-solid-fallback: #080c16; letter-spacing: 0.5px; } @@ -146,6 +161,7 @@ --board-hover-border: rgba(157, 92, 255, 0.22); --toggle-on-bg: rgba(224,128,48,0.22); --logo-shadow: rgba(157, 92, 255, 0.45); + --bg-solid-fallback: #08050f; } [data-theme="event-horizon"] .board { border-color: rgba(157, 92, 255, 0.15); } [data-theme="event-horizon"] .bm-item:hover { background: rgba(157, 92, 255, 0.08); } @@ -172,6 +188,7 @@ --board-hover-border: rgba(46, 184, 184, 0.20); --toggle-on-bg: rgba(78,207,207,0.22); --logo-shadow: rgba(46, 184, 184, 0.45); + --bg-solid-fallback: #060a0a; } [data-theme="merchantman"] .board { border-color: rgba(46, 184, 184, 0.12); } [data-theme="merchantman"] .bm-item:hover { background: rgba(46, 184, 184, 0.06); } @@ -197,6 +214,7 @@ --board-hover-border: rgba(125, 179, 255, 0.22); --toggle-on-bg: rgba(91,159,255,0.22); --logo-shadow: rgba(125, 179, 255, 0.50); + --bg-solid-fallback: #070a14; } [data-theme="julia-jin"] .logo { font-family: 'Cinzel', serif; letter-spacing: 5px; text-transform: uppercase; } [data-theme="julia-jin"] .clock { font-family: 'Cinzel', serif; font-weight: 600; } @@ -225,6 +243,7 @@ --board-hover-border: rgba(255, 140, 61, 0.22); --toggle-on-bg: rgba(240,124,48,0.22); --logo-shadow: rgba(255, 140, 61, 0.45); + --bg-solid-fallback: #0f0a08; } [data-theme="sc-sunset"] .board { border-color: rgba(255, 140, 61, 0.15); @@ -253,6 +272,7 @@ --board-hover-border: rgba(50, 255, 106, 0.20); --toggle-on-bg: rgba(34,204,68,0.20); --logo-shadow: rgba(50, 255, 106, 0.40); + --bg-solid-fallback: #050805; --danger: #ff4d4d; } [data-theme="hellion-hud"] .board { @@ -287,6 +307,7 @@ --board-hover-border: rgba(30, 255, 142, 0.25); --toggle-on-bg: rgba(0,232,122,0.18); --logo-shadow: rgba(30, 255, 142, 0.60); + --bg-solid-fallback: #040705; } [data-theme="hellion-energy"] .board { border-color: rgba(30, 255, 142, 0.15); @@ -322,6 +343,7 @@ --board-hover-border: rgba(0, 180, 216, 0.25); --toggle-on-bg: rgba(0, 180, 216, 0.20); --logo-shadow: rgba(0, 180, 216, 0.40); + --bg-solid-fallback: #1a0f08; } [data-theme="satisfactory"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; } [data-theme="satisfactory"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); } @@ -352,6 +374,7 @@ --board-hover-border: rgba(46, 196, 160, 0.22); --toggle-on-bg: rgba(46, 196, 160, 0.18); --logo-shadow: rgba(46, 196, 160, 0.50); + --bg-solid-fallback: #020d0c; } [data-theme="avorion"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 500; letter-spacing: 6px; text-transform: uppercase; } [data-theme="avorion"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 400; color: var(--accent); } @@ -382,6 +405,7 @@ --board-hover-border: rgba(94, 194, 255, 0.25); --toggle-on-bg: rgba(94, 194, 255, 0.20); --logo-shadow: rgba(94, 194, 255, 0.45); + --bg-solid-fallback: #0d0f12; } [data-theme="hellion-stealth"] .logo { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 4px; } [data-theme="hellion-stealth"] .clock { font-family: 'Rajdhani', sans-serif; font-weight: 600; color: var(--accent); } From 02cdee76a81c58586deff72fbf164ba49872fb82 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:27:17 +0200 Subject: [PATCH 10/13] 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. --- newtab.html | 16 ++++++++-------- src/js/i18n.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/newtab.html b/newtab.html index bdc7daf..ee988e6 100644 --- a/newtab.html +++ b/newtab.html @@ -23,23 +23,23 @@
- - - - - @@ -195,7 +195,7 @@ Onboarding wiederholen Willkommens-Tour erneut anzeigen
- + @@ -212,7 +212,7 @@ Alles zurücksetzen Löscht alle Boards, Notes und Einstellungen - + @@ -371,7 +371,7 @@ Datei hochladen Lokales Bild als Hintergrund verwenden - + diff --git a/src/js/i18n.js b/src/js/i18n.js index 42486ea..313c299 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -202,6 +202,13 @@ const STRINGS = { 'header.theme': 'Darstellung', 'header.settings': 'Einstellungen', + // Header Tooltips + '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', + // Settings-Panel Überschrift 'settings.title': 'Einstellungen', @@ -255,6 +262,13 @@ const STRINGS = { 'settings.bg_upload.desc': 'Lokales Bild als Hintergrund verwenden', 'settings.search_engine_toggle': 'Suchmaschine wechseln', + // Settings Buttons + Validierung + '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', + // Modals 'modal.new_board': 'Neues Board', 'modal.board_name': 'Board-Name...', @@ -493,6 +507,13 @@ const STRINGS = { 'header.theme': 'Theme', 'header.settings': 'Settings', + // Header Tooltips + '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', + // Settings panel heading 'settings.title': 'Settings', @@ -546,6 +567,13 @@ const STRINGS = { 'settings.bg_upload.desc': 'Use a local image as background', 'settings.search_engine_toggle': 'Switch search engine', + // Settings Buttons + Validation + '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', + // Modals 'modal.new_board': 'New Board', 'modal.board_name': 'Board name...', From 536e0771a4efe5eba847ac748b9c03c4d01b8bf7 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:28:22 +0200 Subject: [PATCH 11/13] =?UTF-8?q?chore(release):=20bump=20version=20to=20v?= =?UTF-8?q?2.0.1=20=E2=80=94=20hardening=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security fixes, widget event system, local favicons, i18n completeness, backdrop-filter fallback, code quality improvements. See CHANGELOG.md. --- CHANGELOG.md | 23 +++++++++++++++++++++++ manifest.firefox.json | 2 +- manifest.json | 2 +- manifest.opera.json | 2 +- src/js/app.js | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79db7e6..9f142e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,29 @@ All notable changes per version. Format based on [Keep a Changelog](https://keep --- +### 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 + +--- + ### v2.0.0 — 22.03.2026 #### New Features diff --git a/manifest.firefox.json b/manifest.firefox.json index 1bcae52..af05674 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "default_locale": "en", - "version": "2.0.0", + "version": "2.0.1", "description": "__MSG_extDesc__", "author": "Hellion Online Media - Florian Wathling", "homepage_url": "https://hellion-media.de", diff --git a/manifest.json b/manifest.json index ed2dfeb..bb4466e 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "default_locale": "en", - "version": "2.0.0", + "version": "2.0.1", "description": "__MSG_extDesc__", "author": "Hellion Online Media - Florian Wathling", "homepage_url": "https://hellion-media.de", diff --git a/manifest.opera.json b/manifest.opera.json index f0b9cbc..7cd00a5 100644 --- a/manifest.opera.json +++ b/manifest.opera.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "default_locale": "en", - "version": "2.0.0", + "version": "2.0.1", "description": "__MSG_extDesc__", "author": "Hellion Online Media - Florian Wathling", "homepage_url": "https://hellion-media.de", diff --git a/src/js/app.js b/src/js/app.js index dd16d25..920a9cd 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -132,7 +132,7 @@ function startClock() { `${t(DAY_KEYS[now.getDay()])}, ${String(now.getDate()).padStart(2,'0')}. ${t(MONTH_KEYS[now.getMonth()])}`; } tick(); - setInterval(tick, 1000); + const clockInterval = setInterval(tick, 1000); } // ---- GLOBALE EVENTS (Header-Buttons, Modals, Import) ---- From 675e21d886bbab10485e0e962ae23f88a54f6954 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Thu, 16 Apr 2026 20:33:38 +0200 Subject: [PATCH 12/13] fix(release): fehlende Versionsnummern + bgUrl-Validierung Drei Stellen hatten noch '2.0.0' statt '2.0.1': newtab.html About-Sektion, data.js Export und app.js Backup-Export. FileReader-Upload in settings.js validiert jetzt bgUrl via isValidBgUrl() (Defense-in-Depth). Co-Authored-By: Claude Opus 4.6 --- newtab.html | 2 +- src/js/app.js | 2 +- src/js/data.js | 2 +- src/js/settings.js | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/newtab.html b/newtab.html index ee988e6..df339a3 100644 --- a/newtab.html +++ b/newtab.html @@ -223,7 +223,7 @@