No One Can Stop Me Now

This commit is contained in:
DigiJ
2026-03-13 23:48:47 -07:00
parent 4d3570781e
commit 1a138a2bd0
428 changed files with 519668 additions and 259 deletions

View File

@@ -0,0 +1,9 @@
package web
import "embed"
//go:embed templates/*.html
var TemplateFS embed.FS
//go:embed static
var StaticFS embed.FS

View File

@@ -0,0 +1,370 @@
/* Setec App Manager — Dark Theme */
:root {
--primary: #6366f1;
--primary-hover: #818cf8;
--surface: #222536;
--bg: #1a1b2e;
--text: #e2e8f0;
--text-muted: #94a3b8;
--border: #2e3148;
--ok: #22c55e;
--err: #ef4444;
--warn: #f59e0b;
--radius: 6px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
min-height: 100vh;
line-height: 1.5;
}
/* ── Sidebar ── */
.sidebar {
width: 220px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 10;
}
.sidebar-header {
padding: 1.25rem 1rem;
border-bottom: 1px solid var(--border);
}
.sidebar-header h1 {
font-size: 1.25rem;
color: var(--primary);
letter-spacing: 0.1em;
}
.sidebar-header .subtitle {
font-size: .75rem;
color: var(--text-muted);
}
.nav-menu {
list-style: none;
flex: 1;
padding: .5rem 0;
overflow-y: auto;
}
.sidebar-logo {
width: 160px;
height: auto;
}
.nav-icon {
width: 18px;
height: 18px;
vertical-align: middle;
margin-right: 8px;
flex-shrink: 0;
}
.nav-link {
display: flex;
align-items: center;
padding: .55rem 1rem;
color: var(--text-muted);
text-decoration: none;
font-size: .9rem;
transition: background .15s, color .15s;
}
.nav-link:hover,
.nav-link.active {
background: rgba(99, 102, 241, .1);
color: var(--primary);
}
.sidebar-footer {
padding: .75rem 1rem;
border-top: 1px solid var(--border);
font-size: .8rem;
}
.sidebar-footer .user-badge {
color: var(--text-muted);
display: block;
margin-bottom: .25rem;
}
.sidebar-footer .logout {
color: var(--err);
font-size: .8rem;
}
/* ── Content ── */
.content {
margin-left: 220px;
flex: 1;
padding: 1.5rem 2rem;
min-height: 100vh;
}
.content h2 {
margin-bottom: 1rem;
font-size: 1.4rem;
}
.content h3 {
margin: 1.25rem 0 .75rem;
font-size: 1.1rem;
color: var(--text-muted);
}
/* ── Stat Cards / Grid ── */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.25rem;
}
.stat-card h3 {
margin: 0 0 .5rem;
font-size: .95rem;
color: var(--text-muted);
}
.stat-card .big-number {
font-size: 2rem;
font-weight: 700;
color: var(--primary);
}
/* ── Data Table ── */
.data-table {
width: 100%;
border-collapse: collapse;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 1rem;
}
.data-table th,
.data-table td {
padding: .6rem .85rem;
text-align: left;
border-bottom: 1px solid var(--border);
font-size: .9rem;
}
.data-table th {
background: rgba(99, 102, 241, .08);
color: var(--text-muted);
font-weight: 600;
font-size: .8rem;
text-transform: uppercase;
letter-spacing: .04em;
}
.data-table tbody tr:hover {
background: rgba(99, 102, 241, .04);
}
.data-table a {
color: var(--primary);
text-decoration: none;
}
.data-table a:hover {
text-decoration: underline;
}
/* ── Buttons ── */
.btn {
display: inline-block;
padding: .5rem 1rem;
font-size: .875rem;
font-weight: 500;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
color: var(--text);
cursor: pointer;
text-decoration: none;
transition: background .15s, border-color .15s;
line-height: 1.4;
}
.btn:hover {
background: var(--border);
}
.btn-primary {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn-primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
.btn-sm {
padding: .3rem .6rem;
font-size: .8rem;
}
/* ── Badges ── */
.badge {
display: inline-block;
padding: .15rem .5rem;
font-size: .75rem;
font-weight: 600;
border-radius: 999px;
background: var(--border);
color: var(--text-muted);
}
.badge-ok {
background: rgba(34, 197, 94, .15);
color: var(--ok);
}
.badge-err {
background: rgba(239, 68, 68, .15);
color: var(--err);
}
/* ── Forms ── */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: .3rem;
font-size: .85rem;
color: var(--text-muted);
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="email"],
.form-group select,
.form-group textarea {
width: 100%;
padding: .5rem .75rem;
font-size: .9rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
outline: none;
transition: border-color .15s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: var(--primary);
}
/* ── Progress Bar ── */
.progress-bar {
width: 100%;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
margin: .4rem 0;
}
.progress-fill {
height: 100%;
background: var(--primary);
border-radius: 4px;
transition: width .3s ease;
}
/* ── Login Page ── */
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg);
}
.login-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
width: 100%;
max-width: 380px;
}
.login-card h1 {
color: var(--primary);
letter-spacing: 0.1em;
margin-bottom: .25rem;
}
.login-card .subtitle {
color: var(--text-muted);
margin-bottom: 1.5rem;
font-size: .85rem;
}
.login-card .btn {
width: 100%;
margin-top: .5rem;
}
/* ── Error Message ── */
.error-msg {
margin-top: .75rem;
padding: .5rem .75rem;
background: rgba(239, 68, 68, .1);
border: 1px solid rgba(239, 68, 68, .3);
border-radius: var(--radius);
color: var(--err);
font-size: .85rem;
}
/* ── Utility ── */
code {
background: var(--bg);
padding: .1rem .35rem;
border-radius: 3px;
font-size: .85em;
}
p { margin-bottom: .35rem; }
/* ── Responsive ── */
@media (max-width: 768px) {
.sidebar { width: 60px; }
.sidebar-header h1 { font-size: .9rem; }
.sidebar-header .subtitle,
.sidebar-footer .user-badge { display: none; }
.nav-link { padding: .5rem; font-size: .75rem; text-align: center; }
.content { margin-left: 60px; padding: 1rem; }
.stats-grid { grid-template-columns: 1fr; }
}

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<defs>
<linearGradient id="fg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#a855f7"/>
</linearGradient>
</defs>
<rect width="32" height="32" rx="6" fill="#1e1e2e"/>
<path d="M16 3 L28 9 L28 19 Q28 27 16 30 Q4 27 4 19 L4 9 Z"
fill="none" stroke="url(#fg)" stroke-width="1.5" opacity="0.6"/>
<text x="16" y="22" font-family="monospace" font-size="16" font-weight="bold"
fill="url(#fg)" text-anchor="middle">S</text>
</svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<!-- Dashboard -->
<symbol id="icon-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
</symbol>
<!-- Sites / Globe -->
<symbol id="icon-sites" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</symbol>
<!-- Shield / Security -->
<symbol id="icon-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</symbol>
<!-- Lock / SSL -->
<symbol id="icon-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</symbol>
<!-- Server / Nginx -->
<symbol id="icon-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/>
<line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>
</symbol>
<!-- Firewall / Shield-off -->
<symbol id="icon-firewall" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
<line x1="9" y1="9" x2="15" y2="15"/><line x1="15" y1="9" x2="9" y2="15"/>
</symbol>
<!-- Users -->
<symbol id="icon-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</symbol>
<!-- Backup / Archive -->
<symbol id="icon-backup" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/>
<line x1="10" y1="12" x2="14" y2="12"/>
</symbol>
<!-- Monitor / Activity -->
<symbol id="icon-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</symbol>
<!-- Logs / Terminal -->
<symbol id="icon-logs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
</symbol>
<!-- Float / USB / Link -->
<symbol id="icon-float" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</symbol>
<!-- Hosting / Cloud-Server -->
<symbol id="icon-hosting" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
<line x1="12" y1="13" x2="12" y2="17"/><circle cx="12" cy="13" r="1"/>
</symbol>
<!-- AUTARCH / Hexagon -->
<symbol id="icon-autarch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 60" width="300" height="60">
<defs>
<linearGradient id="brandGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#a855f7"/>
</linearGradient>
</defs>
<!-- Shield icon -->
<path d="M30 5 L52 15 L52 33 Q52 48 30 53 Q8 48 8 33 L8 15 Z"
fill="url(#brandGrad)" opacity="0.15" stroke="url(#brandGrad)" stroke-width="1.5"/>
<text x="30" y="38" font-family="monospace" font-size="24" font-weight="bold"
fill="url(#brandGrad)" text-anchor="middle">S</text>
<!-- SETEC text -->
<text x="70" y="32" font-family="monospace" font-size="26" font-weight="bold"
fill="url(#brandGrad)" letter-spacing="4">SETEC</text>
<!-- Subtitle -->
<text x="70" y="48" font-family="monospace" font-size="11" fill="#6b7280"
letter-spacing="2">APP MANAGER</text>
</svg>

