277 lines
11 KiB
HTML
277 lines
11 KiB
HTML
{% 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
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 %}
|