diff --git a/TASKS.md b/TASKS.md index f515676..eeb3163 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,14 +2,14 @@ ## Current -### `feat/frontend-xss-hardening` +### `feat/frontend-template-safety-cleanup` Status: in_progress DoD: -- API-provided values are not rendered into `innerHTML` on user-facing admin pages -- error strings from API responses are rendered through text nodes -- streaming admin pages use DOM construction instead of HTML string concatenation for dynamic cards +- local UI helpers do not use `innerHTML` for static SVG or icon insertion +- wizard shell and workspace shell use DOM-based icon and fragment construction +- remaining HTML-bearing i18n/template paths are explicitly isolated and tracked ## Next diff --git a/apps/ui/js/api-keys.js b/apps/ui/js/api-keys.js index a7f1f8e..b03ac2c 100644 --- a/apps/ui/js/api-keys.js +++ b/apps/ui/js/api-keys.js @@ -18,6 +18,16 @@ function tfKey(key, vars) { return window.tf ? tf(key, vars) : tKey(key); } +function buildIconSvg(href, width, height) { + var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', String(width)); + svg.setAttribute('height', String(height)); + var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + use.setAttribute('href', href); + svg.appendChild(use); + return svg; +} + function mapKeyRecord(record) { var apiKey = record.api_key || {}; return { @@ -310,7 +320,13 @@ document.getElementById('copy-key-btn').addEventListener('click', function() { if (navigator.clipboard) { navigator.clipboard.writeText(value).catch(function() {}); } - this.innerHTML = ''; + this.replaceChildren( + buildIconSvg( + (window.APP_BASE || '') + 'icons/general/check.svg#icon', + 13, + 13 + ) + ); if (window.CrankUi) { window.CrankUi.info( tKey('apikeys.toast.copy_message'), diff --git a/apps/ui/js/workspace.js b/apps/ui/js/workspace.js index 2b82d97..7f209bc 100644 --- a/apps/ui/js/workspace.js +++ b/apps/ui/js/workspace.js @@ -12,6 +12,16 @@ function tfKey(key, vars) { return typeof tf === 'function' ? tf(key, vars) : key; } +function buildIconSvg(href, width, height) { + var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', String(width)); + svg.setAttribute('height', String(height)); + var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + use.setAttribute('href', href); + svg.appendChild(use); + return svg; +} + function workspaceColor(index) { return ['#0d9488', '#7c3aed', '#0891b2', '#f59e0b', '#2563eb'][index % 5]; } @@ -139,10 +149,11 @@ function renderWorkspaceList() { item.appendChild(info); if (active) { - var check = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - check.setAttribute('width', '12'); - check.setAttribute('height', '12'); - check.innerHTML = ''; + var check = buildIconSvg( + (window.APP_BASE || '') + 'icons/general/check.svg#icon', + 12, + 12 + ); item.appendChild(check); } @@ -160,7 +171,13 @@ function renderWorkspaceList() { var dd = document.getElementById('ws-dropdown'); if (dd) dd.style.display = 'none'; }); - manageLink.innerHTML = ''; + manageLink.appendChild( + buildIconSvg( + (window.APP_BASE || '') + 'icons/general/settings.svg#icon', + 14, + 14 + ) + ); manageLink.appendChild(document.createTextNode(tKey('settings.ws.title'))); list.appendChild(manageLink); }