Initial commit — SETEC LABS Manager (Setec_CDM)

Flask-based VPS management panel with SSH remote command execution.
Includes E2E encrypted SSH tunnel (AES-256-GCM + Go agent), setup wizard,
security hardening tools, DNS management, firewall configs, monitoring,
backup, and .sec patch update system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DigiJ
2026-03-13 12:39:02 -07:00
commit 9e839ee826
62 changed files with 14605 additions and 0 deletions

View File

@@ -0,0 +1,299 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SETEC LABS - {% block title %}Manager{% endblock %}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
background: #0a0a0a;
color: #00ff41;
min-height: 100vh;
display: flex;
}
a { color: #00ff41; text-decoration: none; }
a:hover { color: #fff; }
/* Sidebar */
.sidebar {
width: 220px;
background: #111;
border-right: 1px solid #00ff41;
padding: 0;
position: fixed;
top: 0; left: 0; bottom: 0;
display: flex;
flex-direction: column;
}
.sidebar-logo {
padding: 10px 10px;
border-bottom: 1px solid #333;
text-align: center;
}
.sidebar-logo img { width: 100%; max-width: 200px; height: auto; }
.sidebar nav { padding: 10px 0; flex: 1; overflow-y: auto; }
.sidebar nav a {
display: block;
padding: 10px 20px;
border-bottom: 1px solid #1a1a1a;
transition: background 0.2s;
}
.sidebar nav a:hover, .sidebar nav a.active {
background: #1a2a1a;
color: #fff;
}
.sidebar nav a .icon { margin-right: 8px; }
.sidebar-bottom {
border-top: 1px solid #333;
flex-shrink: 0;
}
.sidebar-bottom a {
display: block;
padding: 10px 20px;
border-bottom: 1px solid #1a1a1a;
transition: background 0.2s;
color: #00ff41;
text-decoration: none;
}
.sidebar-bottom a:hover, .sidebar-bottom a.active {
background: #1a2a1a;
color: #fff;
}
.sidebar-bottom a .icon { margin-right: 8px; }
.sidebar .ver {
text-align: center;
font-size: 10px;
color: #555;
padding: 8px 0;
}
/* Main content */
.main {
margin-left: 220px;
flex: 1;
padding: 20px 30px;
min-height: 100vh;
}
h1 {
font-size: 18px;
border-bottom: 1px solid #333;
padding-bottom: 8px;
margin-bottom: 20px;
}
h2 { font-size: 14px; margin: 15px 0 8px; color: #88ff88; }
/* Cards */
.card {
background: #111;
border: 1px solid #333;
padding: 15px;
margin-bottom: 15px;
}
.card-title {
font-size: 13px;
color: #88ff88;
margin-bottom: 10px;
border-bottom: 1px solid #222;
padding-bottom: 5px;
}
/* Buttons */
.btn {
background: #1a2a1a;
color: #00ff41;
border: 1px solid #00ff41;
padding: 6px 14px;
font-family: 'Courier New', monospace;
font-size: 12px;
cursor: pointer;
margin: 3px;
transition: all 0.2s;
}
.btn:hover { background: #00ff41; color: #000; }
.btn-danger { border-color: #ff4444; color: #ff4444; }
.btn-danger:hover { background: #ff4444; color: #000; }
.btn-warn { border-color: #ffaa00; color: #ffaa00; }
.btn-warn:hover { background: #ffaa00; color: #000; }
/* Output */
.output {
background: #000;
border: 1px solid #333;
padding: 12px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
max-height: 500px;
overflow-y: auto;
color: #00ff41;
margin-top: 10px;
}
.output .err { color: #ff4444; }
.output .info { color: #888; }
/* Forms */
input, select, textarea {
background: #000;
color: #00ff41;
border: 1px solid #333;
padding: 6px 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
margin: 3px;
}
input:focus, textarea:focus { border-color: #00ff41; outline: none; }
label { font-size: 12px; color: #888; display: block; margin: 5px 3px 2px; }
/* Table */
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { text-align: left; color: #888; border-bottom: 1px solid #333; padding: 6px; }
td { padding: 6px; border-bottom: 1px solid #1a1a1a; }
tr:hover { background: #1a1a1a; }
/* Grid */
.grid { display: grid; gap: 15px; }
.grid-2 { grid-template-columns: 1fr 1fr; }
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
/* Status indicators */
.status-ok { color: #00ff41; }
.status-err { color: #ff4444; }
.status-warn { color: #ffaa00; }
/* Loading */
.loading::after {
content: '...';
animation: dots 1s steps(3) infinite;
}
@keyframes dots {
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
}
.toolbar { margin-bottom: 15px; display: flex; gap: 5px; flex-wrap: wrap; }
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-logo">
<img src="{{ url_for('static', filename='setec_labs_logo.svg') }}" alt="SETEC LABS">
</div>
<nav>
<a href="/" class="{% if request.endpoint == 'dashboard' %}active{% endif %}">
<span class="icon">[~]</span> Dashboard
</a>
<a href="/docker" class="{% if request.endpoint == 'docker_page' %}active{% endif %}">
<span class="icon">[#]</span> Docker
</a>
<a href="/dns" class="{% if request.endpoint == 'dns_page' %}active{% endif %}">
<span class="icon">[@]</span> DNS
</a>
<a href="/nginx" class="{% if request.endpoint == 'nginx_page' %}active{% endif %}">
<span class="icon">[>]</span> Nginx
</a>
<a href="/smtp" class="{% if request.endpoint == 'smtp_page' %}active{% endif %}">
<span class="icon">[*]</span> SMTP
</a>
<a href="/firewall" class="{% if request.endpoint == 'firewall_page' %}active{% endif %}">
<span class="icon">[|]</span> Firewall
</a>
<a href="/fail2ban" class="{% if request.endpoint == 'fail2ban_page' %}active{% endif %}">
<span class="icon">[!]</span> Fail2Ban
</a>
<a href="/frontpage" class="{% if request.endpoint == 'frontpage_page' %}active{% endif %}">
<span class="icon">[&lt;]</span> Front Page
</a>
<a href="/security" class="{% if request.endpoint == 'security_page' %}active{% endif %}">
<span class="icon">[&amp;]</span> Security
</a>
<a href="/detect" class="{% if request.endpoint == 'detect_page' %}active{% endif %}">
<span class="icon">[?]</span> Detect
</a>
<a href="/configs" class="{% if request.endpoint == 'configs_page' %}active{% endif %}">
<span class="icon">[=]</span> Configs
</a>
<a href="/files" class="{% if request.endpoint == 'files_page' %}active{% endif %}">
<span class="icon">[/]</span> Files
</a>
<a href="/terminal" class="{% if request.endpoint == 'terminal_page' %}active{% endif %}">
<span class="icon">[$]</span> Terminal
</a>
<a href="/settings" class="{% if request.endpoint == 'settings_page' %}active{% endif %}">
<span class="icon">[%]</span> Settings
</a>
</nav>
<div class="sidebar-bottom">
<a href="/docs" class="{% if request.endpoint == 'docs_page' %}active{% endif %}">
<span class="icon">[^]</span> Docs
</a>
<a href="/wizard" class="{% if request.endpoint == 'wizard_page' %}active{% endif %}">
<span class="icon">[+]</span> Setup Wizard
</a>
<div class="ver">setec-mgr v2.0</div>
</div>
</div>
<div class="main">
{% block content %}{% endblock %}
</div>
<script>
async function api(url, opts = {}) {
const el = document.getElementById('output');
if (el) el.innerHTML = '<span class="info">Loading...</span>';
try {
const r = await fetch(url, opts);
const j = await r.json();
return j;
} catch (e) {
if (el) el.innerHTML = '<span class="err">Error: ' + e.message + '</span>';
return {ok: false, error: e.message};
}
}
async function apiGet(url) { return api(url); }
async function apiPost(url, body = {}) {
return api(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
});
}
async function apiDelete(url, body = {}) {
return api(url, {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
});
}
function showResult(res, elId = 'output') {
const el = document.getElementById(elId);
if (!el) return;
if (!res.ok) {
el.innerHTML = '<span class="err">ERROR: ' + (res.error || 'Unknown error') + '</span>';
return;
}
const d = res.data;
if (typeof d === 'string') {
el.textContent = d;
} else if (d && d.stdout !== undefined) {
let html = '';
if (d.stdout) html += d.stdout;
if (d.stderr) html += '<span class="err">' + escHtml(d.stderr) + '</span>';
if (d.exit_code && d.exit_code !== 0) html += '\n<span class="err">[exit code: ' + d.exit_code + ']</span>';
el.innerHTML = html || '<span class="info">(no output)</span>';
} else {
el.textContent = JSON.stringify(d, null, 2);
}
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,222 @@
{% extends "base.html" %}
{% block title %}Config Editor{% endblock %}
{% block content %}
<h1>[=] Config Editor</h1>
<div class="grid grid-2">
<div>
<div class="card">
<div class="card-title">Quick Access</div>
<h2>Nginx</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/nginx/nginx.conf')">nginx.conf</button>
<button class="btn" onclick="listDir('/etc/nginx/sites-available/')">sites-available/</button>
<button class="btn" onclick="listDir('/etc/nginx/conf.d/')">conf.d/</button>
<button class="btn" onclick="listDir('/etc/nginx/snippets/')">snippets/</button>
</div>
<h2>GitLab</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/gitlab/gitlab.rb')">gitlab.rb</button>
</div>
<h2>Gitea</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/gitea/app.ini')">app.ini (etc)</button>
<button class="btn" onclick="loadConfig('/var/lib/gitea/custom/conf/app.ini')">app.ini (custom)</button>
</div>
<h2>SSH</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/ssh/sshd_config')">sshd_config</button>
<button class="btn" onclick="loadConfig('/etc/ssh/ssh_config')">ssh_config</button>
</div>
<h2>Mail</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/postfix/main.cf')">postfix main.cf</button>
<button class="btn" onclick="loadConfig('/etc/postfix/master.cf')">postfix master.cf</button>
<button class="btn" onclick="loadConfig('/etc/opendkim.conf')">opendkim.conf</button>
</div>
<h2>Security</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/fail2ban/jail.local')">fail2ban jail</button>
<button class="btn" onclick="loadConfig('/etc/ufw/ufw.conf')">ufw.conf</button>
</div>
<h2>Docker</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/docker/daemon.json')">daemon.json</button>
<button class="btn" onclick="loadConfig('/opt/seteclabs/docker-compose.yml')">docker-compose.yml</button>
</div>
<h2>System</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/hosts')">hosts</button>
<button class="btn" onclick="loadConfig('/etc/hostname')">hostname</button>
<button class="btn" onclick="loadConfig('/etc/resolv.conf')">resolv.conf</button>
<button class="btn" onclick="loadConfig('/etc/fstab')">fstab</button>
<button class="btn" onclick="loadConfig('/etc/crontab')">crontab</button>
</div>
<h2>Custom Path</h2>
<div class="toolbar">
<input type="text" id="custom-path" placeholder="/etc/some/config.conf" style="width:300px">
<button class="btn" onclick="loadConfig(document.getElementById('custom-path').value)">Load</button>
</div>
</div>
<div class="card">
<div class="card-title">Directory Browser</div>
<div class="output" id="dir-list" style="max-height:300px"><span class="info">Click a directory button above</span></div>
</div>
</div>
<div>
<div class="card">
<div class="card-title">
Editor: <span id="current-file" style="color:#ffaa00">none</span>
</div>
<textarea id="config-editor" rows="30" style="width:100%;resize:vertical;tab-size:4" spellcheck="false"></textarea>
<div class="toolbar" style="margin-top:5px">
<button class="btn" onclick="saveConfig()">Save</button>
<button class="btn" onclick="testConfig()">Test Nginx</button>
<button class="btn" onclick="showDiff()">Diff vs Backup</button>
<button class="btn" onclick="showBackups()">List Backups</button>
<button class="btn btn-warn" onclick="reloadSvc()">Reload Service</button>
</div>
<div style="margin-top:5px">
<label>Reload service after save:</label>
<select id="reload-svc">
<option value="">-- none --</option>
<option value="nginx">nginx</option>
<option value="sshd">sshd</option>
<option value="postfix">postfix</option>
<option value="opendkim">opendkim</option>
<option value="fail2ban">fail2ban</option>
<option value="docker">docker</option>
<option value="redis">redis</option>
<option value="postgresql">postgresql</option>
<option value="mysql">mysql</option>
<option value="dovecot">dovecot</option>
<option value="grafana-server">grafana</option>
<option value="prometheus">prometheus</option>
</select>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready. Click a config file to edit.</span></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentFile = '';
// Check URL params for ?file=
const params = new URLSearchParams(window.location.search);
if (params.get('file')) {
loadConfig(params.get('file'));
}
async function loadConfig(path) {
if (!path) return;
currentFile = path;
document.getElementById('current-file').textContent = path;
document.getElementById('config-editor').value = 'Loading...';
const res = await apiGet('/api/configs/read?path=' + encodeURIComponent(path));
if (res.ok) {
document.getElementById('config-editor').value = res.data.stdout || '';
document.getElementById('output').innerHTML = '<span class="info">Loaded ' + escHtml(path) + ' (' + (res.data.stdout||'').split('\n').length + ' lines)</span>';
} else {
document.getElementById('config-editor').value = '';
document.getElementById('output').innerHTML = '<span class="err">' + escHtml(res.error || 'Failed to read file') + '</span>';
}
// Auto-detect reload service
if (path.includes('nginx')) document.getElementById('reload-svc').value = 'nginx';
else if (path.includes('postfix')) document.getElementById('reload-svc').value = 'postfix';
else if (path.includes('sshd') || path.includes('/ssh/')) document.getElementById('reload-svc').value = 'sshd';
else if (path.includes('opendkim')) document.getElementById('reload-svc').value = 'opendkim';
else if (path.includes('fail2ban')) document.getElementById('reload-svc').value = 'fail2ban';
else if (path.includes('docker')) document.getElementById('reload-svc').value = 'docker';
}
async function saveConfig() {
if (!currentFile) { alert('No file loaded'); return; }
if (!confirm('Save changes to ' + currentFile + '?\nA backup will be created automatically.')) return;
const content = document.getElementById('config-editor').value;
const res = await apiPost('/api/configs/write', {path: currentFile, content: content});
if (res.ok) {
document.getElementById('output').innerHTML = '<span class="status-ok">Saved ' + escHtml(currentFile) + ' (backup created)</span>';
// Auto-reload service if selected
const svc = document.getElementById('reload-svc').value;
if (svc) {
const r2 = await apiPost('/api/configs/reload-service', {service: svc});
showResult(r2);
}
} else {
showResult(res);
}
}
async function testConfig() {
const res = await apiPost('/api/configs/test-nginx');
showResult(res);
}
async function showDiff() {
if (!currentFile) return;
const res = await apiPost('/api/configs/diff', {path: currentFile});
showResult(res);
}
async function showBackups() {
if (!currentFile) return;
const res = await apiGet('/api/configs/backups?path=' + encodeURIComponent(currentFile));
showResult(res);
}
async function reloadSvc() {
const svc = document.getElementById('reload-svc').value;
if (!svc) { alert('Select a service to reload'); return; }
const res = await apiPost('/api/configs/reload-service', {service: svc});
showResult(res);
}
async function listDir(path) {
const res = await apiGet('/api/files/list?path=' + encodeURIComponent(path));
const el = document.getElementById('dir-list');
if (!res.ok) { el.innerHTML = '<span class="err">' + res.error + '</span>'; return; }
const lines = (res.data.stdout || '').trim().split('\n');
let html = '';
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 9) continue;
const fname = parts.slice(8).join(' ');
if (fname === '.' || fname === '..') continue;
const isDir = line.startsWith('d');
const fullPath = path.replace(/\/$/, '') + '/' + fname;
if (isDir) {
html += `<a href="#" onclick="listDir('${fullPath}/');return false" style="color:#ffaa00">${fname}/</a>\n`;
} else {
html += `<a href="#" onclick="loadConfig('${fullPath}');return false">${fname}</a>\n`;
}
}
el.innerHTML = html || '<span class="info">(empty)</span>';
}
// Handle tab key in editor
document.getElementById('config-editor').addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 4;
}
// Ctrl+S to save
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
saveConfig();
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>[~] Dashboard</h1>
<div class="toolbar">
<button class="btn" onclick="loadStatus()">Refresh Status</button>
<button class="btn" onclick="loadDomains()">Check Domains</button>
<button class="btn" onclick="deploySite()">Deploy Site</button>
<button class="btn" onclick="testSSH()">Test SSH</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Server Status</div>
<div class="output" id="status-output"><span class="info">Click "Refresh Status" to load</span></div>
</div>
<div class="card">
<div class="card-title">Domain Status</div>
<div class="output" id="domain-output"><span class="info">Click "Check Domains" to load</span></div>
</div>
</div>
<div class="card">
<div class="card-title">Quick Actions</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadStatus() {
const res = await apiGet('/api/status');
showResult(res, 'status-output');
}
async function loadDomains() {
const res = await apiGet('/api/status/domains');
showResult(res, 'domain-output');
}
async function deploySite() {
if (!confirm('Deploy site/ to VPS?')) return;
document.getElementById('output').innerHTML = '<span class="info">Deploying...</span>';
const res = await apiPost('/api/deploy/site');
showResult(res);
}
async function testSSH() {
const res = await apiGet('/api/ssh/test');
showResult(res);
}
// Auto-load on page load
loadStatus();
loadDomains();
</script>
{% endblock %}

View File

@@ -0,0 +1,140 @@
{% extends "base.html" %}
{% block title %}Service Detection{% endblock %}
{% block content %}
<h1>[?] Service Detection</h1>
<div class="toolbar">
<button class="btn" onclick="runScan()">Scan Server</button>
<button class="btn" onclick="browseAll()">Browse All 200+ Services</button>
</div>
<div id="scan-results"></div>
<div class="card" id="browse-panel" style="display:none">
<div class="card-title">Service Database ({{count}} services)</div>
<input type="text" id="svc-search" placeholder="Filter services..." style="width:100%;margin-bottom:10px" oninput="filterServices()">
<div id="all-services" style="max-height:600px;overflow-y:auto"></div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Click "Scan Server" to detect installed services</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
let allServices = {};
async function runScan() {
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="card-title">Scanning...</div><div class="output"><span class="info">Checking processes, ports, packages, configs, docker, systemd...</span></div></div>';
const res = await apiGet('/api/detect/scan');
if (!res.ok) {
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="output"><span class="err">'+res.error+'</span></div></div>';
return;
}
const services = res.data;
if (!services.length) {
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="output"><span class="info">No services detected (or SSH failed)</span></div></div>';
return;
}
// Group by category
const groups = {};
services.forEach(s => {
const cat = s.category;
if (!groups[cat]) groups[cat] = [];
groups[cat].push(s);
});
let html = '<div class="card"><div class="card-title">Detected Services (' + services.length + ' found)</div></div>';
for (const [cat, svcs] of Object.entries(groups)) {
html += '<div class="card"><div class="card-title">' + escHtml(cat) + ' (' + svcs.length + ')</div>';
html += '<table><thead><tr><th>Service</th><th>Confidence</th><th>Evidence</th><th>Configs</th><th>Actions</th></tr></thead><tbody>';
for (const s of svcs) {
const conf = s.score >= 5 ? 'status-ok' : s.score >= 3 ? 'status-warn' : 'status-err';
const confLabel = s.score >= 5 ? 'HIGH' : s.score >= 3 ? 'MED' : 'LOW';
const configLinks = s.configs.filter(c => c && !c.endsWith('/')).map(c =>
`<a href="#" onclick="editConfig('${c}');return false" style="font-size:11px">${c.split('/').pop()}</a>`
).join(', ');
const configDirs = s.configs.filter(c => c && c.endsWith('/')).map(c =>
`<a href="#" onclick="browseConfig('${c}');return false" style="font-size:11px;color:#888">${c}</a>`
).join(', ');
html += '<tr>';
html += '<td><strong>' + escHtml(s.name) + '</strong></td>';
html += '<td class="' + conf + '">' + confLabel + ' (' + s.score + ')</td>';
html += '<td style="font-size:11px;color:#888">' + s.evidence.map(escHtml).join(', ') + '</td>';
html += '<td>' + configLinks + (configDirs ? '<br>' + configDirs : '') + '</td>';
html += '<td>';
if (s.ports.length) html += '<span style="color:#555">ports: ' + s.ports.join(',') + '</span> ';
html += '</td>';
html += '</tr>';
}
html += '</tbody></table></div>';
}
document.getElementById('scan-results').innerHTML = html;
}
function editConfig(path) {
window.location.href = '/configs?file=' + encodeURIComponent(path);
}
function browseConfig(path) {
window.location.href = '/files?path=' + encodeURIComponent(path);
}
async function browseAll() {
const panel = document.getElementById('browse-panel');
panel.style.display = '';
if (Object.keys(allServices).length) return;
const res = await apiGet('/api/detect/all-services');
if (!res.ok) return;
allServices = res.data;
renderAllServices(allServices);
}
function renderAllServices(data) {
let html = '';
let total = 0;
for (const [cat, svcs] of Object.entries(data)) {
total += svcs.length;
html += '<h2 style="margin-top:10px">' + escHtml(cat) + ' (' + svcs.length + ')</h2>';
html += '<table><thead><tr><th>Service</th><th>Ports</th><th>Packages</th><th>Config Files</th></tr></thead><tbody>';
for (const s of svcs) {
html += '<tr>';
html += '<td>' + escHtml(s.name) + '</td>';
html += '<td style="color:#888">' + (s.ports.length ? s.ports.join(', ') : '-') + '</td>';
html += '<td style="font-size:11px;color:#888">' + (s.packages.length ? s.packages.join(', ') : '-') + '</td>';
html += '<td style="font-size:11px">' + (s.configs.length ? s.configs.map(c =>
'<a href="#" onclick="editConfig(\'' + c + '\');return false">' + c + '</a>'
).join('<br>') : '-') + '</td>';
html += '</tr>';
}
html += '</tbody></table>';
}
document.getElementById('all-services').innerHTML = html;
document.querySelector('#browse-panel .card-title').textContent = 'Service Database (' + total + ' services)';
}
function filterServices() {
const q = document.getElementById('svc-search').value.toLowerCase();
if (!q) { renderAllServices(allServices); return; }
const filtered = {};
for (const [cat, svcs] of Object.entries(allServices)) {
const matches = svcs.filter(s =>
s.name.toLowerCase().includes(q) ||
cat.toLowerCase().includes(q) ||
s.packages.some(p => p.includes(q)) ||
s.configs.some(c => c.includes(q))
);
if (matches.length) filtered[cat] = matches;
}
renderAllServices(filtered);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,101 @@
{% extends "base.html" %}
{% block title %}DNS{% endblock %}
{% block content %}
<h1>[@] DNS Management</h1>
<div class="toolbar">
<button class="btn" onclick="loadRecords()">Refresh Records</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">DNS Records (Hostinger)</div>
<div id="dns-records" class="output" style="max-height:600px"><span class="info">Loading...</span></div>
</div>
<div>
<div class="card">
<div class="card-title">Add Record</div>
<label>Type</label>
<select id="dns-type">
<option value="A">A</option>
<option value="TXT">TXT</option>
</select>
<label>Name (subdomain or @ for root)</label>
<input type="text" id="dns-name" placeholder="e.g. app" style="width:100%">
<label>Value</label>
<input type="text" id="dns-value" placeholder="e.g. 31.220.20.55" style="width:100%">
<br><br>
<button class="btn" onclick="addRecord()">Add Record</button>
</div>
<div class="card">
<div class="card-title">Delete Record</div>
<label>Record ID</label>
<input type="text" id="dns-del-id" placeholder="record ID from list" style="width:100%">
<br><br>
<button class="btn btn-danger" onclick="deleteRecord()">Delete Record</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadRecords() {
const el = document.getElementById('dns-records');
el.innerHTML = '<span class="info">Loading...</span>';
const res = await apiGet('/api/dns/records');
if (!res.ok) { el.innerHTML = '<span class="err">'+res.error+'</span>'; return; }
const records = Array.isArray(res.data) ? res.data : (res.data.records || [res.data]);
let html = '';
for (const r of records) {
if (Array.isArray(r)) {
for (const rec of r) {
html += formatRecord(rec);
}
} else {
html += formatRecord(r);
}
}
el.innerHTML = html || '<span class="info">No records found</span>';
}
function formatRecord(r) {
const id = r.id || r.record_id || '?';
const type = r.type || '?';
const name = r.name || r.host || '?';
const val = r.content || r.value || r.address || '?';
const ttl = r.ttl || '';
return `<div style="margin-bottom:4px;border-bottom:1px solid #222;padding-bottom:4px">` +
`<span style="color:#888">[${id}]</span> ` +
`<span style="color:#ffaa00">${type}</span> ` +
`${name}${val} ` +
`<span style="color:#555">TTL:${ttl}</span></div>`;
}
async function addRecord() {
const type = document.getElementById('dns-type').value;
const name = document.getElementById('dns-name').value;
const value = document.getElementById('dns-value').value;
if (!name || !value) { alert('Fill in name and value'); return; }
const res = await apiPost('/api/dns/add', {type, name, value});
showResult(res);
loadRecords();
}
async function deleteRecord() {
const id = document.getElementById('dns-del-id').value;
if (!id) return;
if (!confirm('Delete record '+id+'?')) return;
const res = await apiDelete('/api/dns/delete/'+id);
showResult(res);
loadRecords();
}
loadRecords();
</script>
{% endblock %}

View File

@@ -0,0 +1,334 @@
{% extends "base.html" %}
{% block title %}Docker{% endblock %}
{% block content %}
<h1>[#] Docker Management</h1>
<div class="toolbar">
<button class="btn" onclick="loadContainers()">Refresh</button>
<button class="btn" onclick="composeAction('up')">Compose Up</button>
<button class="btn btn-warn" onclick="composeAction('down')">Compose Down</button>
<button class="btn" onclick="composeAction('pull')">Compose Pull</button>
<button class="btn" onclick="showTab('containers')">Containers</button>
<button class="btn" onclick="showTab('store')">App Store</button>
<button class="btn" onclick="showTab('install')">Install from Git/URL</button>
</div>
<!-- TAB: Containers -->
<div id="tab-containers">
<div class="card">
<div class="card-title">Running Containers</div>
<table>
<thead><tr><th>Name</th><th>Image</th><th>Status</th><th>Ports / Web UI</th><th>Actions</th></tr></thead>
<tbody id="container-list"><tr><td colspan="5" class="info">Loading...</td></tr></tbody>
</table>
</div>
<div class="card">
<div class="card-title">Container Logs</div>
<div class="toolbar">
<input type="text" id="log-container" placeholder="container name" style="width:200px">
<input type="number" id="log-lines" value="50" style="width:80px">
<button class="btn" onclick="loadLogs()">View Logs</button>
</div>
<div class="output" id="output"><span class="info">Select a container to view logs</span></div>
</div>
</div>
<!-- TAB: App Store -->
<div id="tab-store" style="display:none">
<div class="card">
<div class="card-title">Docker App Store</div>
<div class="toolbar">
<input type="text" id="store-search" placeholder="Search apps..." style="width:300px" oninput="filterStore()">
<select id="store-cat" onchange="filterStore()">
<option value="">All Categories</option>
</select>
</div>
<div id="store-grid" class="grid grid-2" style="margin-top:10px"></div>
</div>
<div class="card">
<div class="card-title">Install Output</div>
<div class="output" id="store-output"><span class="info">Select an app to install</span></div>
</div>
</div>
<!-- TAB: Install from Git/URL -->
<div id="tab-install" style="display:none">
<div class="grid grid-2">
<div class="card">
<div class="card-title">Install from GitHub / Git Repo</div>
<label>Git Repository URL</label>
<input type="text" id="git-repo" placeholder="https://github.com/user/repo.git" style="width:100%">
<label>App Name (optional, auto-detected from URL)</label>
<input type="text" id="git-name" placeholder="my-app" style="width:100%">
<br><br>
<button class="btn" onclick="installGit()">Clone & Deploy</button>
<p style="font-size:11px;color:#555;margin-top:8px">
Clones the repo to /opt/seteclabs/&lt;name&gt; and runs docker compose up if a compose file is found.
</p>
</div>
<div class="card">
<div class="card-title">Install from URL (tar.gz, zip, binary)</div>
<label>Download URL</label>
<input type="text" id="url-url" placeholder="https://example.com/app.tar.gz" style="width:100%">
<label>App Name</label>
<input type="text" id="url-name" placeholder="my-app" style="width:100%">
<br><br>
<button class="btn" onclick="installUrl()">Download & Deploy</button>
<p style="font-size:11px;color:#555;margin-top:8px">
Downloads to /opt/seteclabs/&lt;name&gt;/, extracts if archive, runs docker compose if found.
</p>
</div>
</div>
<div class="card">
<div class="card-title">Install from Docker Compose (paste YAML)</div>
<label>App Name</label>
<input type="text" id="compose-name" placeholder="my-app" style="width:300px">
<label>Docker Compose YAML</label>
<textarea id="compose-yaml" rows="12" style="width:100%;tab-size:2" placeholder="services:
myapp:
image: myimage:latest
ports:
- '8080:80'"></textarea>
<br>
<button class="btn" onclick="installCompose()">Deploy Compose</button>
</div>
<div class="card">
<div class="card-title">Install Output</div>
<div class="output" id="install-output"><span class="info">Ready.</span></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const VPS_IP = '31.220.20.55';
let storeData = [];
// Known container -> web UI port mappings
const UI_MAP = {
'gitea': {port: 3000, proto: 'https', host: 'repo.seteclabs.io', path: '/'},
'gitlab': {port: 8080, proto: 'https', host: 'git.seteclabs.io', path: '/'},
'listmonk': {port: 9000, proto: 'https', host: 'lists.seteclabs.io', path: '/'},
'portainer': {port: 9443, proto: 'https', path: '/'},
'uptime-kuma': {port: 3001, path: '/'},
'grafana': {port: 3002, path: '/'},
'netdata': {port: 19999, path: '/'},
'nginx-proxy-manager': {port: 81, path: '/'},
'dockge': {port: 5001, path: '/'},
'nextcloud': {port: 8081, path: '/'},
'filebrowser': {port: 8082, path: '/'},
'vaultwarden': {port: 8083, path: '/'},
'wikijs': {port: 3003, path: '/'},
'n8n': {port: 5678, path: '/'},
'nodered': {port: 1880, path: '/'},
'privatebin': {port: 8090, path: '/'},
'it-tools': {port: 8091, path: '/'},
'cyberchef': {port: 8092, path: '/'},
'jellyfin': {port: 8096, path: '/'},
'homer': {port: 8094, path: '/'},
'homarr': {port: 7575, path: '/'},
'wg-easy': {port: 51821, path: '/'},
'woodpecker': {port: 8000, path: '/'},
'plausible': {port: 8088, path: '/'},
'umami': {port: 8089, path: '/'},
'gotify': {port: 8085, path: '/'},
'ntfy': {port: 8086, path: '/'},
'cockpit': {port: 9090, path: '/'},
'minio': {port: 9011, path: '/'},
'bookstack': {port: 6875, path: '/'},
'stirling-pdf': {port: 8093, path: '/'},
};
function getWebLink(name, ports) {
// Check known map first
const known = UI_MAP[name];
if (known) {
const host = known.host || VPS_IP;
const proto = known.proto || 'http';
const port = (proto === 'https' && (known.port === 443 || known.host)) ? '' : ':' + known.port;
return `${proto}://${host}${port}${known.path}`;
}
// Try to extract from port mapping string
if (ports) {
const match = ports.match(/0\.0\.0\.0:(\d+)/);
if (match) return `http://${VPS_IP}:${match[1]}`;
const match2 = ports.match(/:::(\d+)/);
if (match2) return `http://${VPS_IP}:${match2[1]}`;
}
return null;
}
// Tab switching
function showTab(tab) {
document.getElementById('tab-containers').style.display = tab === 'containers' ? '' : 'none';
document.getElementById('tab-store').style.display = tab === 'store' ? '' : 'none';
document.getElementById('tab-install').style.display = tab === 'install' ? '' : 'none';
if (tab === 'store' && !storeData.length) loadStore();
}
// ── Containers ──
async function loadContainers() {
const res = await apiGet('/api/docker/list');
const tbody = document.getElementById('container-list');
if (!res.ok) { tbody.innerHTML = '<tr><td colspan="5" class="err">'+res.error+'</td></tr>'; return; }
const lines = res.data.stdout.trim().split('\n').filter(l => l);
if (!lines.length) { tbody.innerHTML = '<tr><td colspan="5" class="info">No containers</td></tr>'; return; }
tbody.innerHTML = lines.map(line => {
const [id, name, image, status, ports] = line.split('|');
const running = status && status.includes('Up');
const webLink = getWebLink(name, ports);
const portDisplay = webLink
? `<a href="${webLink}" target="_blank" style="color:#00ff41">${webLink}</a>`
: `<span style="color:#888;font-size:11px">${ports||'none'}</span>`;
return `<tr>
<td><strong>${name||''}</strong></td>
<td style="color:#888;font-size:11px">${image||''}</td>
<td class="${running?'status-ok':'status-err'}">${status||''}</td>
<td>${portDisplay}</td>
<td>
<button class="btn" onclick="dockerAction('restart','${name}')">restart</button>
${running
? `<button class="btn btn-warn" onclick="dockerAction('stop','${name}')">stop</button>`
: `<button class="btn" onclick="dockerAction('start','${name}')">start</button>`}
<button class="btn" onclick="viewLogs('${name}')">logs</button>
</td>
</tr>`;
}).join('');
}
async function dockerAction(action, name) {
const res = await apiPost('/api/docker/'+action+'/'+name);
showResult(res);
setTimeout(loadContainers, 1000);
}
async function composeAction(action) {
document.getElementById('output').innerHTML = '<span class="info">Running compose '+action+'...</span>';
const res = await apiPost('/api/docker/compose/'+action);
showResult(res);
loadContainers();
}
function viewLogs(name) {
showTab('containers');
document.getElementById('log-container').value = name;
loadLogs();
}
async function loadLogs() {
const name = document.getElementById('log-container').value;
const lines = document.getElementById('log-lines').value;
if (!name) return;
const res = await apiGet('/api/docker/logs/'+name+'?lines='+lines);
showResult(res);
}
// ── Store ──
async function loadStore() {
const res = await apiGet('/api/docker/store');
if (!res.ok) return;
storeData = res.data;
const catSelect = document.getElementById('store-cat');
const cats = res.categories || [...new Set(storeData.map(a => a.cat))];
cats.forEach(c => {
const opt = document.createElement('option');
opt.value = c; opt.textContent = c;
catSelect.appendChild(opt);
});
renderStore(storeData);
}
function filterStore() {
const q = document.getElementById('store-search').value.toLowerCase();
const cat = document.getElementById('store-cat').value;
const filtered = storeData.filter(a =>
(!q || a.name.toLowerCase().includes(q) || a.desc.toLowerCase().includes(q)) &&
(!cat || a.cat === cat)
);
renderStore(filtered);
}
function renderStore(apps) {
const grid = document.getElementById('store-grid');
grid.innerHTML = apps.map(a => {
const ports = Object.entries(a.ports).map(([p, l]) => `${p} (${l})`).join(', ');
return `<div class="card" style="padding:12px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<strong style="color:#00ff41;font-size:14px">${escHtml(a.name)}</strong>
<span style="color:#555;font-size:11px;margin-left:8px">${escHtml(a.cat)}</span>
</div>
<button class="btn" onclick="installStore('${escHtml(a.name)}')">Install</button>
</div>
<div style="color:#aaa;font-size:12px;margin:6px 0">${escHtml(a.desc)}</div>
<div style="font-size:11px;color:#555">
Ports: ${ports}<br>
Image: ${escHtml(a.image)}
</div>
${a.notes ? `<div style="font-size:11px;color:#ffaa00;margin-top:4px">${escHtml(a.notes)}</div>` : ''}
</div>`;
}).join('');
if (!apps.length) grid.innerHTML = '<div class="info" style="padding:20px">No apps match your search</div>';
}
async function installStore(name) {
if (!confirm('Install ' + name + '?\nThis will add it to docker-compose.yml and start it.')) return;
const out = document.getElementById('store-output');
out.innerHTML = '<span class="info">Installing ' + name + '...</span>';
const res = await apiPost('/api/docker/store/install', {name});
showResult(res, 'store-output');
loadContainers();
}
// ── Git/URL Install ──
async function installGit() {
const repo = document.getElementById('git-repo').value.trim();
const name = document.getElementById('git-name').value.trim();
if (!repo) { alert('Enter a git repo URL'); return; }
const out = document.getElementById('install-output');
out.innerHTML = '<span class="info">Cloning and deploying...</span>';
const res = await apiPost('/api/docker/install-git', {repo, name});
showResult(res, 'install-output');
loadContainers();
}
async function installUrl() {
const url = document.getElementById('url-url').value.trim();
const name = document.getElementById('url-name').value.trim();
if (!url || !name) { alert('Enter URL and app name'); return; }
const out = document.getElementById('install-output');
out.innerHTML = '<span class="info">Downloading and deploying...</span>';
const res = await apiPost('/api/docker/install-url', {url, name});
showResult(res, 'install-output');
loadContainers();
}
async function installCompose() {
const name = document.getElementById('compose-name').value.trim();
const compose = document.getElementById('compose-yaml').value;
if (!name || !compose) { alert('Enter app name and compose YAML'); return; }
const out = document.getElementById('install-output');
out.innerHTML = '<span class="info">Deploying compose stack...</span>';
const res = await apiPost('/api/docker/install-compose', {name, compose});
showResult(res, 'install-output');
loadContainers();
}
// Tab key in compose textarea
document.getElementById('compose-yaml').addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const s = this.selectionStart;
this.value = this.value.substring(0, s) + ' ' + this.value.substring(this.selectionEnd);
this.selectionStart = this.selectionEnd = s + 2;
}
});
loadContainers();
</script>
{% endblock %}

View File

@@ -0,0 +1,609 @@
{% extends "base.html" %}
{% block title %}Documentation{% endblock %}
{% block content %}
<h1>[^] Documentation</h1>
<div class="toolbar">
<button class="btn" onclick="showDoc('manual')">User Manual</button>
<button class="btn" onclick="showDoc('hostlinks')">Host API / SSH Links</button>
<button class="btn" onclick="showDoc('troubleshoot')">Troubleshooting Guide</button>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- USER MANUAL -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="doc-manual" class="card">
<div class="card-title">User Manual</div>
<div style="font-size:12px;line-height:1.7;color:#ccc">
<h2>1. Introduction</h2>
<p>SETEC LABS Manager is a web-based VPS management panel that connects to your Linux server over SSH.
It provides a terminal-style interface for managing security tools, firewalls, DNS, nginx, Docker,
email, and more &mdash; all from your browser.</p>
<p style="color:#888;font-size:11px">Requirements: A Linux VPS (Debian/Ubuntu recommended), SSH key access, Python 3.10+</p>
<h2>2. Installation</h2>
<div style="background:#000;padding:10px;border:1px solid #333;margin:8px 0">
<span style="color:#888"># Clone the repository</span><br>
<span style="color:#00ff41">git clone https://repo.seteclabs.io/setec/setec-mgr.git</span><br>
<span style="color:#00ff41">cd setec-mgr</span><br><br>
<span style="color:#888"># Install dependencies</span><br>
<span style="color:#00ff41">pip install -r requirements.txt</span><br><br>
<span style="color:#888"># Start the manager</span><br>
<span style="color:#00ff41">python app.py</span><br><br>
<span style="color:#888"># Open in browser</span><br>
<span style="color:#00ff41">http://localhost:5000</span>
</div>
<h2>3. Initial Setup (Setup Wizard)</h2>
<p>On first launch, click <strong style="color:#00ff41">Setup Wizard</strong> in the sidebar. The wizard walks you through:</p>
<ol style="margin-left:15px;line-height:2">
<li><strong style="color:#88ff88">Terms of Service</strong> &mdash; Read and accept the disclaimer.</li>
<li><strong style="color:#88ff88">SSH Keys</strong> &mdash; Select existing keys or generate new ones with host-specific guidance.</li>
<li><strong style="color:#88ff88">VPS Connection</strong> &mdash; Enter your server IP, SSH username, port (2222 recommended), and key path.</li>
<li><strong style="color:#88ff88">DNS API</strong> &mdash; Select your hosting provider, enter your domain and API key.</li>
<li><strong style="color:#88ff88">Paths</strong> &mdash; Set web root and Docker Compose file location.</li>
<li><strong style="color:#88ff88">Connection Test</strong> &mdash; Verify SSH and API connectivity.</li>
</ol>
<p style="color:#888;font-size:11px">You can re-run the wizard at any time. All settings are also editable from the Settings page.</p>
<h2>4. Dashboard</h2>
<p>The Dashboard shows a real-time overview of your server:</p>
<ul style="margin-left:15px;line-height:2">
<li><strong>System Info</strong> &mdash; Hostname, OS, kernel, uptime</li>
<li><strong>Resource Usage</strong> &mdash; CPU, RAM, disk, swap</li>
<li><strong>Network</strong> &mdash; Active connections, listening ports</li>
<li><strong>Services</strong> &mdash; Status of key services (nginx, sshd, etc.)</li>
</ul>
<h2>5. Docker Management</h2>
<p>Manage containers, images, volumes, and networks. Start/stop/restart containers, view logs,
pull images, and manage your Docker Compose stack.</p>
<div style="background:#000;padding:8px;border:1px solid #333;margin:8px 0;font-size:11px;color:#888">
Note: SETEC runs services natively with systemd by default. Docker management is provided for
users who have containerized workloads.
</div>
<h2>6. DNS Management</h2>
<p>View, add, and delete DNS records through your hosting provider's API. Supports:</p>
<ul style="margin-left:15px;line-height:2">
<li>A, AAAA, CNAME, MX, TXT, NS records</li>
<li>10 hosting providers (see Host API / SSH Links tab)</li>
<li>Fallback to <code style="color:#00ff41">dig</code> when API is unavailable</li>
</ul>
<h2>7. Nginx Management</h2>
<p>Create and manage nginx virtual hosts, enable/disable sites, view access and error logs,
test configuration, and manage SSL certificates with Let's Encrypt (certbot).</p>
<h2>8. SMTP / Email</h2>
<p>Configure and manage mail services. View mail queue, check DKIM/SPF/DMARC records,
test email delivery, and manage Postfix configuration.</p>
<h2>9. Firewall</h2>
<p>The Firewall page (separate from Security) provides:</p>
<ul style="margin-left:15px;line-height:2">
<li><strong>Dashboard</strong> &mdash; Firewall activity overview and monitoring</li>
<li><strong>UFW</strong> &mdash; Simplified firewall rule management</li>
<li><strong>iptables</strong> &mdash; Advanced packet filtering rules</li>
<li><strong>nftables</strong> &mdash; Modern netfilter framework management</li>
<li><strong>firewalld</strong> &mdash; Zone-based firewall management</li>
<li><strong>CSF</strong> &mdash; ConfigServer Security &amp; Firewall</li>
<li><strong>Migration</strong> &mdash; Convert between UFW and iptables with one click</li>
</ul>
<h2>10. Fail2Ban</h2>
<p>Manage Fail2Ban jails, view banned IPs, check jail status, and configure ban rules
to protect against brute-force attacks on SSH, nginx, and other services.</p>
<h2>11. Security Center</h2>
<p>The Security page is your central hub for hardening and monitoring:</p>
<p style="color:#ffaa00;margin-top:10px">Hardening Tools</p>
<ul style="margin-left:15px;line-height:2">
<li><strong>SSH Hardening</strong> &mdash; Disable root login, enforce key auth, change port</li>
<li><strong>Kernel Hardening</strong> &mdash; Sysctl tweaks for network and memory protection</li>
<li><strong>Auto Updates</strong> &mdash; Enable unattended-upgrades for security patches</li>
<li><strong>.sec Patch System</strong> &mdash; Apply SETEC-curated distro-specific security patches</li>
</ul>
<p style="color:#ffaa00;margin-top:10px">Security Applications (each with full management tab)</p>
<table style="margin:8px 0">
<tr><th>App</th><th>Purpose</th></tr>
<tr><td style="color:#00ff41">ClamAV</td><td>Antivirus scanning, quarantine management, scheduled scans</td></tr>
<tr><td style="color:#00ff41">rkhunter</td><td>Rootkit detection, file property checks</td></tr>
<tr><td style="color:#00ff41">chkrootkit</td><td>Alternative rootkit scanner with expert mode</td></tr>
<tr><td style="color:#00ff41">Lynis</td><td>Security auditing and hardening index scoring</td></tr>
<tr><td style="color:#00ff41">OSSEC</td><td>Host-based intrusion detection (HIDS), log monitoring, alerts</td></tr>
<tr><td style="color:#00ff41">ModSecurity</td><td>Web application firewall (WAF) for nginx, OWASP CRS rules</td></tr>
<tr><td style="color:#00ff41">AIDE</td><td>File integrity monitoring, baseline comparison</td></tr>
<tr><td style="color:#00ff41">Cowrie</td><td>SSH/Telnet honeypot for attacker monitoring</td></tr>
</table>
<p style="color:#888;font-size:11px">Each app tab provides: install/uninstall, status, configuration, scanning/auditing, logs, and scheduled tasks.</p>
<h2>12. Detect</h2>
<p>Server detection and fingerprinting. Identifies installed software, open ports,
running services, and potential security issues.</p>
<h2>13. Configs</h2>
<p>View and edit critical configuration files directly: sshd_config, nginx.conf,
jail.local, and other system configs with syntax-aware editing.</p>
<h2>14. Files</h2>
<p>Browse the server filesystem, view file contents, upload and download files,
manage permissions, and navigate directories.</p>
<h2>15. Terminal</h2>
<p>Direct SSH terminal access from the browser. Execute commands on your server
with full output display. Useful for tasks not covered by the GUI.</p>
<h2>16. Settings</h2>
<p>Configure all SETEC Manager settings:</p>
<ul style="margin-left:15px;line-height:2">
<li><strong>VPS Connection</strong> &mdash; Host, user, port, SSH key path</li>
<li><strong>Hosting Provider API</strong> &mdash; Provider selection, API key, documentation links</li>
<li><strong>Domain &amp; Paths</strong> &mdash; Domain, web root, compose path</li>
</ul>
<h2>17. Front Page</h2>
<p>Manage the public-facing landing page for your domain. Edit content,
configure styling, and deploy updates.</p>
<h2>18. Keyboard Shortcuts &amp; Tips</h2>
<ul style="margin-left:15px;line-height:2">
<li>All actions use AJAX &mdash; the page never fully reloads</li>
<li>Output panels are scrollable; long scan outputs won't overflow</li>
<li>Red text = error, yellow text = warning, green text = success</li>
<li>Every destructive action (uninstall, delete, purge) requires confirmation</li>
<li>SSH connection is shared &mdash; the manager reuses a single SSH session</li>
</ul>
<h2>19. Getting Help</h2>
<ul style="margin-left:15px;line-height:2">
<li>Official repo: <a href="https://repo.seteclabs.io" target="_blank">repo.seteclabs.io</a></li>
<li>GitHub mirror: <a href="https://github.com/DigiJEth" target="_blank">github.com/DigiJEth</a></li>
<li>Submit issues and feature requests at the Gitea repo</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- HOST API / SSH LINKS -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="doc-hostlinks" class="card" style="display:none">
<div class="card-title">Host API / SSH Links</div>
<div style="font-size:12px;line-height:1.7;color:#ccc">
<p style="color:#888;margin-bottom:15px">Quick-access links to API key generation and SSH key setup guides for every supported hosting provider.</p>
<!-- Hostinger -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Hostinger</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key Gen:</td>
<td>hPanel &rarr; Profile &rarr; API Keys &rarr; Create new key with DNS permissions<br>
<a href="https://developers.hostinger.com" target="_blank">developers.hostinger.com</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>hPanel &rarr; VPS &rarr; Settings &rarr; SSH Keys &rarr; Add SSH Key<br>
<a href="https://support.hostinger.com/en/articles/1583522-how-to-generate-ssh-keys" target="_blank">support.hostinger.com/.../how-to-generate-ssh-keys</a></td>
</tr>
</table>
</div>
<!-- Cloudflare -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Cloudflare</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Token:</td>
<td>dash.cloudflare.com &rarr; My Profile &rarr; API Tokens &rarr; Create Token &rarr; "Edit zone DNS" template<br>
<a href="https://developers.cloudflare.com/api" target="_blank">developers.cloudflare.com/api</a></td>
</tr>
<tr>
<td style="color:#888">SSH (Tunnel):</td>
<td>Cloudflare is a DNS/CDN provider, not a VPS host. If using Cloudflare Tunnel for SSH:<br>
<a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/use-cases/ssh/" target="_blank">developers.cloudflare.com/.../ssh</a></td>
</tr>
</table>
</div>
<!-- DigitalOcean -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>DigitalOcean</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Token:</td>
<td>cloud.digitalocean.com &rarr; API &rarr; Tokens &rarr; Generate New Token (read+write)<br>
<a href="https://docs.digitalocean.com/reference/api" target="_blank">docs.digitalocean.com/reference/api</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Settings &rarr; Security &rarr; SSH Keys &rarr; Add SSH Key<br>
<a href="https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/" target="_blank">docs.digitalocean.com/.../add-ssh-keys</a></td>
</tr>
</table>
</div>
<!-- Vultr -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Vultr</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>my.vultr.com &rarr; Account &rarr; API &rarr; Enable API &rarr; copy key (whitelist server IP)<br>
<a href="https://www.vultr.com/api" target="_blank">vultr.com/api</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Account &rarr; SSH Keys &rarr; Add SSH Key<br>
<a href="https://docs.vultr.com/how-do-i-generate-ssh-keys" target="_blank">docs.vultr.com/how-do-i-generate-ssh-keys</a></td>
</tr>
</table>
</div>
<!-- Linode -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Linode (Akamai)</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Token:</td>
<td>cloud.linode.com &rarr; My Profile &rarr; API Tokens &rarr; Create Personal Access Token (Domains read/write)<br>
<a href="https://www.linode.com/docs/api" target="_blank">linode.com/docs/api</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Profile &rarr; SSH Keys &rarr; Add SSH Key (injected into new Linodes at creation)<br>
<a href="https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/" target="_blank">linode.com/docs/.../use-public-key-authentication-with-ssh</a></td>
</tr>
</table>
</div>
<!-- GoDaddy -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>GoDaddy</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>developer.godaddy.com &rarr; API Keys &rarr; Create New API Key. Format: <span style="color:#00ff41">key:secret</span><br>
<a href="https://developer.godaddy.com" target="_blank">developer.godaddy.com</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>GoDaddy is primarily a domain registrar. For VPS/dedicated hosting SSH:<br>
<a href="https://www.godaddy.com/help/generate-ssh-keys-40767" target="_blank">godaddy.com/help/generate-ssh-keys-40767</a></td>
</tr>
</table>
</div>
<!-- Namecheap -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Namecheap</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>Profile &rarr; Tools &rarr; API Access &rarr; Enable API (requires IP whitelist)<br>
<a href="https://www.namecheap.com/support/api/intro" target="_blank">namecheap.com/support/api/intro</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Namecheap is primarily a domain registrar. For hosting products:<br>
<a href="https://www.namecheap.com/support/knowledgebase/article.aspx/9356/69/how-to-generate-an-ssh-key/" target="_blank">namecheap.com/.../how-to-generate-an-ssh-key</a></td>
</tr>
</table>
</div>
<!-- Hetzner -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Hetzner</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Token:</td>
<td>dns.hetzner.com &rarr; API Tokens &rarr; Create new token<br>
<a href="https://dns.hetzner.com/api-docs" target="_blank">dns.hetzner.com/api-docs</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Cloud Console &rarr; Security &rarr; SSH Keys &rarr; Add SSH Key<br>
<a href="https://docs.hetzner.com/cloud/servers/getting-started/connecting-to-the-server/" target="_blank">docs.hetzner.com/.../connecting-to-the-server</a></td>
</tr>
</table>
</div>
<!-- OVH -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>OVH / OVHcloud</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>Requires Application Key, Application Secret, and Consumer Key<br>
<a href="https://api.ovh.com/createApp" target="_blank">api.ovh.com/createApp</a><br>
<a href="https://api.ovh.com" target="_blank">api.ovh.com (documentation)</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Control Panel &rarr; Public Cloud &rarr; SSH Keys &rarr; Add key<br>
<a href="https://help.ovhcloud.com/csm/en-dedicated-servers-creating-ssh-keys" target="_blank">help.ovhcloud.com/.../creating-ssh-keys</a></td>
</tr>
</table>
</div>
<!-- AWS Route 53 -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>AWS Route 53</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>IAM Console &rarr; Users &rarr; Create Access Key (needs AmazonRoute53FullAccess). Format: <span style="color:#00ff41">ACCESS_KEY:SECRET_KEY</span><br>
<a href="https://docs.aws.amazon.com/Route53/latest/APIReference" target="_blank">docs.aws.amazon.com/Route53/latest/APIReference</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>EC2 Console &rarr; Key Pairs &rarr; Create/Import key pair (downloads .pem file)<br>
<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html" target="_blank">docs.aws.amazon.com/.../ec2-key-pairs.html</a></td>
</tr>
</table>
</div>
<!-- Contabo -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Contabo</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API:</td>
<td>Contabo does not provide a DNS API. Use a third-party DNS provider (Cloudflare, etc.) or manage records manually.<br>
<a href="https://api.contabo.com" target="_blank">api.contabo.com (server management API only)</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>No panel-based SSH key manager. Generate keys locally and copy with <code style="color:#00ff41">ssh-copy-id</code><br>
<a href="https://contabo.com/blog/establishing-connection-server-ssh/" target="_blank">contabo.com/blog/establishing-connection-server-ssh</a></td>
</tr>
</table>
</div>
<!-- Generic SSH keygen -->
<div style="border:1px solid #00ff41;padding:12px;margin-top:15px;background:#0a0a0a">
<p style="color:#88ff88;font-size:13px;margin-bottom:8px"><strong>Universal: Generate SSH Keys (any provider)</strong></p>
<div style="background:#000;padding:10px;border:1px solid #333;margin:8px 0;font-size:11px">
<span style="color:#888"># Generate ed25519 key pair (recommended)</span><br>
<span style="color:#00ff41">ssh-keygen -t ed25519 -f C:/keys/setec -N ""</span><br><br>
<span style="color:#888"># Copy public key to server</span><br>
<span style="color:#00ff41">ssh-copy-id -i C:/keys/setec.pub -p 2222 root@YOUR_SERVER_IP</span><br><br>
<span style="color:#888"># Test connection</span><br>
<span style="color:#00ff41">ssh -i C:/keys/setec -p 2222 root@YOUR_SERVER_IP</span><br><br>
<span style="color:#888"># (Alternative) RSA 4096-bit key</span><br>
<span style="color:#00ff41">ssh-keygen -t rsa -b 4096 -f C:/keys/setec -N ""</span>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TROUBLESHOOTING GUIDE -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="doc-troubleshoot" class="card" style="display:none">
<div class="card-title">Troubleshooting Guide</div>
<div style="font-size:12px;line-height:1.7;color:#ccc">
<!-- SSH Connection -->
<h2>SSH Connection Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Connection refused" or "Connection timed out"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Verify the server IP is correct and the VPS is powered on</li>
<li>Check the SSH port matches what's configured: <code style="color:#00ff41">nc -zv YOUR_IP YOUR_PORT</code></li>
<li>Confirm the port is open on the server firewall:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">ufw status</span> &nbsp;or&nbsp; <span style="color:#00ff41">iptables -L -n | grep YOUR_PORT</span>
</div>
</li>
<li>Check if your home IP is blocked (fail2ban, CSF, or hosting provider firewall)</li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Permission denied (publickey)"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Verify the key path in Settings points to your <strong>private</strong> key (not .pub)</li>
<li>Check the public key is in the server's authorized_keys:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">cat ~/.ssh/authorized_keys</span>
</div>
</li>
<li>Fix permissions on the server:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys</span>
</div>
</li>
<li>Test manually with verbose output:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">ssh -i /path/to/key -p PORT user@IP -vvv</span>
</div>
</li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Host key verification failed"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>The server's fingerprint changed (reinstall, IP reassignment, or MITM)</li>
<li>Remove the old key: <code style="color:#00ff41">ssh-keygen -R YOUR_IP</code></li>
<li>Reconnect and verify the new fingerprint with your hosting provider</li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>SSH connects but commands time out</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Server may be under heavy load &mdash; check CPU/RAM in your hosting panel</li>
<li>Long-running commands (full scans, package installs) have extended timeouts but may still exceed them</li>
<li>Try running the command directly from Terminal page for real-time output</li>
</ul>
</div>
<!-- DNS API -->
<h2>DNS API Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "HTTP 401" or "HTTP 403"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>API key is invalid, expired, or has insufficient permissions</li>
<li>Regenerate a new API key from your provider's dashboard (see Host API Links)</li>
<li>Ensure the key has DNS read/write permissions</li>
<li>Some providers (Vultr, Namecheap) require IP whitelisting</li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "HTTP 530" or Cloudflare blocking API calls</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>If your domain uses Cloudflare as a proxy, API calls from your VPS may be intercepted</li>
<li>SETEC Manager routes API calls through your VPS via SSH to bypass this</li>
<li>If still failing, try making the API call directly from your local machine</li>
<li>Check if the provider's API endpoint is behind Cloudflare</li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>DNS changes not propagating</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>DNS propagation takes time (minutes to 48 hours depending on TTL)</li>
<li>Check current state: <code style="color:#00ff41">dig +short A yourdomain.com</code></li>
<li>Check with different resolvers: <code style="color:#00ff41">dig @8.8.8.8 +short A yourdomain.com</code></li>
<li>Lower your TTL to 300 before making changes for faster propagation</li>
</ul>
</div>
<!-- Security Tools -->
<h2>Security Tool Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Tool install fails with "dpkg lock" error</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Another package operation is in progress (apt update, unattended-upgrades)</li>
<li>Wait a few minutes and retry, or check:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">lsof /var/lib/dpkg/lock-frontend</span>
</div>
</li>
<li>If the process is stuck, kill it (last resort):
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">kill -9 PID && dpkg --configure -a</span>
</div>
</li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>ClamAV: "freshclam" or virus DB errors</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Stop freshclam service before manual update: <code style="color:#00ff41">systemctl stop clamav-freshclam</code></li>
<li>Run manual update: <code style="color:#00ff41">freshclam</code></li>
<li>ClamAV CDN may rate-limit &mdash; wait and retry in 30 minutes</li>
<li>Check if DNS resolves: <code style="color:#00ff41">dig database.clamav.net</code></li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>Scans/audits appear to hang or run forever</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Full system scans (ClamAV, Lynis, AIDE) can take 10-60+ minutes</li>
<li>Use "Quick Scan" options when available for faster results</li>
<li>Check server load &mdash; scans are CPU-intensive</li>
<li>For long scans, use the Terminal page for real-time output</li>
</ul>
</div>
<!-- Firewall -->
<h2>Firewall Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Locked out of server after firewall change</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li style="color:#ff4444"><strong>Prevention:</strong> ALWAYS allow your SSH port before enabling the firewall</li>
<li>Use your hosting provider's console/VNC access to regain control</li>
<li>From console: <code style="color:#00ff41">ufw allow 2222/tcp && ufw reload</code></li>
<li>Or disable the firewall entirely: <code style="color:#00ff41">ufw disable</code></li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>Multiple firewalls conflicting</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Only run ONE firewall at a time (UFW, iptables raw, nftables, firewalld, or CSF)</li>
<li>UFW is a frontend for iptables &mdash; they share the same backend</li>
<li>Use the Migration tabs (UFW&harr;iptables) to safely switch</li>
<li>Check what's active: <code style="color:#00ff41">ufw status</code>, <code style="color:#00ff41">iptables -L -n</code>, <code style="color:#00ff41">nft list ruleset</code></li>
</ul>
</div>
<!-- Nginx / SSL -->
<h2>Nginx / SSL Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Certbot SSL fails: "DNS problem: NXDOMAIN"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>The domain/subdomain doesn't have a DNS A record pointing to your server</li>
<li>Add the A record first, wait for propagation, then retry certbot</li>
<li>Verify: <code style="color:#00ff41">dig +short A subdomain.yourdomain.com</code></li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Nginx won't start: "address already in use"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Another process is using port 80/443: <code style="color:#00ff41">ss -tlnp | grep ':80\|:443'</code></li>
<li>Common culprit: Apache. Stop it: <code style="color:#00ff41">systemctl stop apache2 && systemctl disable apache2</code></li>
</ul>
</div>
<!-- General -->
<h2>General Issues</h2>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>Manager shows "Loading..." forever</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>SSH connection dropped &mdash; refresh the page to reconnect</li>
<li>Check that <code style="color:#00ff41">python app.py</code> is still running in your terminal</li>
<li>Check browser console (F12) for JavaScript errors</li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>Settings not saving / resetting on restart</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Config is stored at <code style="color:#00ff41">~/.setec-mgr/config.json</code></li>
<li>Check file permissions: <code style="color:#00ff41">ls -la ~/.setec-mgr/</code></li>
<li>View current config: <code style="color:#00ff41">cat ~/.setec-mgr/config.json</code></li>
</ul>
</div>
<div style="border:1px solid #00ff41;padding:12px;margin-top:20px;background:#0a0a0a">
<p style="color:#88ff88;margin-bottom:5px"><strong>Still need help?</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Submit a ticket: <a href="https://repo.seteclabs.io" target="_blank">repo.seteclabs.io</a></li>
<li>GitHub mirror: <a href="https://github.com/DigiJEth" target="_blank">github.com/DigiJEth</a></li>
<li>Include: error message, server OS, SETEC Manager version, and steps to reproduce</li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showDoc(id) {
document.getElementById('doc-manual').style.display = (id === 'manual') ? 'block' : 'none';
document.getElementById('doc-hostlinks').style.display = (id === 'hostlinks') ? 'block' : 'none';
document.getElementById('doc-troubleshoot').style.display = (id === 'troubleshoot') ? 'block' : 'none';
}
</script>
{% endblock %}

View File

@@ -0,0 +1,168 @@
{% extends "base.html" %}
{% block title %}Fail2Ban{% endblock %}
{% block content %}
<h1>[!] Fail2Ban</h1>
<div class="toolbar">
<button class="btn" onclick="loadStatus()">Refresh</button>
<button class="btn" onclick="loadAllJails()">All Jails Detail</button>
<button class="btn" onclick="loadLog()">View Log</button>
<button class="btn" onclick="loadConfig()">Edit Config</button>
<button class="btn btn-warn" onclick="reloadF2B()">Reload</button>
<button class="btn btn-danger" onclick="unbanAll()">Unban All</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Status</div>
<div class="output" id="status-output"><span class="info">Loading...</span></div>
</div>
<div class="card">
<div class="card-title">Jail Details</div>
<div class="toolbar">
<select id="jail-select" onchange="loadJail()">
<option value="">-- select jail --</option>
<option value="sshd">sshd</option>
<option value="nginx-http-auth">nginx-http-auth</option>
<option value="nginx-botsearch">nginx-botsearch</option>
<option value="nginx-badbots">nginx-badbots</option>
<option value="postfix">postfix</option>
</select>
</div>
<div class="output" id="jail-output"><span class="info">Select a jail</span></div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Ban IP</div>
<label>Jail</label>
<select id="ban-jail">
<option value="sshd">sshd</option>
<option value="nginx-http-auth">nginx-http-auth</option>
<option value="nginx-botsearch">nginx-botsearch</option>
<option value="nginx-badbots">nginx-badbots</option>
<option value="postfix">postfix</option>
</select>
<label>IP Address</label>
<input type="text" id="ban-ip" placeholder="1.2.3.4" style="width:200px">
<br><br>
<button class="btn btn-danger" onclick="banIP()">Ban</button>
</div>
<div class="card">
<div class="card-title">Unban IP</div>
<label>Jail</label>
<select id="unban-jail">
<option value="sshd">sshd</option>
<option value="nginx-http-auth">nginx-http-auth</option>
<option value="nginx-botsearch">nginx-botsearch</option>
<option value="nginx-badbots">nginx-badbots</option>
<option value="postfix">postfix</option>
</select>
<label>IP Address</label>
<input type="text" id="unban-ip" placeholder="1.2.3.4" style="width:200px">
<br><br>
<button class="btn" onclick="unbanIP()">Unban</button>
</div>
</div>
<div class="card" id="config-card" style="display:none">
<div class="card-title">jail.local</div>
<textarea id="config-editor" rows="20" style="width:100%;tab-size:4" spellcheck="false"></textarea>
<br>
<button class="btn" onclick="saveConfig()">Save & Reload</button>
</div>
<div class="card">
<div class="card-title">Output / Log</div>
<div class="output" id="output" style="max-height:500px"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadStatus() {
const res = await apiGet('/api/fail2ban/status');
showResult(res, 'status-output');
}
async function loadJail() {
const name = document.getElementById('jail-select').value;
if (!name) return;
const res = await apiGet('/api/fail2ban/jail/' + name);
showResult(res, 'jail-output');
}
async function loadAllJails() {
const res = await apiGet('/api/fail2ban/jails');
showResult(res);
}
async function banIP() {
const jail = document.getElementById('ban-jail').value;
const ip = document.getElementById('ban-ip').value.trim();
if (!ip) { alert('Enter an IP'); return; }
const res = await apiPost('/api/fail2ban/ban', {jail, ip});
showResult(res);
loadJail();
}
async function unbanIP() {
const jail = document.getElementById('unban-jail').value;
const ip = document.getElementById('unban-ip').value.trim();
if (!ip) { alert('Enter an IP'); return; }
const res = await apiPost('/api/fail2ban/unban', {jail, ip});
showResult(res);
loadJail();
}
async function unbanAll() {
if (!confirm('Unban ALL IPs from ALL jails?')) return;
const res = await apiPost('/api/fail2ban/unban-all');
showResult(res);
loadStatus();
}
async function reloadF2B() {
const res = await apiPost('/api/fail2ban/reload');
showResult(res);
loadStatus();
}
async function loadLog() {
const res = await apiGet('/api/fail2ban/log?lines=50');
showResult(res);
}
async function loadConfig() {
document.getElementById('config-card').style.display = '';
const res = await apiGet('/api/fail2ban/config');
if (res.ok) {
document.getElementById('config-editor').value = res.data.stdout || '';
}
}
async function saveConfig() {
const content = document.getElementById('config-editor').value;
if (!confirm('Save config and reload fail2ban?')) return;
const res = await apiPost('/api/fail2ban/config/save', {content});
showResult(res);
loadStatus();
}
document.getElementById('config-editor').addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const s = this.selectionStart;
this.value = this.value.substring(0, s) + ' ' + this.value.substring(this.selectionEnd);
this.selectionStart = this.selectionEnd = s + 4;
}
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
saveConfig();
}
});
loadStatus();
</script>
{% endblock %}

View File

@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block title %}Files{% endblock %}
{% block content %}
<h1>[/] File Manager</h1>
<div class="toolbar">
<input type="text" id="file-path" value="/var/www" style="width:400px">
<button class="btn" onclick="listFiles()">List</button>
<button class="btn" onclick="readFile()">Read File</button>
<button class="btn" onclick="mkdirRemote()">mkdir</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Directory Listing</div>
<div class="output" id="dir-output" style="max-height:600px;cursor:pointer" onclick="handleDirClick(event)">
<span class="info">Enter a path and click List</span>
</div>
</div>
<div>
<div class="card">
<div class="card-title">File Editor</div>
<label>Path</label>
<input type="text" id="edit-path" style="width:100%">
<textarea id="edit-content" rows="20" style="width:100%;resize:vertical"></textarea>
<br>
<button class="btn" onclick="saveFile()">Save File</button>
<button class="btn btn-danger" onclick="deleteFile()">Delete</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function listFiles() {
const path = document.getElementById('file-path').value;
const res = await apiGet('/api/files/list?path='+encodeURIComponent(path));
showResult(res, 'dir-output');
}
async function readFile() {
const path = document.getElementById('file-path').value;
const res = await apiGet('/api/files/read?path='+encodeURIComponent(path));
if (res.ok) {
document.getElementById('edit-path').value = path;
document.getElementById('edit-content').value = res.data.stdout || '';
}
showResult(res);
}
async function saveFile() {
const path = document.getElementById('edit-path').value;
const content = document.getElementById('edit-content').value;
if (!path) { alert('Enter file path'); return; }
if (!confirm('Save to '+path+'?')) return;
const res = await apiPost('/api/files/write', {path, content});
showResult(res);
}
async function deleteFile() {
const path = document.getElementById('edit-path').value || document.getElementById('file-path').value;
if (!path) return;
if (!confirm('DELETE '+path+'? This cannot be undone!')) return;
const res = await apiDelete('/api/files/delete', {path});
showResult(res);
listFiles();
}
async function mkdirRemote() {
const path = document.getElementById('file-path').value;
if (!path) return;
const res = await apiPost('/api/files/mkdir', {path});
showResult(res);
}
function handleDirClick(event) {
// Allow clicking on filenames in the listing
const text = window.getSelection().toString().trim();
if (text && !text.includes('\n')) {
const base = document.getElementById('file-path').value.replace(/\/$/,'');
document.getElementById('file-path').value = base + '/' + text;
}
}
listFiles();
</script>
{% endblock %}

View File

@@ -0,0 +1,990 @@
{% extends "base.html" %}
{% block title %}Firewall{% endblock %}
{% block content %}
<h1>[|] Firewall Manager</h1>
<!-- Tab navigation -->
<div class="toolbar" id="tabs">
<button class="btn" onclick="showTab('dashboard')" id="tab-dashboard">Dashboard</button>
<button class="btn" onclick="showTab('ufw')" id="tab-ufw">UFW</button>
<button class="btn" onclick="showTab('iptables')" id="tab-iptables">iptables</button>
<button class="btn" onclick="showTab('nftables')" id="tab-nftables">nftables</button>
<button class="btn" onclick="showTab('firewalld')" id="tab-firewalld">firewalld</button>
<button class="btn" onclick="showTab('csf')" id="tab-csf">CSF</button>
<button class="btn" onclick="showTab('ufw2ip')" id="tab-ufw2ip">UFW → IP</button>
<button class="btn" onclick="showTab('ip2ufw')" id="tab-ip2ufw">IP → UFW</button>
</div>
<!-- ═══════════════════════ DASHBOARD TAB ═══════════════════════ -->
<div class="tab-content" id="panel-dashboard">
<div class="grid grid-2">
<div class="card">
<div class="card-title">Firewall Detection</div>
<div id="fw-detect" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwDetect()">Detect Firewalls</button>
</div>
<div class="card">
<div class="card-title">Open Ports</div>
<div id="fw-ports" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwPorts()">Scan Ports</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Active Connections</div>
<div id="fw-conns" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwConns()">Refresh</button>
</div>
<div class="card">
<div class="card-title">Connection Stats by State</div>
<div id="fw-connstats" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwConnStats()">Refresh</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Top Connections by IP</div>
<div id="fw-topip" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwTopIPs()">Refresh</button>
</div>
<div class="card">
<div class="card-title">Recent Blocked / Dropped</div>
<div id="fw-blocked" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwBlocked()">Refresh</button>
</div>
</div>
<div class="card">
<div class="card-title">Firewall Log</div>
<div id="fw-log" class="output" style="max-height:400px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwLog()">Load Log</button>
</div>
</div>
<!-- ═══════════════════════ UFW TAB ═══════════════════════ -->
<div class="tab-content" id="panel-ufw" style="display:none;">
<div class="card">
<div class="card-title">UFW Status</div>
<div id="ufw-status" class="output" style="max-height:400px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="ufwStatus()">Refresh</button>
<button class="btn btn-warn" onclick="ufwEnable()">Enable UFW</button>
<button class="btn btn-danger" onclick="ufwDisable()">Disable UFW</button>
<button class="btn" onclick="ufwNumbered()">Numbered Rules</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Add Rule</div>
<label>Rule (e.g. "allow 8080/tcp", "deny from 1.2.3.4")</label>
<input type="text" id="ufw-rule" placeholder="allow 443/tcp" style="width:100%;">
<button class="btn" onclick="ufwAdd()" style="margin-top:8px;">Add Rule</button>
</div>
<div class="card">
<div class="card-title">Delete Rule</div>
<label>Rule to delete (e.g. "allow 8080/tcp")</label>
<input type="text" id="ufw-del-rule" placeholder="allow 8080/tcp" style="width:100%;">
<button class="btn btn-danger" onclick="ufwDel()" style="margin-top:8px;">Delete Rule</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Presets</div>
<button class="btn" onclick="ufwPreset('webserver')">Web Server (80, 443)</button>
<button class="btn" onclick="ufwPreset('mailserver')">Mail Server (25, 465, 587, 993)</button>
<button class="btn btn-danger" onclick="ufwPreset('lockdown')">Lockdown (SSH only)</button>
</div>
<div class="card">
<div class="card-title">Defaults</div>
<div id="ufw-defaults" class="output" style="max-height:150px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="ufwDefault('deny','incoming')">Default Deny In</button>
<button class="btn" onclick="ufwDefault('allow','outgoing')">Default Allow Out</button>
<button class="btn btn-danger" onclick="ufwDefault('deny','outgoing')">Default Deny Out</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">UFW Application Profiles</div>
<div id="ufw-apps" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="ufwAppList()">List App Profiles</button>
</div>
<div class="card">
<div class="card-title">UFW Log</div>
<div id="ufw-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="ufwLog()">View Log</button>
<button class="btn" onclick="ufwLogLevel('on')">Logging On</button>
<button class="btn" onclick="ufwLogLevel('high')">Logging High</button>
<button class="btn btn-danger" onclick="ufwLogLevel('off')">Logging Off</button>
</div>
</div>
</div>
<!-- ═══════════════════════ IPTABLES TAB ═══════════════════════ -->
<div class="tab-content" id="panel-iptables" style="display:none;">
<div id="ipt-not-installed" style="display:none;">
<div class="card">
<div class="card-title">iptables</div>
<p style="color:#888;margin-bottom:10px;">iptables is the traditional Linux packet filter. It is usually pre-installed.</p>
<div id="ipt-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-warn" onclick="iptInstall()">Install iptables</button>
</div>
</div>
<div id="ipt-main">
<div class="card">
<div class="card-title">iptables Rules (filter)</div>
<div id="ipt-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="iptList()">Filter Table</button>
<button class="btn" onclick="iptListNat()">NAT Table</button>
<button class="btn" onclick="iptListMangle()">Mangle Table</button>
<button class="btn" onclick="iptCounters()">Counters</button>
<button class="btn" onclick="iptIp6()">IPv6 Rules</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Add Rule</div>
<label>Chain</label>
<select id="ipt-chain" style="width:120px;">
<option>INPUT</option>
<option>OUTPUT</option>
<option>FORWARD</option>
</select>
<label>Rule (e.g. "-p tcp --dport 80 -j ACCEPT")</label>
<input type="text" id="ipt-rule" placeholder="-p tcp --dport 80 -j ACCEPT" style="width:100%;">
<div style="margin-top:8px;">
<button class="btn" onclick="iptAdd()">Append Rule</button>
<label style="display:inline;">Position</label>
<input type="number" id="ipt-pos" value="1" style="width:50px;">
<button class="btn" onclick="iptInsert()">Insert at Position</button>
</div>
</div>
<div class="card">
<div class="card-title">Delete Rule</div>
<label>Chain</label>
<select id="ipt-del-chain" style="width:120px;">
<option>INPUT</option>
<option>OUTPUT</option>
<option>FORWARD</option>
</select>
<label>Rule Number</label>
<input type="number" id="ipt-del-num" value="1" style="width:80px;">
<button class="btn btn-danger" onclick="iptDelete()" style="margin-top:8px;">Delete Rule</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Chain Policy</div>
<label>Chain</label>
<select id="ipt-pol-chain" style="width:120px;">
<option>INPUT</option>
<option>OUTPUT</option>
<option>FORWARD</option>
</select>
<label>Policy</label>
<select id="ipt-pol-target" style="width:120px;">
<option>ACCEPT</option>
<option>DROP</option>
</select>
<button class="btn btn-warn" onclick="iptPolicy()" style="margin-top:8px;">Set Policy</button>
</div>
<div class="card">
<div class="card-title">Quick Actions</div>
<div style="margin-bottom:8px;">
<label>IP Address</label>
<input type="text" id="ipt-ip" placeholder="1.2.3.4" style="width:160px;">
</div>
<div class="toolbar">
<button class="btn btn-danger" onclick="iptBlockIP()">Block IP</button>
<button class="btn" onclick="iptUnblockIP()">Unblock IP</button>
<button class="btn" onclick="iptListBlocked()">Show Blocked</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Persistence</div>
<div id="ipt-persist" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn btn-warn" onclick="iptSave()">Save Rules</button>
<button class="btn" onclick="iptRestore()">Restore Saved</button>
</div>
</div>
<div class="card">
<div class="card-title">Flush</div>
<div id="ipt-flush-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn btn-danger" onclick="iptFlush()">Flush All Rules</button>
<button class="btn" onclick="iptZero()">Zero Counters</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">iptables Log</div>
<div id="ipt-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="iptLog()">View Log</button>
</div>
</div>
</div>
<!-- ═══════════════════════ NFTABLES TAB ═══════════════════════ -->
<div class="tab-content" id="panel-nftables" style="display:none;">
<div id="nft-not-installed" style="display:none;">
<div class="card">
<div class="card-title">nftables</div>
<p style="color:#888;margin-bottom:10px;">nftables is the modern replacement for iptables. Provides better performance and a cleaner syntax.</p>
<div id="nft-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-warn" onclick="nftInstall()">Install nftables</button>
</div>
</div>
<div id="nft-main">
<div class="card">
<div class="card-title">nftables Ruleset</div>
<div id="nft-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="nftList()">Full Ruleset</button>
<button class="btn" onclick="nftTables()">Tables</button>
<button class="btn" onclick="nftCounters()">Counters</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">List Chains</div>
<div id="nft-chains" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>Table (e.g. "inet filter")</label>
<input type="text" id="nft-table" value="inet filter" style="width:200px;">
<button class="btn" onclick="nftChains()" style="margin-top:8px;">List Chains</button>
</div>
<div class="card">
<div class="card-title">Add Rule</div>
<label>Table</label>
<input type="text" id="nft-add-table" value="inet filter" style="width:180px;">
<label>Chain</label>
<input type="text" id="nft-add-chain" value="input" style="width:180px;">
<label>Rule (e.g. "tcp dport 80 accept")</label>
<input type="text" id="nft-add-rule" placeholder="tcp dport 80 accept" style="width:100%;">
<button class="btn" onclick="nftAddRule()" style="margin-top:8px;">Add Rule</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Delete Rule</div>
<label>Table</label>
<input type="text" id="nft-del-table" value="inet filter" style="width:180px;">
<label>Chain</label>
<input type="text" id="nft-del-chain" value="input" style="width:180px;">
<label>Handle Number</label>
<input type="number" id="nft-del-handle" style="width:80px;">
<button class="btn btn-danger" onclick="nftDelRule()" style="margin-top:8px;">Delete Rule</button>
</div>
<div class="card">
<div class="card-title">Create Table / Chain</div>
<label>Family</label>
<select id="nft-family" style="width:100px;">
<option>inet</option>
<option>ip</option>
<option>ip6</option>
<option>bridge</option>
</select>
<label>Table Name</label>
<input type="text" id="nft-new-table" placeholder="mytable" style="width:150px;">
<button class="btn" onclick="nftCreateTable()" style="margin-top:8px;">Create Table</button>
<div style="margin-top:10px;border-top:1px solid #333;padding-top:8px;">
<label>Chain Name</label>
<input type="text" id="nft-new-chain" placeholder="mychain" style="width:150px;">
<label>Hook</label>
<select id="nft-hook" style="width:100px;">
<option>input</option>
<option>output</option>
<option>forward</option>
<option>prerouting</option>
<option>postrouting</option>
</select>
<button class="btn" onclick="nftCreateChain()" style="margin-top:8px;">Create Chain</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Persistence</div>
<div id="nft-persist" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn btn-warn" onclick="nftSave()">Save Ruleset</button>
<button class="btn" onclick="nftRestore()">Restore Saved</button>
<button class="btn" onclick="nftConfig()">View Config File</button>
</div>
</div>
<div class="card">
<div class="card-title">Flush</div>
<div id="nft-flush-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-danger" onclick="nftFlush()">Flush All</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════ FIREWALLD TAB ═══════════════════════ -->
<div class="tab-content" id="panel-firewalld" style="display:none;">
<div id="fwd-not-installed" style="display:none;">
<div class="card">
<div class="card-title">firewalld</div>
<p style="color:#888;margin-bottom:10px;">firewalld is a zone-based firewall manager. Common on RHEL/CentOS but also available on Debian/Ubuntu.</p>
<div id="fwd-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-warn" onclick="fwdInstall()">Install firewalld</button>
</div>
</div>
<div id="fwd-main">
<div class="grid grid-2">
<div class="card">
<div class="card-title">firewalld Status</div>
<div id="fwd-status" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="fwdStatus()">Refresh</button>
<button class="btn btn-warn" onclick="fwdReload()">Reload</button>
<button class="btn btn-danger" onclick="fwdPanicOn()">Panic On</button>
<button class="btn" onclick="fwdPanicOff()">Panic Off</button>
</div>
</div>
<div class="card">
<div class="card-title">Zones</div>
<div id="fwd-zones" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwdZones()">List Zones</button>
<div style="margin-top:8px;">
<label>Zone</label>
<input type="text" id="fwd-zone" value="public" style="width:120px;">
<button class="btn" onclick="fwdZoneInfo()">Zone Info</button>
<button class="btn btn-warn" onclick="fwdSetDefault()">Set as Default</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Services</div>
<div id="fwd-services" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>Service (e.g. http, https, ssh, smtp)</label>
<input type="text" id="fwd-service" placeholder="http" style="width:150px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn" onclick="fwdServicesList()">List Available</button>
<button class="btn btn-warn" onclick="fwdAddService()">Add Service</button>
<button class="btn btn-danger" onclick="fwdRemoveService()">Remove Service</button>
</div>
</div>
<div class="card">
<div class="card-title">Ports</div>
<div id="fwd-ports" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>Port (e.g. 8080/tcp)</label>
<input type="text" id="fwd-port" placeholder="8080/tcp" style="width:150px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn btn-warn" onclick="fwdAddPort()">Add Port</button>
<button class="btn btn-danger" onclick="fwdRemovePort()">Remove Port</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Rich Rules</div>
<div id="fwd-rich" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>Rich Rule</label>
<input type="text" id="fwd-rich-rule" placeholder='rule family="ipv4" source address="1.2.3.4" drop' style="width:100%;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn btn-warn" onclick="fwdAddRich()">Add Rich Rule</button>
<button class="btn btn-danger" onclick="fwdRemoveRich()">Remove Rich Rule</button>
</div>
</div>
<div class="card">
<div class="card-title">Block / Unblock IP</div>
<div id="fwd-block" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>IP Address</label>
<input type="text" id="fwd-ip" placeholder="1.2.3.4" style="width:160px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn btn-danger" onclick="fwdBlockIP()">Block IP</button>
<button class="btn" onclick="fwdUnblockIP()">Unblock IP</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">firewalld Log</div>
<div id="fwd-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwdLog()">View Log</button>
</div>
</div>
</div>
<!-- ═══════════════════════ CSF TAB ═══════════════════════ -->
<div class="tab-content" id="panel-csf" style="display:none;">
<div id="csf-not-installed" style="display:none;">
<div class="card">
<div class="card-title">CSF (ConfigServer Security & Firewall)</div>
<p style="color:#888;margin-bottom:10px;">CSF is a stateful packet inspection firewall with login/intrusion detection (LFD). Popular on cPanel servers but works standalone.</p>
<div id="csf-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-warn" onclick="csfInstall()">Install CSF</button>
</div>
</div>
<div id="csf-main">
<div class="grid grid-2">
<div class="card">
<div class="card-title">CSF Status</div>
<div id="csf-status" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="csfStatus()">Refresh</button>
<button class="btn btn-warn" onclick="csfStart()">Start</button>
<button class="btn btn-danger" onclick="csfStop()">Stop (Flush)</button>
<button class="btn" onclick="csfRestart()">Restart</button>
</div>
</div>
<div class="card">
<div class="card-title">Test iptables Modules</div>
<div id="csf-test" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="csfTest()">Run Test</button>
</div>
</div>
<div class="card">
<div class="card-title">Rules</div>
<div id="csf-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<button class="btn" onclick="csfList()">List All Rules</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Allow / Deny IP</div>
<div id="csf-ip-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<label>IP Address</label>
<input type="text" id="csf-ip" placeholder="1.2.3.4" style="width:160px;">
<label>Comment</label>
<input type="text" id="csf-comment" placeholder="reason" style="width:200px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn" onclick="csfAllow()">Allow</button>
<button class="btn btn-danger" onclick="csfDeny()">Deny</button>
<button class="btn" onclick="csfRemove()">Remove</button>
<button class="btn" onclick="csfGrep()">Search IP</button>
</div>
</div>
<div class="card">
<div class="card-title">Temporary Rules</div>
<div id="csf-temp" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<label>TTL (seconds)</label>
<input type="number" id="csf-ttl" value="3600" style="width:100px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn" onclick="csfTempAllow()">Temp Allow</button>
<button class="btn btn-danger" onclick="csfTempDeny()">Temp Deny</button>
<button class="btn" onclick="csfTempList()">List Temp Rules</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Configuration</div>
<div id="csf-config" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="csfConfig()">View Key Settings</button>
</div>
<div class="card">
<div class="card-title">LFD Log</div>
<div id="csf-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="csfLog()">View Log</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════ UFW → IPTABLES TAB ═══════════════════════ -->
<div class="tab-content" id="panel-ufw2ip" style="display:none;">
<div class="card">
<div class="card-title">Migrate: UFW → iptables</div>
<p style="color:#888;margin-bottom:15px;">
This will disable UFW and switch to raw iptables management. Your existing firewall rules
(which UFW manages via iptables under the hood) will be preserved and saved to
<code>/etc/iptables/rules.v4</code>. iptables-persistent will be installed to ensure
rules survive reboots.
</p>
<div style="margin-bottom:15px;padding:10px;border:1px solid #ffaa00;color:#ffaa00;">
<strong>What happens:</strong><br>
1. Current UFW/iptables state is backed up<br>
2. UFW is disabled and its service stopped<br>
3. The iptables rules that UFW generated are saved<br>
4. iptables-persistent is installed for persistence<br>
5. You manage rules directly via the iptables tab
</div>
<div id="m-ufw2ip-out" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="previewUfw2Ip()">Preview Current Rules</button>
<button class="btn btn-danger" onclick="runUfw2Ip()">Migrate UFW → iptables</button>
</div>
</div>
</div>
<!-- ═══════════════════════ IPTABLES → UFW TAB ═══════════════════════ -->
<div class="tab-content" id="panel-ip2ufw" style="display:none;">
<div class="card">
<div class="card-title">Migrate: iptables → UFW</div>
<p style="color:#888;margin-bottom:15px;">
This will convert your iptables rules to UFW and enable UFW as the primary firewall frontend.
Your current iptables rules are backed up before any changes. TCP/UDP ACCEPT rules on INPUT
are automatically converted to UFW allow rules.
</p>
<div style="margin-bottom:15px;padding:10px;border:1px solid #ffaa00;color:#ffaa00;">
<strong>What happens:</strong><br>
1. Current iptables rules are backed up<br>
2. UFW is installed (if not present) and reset<br>
3. Default deny incoming / allow outgoing is set<br>
4. iptables ACCEPT rules are converted to UFW allow rules<br>
5. UFW is enabled, iptables-persistent is disabled<br>
6. You manage rules via the UFW tab
</div>
<div id="m-ip2ufw-out" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="previewIp2Ufw()">Preview Current Rules</button>
<button class="btn btn-danger" onclick="runIp2Ufw()">Migrate iptables → UFW</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// ── Tab switching ──
function showTab(name) {
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
document.querySelectorAll('#tabs .btn').forEach(el => el.style.background = '');
document.getElementById('panel-' + name).style.display = 'block';
document.getElementById('tab-' + name).style.background = '#1a2a1a';
}
showTab('dashboard');
// ── Dashboard ──
async function fwDetect() {
const r = await apiGet('/api/firewall/detect');
showResult(r, 'fw-detect');
}
async function fwPorts() {
const r = await apiGet('/api/firewall/ports');
showResult(r, 'fw-ports');
}
async function fwConns() {
const r = await apiGet('/api/firewall/connections');
showResult(r, 'fw-conns');
}
async function fwConnStats() {
const r = await apiGet('/api/firewall/connection-stats');
showResult(r, 'fw-connstats');
}
async function fwTopIPs() {
const r = await apiGet('/api/firewall/top-ips');
showResult(r, 'fw-topip');
}
async function fwBlocked() {
const r = await apiGet('/api/firewall/blocked');
showResult(r, 'fw-blocked');
}
async function fwLog() {
const r = await apiGet('/api/firewall/log');
showResult(r, 'fw-log');
}
// ── UFW ──
async function ufwStatus() {
const r = await apiGet('/api/security/firewall/status');
showResult(r, 'ufw-status');
}
async function ufwNumbered() {
const r = await apiGet('/api/firewall/ufw/numbered');
showResult(r, 'ufw-status');
}
async function ufwEnable() {
const r = await apiPost('/api/security/firewall/enable');
showResult(r, 'ufw-status');
}
async function ufwDisable() {
if (!confirm('Disable UFW?')) return;
const r = await apiPost('/api/security/firewall/disable');
showResult(r, 'ufw-status');
}
async function ufwAdd() {
const rule = document.getElementById('ufw-rule').value;
const r = await apiPost('/api/security/firewall/add', {rule});
showResult(r, 'ufw-status');
}
async function ufwDel() {
const rule = document.getElementById('ufw-del-rule').value;
const r = await apiPost('/api/security/firewall/delete', {rule});
showResult(r, 'ufw-status');
}
async function ufwPreset(name) {
if (!confirm('Apply ' + name + ' firewall preset?')) return;
const r = await apiPost('/api/security/firewall/preset', {preset: name});
showResult(r, 'ufw-status');
}
async function ufwDefault(policy, direction) {
const r = await apiPost('/api/firewall/ufw/default', {policy, direction});
showResult(r, 'ufw-defaults');
}
async function ufwAppList() {
const r = await apiGet('/api/firewall/ufw/app-list');
showResult(r, 'ufw-apps');
}
async function ufwLog() {
const r = await apiGet('/api/firewall/ufw/log');
showResult(r, 'ufw-log');
}
async function ufwLogLevel(level) {
const r = await apiPost('/api/firewall/ufw/log-level', {level});
showResult(r, 'ufw-log');
}
// ── iptables ──
async function iptList() {
const r = await apiGet('/api/firewall/iptables/list');
showResult(r, 'ipt-rules');
}
async function iptListNat() {
const r = await apiGet('/api/firewall/iptables/list-nat');
showResult(r, 'ipt-rules');
}
async function iptListMangle() {
const r = await apiGet('/api/firewall/iptables/list-mangle');
showResult(r, 'ipt-rules');
}
async function iptCounters() {
const r = await apiGet('/api/firewall/iptables/counters');
showResult(r, 'ipt-rules');
}
async function iptIp6() {
const r = await apiGet('/api/firewall/iptables/ip6');
showResult(r, 'ipt-rules');
}
async function iptAdd() {
const chain = document.getElementById('ipt-chain').value;
const rule = document.getElementById('ipt-rule').value;
const r = await apiPost('/api/firewall/iptables/add', {chain, rule});
showResult(r, 'ipt-rules');
}
async function iptInsert() {
const chain = document.getElementById('ipt-chain').value;
const rule = document.getElementById('ipt-rule').value;
const pos = parseInt(document.getElementById('ipt-pos').value);
const r = await apiPost('/api/firewall/iptables/insert', {chain, rule, position: pos});
showResult(r, 'ipt-rules');
}
async function iptDelete() {
const chain = document.getElementById('ipt-del-chain').value;
const rule_num = parseInt(document.getElementById('ipt-del-num').value);
if (!confirm('Delete rule ' + rule_num + ' from ' + chain + '?')) return;
const r = await apiPost('/api/firewall/iptables/delete', {chain, rule_num});
showResult(r, 'ipt-rules');
}
async function iptPolicy() {
const chain = document.getElementById('ipt-pol-chain').value;
const target = document.getElementById('ipt-pol-target').value;
if (!confirm('Set ' + chain + ' policy to ' + target + '?')) return;
const r = await apiPost('/api/firewall/iptables/policy', {chain, target});
showResult(r, 'ipt-rules');
}
async function iptBlockIP() {
const ip = document.getElementById('ipt-ip').value;
if (!ip) return;
const r = await apiPost('/api/firewall/iptables/block-ip', {ip});
showResult(r, 'ipt-rules');
}
async function iptUnblockIP() {
const ip = document.getElementById('ipt-ip').value;
if (!ip) return;
const r = await apiPost('/api/firewall/iptables/unblock-ip', {ip});
showResult(r, 'ipt-rules');
}
async function iptListBlocked() {
const r = await apiGet('/api/firewall/iptables/blocked');
showResult(r, 'ipt-rules');
}
async function iptSave() {
const r = await apiPost('/api/firewall/iptables/save');
showResult(r, 'ipt-persist');
}
async function iptRestore() {
const r = await apiPost('/api/firewall/iptables/restore');
showResult(r, 'ipt-persist');
}
async function iptFlush() {
if (!confirm('Flush ALL iptables rules? This may lock you out if policy is DROP!')) return;
const r = await apiPost('/api/firewall/iptables/flush');
showResult(r, 'ipt-flush-out');
}
async function iptZero() {
const r = await apiPost('/api/firewall/iptables/zero');
showResult(r, 'ipt-flush-out');
}
async function iptLog() {
const r = await apiGet('/api/firewall/iptables/log');
showResult(r, 'ipt-log');
}
async function iptInstall() {
const r = await apiPost('/api/firewall/iptables/install');
showResult(r, 'ipt-install-out');
}
// ── nftables ──
async function nftList() {
const r = await apiGet('/api/firewall/nftables/list');
showResult(r, 'nft-rules');
}
async function nftTables() {
const r = await apiGet('/api/firewall/nftables/tables');
showResult(r, 'nft-rules');
}
async function nftCounters() {
const r = await apiGet('/api/firewall/nftables/counters');
showResult(r, 'nft-rules');
}
async function nftChains() {
const table = document.getElementById('nft-table').value;
const r = await apiPost('/api/firewall/nftables/chains', {table});
showResult(r, 'nft-chains');
}
async function nftAddRule() {
const table = document.getElementById('nft-add-table').value;
const chain = document.getElementById('nft-add-chain').value;
const rule = document.getElementById('nft-add-rule').value;
const r = await apiPost('/api/firewall/nftables/add-rule', {table, chain, rule});
showResult(r, 'nft-rules');
}
async function nftDelRule() {
const table = document.getElementById('nft-del-table').value;
const chain = document.getElementById('nft-del-chain').value;
const handle = parseInt(document.getElementById('nft-del-handle').value);
if (!confirm('Delete rule handle ' + handle + '?')) return;
const r = await apiPost('/api/firewall/nftables/delete-rule', {table, chain, handle});
showResult(r, 'nft-rules');
}
async function nftCreateTable() {
const family = document.getElementById('nft-family').value;
const name = document.getElementById('nft-new-table').value;
const r = await apiPost('/api/firewall/nftables/create-table', {family, name});
showResult(r, 'nft-rules');
}
async function nftCreateChain() {
const table = document.getElementById('nft-family').value + ' ' + document.getElementById('nft-new-table').value;
const chain = document.getElementById('nft-new-chain').value;
const hook = document.getElementById('nft-hook').value;
const r = await apiPost('/api/firewall/nftables/create-chain', {table, chain, hook});
showResult(r, 'nft-rules');
}
async function nftSave() {
const r = await apiPost('/api/firewall/nftables/save');
showResult(r, 'nft-persist');
}
async function nftRestore() {
const r = await apiPost('/api/firewall/nftables/restore');
showResult(r, 'nft-persist');
}
async function nftConfig() {
const r = await apiGet('/api/firewall/nftables/config');
showResult(r, 'nft-persist');
}
async function nftFlush() {
if (!confirm('Flush ALL nftables rules?')) return;
const r = await apiPost('/api/firewall/nftables/flush');
showResult(r, 'nft-flush-out');
}
async function nftInstall() {
document.getElementById('nft-install-out').innerHTML = '<span class="info">Installing nftables...</span>';
const r = await apiPost('/api/firewall/nftables/install');
showResult(r, 'nft-install-out');
}
// ── firewalld ──
async function fwdStatus() {
const r = await apiGet('/api/firewall/firewalld/status');
showResult(r, 'fwd-status');
}
async function fwdReload() {
const r = await apiPost('/api/firewall/firewalld/reload');
showResult(r, 'fwd-status');
}
async function fwdPanicOn() {
if (!confirm('Enable panic mode? ALL network traffic will be blocked!')) return;
const r = await apiPost('/api/firewall/firewalld/panic-on');
showResult(r, 'fwd-status');
}
async function fwdPanicOff() {
const r = await apiPost('/api/firewall/firewalld/panic-off');
showResult(r, 'fwd-status');
}
async function fwdZones() {
const r = await apiGet('/api/firewall/firewalld/zones');
showResult(r, 'fwd-zones');
}
async function fwdZoneInfo() {
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/zone-info', {zone});
showResult(r, 'fwd-zones');
}
async function fwdSetDefault() {
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/default-zone', {zone});
showResult(r, 'fwd-zones');
}
async function fwdServicesList() {
const r = await apiGet('/api/firewall/firewalld/services-list');
showResult(r, 'fwd-services');
}
async function fwdAddService() {
const svc = document.getElementById('fwd-service').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/add-service', {service: svc, zone});
showResult(r, 'fwd-services');
}
async function fwdRemoveService() {
const svc = document.getElementById('fwd-service').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/remove-service', {service: svc, zone});
showResult(r, 'fwd-services');
}
async function fwdAddPort() {
const port = document.getElementById('fwd-port').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/add-port', {port, zone});
showResult(r, 'fwd-ports');
}
async function fwdRemovePort() {
const port = document.getElementById('fwd-port').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/remove-port', {port, zone});
showResult(r, 'fwd-ports');
}
async function fwdAddRich() {
const rule = document.getElementById('fwd-rich-rule').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/add-rich-rule', {rule, zone});
showResult(r, 'fwd-rich');
}
async function fwdRemoveRich() {
const rule = document.getElementById('fwd-rich-rule').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/remove-rich-rule', {rule, zone});
showResult(r, 'fwd-rich');
}
async function fwdBlockIP() {
const ip = document.getElementById('fwd-ip').value;
const r = await apiPost('/api/firewall/firewalld/block-ip', {ip});
showResult(r, 'fwd-block');
}
async function fwdUnblockIP() {
const ip = document.getElementById('fwd-ip').value;
const r = await apiPost('/api/firewall/firewalld/unblock-ip', {ip});
showResult(r, 'fwd-block');
}
async function fwdLog() {
const r = await apiGet('/api/firewall/firewalld/log');
showResult(r, 'fwd-log');
}
async function fwdInstall() {
document.getElementById('fwd-install-out').innerHTML = '<span class="info">Installing firewalld...</span>';
const r = await apiPost('/api/firewall/firewalld/install');
showResult(r, 'fwd-install-out');
}
// ── CSF ──
async function csfStatus() {
const r = await apiGet('/api/firewall/csf/status');
showResult(r, 'csf-status');
}
async function csfStart() {
const r = await apiPost('/api/firewall/csf/start');
showResult(r, 'csf-status');
}
async function csfStop() {
if (!confirm('Stop CSF? This will flush all rules.')) return;
const r = await apiPost('/api/firewall/csf/stop');
showResult(r, 'csf-status');
}
async function csfRestart() {
const r = await apiPost('/api/firewall/csf/restart');
showResult(r, 'csf-status');
}
async function csfTest() {
const r = await apiGet('/api/firewall/csf/test');
showResult(r, 'csf-test');
}
async function csfList() {
const r = await apiGet('/api/firewall/csf/list');
showResult(r, 'csf-rules');
}
async function csfAllow() {
const ip = document.getElementById('csf-ip').value;
const comment = document.getElementById('csf-comment').value;
const r = await apiPost('/api/firewall/csf/allow', {ip, comment});
showResult(r, 'csf-ip-out');
}
async function csfDeny() {
const ip = document.getElementById('csf-ip').value;
const comment = document.getElementById('csf-comment').value;
const r = await apiPost('/api/firewall/csf/deny', {ip, comment});
showResult(r, 'csf-ip-out');
}
async function csfRemove() {
const ip = document.getElementById('csf-ip').value;
const r = await apiPost('/api/firewall/csf/remove', {ip});
showResult(r, 'csf-ip-out');
}
async function csfGrep() {
const ip = document.getElementById('csf-ip').value;
const r = await apiPost('/api/firewall/csf/grep', {ip});
showResult(r, 'csf-ip-out');
}
async function csfTempAllow() {
const ip = document.getElementById('csf-ip').value;
const ttl = parseInt(document.getElementById('csf-ttl').value);
const r = await apiPost('/api/firewall/csf/temp-allow', {ip, ttl});
showResult(r, 'csf-temp');
}
async function csfTempDeny() {
const ip = document.getElementById('csf-ip').value;
const ttl = parseInt(document.getElementById('csf-ttl').value);
const r = await apiPost('/api/firewall/csf/temp-deny', {ip, ttl});
showResult(r, 'csf-temp');
}
async function csfTempList() {
const r = await apiGet('/api/firewall/csf/temp-list');
showResult(r, 'csf-temp');
}
async function csfConfig() {
const r = await apiGet('/api/firewall/csf/config');
showResult(r, 'csf-config');
}
async function csfLog() {
const r = await apiGet('/api/firewall/csf/log');
showResult(r, 'csf-log');
}
async function csfInstall() {
document.getElementById('csf-install-out').innerHTML = '<span class="info">Installing CSF...</span>';
const r = await apiPost('/api/firewall/csf/install');
showResult(r, 'csf-install-out');
}
// ── Migration: UFW → iptables ──
async function previewUfw2Ip() {
const r = await apiGet('/api/security/firewall/status');
showResult(r, 'm-ufw2ip-out');
}
async function runUfw2Ip() {
if (!confirm('Migrate from UFW to raw iptables? UFW will be disabled. Make sure you have console access as backup!')) return;
document.getElementById('m-ufw2ip-out').innerHTML = '<span class="info">Migrating UFW → iptables...</span>';
const r = await apiPost('/api/firewall/migrate/ufw-to-iptables');
showResult(r, 'm-ufw2ip-out');
}
// ── Migration: iptables → UFW ──
async function previewIp2Ufw() {
const r = await apiGet('/api/firewall/iptables/list');
showResult(r, 'm-ip2ufw-out');
}
async function runIp2Ufw() {
if (!confirm('Migrate from iptables to UFW? Current rules will be converted. Make sure you have console access as backup!')) return;
document.getElementById('m-ip2ufw-out').innerHTML = '<span class="info">Migrating iptables → UFW...</span>';
const r = await apiPost('/api/firewall/migrate/iptables-to-ufw');
showResult(r, 'm-ip2ufw-out');
}
// Auto-load dashboard on page load
fwDetect();
</script>
{% endblock %}

View File

@@ -0,0 +1,182 @@
{% extends "base.html" %}
{% block title %}Front Page Editor{% endblock %}
{% block content %}
<h1>[&lt;] Front Page Editor</h1>
<div class="toolbar">
<button class="btn" onclick="loadFiles()">Refresh Files</button>
<button class="btn" onclick="newFile()">+ New File</button>
<button class="btn" onclick="openPreview()">Preview Site</button>
</div>
<div style="display: flex; gap: 15px; height: calc(100vh - 140px);">
<!-- File list panel -->
<div style="width: 220px; flex-shrink: 0;">
<div class="card" style="height: 100%; overflow-y: auto;">
<div class="card-title">Site Files</div>
<div id="file-list" style="font-size: 12px;"></div>
</div>
</div>
<!-- Editor panel -->
<div style="flex: 1; display: flex; flex-direction: column;">
<div style="margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
<span id="current-file" style="color: #888; font-size: 12px;">No file selected</span>
<span id="modified-indicator" style="color: #ffaa00; font-size: 12px; display: none;">* modified</span>
<div style="margin-left: auto;">
<button class="btn" onclick="saveFile()" id="save-btn" disabled>Save</button>
<button class="btn btn-danger" onclick="deleteFile()" id="delete-btn" disabled>Delete</button>
</div>
</div>
<textarea id="editor" style="flex: 1; width: 100%; resize: none; font-size: 13px; line-height: 1.5; tab-size: 2;" disabled></textarea>
<div id="output" class="output" style="max-height: 80px; margin-top: 8px;"></div>
</div>
</div>
<script>
let currentFile = null;
let originalContent = '';
const editor = document.getElementById('editor');
const fileList = document.getElementById('file-list');
const currentFileEl = document.getElementById('current-file');
const modifiedEl = document.getElementById('modified-indicator');
const saveBtn = document.getElementById('save-btn');
const deleteBtn = document.getElementById('delete-btn');
editor.addEventListener('input', () => {
const modified = editor.value !== originalContent;
modifiedEl.style.display = modified ? 'inline' : 'none';
});
// Handle Ctrl+S to save
editor.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
if (currentFile) saveFile();
}
// Tab inserts spaces
if (e.key === 'Tab') {
e.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 2;
}
});
async function loadFiles() {
const res = await apiGet('/api/frontpage/list');
if (!res.ok) {
fileList.innerHTML = '<span class="err">Failed to load</span>';
return;
}
const lines = (res.data.stdout || '').trim().split('\n').filter(l => l.trim());
if (lines.length === 0) {
fileList.innerHTML = '<span style="color:#888">No files found</span>';
return;
}
let html = '';
lines.forEach(line => {
const parts = line.split('|');
const name = parts[0];
const size = parts[1] ? formatSize(parseInt(parts[1])) : '';
const ext = name.split('.').pop();
const icon = ext === 'html' ? '&lt;/&gt;' : ext === 'css' ? '#' : ext === 'js' ? 'js' : '?';
const active = name === currentFile ? 'background:#1a2a1a;color:#fff;' : '';
html += `<div onclick="openFile('${name}')" style="padding:6px 8px;cursor:pointer;border-bottom:1px solid #1a1a1a;${active}" onmouseover="this.style.background='#1a2a1a'" onmouseout="this.style.background='${name === currentFile ? '#1a2a1a' : ''}'">
<span style="color:#555">[${icon}]</span> ${name}
<span style="color:#555;font-size:10px;float:right">${size}</span>
</div>`;
});
fileList.innerHTML = html;
}
async function openFile(name) {
if (currentFile && editor.value !== originalContent) {
if (!confirm('Unsaved changes. Discard?')) return;
}
const res = await apiGet('/api/frontpage/read?file=' + encodeURIComponent(name));
if (!res.ok) {
showResult(res);
return;
}
currentFile = name;
originalContent = res.data.stdout || '';
editor.value = originalContent;
editor.disabled = false;
saveBtn.disabled = false;
deleteBtn.disabled = false;
currentFileEl.textContent = name;
modifiedEl.style.display = 'none';
document.getElementById('output').innerHTML = '<span class="info">Loaded ' + name + '</span>';
loadFiles(); // refresh to highlight active
}
async function saveFile() {
if (!currentFile) return;
const res = await apiPost('/api/frontpage/write', {
file: currentFile,
content: editor.value
});
if (res.ok) {
originalContent = editor.value;
modifiedEl.style.display = 'none';
document.getElementById('output').innerHTML = '<span class="status-ok">Saved ' + currentFile + '</span>';
} else {
showResult(res);
}
}
async function deleteFile() {
if (!currentFile) return;
if (!confirm('Delete ' + currentFile + '?')) return;
const res = await api('/api/frontpage/delete', {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({file: currentFile})
});
if (res.ok) {
document.getElementById('output').innerHTML = '<span class="status-ok">Deleted ' + currentFile + '</span>';
currentFile = null;
editor.value = '';
editor.disabled = true;
saveBtn.disabled = true;
deleteBtn.disabled = true;
currentFileEl.textContent = 'No file selected';
loadFiles();
} else {
showResult(res);
}
}
async function newFile() {
const name = prompt('File name (e.g. newpage.html):');
if (!name) return;
const res = await apiPost('/api/frontpage/new', {file: name});
if (res.ok) {
await loadFiles();
openFile(name);
} else {
showResult(res);
}
}
async function openPreview() {
const res = await apiGet('/api/frontpage/preview-url');
if (res.ok) {
window.open(res.data, '_blank');
}
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + 'B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + 'K';
return (bytes / 1048576).toFixed(1) + 'M';
}
// Load on page open
loadFiles();
</script>
{% endblock %}

View File

@@ -0,0 +1,107 @@
{% extends "base.html" %}
{% block title %}Nginx{% endblock %}
{% block content %}
<h1>[>] Nginx / Subdomains</h1>
<div class="toolbar">
<button class="btn" onclick="loadSites()">Refresh Sites</button>
<button class="btn" onclick="reloadNginx()">Reload Nginx</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Active Sites</div>
<div class="output" id="sites-output"><span class="info">Loading...</span></div>
</div>
<div>
<div class="card">
<div class="card-title">Add Subdomain</div>
<label>Subdomain (e.g. "api" for api.seteclabs.io)</label>
<input type="text" id="sub-name" placeholder="subdomain" style="width:100%">
<label>Type</label>
<select id="sub-type" onchange="toggleSubFields()">
<option value="proxy">Reverse Proxy</option>
<option value="static">Static Files</option>
</select>
<div id="proxy-fields">
<label>Proxy Port</label>
<input type="number" id="sub-port" placeholder="e.g. 8080" style="width:100%">
</div>
<div id="static-fields" style="display:none">
<label>Document Root</label>
<input type="text" id="sub-root" placeholder="/var/www/sub.seteclabs.io" style="width:100%">
</div>
<br>
<button class="btn" onclick="addSubdomain()">Create Vhost</button>
</div>
<div class="card">
<div class="card-title">SSL Certificate</div>
<label>Domain</label>
<input type="text" id="ssl-domain" placeholder="sub.seteclabs.io" style="width:100%">
<br><br>
<button class="btn" onclick="addSSL()">Install SSL (Certbot)</button>
</div>
<div class="card">
<div class="card-title">View Config</div>
<label>Site name</label>
<input type="text" id="config-site" placeholder="site filename" style="width:100%">
<br><br>
<button class="btn" onclick="viewConfig()">View</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleSubFields() {
const t = document.getElementById('sub-type').value;
document.getElementById('proxy-fields').style.display = t === 'proxy' ? '' : 'none';
document.getElementById('static-fields').style.display = t === 'static' ? '' : 'none';
}
async function loadSites() {
const res = await apiGet('/api/nginx/sites');
showResult(res, 'sites-output');
}
async function reloadNginx() {
const res = await apiPost('/api/nginx/reload');
showResult(res);
}
async function addSubdomain() {
const sub = document.getElementById('sub-name').value;
const type = document.getElementById('sub-type').value;
if (!sub) { alert('Enter subdomain'); return; }
const body = {subdomain: sub};
if (type === 'proxy') body.proxy_port = document.getElementById('sub-port').value;
else body.static_root = document.getElementById('sub-root').value;
const res = await apiPost('/api/nginx/add-subdomain', body);
showResult(res);
loadSites();
}
async function addSSL() {
const domain = document.getElementById('ssl-domain').value;
if (!domain) return;
document.getElementById('output').innerHTML = '<span class="info">Installing SSL for '+domain+'... (may take a minute)</span>';
const res = await apiPost('/api/nginx/ssl/'+domain);
showResult(res);
}
async function viewConfig() {
const site = document.getElementById('config-site').value;
if (!site) return;
const res = await apiGet('/api/nginx/config/'+site);
showResult(res);
}
loadSites();
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,216 @@
{% extends "base.html" %}
{% block title %}Settings{% endblock %}
{% block content %}
<h1>[%] Settings</h1>
<div class="grid grid-2">
<div class="card">
<div class="card-title">VPS Connection</div>
<label>Host</label>
<input type="text" id="s-host" style="width:100%">
<label>User</label>
<input type="text" id="s-user" style="width:100%">
<label>Port</label>
<input type="number" id="s-port" style="width:100%">
<label>SSH Key Path</label>
<input type="text" id="s-key" style="width:100%">
</div>
<div class="card">
<div class="card-title">Domain & Paths</div>
<label>Domain</label>
<input type="text" id="s-domain" style="width:100%">
<label>Web Root</label>
<input type="text" id="s-webroot" style="width:100%">
<label>Compose Path</label>
<input type="text" id="s-compose" style="width:100%">
</div>
</div>
<div class="card">
<div class="card-title">Hosting Provider API</div>
<p style="color:#888;font-size:11px;margin-bottom:10px">Select your hosting provider and enter API credentials for DNS management.</p>
<label>Provider</label>
<select id="s-provider" style="width:100%" onchange="providerChanged()">
<option value="">-- Select Provider --</option>
</select>
<div id="provider-notes" style="font-size:11px;color:#555;margin:5px 3px"></div>
<label id="lbl-apikey">API Key</label>
<input type="text" id="s-apikey" style="width:100%" placeholder="Enter API key">
<div id="provider-docs" style="font-size:11px;margin:5px 3px"></div>
</div>
<div class="card">
<div class="card-title">E2E SSH Encryption</div>
<p style="color:#888;font-size:11px;margin-bottom:10px">
Encrypts all SSH commands with AES-256-GCM before transport. Requires the setec-agent on the VPS.
</p>
<div id="e2e-status" style="margin-bottom:10px;font-size:12px;color:#555">Loading...</div>
<div class="toolbar" style="margin-bottom:0">
<button class="btn" id="btn-e2e-toggle" onclick="toggleE2E()">Enable E2E</button>
<button class="btn" onclick="deployE2E()">Deploy Agent</button>
<button class="btn" onclick="testE2E()">Test Tunnel</button>
</div>
<div class="output" id="e2e-output" style="margin-top:10px;display:none"></div>
</div>
<div class="toolbar">
<button class="btn" onclick="saveSettings()">Save Settings</button>
<button class="btn" onclick="loadSettings()">Reload</button>
<button class="btn" onclick="testConnection()">Test SSH Connection</button>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
let providers = [];
async function loadProviders() {
const res = await apiGet('/api/hosting/providers');
if (!res.ok) return;
providers = res.data;
const sel = document.getElementById('s-provider');
providers.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
sel.appendChild(opt);
});
}
function providerChanged() {
const id = document.getElementById('s-provider').value;
const p = providers.find(x => x.id === id);
if (p) {
document.getElementById('lbl-apikey').textContent = p.api_key_label || 'API Key';
document.getElementById('provider-notes').textContent = p.notes || '';
document.getElementById('provider-docs').innerHTML = p.docs ? '<a href="' + p.docs + '" target="_blank">' + p.docs + '</a>' : '';
} else {
document.getElementById('lbl-apikey').textContent = 'API Key';
document.getElementById('provider-notes').textContent = '';
document.getElementById('provider-docs').innerHTML = '';
}
}
async function loadSettings() {
const res = await apiGet('/api/settings');
if (!res.ok) { showResult(res); return; }
const d = res.data;
document.getElementById('s-host').value = d.vps_host || '';
document.getElementById('s-user').value = d.vps_user || '';
document.getElementById('s-port').value = d.vps_port || 22;
document.getElementById('s-key').value = d.ssh_key_path || '';
document.getElementById('s-domain').value = d.domain || '';
document.getElementById('s-apikey').value = d.hostinger_api_key || '';
document.getElementById('s-webroot').value = d.web_root || '';
document.getElementById('s-compose').value = d.compose_path || '';
if (d.hosting_provider) {
document.getElementById('s-provider').value = d.hosting_provider;
providerChanged();
}
document.getElementById('output').innerHTML = '<span class="info">Settings loaded.</span>';
}
async function saveSettings() {
const body = {
vps_host: document.getElementById('s-host').value,
vps_user: document.getElementById('s-user').value,
vps_port: parseInt(document.getElementById('s-port').value),
ssh_key_path: document.getElementById('s-key').value,
domain: document.getElementById('s-domain').value,
hostinger_api_key: document.getElementById('s-apikey').value,
hosting_provider: document.getElementById('s-provider').value,
web_root: document.getElementById('s-webroot').value,
compose_path: document.getElementById('s-compose').value,
};
const res = await apiPost('/api/settings', body);
showResult(res);
}
async function testConnection() {
document.getElementById('output').innerHTML = '<span class="info">Testing SSH...</span>';
const res = await apiGet('/api/ssh/test');
showResult(res);
}
// ── E2E Tunnel ──
async function loadE2EStatus() {
const res = await apiGet('/api/e2e/status');
const el = document.getElementById('e2e-status');
const btn = document.getElementById('btn-e2e-toggle');
if (!res.ok) { el.innerHTML = '<span class="error">Failed to load E2E status</span>'; return; }
const d = res.data;
const active = d.e2e_enabled && d.e2e_deployed;
let html = 'Status: ';
if (active) {
html += '<span class="status-ok">ACTIVE</span>';
btn.textContent = 'Disable E2E';
} else if (d.e2e_deployed) {
html += '<span class="status-warn">Deployed but disabled</span>';
btn.textContent = 'Enable E2E';
} else {
html += '<span class="status-err">Not deployed</span>';
btn.textContent = 'Enable E2E';
}
html += ' &nbsp;|&nbsp; Agent: <code>' + d.agent_path + '</code>';
el.innerHTML = html;
}
async function toggleE2E() {
const res = await apiGet('/api/e2e/status');
if (!res.ok) return;
const nowEnabled = res.data.e2e_enabled && res.data.e2e_deployed;
const out = document.getElementById('e2e-output');
out.style.display = 'block';
out.innerHTML = '<span class="info">' + (nowEnabled ? 'Disabling' : 'Enabling') + ' E2E...</span>';
const r = await apiPost('/api/e2e/toggle', {enabled: !nowEnabled});
if (r.ok) {
out.innerHTML = '<span class="status-ok">E2E ' + (!nowEnabled ? 'enabled' : 'disabled') + '</span>';
} else {
out.innerHTML = '<span class="error">' + (r.error || 'Failed') + '</span>';
}
loadE2EStatus();
}
async function deployE2E() {
const out = document.getElementById('e2e-output');
out.style.display = 'block';
out.innerHTML = '<span class="info">Deploying agent + tunnel key to VPS... this may take a moment.</span>';
const r = await apiPost('/api/e2e/deploy', {});
if (r.ok) {
let html = '<span class="status-ok">Deployment complete!</span><br>';
(r.data || []).forEach(s => {
html += (s.ok ? '<span class="status-ok">[OK]</span>' : '<span class="status-err">[FAIL]</span>') + ' ' + s.step + '<br>';
});
out.innerHTML = html;
} else {
let html = '<span class="error">' + (r.error || 'Deploy failed') + '</span><br>';
(r.data || []).forEach(s => {
html += (s.ok ? '<span class="status-ok">[OK]</span>' : '<span class="status-err">[FAIL]</span>') + ' ' + s.step + '<br>';
});
out.innerHTML = html;
}
loadE2EStatus();
}
async function testE2E() {
const out = document.getElementById('e2e-output');
out.style.display = 'block';
out.innerHTML = '<span class="info">Testing E2E tunnel...</span>';
const r = await apiPost('/api/e2e/test', {});
if (r.ok) {
out.innerHTML = '<span class="status-ok">E2E tunnel working!</span><br><pre>' + (r.data.output || '') + '</pre>';
} else {
out.innerHTML = '<span class="error">' + (r.error || 'Test failed') + '</span>';
}
}
loadProviders();
loadSettings();
loadE2EStatus();
</script>
{% endblock %}

View File

@@ -0,0 +1,161 @@
{% extends "base.html" %}
{% block title %}SMTP{% endblock %}
{% block content %}
<h1>[*] SMTP / Mail</h1>
<div class="toolbar">
<button class="btn" onclick="loadSmtpStatus()">Status</button>
<button class="btn" onclick="checkDNS()">Check DNS</button>
<button class="btn" onclick="flushQueue()">Flush Queue</button>
<button class="btn btn-warn" onclick="restartSmtp()">Restart Postfix</button>
<button class="btn" onclick="showTab('status')">Status</button>
<button class="btn" onclick="showTab('send')">Send Email</button>
<button class="btn" onclick="showTab('mass')">Mass Email (BCC)</button>
</div>
<!-- TAB: Status -->
<div id="tab-status">
<div class="grid grid-2">
<div class="card">
<div class="card-title">SMTP Status</div>
<div class="output" id="smtp-output"><span class="info">Loading...</span></div>
</div>
<div class="card">
<div class="card-title">DNS Records (SPF/DKIM/DMARC)</div>
<div class="output" id="dns-output"><span class="info">Click "Check DNS"</span></div>
</div>
</div>
<div class="card">
<div class="card-title">Quick Test</div>
<input type="email" id="test-email" placeholder="recipient@example.com" style="width:300px">
<button class="btn" onclick="sendTest()">Send Test</button>
</div>
</div>
<!-- TAB: Send Email -->
<div id="tab-send" style="display:none">
<div class="card">
<div class="card-title">Compose Email</div>
<label>From</label>
<input type="text" id="send-from" value="noreply@seteclabs.io" style="width:100%">
<label>To</label>
<input type="text" id="send-to" placeholder="recipient@example.com" style="width:100%">
<label>Subject</label>
<input type="text" id="send-subject" placeholder="Email subject" style="width:100%">
<label>Body</label>
<textarea id="send-body" rows="10" style="width:100%" placeholder="Type your message here..."></textarea>
<br><br>
<button class="btn" onclick="sendEmail()">Send Email</button>
</div>
</div>
<!-- TAB: Mass Email (BCC) -->
<div id="tab-mass" style="display:none">
<div class="card">
<div class="card-title">Mass Email (BCC Mode)</div>
<p style="color:#888;font-size:12px;margin-bottom:10px">
Each recipient gets their own individual email. No one can see other recipients' addresses.
</p>
<label>From</label>
<input type="text" id="mass-from" value="noreply@seteclabs.io" style="width:100%">
<label>Recipients (comma or newline separated)</label>
<textarea id="mass-to" rows="5" style="width:100%" placeholder="joe@example.com, sam@example.com, bob@example.com
or one per line:
joe@example.com
sam@example.com
bob@example.com"></textarea>
<label>Subject</label>
<input type="text" id="mass-subject" placeholder="Email subject" style="width:100%">
<label>Body</label>
<textarea id="mass-body" rows="10" style="width:100%" placeholder="Type your message here..."></textarea>
<br><br>
<div class="toolbar">
<button class="btn" onclick="previewMass()">Preview (count recipients)</button>
<button class="btn btn-warn" onclick="sendMass()">Send to All</button>
</div>
<div id="mass-preview" style="margin-top:10px;font-size:12px;color:#888"></div>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showTab(tab) {
document.getElementById('tab-status').style.display = tab === 'status' ? '' : 'none';
document.getElementById('tab-send').style.display = tab === 'send' ? '' : 'none';
document.getElementById('tab-mass').style.display = tab === 'mass' ? '' : 'none';
}
// ── Status ──
async function loadSmtpStatus() {
const res = await apiGet('/api/smtp/status');
showResult(res, 'smtp-output');
}
async function checkDNS() {
const res = await apiGet('/api/smtp/dns-check');
showResult(res, 'dns-output');
}
async function flushQueue() {
const res = await apiPost('/api/smtp/flush');
showResult(res);
}
async function restartSmtp() {
const res = await apiPost('/api/smtp/restart');
showResult(res);
}
async function sendTest() {
const to = document.getElementById('test-email').value;
if (!to) { alert('Enter email address'); return; }
const res = await apiPost('/api/smtp/send-test', {to});
showResult(res);
}
// ── Send Email ──
async function sendEmail() {
const from = document.getElementById('send-from').value.trim();
const to = document.getElementById('send-to').value.trim();
const subject = document.getElementById('send-subject').value.trim();
const body = document.getElementById('send-body').value;
if (!to || !subject) { alert('Fill in To and Subject'); return; }
const res = await apiPost('/api/smtp/send', {from, to, subject, body});
showResult(res);
}
// ── Mass Email ──
function parseRecipients() {
const raw = document.getElementById('mass-to').value;
return raw.split(/[,\n]+/).map(e => e.trim()).filter(e => e && e.includes('@'));
}
function previewMass() {
const recipients = parseRecipients();
const el = document.getElementById('mass-preview');
el.innerHTML = `<span style="color:#00ff41">${recipients.length} recipients found:</span><br>` +
recipients.map(r => ` - ${escHtml(r)}`).join('<br>');
}
async function sendMass() {
const recipients = parseRecipients();
if (!recipients.length) { alert('Enter at least one recipient'); return; }
const from = document.getElementById('mass-from').value.trim();
const subject = document.getElementById('mass-subject').value.trim();
const body = document.getElementById('mass-body').value;
if (!subject) { alert('Enter a subject'); return; }
if (!confirm(`Send email to ${recipients.length} recipients individually (BCC mode)?`)) return;
const out = document.getElementById('output');
out.innerHTML = `<span class="info">Sending to ${recipients.length} recipients...</span>\n`;
const res = await apiPost('/api/smtp/send-mass', {from, recipients, subject, body});
showResult(res);
}
loadSmtpStatus();
</script>
{% endblock %}

View File

@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block title %}Terminal{% endblock %}
{% block content %}
<h1>[$] SSH Terminal</h1>
<div class="card">
<div class="card-title">Remote Shell (root@VPS)</div>
<div class="output" id="output" style="min-height:400px;max-height:700px"></div>
<div style="display:flex;margin-top:5px">
<span style="color:#00ff41;padding:6px">$</span>
<input type="text" id="cmd-input" placeholder="enter command..." style="flex:1"
onkeydown="if(event.key==='Enter')runCmd()"
autofocus>
<button class="btn" onclick="runCmd()">Run</button>
</div>
</div>
<div class="card">
<div class="card-title">Quick Commands</div>
<div class="toolbar">
<button class="btn" onclick="quickCmd('uptime')">uptime</button>
<button class="btn" onclick="quickCmd('free -h')">memory</button>
<button class="btn" onclick="quickCmd('df -h /')">disk</button>
<button class="btn" onclick="quickCmd('docker ps')">docker ps</button>
<button class="btn" onclick="quickCmd('systemctl status nginx')">nginx status</button>
<button class="btn" onclick="quickCmd('ufw status')">firewall</button>
<button class="btn" onclick="quickCmd('tail -20 /var/log/syslog')">syslog</button>
<button class="btn" onclick="quickCmd('last -10')">last logins</button>
<button class="btn" onclick="quickCmd('netstat -tlnp')">open ports</button>
<button class="btn" onclick="quickCmd('gitlab-ctl status')">gitlab status</button>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const history = [];
let histIdx = -1;
const cmdInput = document.getElementById('cmd-input');
const output = document.getElementById('output');
cmdInput.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
if (histIdx < history.length - 1) { histIdx++; cmdInput.value = history[histIdx]; }
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (histIdx > 0) { histIdx--; cmdInput.value = history[histIdx]; }
else { histIdx = -1; cmdInput.value = ''; }
}
});
async function runCmd() {
const cmd = cmdInput.value.trim();
if (!cmd) return;
history.unshift(cmd);
histIdx = -1;
cmdInput.value = '';
output.innerHTML += '<span style="color:#888">$ ' + escHtml(cmd) + '</span>\n';
output.innerHTML += '<span class="info">running...</span>\n';
output.scrollTop = output.scrollHeight;
const res = await apiPost('/api/terminal/exec', {cmd});
// Remove the "running..." line
output.innerHTML = output.innerHTML.replace(/<span class="info">running\.\.\.<\/span>\n$/, '');
if (res.ok && res.data) {
if (res.data.stdout) output.innerHTML += escHtml(res.data.stdout);
if (res.data.stderr) output.innerHTML += '<span class="err">' + escHtml(res.data.stderr) + '</span>';
if (res.data.exit_code && res.data.exit_code !== 0)
output.innerHTML += '<span class="err">[exit: ' + res.data.exit_code + ']</span>\n';
} else {
output.innerHTML += '<span class="err">Error: ' + (res.error || 'unknown') + '</span>\n';
}
output.innerHTML += '\n';
output.scrollTop = output.scrollHeight;
}
function quickCmd(cmd) {
cmdInput.value = cmd;
runCmd();
}
</script>
{% endblock %}

View File

@@ -0,0 +1,516 @@
{% extends "base.html" %}
{% block title %}Setup Wizard{% endblock %}
{% block content %}
<h1>[+] Setup Wizard</h1>
<div id="wiz-steps" style="margin-bottom:15px;font-size:11px;color:#555">
<span id="ws-1" class="status-ok">[1] Terms</span> &rarr;
<span id="ws-2">[2] SSH Keys</span> &rarr;
<span id="ws-3">[3] VPS</span> &rarr;
<span id="ws-4">[4] API</span> &rarr;
<span id="ws-5">[5] Paths</span> &rarr;
<span id="ws-6">[6] Test</span>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 1: Terms / License -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-1" class="card">
<div class="card-title">License Agreement &amp; Terms of Use</div>
<div style="font-size:12px;line-height:1.6;max-height:400px;overflow-y:auto;padding:10px;background:#000;border:1px solid #333;margin-bottom:15px">
<p style="color:#ff4444;margin-bottom:10px"><strong>IMPORTANT - READ BEFORE CONTINUING</strong></p>
<p style="margin-bottom:10px"><strong style="color:#ff4444">1. RESTRICTED USE LICENSE</strong><br>
This software is licensed for use by <strong>private individuals, independent security
researchers, and non-governmental organizations ONLY</strong>. By using this software you
affirm that you are not acting on behalf of, employed by, contracted by, or otherwise
affiliated with any:<br>
&bull; <strong style="color:#ff4444">Law enforcement agency</strong> (local, state, federal, or international)<br>
&bull; <strong style="color:#ff4444">Government agency or department</strong> (civilian or military)<br>
&bull; <strong style="color:#ff4444">Intelligence service</strong> (domestic or foreign)<br>
&bull; <strong style="color:#ff4444">Government contractor</strong> performing work for any of the above<br>
<br>
Use of this software by any of the above entities or their agents is <strong>strictly
prohibited</strong> and constitutes a violation of this license. This restriction applies
regardless of the purpose, including but not limited to: investigations, surveillance,
offensive operations, defensive operations, or "research." No exceptions.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">2. NO WARRANTY</strong><br>
This software is provided "AS IS" without warranty of any kind, express or implied.
SETEC LABS makes no guarantees regarding reliability, availability, or fitness for
any particular purpose. Use at your own risk.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">3. NO GUARANTEE OF SUPPORT</strong><br>
While the community may assist, there is no obligation to provide support, updates,
or bug fixes. This is free, open-source software maintained by volunteers.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">4. LIMITATION OF LIABILITY</strong><br>
Under no circumstances shall SETEC LABS, its contributors, or the darkHal group be
liable for any direct, indirect, incidental, or consequential damages arising from
the use of this software. This includes but is not limited to: data loss, system
downtime, security breaches, or misconfiguration of your server.</p>
<p style="margin-bottom:10px"><strong style="color:#ff4444">5. IF YOU PAID FOR THIS SOFTWARE, YOU WERE SCAMMED</strong><br>
SETEC LABS Manager is <strong>100% free and open source</strong>. If someone charged you
money for this application, you were ripped off and likely received a version bundled
with malware. Delete it immediately.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">6. OFFICIAL DOWNLOAD SOURCES</strong><br>
Only download SETEC applications from these trusted sources:<br>
&bull; <span style="color:#00ff41">repo.seteclabs.io</span> (Official Gitea repository)<br>
&bull; <span style="color:#00ff41">github.com/DigiJEth</span> (Official GitHub mirror)<br>
Any other source is unauthorized and potentially dangerous.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">7. ROOT ACCESS WARNING</strong><br>
This software executes commands on remote servers via SSH with the privileges of the
configured user. Misconfiguration can result in data loss or security vulnerabilities.
You are solely responsible for the actions performed through this tool.</p>
<p style="color:#888;font-size:10px;margin-top:15px;border-top:1px solid #333;padding-top:10px">
SETEC LABS Manager &bull; Free Software &bull; darkHal Group &bull; For the people, not the state.</p>
</div>
<div style="margin-bottom:10px">
<label style="display:inline;cursor:pointer">
<input type="checkbox" id="tos-accept" onchange="tosChanged()" style="margin-right:8px">
<span style="color:#ffaa00">I have read and accept these terms and I am not affiliated with any government or law enforcement entity</span>
</label>
</div>
<button class="btn" id="btn-tos-next" onclick="acceptTOS()" disabled style="opacity:0.3">Next &rarr;</button>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 2: SSH Key Selection/Generation -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-2" class="card" style="display:none">
<div class="card-title">SSH Key Setup</div>
<p style="font-size:12px;color:#888;margin-bottom:15px">
SETEC Manager connects to your VPS via SSH using key-based authentication.
Do you already have SSH keys generated?
</p>
<div style="margin-bottom:15px">
<button class="btn" id="btn-has-keys" onclick="sshKeyChoice('yes')" style="padding:10px 20px">
Yes, I have SSH keys
</button>
<button class="btn" id="btn-no-keys" onclick="sshKeyChoice('no')" style="padding:10px 20px">
No, I need to create them
</button>
</div>
<!-- YES: User has keys -->
<div id="ssh-has-keys" style="display:none">
<label>SSH Private Key Path</label>
<input type="text" id="w-key" style="width:100%" placeholder="C:/keys/setec">
<div style="font-size:10px;color:#555;margin:2px 3px 5px">
Enter the full path to your <strong style="color:#888">private</strong> key file (not the .pub file).
</div>
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
Common locations:<br>
<span style="color:#00ff41;line-height:1.8">
&bull; C:/Users/YourName/.ssh/id_ed25519<br>
&bull; C:/Users/YourName/.ssh/id_rsa<br>
&bull; C:/keys/setec<br>
&bull; ~/.ssh/id_ed25519 (Linux/Mac)
</span>
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(1)">&larr; Back</button>
<button class="btn" onclick="saveKeyAndContinue()">Save &amp; Continue &rarr;</button>
</div>
</div>
<!-- NO: User needs keys -->
<div id="ssh-no-keys" style="display:none">
<p style="font-size:12px;color:#ffaa00;margin-bottom:10px">
No problem! Select your VPS hosting provider below for a step-by-step guide.
</p>
<label>Select your VPS host</label>
<select id="w-ssh-host" style="width:100%" onchange="sshHostChanged()">
<option value="">-- Select Host --</option>
<option value="hostinger">Hostinger</option>
<option value="digitalocean">DigitalOcean</option>
<option value="vultr">Vultr</option>
<option value="linode">Linode (Akamai)</option>
<option value="hetzner">Hetzner</option>
<option value="ovh">OVH / OVHcloud</option>
<option value="aws">AWS (EC2)</option>
<option value="contabo">Contabo</option>
<option value="other">Other / Self-Hosted</option>
</select>
<div id="ssh-host-guide" style="display:none;margin-top:15px;padding:12px;background:#000;border:1px solid #333;font-size:11px;line-height:1.8"></div>
<div id="ssh-generic-guide" style="display:none;margin-top:15px;padding:12px;background:#000;border:1px solid #333">
<p style="color:#88ff88;font-size:12px;margin-bottom:8px"><strong>Generate SSH Keys (any platform)</strong></p>
<p style="font-size:11px;color:#888;margin-bottom:8px">Open a terminal and run:</p>
<div style="font-size:10px;margin-bottom:10px">
<p style="color:#888;margin-bottom:3px">1. Generate key pair:</p>
<code style="color:#00ff41">ssh-keygen -t ed25519 -f C:/keys/setec -N ""</code>
</div>
<div style="font-size:10px;margin-bottom:10px">
<p style="color:#888;margin-bottom:3px">2. Copy the public key to your server:</p>
<code style="color:#00ff41">ssh-copy-id -i C:/keys/setec.pub root@YOUR_SERVER_IP</code>
</div>
<div style="font-size:10px;margin-bottom:10px">
<p style="color:#888;margin-bottom:3px">3. Test the connection:</p>
<code style="color:#00ff41">ssh -i C:/keys/setec root@YOUR_SERVER_IP</code>
</div>
<p style="font-size:10px;color:#555;margin-top:8px">Your private key will be at <span style="color:#00ff41">C:/keys/setec</span></p>
</div>
<div style="margin-top:15px">
<label>Once your keys are ready, enter the private key path:</label>
<input type="text" id="w-key-new" style="width:100%" placeholder="C:/keys/setec" value="C:/keys/setec">
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(1)">&larr; Back</button>
<button class="btn" onclick="saveNewKeyAndContinue()">Save &amp; Continue &rarr;</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 3: VPS Connection -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-3" class="card" style="display:none">
<div class="card-title">VPS Connection Setup</div>
<label>Server IP Address</label>
<input type="text" id="w-host" style="width:100%" placeholder="e.g. 192.168.1.100">
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
Find your VPS IP in your hosting provider's control panel.
</div>
<label>SSH Username</label>
<input type="text" id="w-user" style="width:100%" placeholder="root" value="root">
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
<strong style="color:#888">Recommended:</strong> Use <span style="color:#00ff41">root</span> or create a sudo user:<br>
<code style="color:#00ff41;font-size:10px">adduser setecadmin && usermod -aG sudo setecadmin</code><br>
<span style="color:#ffaa00">Important:</span> Disable password login after setting up SSH keys:<br>
<code style="color:#00ff41;font-size:10px">sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && systemctl restart sshd</code>
</div>
<label>SSH Port</label>
<input type="number" id="w-port" style="width:100%" placeholder="2222" value="2222">
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
<strong style="color:#ffaa00">We strongly recommend port 2222</strong> instead of default 22 to reduce brute-force attacks.<br>
<code style="color:#00ff41;font-size:10px">sed -i 's/^#*Port .*/Port 2222/' /etc/ssh/sshd_config && ufw allow 2222/tcp && systemctl restart sshd</code><br>
<span style="color:#ff4444">Do NOT close your current session until you verify the new port works!</span>
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(2)">&larr; Back</button>
<button class="btn" onclick="saveVPS()">Save &amp; Continue &rarr;</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 4: API Setup -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-4" class="card" style="display:none">
<div class="card-title">DNS API Setup</div>
<label>Domain</label>
<input type="text" id="w-domain" style="width:100%" placeholder="example.com">
<div style="font-size:10px;color:#555;margin:2px 3px 5px">Your primary domain name managed by this panel.</div>
<label>DNS Provider</label>
<select id="w-provider" style="width:100%" onchange="wizProviderChanged()">
<option value="">-- Select Provider --</option>
</select>
<div id="w-provider-notes" style="font-size:11px;color:#ffaa00;margin:5px 3px;min-height:20px"></div>
<label id="w-lbl-apikey">API Key</label>
<input type="text" id="w-apikey" style="width:100%" placeholder="Enter API key">
<div id="w-provider-docs" style="font-size:11px;margin:5px 3px"></div>
<div id="w-provider-help" style="font-size:10px;color:#555;margin:2px 3px 10px;display:none">
<div id="w-help-content"></div>
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(3)">&larr; Back</button>
<button class="btn" onclick="saveAPI()">Save &amp; Continue &rarr;</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 5: Paths -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-5" class="card" style="display:none">
<div class="card-title">Web Root &amp; Compose Path</div>
<label>Web Root Directory</label>
<input type="text" id="w-webroot" style="width:100%" placeholder="/var/www" value="/var/www">
<div style="font-size:10px;color:#555;margin:2px 3px 10px">Default: <span style="color:#00ff41">/var/www</span></div>
<label>Docker Compose Path</label>
<input type="text" id="w-compose" style="width:100%" placeholder="/opt/seteclabs/docker-compose.yml">
<div style="font-size:10px;color:#00ff41;margin:0 3px 5px;line-height:1.8">
&bull; /opt/seteclabs/docker-compose.yml<br>
&bull; /root/docker-compose.yml<br>
&bull; /srv/docker-compose.yml
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(4)">&larr; Back</button>
<button class="btn" onclick="savePaths()">Save &amp; Finish Setup &rarr;</button>
</div>
</div>
<!-- Step 6: Test (modal trigger) -->
<div id="step-6" style="display:none"></div>
<!-- Modal overlay -->
<div id="wiz-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:999;display:none;align-items:center;justify-content:center">
<div style="background:#111;border:1px solid #00ff41;padding:25px;max-width:500px;width:90%;max-height:80vh;overflow-y:auto">
<div id="modal-title" style="font-size:14px;color:#88ff88;margin-bottom:15px;border-bottom:1px solid #333;padding-bottom:8px">Setup Complete</div>
<div id="modal-body" style="font-size:12px;line-height:1.6"></div>
<div id="modal-buttons" style="margin-top:15px"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let wizProviders = [];
let currentStep = 1;
// ── Step navigation ─────────────────────────────────────────────
function goStep(n) {
for (let i = 1; i <= 6; i++) {
const el = document.getElementById('step-' + i);
if (el) el.style.display = (i === n) ? 'block' : 'none';
const ws = document.getElementById('ws-' + i);
if (ws) ws.className = (i <= n) ? 'status-ok' : '';
}
currentStep = n;
}
function tosChanged() {
const btn = document.getElementById('btn-tos-next');
if (document.getElementById('tos-accept').checked) {
btn.disabled = false; btn.style.opacity = '1';
} else {
btn.disabled = true; btn.style.opacity = '0.3';
}
}
// ── TOS acceptance ──────────────────────────────────────────────
async function acceptTOS() {
const res = await fetch('/api/wizard/accept-tos', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({})
}).then(r => r.json());
if (res.ok) {
goStep(2);
} else {
alert('Error saving TOS acceptance: ' + (res.error || 'Unknown'));
}
}
// ── Provider help ────────────────────────────────────────────────
const providerHelp = {
hostinger: 'Log in to <strong>hPanel</strong> &rarr; click your profile icon &rarr; <strong>API Keys</strong> &rarr; Create new key with DNS permissions.',
cloudflare: 'Go to <strong>dash.cloudflare.com</strong> &rarr; My Profile &rarr; <strong>API Tokens</strong> &rarr; Create Token &rarr; use "Edit zone DNS" template.',
digitalocean: 'Go to <strong>cloud.digitalocean.com</strong> &rarr; API &rarr; <strong>Tokens</strong> &rarr; Generate New Token with read+write scope.',
vultr: 'Go to <strong>my.vultr.com</strong> &rarr; Account &rarr; <strong>API</strong> &rarr; Enable API and copy the key.',
linode: 'Go to <strong>cloud.linode.com</strong> &rarr; My Profile &rarr; <strong>API Tokens</strong> &rarr; Create Personal Access Token.',
godaddy: 'Go to <strong>developer.godaddy.com</strong> &rarr; API Keys &rarr; Create New API Key. Format: <span style="color:#00ff41">key:secret</span>.',
namecheap: 'Go to <strong>namecheap.com</strong> &rarr; Profile &rarr; Tools &rarr; <strong>API Access</strong>. Whitelist your IP.',
hetzner: 'Go to <strong>dns.hetzner.com</strong> &rarr; API Tokens &rarr; Create new token.',
ovh: 'Go to <strong>api.ovh.com/createApp</strong>. You need Application Key, Application Secret, and Consumer Key.',
aws_route53: 'In <strong>AWS IAM Console</strong>, create access key with Route53 permissions. Format: <span style="color:#00ff41">ACCESS_KEY:SECRET</span>.'
};
async function loadWizProviders() {
const res = await apiGet('/api/hosting/providers');
if (!res.ok) return;
wizProviders = res.data;
const sel = document.getElementById('w-provider');
wizProviders.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
sel.appendChild(opt);
});
}
function wizProviderChanged() {
const id = document.getElementById('w-provider').value;
const p = wizProviders.find(x => x.id === id);
if (p) {
document.getElementById('w-lbl-apikey').textContent = p.api_key_label || 'API Key';
document.getElementById('w-provider-notes').textContent = p.notes || '';
document.getElementById('w-provider-docs').innerHTML = p.docs ?
'Docs: <a href="' + escHtml(p.docs) + '" target="_blank" style="color:#00ff41">' + escHtml(p.docs) + '</a>' : '';
const helpDiv = document.getElementById('w-provider-help');
const helpContent = document.getElementById('w-help-content');
if (providerHelp[id]) {
helpContent.innerHTML = '<strong style="color:#88ff88">How to get your key:</strong><br>' + providerHelp[id];
helpDiv.style.display = 'block';
} else { helpDiv.style.display = 'none'; }
} else {
document.getElementById('w-lbl-apikey').textContent = 'API Key';
document.getElementById('w-provider-notes').textContent = '';
document.getElementById('w-provider-docs').innerHTML = '';
document.getElementById('w-provider-help').style.display = 'none';
}
}
// ── SSH host guides ─────────────────────────────────────────────
const sshHostGuides = {
hostinger: { name: 'Hostinger', url: 'https://support.hostinger.com/en/articles/1583522-how-to-generate-ssh-keys', steps: 'Log in to <strong>hPanel</strong> &rarr; VPS &rarr; Settings &rarr; <strong>SSH Keys</strong> &rarr; Add SSH Key.' },
digitalocean: { name: 'DigitalOcean', url: 'https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/', steps: 'Go to <strong>Settings</strong> &rarr; <strong>Security</strong> &rarr; SSH Keys &rarr; Add SSH Key.' },
vultr: { name: 'Vultr', url: 'https://docs.vultr.com/how-do-i-generate-ssh-keys', steps: 'Go to <strong>Account</strong> &rarr; <strong>SSH Keys</strong> &rarr; Add SSH Key.' },
linode: { name: 'Linode (Akamai)', url: 'https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/', steps: 'Go to <strong>Profile</strong> &rarr; <strong>SSH Keys</strong> &rarr; Add SSH Key.' },
hetzner: { name: 'Hetzner', url: 'https://docs.hetzner.com/cloud/servers/getting-started/connecting-to-the-server/', steps: 'Go to <strong>Security</strong> &rarr; <strong>SSH Keys</strong> in Hetzner Cloud Console.' },
ovh: { name: 'OVH', url: 'https://help.ovhcloud.com/csm/en-dedicated-servers-creating-ssh-keys?id=kb_article_view&sysparm_article=KB0047697', steps: 'In OVHcloud Control Panel &rarr; <strong>Public Cloud</strong> &rarr; <strong>SSH Keys</strong>.' },
aws: { name: 'AWS (EC2)', url: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html', steps: 'AWS Console &rarr; EC2 &rarr; <strong>Key Pairs</strong> &rarr; Create or Import.' },
contabo: { name: 'Contabo', url: 'https://contabo.com/blog/establishing-connection-server-ssh/', steps: 'Generate keys locally, copy manually with <code style="color:#00ff41">ssh-copy-id</code>.' },
other: { name: 'Other', url: '', steps: '' }
};
function sshKeyChoice(choice) {
document.getElementById('ssh-has-keys').style.display = (choice === 'yes') ? 'block' : 'none';
document.getElementById('ssh-no-keys').style.display = (choice === 'no') ? 'block' : 'none';
document.getElementById('btn-has-keys').style.background = (choice === 'yes') ? '#00ff41' : '';
document.getElementById('btn-has-keys').style.color = (choice === 'yes') ? '#000' : '';
document.getElementById('btn-no-keys').style.background = (choice === 'no') ? '#00ff41' : '';
document.getElementById('btn-no-keys').style.color = (choice === 'no') ? '#000' : '';
}
function sshHostChanged() {
const id = document.getElementById('w-ssh-host').value;
const guideDiv = document.getElementById('ssh-host-guide');
const genericDiv = document.getElementById('ssh-generic-guide');
if (!id) { guideDiv.style.display = 'none'; genericDiv.style.display = 'none'; return; }
const host = sshHostGuides[id];
genericDiv.style.display = 'block';
if (host && (host.url || host.steps)) {
let html = '<p style="color:#88ff88;font-size:12px;margin-bottom:8px"><strong>' + escHtml(host.name) + ' SSH Key Guide</strong></p>';
if (host.steps) html += '<p style="font-size:11px;color:#888;margin-bottom:8px">' + host.steps + '</p>';
if (host.url) html += '<p style="margin-top:8px"><a href="' + escHtml(host.url) + '" target="_blank" style="color:#00ff41">' + escHtml(host.url) + '</a></p>';
guideDiv.innerHTML = html;
guideDiv.style.display = 'block';
} else { guideDiv.style.display = 'none'; }
}
function saveKeyAndContinue() {
if (!document.getElementById('w-key').value) { alert('Enter SSH private key path.'); return; }
goStep(3);
}
function saveNewKeyAndContinue() {
const k = document.getElementById('w-key-new').value;
if (!k) { alert('Enter SSH private key path.'); return; }
document.getElementById('w-key').value = k;
goStep(3);
}
function getKeyPath() {
return document.getElementById('w-key').value || document.getElementById('w-key-new').value || '';
}
async function saveVPS() {
const body = { vps_host: document.getElementById('w-host').value, vps_user: document.getElementById('w-user').value,
vps_port: parseInt(document.getElementById('w-port').value) || 22, ssh_key_path: getKeyPath() };
if (!body.vps_host) { alert('Enter server IP.'); return; }
if (!body.ssh_key_path) { alert('Go back and enter SSH key path.'); return; }
const res = await apiPost('/api/settings', body);
if (res.ok) goStep(4); else alert('Error: ' + (res.error || 'Unknown'));
}
async function saveAPI() {
const body = { domain: document.getElementById('w-domain').value, hosting_provider: document.getElementById('w-provider').value,
hostinger_api_key: document.getElementById('w-apikey').value };
if (!body.domain) { alert('Enter your domain.'); return; }
const res = await apiPost('/api/settings', body);
if (res.ok) goStep(5); else alert('Error: ' + (res.error || 'Unknown'));
}
async function savePaths() {
const body = { web_root: document.getElementById('w-webroot').value || '/var/www',
compose_path: document.getElementById('w-compose').value, setup_complete: true };
const res = await apiPost('/api/settings', body);
if (res.ok) showTestModal(); else alert('Error: ' + (res.error || 'Unknown'));
}
// ── Test modal (same as before) ─────────────────────────────────
function showTestModal() {
document.getElementById('wiz-modal').style.display = 'flex';
document.getElementById('modal-title').textContent = 'Setup Complete!';
document.getElementById('modal-body').innerHTML =
'<p style="margin-bottom:10px">Your settings have been saved.</p>' +
'<p>Would you like to test the connection?</p>';
document.getElementById('modal-buttons').innerHTML =
'<button class="btn" onclick="runTest()">Yes, Test Connection</button> ' +
'<button class="btn" onclick="closeModal()">Skip</button>';
}
async function runTest() {
document.getElementById('modal-title').textContent = 'Testing Connection...';
document.getElementById('modal-body').innerHTML = '<span style="color:#00ff41">Connecting via SSH...</span>';
document.getElementById('modal-buttons').innerHTML = '';
const sshRes = await apiGet('/api/wizard/test');
if (sshRes.ok && sshRes.data) {
const d = sshRes.data;
if (d.ssh_ok) {
let html = '<p style="color:#00ff41;margin-bottom:10px"><strong>SSH: SUCCESS</strong></p>';
html += '<div style="background:#000;padding:8px;border:1px solid #333;font-size:11px;margin-bottom:10px">' + escHtml(d.ssh_output || '') + '</div>';
if (d.api_ok) html += '<p style="color:#00ff41"><strong>DNS API: SUCCESS</strong></p>';
else if (d.api_error) html += '<p style="color:#ffaa00"><strong>DNS API: ' + escHtml(d.api_error) + '</strong></p>';
document.getElementById('modal-title').textContent = 'Connection Successful!';
document.getElementById('modal-body').innerHTML = html;
document.getElementById('modal-buttons').innerHTML =
'<button class="btn" onclick="window.location.href=\'/\'">Go to Dashboard</button>';
} else { showTestFailed(d.ssh_error || d.error || 'Connection failed'); }
} else { showTestFailed(sshRes.error || 'Connection failed'); }
}
function showTestFailed(error) {
document.getElementById('modal-title').textContent = 'Connection Failed';
document.getElementById('modal-body').innerHTML =
'<p style="color:#ff4444;margin-bottom:10px"><strong>Connection Failed</strong></p>' +
'<div style="background:#000;padding:8px;border:1px solid #ff4444;font-size:11px;margin-bottom:10px;color:#ff4444;white-space:pre-wrap">' + escHtml(error) + '</div>' +
'<p style="color:#ffaa00;margin-bottom:8px">Check:</p>' +
'<ul style="font-size:11px;color:#888;margin-left:15px;line-height:1.8">' +
'<li>Server IP is correct and VPS is running</li>' +
'<li>SSH port is open and correct</li>' +
'<li>SSH key exists at the specified path</li>' +
'<li>Public key is in ~/.ssh/authorized_keys on server</li></ul>';
document.getElementById('modal-buttons').innerHTML =
'<button class="btn" onclick="closeModal()">Close &amp; Fix Settings</button>';
}
function closeModal() { document.getElementById('wiz-modal').style.display = 'none'; }
// ── Prefill ─────────────────────────────────────────────────────
async function prefillWizard() {
const res = await apiGet('/api/settings');
if (!res.ok) return;
const d = res.data;
if (d.vps_host) document.getElementById('w-host').value = d.vps_host;
if (d.vps_user) document.getElementById('w-user').value = d.vps_user;
if (d.vps_port) document.getElementById('w-port').value = d.vps_port;
if (d.ssh_key_path) {
document.getElementById('w-key').value = d.ssh_key_path;
document.getElementById('w-key-new').value = d.ssh_key_path;
}
if (d.domain) document.getElementById('w-domain').value = d.domain;
if (d.hostinger_api_key) document.getElementById('w-apikey').value = d.hostinger_api_key;
if (d.web_root) document.getElementById('w-webroot').value = d.web_root;
if (d.compose_path) document.getElementById('w-compose').value = d.compose_path;
if (d.hosting_provider) {
document.getElementById('w-provider').value = d.hosting_provider;
wizProviderChanged();
}
}
loadWizProviders().then(() => prefillWizard());
</script>
{% endblock %}