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>
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:closeevent 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.onis a function (typeWidgetManager.onin console) -
WidgetManager._emitteris 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:
- Create a note → minimize it → verify it fades out and disappears
- Click the note in the widget toolbar to reopen → verify it appears smoothly
- 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:
- Calculator: Open → type a calculation → minimize → reopen → verify history is still there → close → reopen from toolbar
- Timer: Open → set a time → minimize → reopen → verify time is preserved → close
- 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:
- Open Theme Modal → upload a local image → verify it displays as background
- Try entering
javascript:alert(1)in the URL input → verify it's rejected with a dialog - 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:
- Export current data as JSON
- Edit the exported JSON: add a bookmark with
javascript:alert(1)URL → import → verify the bad bookmark is silently skipped - Import a normal JSON backup → verify boards, notes, calculator history, timer presets all appear correctly
- 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(removegetFaviconUrl) -
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:
- All bookmarks show a colored letter icon
- Different bookmark titles produce different colors
- The icons are aligned and properly sized in all themes
- No network requests to google.com in the Network tab (F12 → Network)
- 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-fallbackper theme +@supports notblock) -
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(adddata-i18n-titleto 5 header buttons) -
Modify:
newtab.html:198, 215, 374(adddata-i18nto 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:
- Set language to English → hover over header buttons → verify English tooltips
- Set language to German → hover → verify German tooltips
- Open Settings → verify "Start", "Reset", "Upload" buttons have
data-i18nattributes (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:
- Reload extension
- Verify version in
chrome://extensionsshows 2.0.1 - Open/close/minimize/reopen widgets of all types
- Switch language DE/EN — all tooltips translate
- Import/export JSON data
- Upload background image
- Check Network tab — zero external requests
- 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."