changes
This commit is contained in:
140
README.md
140
README.md
@@ -1,20 +1,42 @@
|
|||||||
# 1) katalog + venv
|
# PVE HA Web Panel — Deployment Guide
|
||||||
sudo mkdir -p /opt/pve-ha-web
|
|
||||||
sudo chown -R $USER:$USER /opt/pve-ha-web
|
This guide explains how to deploy **PVE HA Web Panel** on a Proxmox host or any Debian/Ubuntu-like system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Create directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/pve-ha-web
|
||||||
cd /opt/pve-ha-web
|
cd /opt/pve-ha-web
|
||||||
|
```
|
||||||
|
|
||||||
# 2) pliki aplikacji (app.py, templates/, static/, requirements.txt) — skopiuj tu
|
## 2) Get the application
|
||||||
# …gdy już je masz w katalogu…
|
|
||||||
|
|
||||||
# 3) virtualenv + deps
|
Clone the repository (includes `app.py`, `templates/`, `static/`, `requirements.txt`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.linuxiarz.pl/gru/pve-ha-web.git .
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** The trailing `.` clones directly into `/opt/pve-ha-web` instead of a nested subfolder.
|
||||||
|
|
||||||
|
## 3) Python virtualenv & dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
deactivate
|
deactivate
|
||||||
|
```
|
||||||
|
|
||||||
# 4) systemd unit
|
## 4) systemd unit
|
||||||
sudo tee /etc/systemd/system/pve-ha-web.service >/dev/null <<'UNIT'
|
|
||||||
|
Create a service unit file for Gunicorn:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tee /etc/systemd/system/pve-ha-web.service >/dev/null <<'UNIT'
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=PVE HA Web Panel
|
Description=PVE HA Web Panel
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -23,7 +45,7 @@ After=network.target
|
|||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/opt/pve-ha-web
|
WorkingDirectory=/opt/pve-ha-web
|
||||||
Environment="PYTHONUNBUFFERED=1"
|
Environment="PYTHONUNBUFFERED=1"
|
||||||
ExecStart=/opt/pve-ha-web/venv/bin/gunicorn -w 2 -b 0.0.0.0:8000 app:app
|
ExecStart=/opt/pve-ha-web/venv/bin/gunicorn -w 2 -b 0.0.0.0:8007 app:app
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
User=root
|
User=root
|
||||||
@@ -32,11 +54,101 @@ Group=root
|
|||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
UNIT
|
UNIT
|
||||||
|
```
|
||||||
|
|
||||||
# 5) start + autostart
|
> **Port:** The app listens on `8007` by default. Adjust as needed.
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now pve-ha-web
|
|
||||||
|
|
||||||
# 6) sprawdzenie
|
## 5) Enable & start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now pve-ha-web
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6) Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
systemctl status pve-ha-web
|
systemctl status pve-ha-web
|
||||||
ss -ltnp | grep :8000
|
ss -ltnp | grep :8007
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the app in the browser: `http://<server-ip>:8007`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optional: Reverse proxy (Nginx)
|
||||||
|
|
||||||
|
If you want to expose it under standard HTTPS with a domain:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ha.example.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name ha.example.com;
|
||||||
|
|
||||||
|
# TLS certs (replace with your paths / cert manager)
|
||||||
|
ssl_certificate /etc/letsencrypt/live/ha.example.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/ha.example.com/privkey.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8007;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reload Nginx: `sudo systemctl reload nginx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Management
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u pve-ha-web -e -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart / stop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart pve-ha-web
|
||||||
|
sudo systemctl stop pve-ha-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update to latest version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/pve-ha-web
|
||||||
|
git pull --rebase
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
deactivate
|
||||||
|
sudo systemctl restart pve-ha-web
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable --now pve-ha-web
|
||||||
|
sudo rm -f /etc/systemd/system/pve-ha-web.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo rm -rf /opt/pve-ha-web
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
- Author: **linuxiarz.pl**
|
||||||
|
- Source: <https://gitea.linuxiarz.pl/gru/pve-ha-web>
|
Binary file not shown.
6
app.py
6
app.py
@@ -291,10 +291,6 @@ def node_ha_services(node: str) -> Dict[str, str]:
|
|||||||
return {"crm_state": one("pve-ha-crm"), "lrm_state": one("pve-ha-lrm")}
|
return {"crm_state": one("pve-ha-crm"), "lrm_state": one("pve-ha-lrm")}
|
||||||
|
|
||||||
def units_for_node(node: str) -> Dict[str, str]:
|
def units_for_node(node: str) -> Dict[str, str]:
|
||||||
"""
|
|
||||||
Preferuj /nodes/<node>/services. Normalizuj nazwy (bez .service)
|
|
||||||
i mapuj różne pola na 'active'/'inactive'.
|
|
||||||
"""
|
|
||||||
wanted = {"watchdog-mux", "pve-ha-crm", "pve-ha-lrm"}
|
wanted = {"watchdog-mux", "pve-ha-crm", "pve-ha-lrm"}
|
||||||
svc = get_json(["pvesh", "get", f"/nodes/{node}/services"]) or []
|
svc = get_json(["pvesh", "get", f"/nodes/{node}/services"]) or []
|
||||||
states: Dict[str, str] = {}
|
states: Dict[str, str] = {}
|
||||||
@@ -438,7 +434,7 @@ def api_disable():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
p = argparse.ArgumentParser()
|
p = argparse.ArgumentParser()
|
||||||
p.add_argument("--bind", default="127.0.0.1:8088", help="addr:port")
|
p.add_argument("--bind", default="127.0.0.1:8007", help="addr:port")
|
||||||
p.add_argument("--node", default=DEFAULT_NODE, help="default node")
|
p.add_argument("--node", default=DEFAULT_NODE, help="default node")
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
DEFAULT_NODE = args.node
|
DEFAULT_NODE = args.node
|
||||||
|
180
static/main.js
180
static/main.js
@@ -5,9 +5,11 @@ function ensureArr(a){ return Array.isArray(a)?a:[]; }
|
|||||||
function pct(p) { if (p == null) return '—'; return (p * 100).toFixed(1) + '%'; }
|
function pct(p) { if (p == null) return '—'; return (p * 100).toFixed(1) + '%'; }
|
||||||
function humanBytes(n) { if (n == null) return '—'; const u = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; let i = 0, x = +n; while (x >= 1024 && i < u.length - 1) { x /= 1024; i++; } return x.toFixed(1) + ' ' + u[i]; }
|
function humanBytes(n) { if (n == null) return '—'; const u = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; let i = 0, x = +n; while (x >= 1024 && i < u.length - 1) { x /= 1024; i++; } return x.toFixed(1) + ' ' + u[i]; }
|
||||||
function badge(txt, kind) {
|
function badge(txt, kind) {
|
||||||
const cls={ok:'bg-success-subtle text-success-emphasis',warn:'bg-warning-subtle text-warning-emphasis',
|
const cls = {
|
||||||
|
ok: 'bg-success-subtle text-success-emphasis', warn: 'bg-warning-subtle text-warning-emphasis',
|
||||||
err: 'bg-danger-subtle text-danger-emphasis', info: 'bg-info-subtle text-info-emphasis',
|
err: 'bg-danger-subtle text-danger-emphasis', info: 'bg-info-subtle text-info-emphasis',
|
||||||
dark:'bg-secondary-subtle text-secondary-emphasis'}[kind||'dark'];
|
dark: 'bg-secondary-subtle text-secondary-emphasis'
|
||||||
|
}[kind || 'dark'];
|
||||||
return `<span class="badge rounded-pill ${cls}">${safe(txt)}</span>`;
|
return `<span class="badge rounded-pill ${cls}">${safe(txt)}</span>`;
|
||||||
}
|
}
|
||||||
function rowHTML(cols, attrs = '') { return `<tr ${attrs}>${cols.map(c => `<td>${c ?? '—'}</td>`).join('')}</tr>`; }
|
function rowHTML(cols, attrs = '') { return `<tr ${attrs}>${cols.map(c => `<td>${c ?? '—'}</td>`).join('')}</tr>`; }
|
||||||
@@ -49,13 +51,15 @@ function kvGrid(obj, keys, titleMap={}){
|
|||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
// prefer first non-empty
|
||||||
|
function pick(...vals) { for (const v of vals) { if (v !== undefined && v !== null && v !== '') return v; } return ''; }
|
||||||
|
|
||||||
// ------ DOM refs ------
|
// ------ DOM refs ------
|
||||||
const nodeInput = $('#node'), btnEnable = $('#btnEnable'), btnDisable = $('#btnDisable'), btnToggleAll = $('#btnToggleAll');
|
const nodeInput = $('#node'), btnEnable = $('#btnEnable'), btnDisable = $('#btnDisable'), btnToggleAll = $('#btnToggleAll');
|
||||||
const btnRefresh = $('#btnRefresh'), btnAuto = $('#btnAuto'), selInterval = $('#selInterval');
|
const btnRefresh = $('#btnRefresh'), btnAuto = $('#btnAuto'), selInterval = $('#selInterval');
|
||||||
const healthDot = $('#healthDot'), healthTitle = $('#healthTitle'), healthSub = $('#healthSub');
|
const healthDot = $('#healthDot'), healthTitle = $('#healthTitle'), healthSub = $('#healthSub');
|
||||||
const qSummary = $('#q-summary'), qCardsWrap = $('#q-cards'), unitsBox = $('#units'), replBox = $('#repl');
|
const qSummary = $('#q-summary'), qCardsWrap = $('#q-cards'), unitsBox = $('#units'), replBox = $('#repl');
|
||||||
const tblHaRes=$('#ha-res tbody'), tblHaStatus=$('#ha-status tbody'), tblNodes=$('#nodes tbody'), tblNonHA=$('#nonha tbody');
|
const tblHaRes = $('#ha-res'), tblHaStatus = $('#ha-status'), tblNodes = $('#nodes'), tblNonHA = $('#nonha');
|
||||||
const pvecmPre = $('#pvecm'), cfgtoolPre = $('#cfgtool'), footer = $('#footer');
|
const pvecmPre = $('#pvecm'), cfgtoolPre = $('#cfgtool'), footer = $('#footer');
|
||||||
|
|
||||||
// ------ actions ------
|
// ------ actions ------
|
||||||
@@ -117,6 +121,22 @@ async function fetchNodeDetail(name){
|
|||||||
return await r.json();
|
return await r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------ Normalizacja stanu usług ------
|
||||||
|
function normSvcState(s) {
|
||||||
|
const raw = String(pick(
|
||||||
|
s.active, s['active-state'], s.ActiveState, s.activestate,
|
||||||
|
s.state, s.SubState, s.substate
|
||||||
|
)).toLowerCase();
|
||||||
|
const bad = String(pick(s.result, s.Result, s['exit-code'])).toLowerCase();
|
||||||
|
|
||||||
|
if (/failed|error|dead/.test(bad)) return 'failed';
|
||||||
|
if (/^active$|running/.test(raw)) return 'active';
|
||||||
|
if (/activating|starting/.test(raw)) return 'activating';
|
||||||
|
if (/deactivating|stopping/.test(raw)) return 'deactivating';
|
||||||
|
if (/failed|dead/.test(raw)) return 'failed';
|
||||||
|
return 'inactive';
|
||||||
|
}
|
||||||
|
|
||||||
// ------ VM detail card ------
|
// ------ VM detail card ------
|
||||||
function renderVmDetailCard(d) {
|
function renderVmDetailCard(d) {
|
||||||
const meta = d.meta || {};
|
const meta = d.meta || {};
|
||||||
@@ -137,8 +157,6 @@ function renderVmDetailCard(d){
|
|||||||
const balloonEnabled = (cfg.balloon !== undefined) ? (Number(cfg.balloon) !== 0) : (cur.balloon !== undefined && Number(cur.balloon) !== 0);
|
const balloonEnabled = (cfg.balloon !== undefined) ? (Number(cfg.balloon) !== 0) : (cur.balloon !== undefined && Number(cur.balloon) !== 0);
|
||||||
const binfo = cur.ballooninfo || null;
|
const binfo = cur.ballooninfo || null;
|
||||||
|
|
||||||
const nets = parseVmNetworks(cfg);
|
|
||||||
|
|
||||||
let guestName = agOS && (agOS.name || agOS.pretty_name) || (agInfo && agInfo.version) || '';
|
let guestName = agOS && (agOS.name || agOS.pretty_name) || (agInfo && agInfo.version) || '';
|
||||||
let guestIPs = [];
|
let guestIPs = [];
|
||||||
if (Array.isArray(agIfs)) {
|
if (Array.isArray(agIfs)) {
|
||||||
@@ -149,12 +167,6 @@ function renderVmDetailCard(d){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const nicStats = cur.nics || {};
|
|
||||||
const nicRows = Object.keys(nicStats).sort().map(ifn=>{
|
|
||||||
const ns = nicStats[ifn]||{};
|
|
||||||
return rowHTML([ifn, humanBytes(ns.netin||0), humanBytes(ns.netout||0)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const bstat = cur.blockstat || {};
|
const bstat = cur.blockstat || {};
|
||||||
const bRows = Object.keys(bstat).sort().map(dev => {
|
const bRows = Object.keys(bstat).sort().map(dev => {
|
||||||
const s = bstat[dev] || {};
|
const s = bstat[dev] || {};
|
||||||
@@ -178,44 +190,8 @@ function renderVmDetailCard(d){
|
|||||||
'Pressure MEM (some/full)': `${safe(cur.pressurememorysome)}/${safe(cur.pressurememoryfull)}`
|
'Pressure MEM (some/full)': `${safe(cur.pressurememorysome)}/${safe(cur.pressurememoryfull)}`
|
||||||
};
|
};
|
||||||
|
|
||||||
const head = `
|
const nets = parseVmNetworks(cfg);
|
||||||
<div class="d-flex flex-wrap align-items-center gap-3 mb-2">
|
const netRows = nets.map(n => {
|
||||||
<div class="fw-bold">${meta.name || cfg.name || d.sid}</div>
|
|
||||||
<div class="text-muted small">${(d.type||'').toUpperCase()} / VMID ${d.vmid} @ ${d.node}</div>
|
|
||||||
<div class="vr"></div>
|
|
||||||
<div>${statusBadge}</div>
|
|
||||||
${meta.hastate ? `<div class="vr"></div><div class="small">HA: ${badge(meta.hastate, /started/i.test(meta.hastate)?'ok':'warn')}</div>` : ''}
|
|
||||||
${ha.state ? `<div class="vr"></div><div class="small">HA runtime: ${haBadge}</div>` : ''}
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const quick = `
|
|
||||||
<div class="row row-cols-2 row-cols-md-4 g-2">
|
|
||||||
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
|
||||||
<div class="text-muted small">CPU</div><div class="fw-semibold">${cur.cpu !== undefined ? (cur.cpu*100).toFixed(1)+'%' : '—'}</div>
|
|
||||||
<div class="small text-muted">vCPUs: ${safe(cur.cpus ?? cfg.cores ?? cfg.sockets)}</div>
|
|
||||||
</div></div></div>
|
|
||||||
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
|
||||||
<div class="text-muted small">Memory (used/free/total)</div>
|
|
||||||
<div class="fw-semibold">${
|
|
||||||
(used!=null && maxmem!=null)
|
|
||||||
? `${humanBytes(used)} / ${humanBytes(free)} / ${humanBytes(maxmem)}`
|
|
||||||
: '—'
|
|
||||||
}</div>
|
|
||||||
<div class="small mt-1">Ballooning: ${balloonEnabled ? badge('enabled','ok') : badge('disabled','err')}</div>
|
|
||||||
${binfo ? `<div class="small text-muted mt-1">Balloon actual: ${humanBytes(binfo.actual)} | guest free: ${humanBytes(binfo.free_mem)} | guest total: ${humanBytes(binfo.total_mem)}</div>` : ''}
|
|
||||||
</div></div></div>
|
|
||||||
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
|
||||||
<div class="text-muted small">Disk (used/total)</div>
|
|
||||||
<div class="fw-semibold">${(cur.disk!=null && cur.maxdisk!=null) ? `${humanBytes(cur.disk)} / ${humanBytes(cur.maxdisk)}` : '—'}</div>
|
|
||||||
<div class="small text-muted mt-1">R: ${humanBytes(cur.diskread||0)} | W: ${humanBytes(cur.diskwrite||0)}</div>
|
|
||||||
</div></div></div>
|
|
||||||
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
|
||||||
<div class="text-muted small">Uptime</div><div class="fw-semibold">${fmtSeconds(cur.uptime)}</div>
|
|
||||||
<div class="small text-muted mt-1">Tags: ${safe(cfg.tags||meta.tags||'—')}</div>
|
|
||||||
</div></div></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const netRows = (parseVmNetworks(cfg)).map(n=>{
|
|
||||||
const br = n.bridge || n.br || '—';
|
const br = n.bridge || n.br || '—';
|
||||||
const mdl = n.model || n.type || (n.raw?.split(',')[0]?.split('=')[0]) || 'virtio';
|
const mdl = n.model || n.type || (n.raw?.split(',')[0]?.split('=')[0]) || 'virtio';
|
||||||
const mac = n.hwaddr || n.mac || (n.raw?.match(/([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}/)?.[0] || '—');
|
const mac = n.hwaddr || n.mac || (n.raw?.match(/([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}/)?.[0] || '—');
|
||||||
@@ -231,22 +207,6 @@ function renderVmDetailCard(d){
|
|||||||
</table>
|
</table>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const nicLive = `
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-striped align-middle table-nowrap">
|
|
||||||
<thead><tr><th>TAP</th><th>RX</th><th>TX</th></tr></thead>
|
|
||||||
<tbody>${nicRows.length?nicRows.join(''):rowHTML(['—','—','—'])}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const blkTable = `
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-striped align-middle table-nowrap">
|
|
||||||
<thead><tr><th>Device</th><th>Read bytes</th><th>Read ops</th><th>Write bytes</th><th>Write ops</th><th>Flush ops</th><th>Highest offset</th></tr></thead>
|
|
||||||
<tbody>${bRows.length?bRows.join(''):rowHTML(['—','—','—','—','—','—','—'])}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const agentSummary = (agInfo || agOS || guestIPs.length)
|
const agentSummary = (agInfo || agOS || guestIPs.length)
|
||||||
? `<div class="small">
|
? `<div class="small">
|
||||||
${agOS ? `<div>Guest OS: <strong>${safe(guestName)}</strong></div>` : ''}
|
${agOS ? `<div>Guest OS: <strong>${safe(guestName)}</strong></div>` : ''}
|
||||||
@@ -283,19 +243,53 @@ function renderVmDetailCard(d){
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${head}
|
<div class="d-flex flex-wrap align-items-center gap-3 mb-2">
|
||||||
${quick}
|
<div class="fw-bold">${safe(meta.name || cfg.name || d.sid)}</div>
|
||||||
|
<div class="text-muted small">${(d.type || '').toUpperCase()} / VMID ${safe(d.vmid)} @ ${safe(d.node)}</div>
|
||||||
|
<div class="vr"></div>
|
||||||
|
<div>${statusBadge}</div>
|
||||||
|
${meta.hastate ? `<div class="vr"></div><div class="small">HA: ${badge(meta.hastate, /started/i.test(meta.hastate) ? 'ok' : 'warn')}</div>` : ''}
|
||||||
|
${ha.state ? `<div class="vr"></div><div class="small">HA runtime: ${haBadge}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-2 row-cols-md-4 g-2">
|
||||||
|
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
||||||
|
<div class="text-muted small">CPU</div><div class="fw-semibold">${cur.cpu !== undefined ? (cur.cpu * 100).toFixed(1) + '%' : '—'}</div>
|
||||||
|
<div class="small text-muted">vCPUs: ${safe(cur.cpus ?? cfg.cores ?? cfg.sockets)}</div>
|
||||||
|
</div></div></div>
|
||||||
|
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
||||||
|
<div class="text-muted small">Memory (used/free/total)</div>
|
||||||
|
<div class="fw-semibold">${(used != null && maxmem != null)
|
||||||
|
? `${humanBytes(used)} / ${humanBytes(free)} / ${humanBytes(maxmem)}`
|
||||||
|
: '—'
|
||||||
|
}</div>
|
||||||
|
<div class="small mt-1">Ballooning: ${balloonEnabled ? badge('enabled', 'ok') : badge('disabled', 'err')}</div>
|
||||||
|
${binfo ? `<div class="small text-muted mt-1">Balloon actual: ${humanBytes(binfo.actual)} | guest free: ${humanBytes(binfo.free_mem)} | guest total: ${humanBytes(binfo.total_mem)}</div>` : ''}
|
||||||
|
</div></div></div>
|
||||||
|
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
||||||
|
<div class="text-muted small">Disk (used/total)</div>
|
||||||
|
<div class="fw-semibold">${(cur.disk != null && cur.maxdisk != null) ? `${humanBytes(cur.disk)} / ${humanBytes(cur.maxdisk)}` : '—'}</div>
|
||||||
|
<div class="small text-muted mt-1">R: ${humanBytes(cur.diskread || 0)} | W: ${humanBytes(cur.diskwrite || 0)}</div>
|
||||||
|
</div></div></div>
|
||||||
|
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
||||||
|
<div class="text-muted small">Uptime</div><div class="fw-semibold">${fmtSeconds(cur.uptime)}</div>
|
||||||
|
<div class="small text-muted mt-1">Tags: ${safe(cfg.tags || meta.tags || '—')}</div>
|
||||||
|
</div></div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="fw-semibold mb-1">Network (config)</div>
|
<div class="fw-semibold mb-1">Network (config)</div>
|
||||||
${netTable}
|
${netTable}
|
||||||
<div class="fw-semibold mt-3 mb-1">Network (live)</div>
|
|
||||||
${nicLive}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="fw-semibold mb-1">Disks (block statistics)</div>
|
<div class="fw-semibold mb-1">Disks (block statistics)</div>
|
||||||
${blkTable}
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped align-middle table-nowrap">
|
||||||
|
<thead><tr><th>Device</th><th>Read bytes</th><th>Read ops</th><th>Write bytes</th><th>Write ops</th><th>Flush ops</th><th>Highest offset</th></tr></thead>
|
||||||
|
<tbody>${Object.keys(bstat).length ? bRows.join('') : rowHTML(['—', '—', '—', '—', '—', '—', '—'])}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
@@ -319,7 +313,6 @@ function renderNodeDetailCard(d){
|
|||||||
const tm = d.time || {};
|
const tm = d.time || {};
|
||||||
const svcs = ensureArr(d.services);
|
const svcs = ensureArr(d.services);
|
||||||
const netcfg = ensureArr(d.network_cfg);
|
const netcfg = ensureArr(d.network_cfg);
|
||||||
const netstat = ensureArr(d.netstat);
|
|
||||||
const disks = ensureArr(d.disks);
|
const disks = ensureArr(d.disks);
|
||||||
const sub = d.subscription || {};
|
const sub = d.subscription || {};
|
||||||
|
|
||||||
@@ -351,14 +344,12 @@ function renderNodeDetailCard(d){
|
|||||||
<div class="row row-cols-2 row-cols-md-4 g-2">
|
<div class="row row-cols-2 row-cols-md-4 g-2">
|
||||||
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
||||||
<div class="text-muted small">Memory</div>
|
<div class="text-muted small">Memory</div>
|
||||||
<div class="fw-semibold">${
|
<div class="fw-semibold">${(mem.used != null && mem.total != null) ? `${humanBytes(mem.used)} / ${humanBytes(mem.total)} (${pct(mem.used / mem.total)})` : '—'
|
||||||
(mem.used!=null && mem.total!=null) ? `${humanBytes(mem.used)} / ${humanBytes(mem.total)} (${pct(mem.used/mem.total)})` : '—'
|
|
||||||
}</div>
|
}</div>
|
||||||
</div></div></div>
|
</div></div></div>
|
||||||
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
||||||
<div class="text-muted small">RootFS</div>
|
<div class="text-muted small">RootFS</div>
|
||||||
<div class="fw-semibold">${
|
<div class="fw-semibold">${(root.used != null && root.total != null) ? `${humanBytes(root.used)} / ${humanBytes(root.total)} (${pct(root.used / root.total)})` : '—'
|
||||||
(root.used!=null && root.total!=null) ? `${humanBytes(root.used)} / ${humanBytes(root.total)} (${pct(root.used/root.total)})` : '—'
|
|
||||||
}</div>
|
}</div>
|
||||||
</div></div></div>
|
</div></div></div>
|
||||||
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
<div class="col"><div class="card border-0"><div class="card-body p-2">
|
||||||
@@ -371,18 +362,19 @@ function renderNodeDetailCard(d){
|
|||||||
</div></div></div>
|
</div></div></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// --- NORMALIZE service names + build badge
|
// --- NORMALIZE service names + badge
|
||||||
const svcMap = {};
|
const svcMap = {};
|
||||||
svcs.forEach(s => {
|
svcs.forEach(s => {
|
||||||
const raw = (s && s.name) ? s.name : '';
|
const raw = (s && s.name) ? s.name : '';
|
||||||
const key = raw.replace(/\.service$/, ''); // normalize
|
const key = raw.replace(/\.service$/, ''); // normalize
|
||||||
const st = (s && (s.state || s.active)) || '';
|
svcMap[key] = normSvcState(s);
|
||||||
svcMap[key] = st;
|
|
||||||
});
|
});
|
||||||
function svcBadge(name) {
|
function svcBadge(name) {
|
||||||
const st = String(svcMap[name] || '').toLowerCase();
|
const st = String(svcMap[name] || '').toLowerCase();
|
||||||
return (/running|active/.test(st)) ? badge('active','ok') :
|
if (st === 'active') return badge('active', 'ok');
|
||||||
(st ? badge(st,'err') : badge('inactive','err'));
|
if (st === 'activating' || st === 'deactivating') return badge(st, 'warn');
|
||||||
|
if (st === 'failed') return badge('failed', 'err');
|
||||||
|
return badge('inactive', 'err');
|
||||||
}
|
}
|
||||||
const svcTable = `
|
const svcTable = `
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -410,16 +402,6 @@ function renderNodeDetailCard(d){
|
|||||||
</table>
|
</table>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// Netstat live
|
|
||||||
const nsRows = netstat.map(x=> rowHTML([safe(x.dev||x.iface), humanBytes(x.rx_bytes||x['receive-bytes']||0), humanBytes(x.tx_bytes||x['transmit-bytes']||0)]));
|
|
||||||
const netLiveTable = `
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-striped align-middle table-nowrap">
|
|
||||||
<thead><tr><th>IF</th><th>RX</th><th>TX</th></tr></thead>
|
|
||||||
<tbody>${nsRows.length?nsRows.join(''):rowHTML(['—','—','—'])}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
// Disks
|
// Disks
|
||||||
const diskRows = disks.map(dv => rowHTML([safe(dv.devpath || dv.kname || dv.dev), safe(dv.model), safe(dv.size ? humanBytes(dv.size) : '—'), safe(dv.health || dv.wearout || '—'), safe(dv.serial || '—')]));
|
const diskRows = disks.map(dv => rowHTML([safe(dv.devpath || dv.kname || dv.dev), safe(dv.model), safe(dv.size ? humanBytes(dv.size) : '—'), safe(dv.health || dv.wearout || '—'), safe(dv.serial || '—')]));
|
||||||
const diskTable = `
|
const diskTable = `
|
||||||
@@ -462,8 +444,6 @@ function renderNodeDetailCard(d){
|
|||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="fw-semibold mb-1">Network (config)</div>
|
<div class="fw-semibold mb-1">Network (config)</div>
|
||||||
${netCfgTable}
|
${netCfgTable}
|
||||||
<div class="fw-semibold mt-3 mb-1">Network (live)</div>
|
|
||||||
${netLiveTable}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
@@ -536,7 +516,7 @@ function parsePveSr(text){
|
|||||||
}
|
}
|
||||||
function renderReplication(text) {
|
function renderReplication(text) {
|
||||||
const arr = parsePveSr(text);
|
const arr = parsePveSr(text);
|
||||||
if(!arr.length){ replBox.innerHTML='<span class="text-muted small">No replication jobs</span>'; return; }
|
if (!arr.length) { replBox && (replBox.innerHTML = '<span class="text-muted small">No replication jobs</span>'); return; }
|
||||||
const rows = arr.map(x => {
|
const rows = arr.map(x => {
|
||||||
const en = /^yes$/i.test(x.enabled) ? badge('Yes', 'ok') : badge('No', 'err');
|
const en = /^yes$/i.test(x.enabled) ? badge('Yes', 'ok') : badge('No', 'err');
|
||||||
const st = /^ok$/i.test(x.state) ? badge(x.state, 'ok') : badge(x.state, 'err');
|
const st = /^ok$/i.test(x.state) ? badge(x.state, 'ok') : badge(x.state, 'err');
|
||||||
@@ -559,13 +539,13 @@ function renderHAResources(list){
|
|||||||
rows.push(`<tr class="vm-detail d-none"><td colspan="5"><div class="spinner-border spinner-border-sm me-2 d-none"></div><div class="vm-json">—</div></td></tr>`);
|
rows.push(`<tr class="vm-detail d-none"><td colspan="5"><div class="spinner-border spinner-border-sm me-2 d-none"></div><div class="vm-json">—</div></td></tr>`);
|
||||||
});
|
});
|
||||||
setRows(tblHaRes, rows.length ? rows : [rowHTML(['—', '—', '—', '—', '—'])]);
|
setRows(tblHaRes, rows.length ? rows : [rowHTML(['—', '—', '—', '—', '—'])]);
|
||||||
Array.from(document.querySelectorAll('#ha-res tbody tr.vm-row')).forEach((tr,i)=>{
|
Array.from(document.querySelectorAll('#ha-res tr.vm-row')).forEach((tr, i) => {
|
||||||
tr.onclick = async () => {
|
tr.onclick = async () => {
|
||||||
const detailRow = tblHaRes.querySelectorAll('tr.vm-detail')[i];
|
const detailRow = tblHaRes.querySelectorAll('tr.vm-detail')[i];
|
||||||
const content = detailRow.querySelector('.vm-json');
|
const content = detailRow.querySelector('.vm-json');
|
||||||
const spin = detailRow.querySelector('.spinner-border');
|
const spin = detailRow.querySelector('.spinner-border');
|
||||||
const open = detailRow.classList.contains('d-none');
|
const open = detailRow.classList.contains('d-none');
|
||||||
document.querySelectorAll('#ha-res tbody tr.vm-detail').forEach(r=>r.classList.add('d-none'));
|
document.querySelectorAll('#ha-res tr.vm-detail').forEach(r => r.classList.add('d-none'));
|
||||||
if (open) {
|
if (open) {
|
||||||
detailRow.classList.remove('d-none'); spin.classList.remove('d-none');
|
detailRow.classList.remove('d-none'); spin.classList.remove('d-none');
|
||||||
const sid = tr.getAttribute('data-sid');
|
const sid = tr.getAttribute('data-sid');
|
||||||
@@ -603,13 +583,13 @@ async function renderNonHA(){
|
|||||||
rows.push(`<tr class="vm-detail d-none"><td colspan="5"><div class="spinner-border spinner-border-sm me-2 d-none"></div><div class="vm-json">—</div></td></tr>`);
|
rows.push(`<tr class="vm-detail d-none"><td colspan="5"><div class="spinner-border spinner-border-sm me-2 d-none"></div><div class="vm-json">—</div></td></tr>`);
|
||||||
});
|
});
|
||||||
setRows(tblNonHA, rows);
|
setRows(tblNonHA, rows);
|
||||||
Array.from(document.querySelectorAll('#nonha tbody tr.vm-row')).forEach((tr,i)=>{
|
Array.from(document.querySelectorAll('#nonha tr.vm-row')).forEach((tr, i) => {
|
||||||
tr.onclick = async () => {
|
tr.onclick = async () => {
|
||||||
const detailRow = tblNonHA.querySelectorAll('tr.vm-detail')[i];
|
const detailRow = tblNonHA.querySelectorAll('tr.vm-detail')[i];
|
||||||
const content = detailRow.querySelector('.vm-json');
|
const content = detailRow.querySelector('.vm-json');
|
||||||
const spin = detailRow.querySelector('.spinner-border');
|
const spin = detailRow.querySelector('.spinner-border');
|
||||||
const open = detailRow.classList.contains('d-none');
|
const open = detailRow.classList.contains('d-none');
|
||||||
document.querySelectorAll('#nonha tbody tr.vm-detail').forEach(r=>r.classList.add('d-none'));
|
document.querySelectorAll('#nonha tr.vm-detail').forEach(r => r.classList.add('d-none'));
|
||||||
if (open) {
|
if (open) {
|
||||||
detailRow.classList.remove('d-none'); spin.classList.remove('d-none');
|
detailRow.classList.remove('d-none'); spin.classList.remove('d-none');
|
||||||
const sid = tr.getAttribute('data-sid');
|
const sid = tr.getAttribute('data-sid');
|
||||||
@@ -639,7 +619,7 @@ function renderNodesTable(nodes){
|
|||||||
|
|
||||||
const main = `<tr class="node-row" data-node="${safe(n.node)}">
|
const main = `<tr class="node-row" data-node="${safe(n.node)}">
|
||||||
<td class="sticky-col fw-semibold">${safe(n.node)}</td>
|
<td class="sticky-col fw-semibold">${safe(n.node)}</td>
|
||||||
<td>${sB}</td><td>${cpu}</td><td>${load}</td><td>${mem}</td><td>${rfs}</td><td>${safe(n.uptime)}</td>
|
<td>${sB}</td><td>${cpu}</td><td>${load}</td><td>${mem}</td><td>${rfs}</td><td>${fmtSeconds(n.uptime)}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
const detail = `<tr class="node-detail d-none">
|
const detail = `<tr class="node-detail d-none">
|
||||||
@@ -653,13 +633,13 @@ function renderNodesTable(nodes){
|
|||||||
});
|
});
|
||||||
setRows(tblNodes, nrows);
|
setRows(tblNodes, nrows);
|
||||||
|
|
||||||
Array.from(document.querySelectorAll('#nodes tbody tr.node-row')).forEach((tr,i)=>{
|
Array.from(document.querySelectorAll('#nodes tr.node-row')).forEach((tr, i) => {
|
||||||
tr.onclick = async () => {
|
tr.onclick = async () => {
|
||||||
const detailRow = tblNodes.querySelectorAll('tr.node-detail')[i];
|
const detailRow = tblNodes.querySelectorAll('tr.node-detail')[i];
|
||||||
const content = detailRow.querySelector('.node-json');
|
const content = detailRow.querySelector('.node-json');
|
||||||
const spin = detailRow.querySelector('.spinner-border');
|
const spin = detailRow.querySelector('.spinner-border');
|
||||||
const open = detailRow.classList.contains('d-none');
|
const open = detailRow.classList.contains('d-none');
|
||||||
document.querySelectorAll('#nodes tbody tr.node-detail').forEach(r=>r.classList.add('d-none'));
|
document.querySelectorAll('#nodes tr.node-detail').forEach(r => r.classList.add('d-none'));
|
||||||
if (open) {
|
if (open) {
|
||||||
detailRow.classList.remove('d-none'); spin.classList.remove('d-none');
|
detailRow.classList.remove('d-none'); spin.classList.remove('d-none');
|
||||||
const name = tr.getAttribute('data-node');
|
const name = tr.getAttribute('data-node');
|
||||||
|
@@ -1,12 +1,27 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-bs-theme="dark">
|
<html lang="en" data-bs-theme="dark">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>PVE HA Panel</title>
|
<title>PVE HA Panel</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', filename='styles.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='styles.css') }}" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
footer.site-footer {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer.site-footer a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer.site-footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
|
|
||||||
@@ -83,7 +98,9 @@
|
|||||||
<div id="c-q" class="accordion-collapse collapse" data-bs-parent="#acc">
|
<div id="c-q" class="accordion-collapse collapse" data-bs-parent="#acc">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<div id="q-summary" class="mb-3 small">Loading…</div>
|
<div id="q-summary" class="mb-3 small">Loading…</div>
|
||||||
<div class="row g-3" id="q-cards"><div class="text-muted small">Loading…</div></div>
|
<div class="row g-3" id="q-cards">
|
||||||
|
<div class="text-muted small">Loading…</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,26 +122,38 @@
|
|||||||
<!-- Replication -->
|
<!-- Replication -->
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="h-repl">
|
<h2 class="accordion-header" id="h-repl">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#c-repl">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#c-repl">
|
||||||
Replication (pvesr)
|
Replication (pvesr)
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="c-repl" class="accordion-collapse collapse" data-bs-parent="#acc">
|
<div id="c-repl" class="accordion-collapse collapse" data-bs-parent="#acc">
|
||||||
<div class="accordion-body"><div id="repl"><span class="text-muted small">Loading…</span></div></div>
|
<div class="accordion-body">
|
||||||
|
<div id="repl"><span class="text-muted small">Loading…</span></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HA resources -->
|
<!-- HA resources -->
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="h-res">
|
<h2 class="accordion-header" id="h-res">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#c-res">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#c-res">
|
||||||
HA — resources
|
HA — resources
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="c-res" class="accordion-collapse collapse" data-bs-parent="#acc">
|
<div id="c-res" class="accordion-collapse collapse" data-bs-parent="#acc">
|
||||||
<div class="accordion-body table-responsive">
|
<div class="accordion-body table-responsive">
|
||||||
<table class="table table-sm table-striped align-middle table-nowrap" id="ha-res">
|
<table class="table table-sm table-striped align-middle table-nowrap" id="ha-res">
|
||||||
<thead><tr><th>SID</th><th>State</th><th>Node</th><th>Group</th><th>Flags/Comment</th></tr></thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>SID</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Node</th>
|
||||||
|
<th>Group</th>
|
||||||
|
<th>Flags/Comment</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="small text-muted">Click a row to expand VM/CT details.</div>
|
<div class="small text-muted">Click a row to expand VM/CT details.</div>
|
||||||
@@ -135,15 +164,27 @@
|
|||||||
<!-- HA status -->
|
<!-- HA status -->
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="h-hastatus">
|
<h2 class="accordion-header" id="h-hastatus">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#c-hastatus">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#c-hastatus">
|
||||||
HA — status
|
HA — status
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="c-hastatus" class="accordion-collapse collapse" data-bs-parent="#acc">
|
<div id="c-hastatus" class="accordion-collapse collapse" data-bs-parent="#acc">
|
||||||
<div class="accordion-body table-responsive">
|
<div class="accordion-body table-responsive">
|
||||||
<table class="table table-sm table-striped align-middle table-nowrap" id="ha-status">
|
<table class="table table-sm table-striped align-middle table-nowrap" id="ha-status">
|
||||||
<thead><tr><th>Node</th><th>Status</th><th>CRM</th><th>LRM</th></tr></thead>
|
<thead>
|
||||||
<tbody><tr><td colspan="4" class="text-muted">Loading…</td></tr></tbody>
|
<tr>
|
||||||
|
<th>Node</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>CRM</th>
|
||||||
|
<th>LRM</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-muted">Loading…</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,15 +193,20 @@
|
|||||||
<!-- Raw -->
|
<!-- Raw -->
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="h-raw">
|
<h2 class="accordion-header" id="h-raw">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#c-raw">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#c-raw">
|
||||||
Raw: pvecm / cfgtool
|
Raw: pvecm / cfgtool
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="c-raw" class="accordion-collapse collapse" data-bs-parent="#acc">
|
<div id="c-raw" class="accordion-collapse collapse" data-bs-parent="#acc">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-lg-6"><pre id="pvecm" class="mb-0 small"></pre></div>
|
<div class="col-lg-6">
|
||||||
<div class="col-lg-6"><pre id="cfgtool" class="mb-0 small"></pre></div>
|
<pre id="pvecm" class="mb-0 small"></pre>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<pre id="cfgtool" class="mb-0 small"></pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,8 +220,20 @@
|
|||||||
<div class="card border-0">
|
<div class="card border-0">
|
||||||
<div class="card-body table-responsive">
|
<div class="card-body table-responsive">
|
||||||
<table class="table table-sm table-striped align-middle table-nowrap" id="nonha">
|
<table class="table table-sm table-striped align-middle table-nowrap" id="nonha">
|
||||||
<thead><tr><th>SID</th><th>Type</th><th>Name</th><th>Node</th><th>Status</th></tr></thead>
|
<thead>
|
||||||
<tbody><tr><td colspan="5" class="text-muted">Loading…</td></tr></tbody>
|
<tr>
|
||||||
|
<th>SID</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Node</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-muted">Loading…</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="small text-muted">Click a row to expand VM/CT details.</div>
|
<div class="small text-muted">Click a row to expand VM/CT details.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,8 +245,22 @@
|
|||||||
<div class="card border-0">
|
<div class="card border-0">
|
||||||
<div class="card-body table-responsive">
|
<div class="card-body table-responsive">
|
||||||
<table class="table table-sm table-striped align-middle table-nowrap" id="nodes">
|
<table class="table table-sm table-striped align-middle table-nowrap" id="nodes">
|
||||||
<thead><tr><th>Node</th><th>Status</th><th>CPU</th><th>Load</th><th>Mem</th><th>RootFS</th><th>Uptime</th></tr></thead>
|
<thead>
|
||||||
<tbody><tr><td colspan="7" class="text-muted">Loading…</td></tr></tbody>
|
<tr>
|
||||||
|
<th>Node</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>CPU</th>
|
||||||
|
<th>Load</th>
|
||||||
|
<th>Mem</th>
|
||||||
|
<th>RootFS</th>
|
||||||
|
<th>Uptime</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-muted">Loading…</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="small text-muted">Click a row to expand node details.</div>
|
<div class="small text-muted">Click a row to expand node details.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,9 +270,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-muted small mt-3" id="footer">—</div>
|
<div class="text-muted small mt-3" id="footer">—</div>
|
||||||
|
|
||||||
|
<!-- SITE FOOTER -->
|
||||||
|
<footer class="site-footer mt-4 pt-3 pb-4 text-muted small">
|
||||||
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
|
<div>© 2025 PVE HA Panel</div>
|
||||||
|
<div>
|
||||||
|
Autor: <strong>linuxiarz.pl</strong>
|
||||||
|
|
|
||||||
|
Kod źródłowy: <a href="https://gitea.linuxiarz.pl/gru/pve-ha-web" target="_blank"
|
||||||
|
rel="noopener">gitea.linuxiarz.pl/gru/pve-ha-web</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="{{ url_for('static', filename='main.js') }}"></script>
|
<script src="{{ url_for('static', filename='main.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Reference in New Issue
Block a user