diff --git a/platform/app.py b/platform/app.py index 1026bb9..6bf6d08 100644 --- a/platform/app.py +++ b/platform/app.py @@ -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(): diff --git a/platform/modules/sites.py b/platform/modules/sites.py index c3d1161..f89da9c 100644 --- a/platform/modules/sites.py +++ b/platform/modules/sites.py @@ -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}" - access_url = f"{proto}://{domain}" + 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), - }, - } diff --git a/platform/static/js/platform.js b/platform/static/js/platform.js index 60872c1..4a18cf7 100644 --- a/platform/static/js/platform.js +++ b/platform/static/js/platform.js @@ -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(), diff --git a/platform/templates/base.html b/platform/templates/base.html index c090446..9ca5a5c 100644 --- a/platform/templates/base.html +++ b/platform/templates/base.html @@ -31,9 +31,6 @@ Application Sites 5 - - Architecture - All Containers diff --git a/platform/templates/pages/architecture.html b/platform/templates/pages/architecture.html deleted file mode 100644 index 63f1fa0..0000000 --- a/platform/templates/pages/architecture.html +++ /dev/null @@ -1,276 +0,0 @@ -{% extends "base.html" %} -{% block content %} - - - - - -
-
App Stacks
-
Containers
-
Running
-
Main Server
-
- -
-
-
Infrastructure Topology
- Loading… -
-
- Running - Stopped / missing - Dependency -
-
-
flowchart TB
-      loading["Loading architecture…"]
-    
-
-
- - - -
-
-
Stack Breakdown
-
Open Sites -
-
-
Loading…
-
-
- - -{% endblock %} diff --git a/platform/templates/pages/settings.html b/platform/templates/pages/settings.html index 9d56329..b74319b 100644 --- a/platform/templates/pages/settings.html +++ b/platform/templates/pages/settings.html @@ -5,6 +5,7 @@
+
diff --git a/platform/templates/pages/sites.html b/platform/templates/pages/sites.html index 3eb0c4b..48da735 100644 --- a/platform/templates/pages/sites.html +++ b/platform/templates/pages/sites.html @@ -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 @@
This platform
{{ platform_url }} +
+ SSL + {% if platform_ssl %} + Configured + {% else %} + Not configured + {% endif %} +
@@ -179,17 +190,22 @@ function esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } -function healthPill(health, containerRunning) { - if (!health) { - return containerRunning - ? 'Unchecked' - : 'Offline'; - } +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 'Checking…'; + } + 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 `${labels[st] || st}${extra}${code}`; + return `${labels[st] || st}${extra}${code}`; +} + +function sslHtml(configured) { + return configured + ? ' Configured' + : ' Not configured'; } function renderSiteCard(site) { @@ -209,13 +225,17 @@ function renderSiteCard(site) {

${esc(site.name)} ${esc(site.category)}

${esc(site.tagline)}

-
${healthPill(site.health, site.container_running)}
+
${healthPill(site.health)}
DOMAIN ${domainHtml}
+
+ SSL + ${sslHtml(site.ssl_configured)} +
URL @@ -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) {
PORT MAPPING
${esc(site.port_mapping || site.port + '→' + site.internal_port)}
DOMAIN
${site.has_domain ? esc(site.domain) : '— (IP only)'}
+
SSL
${site.ssl_configured ? 'Configured' : 'Not configured'}
IP FALLBACK
${esc(site.ip_url)}
HEALTH ENDPOINT
${esc(site.health_url)}
@@ -358,7 +381,7 @@ window.closeSiteModal = closeSiteModal; document.addEventListener('DOMContentLoaded', () => { loadSites(true); - setInterval(() => loadSites(false), 30000); + setInterval(() => loadSites(true), 60000); }); {% endblock %}