Autarch Will Control The Internet
This commit is contained in:
566
web/templates/encmodules.html
Normal file
566
web/templates/encmodules.html
Normal file
@@ -0,0 +1,566 @@
|
||||
{% 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">🔒</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">🔒</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">👁</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 }}')">✕ 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 }}')">
|
||||
🔑 Unlock & Run
|
||||
</button>
|
||||
<button class="btn btn-sm" style="color:var(--text-muted)"
|
||||
onclick="deleteModule('{{ mod.id }}','{{ mod.filename }}')">✕ 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,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
.replace(/"/g,'"');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user