Files
setec_cdm/setec-web/templates/docker.html

335 lines
14 KiB
HTML
Raw Normal View History

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