Autarch/web/templates/encmodules.html

567 lines
22 KiB
HTML
Raw Permalink Normal View History

{% extends "base.html" %}
{% block title %}Encrypted Modules — AUTARCH{% endblock %}
{% block extra_head %}
<style>
/* ── Module grid ─────────────────────────────────────────────────────────── */
.enc-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.enc-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.1rem 1.2rem;
display: flex;
flex-direction: column;
gap: 0.55rem;
transition: border-color 0.15s, box-shadow 0.15s;
position: relative;
overflow: hidden;
}
.enc-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent), transparent);
}
.enc-card:hover {
border-color: var(--accent);
box-shadow: 0 0 16px rgba(0,255,65,0.08);
}
.enc-card.unlocked::before {
background: linear-gradient(90deg, #00cc55, transparent);
}
.enc-card.running::before {
background: linear-gradient(90deg, var(--warning), var(--accent));
animation: shimmer 1.5s linear infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.enc-card-header {
display: flex;
align-items: center;
gap: 0.6rem;
}
.enc-lock-icon {
font-size: 1.1rem;
flex-shrink: 0;
color: var(--text-muted);
transition: color 0.15s;
}
.enc-card.unlocked .enc-lock-icon { color: #00cc55; }
.enc-card-name {
font-size: 0.95rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: 0.03em;
}
.enc-card-version {
font-size: 0.72rem;
color: var(--text-muted);
margin-left: auto;
font-family: monospace;
}
.enc-card-desc {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.5;
}
.enc-card-meta {
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
font-size: 0.72rem;
color: var(--text-muted);
}
.enc-tag {
padding: 1px 6px;
border-radius: 3px;
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.enc-tag-danger { background: rgba(255,50,50,0.15); color: #ff5555; border: 1px solid rgba(255,50,50,0.3); }
.enc-tag-warn { background: rgba(255,170,0,0.12); color: #ffaa00; border: 1px solid rgba(255,170,0,0.25); }
.enc-tag-info { background: rgba(0,200,255,0.1); color: #00c8ff; border: 1px solid rgba(0,200,255,0.2); }
.enc-tag-dim { background: rgba(120,120,120,0.1); color: #888; border: 1px solid rgba(120,120,120,0.2); }
.enc-size { color: var(--text-muted); font-family: monospace; }
/* ── Unlock panel ────────────────────────────────────────────────────────── */
.enc-unlock-panel {
display: none;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.4rem;
padding-top: 0.75rem;
border-top: 1px solid var(--border);
}
.enc-unlock-panel.visible { display: flex; }
.enc-key-row {
display: flex;
gap: 0.4rem;
}
.enc-key-input {
flex: 1;
font-family: monospace;
font-size: 0.82rem;
background: var(--bg-input, #111);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.35rem 0.6rem;
}
.enc-key-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(0,255,65,0.12);
}
/* ── Params editor ───────────────────────────────────────────────────────── */
.enc-params {
display: none;
flex-direction: column;
gap: 0.4rem;
margin-top: 0.35rem;
}
.enc-params.visible { display: flex; }
.enc-params label { font-size: 0.75rem; color: var(--text-muted); }
.enc-params textarea {
font-family: monospace;
font-size: 0.78rem;
background: #0a0a12;
color: #00ff41;
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.4rem 0.6rem;
resize: vertical;
min-height: 80px;
}
.enc-params textarea:focus {
outline: none;
border-color: var(--accent);
}
/* ── Card actions ────────────────────────────────────────────────────────── */
.enc-card-actions {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
margin-top: 0.25rem;
}
/* ── Output terminal ─────────────────────────────────────────────────────── */
.enc-output-wrap {
margin-top: 1.5rem;
display: none;
}
.enc-output-wrap.visible { display: block; }
.enc-output-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.enc-output-title {
font-size: 0.85rem;
font-weight: 700;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.enc-terminal {
background: #050510;
border: 1px solid #1a2a1a;
border-radius: var(--radius);
padding: 0.85rem 1rem;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
line-height: 1.55;
max-height: 420px;
overflow-y: auto;
color: #ccc;
}
.enc-line-info { color: #ccc; }
.enc-line-warn { color: #ffaa00; }
.enc-line-error { color: #ff5555; }
.enc-line-found { color: #00ff41; font-weight: 700; }
.enc-line-module { color: #888; font-style: italic; }
/* ── Upload zone ─────────────────────────────────────────────────────────── */
.enc-upload-zone {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 2rem;
text-align: center;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
color: var(--text-muted);
font-size: 0.85rem;
}
.enc-upload-zone:hover, .enc-upload-zone.drag-over {
border-color: var(--accent);
background: rgba(0,255,65,0.04);
color: var(--text-secondary);
}
.enc-upload-zone input[type=file] { display: none; }
</style>
{% endblock %}
{% block content %}
<div class="page-header" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
<h1>Encrypted Modules</h1>
<span style="font-size:0.78rem;color:var(--text-muted);background:rgba(255,50,50,0.1);
border:1px solid rgba(255,50,50,0.25);border-radius:4px;padding:2px 8px">
AUTHORIZED USE ONLY
</span>
<span id="enc-module-count" style="font-size:0.8rem;color:var(--text-muted);margin-left:auto">
{{ modules|length }} module{{ 's' if modules|length != 1 else '' }} loaded
</span>
</div>
<!-- Upload section -->
<div class="section">
<h2>Upload Module</h2>
<div class="enc-upload-zone" id="upload-zone" onclick="document.getElementById('aes-file-input').click()"
ondragover="event.preventDefault();this.classList.add('drag-over')"
ondragleave="this.classList.remove('drag-over')"
ondrop="handleFileDrop(event)">
<input type="file" id="aes-file-input" accept=".aes" onchange="handleFileSelect(this)">
<div style="font-size:1.5rem;margin-bottom:0.4rem;opacity:0.5">&#x1F512;</div>
<div>Drop an <code>.aes</code> encrypted module here, or click to browse</div>
<div style="font-size:0.72rem;margin-top:0.3rem;opacity:0.6">AES-256 encrypted Python modules</div>
</div>
<div id="upload-status" style="margin-top:0.5rem;font-size:0.82rem;display:none"></div>
</div>
<!-- Module grid -->
<div class="section">
<h2>Available Modules</h2>
{% if not modules %}
<p style="color:var(--text-muted);font-size:0.85rem">
No .aes modules found in <code>modules/encrypted/</code>.
Upload a module above to get started.
</p>
{% else %}
<div class="enc-grid" id="enc-grid">
{% for mod in modules %}
<div class="enc-card" id="card-{{ mod.id }}" data-filename="{{ mod.filename }}">
<div class="enc-card-header">
<span class="enc-lock-icon">&#x1F512;</span>
<span class="enc-card-name">{{ mod.name }}</span>
<span class="enc-card-version">v{{ mod.version }}</span>
</div>
<div class="enc-card-desc">{{ mod.description or 'No description available.' }}</div>
<div class="enc-card-meta">
{% for tag in mod.tags %}
<span class="enc-tag enc-tag-{{ mod.tag_colors.get(tag, 'dim') }}">{{ tag }}</span>
{% endfor %}
<span class="enc-size" style="margin-left:auto">{{ mod.size_kb }} KB</span>
</div>
<!-- Unlock panel -->
<div class="enc-unlock-panel" id="unlock-{{ mod.id }}">
<div class="enc-key-row">
<input type="password" class="enc-key-input" id="key-{{ mod.id }}"
placeholder="Decryption key / password"
onkeypress="if(event.key==='Enter')verifyModule('{{ mod.id }}','{{ mod.filename }}')">
<button class="btn btn-sm" onclick="toggleKeyVisibility('{{ mod.id }}')" title="Show/hide key">&#x1F441;</button>
</div>
<div id="verify-status-{{ mod.id }}" style="font-size:0.75rem;min-height:1em"></div>
<!-- Params editor (shown after verification) -->
<div class="enc-params" id="params-{{ mod.id }}">
<label for="params-ta-{{ mod.id }}">
Run Parameters (JSON) — passed to module's <code>run(params)</code>
</label>
<textarea id="params-ta-{{ mod.id }}" rows="4" spellcheck="false">{}</textarea>
</div>
<div class="enc-card-actions">
<button class="btn btn-sm btn-primary" onclick="verifyModule('{{ mod.id }}','{{ mod.filename }}')">
Unlock
</button>
<button class="btn btn-sm btn-danger" id="run-btn-{{ mod.id }}" style="display:none"
onclick="runModule('{{ mod.id }}','{{ mod.filename }}')">
Run
</button>
<button class="btn btn-sm btn-danger" id="stop-btn-{{ mod.id }}" style="display:none"
onclick="stopModule('{{ mod.id }}')">
Stop
</button>
<button class="btn btn-sm" onclick="cancelUnlock('{{ mod.id }}')">Cancel</button>
<button class="btn btn-sm" style="margin-left:auto;color:var(--text-muted)"
onclick="deleteModule('{{ mod.id }}','{{ mod.filename }}')">&#x2715; Remove</button>
</div>
</div>
<!-- Default actions (locked state) -->
<div class="enc-card-actions" id="locked-actions-{{ mod.id }}">
<button class="btn btn-sm btn-primary" onclick="showUnlock('{{ mod.id }}')">
&#x1F511; Unlock &amp; Run
</button>
<button class="btn btn-sm" style="color:var(--text-muted)"
onclick="deleteModule('{{ mod.id }}','{{ mod.filename }}')">&#x2715; Remove</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Output terminal -->
<div class="enc-output-wrap section" id="output-wrap">
<div class="enc-output-header">
<div class="debug-live-dot" id="run-live-dot"></div>
<span class="enc-output-title" id="output-module-name">Module Output</span>
<button class="btn btn-sm" onclick="clearOutput()">Clear</button>
<button class="btn btn-sm" onclick="exportOutput()">Export</button>
</div>
<div class="enc-terminal" id="enc-terminal"></div>
</div>
<script>
// ── State ─────────────────────────────────────────────────────────────────────
const _activeStreams = {}; // modId -> EventSource
const _runIds = {}; // modId -> run_id
// ── Upload ────────────────────────────────────────────────────────────────────
function handleFileSelect(input) {
if (input.files[0]) uploadFile(input.files[0]);
}
function handleFileDrop(e) {
e.preventDefault();
document.getElementById('upload-zone').classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file) uploadFile(file);
}
async function uploadFile(file) {
const status = document.getElementById('upload-status');
status.style.display = '';
status.style.color = 'var(--text-secondary)';
status.textContent = `Uploading ${file.name}...`;
const fd = new FormData();
fd.append('module_file', file);
try {
const res = await fetch('/encmodules/upload', {method: 'POST', body: fd});
const data = await res.json();
if (data.ok) {
status.style.color = 'var(--success)';
status.textContent = `Uploaded: ${data.module.name} (${data.module.size_kb} KB)`;
setTimeout(() => location.reload(), 1200);
} else {
status.style.color = 'var(--danger)';
status.textContent = 'Upload failed: ' + (data.error || 'unknown error');
}
} catch(e) {
status.style.color = 'var(--danger)';
status.textContent = 'Upload error: ' + e.message;
}
}
// ── Unlock / verify ───────────────────────────────────────────────────────────
function showUnlock(id) {
document.getElementById('locked-actions-' + id).style.display = 'none';
document.getElementById('unlock-' + id).classList.add('visible');
setTimeout(() => document.getElementById('key-' + id).focus(), 50);
}
function cancelUnlock(id) {
document.getElementById('locked-actions-' + id).style.display = '';
document.getElementById('unlock-' + id).classList.remove('visible');
document.getElementById('verify-status-' + id).textContent = '';
document.getElementById('run-btn-' + id).style.display = 'none';
document.getElementById('params-' + id).classList.remove('visible');
document.getElementById('card-' + id).classList.remove('unlocked');
}
function toggleKeyVisibility(id) {
const inp = document.getElementById('key-' + id);
inp.type = inp.type === 'password' ? 'text' : 'password';
}
async function verifyModule(id, filename) {
const key = document.getElementById('key-' + id).value.trim();
const status = document.getElementById('verify-status-' + id);
if (!key) { status.style.color = 'var(--danger)'; status.textContent = 'Enter a key first.'; return; }
status.style.color = 'var(--text-muted)';
status.textContent = 'Verifying...';
try {
const res = await fetch('/encmodules/verify', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({filename, password: key}),
});
const data = await res.json();
if (data.ok) {
status.style.color = 'var(--success)';
status.textContent = `Unlocked — ${data.lines} lines${data.has_run ? ', run() found' : ', no run() — exec only'}.`;
document.getElementById('card-' + id).classList.add('unlocked');
document.getElementById('card-' + id).querySelector('.enc-lock-icon').textContent = '\uD83D\uDD13';
document.getElementById('run-btn-' + id).style.display = '';
document.getElementById('params-' + id).classList.add('visible');
} else {
status.style.color = 'var(--danger)';
status.textContent = 'Error: ' + (data.error || 'unknown');
document.getElementById('card-' + id).classList.remove('unlocked');
}
} catch(e) {
status.style.color = 'var(--danger)';
status.textContent = 'Error: ' + e.message;
}
}
// ── Run ───────────────────────────────────────────────────────────────────────
async function runModule(id, filename) {
const key = document.getElementById('key-' + id).value.trim();
const rawPrm = document.getElementById('params-ta-' + id).value.trim();
let params = {};
try { params = JSON.parse(rawPrm || '{}'); } catch(e) {
alert('Invalid JSON in params: ' + e.message); return;
}
const runBtn = document.getElementById('run-btn-' + id);
const stopBtn = document.getElementById('stop-btn-' + id);
runBtn.style.display = 'none';
stopBtn.style.display = '';
document.getElementById('card-' + id).classList.add('running');
// Show / clear terminal
const wrap = document.getElementById('output-wrap');
wrap.classList.add('visible');
document.getElementById('output-module-name').textContent =
document.getElementById('card-' + id).querySelector('.enc-card-name').textContent;
document.getElementById('enc-terminal').innerHTML = '';
document.getElementById('run-live-dot').style.background = 'var(--accent)';
try {
const res = await fetch('/encmodules/run', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({filename, password: key, params}),
});
const data = await res.json();
if (!data.ok) {
appendLine('[ERROR] ' + (data.error || 'unknown'), 'error');
resetRunState(id);
return;
}
const runId = data.run_id;
_runIds[id] = runId;
const es = new EventSource('/encmodules/stream/' + runId);
_activeStreams[id] = es;
es.onmessage = function(e) {
const d = JSON.parse(e.data);
if (d.done) {
es.close();
delete _activeStreams[id];
resetRunState(id);
document.getElementById('run-live-dot').style.background = '#555';
return;
}
if (d.error) {
appendLine('[ERROR] ' + escHtml(d.error), 'error');
return;
}
if (d.line) {
const cls = d.found ? 'found'
: d.error ? 'error'
: d.line.includes('[WARN') || d.line.includes('ALERT') ? 'warn'
: d.line.includes('[MODULE]') ? 'module'
: 'info';
appendLine(escHtml(d.line), cls);
}
if (d.result) {
appendLine('--- Result ---', 'module');
appendLine(escHtml(JSON.stringify(d.result, null, 2)), 'module');
}
};
es.onerror = function() {
es.close();
delete _activeStreams[id];
resetRunState(id);
document.getElementById('run-live-dot').style.background = '#555';
};
} catch(err) {
appendLine('[ERROR] ' + err.message, 'error');
resetRunState(id);
}
}
async function stopModule(id) {
const runId = _runIds[id];
if (runId) await fetch('/encmodules/stop/' + runId, {method: 'POST'});
const es = _activeStreams[id];
if (es) { es.close(); delete _activeStreams[id]; }
resetRunState(id);
}
function resetRunState(id) {
document.getElementById('run-btn-' + id).style.display = '';
document.getElementById('stop-btn-' + id).style.display = 'none';
document.getElementById('card-' + id).classList.remove('running');
}
// ── Delete ────────────────────────────────────────────────────────────────────
async function deleteModule(id, filename) {
if (!confirm(`Remove module "${filename}" from the module directory?`)) return;
const res = await fetch('/encmodules/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({filename}),
});
const data = await res.json();
if (data.ok) {
document.getElementById('card-' + id)?.remove();
const remaining = document.querySelectorAll('.enc-card').length;
document.getElementById('enc-module-count').textContent =
remaining + ' module' + (remaining !== 1 ? 's' : '') + ' loaded';
} else {
alert('Delete failed: ' + (data.error || 'unknown'));
}
}
// ── Terminal helpers ──────────────────────────────────────────────────────────
function appendLine(html, cls = 'info') {
const t = document.getElementById('enc-terminal');
const div = document.createElement('div');
div.className = 'enc-line-' + cls;
div.innerHTML = html;
t.appendChild(div);
t.scrollTop = t.scrollHeight;
}
function clearOutput() {
document.getElementById('enc-terminal').innerHTML = '';
}
function exportOutput() {
const lines = Array.from(document.querySelectorAll('#enc-terminal div'))
.map(d => d.textContent).join('\n');
const blob = new Blob([lines], {type: 'text/plain'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'module_output.txt';
a.click();
}
function escHtml(s) {
return String(s)
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/"/g,'&quot;');
}
</script>
{% endblock %}