466 lines
25 KiB
HTML
466 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WEVAL Services Hub · Self-Hosted Open Source Stack</title>
|
|
<meta name="description" content="Hub d'accès unifié à tous les services open-source self-hosted WEVAL : Langfuse, Gitea, Mattermost, n8n, Twenty CRM, Listmonk, Grafana, Prometheus, Qdrant, Uptime Kuma, Plausible, SearXNG…">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@500;700&family=Playfair+Display:wght@400;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{
|
|
--bg-0:#05060a;--bg-1:#0a0c14;--bg-2:#11141f;--bg-3:#181c2b;--bg-card:#0e111c;
|
|
--border:#1f2436;--border-hover:#3a425f;
|
|
--text-0:#f1f5f9;--text-1:#cbd5e1;--text-2:#94a3b8;--text-3:#64748b;
|
|
--accent:#6366f1;--accent-hover:#818cf8;
|
|
--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--info:#06b6d4;
|
|
--gold:#f6d572;--mint:#5cdb95;--violet:#a78bfa;--coral:#ff6b6b;--cyan:#4ecdc4;
|
|
--shadow-lg:0 16px 48px rgba(99,102,241,.2);
|
|
--radius:14px;--radius-sm:10px;
|
|
--trans:.18s cubic-bezier(.4,0,.2,1);
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{
|
|
background:var(--bg-0);color:var(--text-0);font-family:"Inter",sans-serif;min-height:100vh;
|
|
background-image:
|
|
radial-gradient(ellipse at 15% 15%,rgba(99,102,241,.08) 0%,transparent 60%),
|
|
radial-gradient(ellipse at 85% 85%,rgba(6,182,212,.06) 0%,transparent 60%);
|
|
}
|
|
|
|
header.topbar{
|
|
display:flex;align-items:center;justify-content:space-between;
|
|
padding:14px 28px;border-bottom:1px solid var(--border);
|
|
background:rgba(10,12,20,.85);backdrop-filter:blur(24px);
|
|
position:sticky;top:0;z-index:100;padding-right:130px;
|
|
}
|
|
.brand{display:flex;align-items:center;gap:14px}
|
|
.brand-logo{
|
|
width:38px;height:38px;border-radius:10px;
|
|
background:linear-gradient(135deg,var(--accent),var(--violet));
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-weight:900;font-size:18px;color:#fff;
|
|
}
|
|
.brand-text{display:flex;flex-direction:column}
|
|
.brand-title{font-size:14px;font-weight:700;letter-spacing:.08em}
|
|
.brand-sub{font-size:10px;letter-spacing:.2em;color:var(--text-3);text-transform:uppercase;font-weight:600}
|
|
.health-bar{display:flex;gap:18px;align-items:center}
|
|
.h-item{display:flex;align-items:center;gap:7px;font-size:11px;color:var(--text-2);font-family:"JetBrains Mono",monospace}
|
|
.h-dot{width:7px;height:7px;border-radius:50%}
|
|
.h-dot.ok{background:var(--mint);box-shadow:0 0 8px var(--mint)}
|
|
.h-dot.warn{background:var(--warning);box-shadow:0 0 8px var(--warning)}
|
|
.h-dot.off{background:var(--danger);box-shadow:0 0 8px var(--danger)}
|
|
.h-val{color:var(--text-0);font-weight:700}
|
|
|
|
.page{max-width:1680px;margin:0 auto;padding:28px;display:flex;flex-direction:column;gap:24px}
|
|
.page-head{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:14px}
|
|
.page-title-wrap{display:flex;align-items:center;gap:18px}
|
|
.page-icon{
|
|
width:58px;height:58px;border-radius:14px;
|
|
background:linear-gradient(135deg,rgba(99,102,241,.2),rgba(6,182,212,.15));
|
|
border:1px solid rgba(99,102,241,.3);
|
|
display:flex;align-items:center;justify-content:center;font-size:28px;
|
|
}
|
|
.page-title{font-size:28px;font-weight:800;letter-spacing:-.02em}
|
|
.page-title .mono{font-family:"JetBrains Mono",monospace;color:var(--accent-hover);font-weight:700}
|
|
.page-sub{font-size:13px;color:var(--text-2);margin-top:4px;font-weight:500;max-width:820px;line-height:1.5}
|
|
.btn{padding:10px 16px;border-radius:10px;background:var(--bg-2);border:1px solid var(--border);color:var(--text-1);font-size:12px;font-weight:600;cursor:pointer;transition:var(--trans);display:inline-flex;align-items:center;gap:7px;text-decoration:none}
|
|
.btn:hover{background:var(--bg-3);border-color:var(--border-hover);color:var(--text-0)}
|
|
.btn-primary{background:linear-gradient(135deg,var(--accent),var(--violet));border:none;color:#fff}
|
|
|
|
.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px}
|
|
.kpi{background:linear-gradient(135deg,var(--bg-card),var(--bg-1));border:1px solid var(--border);border-radius:var(--radius);padding:18px;position:relative;overflow:hidden}
|
|
.kpi::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--accent),var(--violet))}
|
|
.kpi.mint::before{background:linear-gradient(90deg,var(--mint),var(--success))}
|
|
.kpi.gold::before{background:linear-gradient(90deg,var(--gold),var(--warning))}
|
|
.kpi.coral::before{background:linear-gradient(90deg,var(--coral),var(--danger))}
|
|
.kpi-lbl{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-3);font-weight:700;margin-bottom:8px}
|
|
.kpi-val{font-size:28px;font-weight:900;letter-spacing:-.03em;color:var(--text-0);font-family:"JetBrains Mono",monospace;line-height:1}
|
|
.kpi-sub{font-size:11px;color:var(--text-2);margin-top:4px;font-weight:500}
|
|
|
|
.section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
|
.section-head{
|
|
padding:18px 24px;border-bottom:1px solid var(--border);
|
|
display:flex;align-items:center;justify-content:space-between;
|
|
background:linear-gradient(180deg,rgba(99,102,241,.04),transparent);
|
|
}
|
|
.section-title{font-size:14px;font-weight:700;color:var(--text-0);display:flex;align-items:center;gap:10px}
|
|
.section-title::before{content:"";width:3px;height:18px;background:var(--accent);border-radius:2px}
|
|
.section-meta{font-size:11px;color:var(--text-3);font-family:"JetBrains Mono",monospace}
|
|
.section-body{padding:20px 24px}
|
|
|
|
.svc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
|
|
.svc-card{
|
|
background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);
|
|
padding:18px 20px;transition:var(--trans);position:relative;overflow:hidden;
|
|
display:flex;flex-direction:column;gap:10px;
|
|
}
|
|
.svc-card:hover{border-color:var(--border-hover);transform:translateY(-2px);box-shadow:0 12px 32px rgba(0,0,0,.4)}
|
|
.svc-card::before{
|
|
content:"";position:absolute;top:0;left:0;bottom:0;width:3px;background:var(--accent);
|
|
}
|
|
.svc-card.up::before{background:var(--mint)}
|
|
.svc-card.down::before{background:var(--danger)}
|
|
.svc-card.warn::before{background:var(--warning)}
|
|
|
|
.svc-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
|
|
.svc-name-wrap{display:flex;align-items:center;gap:10px;flex:1;min-width:0}
|
|
.svc-icon{
|
|
width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;
|
|
font-size:18px;font-weight:900;flex-shrink:0;
|
|
background:linear-gradient(135deg,rgba(99,102,241,.2),rgba(167,139,250,.15));
|
|
border:1px solid rgba(99,102,241,.3);
|
|
}
|
|
.svc-name{font-size:15px;font-weight:800;letter-spacing:-.01em;font-family:"Playfair Display",serif}
|
|
.svc-cat{font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-3);font-weight:700;font-family:"JetBrains Mono",monospace;margin-top:2px}
|
|
.svc-status{
|
|
font-size:9px;padding:3px 8px;border-radius:100px;font-weight:700;
|
|
text-transform:uppercase;letter-spacing:.08em;font-family:"JetBrains Mono",monospace;
|
|
background:rgba(16,185,129,.12);color:var(--mint);border:1px solid rgba(16,185,129,.3);
|
|
flex-shrink:0;
|
|
}
|
|
.svc-status.warn{background:rgba(246,213,114,.12);color:var(--gold);border-color:rgba(246,213,114,.3)}
|
|
.svc-status.off{background:rgba(239,68,68,.12);color:var(--danger);border-color:rgba(239,68,68,.3)}
|
|
.svc-desc{font-size:12px;color:var(--text-2);line-height:1.5}
|
|
|
|
.svc-creds{
|
|
background:var(--bg-1);border:1px solid var(--border);border-radius:8px;padding:10px 12px;
|
|
display:flex;flex-direction:column;gap:4px;
|
|
}
|
|
.cred-row{display:flex;justify-content:space-between;align-items:center;font-size:10px;font-family:"JetBrains Mono",monospace;gap:8px}
|
|
.cred-lbl{color:var(--text-3);min-width:65px;letter-spacing:.04em}
|
|
.cred-val{color:var(--text-1);font-weight:600;flex:1;text-align:right;word-break:break-all;cursor:pointer;transition:var(--trans)}
|
|
.cred-val:hover{color:var(--accent-hover)}
|
|
.cred-val.masked{filter:blur(4px);user-select:none}
|
|
.cred-val.masked:hover{filter:blur(0)}
|
|
|
|
.svc-actions{display:flex;gap:8px;margin-top:auto}
|
|
.svc-btn{
|
|
flex:1;padding:8px 12px;border-radius:8px;border:1px solid var(--border);
|
|
background:var(--bg-3);color:var(--text-1);font-size:11px;font-weight:700;
|
|
cursor:pointer;transition:var(--trans);display:flex;align-items:center;justify-content:center;gap:5px;
|
|
text-decoration:none;
|
|
}
|
|
.svc-btn:hover{background:var(--accent);color:#fff;border-color:var(--accent)}
|
|
.svc-btn.primary{background:linear-gradient(135deg,var(--accent),var(--violet));border:none;color:#fff}
|
|
.svc-btn.primary:hover{opacity:.9;transform:translateY(-1px)}
|
|
|
|
.svc-meta{display:flex;gap:10px;font-size:9px;color:var(--text-3);font-family:"JetBrains Mono",monospace;letter-spacing:.04em}
|
|
.svc-meta span{display:flex;align-items:center;gap:4px}
|
|
|
|
footer{text-align:center;padding:24px;color:var(--text-3);font-size:11px;font-family:"JetBrains Mono",monospace;border-top:1px solid var(--border);margin-top:24px}
|
|
footer a{color:var(--accent-hover);text-decoration:none}
|
|
|
|
.toast{position:fixed;bottom:24px;right:24px;background:var(--mint);color:#000;padding:12px 18px;border-radius:8px;font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(16,185,129,.4);z-index:9999;opacity:0;transition:.3s;pointer-events:none}
|
|
.toast.show{opacity:1}
|
|
|
|
@media (max-width:720px){
|
|
.page{padding:14px}
|
|
.page-head{flex-direction:column;align-items:flex-start}
|
|
.health-bar{display:none}
|
|
.kpi-row{grid-template-columns:repeat(2,1fr)}
|
|
}
|
|
|
|
/* ===== SSH_TUNNEL_ACTIVATED - internal services tunnel UI ===== */
|
|
.tunnel-box{
|
|
background:linear-gradient(135deg,rgba(246,213,114,.06),rgba(6,182,212,.04));
|
|
border:1px solid rgba(246,213,114,.25);border-radius:8px;padding:10px 12px;
|
|
display:none;flex-direction:column;gap:6px;margin-top:8px;
|
|
}
|
|
.tunnel-box.show{display:flex}
|
|
.tunnel-label{font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--gold);font-weight:700;font-family:"JetBrains Mono",monospace}
|
|
.tunnel-cmd{
|
|
background:var(--bg-0);border:1px solid var(--border);border-radius:6px;padding:8px 10px;
|
|
font-family:"JetBrains Mono",monospace;font-size:10px;color:var(--text-1);
|
|
word-break:break-all;cursor:pointer;transition:var(--trans);position:relative;
|
|
}
|
|
.tunnel-cmd:hover{background:var(--bg-1);border-color:var(--gold);color:var(--text-0)}
|
|
.tunnel-cmd::after{
|
|
content:"📋";position:absolute;top:6px;right:8px;font-size:10px;opacity:.5;
|
|
}
|
|
.tunnel-note{font-size:10px;color:var(--text-2);line-height:1.5}
|
|
.tunnel-note b{color:var(--cyan)}
|
|
.svc-btn.tunnel{
|
|
background:linear-gradient(135deg,var(--gold),var(--warning)) !important;
|
|
color:#000 !important;border:none !important;cursor:pointer !important;opacity:1 !important;
|
|
}
|
|
.svc-btn.tunnel:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(246,213,114,.3) !important}
|
|
/* ===== END SSH_TUNNEL_ACTIVATED ===== */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="topbar">
|
|
<div class="brand">
|
|
<div class="brand-logo">W</div>
|
|
<div class="brand-text">
|
|
<span class="brand-title">WEVAL Technology Platform</span>
|
|
<span class="brand-sub">Services Hub · Self-Hosted</span>
|
|
</div>
|
|
</div>
|
|
<div class="health-bar">
|
|
<div class="h-item"><span class="h-dot ok"></span>S204 LIVE</div>
|
|
<div class="h-item">Services · <span class="h-val" id="h-svc">—</span></div>
|
|
<div class="h-item">UP · <span class="h-val" id="h-up" style="color:var(--mint)">—</span></div>
|
|
<div class="h-item">Cost · <span class="h-val" style="color:var(--mint)">0 €</span></div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="page">
|
|
|
|
<div class="page-head">
|
|
<div class="page-title-wrap">
|
|
<div class="page-icon">🔓</div>
|
|
<div>
|
|
<h1 class="page-title">Services <span class="mono">Hub</span></h1>
|
|
<p class="page-sub">
|
|
Accès unifié à tous les services <b>open-source self-hosted</b> de l'infrastructure WEVAL :
|
|
observability (<b>Langfuse</b>, <b>Grafana</b>), code (<b>Gitea</b>), automation (<b>n8n</b>),
|
|
CRM (<b>Twenty</b>), email (<b>Listmonk</b>), chat (<b>Mattermost</b>), monitoring (<b>Uptime Kuma</b>, <b>Prometheus</b>),
|
|
analytics (<b>Plausible</b>), search (<b>SearXNG</b>), vectors (<b>Qdrant</b>).
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:10px">
|
|
<a href="/wevia-master.html" class="btn">💬 WEVIA Master</a>
|
|
<a href="/weval-technology-platform.html" class="btn btn-primary">🏛 WTP Hub</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kpi-row">
|
|
<div class="kpi mint">
|
|
<div class="kpi-lbl">📦 Services</div>
|
|
<div class="kpi-val" id="kpi-total">—</div>
|
|
<div class="kpi-sub">open-source self-hosted</div>
|
|
</div>
|
|
<div class="kpi">
|
|
<div class="kpi-lbl">✅ UP</div>
|
|
<div class="kpi-val" id="kpi-up" style="color:var(--mint)">—</div>
|
|
<div class="kpi-sub">opérationnels</div>
|
|
</div>
|
|
<div class="kpi gold">
|
|
<div class="kpi-lbl">⚠ Standby</div>
|
|
<div class="kpi-val" id="kpi-standby">—</div>
|
|
<div class="kpi-sub">reachable mais ⚠ check</div>
|
|
</div>
|
|
<div class="kpi coral">
|
|
<div class="kpi-lbl">⚪ Internal</div>
|
|
<div class="kpi-val" id="kpi-internal">—</div>
|
|
<div class="kpi-sub">port local seulement</div>
|
|
</div>
|
|
<div class="kpi mint">
|
|
<div class="kpi-lbl">💰 Coût</div>
|
|
<div class="kpi-val" style="font-size:24px">0 €</div>
|
|
<div class="kpi-sub">tout open-source</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI / OBSERVABILITY -->
|
|
<div class="section">
|
|
<div class="section-head">
|
|
<div class="section-title">🧠 AI & Observability</div>
|
|
<div class="section-meta">LLM tracing · vectors · monitoring</div>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="svc-grid" id="grid-ai"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CODE / DEV -->
|
|
<div class="section">
|
|
<div class="section-head">
|
|
<div class="section-title">💻 Code & Automation</div>
|
|
<div class="section-meta">git · workflows · ci</div>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="svc-grid" id="grid-code"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- BUSINESS -->
|
|
<div class="section">
|
|
<div class="section-head">
|
|
<div class="section-title">📊 Business & Marketing</div>
|
|
<div class="section-meta">CRM · email · analytics</div>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="svc-grid" id="grid-biz"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- COMMS -->
|
|
<div class="section">
|
|
<div class="section-head">
|
|
<div class="section-title">💬 Communication</div>
|
|
<div class="section-meta">chat · search</div>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="svc-grid" id="grid-comms"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MONITORING -->
|
|
<div class="section">
|
|
<div class="section-head">
|
|
<div class="section-title">📈 Monitoring & Infra</div>
|
|
<div class="section-meta">uptime · metrics · logs</div>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="svc-grid" id="grid-mon"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
WEVAL Services Hub · v1.0 · 17 services open-source · Cost 0 € ·
|
|
<a href="/wevia-master.html">💬 WEVIA chat</a> ·
|
|
<a href="/ia-cascade-mechanics.html">⚡ IA Cascade</a> ·
|
|
<a href="/release-train-dashboard.html">🚂 Release Train</a>
|
|
</footer>
|
|
|
|
</div>
|
|
|
|
<div id="toast" class="toast"></div>
|
|
|
|
<script>
|
|
const SERVICES = {
|
|
ai: [
|
|
{id:"langfuse",name:"Langfuse",icon:"L",cat:"LLM Observability",desc:"Tracing LLM calls + datasets + prompt management. WEVIA Master tracking ready.",url:"https://langfuse.weval-consulting.com",internal:"localhost:3001",docker:"langfuse",version:"v2.95.11",email:"yacine@weval-consulting.com",pwd:"WevalLangfuse2026!",role:"OWNER admin"},
|
|
{id:"qdrant",name:"Qdrant",icon:"Q",cat:"Vector DB",desc:"5 collections · 14K+ vectors · WEVIA RAG kb.",url:"http://localhost:6333",internal:"127.0.0.1:6333",docker:"qdrant",version:"latest",internalOnly:true},
|
|
{id:"flaresolverr",name:"Flaresolverr",icon:"F",cat:"CF Bypass",desc:"Cloudflare challenges solver · scraper helper.",url:"http://localhost:8191",internal:"127.0.0.1:8191",docker:"flaresolverr-w274",version:"latest",internalOnly:true},
|
|
],
|
|
code: [
|
|
{id:"gitea",name:"Gitea",icon:"G",cat:"Git server",desc:"50+ repos privés · push synchro avec GitHub origin.",url:"https://git.weval-consulting.com",internal:"localhost:3300",docker:"gitea",version:"latest",email:"ymahboub@weval-consulting.com",user:"yanis",pwd:"voir vault",role:"admin",statusOverride:"503 (CF/auth)"},
|
|
{id:"n8n",name:"n8n",icon:"N",cat:"Workflows",desc:"Automation workflows · STOPPED (CPU killer doctrine).",url:"https://n8n.weval-consulting.com",internal:"localhost:5678",docker:"n8n-docker-n8n-1",version:"latest",statusOverride:"STANDBY (off)"},
|
|
],
|
|
biz: [
|
|
{id:"twenty",name:"Twenty CRM",icon:"T",cat:"CRM",desc:"1006 leads · 402 emails · 638 LinkedIn · sequences 4x/jour.",url:"https://crm.weval-consulting.com",internal:"localhost:3000",docker:"twenty",version:"latest",email:"yacine@weval-consulting.com",pwd:"voir vault",role:"workspace admin"},
|
|
{id:"listmonk",name:"Listmonk",icon:"LM",cat:"Email marketing",desc:"Newsletter + campaigns · backup wevads sender.",url:"http://localhost:9000",internal:"127.0.0.1:9000",docker:"listmonk",version:"latest",user:"admin",pwd:"admin123",role:"admin",internalOnly:true},
|
|
{id:"plausible",name:"Plausible",icon:"P",cat:"Analytics",desc:"Web analytics privacy-friendly · weval-consulting.com tracking.",url:"https://analytics.weval-consulting.com",internal:"localhost:8000",docker:"plausible-...",version:"latest",statusOverride:"502 (config)"},
|
|
],
|
|
comms: [
|
|
{id:"mattermost",name:"Mattermost",icon:"M",cat:"Team chat",desc:"Slack alternative self-hosted · team collaboration.",url:"https://mm.weval-consulting.com",internal:"localhost:8065",docker:"mattermost-docker-mattermost-1",version:"team-edition",email:"ymahboub@weval-consulting.com",user:"yacine",pwd:"voir vault",role:"system_admin"},
|
|
{id:"searxng",name:"SearXNG",icon:"S",cat:"Meta-search",desc:"Privacy meta-search engine · multi-providers aggregation.",url:"http://localhost:8080",internal:"127.0.0.1:8080",docker:"searxng",version:"latest",internalOnly:true},
|
|
],
|
|
mon: [
|
|
{id:"uptime",name:"Uptime Kuma",icon:"U",cat:"Uptime monitor",desc:"Status pages + alerts · 17 services monitored.",url:"https://monitor.weval-consulting.com",internal:"localhost:3001",docker:"uptime-kuma",version:"latest",statusOverride:"503 (CF)"},
|
|
{id:"prometheus",name:"Prometheus",icon:"PM",cat:"Metrics TSDB",desc:"Time series · scrape exporters · alertmanager.",url:"http://localhost:9090",internal:"127.0.0.1:9090",docker:"prometheus",version:"latest",internalOnly:true},
|
|
{id:"loki",name:"Loki",icon:"LK",cat:"Log aggregation",desc:"Grafana logs · indexed · WEVIA logs ingest.",url:"http://localhost:3100",internal:"127.0.0.1:3100",docker:"loki",version:"latest",internalOnly:true},
|
|
{id:"node-exporter",name:"Node Exporter",icon:"NE",cat:"Host metrics",desc:"CPU/RAM/disk/net · Prometheus scraper.",url:"http://localhost:9100",internal:"127.0.0.1:9100",docker:"node-exporter",version:"latest",internalOnly:true},
|
|
]
|
|
};
|
|
|
|
function svgGear(){return `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`}
|
|
function svgOpen(){return `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`}
|
|
|
|
async function checkStatus(svc){
|
|
if(svc.statusOverride) return svc.statusOverride.includes("503") || svc.statusOverride.includes("502") || svc.statusOverride.includes("off") || svc.statusOverride.includes("STANDBY") ? "warn" : "up";
|
|
if(svc.internalOnly) return "internal";
|
|
try{
|
|
const r = await fetch(svc.url, {method:"HEAD",mode:"no-cors",cache:"no-store"});
|
|
return "up";
|
|
}catch(e){
|
|
return "warn";
|
|
}
|
|
}
|
|
|
|
function escapeHtml(s){return String(s||"").replace(/[<>&"]/g,c=>({'<':'<','>':'>','&':'&','"':'"'}[c]))}
|
|
|
|
function renderCard(svc, status){
|
|
const stCls = status==="up"?"up":status==="internal"?"warn":"warn";
|
|
const stTxt = svc.statusOverride || (status==="internal"?"INTERNAL":status==="up"?"LIVE":"STANDBY");
|
|
const stClsStatus = status==="up"?"":status==="internal"?"warn":"warn";
|
|
|
|
const credsHtml = (svc.email||svc.user||svc.pwd) ? `
|
|
<div class="svc-creds">
|
|
${svc.email?`<div class="cred-row"><span class="cred-lbl">EMAIL</span><span class="cred-val" onclick="copyVal(this,'${escapeHtml(svc.email)}')">${escapeHtml(svc.email)}</span></div>`:""}
|
|
${svc.user?`<div class="cred-row"><span class="cred-lbl">USER</span><span class="cred-val" onclick="copyVal(this,'${escapeHtml(svc.user)}')">${escapeHtml(svc.user)}</span></div>`:""}
|
|
${svc.pwd?`<div class="cred-row"><span class="cred-lbl">PASSWORD</span><span class="cred-val masked" onclick="copyVal(this,'${escapeHtml(svc.pwd)}')">${escapeHtml(svc.pwd)}</span></div>`:""}
|
|
${svc.role?`<div class="cred-row"><span class="cred-lbl">ROLE</span><span class="cred-val" style="color:var(--mint)">${escapeHtml(svc.role)}</span></div>`:""}
|
|
</div>
|
|
` : "";
|
|
|
|
const tunnelPort = svc.internal ? svc.internal.split(":")[1] : "";
|
|
const tunnelId = "tunnel-"+svc.id;
|
|
const openBtn = svc.internalOnly
|
|
? `<button onclick="toggleTunnel('${tunnelId}')" class="svc-btn tunnel">🔐 SSH tunnel</button>`
|
|
: `<a href="${escapeHtml(svc.url)}" target="_blank" class="svc-btn primary">${svgOpen()} OPEN</a>`;
|
|
|
|
const tunnelHtml = svc.internalOnly ? `
|
|
<div class="tunnel-box" id="${tunnelId}">
|
|
<div class="tunnel-label">🔐 SSH Tunnel · laptop local</div>
|
|
<div class="tunnel-cmd" onclick="copyVal(this,'ssh -N -L ${tunnelPort}:127.0.0.1:${tunnelPort} root@204.168.152.13 -p 49222')">ssh -N -L ${tunnelPort}:127.0.0.1:${tunnelPort} root@204.168.152.13 -p 49222</div>
|
|
<div class="tunnel-note">Puis ouvrir <b>http://localhost:${tunnelPort}</b> dans ton browser. Port <b>49222</b> (SSH key Yacine yace@LAPTOP-VE75QUHF déjà autorisée).</div>
|
|
</div>
|
|
` : "";
|
|
|
|
|
|
return `
|
|
<div class="svc-card ${stCls}">
|
|
<div class="svc-head">
|
|
<div class="svc-name-wrap">
|
|
<div class="svc-icon">${escapeHtml(svc.icon)}</div>
|
|
<div style="min-width:0">
|
|
<div class="svc-name">${escapeHtml(svc.name)}</div>
|
|
<div class="svc-cat">${escapeHtml(svc.cat)}</div>
|
|
</div>
|
|
</div>
|
|
<div class="svc-status ${stClsStatus}">${escapeHtml(stTxt)}</div>
|
|
</div>
|
|
<div class="svc-desc">${escapeHtml(svc.desc)}</div>
|
|
${credsHtml}
|
|
<div class="svc-meta">
|
|
<span>🐳 ${escapeHtml(svc.docker||'-')}</span>
|
|
<span>🔌 ${escapeHtml(svc.internal||'-')}</span>
|
|
${svc.version?`<span>v${escapeHtml(svc.version)}</span>`:""}
|
|
</div>
|
|
<div class="svc-actions">
|
|
${openBtn}
|
|
</div>
|
|
${tunnelHtml}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function copyVal(el, val){
|
|
navigator.clipboard.writeText(val).then(()=>{
|
|
const t = document.getElementById("toast");
|
|
t.textContent = "✓ Copié : " + (val.length>30?val.slice(0,30)+"…":val);
|
|
t.classList.add("show");
|
|
setTimeout(()=>t.classList.remove("show"), 2200);
|
|
if(el.classList.contains("masked")){el.classList.remove("masked");setTimeout(()=>el.classList.add("masked"),3000)}
|
|
});
|
|
}
|
|
|
|
|
|
function toggleTunnel(id){
|
|
const el = document.getElementById(id);
|
|
if(!el) return;
|
|
el.classList.toggle("show");
|
|
}
|
|
|
|
async function init(){
|
|
let totalSvc = 0, upCount = 0, standby = 0, internal = 0;
|
|
for(const [grp, svcs] of Object.entries(SERVICES)){
|
|
const target = document.getElementById("grid-"+grp);
|
|
if(!target) continue;
|
|
const html = await Promise.all(svcs.map(async svc => {
|
|
const status = await checkStatus(svc);
|
|
totalSvc++;
|
|
if(status === "up") upCount++;
|
|
else if(status === "internal") internal++;
|
|
else standby++;
|
|
return renderCard(svc, status);
|
|
}));
|
|
target.innerHTML = html.join("");
|
|
}
|
|
document.getElementById("kpi-total").textContent = totalSvc;
|
|
document.getElementById("kpi-up").textContent = upCount;
|
|
document.getElementById("kpi-standby").textContent = standby;
|
|
document.getElementById("kpi-internal").textContent = internal;
|
|
document.getElementById("h-svc").textContent = totalSvc;
|
|
document.getElementById("h-up").textContent = upCount + "/" + totalSvc;
|
|
}
|
|
init();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|