Skip to content

Commit 9b9684a

Browse files
committed
Fix UI CSP interactions and media sizing
1 parent d299c33 commit 9b9684a

11 files changed

Lines changed: 199 additions & 63 deletions

File tree

services/ui/ui/app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ def add_security_headers(response):
168168
response.headers["Content-Security-Policy"] = (
169169
"default-src 'self'; "
170170
f"script-src 'self' 'nonce-{nonce}'; "
171-
f"style-src 'self' 'nonce-{nonce}' 'unsafe-inline'; "
171+
"style-src 'self' 'unsafe-inline'; "
172+
f"style-src-elem 'self' 'nonce-{nonce}'; "
173+
"style-src-attr 'unsafe-inline'; "
172174
"img-src 'self' data:; "
173175
"media-src 'self' data:; "
174176
"font-src 'self'; "

services/ui/ui/templates/base.html

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="referrer" content="no-referrer">
77
<meta name="csrf-token" content="{{ csrf_token }}">
88
<title>{% block title %}SecAI OS{% endblock %}</title>
9-
<style>
9+
<style nonce="{{ csp_nonce }}">
1010
/* -- Reset & Variables -- */
1111
:root {
1212
--bg: #0f1117;
@@ -41,6 +41,15 @@
4141
overflow: hidden;
4242
display: flex;
4343
}
44+
img,
45+
video,
46+
canvas {
47+
max-width: 100%;
48+
height: auto;
49+
}
50+
svg {
51+
flex-shrink: 0;
52+
}
4453

4554
/* -- Sidebar -- */
4655
.sidebar {
@@ -596,7 +605,7 @@
596605
<main class="main">
597606
<header class="topbar">
598607
<div class="topbar-left">
599-
<button class="mobile-toggle" onclick="document.getElementById('sidebar').classList.toggle('open')">
608+
<button class="mobile-toggle" id="mobile-toggle" type="button" aria-label="Toggle navigation">
600609
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
601610
</button>
602611
<h1>{% block page_title %}{% endblock %}</h1>
@@ -622,8 +631,8 @@ <h1>{% block page_title %}{% endblock %}</h1>
622631
<h3 id="modal-title"></h3>
623632
<p id="modal-body"></p>
624633
<div class="modal-actions">
625-
<button class="btn" id="modal-cancel" onclick="closeModal(false)">Cancel</button>
626-
<button class="btn btn-danger" id="modal-confirm" onclick="closeModal(true)">Confirm</button>
634+
<button class="btn" id="modal-cancel" type="button">Cancel</button>
635+
<button class="btn btn-danger" id="modal-confirm" type="button">Confirm</button>
627636
</div>
628637
</div>
629638
</div>
@@ -679,6 +688,16 @@ <h3 id="modal-title"></h3>
679688
document.getElementById('modal-overlay').classList.add('hidden');
680689
if (_modalResolve) { _modalResolve(result); _modalResolve = null; }
681690
}
691+
document.getElementById('mobile-toggle').addEventListener('click', function(e) {
692+
e.stopPropagation();
693+
document.getElementById('sidebar').classList.toggle('open');
694+
});
695+
document.getElementById('modal-cancel').addEventListener('click', function() {
696+
closeModal(false);
697+
});
698+
document.getElementById('modal-confirm').addEventListener('click', function() {
699+
closeModal(true);
700+
});
682701
document.getElementById('modal-overlay').addEventListener('click', function(e) {
683702
if (e.target === e.currentTarget) closeModal(false);
684703
});

services/ui/ui/templates/generate.html

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,30 @@
22
{% block title %}SecAI OS - Generate{% endblock %}
33
{% block page_title %}Image &amp; Video Generation{% endblock %}
44
{% block content_class %}content-wide{% endblock %}
5+
{% block extra_css %}
6+
<style nonce="{{ csp_nonce }}">
7+
.generation-result {
8+
margin-top: 1.5rem;
9+
text-align: center;
10+
}
11+
.generated-media {
12+
display: block;
13+
max-width: min(100%, 640px);
14+
max-height: 60vh;
15+
width: auto;
16+
height: auto;
17+
margin: 0 auto 1rem;
18+
border-radius: var(--radius-lg);
19+
object-fit: contain;
20+
}
21+
</style>
22+
{% endblock %}
523

624
{% block content %}
725
<div class="tabs">
8-
<button class="tab active" onclick="switchTab('image', this)">Text to Image</button>
9-
<button class="tab" onclick="switchTab('video', this)">Text to Video</button>
10-
<button class="tab" onclick="switchTab('img2img', this)">Image to Image</button>
26+
<button class="tab active" type="button" data-tab="image">Text to Image</button>
27+
<button class="tab" type="button" data-tab="video">Text to Video</button>
28+
<button class="tab" type="button" data-tab="img2img">Image to Image</button>
1129
</div>
1230

1331
<div id="panel-image">
@@ -25,7 +43,7 @@
2543
<div class="form-group"><label class="form-label">Steps</label><input class="form-input" id="img-steps" type="number" value="30"></div>
2644
<div class="form-group"><label class="form-label">Seed</label><input class="form-input" id="img-seed" type="number" placeholder="Random"></div>
2745
</div>
28-
<button class="btn btn-primary btn-lg" id="btn-image" onclick="generateImage()">Generate Image</button>
46+
<button class="btn btn-primary btn-lg" id="btn-image" type="button">Generate Image</button>
2947
</div>
3048

3149
<div id="panel-video" class="hidden">
@@ -39,7 +57,7 @@
3957
<div class="form-group"><label class="form-label">Frames</label><input class="form-input" id="vid-frames" type="number" value="25"></div>
4058
<div class="form-group"><label class="form-label">Steps</label><input class="form-input" id="vid-steps" type="number" value="25"></div>
4159
</div>
42-
<button class="btn btn-primary btn-lg" id="btn-video" onclick="generateVideo()">Generate Video</button>
60+
<button class="btn btn-primary btn-lg" id="btn-video" type="button">Generate Video</button>
4361
</div>
4462

4563
<div id="panel-img2img" class="hidden">
@@ -55,10 +73,10 @@
5573
<div class="form-group"><label class="form-label">Strength</label><input class="form-input" id="i2i-strength" type="number" value="0.75" step="0.05" min="0" max="1"></div>
5674
<div class="form-group"><label class="form-label">Steps</label><input class="form-input" id="i2i-steps" type="number" value="30"></div>
5775
</div>
58-
<button class="btn btn-primary btn-lg" id="btn-img2img" onclick="generateImg2Img()">Generate</button>
76+
<button class="btn btn-primary btn-lg" id="btn-img2img" type="button">Generate</button>
5977
</div>
6078

61-
<div id="result" style="margin-top:1.5rem;text-align:center"></div>
79+
<div id="result" class="generation-result"></div>
6280
<div id="gen-status" class="hidden" style="margin-top:1rem;padding:1rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);font-size:0.85rem;color:var(--text-muted)"></div>
6381
{% endblock %}
6482

@@ -99,7 +117,7 @@
99117
if (data.error) { showGenStatus('Error: ' + data.error); return; }
100118
var html = '';
101119
(data.images || []).forEach(function(b64) {
102-
html += '<img src="data:image/png;base64,' + b64 + '" style="max-width:100%;border-radius:var(--radius-lg);margin-bottom:1rem">';
120+
html += '<img class="generated-media" src="data:image/png;base64,' + b64 + '" alt="Generated image">';
103121
});
104122
document.getElementById('result').innerHTML = html;
105123
showGenStatus('Generated in ' + data.elapsed_seconds + 's');
@@ -126,7 +144,7 @@
126144
var data = await resp.json();
127145
if (data.error) { showGenStatus('Error: ' + data.error); return; }
128146
document.getElementById('result').innerHTML =
129-
'<video controls autoplay style="max-width:100%;border-radius:var(--radius-lg)"><source src="data:video/mp4;base64,' + data.video + '" type="video/mp4"></video>';
147+
'<video class="generated-media" controls autoplay><source src="data:video/mp4;base64,' + data.video + '" type="video/mp4"></video>';
130148
showGenStatus('Generated in ' + data.elapsed_seconds + 's');
131149
} catch(e) { showGenStatus('Error: ' + e.message); }
132150
finally { btn.disabled = false; }
@@ -155,12 +173,21 @@
155173
var data = await resp.json();
156174
if (data.error) { showGenStatus('Error: ' + data.error); return; }
157175
document.getElementById('result').innerHTML =
158-
'<img src="data:image/png;base64,' + data.image + '" style="max-width:100%;border-radius:var(--radius-lg)">';
176+
'<img class="generated-media" src="data:image/png;base64,' + data.image + '" alt="Generated image">';
159177
showGenStatus('Generated in ' + data.elapsed_seconds + 's');
160178
} catch(e) { showGenStatus('Error: ' + e.message); }
161179
finally { btn.disabled = false; }
162180
};
163181
reader.readAsDataURL(file);
164182
}
183+
184+
document.querySelectorAll('.tab[data-tab]').forEach(function(button) {
185+
button.addEventListener('click', function() {
186+
switchTab(button.getAttribute('data-tab'), button);
187+
});
188+
});
189+
document.getElementById('btn-image').addEventListener('click', generateImage);
190+
document.getElementById('btn-video').addEventListener('click', generateVideo);
191+
document.getElementById('btn-img2img').addEventListener('click', generateImg2Img);
165192
</script>
166193
{% endblock %}

