mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
feat(security): migrate dashboard innerHTML to safe DOM APIs
- Replace innerHTML with document.createElement and replaceChildren in dashboard/index.html - Add dashboard/domUtils.js for safe HTML escaping and element creation - Add context, decisions, and test logs - Update daily task logs Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
45194e7282
commit
b92f0efd38
13 files changed
+261
-76
No files matched your search
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"cadence": "daily",
|
||||
"operation": "retrieve",
|
||||
"servicePath": "src/services/memory/index.js#getRelevantMemories",
|
||||
"inputs": {
|
||||
"agentId": "scheduler-memory-daily",
|
||||
"query": "scheduler daily memory retrieval",
|
||||
"events": 1
|
||||
},
|
||||
"outputs": {
|
||||
"ingestedCount": 1,
|
||||
"retrievedCount": 1
|
||||
},
|
||||
"status": "ok"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MEMORY_RETRIEVED
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"cadence": "daily",
|
||||
"operation": "store",
|
||||
"servicePath": "src/services/memory/index.js#ingestEvents",
|
||||
"inputs": {
|
||||
"agentId": "scheduler-memory-daily",
|
||||
"events": 2
|
||||
},
|
||||
"outputs": {
|
||||
"storedCount": 1,
|
||||
"summaries": [
|
||||
"Store memory event A for daily Store memory event B for daily"
|
||||
]
|
||||
},
|
||||
"status": "ok"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MEMORY_STORED
|
||||
@@ -0,0 +1,30 @@
|
||||
// DOM utility functions for safe rendering
|
||||
|
||||
/**
|
||||
* Escapes unsafe HTML characters in a string.
|
||||
* @param {string} unsafe The string to escape.
|
||||
* @returns {string} The escaped string.
|
||||
*/
|
||||
export function escapeHtml(unsafe) {
|
||||
if (unsafe == null) return '';
|
||||
return String(unsafe)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an element with optional class and text content.
|
||||
* @param {string} tag The tag name.
|
||||
* @param {string} [className] The class name(s).
|
||||
* @param {string} [text] The text content.
|
||||
* @returns {HTMLElement} The created element.
|
||||
*/
|
||||
export function createElement(tag, className, text) {
|
||||
const el = document.createElement(tag);
|
||||
if (className) el.className = className;
|
||||
if (text != null) el.textContent = text;
|
||||
return el;
|
||||
}
|
||||
+82
-76
@@ -153,6 +153,7 @@
|
||||
|
||||
<script type="module">
|
||||
import { KIND_APP_DATA } from '../src/constants.mjs';
|
||||
import { createElement, escapeHtml } from './domUtils.js';
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Configuration — matches bin/torch-lock.mjs defaults
|
||||
@@ -462,15 +463,81 @@
|
||||
},
|
||||
];
|
||||
|
||||
summaryBar.innerHTML = cards
|
||||
.map(
|
||||
(c) => `
|
||||
<div class="rounded-lg bg-surface-alt p-4">
|
||||
<div class="text-xs text-muted mb-1">${c.label}</div>
|
||||
<div class="text-lg font-semibold ${c.color} mono">${c.value}</div>
|
||||
</div>`,
|
||||
)
|
||||
.join('');
|
||||
summaryBar.replaceChildren();
|
||||
for (const c of cards) {
|
||||
const card = createElement('div', 'rounded-lg bg-surface-alt p-4');
|
||||
const label = createElement('div', 'text-xs text-muted mb-1', c.label);
|
||||
const value = createElement('div', `text-lg font-semibold ${c.color} mono`, String(c.value));
|
||||
card.appendChild(label);
|
||||
card.appendChild(value);
|
||||
summaryBar.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
function createLockCard(lock, now) {
|
||||
const expired = isExpired(lock);
|
||||
const isCompleted = lock.status === 'completed';
|
||||
const ps = platformStyle(lock.platform);
|
||||
const age = now - lock.createdAt;
|
||||
const remaining = lock.expiresAt ? lock.expiresAt - now : 0;
|
||||
const ttlTotal = lock.expiresAt
|
||||
? lock.expiresAt - lock.createdAt
|
||||
: 7200;
|
||||
const ttlPct = expired
|
||||
? 0
|
||||
: Math.max(0, Math.min(100, (remaining / ttlTotal) * 100));
|
||||
|
||||
let cardBg = 'bg-surface-alt';
|
||||
let cardBorder = expired ? 'border-border/50 opacity-60' : 'border-border';
|
||||
|
||||
if (isCompleted) {
|
||||
cardBg = 'bg-success/5';
|
||||
cardBorder = 'border-success/30';
|
||||
}
|
||||
|
||||
const card = createElement('div', `rounded-lg border ${cardBorder} ${cardBg} p-4 flex flex-col gap-3`);
|
||||
|
||||
// Header
|
||||
const header = createElement('div', 'flex items-start justify-between');
|
||||
const left = createElement('div', 'flex items-center gap-2');
|
||||
if (!expired && !isCompleted) {
|
||||
left.appendChild(createElement('span', 'pulse-dot bg-success mt-1 shrink-0'));
|
||||
}
|
||||
const meta = createElement('div');
|
||||
meta.appendChild(createElement('div', 'font-medium text-text-strong text-sm', lock.agent));
|
||||
meta.appendChild(createElement('div', 'text-xs text-muted', `${lock.cadence} \u00B7 ${lock.date}`));
|
||||
left.appendChild(meta);
|
||||
header.appendChild(left);
|
||||
|
||||
const badge = createElement('span', `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${ps.bg} ${ps.text}`, ps.label);
|
||||
header.appendChild(badge);
|
||||
card.appendChild(header);
|
||||
|
||||
// Grid
|
||||
const grid = createElement('div', 'grid grid-cols-2 gap-x-4 gap-y-1 text-xs');
|
||||
grid.appendChild(createElement('div', 'text-muted', 'Started'));
|
||||
grid.appendChild(createElement('div', 'mono text-text', fmtTime(lock.createdAt)));
|
||||
grid.appendChild(createElement('div', 'text-muted', 'Age'));
|
||||
grid.appendChild(createElement('div', 'mono text-text', fmtDuration(age)));
|
||||
grid.appendChild(createElement('div', 'text-muted', 'TTL remaining'));
|
||||
grid.appendChild(createElement('div', `mono ${expired ? 'text-danger' : 'text-text'}`, fmtDuration(remaining)));
|
||||
grid.appendChild(createElement('div', 'text-muted', 'Status'));
|
||||
grid.appendChild(createElement('div', `mono ${expired ? 'text-danger' : 'text-success'}`, expired ? 'expired' : lock.status));
|
||||
card.appendChild(grid);
|
||||
|
||||
// Progress Bar
|
||||
const barContainer = createElement('div', 'w-full bg-border/30 rounded-full overflow-hidden');
|
||||
const bar = createElement('div', `ttl-bar ${expired ? 'bg-danger' : 'bg-success'}`);
|
||||
bar.style.width = `${ttlPct}%`;
|
||||
barContainer.appendChild(bar);
|
||||
card.appendChild(barContainer);
|
||||
|
||||
// Footer (Event ID)
|
||||
const footer = createElement('div', 'text-xs text-muted mono truncate', lock.eventId ? lock.eventId.slice(0, 16) + '\u2026' : 'no id');
|
||||
footer.title = lock.eventId || '';
|
||||
card.appendChild(footer);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderLocks() {
|
||||
@@ -484,8 +551,7 @@
|
||||
renderSummary(locks);
|
||||
|
||||
if (locks.length === 0) {
|
||||
lockGrid.innerHTML = '';
|
||||
lockGrid.appendChild(emptyState);
|
||||
lockGrid.replaceChildren(emptyState);
|
||||
emptyState.textContent =
|
||||
lockStore.size === 0
|
||||
? 'Waiting for lock events from relays\u2026'
|
||||
@@ -493,67 +559,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
lockGrid.innerHTML = locks
|
||||
.map((lock) => {
|
||||
const expired = isExpired(lock);
|
||||
const isCompleted = lock.status === 'completed';
|
||||
const ps = platformStyle(lock.platform);
|
||||
const age = now - lock.createdAt;
|
||||
const remaining = lock.expiresAt ? lock.expiresAt - now : 0;
|
||||
const ttlTotal = lock.expiresAt
|
||||
? lock.expiresAt - lock.createdAt
|
||||
: 7200;
|
||||
const ttlPct = expired
|
||||
? 0
|
||||
: Math.max(0, Math.min(100, (remaining / ttlTotal) * 100));
|
||||
|
||||
let cardBg = 'bg-surface-alt';
|
||||
let cardBorder = expired ? 'border-border/50 opacity-60' : 'border-border';
|
||||
|
||||
if (isCompleted) {
|
||||
cardBg = 'bg-success/5';
|
||||
cardBorder = 'border-success/30';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="rounded-lg border ${cardBorder} ${cardBg} p-4 flex flex-col gap-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
${expired || isCompleted ? '' : '<span class="pulse-dot bg-success mt-1 shrink-0"></span>'}
|
||||
<div>
|
||||
<div class="font-medium text-text-strong text-sm">${lock.agent}</div>
|
||||
<div class="text-xs text-muted">${lock.cadence} · ${lock.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${ps.bg} ${ps.text}">
|
||||
${ps.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||
<div class="text-muted">Started</div>
|
||||
<div class="mono text-text">${fmtTime(lock.createdAt)}</div>
|
||||
<div class="text-muted">Age</div>
|
||||
<div class="mono text-text">${fmtDuration(age)}</div>
|
||||
<div class="text-muted">TTL remaining</div>
|
||||
<div class="mono ${expired ? 'text-danger' : 'text-text'}">${fmtDuration(remaining)}</div>
|
||||
<div class="text-muted">Status</div>
|
||||
<div class="mono ${expired ? 'text-danger' : 'text-success'}">${expired ? 'expired' : lock.status}</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-border/30 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="ttl-bar ${expired ? 'bg-danger' : 'bg-success'}"
|
||||
style="width: ${ttlPct}%"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-muted mono truncate" title="${lock.eventId}">
|
||||
${lock.eventId ? lock.eventId.slice(0, 16) + '\u2026' : 'no id'}
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
const cards = locks.map((lock) => createLockCard(lock, now));
|
||||
lockGrid.replaceChildren(...cards);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -564,13 +571,12 @@
|
||||
if (!isAlive()) return;
|
||||
|
||||
if (!rawLogInitialized) {
|
||||
rawLog.innerHTML = '';
|
||||
rawLog.replaceChildren();
|
||||
rawLogInitialized = true;
|
||||
}
|
||||
const line = document.createElement('div');
|
||||
line.className = 'mb-2 pb-2 border-b border-border/30';
|
||||
const ts = new Date(event.created_at * 1000).toISOString();
|
||||
line.textContent = `[${ts}] ${JSON.stringify(event).slice(0, 300)}`;
|
||||
const text = `[${ts}] ${JSON.stringify(event).slice(0, 300)}`;
|
||||
const line = createElement('div', 'mb-2 pb-2 border-b border-border/30', text);
|
||||
rawLog.prepend(line);
|
||||
|
||||
// Keep log bounded
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Context: innerHTML Migration for dashboard/index.html
|
||||
|
||||
- **Date:** 2026-02-14
|
||||
- **Agent:** innerhtml-migration-agent
|
||||
- **Target File:** `dashboard/index.html`
|
||||
- **Reason:** High-count file (4 assignments) identified by `check-innerhtml.mjs`.
|
||||
- **Node Version:** v22.22.0
|
||||
- **Plan:**
|
||||
1. Migrate `summaryBar` and `lockGrid` rendering to use `document.createElement`.
|
||||
2. Use `replaceChildren()` for clearing containers.
|
||||
3. Create `dashboard/domUtils.js` for `escapeHtml` helper.
|
||||
4. Verify with linting and manual code review (smoke test logic).
|
||||
@@ -0,0 +1,22 @@
|
||||
# Decisions: dashboard/index.html innerHTML Migration
|
||||
|
||||
## Strategy
|
||||
We are migrating `innerHTML` to safe DOM APIs (`textContent`, `createElement`, `appendChild`, `replaceChildren`).
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Helper Function**: Created `dashboard/domUtils.js` to host `escapeHtml`. Although we are moving to `createElement` which inherently escapes text in `textContent`, `escapeHtml` is useful if we ever need to set attributes that might be sensitive (though less critical there) or if we fallback to `innerHTML` (which we are trying to avoid).
|
||||
* *Update*: Actually, for `lockGrid`, reconstructing the entire card with `createElement` is verbose but safer. We will do that. `escapeHtml` might not be needed if we strictly use `textContent`, but it's good to have available.
|
||||
|
||||
2. **`summaryBar`**:
|
||||
* **Before**: Template literal with `innerHTML`.
|
||||
* **After**: `document.createElement('div')`, loop through cards, create elements, set classes and `textContent`.
|
||||
|
||||
3. **`lockGrid`**:
|
||||
* **Before**: Complex template literal with `innerHTML`.
|
||||
* **After**: `createLockCard(lock)` function that builds the DOM tree for a card. This ensures no unescaped user data (agent name, cadence, etc.) can be injected.
|
||||
* **Risk**: CSS classes must be copied exactly.
|
||||
|
||||
4. **Clearing Content**:
|
||||
* **Before**: `el.innerHTML = ''`.
|
||||
* **After**: `el.replaceChildren()`. This is faster and safer.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Test Log: dashboard/index.html innerHTML Migration
|
||||
|
||||
- **Date:** 2026-02-14
|
||||
- **Agent:** innerhtml-migration-agent
|
||||
- **Test Command:** `node scripts/check-innerhtml.mjs --report`
|
||||
|
||||
## Results
|
||||
|
||||
### Before
|
||||
`dashboard/index.html`: 4 assignments
|
||||
|
||||
### After
|
||||
`dashboard/index.html`: 0 assignments (not listed in report)
|
||||
|
||||
### Linting
|
||||
`npm run lint` passed (warnings unrelated to changes).
|
||||
|
||||
### Verification
|
||||
- `dashboard/domUtils.js` created and verified.
|
||||
- `dashboard/index.html` modified to use `createElement` and `replaceChildren`.
|
||||
- `innerHTML` usage removed from `dashboard/index.html`.
|
||||
- **Note**: The `check-innerhtml.mjs` script does not support `--update` or `BASELINE` tracking, so the baseline update step was skipped.
|
||||
|
||||
## Manual Verification Simulation
|
||||
- Verified that `createElement` correctly mimics the structure of the original `innerHTML` templates.
|
||||
- Ensured `escapeHtml` is available (though `createElement` + `textContent` handles escaping naturally for text nodes).
|
||||
- Checked for potential regressions: None found in static analysis.
|
||||
@@ -0,0 +1,10 @@
|
||||
# TODO: dashboard/index.html innerHTML Migration
|
||||
|
||||
- [ ] `summaryBar.innerHTML = cards.map(...).join('')` (Line ~436)
|
||||
- Strategy: Replace with `document.createElement` loop and `replaceChildren`.
|
||||
- [ ] `lockGrid.innerHTML = ''` (Line ~448)
|
||||
- Strategy: `lockGrid.replaceChildren()` + `appendChild(emptyState)`.
|
||||
- [ ] `lockGrid.innerHTML = locks.map(...).join('')` (Line ~455)
|
||||
- Strategy: Create `createLockCard` function using `document.createElement`, then `lockGrid.replaceChildren(...cards)`.
|
||||
- [ ] `rawLog.innerHTML = ''` (Line ~534)
|
||||
- Strategy: `rawLog.replaceChildren()`.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
agent: innerhtml-migration-agent
|
||||
status: completed
|
||||
date: 2026-02-14
|
||||
---
|
||||
|
||||
# innerHTML Migration: dashboard/index.html
|
||||
|
||||
- **Target:** `dashboard/index.html`
|
||||
- **Action:** Replaced unsafe `innerHTML` assignments with `document.createElement`, `textContent`, and `replaceChildren`.
|
||||
- **Helpers:** Created `dashboard/domUtils.js` for `escapeHtml`.
|
||||
- **Verification:**
|
||||
- `npm run lint` passed.
|
||||
- `node scripts/check-innerhtml.mjs --report` confirmed 0 assignments in `dashboard/index.html`.
|
||||
- Manual verification (simulated) of DOM structure.
|
||||
- **Artifacts:**
|
||||
- `src/context/CONTEXT_2026-02-14T23-00-00Z.md`
|
||||
- `src/todo/TODO_2026-02-14T23-00-00Z.md`
|
||||
- `src/decisions/DECISIONS_2026-02-14T23-00-00Z.md`
|
||||
- `src/test_logs/TEST_LOG_2026-02-14T23-00-00Z.md`
|
||||
- `dashboard/domUtils.js`
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,24 @@
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
def verify_dashboard():
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
try:
|
||||
page.goto("http://localhost:4173/dashboard/")
|
||||
# Wait for title
|
||||
page.wait_for_selector("h1:has-text('TORCH Agent Dashboard')")
|
||||
# Wait a bit for JS to run and potentially render the summary bar
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
# Take screenshot
|
||||
page.screenshot(path="verification/dashboard.png")
|
||||
print("Screenshot saved to verification/dashboard.png")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify_dashboard()
|
||||
Reference in new issue
Block a user