Ajoute un journal par utilisateur des notifications ntfy et de la présence Tasker

Chaque utilisateur a désormais son propre fichier logs.json dans son
dossier users/<id>/, alimenté automatiquement quand un rappel ntfy est
envoyé (ou échoue) et quand Tasker signale une arrivée ou un départ.
Une nouvelle page /logs, accessible depuis la nav, affiche ces entrées
avec filtres (Tout / Notifications / Présence), et le journal tourne à
500 entrées pour rester compact.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-07-19 23:46:08 +02:00
parent ef30820c39
commit 5fdef77e2b
5 changed files with 221 additions and 4 deletions

View File

@ -1032,6 +1032,7 @@
{% if current_user is defined and current_user %}
<a href="/" class="nav-link">Semaine courante</a>
<a href="/stats" class="nav-link">Statistiques</a>
<a href="/logs" class="nav-link">Journaux</a>
<a href="/settings" class="nav-link">Réglages</a>
<span class="nav-user">{{ current_user }}</span>
<a href="/logout" class="nav-link nav-logout">Déconnexion</a>

141
app/templates/logs.html Normal file
View File

@ -0,0 +1,141 @@
{% extends "base.html" %}
{% block content %}
<div class="stats-head">
<div>
<span class="stats-title">Journaux</span>
<div class="stats-meta">
Notifications ntfy et détections de présence (Tasker) — {{ current_user }}
</div>
</div>
</div>
<div class="logs-tabs" role="tablist">
{% set counts = {'all': logs|length, 'notif': logs | selectattr('type', 'equalto', 'notif') | list | length, 'presence': logs | selectattr('type', 'equalto', 'presence') | list | length} %}
<a class="ltab{% if filter == 'all' %} active{% endif %}" href="/logs" role="tab">
Tout <span class="ltab-count">{{ counts.all }}</span>
</a>
<a class="ltab{% if filter == 'notif' %} active{% endif %}" href="/logs?filter=notif" role="tab">
Notifications <span class="ltab-count">{{ counts.notif }}</span>
</a>
<a class="ltab{% if filter == 'presence' %} active{% endif %}" href="/logs?filter=presence" role="tab">
Présence <span class="ltab-count">{{ counts.presence }}</span>
</a>
</div>
{% set visible = logs if filter == 'all' else logs | selectattr('type', 'equalto', filter) | list %}
{% if visible %}
<div class="logs-list">
{% for entry in visible %}
<div class="log-row log-{{ entry.type }}{% if entry.event == 'échec' %} log-failed{% endif %}">
<div class="log-ts">{{ entry.ts }}</div>
<div class="log-type">{{ 'Notification' if entry.type == 'notif' else 'Présence' }}</div>
<div class="log-body">
<span class="log-event">{{ entry.event }}</span>
{% if entry.message %}<span class="log-msg">{{ entry.message }}</span>{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="logs-empty">Aucun événement enregistré pour le moment.</div>
{% endif %}
<style>
.logs-tabs {
display: flex;
gap: .4rem;
border-bottom: 1.5px solid var(--rule);
padding-bottom: .6rem;
flex-wrap: wrap;
}
.ltab {
font-family: var(--mono);
font-size: 11px;
letter-spacing: .04em;
color: var(--muted);
text-decoration: none;
border: 1.5px solid var(--rule);
padding: .35rem .7rem;
display: inline-flex;
align-items: center;
gap: .45rem;
transition: border-color .15s, color .15s, background .15s;
}
.ltab:hover { border-color: var(--text); color: var(--text); }
.ltab.active { border-color: var(--text); color: var(--text); background: var(--surface); }
.ltab-count {
font-size: 10px;
background: var(--ground);
border: 1px solid var(--rule);
padding: .05rem .35rem;
color: var(--muted);
}
.logs-list {
border: 1.5px solid var(--rule);
background: var(--surface);
display: flex;
flex-direction: column;
}
.log-row {
display: grid;
grid-template-columns: 180px 130px 1fr;
gap: 1rem;
padding: .55rem 1rem;
border-bottom: 1px solid var(--rule-l);
font-size: 13px;
align-items: baseline;
}
.log-row:last-child { border-bottom: none; }
.log-row:hover { background: var(--ground); }
.log-ts {
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
white-space: nowrap;
}
.log-type {
font-family: var(--mono);
font-size: 10px;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--muted);
white-space: nowrap;
}
.log-notif .log-type { color: var(--accent); }
.log-presence .log-type { color: var(--accent-2); }
.log-body { display: flex; flex-direction: column; gap: .15rem; }
.log-event {
font-weight: 500;
color: var(--text);
}
.log-msg {
font-size: 12px;
color: var(--muted);
line-height: 1.45;
}
.log-failed {
background: var(--neg-bg);
}
.log-failed .log-event { color: var(--neg); }
.log-failed:hover { background: var(--neg-bg); }
.logs-empty {
border: 1.5px solid var(--rule);
background: var(--surface);
padding: 2rem;
text-align: center;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 640px) {
.log-row {
grid-template-columns: 1fr;
gap: .2rem;
}
.log-ts { font-size: 10px; }
}
</style>
{% endblock %}