Files
Monitorink/backend/templates/dashboard.html
jerem c4e5c141aa Trackers: affiche les jetons de seed (torr9 jeton_balance)
Champ tokens optionnel sur TrackerStat (None = tracker sans jetons) ; torr9 le
remplit depuis jeton_balance de /users/me. Ligne « N jetons » conditionnelle sous
envoyé/reçu, masquée pour les trackers sans système de jetons (c411).
2026-06-17 10:43:32 +02:00

273 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: 1fr auto;
grid-template-areas: "left right" "foot foot";
column-gap: 52px;
}
.pane { grid-area: left; display: flex; flex-direction: column; min-width: 0; }
.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: 26px; }
.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); }
hr.div { border: 0; border-top: 4px solid var(--ink); margin: 30px 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); }
.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: 13px 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: 31px; }
.trk .ratio { font-size: 46px; font-weight: 800; line-height: .9; }
.trk .io { font-size: 25px; font-weight: 500; margin-top: 5px; }
/* ============================ 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></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 %}<div class="io num">envoyé {{ t.up_h }} · reçu {{ t.down_h }}</div>
{% if t.tokens is not none %}<div class="io num">{{ t.tokens_h }} {{ t.tokens_label }}</div>{% endif %}{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% if ha_states %}
<hr class="div">
<div class="label"><span class="t">Maison</span></div>
<div class="rows grid2">
{% for s in ha_states %}
<div class="row"><span class="k">{{ s.label }}</span><span class="v num">{{ s.display }}</span></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>