After

Width:  |  Height:  |  Size: 939 B

View File

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#a855f7;stop-opacity:1" />
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Shield shape -->
<path d="M100 10 L180 50 L180 120 Q180 170 100 190 Q20 170 20 120 L20 50 Z"
fill="url(#grad)" opacity="0.15" stroke="url(#grad)" stroke-width="2"/>
<!-- Inner shield -->
<path d="M100 25 L165 58 L165 115 Q165 158 100 175 Q35 158 35 115 L35 58 Z"
fill="none" stroke="url(#grad)" stroke-width="1.5" opacity="0.4"/>
<!-- S letter -->
<text x="100" y="125" font-family="monospace" font-size="90" font-weight="bold"
fill="url(#grad)" text-anchor="middle" filter="url(#glow)">S</text>
<!-- Circuit lines -->
<line x1="55" y1="155" x2="30" y2="155" stroke="#6366f1" stroke-width="1.5" opacity="0.5"/>
<circle cx="30" cy="155" r="3" fill="#6366f1" opacity="0.5"/>
<line x1="145" y1="155" x2="170" y2="155" stroke="#a855f7" stroke-width="1.5" opacity="0.5"/>
<circle cx="170" cy="155" r="3" fill="#a855f7" opacity="0.5"/>
<line x1="100" y1="45" x2="100" y2="25" stroke="#6366f1" stroke-width="1.5" opacity="0.5"/>
<circle cx="100" cy="22" r="3" fill="#6366f1" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 50" width="180" height="50">
<defs>
<linearGradient id="sideGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#a855f7"/>
</linearGradient>
</defs>
<!-- Mini shield -->
<path d="M22 4 L38 11 L38 25 Q38 36 22 40 Q6 36 6 25 L6 11 Z"
fill="url(#sideGrad)" opacity="0.2" stroke="url(#sideGrad)" stroke-width="1"/>
<text x="22" y="28" font-family="monospace" font-size="17" font-weight="bold"
fill="url(#sideGrad)" text-anchor="middle">S</text>
<!-- Text -->
<text x="52" y="24" font-family="monospace" font-size="18" font-weight="bold"
fill="#f9fafb" letter-spacing="3">SETEC</text>
<text x="52" y="38" font-family="monospace" font-size="8" fill="#6b7280"
letter-spacing="1.5">APP MANAGER</text>
</svg>

After

Width:  |  Height:  |  Size: 899 B

View File

@@ -0,0 +1,109 @@
/* Setec App Manager — Client JS */
// ── API helper ──
const api = {
async get(url) {
const r = await fetch(url, { credentials: 'same-origin' });
if (r.status === 401) { window.location.href = '/login'; return null; }
if (!r.ok) throw new Error(`GET ${url}: ${r.status}`);
return r.json();
},
async post(url, body) {
const r = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined
});
if (r.status === 401) { window.location.href = '/login'; return null; }
if (!r.ok) throw new Error(`POST ${url}: ${r.status}`);
return r.json();
},
async del(url) {
const r = await fetch(url, { method: 'DELETE', credentials: 'same-origin' });
if (r.status === 401) { window.location.href = '/login'; return null; }
if (!r.ok) throw new Error(`DELETE ${url}: ${r.status}`);
return r.json();
}
};
// ── Active nav highlight ──
(function highlightNav() {
const path = window.location.pathname;
document.querySelectorAll('.nav-link').forEach(link => {
const href = link.getAttribute('href');
if (href === '/' && path === '/') {
link.classList.add('active');
} else if (href !== '/' && path.startsWith(href)) {
link.classList.add('active');
}
});
})();
// ── Dashboard auto-refresh ──
(function dashboardRefresh() {
if (window.location.pathname !== '/') return;
const INTERVAL = 10000; // 10 seconds
async function refresh() {
try {
const data = await api.get('/api/stats');
if (!data) return;
// Update stat card values if elements exist
const updates = {
cpuBar: { style: `width: ${data.cpu || 0}%` },
memBar: { style: `width: ${data.mem_percent || 0}%` },
diskBar: { style: `width: ${data.disk_percent || 0}%` },
};
for (const [id, props] of Object.entries(updates)) {
const el = document.getElementById(id);
if (el && props.style) el.setAttribute('style', props.style);
if (el && props.text) el.textContent = props.text;
}
} catch (e) {
console.warn('Stats refresh failed:', e.message);
}
}
setInterval(refresh, INTERVAL);
})();
// ── Monitor page auto-refresh ──
(function monitorRefresh() {
if (window.location.pathname !== '/monitor') return;
const INTERVAL = 5000;
async function refresh() {
try {
const data = await api.get('/api/stats');
if (!data) return;
const bar = (id, pct) => {
const el = document.getElementById(id);
if (el) el.style.width = pct + '%';
};
const txt = (id, val) => {
const el = document.getElementById(id);
if (el) el.textContent = val;
};
bar('cpuBar', data.cpu || 0);
txt('cpuText', (data.cpu || 0).toFixed(1) + '%');
bar('memBar', data.mem_percent || 0);
bar('diskBar', data.disk_percent || 0);
if (data.mem_text) txt('memText', data.mem_text);
if (data.disk_text) txt('diskText', data.disk_text);
if (data.net_in) txt('netIn', data.net_in);
if (data.net_out) txt('netOut', data.net_out);
} catch (e) {
console.warn('Monitor refresh failed:', e.message);
}
}
setInterval(refresh, INTERVAL);
})();

View File

@@ -0,0 +1,50 @@
{{define "content"}}
<h2>AUTARCH</h2>
<div class="stats-grid">
<div class="stat-card">
<h3>Status</h3>
{{if .Data.Installed}}
<span class="badge badge-ok">Installed</span>
<p><strong>Version:</strong> {{.Data.Version}}</p>
{{else}}
<span class="badge badge-err">Not Installed</span>
{{end}}
<p><strong>Service:</strong>
{{if .Data.Running}}
<span class="badge badge-ok">Running</span>
{{else}}
<span class="badge badge-err">Stopped</span>
{{end}}
</p>
</div>
<div class="stat-card">
<h3>Info</h3>
<p><strong>Port:</strong> {{.Data.Port}}</p>
<p><strong>Uptime:</strong> {{.Data.Uptime}}</p>
{{if .Data.UpdateAvailable}}
<p><span class="badge badge-ok">Update Available: {{.Data.LatestVersion}}</span></p>
{{end}}
</div>
</div>
<h3>Actions</h3>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
{{if not .Data.Installed}}
<form method="POST" action="/autarch/install"><button class="btn btn-primary">Install</button></form>
{{else}}
{{if .Data.Running}}
<form method="POST" action="/autarch/stop"><button class="btn">Stop</button></form>
<form method="POST" action="/autarch/restart"><button class="btn">Restart</button></form>
{{else}}
<form method="POST" action="/autarch/start"><button class="btn btn-primary">Start</button></form>
{{end}}
{{if .Data.UpdateAvailable}}
<form method="POST" action="/autarch/update"><button class="btn btn-primary">Update</button></form>
{{end}}
<form method="POST" action="/autarch/uninstall" onsubmit="return confirm('Uninstall AUTARCH?')">
<button class="btn">Uninstall</button>
</form>
{{end}}
</div>
{{end}}

View File