services/ui/ui/templates/index.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{% block page_title %}Chat{% endblock %}
44

55
{% block extra_css %}
6-
<style>
6+
<style nonce="{{ csp_nonce }}">
77
.content { padding: 0 !important; display: flex; flex-direction: column; }
88
.chat-container {
99
flex: 1;
@@ -116,11 +116,11 @@ <h3>Local-first. Private by default.</h3>
116116
</div>
117117
</div>
118118
<div class="chat-input-area">
119-
<button class="btn btn-sm" id="search-toggle" onclick="toggleSearch()" title="Toggle web search (Tor-routed)" style="min-height:44px;padding:0 0.65rem;opacity:0.4">
119+
<button class="btn btn-sm" id="search-toggle" type="button" title="Toggle web search (Tor-routed)" style="min-height:44px;padding:0 0.65rem;opacity:0.4">
120120
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
121121
</button>
122122
<textarea id="user-input" rows="1" placeholder="Type a message... (Enter to send, Shift+Enter for newline)" autofocus></textarea>
123-
<button class="btn btn-primary" id="send-btn" onclick="sendMessage()">Send</button>
123+
<button class="btn btn-primary" id="send-btn" type="button">Send</button>
124124
</div>
125125
{% endblock %}
126126

@@ -184,6 +184,8 @@ <h3>Local-first. Private by default.</h3>
184184
userInput.style.height = 'auto';
185185
userInput.style.height = Math.min(userInput.scrollHeight, 160) + 'px';
186186
});
187+
searchToggle.addEventListener('click', toggleSearch);
188+
sendBtn.addEventListener('click', sendMessage);
187189

