567 lines
22 KiB
HTML
567 lines
22 KiB
HTML
|
|
{% 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 %}
|