Redesign admin Infra page: full-screen 4-column layout with Docker stats
Complete rewrite of /admin/infra into 4 columns: - Col 1 (Serveur): CPU, RAM, Disk, System, Services (Caddy, Docker, SSL cert) - Col 2 (Containers): All Docker containers with CPU%, RAM, state via Docker API - Col 3 (Redis): Global stats + per-DB (Messenger, Sessions, Cache) - Col 4 (PostgreSQL): Instance stats + PgBouncer pools/stats Extract all infra logic into InfraService. Mount Docker socket (read-only) in PHP container for container stats. Check SSL cert expiry and Caddy status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,296 +3,277 @@
|
||||
{% block title %}Infrastructure{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page mb-8">Infrastructure</h1>
|
||||
<div class="grid grid-cols-1 xl:grid-cols-4 gap-4 min-h-[calc(100vh-80px)]">
|
||||
|
||||
{# ── COL 1: Serveur ────────────────────────────────── #}
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-lg font-black uppercase tracking-tighter italic heading-page">Serveur</h2>
|
||||
|
||||
{# Systeme #}
|
||||
<div class="admin-card">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-3">Systeme</h3>
|
||||
{{ _self.r('Hostname', server.hostname) }}
|
||||
{{ _self.r('OS', server.os) }}
|
||||
{{ _self.r('Uptime', server.uptime) }}
|
||||
</div>
|
||||
|
||||
{# Server #}
|
||||
<h2 class="text-xl font-black uppercase tracking-tighter italic heading-page mb-4">Serveur</h2>
|
||||
<div class="flex flex-wrap gap-6 mb-8">
|
||||
{# CPU #}
|
||||
<div class="flex-1 min-w-[280px]">
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">CPU</h2>
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('Modele', server.cpu_model) }}
|
||||
{{ _self.row('Coeurs', server.cpu_cores) }}
|
||||
{{ _self.row('Load 1m / 5m / 15m', server.load_1m ~ ' / ' ~ server.load_5m ~ ' / ' ~ server.load_15m) }}
|
||||
{{ _self.row_color('Charge CPU', server.load_percent != '?' ? server.load_percent ~ '%' : '?', server.load_percent != '?' and server.load_percent < 70 ? 'green' : (server.load_percent == '?' ? 'gray' : (server.load_percent < 90 ? 'yellow' : 'red'))) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-3">CPU</h3>
|
||||
{{ _self.r('Modele', server.cpu_model) }}
|
||||
{{ _self.r('Coeurs', server.cpu_cores) }}
|
||||
{{ _self.r('Load 1/5/15m', server.load_1m ~ ' / ' ~ server.load_5m ~ ' / ' ~ server.load_15m) }}
|
||||
{{ _self.c('Charge', server.load_percent != '?' ? server.load_percent ~ '%' : '?', _self.pct_color(server.load_percent)) }}
|
||||
</div>
|
||||
|
||||
{# RAM #}
|
||||
<div class="flex-1 min-w-[280px]">
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">RAM</h2>
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('Total', server.ram_total) }}
|
||||
{{ _self.row('Utilisee', server.ram_used) }}
|
||||
{{ _self.row('Disponible', server.ram_free) }}
|
||||
{{ _self.row_color('Utilisation', server.ram_usage_percent != '?' ? server.ram_usage_percent ~ '%' : '?', server.ram_usage_percent != '?' and server.ram_usage_percent < 70 ? 'green' : (server.ram_usage_percent == '?' ? 'gray' : (server.ram_usage_percent < 90 ? 'yellow' : 'red'))) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-3">RAM</h3>
|
||||
{{ _self.r('Total', server.ram_total) }}
|
||||
{{ _self.r('Utilisee', server.ram_used) }}
|
||||
{{ _self.r('Disponible', server.ram_free) }}
|
||||
{{ _self.c('Utilisation', server.ram_percent != '?' ? server.ram_percent ~ '%' : '?', _self.pct_color(server.ram_percent)) }}
|
||||
</div>
|
||||
|
||||
{# Disk #}
|
||||
<div class="flex-1 min-w-[280px]">
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Disque</h2>
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('Total', server.disk_total) }}
|
||||
{{ _self.row('Utilise', server.disk_used) }}
|
||||
{{ _self.row('Libre', server.disk_free) }}
|
||||
{{ _self.row_color('Utilisation', server.disk_usage_percent != '?' ? server.disk_usage_percent ~ '%' : '?', server.disk_usage_percent != '?' and server.disk_usage_percent < 70 ? 'green' : (server.disk_usage_percent == '?' ? 'gray' : (server.disk_usage_percent < 90 ? 'yellow' : 'red'))) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-3">Disque</h3>
|
||||
{{ _self.r('Total', server.disk_total) }}
|
||||
{{ _self.r('Utilise', server.disk_used) }}
|
||||
{{ _self.r('Libre', server.disk_free) }}
|
||||
{{ _self.c('Utilisation', server.disk_percent != '?' ? server.disk_percent ~ '%' : '?', _self.pct_color(server.disk_percent)) }}
|
||||
</div>
|
||||
|
||||
{# System #}
|
||||
<div class="flex-1 min-w-[280px]">
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Systeme</h2>
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('Hostname', server.hostname) }}
|
||||
{{ _self.row('OS', server.os) }}
|
||||
{{ _self.row('Uptime', server.uptime) }}
|
||||
</div>
|
||||
{# Services #}
|
||||
<div class="admin-card">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-3">Services</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
{{ _self.svc('Caddy', server.caddy.status, server.caddy.info) }}
|
||||
{{ _self.svc('Docker', server.docker.status, server.docker.info) }}
|
||||
{{ _self.svc_ssl(server.ssl) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# PHP Containers #}
|
||||
<h2 class="text-xl font-black uppercase tracking-tighter italic heading-page mb-4">PHP</h2>
|
||||
<div class="flex flex-wrap gap-6 mb-8">
|
||||
{% for container in php_containers %}
|
||||
<div class="flex-1 min-w-[300px]">
|
||||
{# ── COL 2: Containers ─────────────────────────────── #}
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-lg font-black uppercase tracking-tighter italic heading-page">Containers</h2>
|
||||
|
||||
{% if containers|length > 0 %}
|
||||
{% for c in containers %}
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest">{{ container.hostname }}</h2>
|
||||
<span class="admin-badge-green text-xs font-black uppercase">PHP {{ container.php_version }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('SAPI', container.sapi) }}
|
||||
{{ _self.row('Uptime', container.uptime) }}
|
||||
{{ _self.row('CPU Cores', container.cpu_cores) }}
|
||||
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">CPU</p>
|
||||
{{ _self.row_color('Utilisation', container.cpu_usage_percent, container.cpu_usage_percent != 'N/A' and container.cpu_usage_percent|replace({'%': ''})|number_format < 70 ? 'green' : (container.cpu_usage_percent == 'N/A' ? 'gray' : (container.cpu_usage_percent|replace({'%': ''})|number_format < 90 ? 'yellow' : 'red'))) }}
|
||||
{{ _self.row('Load 1m / 5m / 15m', container.load_1m ~ ' / ' ~ container.load_5m ~ ' / ' ~ container.load_15m) }}
|
||||
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Memoire</p>
|
||||
{{ _self.row('Utilisee', container.ram_used) }}
|
||||
{{ _self.row('Total', container.ram_total) }}
|
||||
{{ _self.row('Libre', container.ram_free) }}
|
||||
{{ _self.row_color('Utilisation', container.ram_usage_percent ~ (container.ram_usage_percent != '?' ? '%' : ''), container.ram_usage_percent != '?' and container.ram_usage_percent < 70 ? 'green' : (container.ram_usage_percent == '?' ? 'gray' : (container.ram_usage_percent < 90 ? 'yellow' : 'red'))) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# PostgreSQL & PgBouncer #}
|
||||
<h2 class="text-xl font-black uppercase tracking-tighter italic heading-page mb-4">PostgreSQL & PgBouncer</h2>
|
||||
<div class="flex flex-wrap gap-6 mb-8">
|
||||
{# PostgreSQL #}
|
||||
<div class="flex-1 min-w-[350px]">
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest">PostgreSQL</h2>
|
||||
{% if postgres.connected %}
|
||||
<span class="admin-badge-green text-xs font-black uppercase">Connecte</span>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest">{{ c.name }}</h3>
|
||||
{% if c.state == 'running' %}
|
||||
<span class="admin-badge-green text-[9px] font-black uppercase">{{ c.state }}</span>
|
||||
{% else %}
|
||||
<span class="admin-badge-red text-xs font-black uppercase">Deconnecte</span>
|
||||
<span class="admin-badge-red text-[9px] font-black uppercase">{{ c.state }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if postgres.connected %}
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('Version', postgres.version) }}
|
||||
{{ _self.row('Uptime', postgres.uptime) }}
|
||||
{{ _self.row('Taille BDD', postgres.db_size) }}
|
||||
{{ _self.row('Tables', postgres.table_count) }}
|
||||
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Connexions</p>
|
||||
{{ _self.row('Actives', postgres.active_connections) }}
|
||||
{{ _self.row('Total', postgres.total_connections ~ ' / ' ~ postgres.max_connections) }}
|
||||
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Performance</p>
|
||||
{{ _self.row_color('Cache Hit Ratio', postgres.cache_hit_ratio, postgres.cache_hit_ratio != 'N/A' and postgres.cache_hit_ratio|replace({'%': ''})|number_format > 95 ? 'green' : (postgres.cache_hit_ratio == 'N/A' ? 'gray' : 'yellow')) }}
|
||||
{{ _self.row_color('Dead Tuples', postgres.dead_tuples|number_format(0, '.', ' '), postgres.dead_tuples < 10000 ? 'green' : (postgres.dead_tuples < 100000 ? 'yellow' : 'red')) }}
|
||||
<p class="text-[10px] text-gray-400 mb-2">{{ c.image }} — {{ c.status }}</p>
|
||||
<div class="grid grid-cols-3 gap-2 text-center">
|
||||
<div>
|
||||
<p class="text-[9px] font-black uppercase text-gray-400">CPU</p>
|
||||
<p class="text-sm font-black {{ _self.pct_class(c.cpu_percent|replace({'%': ''})) }}">{{ c.cpu_percent }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[9px] font-black uppercase text-gray-400">RAM</p>
|
||||
<p class="text-sm font-black">{{ c.ram_used }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[9px] font-black uppercase text-gray-400">RAM %</p>
|
||||
<p class="text-sm font-black {{ _self.pct_class(c.ram_percent|replace({'%': '', 'N/A': '0'})) }}">{{ c.ram_percent }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-red-500 font-bold">{{ postgres.error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# PgBouncer Pools #}
|
||||
{% if pgbouncer.connected %}
|
||||
<div class="flex-1 min-w-[350px]">
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">PgBouncer — Pools</h2>
|
||||
{% if pgbouncer.pools|length > 0 %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="admin-table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">Database</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Mode</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Cl. actifs</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Cl. attente</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Sv. actifs</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Sv. idle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pool in pgbouncer.pools %}
|
||||
<tr>
|
||||
<td class="font-black text-sm">{{ pool.database }}</td>
|
||||
<td class="text-center"><span class="admin-badge-green text-xs font-black uppercase">{{ pool.pool_mode }}</span></td>
|
||||
<td class="text-center font-black">{{ pool.cl_active }}</td>
|
||||
<td class="text-center font-black {% if pool.cl_waiting > 0 %}text-yellow-600{% endif %}">{{ pool.cl_waiting }}</td>
|
||||
<td class="text-center font-black">{{ pool.sv_active }}</td>
|
||||
<td class="text-center font-black text-gray-400">{{ pool.sv_idle }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-400">Aucun pool actif.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# PgBouncer Stats #}
|
||||
<div class="flex-1 min-w-[350px]">
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">PgBouncer — Stats</h2>
|
||||
{% if pgbouncer.stats|length > 0 %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="admin-table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">Database</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Xacts</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Queries</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Avg xact</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Avg query</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stat in pgbouncer.stats %}
|
||||
<tr>
|
||||
<td class="font-black text-sm">{{ stat.database }}</td>
|
||||
<td class="text-right text-sm font-bold">{{ stat.total_xact_count|number_format(0, '.', ' ') }}</td>
|
||||
<td class="text-right text-sm font-bold">{{ stat.total_query_count|number_format(0, '.', ' ') }}</td>
|
||||
<td class="text-right text-sm {% if stat.avg_xact_time > 100000 %}text-red-600{% elseif stat.avg_xact_time > 10000 %}text-yellow-600{% else %}text-green-600{% endif %} font-bold">{{ stat.avg_xact_time|number_format(0, '.', ' ') }}us</td>
|
||||
<td class="text-right text-sm {% if stat.avg_query_time > 50000 %}text-red-600{% elseif stat.avg_query_time > 5000 %}text-yellow-600{% else %}text-green-600{% endif %} font-bold">{{ stat.avg_query_time|number_format(0, '.', ' ') }}us</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-400">Aucune stat disponible.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="flex-1 min-w-[350px]">
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest">PgBouncer</h2>
|
||||
<span class="admin-badge-red text-xs font-black uppercase">Deconnecte</span>
|
||||
</div>
|
||||
<p class="text-sm text-red-500 font-bold">{{ pgbouncer.error }}</p>
|
||||
<p class="text-sm text-gray-400">Docker socket inaccessible.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Redis #}
|
||||
<h2 class="text-xl font-black uppercase tracking-tighter italic heading-page mb-4">Redis</h2>
|
||||
<div class="flex flex-wrap gap-6 mb-8">
|
||||
{# ── COL 3: Redis ──────────────────────────────────── #}
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-lg font-black uppercase tracking-tighter italic heading-page">Redis</h2>
|
||||
|
||||
{# Global #}
|
||||
<div class="flex-1 min-w-[350px]">
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest">Global</h2>
|
||||
{% if redis_global.connected %}
|
||||
<span class="admin-badge-green text-xs font-black uppercase">Connecte</span>
|
||||
{% else %}
|
||||
<span class="admin-badge-red text-xs font-black uppercase">Deconnecte</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400">Global</h3>
|
||||
{% if redis_global.connected %}
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('Version', redis_global.version) }}
|
||||
{{ _self.row('Uptime', redis_global.uptime_days ~ ' jours') }}
|
||||
{{ _self.row('Role', redis_global.role) }}
|
||||
{{ _self.row('Clients connectes', redis_global.connected_clients) }}
|
||||
{{ _self.row('Ops/sec', redis_global.instantaneous_ops_per_sec) }}
|
||||
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Memoire</p>
|
||||
{{ _self.row('Utilisee', redis_global.used_memory_human) }}
|
||||
{{ _self.row('Pic', redis_global.used_memory_peak_human) }}
|
||||
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Performance</p>
|
||||
{{ _self.row('Commandes', redis_global.total_commands_processed|number_format(0, '.', ' ')) }}
|
||||
{{ _self.row('Hits / Misses', redis_global.keyspace_hits|number_format(0, '.', ' ') ~ ' / ' ~ redis_global.keyspace_misses|number_format(0, '.', ' ')) }}
|
||||
{{ _self.row_color('Hit Rate', redis_global.hit_rate, redis_global.hit_rate != 'N/A' and redis_global.hit_rate|replace({'%': ''})|number_format > 80 ? 'green' : (redis_global.hit_rate == 'N/A' ? 'gray' : 'red')) }}
|
||||
{{ _self.row_color('Cles evictees', redis_global.evicted_keys, redis_global.evicted_keys == '0' ? 'green' : 'red') }}
|
||||
</div>
|
||||
<span class="admin-badge-green text-[9px] font-black uppercase">v{{ redis_global.version }}</span>
|
||||
{% else %}
|
||||
<p class="text-sm text-red-500 font-bold">{{ redis_global.error }}</p>
|
||||
<span class="admin-badge-red text-[9px] font-black uppercase">Deconnecte</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if redis_global.connected %}
|
||||
{{ _self.r('Uptime', redis_global.uptime_days ~ 'j') }}
|
||||
{{ _self.r('Role', redis_global.role) }}
|
||||
{{ _self.r('Clients', redis_global.connected_clients) }}
|
||||
{{ _self.r('Ops/sec', redis_global.ops_per_sec) }}
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
{{ _self.r('Memoire', redis_global.used_memory_human ~ ' / pic ' ~ redis_global.used_memory_peak_human) }}
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
{{ _self.r('Commandes', redis_global.total_commands_processed|number_format(0, '.', ' ')) }}
|
||||
{{ _self.r('Hits / Misses', redis_global.keyspace_hits|number_format(0, '.', ' ') ~ ' / ' ~ redis_global.keyspace_misses|number_format(0, '.', ' ')) }}
|
||||
{{ _self.c('Hit Rate', redis_global.hit_rate, redis_global.hit_rate != 'N/A' and redis_global.hit_rate|replace({'%': ''})|number_format > 80 ? 'green' : (redis_global.hit_rate == 'N/A' ? 'gray' : 'red')) }}
|
||||
{{ _self.c('Evictees', redis_global.evicted_keys, redis_global.evicted_keys == '0' ? 'green' : 'red') }}
|
||||
{% else %}
|
||||
<p class="text-sm text-red-500 font-bold">{{ redis_global.error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Per-DB cards #}
|
||||
{# Per-DB #}
|
||||
{% for db in redis_dbs %}
|
||||
<div class="flex-1 min-w-[220px]">
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest">{{ db.label }}</h2>
|
||||
{% if db.connected %}
|
||||
<span class="admin-badge-green text-xs font-black uppercase">{{ db.db }}</span>
|
||||
{% else %}
|
||||
<span class="admin-badge-red text-xs font-black uppercase">Erreur</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400">{{ db.label }}</h3>
|
||||
{% if db.connected %}
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('Cles', db.keys|number_format(0, '.', ' ')) }}
|
||||
{{ _self.row('TTL moyen', db.avg_ttl) }}
|
||||
</div>
|
||||
<span class="admin-badge-green text-[9px] font-black uppercase">{{ db.db }}</span>
|
||||
{% else %}
|
||||
<p class="text-sm text-red-500 font-bold">{{ db.error }}</p>
|
||||
<span class="admin-badge-red text-[9px] font-black uppercase">Erreur</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if db.connected %}
|
||||
{{ _self.r('Cles', db.keys|number_format(0, '.', ' ')) }}
|
||||
{{ _self.r('TTL moyen', db.avg_ttl) }}
|
||||
{% else %}
|
||||
<p class="text-xs text-red-500 font-bold">{{ db.error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# ── COL 4: PostgreSQL & PgBouncer ─────────────────── #}
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-lg font-black uppercase tracking-tighter italic heading-page">PostgreSQL</h2>
|
||||
|
||||
{# PostgreSQL #}
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400">Instance</h3>
|
||||
{% if postgres.connected %}
|
||||
<span class="admin-badge-green text-[9px] font-black uppercase">v{{ postgres.version }}</span>
|
||||
{% else %}
|
||||
<span class="admin-badge-red text-[9px] font-black uppercase">Deconnecte</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if postgres.connected %}
|
||||
{{ _self.r('Uptime', postgres.uptime) }}
|
||||
{{ _self.r('Taille BDD', postgres.db_size) }}
|
||||
{{ _self.r('Tables', postgres.table_count) }}
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
{{ _self.r('Connexions actives', postgres.active_connections) }}
|
||||
{{ _self.r('Connexions total', postgres.total_connections ~ ' / ' ~ postgres.max_connections) }}
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
{{ _self.c('Cache Hit', postgres.cache_hit_ratio, postgres.cache_hit_ratio != 'N/A' and postgres.cache_hit_ratio|replace({'%': ''})|number_format > 95 ? 'green' : (postgres.cache_hit_ratio == 'N/A' ? 'gray' : 'yellow')) }}
|
||||
{{ _self.c('Dead Tuples', postgres.dead_tuples|number_format(0, '.', ' '), postgres.dead_tuples < 10000 ? 'green' : (postgres.dead_tuples < 100000 ? 'yellow' : 'red')) }}
|
||||
{% else %}
|
||||
<p class="text-sm text-red-500 font-bold">{{ postgres.error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# PgBouncer #}
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-[10px] font-black uppercase tracking-widest text-gray-400">PgBouncer</h3>
|
||||
{% if pgbouncer.connected %}
|
||||
<span class="admin-badge-green text-[9px] font-black uppercase">Connecte</span>
|
||||
{% else %}
|
||||
<span class="admin-badge-red text-[9px] font-black uppercase">Deconnecte</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if pgbouncer.connected %}
|
||||
{% if pgbouncer.pools|length > 0 %}
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2">Pools</p>
|
||||
{% for pool in pgbouncer.pools %}
|
||||
<div class="bg-gray-50 p-2 mb-2 border border-gray-200">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-black">{{ pool.database }}</span>
|
||||
<span class="text-[9px] font-bold text-gray-400 uppercase">{{ pool.pool_mode }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-1 text-[11px]">
|
||||
{{ _self.r('Cl. actifs', pool.cl_active) }}
|
||||
<div class="flex justify-between"><span class="text-gray-500 font-bold">Cl. attente</span><span class="font-black {% if pool.cl_waiting > 0 %}text-yellow-600{% endif %}">{{ pool.cl_waiting }}</span></div>
|
||||
{{ _self.r('Sv. actifs', pool.sv_active) }}
|
||||
{{ _self.r('Sv. idle', pool.sv_idle) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-xs text-gray-400 mb-2">Aucun pool actif.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if pgbouncer.stats|length > 0 %}
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2 mt-3">Stats</p>
|
||||
{% for stat in pgbouncer.stats %}
|
||||
<div class="bg-gray-50 p-2 mb-2 border border-gray-200">
|
||||
<p class="text-xs font-black mb-1">{{ stat.database }}</p>
|
||||
<div class="text-[11px]">
|
||||
{{ _self.r('Transactions', stat.total_xact_count|number_format(0, '.', ' ')) }}
|
||||
{{ _self.r('Requetes', stat.total_query_count|number_format(0, '.', ' ')) }}
|
||||
{{ _self.c('Avg xact', stat.avg_xact_time|number_format(0, '.', ' ') ~ 'us', stat.avg_xact_time > 100000 ? 'red' : (stat.avg_xact_time > 10000 ? 'yellow' : 'green')) }}
|
||||
{{ _self.c('Avg query', stat.avg_query_time|number_format(0, '.', ' ') ~ 'us', stat.avg_query_time > 50000 ? 'red' : (stat.avg_query_time > 5000 ? 'yellow' : 'green')) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-sm text-red-500 font-bold">{{ pgbouncer.error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro row(label, value) %}
|
||||
<div class="flex justify-between py-1.5 text-sm">
|
||||
{# ── Macros ──────────────────────────────────────────── #}
|
||||
|
||||
{% macro r(label, value) %}
|
||||
<div class="flex justify-between py-0.5 text-[12px]">
|
||||
<span class="text-gray-500 font-bold">{{ label }}</span>
|
||||
<span class="font-black">{{ value }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro row_color(label, value, color) %}
|
||||
<div class="flex justify-between py-1.5 text-sm">
|
||||
{% macro c(label, value, color) %}
|
||||
<div class="flex justify-between py-0.5 text-[12px]">
|
||||
<span class="text-gray-500 font-bold">{{ label }}</span>
|
||||
<span class="font-black {% if color == 'green' %}text-green-600{% elseif color == 'red' %}text-red-600{% elseif color == 'yellow' %}text-yellow-600{% else %}text-gray-400{% endif %}">{{ value }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro svc(name, status, info) %}
|
||||
<div class="flex items-center justify-between py-0.5 text-[12px]">
|
||||
<span class="font-bold text-gray-500">{{ name }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[11px] text-gray-400">{{ info }}</span>
|
||||
{% if status == 'ok' %}
|
||||
<span class="w-2 h-2 rounded-full bg-green-500 inline-block"></span>
|
||||
{% else %}
|
||||
<span class="w-2 h-2 rounded-full bg-red-500 inline-block"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro svc_ssl(ssl) %}
|
||||
<div class="flex items-center justify-between py-0.5 text-[12px]">
|
||||
<span class="font-bold text-gray-500">SSL {{ ssl.domain }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[11px] text-gray-400">{{ ssl.issuer }} — {{ ssl.valid_until }} ({{ ssl.days_left }}j)</span>
|
||||
{% if ssl.status == 'ok' %}
|
||||
<span class="w-2 h-2 rounded-full bg-green-500 inline-block"></span>
|
||||
{% elseif ssl.status == 'warning' %}
|
||||
<span class="w-2 h-2 rounded-full bg-yellow-500 inline-block"></span>
|
||||
{% else %}
|
||||
<span class="w-2 h-2 rounded-full bg-red-500 inline-block"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro pct_color(val) %}{% if val != '?' and val < 70 %}green{% elseif val == '?' %}gray{% elseif val < 90 %}yellow{% else %}red{% endif %}{% endmacro %}
|
||||
|
||||
{% macro pct_class(val) %}{% if val != '?' and val < 70 %}text-green-600{% elseif val == '?' %}text-gray-400{% elseif val < 90 %}text-yellow-600{% else %}text-red-600{% endif %}{% endmacro %}
|
||||
|
||||
Reference in New Issue
Block a user