188190
async function sendMessage() {
189191
var text = userInput.value.trim();

services/ui/ui/templates/login.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<meta name="referrer" content="no-referrer">
77
<title>SecAI OS - Login</title>
8-
<style>
8+
<style nonce="{{ csp_nonce }}">
99
:root {
1010
--bg: #0f1117;
1111
--surface: #1a1b26;
@@ -144,7 +144,7 @@ <h1 id="title">Unlock Appliance</h1>
144144

145145
<div class="error-msg" id="error-msg"></div>
146146

147-
<form id="auth-form" onsubmit="handleSubmit(event)">
147+
<form id="auth-form">
148148
<div class="form-group">
149149
<label class="form-label" for="passphrase">Passphrase</label>
150150
<input class="form-input" type="password" id="passphrase" name="passphrase"
@@ -265,6 +265,7 @@ <h1 id="title">Unlock Appliance</h1>
265265
}
266266
}
267267

268+
document.getElementById('auth-form').addEventListener('submit', handleSubmit);
268269
checkSetup();
269270
</script>
270271
</body>

services/ui/ui/templates/models.html

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<div id="integrity-banner" class="alert alert-info mb-lg">
1010
<span class="status-dot gray" id="integrity-dot" style="width:8px;height:8px"></span>
1111
<span id="integrity-text">Checking model integrity...</span>
12-
<button class="btn btn-sm" id="verify-all-btn" onclick="verifyAll()" style="margin-left:auto">Verify All</button>
12+
<button class="btn btn-sm" id="verify-all-btn" type="button" style="margin-left:auto">Verify All</button>
1313
</div>
1414

