Initial commit - podman-mvp net na toevoegen cpu en mem kolommen

This commit is contained in:
kodi
2026-02-18 08:17:27 +01:00
commit 62e195c59e
56 changed files with 22164 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Podman MVP | Dashboard V4.5</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/theme/material-darker.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/mode/yaml/yaml.min.js"></script>
<style>
:root {
--sidebar-width: 250px;
--portainer-blue: #00b3e6;
--bg-dark: #2c3e50;
--bg-light: #f4f6f9;
--border-color: #dce1e5;
}
body { font-family: 'Segoe UI', sans-serif; margin: 0; display: flex; background: var(--bg-light); height: 100vh; overflow: hidden; }
.sidebar { width: var(--sidebar-width); background: var(--bg-dark); color: white; display: flex; flex-direction: column; }
.sidebar-header { padding: 15px 20px; background: #1a252f; font-weight: bold; display: flex; align-items: center; gap: 10px; }
.sidebar-header img { width: 30px; }
.nav-item { padding: 15px 20px; cursor: pointer; border-left: 4px solid transparent; transition: 0.2s; }
.nav-item:hover { background: #34495e; }
.nav-item.active { background: #34495e; border-left-color: var(--portainer-blue); color: var(--portainer-blue); }
.main-content { flex: 1; display: flex; flex-direction: column; overflow-y: auto; }
.topbar { background: white; padding: 15px 30px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
/* Navigatie logica */
.view-container { padding: 25px; display: none; }
.view-container.active { display: block; }
.card { background: white; border-radius: 4px; border: 1px solid var(--border-color); margin-bottom: 20px; }
.card-header { padding: 12px 20px; border-bottom: 1px solid var(--border-color); font-weight: bold; background: #fafafa; display: flex; justify-content: space-between; align-items: center; }
.card-body { padding: 15px; }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; padding: 12px; border-bottom: 2px solid var(--border-color); font-size: 0.8rem; text-transform: uppercase; color: #7f8c8d; }
td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 0.9rem; }
.badge { padding: 4px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: bold; }
.badge-running { background: #d4edda; color: #155724; }
.badge-stopped { background: #f8d7da; color: #721c24; }
.btn { padding: 5px 10px; border-radius: 3px; border: 1px solid #ccc; cursor: pointer; font-size: 0.8rem; background: white; transition: 0.2s; }
.btn:hover { background: #f8f9fa; border-color: #bbb; }
.btn-primary { background: var(--portainer-blue); color: white; border: none; padding: 8px 15px; font-weight: bold; }
/* Modal Styles */
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; }
.modal-content { background: white; width: 80%; max-height: 80%; border-radius: 8px; display: flex; flex-direction: column; position: relative; }
.modal-header { padding: 15px 20px; border-bottom: 1px solid #ddd; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
.log-container { background: #1e1e1e; color: #d4d4d4; padding: 15px; font-family: 'Consolas', monospace; font-size: 0.85rem; border-radius: 4px; white-space: pre-wrap; }
.inspect-container { background: #f8f9fa; padding: 15px; font-family: monospace; font-size: 0.85rem; border: 1px solid #ddd; border-radius: 4px; }
/* Studio Layout */
.studio-layout { display: flex; gap: 20px; height: calc(100vh - 280px); }
.file-tree { width: 280px; border-right: 1px solid #eee; overflow-y: auto; padding-right: 10px; }
.editor-container { flex: 1; display: flex; flex-direction: column; }
.CodeMirror { flex: 1; border: 1px solid #ddd; font-size: 13px; }
.tree-item { display: flex; justify-content: space-between; align-items: center; padding: 5px 8px; border-radius: 4px; margin-bottom: 2px; }
.tree-item:hover { background: #f0f2f5; }
.tree-folder { font-weight: bold; color: #2c3e50; background: #eaeff2; margin-top: 10px; }
.tree-file { cursor: pointer; color: #2980b9; }
.icon-btn { cursor: pointer; opacity: 0.6; transition: 0.2s; font-style: normal; }
.icon-btn:hover { opacity: 1; transform: scale(1.1); }
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<img src="https://raw.githubusercontent.com/containers/common/main/logos/podman-logo.svg" alt="Podman">
<span>Podman MVP</span>
</div>
<div class="nav-item active" id="nav-dashboard" onclick="showView('dashboard')">Dashboard</div>
<div class="nav-item" id="nav-containers" onclick="showView('containers')">Containers</div>
<div class="nav-item" id="nav-pods" onclick="showView('pods')">Pods</div>
<div class="nav-item" id="nav-workloads" onclick="showView('workloads')">Workload Studio</div>
</div>
<div class="main-content">
<div class="topbar">
<span id="view-title" style="font-weight: bold; color: #34495e;">DASHBOARD</span>
<button class="btn" onclick="refresh()">🔄 Ververs Data</button>
</div>
<div id="dashboard-view" class="view-container active">
<div class="card">
<div class="card-header">Beheerde Workloads</div>
<div class="card-body">
<table>
<thead><tr><th>Naam</th><th>Status</th><th>IP</th><th>Acties</th></tr></thead>
<tbody id="dashboard-list"></tbody>
</table>
</div>
</div>
</div>
<div id="containers-view" class="view-container">
<div class="card">
<div class="card-header">Live Containers</div>
<div class="card-body">
<table>
<thead>
<tr>
<th>Naam</th>
<th>Status</th>
<th>Pod</th>
<th>IP</th>
<th>Poorten</th>
<th>Quick Actions</th>
</tr>
</thead>
<tbody id="live-container-list"></tbody>
</table>
</div>
</div>
</div>
<div id="pods-view" class="view-container">
<div class="card">
<div class="card-header">Active Pods</div>
<div class="card-body">
<table>
<thead><tr><th>Pod Naam</th><th>Status</th><th>Containers</th></tr></thead>
<tbody id="pod-list"></tbody>
</table>
</div>
</div>
</div>
<div id="workloads-view" class="view-container">
<div class="card">
<div class="card-header"><span>Bestandsbeheer</span><button class="btn" onclick="createNewMap()">📁 Nieuwe Hoofdmap</button></div>
<div class="card-body studio-layout">
<div id="file-tree" class="file-tree">Laden...</div>
<div class="editor-container">
<div id="editor-header" style="padding: 10px; background: #34495e; color: white; font-family: monospace; font-size: 0.8rem; border-radius: 4px 4px 0 0;">Selecteer een bestand...</div>
<textarea id="edit-content"></textarea>
<div style="margin-top:15px; display: flex; justify-content: flex-end;"><button class="btn btn-primary" onclick="saveFile()">💾 Bestand Opslaan</button></div>
</div>
</div>
</div>
</div>
</div>
<div id="modal" class="modal-overlay" onclick="closeModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<span id="modal-title">Info</span>
<button class="btn" onclick="closeModal()">✖ Sluiten</button>
</div>
<div class="modal-body" id="modal-body"></div>
</div>
</div>
<script>
let editor;
let currentPath = '';
window.onload = () => {
editor = CodeMirror.fromTextArea(document.getElementById("edit-content"), {
mode: "yaml", theme: "material-darker", lineNumbers: true, tabSize: 2, indentWithTabs: false
});
refresh();
};
async function api(path, method = 'GET', body = null) {
const options = { method, headers: body ? {'Content-Type': 'application/json'} : {} };
if (body) options.body = JSON.stringify(body);
const res = await fetch('/api' + path, options);
return await res.json();
}
// --- NAVIGATIE HERSTELD ---
function showView(view) {
document.querySelectorAll('.view-container').forEach(v => {
v.style.display = 'none';
v.classList.remove('active');
});
const selected = document.getElementById(view + '-view');
if (selected) {
selected.style.display = 'block';
selected.classList.add('active');
}
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const nav = document.getElementById('nav-' + view);
if (nav) nav.classList.add('active');
document.getElementById('view-title').innerText = view.toUpperCase();
if(view === 'workloads') fetchTree();
}
async function fetchDashboard() {
const data = await api('/dashboard');
document.getElementById('dashboard-list').innerHTML = data.map(w => `
<tr><td><strong>${w.name}</strong></td>
<td><span class="badge ${w.status === 'running' ? 'badge-running' : 'badge-stopped'}">${w.status}</span></td>
<td>${w.ip || '-'}</td>
<td><button class="btn" onclick="runAction('start', '${w.name}')">▶</button> <button class="btn" onclick="runAction('stop', '${w.name}')">⏹</button></td></tr>`).join('');
}
// --- CONTAINERS MET POORTEN EN ACTIONS ---
async function fetchContainers() {
const data = await api('/containers');
const list = document.getElementById('live-container-list');
if (!data) return;
list.innerHTML = '';
const realContainers = data.filter(c => !c.IsInfra);
realContainers.forEach(c => {
const name = c.Names[0].replace('/', '');
const ip = c.Networks ? Object.values(c.Networks)[0]?.IPAddress || '-' : '-';
let portMapping = '-';
if (c.Ports && c.Ports.length > 0) {
portMapping = c.Ports.map(p => {
const host = p.host_port;
const container = p.container_port;
return host ? `${host}:${container}` : `${container}/${p.protocol}`;
}).join(', ');
}
list.innerHTML += `
<tr>
<td><strong>${name}</strong></td>
<td><span class="badge ${c.State==='running'?'badge-running':'badge-stopped'}">${c.State}</span></td>
<td>${c.PodName || '-'}</td>
<td>${ip}</td>
<td style="font-family:monospace; font-size:0.8rem;">${portMapping}</td>
<td>
<button class="btn" title="Logs" onclick="showLogs('${name}')">📜</button>
<button class="btn" title="Inspect" onclick="showInspect('${name}')">🔍</button>
</td>
</tr>`;
});
}
async function showLogs(name) {
document.getElementById('modal-title').innerText = `Logs: ${name}`;
document.getElementById('modal-body').innerHTML = '<div class="log-container">Laden...</div>';
document.getElementById('modal').style.display = 'flex';
const data = await api(`/containers/logs/${name}`);
const cleanLogs = data.logs.replace(/[\u0000-\u0008]/g, "");
document.getElementById('modal-body').innerHTML = `<div class="log-container">${cleanLogs || 'Geen logs gevonden.'}</div>`;
}
async function showInspect(name) {
document.getElementById('modal-title').innerText = `Inspect: ${name}`;
document.getElementById('modal-body').innerHTML = '<div class="inspect-container">Laden...</div>';
document.getElementById('modal').style.display = 'flex';
const data = await api(`/containers/inspect/${name}`);
document.getElementById('modal-body').innerHTML = `<pre class="inspect-container">${JSON.stringify(data, null, 2)}</pre>`;
}
function closeModal() { document.getElementById('modal').style.display = 'none'; }
async function fetchTree() {
const data = await api('/files/tree');
const tree = document.getElementById('file-tree');
tree.innerHTML = '';
data.forEach(folder => {
const folderName = folder.path || "root";
tree.innerHTML += `<div class="tree-item tree-folder"><span>📂 ${folderName}</span><div style="display:flex;gap:8px;"><i class="icon-btn" onclick="createFileIn('${folder.path}')"></i><i class="icon-btn" style="color:red" onclick="deleteItem('${folder.path}')">🗑</i></div></div>`;
folder.files.forEach(f => {
const full = folder.path ? `${folder.path}/${f}` : f;
tree.innerHTML += `<div class="tree-item ml-4"><span class="tree-file" onclick="loadFile('${full}')">📄 ${f}</span><i class="icon-btn" style="color:red" onclick="deleteItem('${full}')">🗑</i></div>`;
});
});
}
async function loadFile(path) {
currentPath = path;
const data = await api(`/files/read?path=${encodeURIComponent(path)}`);
editor.setValue(data.content);
document.getElementById('editor-header').innerText = `BESTAND: ${path}`;
}
async function saveFile() {
if(!currentPath) return alert("Selecteer bestand!");
await api(`/files/save?path=${encodeURIComponent(currentPath)}`, 'POST', {content: editor.getValue()});
alert("Opgeslagen!");
fetchTree();
}
async function createFileIn(folderPath) {
const name = prompt(`Naam in ${folderPath || 'root'}:`, "service.yaml");
if(name) {
const full = folderPath ? `${folderPath}/${name}` : name;
const template = "version: '1.0'\nservices:\n ";
await api(`/files/save?path=${encodeURIComponent(full)}`, 'POST', {content: template});
fetchTree();
}
}
async function createNewMap() {
const path = prompt("Nieuwe hoofdmap:");
if(path) { await api(`/files/mkdir?path=${encodeURIComponent(path)}`, 'POST'); fetchTree(); }
}
async function deleteItem(path) {
if(!path || !confirm(`Verwijderen: ${path}?`)) return;
const res = await api(`/files/delete?path=${encodeURIComponent(path)}`, 'DELETE');
if(res.status === 'deleted') fetchTree();
else if(res.detail) alert(res.detail);
}
async function fetchPods() {
const data = await api('/pods');
document.getElementById('pod-list').innerHTML = data.map(p => `<tr><td>${p.Name}</td><td><span class="badge ${p.Status==='Running'?'badge-running':'badge-stopped'}">${p.Status}</span></td><td>${p.Containers.length}</td></tr>`).join('');
}
async function runAction(action, name) {
await api(`/actions/${action}/${name}`, 'POST');
setTimeout(refresh, 1200);
}
function refresh() {
fetchDashboard();
fetchContainers();
fetchPods();
if(document.getElementById('workloads-view').classList.contains('active')) fetchTree();
}
</script>
</body>
</html>