Type yggreborn : login form classique (identifier=EMAIL, password, csrf_token), ratio lu dans l'en-tête (Ratio : X.XX). Pas de up/down ni jetons (ligne envoyé/ reçu rendue conditionnelle sur up/down>0).
273 lines
13 KiB
HTML
273 lines
13 KiB
HTML
<!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 %}{% if t.up_bytes or t.down_bytes %}<div class="io num">envoyé {{ t.up_h }} · reçu {{ t.down_h }}</div>{% endif %}
|
||
{% 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>
|