1515
<div class="section-title">Model Catalog</div>
@@ -19,7 +19,7 @@
1919
</div>
2020

2121
<div class="section-title">Import Model</div>
22-
<div class="upload-zone" id="upload-zone" onclick="document.getElementById('file-input').click()">
22+
<div class="upload-zone" id="upload-zone" role="button" tabindex="0">
2323
<p style="font-size:0.9rem;margin-bottom:0.35rem">Drag and drop a model file here</p>
2424
<p style="font-size:0.8rem">.gguf or .safetensors &mdash; or click to browse</p>
2525
<input type="file" id="file-input" accept=".gguf,.safetensors">
@@ -126,7 +126,7 @@
126126
}
127127
var manifestBtn = '';
128128
if (m.gguf_guard_manifest) {
129-
manifestBtn = '<button class="btn btn-sm btn-accent" onclick="verifyManifest(\'' + esc(m.name) + '\')">Verify Tensors</button>';
129+
manifestBtn = '<button class="btn btn-sm btn-accent" type="button" data-action="verify-manifest" data-name="' + encodeURIComponent(m.name) + '">Verify Tensors</button>';
130130
}
131131
return '<div class="card">' +
132132
'<div class="card-header">' +
@@ -140,9 +140,9 @@
140140
'</div>' +
141141
guardInfo +
142142
'<div class="btn-group mt-md">' +
143-
'<button class="btn btn-sm btn-success" onclick="verifyModel(\'' + esc(m.name) + '\')">Verify Hash</button>' +
143+
'<button class="btn btn-sm btn-success" type="button" data-action="verify-model" data-name="' + encodeURIComponent(m.name) + '">Verify Hash</button>' +
144144
manifestBtn +
145-
'<button class="btn btn-sm btn-danger" onclick="deleteModel(\'' + esc(m.name) + '\')">Delete</button>' +
145+
'<button class="btn btn-sm btn-danger" type="button" data-action="delete-model" data-name="' + encodeURIComponent(m.name) + '">Delete</button>' +
146146
'</div>' +
147147
'</div>';
148148
}).join('');
@@ -315,9 +315,9 @@
315315
btnHtml = '<span class="badge badge-warning">Scanning...</span>';
316316
} else if (dl && dl.status === 'failed') {
317317
btnHtml = '<span class="badge badge-danger">Failed</span>' +
318-
'<button class="btn btn-sm" onclick="downloadCatalogModel(\'' + esc(m.url) + '\',\'' + esc(m.filename) + '\',\'' + esc(m.type) + '\')">Retry</button>';
318+
'<button class="btn btn-sm" type="button" data-action="catalog-download" data-url="' + encodeURIComponent(m.url) + '" data-filename="' + encodeURIComponent(m.filename) + '" data-type="' + encodeURIComponent(m.type) + '">Retry</button>';
319319
} else {
320-
btnHtml = '<button class="btn btn-sm btn-primary" onclick="downloadCatalogModel(\'' + esc(m.url) + '\',\'' + esc(m.filename) + '\',\'' + esc(m.type) + '\')">Download</button>';
320+
btnHtml = '<button class="btn btn-sm btn-primary" type="button" data-action="catalog-download" data-url="' + encodeURIComponent(m.url) + '" data-filename="' + encodeURIComponent(m.filename) + '" data-type="' + encodeURIComponent(m.type) + '">Download</button>';
321321
}
322322

323323
return '<div class="card">' +
@@ -365,6 +365,34 @@
365365
}
366366
}
367367

