ADD: remove architecture , fix sites

This commit is contained in:
2026-06-07 18:31:10 +01:00
parent d4d15da0df
commit 515fc94754
7 changed files with 60 additions and 413 deletions

View File

@@ -34,7 +34,7 @@ from modules.cloud_backup import (
r2_delete_backup, r2_upload_async, get_upload_job,
r2_is_configured, R2_BUCKET_NAME,
)
from modules.sites import get_sites_list, get_site_health, get_architecture, SITES, PLATFORM
from modules.sites import get_sites_list, get_site_health, SITES, PLATFORM
app = Flask(__name__)
app.secret_key = 'navitrends-secret-key-2025'
@@ -124,24 +124,13 @@ def sites_page():
site_count=len(SITES),
platform_domain=PLATFORM['domain'],
platform_url=f"{PLATFORM['domain_protocol']}://{PLATFORM['domain']}",
platform_ssl=PLATFORM.get('ssl_configured', True),
active_page='sites',
page_title='Application Sites',
page_subtitle='direct access · health · deployment info'
)
@app.route('/architecture')
@login_required
def architecture_page():
return render_template(
'pages/architecture.html',
main_server=MAIN_SERVER_IP,
active_page='architecture',
page_title='Architecture',
page_subtitle='stacks · containers · networks'
)
@app.route('/containers')
@login_required
def containers_page():
@@ -210,6 +199,7 @@ def settings_page():
main_server=MAIN_SERVER_IP,
platform_domain=PLATFORM['domain'],
platform_url=f"{PLATFORM['domain_protocol']}://{PLATFORM['domain']}",
platform_ssl=PLATFORM.get('ssl_configured', True),
system=system,
running_on_main=RUNNING_ON_MAIN_SERVER,
active_page='settings',
@@ -284,12 +274,6 @@ def api_site_health(site_id):
return jsonify(result)
@app.route('/api/architecture')
@login_required
def api_architecture():
return jsonify(get_architecture())
@app.route('/api/nav-summary')
@login_required
def api_nav_summary():

View File

