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);
}