368+
document.getElementById('verify-all-btn').addEventListener('click', verifyAll);
369+
uploadZone.addEventListener('click', function() {
370+
fileInput.click();
371+
});
372+
uploadZone.addEventListener('keydown', function(e) {
373+
if (e.key === 'Enter' || e.key === ' ') {
374+
e.preventDefault();
375+
fileInput.click();
376+
}
377+
});
378+
document.getElementById('models-list').addEventListener('click', function(e) {
379+
var target = e.target.closest('[data-action]');
380+
if (!target) return;
381+
var name = decodeURIComponent(target.getAttribute('data-name') || '');
382+
if (target.getAttribute('data-action') === 'verify-model') verifyModel(name);
383+
if (target.getAttribute('data-action') === 'verify-manifest') verifyManifest(name);
384+
if (target.getAttribute('data-action') === 'delete-model') deleteModel(name);
385+
});
386+
document.getElementById('catalog-list').addEventListener('click', function(e) {
387+
var target = e.target.closest('[data-action="catalog-download"]');
388+
if (!target) return;
389+
downloadCatalogModel(
390+
decodeURIComponent(target.getAttribute('data-url') || ''),
391+
decodeURIComponent(target.getAttribute('data-filename') || ''),
392+
decodeURIComponent(target.getAttribute('data-type') || '')
393+
);
394+
});
395+
368396
function refreshAll() { loadModels(); loadQuarantine(); loadIntegrityStatus(); loadCatalog(); }
369397
refreshAll();
370398
setInterval(refreshAll, 10000);

services/ui/ui/templates/security.html

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
<div class="card">
100100
<div class="card-header">
101101
<span class="card-title">Hash-Chained Audit Logs</span>
102-
<button class="btn btn-sm" onclick="verifyAudit()">Verify Now</button>
102+
<button class="btn btn-sm" id="verify-audit-btn" type="button">Verify Now</button>
103103
</div>
104104
<p class="meta" id="audit-detail">Loading audit status...</p>
105105
</div>
@@ -141,7 +141,7 @@
141141
<span class="card-title">Forensic Export</span>
142142
</div>
143143
<p class="meta mb-md">Download a signed forensic bundle containing all incidents, audit log entries, system state, and policy digest for offline analysis or external audit.</p>
144-
<button class="btn btn-sm" onclick="downloadForensicBundle()">Download Bundle</button>
144+
<button class="btn btn-sm" id="download-forensic-btn" type="button">Download Bundle</button>
145145
</div>
146146

147147
<!-- Emergency Controls -->
@@ -156,9 +156,9 @@
156156
</div>
157157
<p class="meta mb-md" id="panic-detail">System operating normally.</p>
158158
<div class="btn-group">
159-
<button class="btn btn-warning btn-sm" onclick="triggerPanic(1)">Level 1: Lock</button>
160-
<button class="btn btn-danger btn-sm" onclick="triggerPanic(2)">Level 2: Shred Keys</button>
161-
<button class="btn btn-danger btn-sm" onclick="triggerPanic(3)">Level 3: Wipe All</button>
159+
<button class="btn btn-warning btn-sm" type="button" data-panic-level="1">Level 1: Lock</button>
160+
<button class="btn btn-danger btn-sm" type="button" data-panic-level="2">Level 2: Shred Keys</button>
161+
<button class="btn btn-danger btn-sm" type="button" data-panic-level="3">Level 3: Wipe All</button>
162162
</div>
163163
<p class="form-hint mt-sm">Level 1 is reversible. Levels 2-3 require passphrase and are irreversible.</p>
164164
</div>
@@ -589,6 +589,14 @@
589589
}
590590
}
591591

592+
document.getElementById('verify-audit-btn').addEventListener('click', verifyAudit);
593+
document.getElementById('download-forensic-btn').addEventListener('click', downloadForensicBundle);
594+
document.querySelectorAll('[data-panic-level]').forEach(function(button) {
595+
button.addEventListener('click', function() {
596+
triggerPanic(parseInt(button.getAttribute('data-panic-level'), 10));
597+
});
598+
});
599+
592600
/* Load all sections */
593601
loadApplianceState();
594602
loadServiceHealth();

0 commit comments

Comments
 (0)