No One Can Stop Me Now
This commit is contained in:
9
services/setec-manager/web/embed.go
Normal file
9
services/setec-manager/web/embed.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package web
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates/*.html
|
||||
var TemplateFS embed.FS
|
||||
|
||||
//go:embed static
|
||||
var StaticFS embed.FS
|
||||
370
services/setec-manager/web/static/css/style.css
Normal file
370
services/setec-manager/web/static/css/style.css
Normal 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; }
|
||||
}
|
||||
13
services/setec-manager/web/static/img/favicon.svg
Normal file
13
services/setec-manager/web/static/img/favicon.svg
Normal 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 |
63
services/setec-manager/web/static/img/icons.svg
Normal file
63
services/setec-manager/web/static/img/icons.svg
Normal 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 |
19
services/setec-manager/web/static/img/logo-full.svg
Normal file
19
services/setec-manager/web/static/img/logo-full.svg
Normal 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 |
28
services/setec-manager/web/static/img/logo.svg
Normal file
28
services/setec-manager/web/static/img/logo.svg
Normal 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 |
18
services/setec-manager/web/static/img/sidebar-logo.svg
Normal file
18
services/setec-manager/web/static/img/sidebar-logo.svg
Normal 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 |
109
services/setec-manager/web/static/js/app.js
Normal file
109
services/setec-manager/web/static/js/app.js
Normal 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);
|
||||
})();
|
||||
50
services/setec-manager/web/templates/autarch.html
Normal file
50
services/setec-manager/web/templates/autarch.html
Normal 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}}
|
||||
31
services/setec-manager/web/templates/backups.html
Normal file
31
services/setec-manager/web/templates/backups.html
Normal 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}}
|
||||
39
services/setec-manager/web/templates/base.html
Normal file
39
services/setec-manager/web/templates/base.html
Normal 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>
|
||||
53
services/setec-manager/web/templates/dashboard.html
Normal file
53
services/setec-manager/web/templates/dashboard.html
Normal 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}}
|
||||
40
services/setec-manager/web/templates/firewall.html
Normal file
40
services/setec-manager/web/templates/firewall.html
Normal 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}}
|
||||
40
services/setec-manager/web/templates/float.html
Normal file
40
services/setec-manager/web/templates/float.html
Normal 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}}
|
||||
783
services/setec-manager/web/templates/hosting.html
Normal file
783
services/setec-manager/web/templates/hosting.html
Normal 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 ? ' — ' + (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}}
|
||||
49
services/setec-manager/web/templates/login.html
Normal file
49
services/setec-manager/web/templates/login.html
Normal 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>
|
||||
38
services/setec-manager/web/templates/logs.html
Normal file
38
services/setec-manager/web/templates/logs.html
Normal 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}}
|
||||
50
services/setec-manager/web/templates/monitor.html
Normal file
50
services/setec-manager/web/templates/monitor.html
Normal 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}}
|
||||
58
services/setec-manager/web/templates/nginx.html
Normal file
58
services/setec-manager/web/templates/nginx.html
Normal 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}}
|
||||
63
services/setec-manager/web/templates/site_detail.html
Normal file
63
services/setec-manager/web/templates/site_detail.html
Normal 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}}
|
||||
31
services/setec-manager/web/templates/site_new.html
Normal file
31
services/setec-manager/web/templates/site_new.html
Normal 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}}
|
||||
44
services/setec-manager/web/templates/sites.html
Normal file
44
services/setec-manager/web/templates/sites.html
Normal 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}}
|
||||
31
services/setec-manager/web/templates/ssl.html
Normal file
31
services/setec-manager/web/templates/ssl.html
Normal 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}}
|
||||
56
services/setec-manager/web/templates/users.html
Normal file
56
services/setec-manager/web/templates/users.html
Normal 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}}
|
||||
Reference in New Issue
Block a user