@@ -0,0 +1,31 @@
{{define "content"}}
<div style="display:flex;justify-content:space-between;align-items:center">
<h2>Backups</h2>
<form method="POST" action="/backups/create"><button class="btn btn-primary">Create Backup</button></form>
</div>
<table class="data-table">
<thead>
<tr><th>Name</th><th>Size</th><th>Date</th><th>Type</th><th>Actions</th></tr>
</thead>
<tbody>
{{range .Data.Backups}}
<tr>
<td>{{.Name}}</td>
<td>{{.Size}}</td>
<td>{{.Date}}</td>
<td><span class="badge badge-ok">{{.Type}}</span></td>
<td>
<a href="/backups/download/{{.Name}}" class="btn btn-sm">Download</a>
<form method="POST" action="/backups/restore/{{.Name}}" style="display:inline"
onsubmit="return confirm('Restore from {{.Name}}?')">
<button class="btn btn-sm">Restore</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="5">No backups found.</td></tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setec App Manager{{if .Title}} — {{.Title}}{{end}}</title>
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="sidebar">
<div class="sidebar-header">
<img src="/static/img/sidebar-logo.svg" alt="Setec" class="sidebar-logo">
</div>
<ul class="nav-menu">
<li><a href="/" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-dashboard"/></svg> Dashboard</a></li>
<li><a href="/sites" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-sites"/></svg> Sites</a></li>
<li><a href="/autarch" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-autarch"/></svg> AUTARCH</a></li>
<li><a href="/ssl" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-lock"/></svg> SSL/TLS</a></li>
<li><a href="/nginx" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-server"/></svg> Nginx</a></li>
<li><a href="/firewall" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-firewall"/></svg> Firewall</a></li>
<li><a href="/users" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-users"/></svg> Users</a></li>
<li><a href="/backups" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-backup"/></svg> Backups</a></li>
<li><a href="/hosting" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-hosting"/></svg> Hosting</a></li>
<li><a href="/monitor" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-monitor"/></svg> Monitor</a></li>
<li><a href="/logs" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-logs"/></svg> Logs</a></li>
<li><a href="/float/sessions" class="nav-link"><svg class="nav-icon"><use href="/static/img/icons.svg#icon-float"/></svg> Float Mode</a></li>
</ul>
<div class="sidebar-footer">
{{if .Claims}}<span class="user-badge">{{.Claims.Username}}</span>{{end}}
<a href="/logout" class="nav-link logout">Logout</a>
</div>
</nav>
<main class="content">
{{template "content" .}}
</main>
<script src="/static/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,53 @@
{{define "content"}}
<h2>Dashboard</h2>
<div class="stats-grid">
<div class="stat-card">
<h3>System</h3>
<p><strong>Hostname:</strong> {{.Data.Hostname}}</p>
<p><strong>OS:</strong> {{.Data.OS}} / {{.Data.Arch}}</p>
<p><strong>CPUs:</strong> {{.Data.CPUs}}</p>
<p><strong>Uptime:</strong> {{.Data.Uptime}}</p>
<p><strong>Load:</strong> {{.Data.LoadAvg}}</p>
</div>
<div class="stat-card">
<h3>Memory</h3>
<div class="progress-bar">
<div class="progress-fill" style="width: {{printf "%.0f" .Data.MemPercent}}%"></div>
</div>
<p>{{.Data.MemUsed}} / {{.Data.MemTotal}} ({{printf "%.1f" .Data.MemPercent}}%)</p>
</div>
<div class="stat-card">
<h3>Disk</h3>
<div class="progress-bar">
<div class="progress-fill" style="width: {{printf "%.0f" .Data.DiskPercent}}%"></div>
</div>
<p>{{.Data.DiskUsed}} / {{.Data.DiskTotal}} ({{printf "%.1f" .Data.DiskPercent}}%)</p>
</div>
<div class="stat-card">
<h3>Sites</h3>
<p class="big-number">{{.Data.SiteCount}}</p>
<a href="/sites" class="btn btn-sm">Manage Sites</a>
</div>
</div>
<h3>Services</h3>
<table class="data-table">
<thead>
<tr><th>Service</th><th>Status</th></tr>
</thead>
<tbody>
{{range .Data.Services}}
<tr>
<td>{{.Name}}</td>
<td>
{{if .Running}}
<span class="badge badge-ok">{{.Status}}</span>
{{else}}
<span class="badge badge-err">{{.Status}}</span>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,40 @@
{{define "content"}}
<h2>Firewall Rules</h2>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<p><strong>Status:</strong>
{{if .Data.Active}}
<span class="badge badge-ok">Active</span>
{{else}}
<span class="badge badge-err">Inactive</span>
{{end}}
</p>
<form method="POST" action="/firewall/reload"><button class="btn btn-sm">Reload</button></form>
</div>
<table class="data-table">
<thead>
<tr><th>#</th><th>Action</th><th>From</th><th>To</th><th>Port</th><th>Proto</th></tr>
</thead>
<tbody>
{{range .Data.Rules}}
<tr>
<td>{{.Num}}</td>
<td>
{{if eq .Action "ALLOW"}}
<span class="badge badge-ok">ALLOW</span>
{{else}}
<span class="badge badge-err">{{.Action}}</span>
{{end}}
</td>
<td>{{.From}}</td>
<td>{{.To}}</td>
<td>{{.Port}}</td>
<td>{{.Proto}}</td>
</tr>
{{else}}
<tr><td colspan="6">No rules configured.</td></tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,40 @@
{{define "content"}}
<h2>Float Mode Sessions</h2>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<p>Active Sessions: <strong>{{len .Data.Sessions}}</strong></p>
<form method="POST" action="/float/sessions/new"><button class="btn btn-primary">New Session</button></form>
</div>
<table class="data-table">
<thead>
<tr><th>Session ID</th><th>Target</th><th>Started</th><th>Status</th><th>Actions</th></tr>
</thead>
<tbody>
{{range .Data.Sessions}}
<tr>
<td><code>{{.ID}}</code></td>
<td>{{.Target}}</td>
<td>{{.Started}}</td>
<td>
{{if eq .Status "active"}}
<span class="badge badge-ok">Active</span>
{{else if eq .Status "paused"}}
<span class="badge">Paused</span>
{{else}}
<span class="badge badge-err">{{.Status}}</span>
{{end}}
</td>
<td>
<a href="/float/sessions/{{.ID}}" class="btn btn-sm">View</a>
<form method="POST" action="/float/sessions/{{.ID}}/stop" style="display:inline">
<button class="btn btn-sm">Stop</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="5">No active float sessions.</td></tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,783 @@
{{define "content"}}
<h2>Hosting Providers</h2>
<!-- Provider Cards -->
<div class="stats-grid" id="provider-cards">
{{if .Data.Providers}}
{{range .Data.Providers}}
<div class="stat-card provider-card" data-provider="{{.Name}}" onclick="selectProvider('{{.Name}}')">
<h3>{{.DisplayName}}</h3>
<p>
{{if .Connected}}
<span class="badge badge-ok">Connected</span>
{{else if .HasConfig}}
<span class="badge badge-err">Disconnected</span>
{{else}}
<span class="badge">Not Configured</span>
{{end}}
</p>
</div>
{{end}}
{{else}}
<div class="stat-card">
<h3>No Providers</h3>
<p style="color: var(--text-muted);">No hosting providers are registered. Providers are loaded at server start.</p>
</div>
{{end}}
</div>
<!-- Tab Navigation -->
<div id="hosting-tabs" style="display:none; margin-top:1.5rem;">
<div style="display:flex; gap:.25rem; border-bottom:1px solid var(--border); margin-bottom:1rem;">
<button class="btn btn-sm tab-btn active" data-tab="config" onclick="switchTab('config')">Configuration</button>
<button class="btn btn-sm tab-btn" data-tab="dns" onclick="switchTab('dns')">DNS</button>
<button class="btn btn-sm tab-btn" data-tab="domains" onclick="switchTab('domains')">Domains</button>
<button class="btn btn-sm tab-btn" data-tab="vps" onclick="switchTab('vps')">VPS</button>
<button class="btn btn-sm tab-btn" data-tab="ssh" onclick="switchTab('ssh')">SSH Keys</button>
<button class="btn btn-sm tab-btn" data-tab="billing" onclick="switchTab('billing')">Billing</button>
</div>
<!-- ═══════ CONFIG TAB ═══════ -->
<div class="tab-panel" id="tab-config">
<h3>API Configuration</h3>
<div style="max-width:500px;">
<div class="form-group">
<label>API Key / Bearer Token</label>
<input type="password" id="cfg-api-key" placeholder="Enter API key">
</div>
<div class="form-group">
<label>API Secret (optional)</label>
<input type="password" id="cfg-api-secret" placeholder="Enter API secret if required">
</div>
<div style="display:flex; gap:.5rem;">
<button class="btn btn-primary" onclick="saveConfig()">Save Credentials</button>
<button class="btn" onclick="testConnection()">Test Connection</button>
</div>
<div id="config-status" style="margin-top:.75rem;"></div>
</div>
</div>
<!-- ═══════ DNS TAB ═══════ -->
<div class="tab-panel" id="tab-dns" style="display:none;">
<div style="display:flex; align-items:center; gap:.75rem; margin-bottom:1rem;">
<h3 style="margin:0;">DNS Records</h3>
<div class="form-group" style="margin:0;">
<select id="dns-domain-select" onchange="loadDNS()">
<option value="">-- select domain --</option>
</select>
</div>
<button class="btn btn-sm" onclick="loadDNS()">Refresh</button>
<button class="btn btn-sm" onclick="showAddDNS()" style="margin-left:auto;">Add Record</button>
</div>
<!-- Add/Edit DNS form (hidden by default) -->
<div id="dns-add-form" style="display:none; margin-bottom:1rem; padding:1rem; background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);">
<div style="display:flex; gap:.75rem; flex-wrap:wrap;">
<div class="form-group" style="margin:0; flex:0 0 120px;">
<label>Type</label>
<select id="dns-type">
<option>A</option><option>AAAA</option><option>CNAME</option>
<option>MX</option><option>TXT</option><option>NS</option>
<option>SRV</option><option>CAA</option>
</select>
</div>
<div class="form-group" style="margin:0; flex:1; min-width:140px;">
<label>Name</label>
<input type="text" id="dns-name" placeholder="@ or subdomain">
</div>
<div class="form-group" style="margin:0; flex:2; min-width:200px;">
<label>Content</label>
<input type="text" id="dns-content" placeholder="Value">
</div>
<div class="form-group" style="margin:0; flex:0 0 80px;">
<label>TTL</label>
<input type="text" id="dns-ttl" value="3600">
</div>
<div class="form-group" style="margin:0; flex:0 0 80px;">
<label>Priority</label>
<input type="text" id="dns-priority" value="0">
</div>
</div>
<div style="margin-top:.5rem; display:flex; gap:.5rem;">
<button class="btn btn-primary btn-sm" onclick="saveDNSRecord()">Save Record</button>
<button class="btn btn-sm" onclick="hideAddDNS()">Cancel</button>
</div>
</div>
<table class="data-table" id="dns-table">
<thead>
<tr><th>Type</th><th>Name</th><th>Content</th><th>TTL</th><th>Priority</th><th>Actions</th></tr>
</thead>
<tbody id="dns-body">
<tr><td colspan="6" style="color:var(--text-muted); text-align:center;">Select a domain to load DNS records</td></tr>
</tbody>
</table>
<div style="margin-top:.75rem;">
<button class="btn btn-sm" onclick="resetDNS()" style="color:var(--err);">Reset DNS to Defaults</button>
</div>
</div>
<!-- ═══════ DOMAINS TAB ═══════ -->
<div class="tab-panel" id="tab-domains" style="display:none;">
<h3>Registered Domains</h3>
<table class="data-table" id="domains-table">
<thead>
<tr><th>Domain</th><th>Status</th><th>Expires</th><th>Locked</th><th>Privacy</th><th>Actions</th></tr>
</thead>
<tbody id="domains-body">
<tr><td colspan="6" style="color:var(--text-muted); text-align:center;">Loading...</td></tr>
</tbody>
</table>
<h3>Check Availability</h3>
<div style="display:flex; gap:.5rem; max-width:600px; margin-bottom:.5rem;">
<div class="form-group" style="margin:0; flex:1;">
<input type="text" id="domain-check-input" placeholder="mydomain">
</div>
<div class="form-group" style="margin:0; flex:1;">
<input type="text" id="domain-check-tlds" placeholder="com,net,org,io" value="com,net,org,io,dev">
</div>
<button class="btn btn-primary" onclick="checkDomain()">Check</button>
</div>
<div id="domain-check-result" style="margin-bottom:1rem;"></div>
<h3>Purchase Domain</h3>
<div style="max-width:500px;">
<div class="form-group">
<label>Domain (full, e.g. example.com)</label>
<input type="text" id="purchase-domain" placeholder="example.com">
</div>
<div style="display:flex; gap:.75rem;">
<div class="form-group" style="flex:1;">
<label>Period (years)</label>
<select id="purchase-period">
<option value="1">1 year</option>
<option value="2">2 years</option>
<option value="3">3 years</option>
<option value="5">5 years</option>
</select>
</div>
<div class="form-group" style="flex:1;">
<label>Options</label>
<div style="display:flex; gap:1rem; padding-top:.35rem;">
<label style="font-size:.85rem;"><input type="checkbox" id="purchase-autorenew" checked> Auto-Renew</label>
<label style="font-size:.85rem;"><input type="checkbox" id="purchase-privacy" checked> Privacy</label>
</div>
</div>
</div>
<button class="btn btn-primary" onclick="purchaseDomain()">Purchase</button>
</div>
</div>
<!-- ═══════ VPS TAB ═══════ -->
<div class="tab-panel" id="tab-vps" style="display:none;">
<div style="display:flex; align-items:center; gap:.75rem; margin-bottom:1rem;">
<h3 style="margin:0;">Virtual Machines</h3>
<button class="btn btn-sm" onclick="loadVMs()">Refresh</button>
<button class="btn btn-sm btn-primary" onclick="showCreateVM()" style="margin-left:auto;">Create VM</button>
</div>
<table class="data-table" id="vms-table">
<thead>
<tr><th>Hostname</th><th>Status</th><th>Plan</th><th>Data Center</th><th>IP Address</th><th>CPU</th><th>RAM</th><th>Disk</th></tr>
</thead>
<tbody id="vms-body">
<tr><td colspan="8" style="color:var(--text-muted); text-align:center;">Loading...</td></tr>
</tbody>
</table>
<!-- Create VM form (hidden by default) -->
<div id="vm-create-form" style="display:none; margin-top:1rem; padding:1rem; background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);">
<h3>Create New VM</h3>
<div style="display:flex; gap:.75rem; flex-wrap:wrap;">
<div class="form-group" style="flex:1; min-width:200px;">
<label>Hostname</label>
<input type="text" id="vm-hostname" placeholder="my-server">
</div>
<div class="form-group" style="flex:1; min-width:200px;">
<label>Data Center</label>
<select id="vm-datacenter"><option value="">Loading...</option></select>
</div>
<div class="form-group" style="flex:1; min-width:200px;">
<label>Plan</label>
<input type="text" id="vm-plan" placeholder="Plan ID">
</div>
</div>
<div style="display:flex; gap:.75rem; flex-wrap:wrap;">
<div class="form-group" style="flex:1; min-width:200px;">
<label>OS Template</label>
<input type="text" id="vm-template" placeholder="ubuntu-22.04">
</div>
<div class="form-group" style="flex:1; min-width:200px;">
<label>Root Password</label>
<input type="password" id="vm-password" placeholder="Secure password">
</div>
<div class="form-group" style="flex:1; min-width:200px;">
<label>SSH Key (optional)</label>
<select id="vm-sshkey"><option value="">None</option></select>
</div>
</div>
<div style="display:flex; gap:.5rem;">
<button class="btn btn-primary" onclick="createVM()">Create VM</button>
<button class="btn" onclick="document.getElementById('vm-create-form').style.display='none'">Cancel</button>
</div>
</div>
</div>
<!-- ═══════ SSH KEYS TAB ═══════ -->
<div class="tab-panel" id="tab-ssh" style="display:none;">
<div style="display:flex; align-items:center; gap:.75rem; margin-bottom:1rem;">
<h3 style="margin:0;">SSH Keys</h3>
<button class="btn btn-sm" onclick="loadSSHKeys()">Refresh</button>
</div>
<table class="data-table" id="ssh-table">
<thead>
<tr><th>Name</th><th>Fingerprint</th><th>Actions</th></tr>
</thead>
<tbody id="ssh-body">
<tr><td colspan="3" style="color:var(--text-muted); text-align:center;">Loading...</td></tr>
</tbody>
</table>
<h3>Add SSH Key</h3>
<div style="max-width:600px;">
<div class="form-group">
<label>Key Name</label>
<input type="text" id="ssh-name" placeholder="my-laptop">
</div>
<div class="form-group">
<label>Public Key</label>
<textarea id="ssh-pubkey" rows="4" placeholder="ssh-rsa AAAA... user@host" style="width:100%;padding:.5rem .75rem;font-size:.9rem;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font-family:monospace;resize:vertical;"></textarea>
</div>
<button class="btn btn-primary" onclick="addSSHKey()">Add Key</button>
</div>
</div>
<!-- ═══════ BILLING TAB ═══════ -->
<div class="tab-panel" id="tab-billing" style="display:none;">
<h3>Subscriptions</h3>
<table class="data-table" id="subs-table">
<thead>
<tr><th>Name</th><th>Plan</th><th>Status</th><th>Price</th><th>Renews</th></tr>
</thead>
<tbody id="subs-body">
<tr><td colspan="5" style="color:var(--text-muted); text-align:center;">Loading...</td></tr>
</tbody>
</table>
<h3>Product Catalog</h3>
<table class="data-table" id="catalog-table">
<thead>
<tr><th>Name</th><th>Category</th><th>Price</th><th>Period</th><th>Description</th></tr>
</thead>
<tbody id="catalog-body">
<tr><td colspan="5" style="color:var(--text-muted); text-align:center;">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Toast notification -->
<div id="toast" style="display:none; position:fixed; bottom:1.5rem; right:1.5rem; padding:.65rem 1.25rem; border-radius:var(--radius); font-size:.875rem; z-index:100; max-width:400px;"></div>
<script>
(function() {
let _provider = '';
// ── Toast ────────────────────────────────────────────────────────────────
function toast(msg, ok) {
const el = document.getElementById('toast');
el.textContent = msg;
el.style.display = 'block';
el.style.background = ok ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)';
el.style.color = ok ? 'var(--ok)' : 'var(--err)';
el.style.border = '1px solid ' + (ok ? 'rgba(34,197,94,.3)' : 'rgba(239,68,68,.3)');
clearTimeout(el._t);
el._t = setTimeout(function() { el.style.display = 'none'; }, 4000);
}
window._toast = toast;
// ── API helpers ──────────────────────────────────────────────────────────
function api(method, path, body) {
const opts = {
method: method,
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }
};
if (body !== undefined) opts.body = JSON.stringify(body);
return fetch(path, opts).then(function(r) {
return r.json().then(function(d) {
if (!r.ok) throw new Error(d.error || 'Request failed');
return d;
});
});
}
// ── Provider selection ───────────────────────────────────────────────────
window.selectProvider = function(name) {
_provider = name;
// Highlight card
document.querySelectorAll('.provider-card').forEach(function(c) {
c.style.borderColor = c.dataset.provider === name ? 'var(--primary)' : 'var(--border)';
});
document.getElementById('hosting-tabs').style.display = 'block';
switchTab('config');
// Pre-load domain list for DNS tab
loadDomainOptions();
};
// ── Tab switching ────────────────────────────────────────────────────────
window.switchTab = function(tab) {
document.querySelectorAll('.tab-panel').forEach(function(p) { p.style.display = 'none'; });
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
var panel = document.getElementById('tab-' + tab);
if (panel) panel.style.display = 'block';
document.querySelectorAll('.tab-btn[data-tab="'+tab+'"]').forEach(function(b) { b.classList.add('active'); });
// Auto-load data when switching tabs
if (tab === 'dns') loadDomainOptions();
if (tab === 'domains') loadDomains();
if (tab === 'vps') { loadVMs(); loadDataCenters(); }
if (tab === 'ssh') loadSSHKeys();
if (tab === 'billing') { loadSubscriptions(); loadCatalog(); }
};
// ── Config tab ───────────────────────────────────────────────────────────
window.saveConfig = function() {
var key = document.getElementById('cfg-api-key').value.trim();
var secret = document.getElementById('cfg-api-secret').value.trim();
if (!key) { toast('API key is required', false); return; }
api('POST', '/hosting/' + _provider + '/config', { api_key: key, api_secret: secret })
.then(function(d) {
toast('Credentials saved' + (d.connected ? ' — connection OK' : ' — connection failed'), d.connected);
updateConfigStatus(d.connected);
})
.catch(function(e) { toast('Save failed: ' + e.message, false); });
};
window.testConnection = function() {
api('POST', '/hosting/' + _provider + '/test', {})
.then(function(d) {
if (d.connected) {
toast('Connection successful', true);
updateConfigStatus(true);
} else {
toast('Connection failed: ' + (d.error || 'unknown error'), false);
updateConfigStatus(false);
}
})
.catch(function(e) { toast('Test failed: ' + e.message, false); });
};
function updateConfigStatus(ok) {
var el = document.getElementById('config-status');
if (ok) {
el.innerHTML = '<span class="badge badge-ok">Connected</span>';
} else {
el.innerHTML = '<span class="badge badge-err">Disconnected</span>';
}
// Update provider card badge
var card = document.querySelector('.provider-card[data-provider="'+_provider+'"]');
if (card) {
var p = card.querySelector('p');
if (p) p.innerHTML = ok
? '<span class="badge badge-ok">Connected</span>'
: '<span class="badge badge-err">Disconnected</span>';
}
}
// ── DNS tab ──────────────────────────────────────────────────────────────
function loadDomainOptions() {
api('GET', '/hosting/' + _provider + '/domains')
.then(function(domains) {
var sel = document.getElementById('dns-domain-select');
var prev = sel.value;
sel.innerHTML = '<option value="">-- select domain --</option>';
if (domains && domains.length) {
domains.forEach(function(d) {
var opt = document.createElement('option');
opt.value = d.name;
opt.textContent = d.name;
sel.appendChild(opt);
});
}
if (prev) { sel.value = prev; }
})
.catch(function() {});
}
window.loadDNS = function() {
var domain = document.getElementById('dns-domain-select').value;
if (!domain) return;
var body = document.getElementById('dns-body');
body.innerHTML = '<tr><td colspan="6" style="text-align:center; color:var(--text-muted)">Loading...</td></tr>';
api('GET', '/hosting/' + _provider + '/dns/' + encodeURIComponent(domain))
.then(function(records) {
if (!records || !records.length) {
body.innerHTML = '<tr><td colspan="6" style="text-align:center; color:var(--text-muted)">No records found</td></tr>';
return;
}
body.innerHTML = '';
records.forEach(function(rec) {
var tr = document.createElement('tr');
tr.innerHTML =
'<td><span class="badge">' + esc(rec.type) + '</span></td>' +
'<td>' + esc(rec.name) + '</td>' +
'<td><code>' + esc(rec.content) + '</code></td>' +
'<td>' + rec.ttl + '</td>' +
'<td>' + (rec.priority || '') + '</td>' +
'<td><button class="btn btn-sm" onclick="deleteDNSRecord(\'' + esc(rec.name) + '\',\'' + esc(rec.type) + '\')" style="color:var(--err);">Delete</button></td>';
body.appendChild(tr);
});
})
.catch(function(e) {
body.innerHTML = '<tr><td colspan="6" style="color:var(--err);text-align:center">' + esc(e.message) + '</td></tr>';
});
};
window.showAddDNS = function() { document.getElementById('dns-add-form').style.display = 'block'; };
window.hideAddDNS = function() { document.getElementById('dns-add-form').style.display = 'none'; };
window.saveDNSRecord = function() {
var domain = document.getElementById('dns-domain-select').value;
if (!domain) { toast('Select a domain first', false); return; }
var record = {
type: document.getElementById('dns-type').value,
name: document.getElementById('dns-name').value.trim(),
content: document.getElementById('dns-content').value.trim(),
ttl: parseInt(document.getElementById('dns-ttl').value) || 3600,
priority: parseInt(document.getElementById('dns-priority').value) || 0
};
api('PUT', '/hosting/' + _provider + '/dns/' + encodeURIComponent(domain), { records: [record], overwrite: false })
.then(function() { toast('DNS record saved', true); hideAddDNS(); loadDNS(); })
.catch(function(e) { toast('Save failed: ' + e.message, false); });
};
window.deleteDNSRecord = function(name, type) {
var domain = document.getElementById('dns-domain-select').value;
if (!confirm('Delete ' + type + ' record for ' + name + '?')) return;
api('DELETE', '/hosting/' + _provider + '/dns/' + encodeURIComponent(domain), { name: name, type: type })
.then(function() { toast('Record deleted', true); loadDNS(); })
.catch(function(e) { toast('Delete failed: ' + e.message, false); });
};
window.resetDNS = function() {
var domain = document.getElementById('dns-domain-select').value;
if (!domain) { toast('Select a domain first', false); return; }
if (!confirm('Reset ALL DNS records for ' + domain + ' to defaults? This cannot be undone.')) return;
api('POST', '/hosting/' + _provider + '/dns/' + encodeURIComponent(domain) + '/reset', {})
.then(function() { toast('DNS reset to defaults', true); loadDNS(); })
.catch(function(e) { toast('Reset failed: ' + e.message, false); });
};
// ── Domains tab ──────────────────────────────────────────────────────────
window.loadDomains = function() {
var body = document.getElementById('domains-body');
body.innerHTML = '<tr><td colspan="6" style="text-align:center; color:var(--text-muted)">Loading...</td></tr>';
api('GET', '/hosting/' + _provider + '/domains')
.then(function(domains) {
if (!domains || !domains.length) {
body.innerHTML = '<tr><td colspan="6" style="text-align:center; color:var(--text-muted)">No domains found</td></tr>';
return;
}
body.innerHTML = '';
domains.forEach(function(d) {
var tr = document.createElement('tr');
var exp = d.expires_at ? new Date(d.expires_at).toLocaleDateString() : '-';
tr.innerHTML =
'<td><strong>' + esc(d.name) + '</strong></td>' +
'<td><span class="badge ' + (d.status === 'active' ? 'badge-ok' : 'badge-err') + '">' + esc(d.status) + '</span></td>' +
'<td>' + exp + '</td>' +
'<td>' +
'<button class="btn btn-sm" onclick="toggleLock(\'' + esc(d.name) + '\',true)">Lock</button> ' +
'<button class="btn btn-sm" onclick="toggleLock(\'' + esc(d.name) + '\',false)">Unlock</button>' +
'</td>' +
'<td>' +
'<button class="btn btn-sm" onclick="togglePrivacy(\'' + esc(d.name) + '\',true)">On</button> ' +
'<button class="btn btn-sm" onclick="togglePrivacy(\'' + esc(d.name) + '\',false)">Off</button>' +
'</td>' +
'<td><button class="btn btn-sm" onclick="editNameservers(\'' + esc(d.name) + '\')">NS</button></td>';
body.appendChild(tr);
});
})
.catch(function(e) {
body.innerHTML = '<tr><td colspan="6" style="color:var(--err);text-align:center">' + esc(e.message) + '</td></tr>';
});
};
window.toggleLock = function(domain, lock) {
api('PUT', '/hosting/' + _provider + '/domains/' + encodeURIComponent(domain) + '/lock', { locked: lock })
.then(function() { toast('Lock ' + (lock ? 'enabled' : 'disabled'), true); loadDomains(); })
.catch(function(e) { toast(e.message, false); });
};
window.togglePrivacy = function(domain, privacy) {
api('PUT', '/hosting/' + _provider + '/domains/' + encodeURIComponent(domain) + '/privacy', { privacy: privacy })
.then(function() { toast('Privacy ' + (privacy ? 'enabled' : 'disabled'), true); loadDomains(); })
.catch(function(e) { toast(e.message, false); });
};
window.editNameservers = function(domain) {
var ns = prompt('Enter nameservers (comma-separated):', '');
if (ns === null) return;
var list = ns.split(',').map(function(s) { return s.trim(); }).filter(Boolean);
if (!list.length) { toast('Provide at least one nameserver', false); return; }
api('PUT', '/hosting/' + _provider + '/domains/' + encodeURIComponent(domain) + '/nameservers', { nameservers: list })
.then(function() { toast('Nameservers updated', true); loadDomains(); })
.catch(function(e) { toast(e.message, false); });
};
window.checkDomain = function() {
var domain = document.getElementById('domain-check-input').value.trim();
if (!domain) return;
var tldsRaw = document.getElementById('domain-check-tlds').value.trim();
var tlds = tldsRaw.split(',').map(function(s) { return s.trim().replace(/^\./, ''); }).filter(Boolean);
if (!tlds.length) tlds = ['com','net','org','io','dev'];
var el = document.getElementById('domain-check-result');
el.innerHTML = '<span style="color:var(--text-muted)">Checking...</span>';
api('POST', '/hosting/' + _provider + '/domains/check', { domain: domain, tlds: tlds })
.then(function(results) {
el.innerHTML = '';
if (!results || !results.length) { el.innerHTML = '<span style="color:var(--text-muted)">No results</span>'; return; }
results.forEach(function(r) {
var badge = r.available
? '<span class="badge badge-ok">Available</span>'
: '<span class="badge badge-err">Taken</span>';
var price = r.available && r.price ? ' &mdash; ' + (r.currency||'USD') + ' ' + r.price.toFixed(2) : '';
el.innerHTML += '<p>' + esc(r.domain) + '.' + esc(r.tld) + ' ' + badge + price + '</p>';
});
})
.catch(function(e) { el.innerHTML = '<span style="color:var(--err)">' + esc(e.message) + '</span>'; });
};
window.purchaseDomain = function() {
var domain = document.getElementById('purchase-domain').value.trim();
if (!domain) { toast('Enter a domain', false); return; }
if (!confirm('Purchase ' + domain + '? This may charge your account.')) return;
api('POST', '/hosting/' + _provider + '/domains/purchase', {
domain: domain,
years: parseInt(document.getElementById('purchase-period').value),
auto_renew: document.getElementById('purchase-autorenew').checked,
privacy: document.getElementById('purchase-privacy').checked
})
.then(function() { toast('Domain purchased successfully', true); loadDomains(); })
.catch(function(e) { toast('Purchase failed: ' + e.message, false); });
};
// ── VPS tab ──────────────────────────────────────────────────────────────
window.loadVMs = function() {
var body = document.getElementById('vms-body');
body.innerHTML = '<tr><td colspan="8" style="text-align:center; color:var(--text-muted)">Loading...</td></tr>';
api('GET', '/hosting/' + _provider + '/vms')
.then(function(vms) {
if (!vms || !vms.length) {
body.innerHTML = '<tr><td colspan="8" style="text-align:center; color:var(--text-muted)">No VMs found</td></tr>';
return;
}
body.innerHTML = '';
vms.forEach(function(vm) {
var statusClass = vm.status === 'running' ? 'badge-ok' : (vm.status === 'stopped' ? 'badge-err' : '');
var ramGB = vm.ram_bytes ? (vm.ram_bytes / (1024*1024*1024)).toFixed(1) : '0';
var diskGB = vm.disk_bytes ? (vm.disk_bytes / (1024*1024*1024)).toFixed(0) : '0';
var tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.onclick = function() { viewVM(vm.id); };
tr.innerHTML =
'<td><strong>' + esc(vm.hostname || vm.id) + '</strong></td>' +
'<td><span class="badge ' + statusClass + '">' + esc(vm.status) + '</span></td>' +
'<td>' + esc(vm.plan) + '</td>' +
'<td>' + esc(vm.data_center) + '</td>' +
'<td><code>' + esc(vm.ip_address || '-') + '</code></td>' +
'<td>' + vm.cpus + '</td>' +
'<td>' + ramGB + ' GB</td>' +
'<td>' + diskGB + ' GB</td>';
body.appendChild(tr);
});
})
.catch(function(e) {
body.innerHTML = '<tr><td colspan="8" style="color:var(--err);text-align:center">' + esc(e.message) + '</td></tr>';
});
};
function viewVM(id) {
api('GET', '/hosting/' + _provider + '/vms/' + encodeURIComponent(id))
.then(function(vm) {
alert('VM: ' + vm.hostname + '\nIP: ' + (vm.ip_address || 'N/A') + '\nStatus: ' + vm.status + '\nOS: ' + (vm.os || 'N/A'));
})
.catch(function(e) { toast(e.message, false); });
}
function loadDataCenters() {
api('GET', '/hosting/' + _provider + '/datacenters')
.then(function(dcs) {
var sel = document.getElementById('vm-datacenter');
sel.innerHTML = '<option value="">-- select --</option>';
if (dcs && dcs.length) {
dcs.forEach(function(dc) {
var opt = document.createElement('option');
opt.value = dc.id;
opt.textContent = dc.name + ' (' + dc.location + ')';
sel.appendChild(opt);
});
}
})
.catch(function() {});
// Also load SSH keys for the VM form
api('GET', '/hosting/' + _provider + '/ssh-keys')
.then(function(keys) {
var sel = document.getElementById('vm-sshkey');
sel.innerHTML = '<option value="">None</option>';
if (keys && keys.length) {
keys.forEach(function(k) {
var opt = document.createElement('option');
opt.value = k.id;
opt.textContent = k.name;
sel.appendChild(opt);
});
}
})
.catch(function() {});
}
window.showCreateVM = function() {
document.getElementById('vm-create-form').style.display = 'block';
loadDataCenters();
};
window.createVM = function() {
var req = {
hostname: document.getElementById('vm-hostname').value.trim(),
plan: document.getElementById('vm-plan').value.trim(),
data_center_id: document.getElementById('vm-datacenter').value,
template: document.getElementById('vm-template').value.trim(),
password: document.getElementById('vm-password').value
};
var sshKey = document.getElementById('vm-sshkey').value;
if (sshKey) req.ssh_key_id = sshKey;
if (!req.plan || !req.data_center_id) {
toast('Plan and data center are required', false);
return;
}
api('POST', '/hosting/' + _provider + '/vms', req)
.then(function(vm) {
toast('VM created: ' + (vm.name || vm.id), true);
document.getElementById('vm-create-form').style.display = 'none';
loadVMs();
})
.catch(function(e) { toast('Create failed: ' + e.message, false); });
};
// ── SSH Keys tab ─────────────────────────────────────────────────────────
window.loadSSHKeys = function() {
var body = document.getElementById('ssh-body');
body.innerHTML = '<tr><td colspan="3" style="text-align:center; color:var(--text-muted)">Loading...</td></tr>';
api('GET', '/hosting/' + _provider + '/ssh-keys')
.then(function(keys) {
if (!keys || !keys.length) {
body.innerHTML = '<tr><td colspan="3" style="text-align:center; color:var(--text-muted)">No SSH keys found</td></tr>';
return;
}
body.innerHTML = '';
keys.forEach(function(k) {
var tr = document.createElement('tr');
tr.innerHTML =
'<td><strong>' + esc(k.name) + '</strong></td>' +
'<td><code>' + esc(k.fingerprint || '-') + '</code></td>' +
'<td><button class="btn btn-sm" onclick="deleteSSHKey(\'' + esc(k.id) + '\',\'' + esc(k.name) + '\')" style="color:var(--err);">Delete</button></td>';
body.appendChild(tr);
});
})
.catch(function(e) {
body.innerHTML = '<tr><td colspan="3" style="color:var(--err);text-align:center">' + esc(e.message) + '</td></tr>';
});
};
window.addSSHKey = function() {
var name = document.getElementById('ssh-name').value.trim();
var pubkey = document.getElementById('ssh-pubkey').value.trim();
if (!name || !pubkey) { toast('Name and public key are required', false); return; }
api('POST', '/hosting/' + _provider + '/ssh-keys', { name: name, public_key: pubkey })
.then(function() {
toast('SSH key added', true);
document.getElementById('ssh-name').value = '';
document.getElementById('ssh-pubkey').value = '';
loadSSHKeys();
})
.catch(function(e) { toast('Add failed: ' + e.message, false); });
};
window.deleteSSHKey = function(id, name) {
if (!confirm('Delete SSH key "' + name + '"?')) return;
api('DELETE', '/hosting/' + _provider + '/ssh-keys/' + encodeURIComponent(id))
.then(function() { toast('Key deleted', true); loadSSHKeys(); })
.catch(function(e) { toast('Delete failed: ' + e.message, false); });
};
// ── Billing tab ──────────────────────────────────────────────────────────
function loadSubscriptions() {
var body = document.getElementById('subs-body');
body.innerHTML = '<tr><td colspan="5" style="text-align:center; color:var(--text-muted)">Loading...</td></tr>';
api('GET', '/hosting/' + _provider + '/subscriptions')
.then(function(subs) {
if (!subs || !subs.length) {
body.innerHTML = '<tr><td colspan="5" style="text-align:center; color:var(--text-muted)">No subscriptions found</td></tr>';
return;
}
body.innerHTML = '';
subs.forEach(function(s) {
var statusClass = s.status === 'active' ? 'badge-ok' : (s.status === 'cancelled' ? 'badge-err' : '');
var renews = s.renews_at ? new Date(s.renews_at).toLocaleDateString() : '-';
var tr = document.createElement('tr');
tr.innerHTML =
'<td>' + esc(s.name) + '</td>' +
'<td>' + esc(s.plan) + '</td>' +
'<td><span class="badge ' + statusClass + '">' + esc(s.status) + '</span></td>' +
'<td>' + (s.currency || 'USD') + ' ' + (s.price || 0).toFixed(2) + '</td>' +
'<td>' + renews + '</td>';
body.appendChild(tr);
});
})
.catch(function(e) {
body.innerHTML = '<tr><td colspan="5" style="color:var(--err);text-align:center">' + esc(e.message) + '</td></tr>';
});
}
function loadCatalog() {
var body = document.getElementById('catalog-body');
body.innerHTML = '<tr><td colspan="5" style="text-align:center; color:var(--text-muted)">Loading...</td></tr>';
api('GET', '/hosting/' + _provider + '/catalog')
.then(function(items) {
if (!items || !items.length) {
body.innerHTML = '<tr><td colspan="5" style="text-align:center; color:var(--text-muted)">No catalog items</td></tr>';
return;
}
body.innerHTML = '';
items.forEach(function(item) {
var price = (item.price_cents / 100).toFixed(2);
var tr = document.createElement('tr');
tr.innerHTML =
'<td><strong>' + esc(item.name) + '</strong></td>' +
'<td><span class="badge">' + esc(item.category) + '</span></td>' +
'<td>' + (item.currency || 'USD') + ' ' + price + '</td>' +
'<td>' + esc(item.period || '-') + '</td>' +
'<td style="color:var(--text-muted); font-size:.85rem;">' + esc(item.description || '') + '</td>';
body.appendChild(tr);
});
})
.catch(function(e) {
body.innerHTML = '<tr><td colspan="5" style="color:var(--err);text-align:center">' + esc(e.message) + '</td></tr>';
});
}
// ── Utility ──────────────────────────────────────────────────────────────
function esc(s) {
if (!s) return '';
var d = document.createElement('div');
d.appendChild(document.createTextNode(s));
return d.innerHTML;
}
})();
</script>
{{end}}

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setec App Manager — Login</title>
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body class="login-page">
<div class="login-card">
<img src="/static/img/logo.svg" alt="Setec" style="width:80px;margin:0 auto 1rem;display:block">
<h1>SETEC</h1>
<p class="subtitle">App Manager</p>
<form id="loginForm" method="POST" action="/login">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
<div id="error" class="error-msg" style="display:none"></div>
</form>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const res = await fetch('/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value
})
});
if (res.ok) {
window.location.href = '/';
} else {
const err = document.getElementById('error');
err.textContent = 'Invalid credentials';
err.style.display = 'block';
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
{{define "content"}}
<h2>Logs</h2>
<div style="display:flex;gap:.5rem;margin-bottom:1rem;flex-wrap:wrap">
<select id="logSource" class="form-group" style="margin:0" onchange="loadLogs()">
{{range .Data.Sources}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
<select id="logLines" class="form-group" style="margin:0" onchange="loadLogs()">
<option value="50">50 lines</option>
<option value="100" selected>100 lines</option>
<option value="500">500 lines</option>
</select>
<button class="btn btn-sm" onclick="loadLogs()">Refresh</button>
<label style="display:flex;align-items:center;gap:.3rem">
<input type="checkbox" id="autoScroll" checked> Auto-scroll
</label>
</div>
<pre id="logOutput" style="background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:1rem;max-height:60vh;overflow:auto;font-size:.85rem;white-space:pre-wrap">{{.Data.Lines}}</pre>
<script>
async function loadLogs() {
const src = document.getElementById('logSource').value;
const n = document.getElementById('logLines').value;
try {
const r = await fetch('/api/logs?source=' + encodeURIComponent(src) + '&lines=' + n);
const d = await r.json();
const el = document.getElementById('logOutput');
el.textContent = d.lines || '';
if (document.getElementById('autoScroll').checked) {
el.scrollTop = el.scrollHeight;
}
} catch(e) { console.error('log fetch failed', e); }
}
</script>
{{end}}

View File

@@ -0,0 +1,50 @@
{{define "content"}}
<h2>System Monitor</h2>
<div class="stats-grid" id="monitorStats">
<div class="stat-card">
<h3>CPU</h3>
<div class="progress-bar">
<div class="progress-fill" id="cpuBar" style="width: {{printf "%.0f" .Data.CPU}}%"></div>
</div>
<p id="cpuText">{{printf "%.1f" .Data.CPU}}%</p>
</div>
<div class="stat-card">
<h3>Memory</h3>
<div class="progress-bar">
<div class="progress-fill" id="memBar" style="width: {{printf "%.0f" .Data.MemPercent}}%"></div>
</div>
<p id="memText">{{.Data.MemUsed}} / {{.Data.MemTotal}} ({{printf "%.1f" .Data.MemPercent}}%)</p>
</div>
<div class="stat-card">
<h3>Disk</h3>
<div class="progress-bar">
<div class="progress-fill" id="diskBar" style="width: {{printf "%.0f" .Data.DiskPercent}}%"></div>
</div>
<p id="diskText">{{.Data.DiskUsed}} / {{.Data.DiskTotal}} ({{printf "%.1f" .Data.DiskPercent}}%)</p>
</div>
<div class="stat-card">
<h3>Network</h3>
<p><strong>In:</strong> <span id="netIn">{{.Data.NetIn}}</span></p>
<p><strong>Out:</strong> <span id="netOut">{{.Data.NetOut}}</span></p>
</div>
</div>
<h3>Processes (Top 10)</h3>
<table class="data-table">
<thead>
<tr><th>PID</th><th>Name</th><th>CPU %</th><th>Mem %</th><th>User</th></tr>
</thead>
<tbody>
{{range .Data.Processes}}
<tr>
<td>{{.PID}}</td>
<td>{{.Name}}</td>
<td>{{printf "%.1f" .CPU}}</td>
<td>{{printf "%.1f" .Mem}}</td>
<td>{{.User}}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,58 @@
{{define "content"}}
<h2>Nginx</h2>
<div class="stats-grid">
<div class="stat-card">
<h3>Service Status</h3>
{{if .Data.Running}}
<span class="badge badge-ok">Running</span>
{{else}}
<span class="badge badge-err">Stopped</span>
{{end}}
<p><strong>Version:</strong> {{.Data.Version}}</p>
<p><strong>Config Test:</strong>
{{if .Data.ConfigOK}}
<span class="badge badge-ok">OK</span>
{{else}}
<span class="badge badge-err">Error</span>
{{end}}
</p>
</div>
<div class="stat-card">
<h3>Connections</h3>
<p><strong>Active:</strong> {{.Data.ActiveConns}}</p>
<p><strong>Requests:</strong> {{.Data.TotalRequests}}</p>
</div>
</div>
<h3>Actions</h3>
<div style="display:flex;gap:.5rem">
<form method="POST" action="/nginx/reload"><button class="btn btn-primary">Reload</button></form>
<form method="POST" action="/nginx/restart"><button class="btn">Restart</button></form>
<form method="POST" action="/nginx/test"><button class="btn">Test Config</button></form>
</div>
<h3>Virtual Hosts</h3>
<table class="data-table">
<thead>
<tr><th>Server Name</th><th>Listen</th><th>Enabled</th></tr>
</thead>
<tbody>
{{range .Data.VHosts}}
<tr>
<td>{{.ServerName}}</td>
<td>{{.Listen}}</td>
<td>
{{if .Enabled}}
<span class="badge badge-ok">Yes</span>
{{else}}
<span class="badge badge-err">No</span>
{{end}}
</td>
</tr>
{{else}}
<tr><td colspan="3">No virtual hosts configured.</td></tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,63 @@
{{define "content"}}
<div style="display:flex;justify-content:space-between;align-items:center">
<h2>{{.Data.Site.Domain}}</h2>
<div style="display:flex;gap:.5rem">
{{if .Data.Site.Running}}
<form method="POST" action="/sites/{{.Data.Site.ID}}/stop"><button class="btn btn-sm">Stop</button></form>
{{else}}
<form method="POST" action="/sites/{{.Data.Site.ID}}/start"><button class="btn btn-sm btn-primary">Start</button></form>
{{end}}
<form method="POST" action="/sites/{{.Data.Site.ID}}/deploy"><button class="btn btn-sm btn-primary">Deploy</button></form>
<a href="/sites" class="btn btn-sm">Back</a>
</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<h3>Info</h3>
<p><strong>Type:</strong> {{.Data.Site.Type}}</p>
<p><strong>Root:</strong> {{.Data.Site.Root}}</p>
<p><strong>Status:</strong>
{{if .Data.Site.Running}}
<span class="badge badge-ok">Running</span>
{{else}}
<span class="badge badge-err">Stopped</span>
{{end}}
</p>
</div>
<div class="stat-card">
<h3>SSL</h3>
{{if .Data.Site.SSL}}
<span class="badge badge-ok">Active</span>
<p><strong>Expires:</strong> {{.Data.Site.SSLExpiry}}</p>
{{else}}
<span class="badge badge-err">Not Configured</span>
{{end}}
</div>
</div>
<h3>Deployment History</h3>
<table class="data-table">
<thead>
<tr><th>Date</th><th>Commit</th><th>Status</th><th>Duration</th></tr>
</thead>
<tbody>
{{range .Data.Deployments}}
<tr>
<td>{{.Date}}</td>
<td><code>{{.Commit}}</code></td>
<td>
{{if eq .Status "ok"}}
<span class="badge badge-ok">OK</span>
{{else}}
<span class="badge badge-err">{{.Status}}</span>
{{end}}
</td>
<td>{{.Duration}}</td>
</tr>
{{else}}
<tr><td colspan="4">No deployments yet.</td></tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,31 @@
{{define "content"}}
<h2>New Site</h2>
<form id="siteForm" method="POST" action="/sites">
<div class="form-group">
<label for="domain">Domain</label>
<input type="text" id="domain" name="domain" placeholder="example.com" required>
</div>
<div class="form-group">
<label for="type">Site Type</label>
<select id="type" name="type">
<option value="static">Static</option>
<option value="proxy">Reverse Proxy</option>
<option value="php">PHP</option>
<option value="node">Node.js</option>
</select>
</div>
<div class="form-group">
<label for="root">Document Root / Upstream</label>
<input type="text" id="root" name="root" placeholder="/var/www/example.com">
</div>
<div class="form-group">
<label>
<input type="checkbox" name="ssl" value="1"> Enable SSL (Let's Encrypt)
</label>
</div>
<div style="display:flex;gap:.5rem">
<button type="submit" class="btn btn-primary">Create Site</button>
<a href="/sites" class="btn">Cancel</a>
</div>
</form>
{{end}}

View File

@@ -0,0 +1,44 @@
{{define "content"}}
<div style="display:flex;justify-content:space-between;align-items:center">
<h2>Sites</h2>
<a href="/sites/new" class="btn btn-primary">+ New Site</a>
</div>
<table class="data-table">
<thead>
<tr>
<th>Domain</th>
<th>Type</th>
<th>SSL</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Data.Sites}}
<tr>
<td><a href="/sites/{{.ID}}">{{.Domain}}</a></td>
<td>{{.Type}}</td>
<td>
{{if .SSL}}
<span class="badge badge-ok">Active</span>
{{else}}
<span class="badge badge-err">None</span>
{{end}}
</td>
<td>
{{if .Running}}
<span class="badge badge-ok">Running</span>
{{else}}
<span class="badge badge-err">Stopped</span>
{{end}}
</td>
<td>
<a href="/sites/{{.ID}}" class="btn btn-sm">View</a>
</td>
</tr>
{{else}}
<tr><td colspan="5">No sites configured.</td></tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,31 @@
{{define "content"}}
<h2>SSL / TLS Certificates</h2>
<table class="data-table">
<thead>
<tr><th>Domain</th><th>Issuer</th><th>Expires</th><th>Status</th><th>Actions</th></tr>
</thead>
<tbody>
{{range .Data.Certs}}
<tr>
<td>{{.Domain}}</td>
<td>{{.Issuer}}</td>
<td>{{.Expiry}}</td>
<td>
{{if .Valid}}
<span class="badge badge-ok">Valid</span>
{{else}}
<span class="badge badge-err">Expired</span>
{{end}}
</td>
<td>
<form method="POST" action="/ssl/renew/{{.Domain}}" style="display:inline">
<button class="btn btn-sm">Renew</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="5">No certificates found.</td></tr>
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -0,0 +1,56 @@
{{define "content"}}
<div style="display:flex;justify-content:space-between;align-items:center">
<h2>Users</h2>
<button class="btn btn-primary" onclick="document.getElementById('newUserForm').style.display='block'">+ Add User</button>
</div>
<div id="newUserForm" style="display:none;margin-bottom:1.5rem">
<div class="stat-card">
<h3>New User</h3>
<form method="POST" action="/users">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="role">Role</label>
<select id="role" name="role">
<option value="admin">Admin</option>
<option value="viewer">Viewer</option>
</select>
</div>
<div style="display:flex;gap:.5rem">
<button type="submit" class="btn btn-primary btn-sm">Create</button>
<button type="button" class="btn btn-sm" onclick="this.closest('#newUserForm').style.display='none'">Cancel</button>
</div>
</form>
</div>
</div>
<table class="data-table">
<thead>
<tr><th>Username</th><th>Role</th><th>Created</th><th>Actions</th></tr>
</thead>
<tbody>
{{range .Data.Users}}
<tr>
<td>{{.Username}}</td>
<td><span class="badge badge-ok">{{.Role}}</span></td>
<td>{{.Created}}</td>
<td>
<form method="POST" action="/users/{{.Username}}/delete" style="display:inline"
onsubmit="return confirm('Delete user {{.Username}}?')">
<button class="btn btn-sm">Delete</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="4">No users.</td></tr>
{{end}}
</tbody>
</table>
{{end}}