@@ -9,7 +9,7 @@ from modules.backups import _ssh_main, get_all_root_containers, get_container_st
# ────────────────────────────────────────────────────────────────
# STATIC SITE REGISTRY (source of truth for UI + architecture)
# STATIC SITE REGISTRY (source of truth for Application Sites UI)
# ────────────────────────────────────────────────────────────────
PLATFORM = {
@@ -18,6 +18,7 @@ PLATFORM = {
'tagline': 'Navitrends ops dashboard',
'domain': 'cloudops.nav.ovh',
'domain_protocol': 'https',
'ssl_configured': True,
'port': 8088,
'internal_port': 5000,
'container': 'management-platform',
@@ -45,6 +46,7 @@ SITES = [
'internal_port': 8000,
'domain': 'erpnext.navitrends.ovh',
'domain_protocol': 'http',
'ssl_configured': False,
'health_path': '/',
'volumes': ['frappe-setup_frappe-sites', 'frappe-setup_mariadb-data'],
},
@@ -65,6 +67,7 @@ SITES = [
'internal_port': 8069,
'domain': 'odooo.nav.ovh',
'domain_protocol': 'https',
'ssl_configured': True,
'health_path': '/web',
'volumes': ['odoo-clean_db-data', 'odoo-clean_odoo-etc'],
},
@@ -85,6 +88,7 @@ SITES = [
'internal_port': 80,
'domain': 'next.cloud.nav.ovh',
'domain_protocol': 'https',
'ssl_configured': True,
'health_path': '/status.php',
'volumes': ['nextcloud-setup_nextcloud-data', 'nextcloud-setup_nextcloud-db-data'],
},
@@ -105,6 +109,7 @@ SITES = [
'internal_port': 80,
'domain': None,
'domain_protocol': 'http',
'ssl_configured': False,
'health_path': '/',
'volumes': ['mautic-setup_mautic-data', 'mautic-setup_mautic-db-data'],
'networks': ['mautic-network'],
@@ -126,6 +131,7 @@ SITES = [
'internal_port': 5678,
'domain': None,
'domain_protocol': 'http',
'ssl_configured': False,
'health_path': '/healthz',
'volumes': ['n8n-setup_n8n-data', 'n8n-setup_n8n-db-data'],
'networks': ['n8n-network', 'integration-network'],
@@ -140,24 +146,30 @@ def _build_urls(site):
port = site['port']
proto = site.get('domain_protocol', 'http')
path = site.get('health_path', '/') or '/'
ssl_configured = site.get('ssl_configured', proto == 'https')
domain = site.get('domain')
has_domain = bool(domain)
if has_domain:
primary_url = f"{proto}://{domain}{path}"
if ssl_configured:
access_url = f"{proto}://{domain}"
health_url = f"{access_url}{path}"
else:
access_url = f"http://{domain}:{port}"
health_url = f"{access_url}{path}"
else:
primary_url = f"http://{ip}:{port}{path}"
access_url = f"http://{ip}:{port}"
health_url = f"{access_url}{path}"
return {
'has_domain': has_domain,
'domain': domain,
'domain_protocol': proto,
'ssl_configured': ssl_configured,
'ip_url': f"http://{ip}:{port}",
'access_url': access_url.rstrip('/'),
'health_url': primary_url,
'health_url': health_url,
}
@@ -317,96 +329,3 @@ def get_site_health(site_id):
probe['status'] = 'up' if probe['reachable'] else 'down'
probe['checked_at'] = time.time()
return probe
def get_architecture():
"""Topology for architecture page — stacks, platform, shared infra."""
ctr_map = _container_map()
stacks = []
for site in SITES:
nodes = []
edges = []
main = site['main_container']
db_nodes = [c for c in site.get('containers', []) if c['role'] == 'Database']
cache_nodes = [c for c in site.get('containers', []) if 'Cache' in c.get('role', '')]
for ctr in site.get('containers', []):
live = ctr_map.get(ctr['name'], {})
is_up = 'Up' in live.get('status', '')
nodes.append({
'id': ctr['name'],
'label': ctr['name'],
'role': ctr['role'],
'type': 'database' if ctr['role'] == 'Database' else (
'cache' if 'Cache' in ctr['role'] else 'app'
),
'status': 'running' if is_up else ('stopped' if live else 'missing'),
'image': live.get('image', ctr.get('image', '')),
})
for db in db_nodes:
edges.append({'from': main, 'to': db['name'], 'label': 'DB'})
for cache in cache_nodes:
edges.append({'from': main, 'to': cache['name'], 'label': 'Redis'})
urls = _build_urls(site)
stacks.append({
'id': site['id'],
'name': site['name'],
'category': site['category'],
'brand_color': site['brand_color'],
'compose_dir': site['compose_dir'],
'port': site['port'],
'domain': site.get('domain'),
'access_url': urls['access_url'],
'nodes': nodes,
'edges': edges,
'networks': site.get('networks', ['bridge']),
'running': sum(1 for n in nodes if n['status'] == 'running'),
'total': len(nodes),
})
platform_status = 'running'
if RUNNING_ON_MAIN_SERVER:
out, _ = _ssh_main("docker inspect --format='{{.State.Status}}' management-platform 2>/dev/null")
if out.strip().lower() not in ('running', 'restarting'):
platform_status = out.strip() or 'unknown'
else:
out, _ = _ssh_main("docker inspect --format='{{.State.Status}}' management-platform 2>/dev/null")
platform_status = out.strip().lower() if out else 'unknown'
plat_urls = _build_urls({
'domain': PLATFORM['domain'],
'domain_protocol': PLATFORM['domain_protocol'],
'port': PLATFORM['port'],
'health_path': PLATFORM['health_path'],
})
return {
'server_ip': MAIN_SERVER_IP,
'platform': {
'id': PLATFORM['id'],
'name': PLATFORM['name'],
'tagline': PLATFORM['tagline'],
'container': PLATFORM['container'],
'port': PLATFORM['port'],
'internal_port': PLATFORM['internal_port'],
'domain': PLATFORM['domain'],
'domain_protocol': PLATFORM['domain_protocol'],
'status': platform_status,
'brand_color': PLATFORM['brand_color'],
**plat_urls,
},
'stacks': stacks,
'shared': {
'networks': ['integration-network', 'mautic-network', 'n8n-network'],
'backup_path': '/root/backups',
'vm_backup_path': '/backups/main-server',
},
'summary': {
'sites': len(SITES),
'containers': sum(s['total'] for s in stacks),
'running': sum(s['running'] for s in stacks),
},
}

View File

@@ -604,8 +604,7 @@ function refreshAll() {
const btn = document.querySelector('.icon-btn');
if (btn) btn.classList.add('spinning');
const extras = [];
if (typeof window.loadSites === 'function') extras.push(window.loadSites(false));
if (typeof window.loadArchitecture === 'function') extras.push(window.loadArchitecture());
if (typeof window.loadSites === 'function') extras.push(window.loadSites(true));
Promise.all([
checkServerStatus(),
refreshSystemMetrics(),

View File

@@ -31,9 +31,6 @@
<i class="fas fa-globe"></i><span>Application Sites</span>
<span class="nav-badge" id="nav-badge-sites">5</span>
</a>
<a class="nav-item {% if active_page == 'architecture' %}active{% endif %}" href="{{ url_for('architecture_page') }}">
<i class="fas fa-sitemap"></i><span>Architecture</span>
</a>
<a class="nav-item {% if active_page == 'containers' %}active{% endif %}" href="{{ url_for('containers_page') }}">
<i class="fas fa-cubes"></i><span>All Containers</span>
<span class="nav-badge" id="nav-badge-containers"></span>

View File

@@ -1,276 +0,0 @@
{% extends "base.html" %}
{% block content %}
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
.arch-summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 20px;
}
.arch-diagram-wrap {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 24px;
overflow-x: auto;
min-height: 320px;
margin-bottom: 20px;
}
.arch-diagram-wrap .mermaid { display: flex; justify-content: center; }
.arch-stacks {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 14px;
}
.arch-stack-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
border-left: 3px solid var(--accent);
}
.arch-stack-card h4 {
font-size: 14px; font-weight: 700; margin: 0 0 4px;
display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.arch-stack-meta { font-size: 11px; color: var(--text3); margin-bottom: 10px; font-family: var(--mono); }
.arch-node-list { display: flex; flex-direction: column; gap: 6px; }
.arch-node {
display: flex; align-items: center; gap: 8px;
font-size: 11px; font-family: var(--mono);
padding: 6px 8px; background: var(--surface2);
border-radius: 6px; border: 1px solid var(--border);
}
.arch-node .role-badge {
font-size: 8px; letter-spacing: 0.06em; padding: 2px 5px;
border-radius: 4px; background: var(--border); color: var(--text3);
}
.arch-node.running .status-dot { background: var(--green); }
.arch-node.stopped .status-dot, .arch-node.missing .status-dot { background: var(--red); }
.status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.arch-legend {
display: flex; gap: 16px; flex-wrap: wrap;
font-size: 11px; color: var(--text2); margin-bottom: 14px;
}
.arch-legend span { display: inline-flex; align-items: center; gap: 6px; }
.infra-box {
background: var(--surface2); border: 1px dashed var(--border2);
border-radius: var(--radius); padding: 14px 16px; margin-bottom: 20px;
}
.infra-box h4 { font-size: 13px; margin: 0 0 10px; }
.infra-list { display: flex; flex-wrap: wrap; gap: 8px; }
.infra-chip {
font-family: var(--mono); font-size: 11px;
padding: 4px 10px; border-radius: 20px;
background: var(--surface); border: 1px solid var(--border);
}
@media (max-width: 900px) { .arch-summary { grid-template-columns: repeat(2, 1fr); } }
</style>
<div class="arch-summary" id="arch-summary">
<div class="stat-card"><div class="stat-number" id="arch-sites"></div><div class="stat-label">App Stacks</div></div>
<div class="stat-card"><div class="stat-number" id="arch-ctrs" style="color:var(--accent2);"></div><div class="stat-label">Containers</div></div>
<div class="stat-card"><div class="stat-number" id="arch-running" style="color:var(--green);"></div><div class="stat-label">Running</div></div>
<div class="stat-card"><div class="stat-number" id="arch-server" style="font-size:14px;color:var(--cyan);"></div><div class="stat-label">Main Server</div></div>
</div>
<div class="card" style="margin-bottom:16px;">
<div class="card-header">
<div class="card-title"><i class="fas fa-sitemap"></i> Infrastructure Topology</div>
<span class="card-meta" id="arch-refresh-meta">Loading…</span>
</div>
<div class="arch-legend">
<span><span class="status-dot" style="background:var(--green);"></span> Running</span>
<span><span class="status-dot" style="background:var(--red);"></span> Stopped / missing</span>
<span><i class="fas fa-arrow-right" style="font-size:10px;color:var(--text3);"></i> Dependency</span>
</div>
<div class="arch-diagram-wrap" id="mermaid-wrap">
<pre class="mermaid" id="mermaid-diagram">flowchart TB
loading["Loading architecture…"]
</pre>
</div>
</div>
<div class="infra-box" id="infra-box" style="display:none;">
<h4><i class="fas fa-server" style="color:var(--accent2);margin-right:6px;"></i>Shared Infrastructure</h4>
<div class="infra-list" id="infra-list"></div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fas fa-layer-group"></i> Stack Breakdown</div>
<a href="/sites" class="btn btn-ghost btn-sm"><i class="fas fa-globe"></i> Open Sites</a>
</div>
<div class="arch-stacks" id="arch-stacks">
<div class="empty-state"><i class="fas fa-spinner fa-spin"></i> Loading…</div>
</div>
</div>
<script>
function esc(s) {
if (s == null) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function nodeStatusClass(st) {
return st === 'running' ? 'running' : 'stopped';
}
function buildMermaid(data) {
const ip = data.server_ip || 'server';
const plat = data.platform || {};
const platSt = plat.status === 'running' ? 'running' : 'stopped';
const platLabel = plat.domain
? `CloudOps Platform<br/>${plat.domain}`
: `CloudOps Platform<br/>:${plat.port || 8088}`;
let lines = ['flowchart TB'];
lines.push(` subgraph HOST["🖥 Main Server ${ip}"]`);
lines.push(` direction TB`);
lines.push(` PLATFORM["${platLabel}"]:::${platSt}`);
lines.push(` INTERNET(("🌐 Users / Internet"))`);
lines.push(` INTERNET --> PLATFORM`);
lines.push(` INTERNET --> APPS`);
lines.push(` subgraph APPS["Application Stacks"]`);
lines.push(` direction LR`);
(data.stacks || []).forEach((stack, i) => {
const sid = 'stack_' + stack.id;
const stackRunning = stack.running === stack.total && stack.total > 0;
const stackCls = stackRunning ? 'running' : (stack.running > 0 ? 'partial' : 'stopped');
lines.push(` subgraph ${sid}["${stack.name} :${stack.port}"]`);
lines.push(` direction TB`);
(stack.nodes || []).forEach(node => {
const nid = node.id.replace(/[^a-zA-Z0-9_]/g, '_');
const cls = node.status === 'running' ? 'running' : 'stopped';
const shortLabel = node.label.length > 22 ? node.label.substring(0, 20) + '…' : node.label;
lines.push(` ${nid}["${shortLabel}<br/><small>${node.role}</small>"]:::${cls}`);
});
(stack.edges || []).forEach(edge => {
const from = edge.from.replace(/[^a-zA-Z0-9_]/g, '_');
const to = edge.to.replace(/[^a-zA-Z0-9_]/g, '_');
lines.push(` ${from} --> ${to}`);
});
lines.push(` end`);
lines.push(` ${sid}:::${stackCls}`);
lines.push(` INTERNET --> ${sid}`);
});
lines.push(` end`);
lines.push(` end`);
const nets = (data.shared && data.shared.networks) || [];
if (nets.length) {
lines.push(` subgraph NETS["Docker Networks"]`);
nets.forEach((n, i) => {
const nid = 'net_' + i;
lines.push(` ${nid}["${n}"]:::network`);
});
lines.push(` end`);
}
lines.push('');
lines.push(' classDef running fill:#0d2818,stroke:#22c55e,color:#e8ecf4');
lines.push(' classDef stopped fill:#2a1215,stroke:#ef4444,color:#e8ecf4');
lines.push(' classDef partial fill:#2a2010,stroke:#f59e0b,color:#e8ecf4');
lines.push(' classDef network fill:#111827,stroke:#a78bfa,color:#e8ecf4');
return lines.join('\n');
}
function renderStackCards(stacks) {
const el = document.getElementById('arch-stacks');
if (!stacks.length) {
el.innerHTML = '<div class="empty-state">No stacks</div>';
return;
}
el.innerHTML = stacks.map(stack => {
const nodes = (stack.nodes || []).map(n => `
<div class="arch-node ${nodeStatusClass(n.status)}">
<span class="status-dot"></span>
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;">${esc(n.label)}</span>
<span class="role-badge">${esc(n.role)}</span>
</div>`).join('');
const domain = stack.domain ? esc(stack.domain) : `IP :${stack.port}`;
return `<div class="arch-stack-card" style="border-left-color:${esc(stack.brand_color)}">
<h4>
${esc(stack.name)}
<span class="badge ${stack.running === stack.total && stack.total ? 'badge-run' : 'badge-stop'}">${stack.running}/${stack.total}</span>
</h4>
<div class="arch-stack-meta">${domain} · ~/docker-compose/${esc(stack.compose_dir)}</div>
<div class="arch-node-list">${nodes}</div>
<div style="margin-top:10px;">
<a href="/sites" class="btn btn-ghost btn-sm" onclick="event.preventDefault();window.location='/sites'">
<i class="fas fa-arrow-up-right-from-square"></i> ${esc(stack.access_url || '')}
</a>
</div>
</div>`;
}).join('');
}
function renderInfra(shared, platform) {
const box = document.getElementById('infra-box');
const list = document.getElementById('infra-list');
const chips = [];
if (platform.domain) {
chips.push(`Platform: ${platform.access_url || platform.domain}`);
}
chips.push(`Container: ${platform.container || 'management-platform'} :${platform.port || 8088}`);
(shared.networks || []).forEach(n => chips.push(`Network: ${n}`));
if (shared.backup_path) chips.push(`Backups: ${shared.backup_path}`);
if (shared.vm_backup_path) chips.push(`VM sync: ${shared.vm_backup_path}`);
list.innerHTML = chips.map(c => `<span class="infra-chip">${esc(c)}</span>`).join('');
box.style.display = '';
}
async function loadArchitecture() {
const meta = document.getElementById('arch-refresh-meta');
try {
meta.textContent = 'Loading…';
const res = await fetch('/api/architecture');
const data = await res.json();
const sum = data.summary || {};
document.getElementById('arch-sites').textContent = sum.sites || 0;
document.getElementById('arch-ctrs').textContent = sum.containers || 0;
document.getElementById('arch-running').textContent = sum.running || 0;
document.getElementById('arch-server').textContent = data.server_ip || '—';
renderStackCards(data.stacks || []);
renderInfra(data.shared || {}, data.platform || {});
const diagram = buildMermaid(data);
const wrap = document.getElementById('mermaid-wrap');
wrap.innerHTML = `<pre class="mermaid" id="mermaid-diagram">${diagram}</pre>`;
mermaid.initialize({
startOnLoad: false,
theme: document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark',
flowchart: { curve: 'basis', padding: 16 },
securityLevel: 'loose',
});
await mermaid.run({ nodes: [document.getElementById('mermaid-diagram')] });
meta.textContent = 'Updated ' + new Date().toLocaleTimeString();
} catch (e) {
meta.textContent = 'Error';
document.getElementById('arch-stacks').innerHTML =
`<div class="empty-state" style="color:var(--red);">Failed: ${esc(e)}</div>`;
}
}
window.loadArchitecture = loadArchitecture;
document.addEventListener('DOMContentLoaded', () => {
loadArchitecture();
setInterval(loadArchitecture, 45000);
});
</script>
{% endblock %}

View File

@@ -5,6 +5,7 @@
<div style="max-width:460px; display:flex; flex-direction:column; gap:14px;">
<div class="form-group"><label class="form-label">MAIN SERVER IP</label><input class="form-input" value="{{ main_server }}" readonly></div>
<div class="form-group"><label class="form-label">PLATFORM DOMAIN</label><input class="form-input" value="{{ platform_domain }}" readonly></div>
<div class="form-group"><label class="form-label">SSL</label><input class="form-input" value="{{ 'Configured' if platform_ssl else 'Not configured' }}" readonly></div>
<div class="form-group"><label class="form-label">PLATFORM URL</label>
<div style="display:flex;gap:8px;align-items:center;">
<input class="form-input" value="{{ platform_url }}" readonly style="flex:1;">

View File

@@ -75,6 +75,9 @@
.site-url-link:hover { text-decoration: underline; }
.domain-set { color: var(--green); }
.domain-unset { color: var(--text3); font-style: italic; }
.ssl-set { color: var(--green); }
.ssl-unset { color: var(--yellow); }
.health-slot { min-width: 88px; min-height: 26px; display: flex; align-items: center; justify-content: flex-end; }
.health-pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px; border-radius: 20px;
@@ -125,6 +128,14 @@
<div>
<div style="font-size:13px;font-weight:600;">This platform</div>
<a class="site-url-link" href="{{ platform_url }}" target="_blank" rel="noopener">{{ platform_url }}</a>
<div style="font-size:11px;margin-top:4px;">
<span class="site-row-label" style="min-width:auto;margin-right:6px;">SSL</span>
{% if platform_ssl %}
<span class="ssl-set"><i class="fas fa-lock"></i> Configured</span>
{% else %}
<span class="ssl-unset"><i class="fas fa-lock-open"></i> Not configured</span>
{% endif %}
</div>
</div>
</div>
<a class="btn btn-ghost btn-sm" href="{{ platform_url }}" target="_blank" rel="noopener">
@@ -179,17 +190,22 @@ function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function healthPill(health, containerRunning) {
if (!health) {
return containerRunning
? '<span class="health-pill checking"><span class="dot"></span>Unchecked</span>'
: '<span class="health-pill offline"><span class="dot"></span>Offline</span>';
}
function healthPill(health) {
if (!health) return '';
const st = health.status || 'down';
const labels = { up: 'Active', down: 'Unreachable', offline: 'Offline', checking: 'Checking…' };
if (st === 'checking') {
return '<span class="health-pill checking"><span class="dot"></span>Checking…</span>';
}
const labels = { up: 'Healthy', down: 'Unhealthy', offline: 'Offline' };
const extra = health.latency_ms != null ? ` · ${health.latency_ms}ms` : '';
const code = health.http_code ? ` · HTTP ${health.http_code}` : '';
return `<span class="health-pill ${st}"><span class="dot"></span>${labels[st] || st}${extra}${code}</span>`;
return `<span class="health-pill ${st === 'up' ? 'up' : (st === 'offline' ? 'offline' : 'down')}"><span class="dot"></span>${labels[st] || st}${extra}${code}</span>`;
}
function sslHtml(configured) {
return configured
? '<span class="ssl-set"><i class="fas fa-lock"></i> Configured</span>'
: '<span class="ssl-unset"><i class="fas fa-lock-open"></i> Not configured</span>';
}
function renderSiteCard(site) {
@@ -209,13 +225,17 @@ function renderSiteCard(site) {
<h3>${esc(site.name)} <span class="site-category">${esc(site.category)}</span></h3>
<p class="site-tagline">${esc(site.tagline)}</p>
</div>
<div id="health-${esc(site.id)}">${healthPill(site.health, site.container_running)}</div>
<div class="health-slot" id="health-${esc(site.id)}">${healthPill(site.health)}</div>
</div>
<div class="site-card-body">
<div class="site-row">
<span class="site-row-label">DOMAIN</span>
<span class="site-row-value">${domainHtml}</span>
</div>
<div class="site-row">
<span class="site-row-label">SSL</span>
<span class="site-row-value">${sslHtml(site.ssl_configured)}</span>
</div>
<div class="site-row">
<span class="site-row-label">URL</span>
<span class="site-row-value">
@@ -254,10 +274,11 @@ function renderSiteCard(site) {
}
function updateSummary(sites) {
const up = sites.filter(s => s.health && s.health.status === 'up').length;
const checked = sites.filter(s => s.health && s.health.status !== 'checking');
const up = checked.filter(s => s.health.status === 'up').length;
const running = sites.reduce((n, s) => n + (s.containers_running || 0), 0);
const domains = sites.filter(s => s.has_domain).length;
document.getElementById('ss-up').textContent = up;
document.getElementById('ss-up').textContent = checked.length ? up : '—';
document.getElementById('ss-running').textContent = running;
document.getElementById('ss-domains').textContent = domains;
}
@@ -283,16 +304,17 @@ async function loadSites(withHealth = false) {
async function pingSite(siteId) {
const el = document.getElementById('health-' + siteId);
if (el) el.innerHTML = healthPill({ status: 'checking' }, true);
if (el) el.innerHTML = healthPill({ status: 'checking' });
try {
const res = await fetch('/api/sites/' + siteId + '/health');
const health = await res.json();
const site = sitesData.find(s => s.id === siteId);
if (site) site.health = health;
if (el) el.innerHTML = healthPill(health, site?.container_running);
if (el) el.innerHTML = healthPill(health);
updateSummary(sitesData);
} catch (e) {
if (el) el.innerHTML = healthPill({ status: 'down', error: String(e) }, true);
if (el) el.innerHTML = healthPill({ status: 'down', error: String(e) });
updateSummary(sitesData);
}
}
@@ -321,6 +343,7 @@ function openSiteDetails(siteId) {
<div class="site-detail-item"><div class="lbl">PORT MAPPING</div><div class="val">${esc(site.port_mapping || site.port + '→' + site.internal_port)}</div></div>
<div class="site-detail-item"><div class="lbl">ACCESS URL</div><div class="val"><a class="site-url-link" href="${esc(site.access_url)}" target="_blank">${esc(site.access_url)}</a></div></div>
<div class="site-detail-item"><div class="lbl">DOMAIN</div><div class="val">${site.has_domain ? esc(site.domain) : '— (IP only)'}</div></div>
<div class="site-detail-item"><div class="lbl">SSL</div><div class="val">${site.ssl_configured ? 'Configured' : 'Not configured'}</div></div>
<div class="site-detail-item"><div class="lbl">IP FALLBACK</div><div class="val">${esc(site.ip_url)}</div></div>
<div class="site-detail-item"><div class="lbl">HEALTH ENDPOINT</div><div class="val">${esc(site.health_url)}</div></div>
</div>
@@ -358,7 +381,7 @@ window.closeSiteModal = closeSiteModal;
document.addEventListener('DOMContentLoaded', () => {
loadSites(true);
setInterval(() => loadSites(false), 30000);
setInterval(() => loadSites(true), 60000);
});
</script>
{% endblock %}