Files
Monitorink/backend/templates/dashboard.html

269 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<style>
/* ============================================================================
MONITORINK — instrument de mesure 1-bit. Canevas PAYSAGE 1680x1264, pivoté 90°
côté backend pour le panneau e-ink portrait de la Kobo.
Règles e-ink : NOIR & BLANC purs, aucun gris (le gris fantôme au refresh partiel).
La hiérarchie passe par taille/graisse ; le « consommé » par des hachures.
Mots = Archivo (grotesque lourd) · Nombres mesurés = JetBrains Mono (tabulaire).
========================================================================= */
{{ fonts | safe }}
* { margin: 0; padding: 0; box-sizing: border-box; }
:root { --ink: #000; --paper: #fff; }
html, body {
width: {{ width }}px; height: {{ height }}px;
background: var(--paper); color: var(--ink);
font-family: "Archivo", "DejaVu Sans", sans-serif;
-webkit-font-smoothing: none;
font-variant-numeric: tabular-nums;
}
.num { font-family: "JetBrains Mono", monospace; font-variant-numeric: tabular-nums; }
body {
padding: 46px 52px 0;
display: grid;
grid-template-columns: 560px 1fr;
grid-template-rows: minmax(0, 1fr) auto;
grid-template-areas: "left right" "foot foot";
column-gap: 52px;
}
.pane { grid-area: left; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
.deck { grid-area: right; display: flex; flex-direction: column; min-width: 0;
padding-left: 52px; border-left: 4px solid var(--ink); }
/* Étiquette de bloc : barre pleine + mot. Encode "section", pas de déco capitale flottante. */
.label { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
.label::before { content: ""; width: 26px; height: 26px; background: var(--ink); flex: 0 0 auto; }
.label .t { font-weight: 800; font-size: 33px; letter-spacing: 1px; text-transform: uppercase; }
.label .meta { font-weight: 700; font-size: 24px; letter-spacing: 1px; text-transform: uppercase;
margin-left: auto; padding: 4px 12px; border: 3px solid var(--ink); }
.label .alarm { background: var(--ink); color: var(--paper); }
/* Horodatage discret de fraîcheur des données (ex. trackers cachés jusqu'à 1 h). */
.label .stamp { margin-left: auto; font-family: "JetBrains Mono", monospace;
font-weight: 500; font-size: 22px; letter-spacing: 0; }
hr.div { border: 0; border-top: 4px solid var(--ink); margin: 22px 0; }
.pane hr.div { margin: 16px 0; }
/* ---- Météo : silhouette 1-bit + relevé mono ---- */
.wx { display: flex; align-items: center; gap: 30px; }
.wx svg { width: 150px; height: 150px; flex: 0 0 auto; }
.wx .temp { font-size: 144px; font-weight: 800; line-height: .82; }
.wx .deg { font-size: 64px; vertical-align: top; }
.wx-meta { margin-top: 18px; font-size: 30px; line-height: 1.45; }
.wx-meta .k { font-family: "Archivo", sans-serif; font-weight: 700;
text-transform: uppercase; letter-spacing: 1px; font-size: 24px; }
/* ---- Lignes de données (NAS, Maison) ---- */
.rows { display: flex; flex-direction: column; }
.rows.grid2 { display: grid; grid-template-columns: 1fr 1fr; column-gap: 40px; }
.row { display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
padding: 13px 0; border-bottom: 2px solid var(--ink); }
.rows .row:last-child { border-bottom: 0; } /* le hr.div suivant sert déjà de séparateur */
.row .k { font-weight: 700; font-size: 31px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .v { font-size: 36px; font-weight: 700; white-space: nowrap; }
.rows.grid2 .k { font-size: 28px; } .rows.grid2 .v { font-size: 32px; }
.ko { display: inline-block; padding: 0 8px; background: var(--ink); color: var(--paper); }
/* ---- Trackers (ratio + envoi/réception du compte, sous le NAS) ---- */
.trk { padding: 10px 0; border-bottom: 2px solid var(--ink); }
.trk:last-child { border-bottom: 0; }
.trk .top { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
.trk .name { font-weight: 700; font-size: 32px; }
.trk .ratio { font-size: 34px; font-weight: 800; line-height: .9; }
.trk .io { font-size: 22px; font-weight: 500; line-height: 1.2; margin-top: 3px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ============================ JAUGE (signature) ============================
Barre de progression intuitive : le noir se REMPLIT de gauche à droite avec la
consommation ; le blanc à droite = budget restant. Repère ▼ = ligne d'alerte
(80 % consommé = 20 % restant). Sous le seuil : consommé en hachures + état alarme. */
.g { margin-bottom: 34px; }
.g:last-child { margin-bottom: 0; }
.g .head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 14px; }
.g .name { font-weight: 700; font-size: 38px; }
.g .pct { font-size: 80px; font-weight: 800; line-height: .8; }
.g .pct .u { font-size: 24px; font-weight: 700; letter-spacing: .5px; }
.meter { position: relative; padding-top: 26px; } /* place pour le repère de seuil */
.meter .mark { position: absolute; top: 0; left: 80%; transform: translateX(-50%);
width: 0; height: 0; border-left: 11px solid transparent;
border-right: 11px solid transparent; border-top: 14px solid var(--ink); }
.track { position: relative; height: 58px; border: 4px solid var(--ink); background: var(--paper);
overflow: hidden; }
.track .fill { position: absolute; top: 0; bottom: 0; left: 0; background: var(--ink); }
.track .tick { position: absolute; top: 0; width: 3px; height: 16px; background: var(--ink); }
.track .t25 { left: 25%; } .track .t50 { left: 50%; } .track .t75 { left: 75%; }
.g.low .fill { background: repeating-linear-gradient(-45deg, #000 0 4px, #fff 4px 11px); }
.g.low .mark { border-top-width: 18px; border-left-width: 13px; border-right-width: 13px; }
.g .sub { font-size: 28px; margin-top: 12px; }
/* Variante compacte (Codex, secondaire). */
.g.mini { margin-bottom: 22px; }
.g.mini .name { font-size: 30px; }
.g.mini .pct { font-size: 52px; }
.g.mini .pct .u { font-size: 22px; }
.g.mini .meter { padding-top: 22px; }
.g.mini .track { height: 38px; }
.g.mini .sub { font-size: 24px; margin-top: 9px; }
.err { font-size: 36px; font-weight: 700; padding: 22px; border: 4px solid var(--ink);
background: repeating-linear-gradient(-45deg, #000 0 4px, #fff 4px 11px); }
.err span { background: var(--paper); padding: 6px 10px; }
/* ---- Pied de page ---- */
footer { grid-area: foot; display: flex; justify-content: space-between; align-items: center;
margin-top: 26px; padding: 18px 0 22px; border-top: 4px solid var(--ink);
font-size: 26px; }
footer .num { font-weight: 500; }
.stale { background: var(--ink); color: var(--paper); padding: 2px 10px; font-weight: 700; }
.space { flex: 1 1 auto; }
</style>
</head>
{# ---- Glyphes météo 1-bit (silhouettes pleines, nettes sur e-ink) ---- #}
{% macro sun(cx, cy, r) -%}
<circle cx="{{cx}}" cy="{{cy}}" r="{{r}}"/>
{% for a in [0,45,90,135,180,225,270,315] %}
<rect x="{{cx-2.5}}" y="{{cy-r-13}}" width="5" height="11" transform="rotate({{a}} {{cx}} {{cy}})"/>
{% endfor %}
{%- endmacro %}
{% macro cloud(ox, oy, s) -%}
<g transform="translate({{ox}},{{oy}}) scale({{s}})">
<circle cx="32" cy="42" r="20"/><circle cx="56" cy="34" r="26"/>
<circle cx="78" cy="44" r="18"/><rect x="30" y="42" width="50" height="22" rx="11"/>
</g>
{%- endmacro %}
{% macro wxicon(kind) -%}
<svg viewBox="0 0 100 100" fill="#000" stroke="#000" stroke-linecap="round">
{% if kind == 'clear' %}{{ sun(50, 50, 21) }}
{% elif kind == 'partly' %}{{ sun(34, 32, 15) }}{{ cloud(18, 36, 0.78) }}
{% elif kind == 'cloudy' %}{{ cloud(8, 22, 0.95) }}
{% elif kind == 'fog' %}{{ cloud(8, 12, 0.9) }}
{% for y in [76, 86, 96] %}<rect x="14" y="{{y}}" width="72" height="5" rx="2"/>{% endfor %}
{% elif kind == 'rain' %}{{ cloud(8, 8, 0.92) }}
{% for x in [30, 50, 70] %}<rect x="{{x}}" y="74" width="5" height="20" rx="2" transform="rotate(18 {{x}} 84)"/>{% endfor %}
{% elif kind == 'snow' %}{{ cloud(8, 8, 0.92) }}
{% for x in [30, 50, 70] %}<circle cx="{{x}}" cy="86" r="5"/>{% endfor %}
{% elif kind == 'storm' %}{{ cloud(8, 4, 0.92) }}
<polygon points="52,66 38,90 50,90 44,100 66,76 53,76 60,66" stroke="none"/>
{% else %}{{ cloud(8, 22, 0.95) }}{% endif %}
</svg>
{%- endmacro %}
{# ---- Jauge : la signature ---- #}
{% macro gauge(g, mini=False) -%}
<div class="g{% if g.remaining < 20 %} low{% endif %}{% if mini %} mini{% endif %}">
<div class="head">
<span class="name">{{ g.name }}</span>
<span class="pct num">{{ g.remaining | round | int }}<span class="u">% restant</span></span>
</div>
<div class="meter">
<span class="mark"></span>
<div class="track">
<span class="tick t25"></span><span class="tick t50"></span><span class="tick t75"></span>
<div class="fill" style="width: {{ (100 - g.remaining) | round(1) }}%;"></div>
</div>
</div>
<div class="sub num">reset {{ g.resets_in }} · {{ (100 - g.remaining) | round | int }}% conso{% if g.extra %} · {{ g.extra }}{% endif %}</div>
</div>
{%- endmacro %}
<body>
<!-- COLONNE GAUCHE : météo + infra maison -->
<div class="pane">
<div class="wx">
{% if weather.ok %}
{{ wxicon(weather.kind) }}
<div>
<div class="temp num">{{ weather.temp | round | int }}<span class="deg">°C</span></div>
</div>
{% else %}
<div class="wx-meta">Météo indisponible</div>
{% endif %}
</div>
{% if weather.ok %}
<div class="wx-meta num">
{{ weather.label }} · ressenti <span class="k">{{ weather.feels_like | round | int }}°</span>
· {{ weather.temp_min | round | int }}° / {{ weather.temp_max | round | int }}°{% if weather.precip_prob is not none %} · pluie {{ weather.precip_prob }}%{% endif %}
</div>
{% endif %}
{% if nas.ok %}
<hr class="div">
<div class="label"><span class="t">NAS</span>
{% if nas.docker_unhealthy %}<span class="meta alarm">{{ nas.docker_unhealthy }} KO</span>{% endif %}</div>
<div class="rows">
{% for d in nas.disks %}
<div class="row"><span class="k">{{ d.label }}</span><span class="v num">{{ d.percent | round | int }}% · {{ d.free_human }}</span></div>
{% endfor %}
<div class="row"><span class="k">Docker</span><span class="v num">{{ nas.docker_running }}/{{ nas.docker_total }}{% if not nas.docker_unhealthy %} ✓{% endif %}</span></div>
<div class="row"><span class="k">VPN</span><span class="v num">{% if nas.vpn_ok %}OK{% if nas.vpn_port %} · {{ nas.vpn_port }}{% endif %}{% else %}<span class="ko">DÉSYNC</span>{% endif %}</span></div>
</div>
{% endif %}
{% if trackers %}
<hr class="div">
<div class="label"><span class="t">Trackers</span>
{% if trackers_updated %}<span class="stamp">maj {{ trackers_updated }}</span>{% endif %}</div>
<div class="rows">
{% for t in trackers %}
<div class="trk">
<div class="top">
<span class="name">{{ t.label }}</span>
{% if t.ok %}<span class="ratio num">{{ t.ratio_h }}</span>
{% else %}<span class="io"><span class="ko">{{ t.error }}</span></span>{% endif %}
</div>
{% if t.ok and (t.has_io or t.tokens is not none) %}<div class="io num">{% if t.has_io %}↑{{ t.up_h }} ↓{{ t.down_h }}{% endif %}{% if t.has_io and t.tokens is not none %} · {% endif %}{% if t.tokens is not none %}{{ t.tokens_h }} {{ t.tokens_label }}{% endif %}</div>{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<div class="space"></div>
</div>
<!-- COLONNE DROITE : budgets (le héros) -->
<div class="deck">
<div class="label"><span class="t">Claude</span>
{% if claude.ok %}<span class="meta">Max 5×</span>{% endif %}</div>
{% if not claude.ok %}
<div class="err"><span>⚠ {{ claude.error }}</span></div>
{% else %}
{% for g in gauges %}{{ gauge(g) }}{% endfor %}
{% if claude.extra or claude.burn_rate %}
<div class="sub num" style="font-size:26px; margin-top:6px;">
{% if claude.extra %}{{ claude.extra.label }}{% endif %}{% if claude.extra and claude.burn_rate %} · {% endif %}{% if claude.burn_rate %}burn {{ claude.burn_rate | round | int }} tok/min{% endif %}
</div>
{% endif %}
{% endif %}
{% if codex.ok %}
<hr class="div">
<div class="label"><span class="t">Codex</span>
{% if codex.plan_type %}<span class="meta">{{ codex.plan_type | capitalize }}</span>{% endif %}
{% if codex.limited %}<span class="meta alarm">Limite</span>{% endif %}</div>
{% for g in codex.gauges %}{{ gauge(g, mini=True) }}{% endfor %}
{% endif %}
<div class="space"></div>
</div>
<footer>
<span class="num">monitorink · 3× bouton page → reboot</span>
<span class="space"></span>
{% if kobo.ok %}<span class="num">{% if kobo.charging %}⚡{% else %}batt{% endif %} {{ kobo.percent }}%{% if kobo.stale %} ?{% endif %}</span>{% endif %}
<span class="num" style="margin-left:28px;">maj {{ updated }}{% if stale %} <span class="stale">PÉRIMÉ</span>{% endif %}</span>
</footer>
</body>
</html>