Files
MamieHenriette/webapp/templates/discord_members.html
T
Mow910 855c13c183 Add guild_member_stats table and update permissions for Discord members
- Created a new table `guild_member_stats` to track message counts and voice activity for users in each guild.
- Updated the database schema and models to include the new table.
- Added a new permission entry for `discord_members` in the `webapp_page_permission` table.
- Enhanced the webapp to include links and settings for managing Discord member statistics.
2026-03-25 22:52:32 +01:00

154 lines
8.2 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.
{% extends "template.html" %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Membres Discord</h1>
<p class="text-sm text-slate-600 dark:text-slate-400">
Rapport par membre : arrivée sur le serveur, invitation enregistrée, activité (messages et vocal depuis le suivi activé) et sanctions.
</p>
{% if guild_name %}
<p class="text-sm font-medium text-primary-600 dark:text-primary-400 mt-1">{{ guild_name }}</p>
{% endif %}
</div>
{% if not bot_connected %}
<div class="mb-6 rounded-lg border border-amber-200 dark:border-amber-900/50 bg-amber-50 dark:bg-amber-950/30 px-4 py-3 text-sm text-amber-900 dark:text-amber-200">
Le bot Discord semble déconnecté : la liste des membres peut être indisponible ou incomplète.
</div>
{% endif %}
{% if load_error %}
<div class="mb-6 rounded-lg border border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-950/30 px-4 py-3 text-sm text-red-800 dark:text-red-200">
{{ load_error }}
{% if guild_choices %}
<ul class="mt-3 space-y-2">
{% for g in guild_choices %}
<li>
<a href="{{ url_for('discord_members', guild_id=g.id) }}" class="font-medium text-primary-600 dark:text-primary-400 hover:underline">{{ g.name }}</a>
<span class="text-slate-500 dark:text-slate-400 font-mono text-xs ml-2">({{ g.id }})</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
{% if not load_error and members|length == 0 and not guild_choices %}
<div class="rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-5 py-8 text-center text-slate-500 dark:text-slate-400 text-sm">
Aucun membre humain à afficher sur ce serveur.
</div>
{% endif %}
{% if members %}
<div class="mb-4 flex flex-col sm:flex-row sm:items-center gap-3">
<label class="sr-only" for="member-search">Rechercher un membre</label>
<input type="search" id="member-search" placeholder="Rechercher par nom ou ID…" autocomplete="off"
class="w-full sm:max-w-md rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-4 py-2.5 text-sm text-slate-800 dark:text-white placeholder-slate-400 focus:ring-2 focus:ring-primary-500 focus:border-primary-500" />
<p class="text-xs text-slate-500 dark:text-slate-400">
Les compteurs <strong>messages</strong> et <strong>vocal</strong> sont cumulés à partir du déploiement du suivi (pas dhistorique rétroactif). Linvitation provient du message de bienvenue si celui-ci était activé au moment du join.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
{% for m in members %}
<div data-member-card data-search="{{ m.search_blob }}" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 overflow-hidden shadow-sm hover:shadow-md transition-shadow">
<div class="p-4 flex gap-4">
<img src="{{ m.avatar_url }}" alt="" width="64" height="64" class="w-16 h-16 rounded-full flex-shrink-0 ring-2 ring-slate-200 dark:ring-slate-600 object-cover" loading="lazy" />
<div class="min-w-0 flex-1">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<h2 class="text-base font-semibold text-slate-900 dark:text-white truncate">{{ m.display_name }}</h2>
<p class="text-xs text-slate-500 dark:text-slate-400 font-mono truncate">@{{ m.name }} · {{ m.id }}</p>
</div>
{% if m.sanction_count > 0 %}
<span class="flex-shrink-0 text-xs font-semibold px-2 py-0.5 rounded-full bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-200">{{ m.sanction_count }} sanction{% if m.sanction_count > 1 %}s{% endif %}</span>
{% endif %}
</div>
<div class="mt-3 flex flex-wrap gap-2">
<span class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-200">
<svg class="w-3.5 h-3.5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
{{ m.message_count }} msg
</span>
<span class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md bg-violet-100 dark:bg-violet-900/40 text-violet-800 dark:text-violet-200">
<svg class="w-3.5 h-3.5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path></svg>
{{ m.voice_label }}
</span>
{% if m.joined_display != '—' %}
<span class="inline-flex items-center text-xs px-2 py-1 rounded-md bg-emerald-100 dark:bg-emerald-900/40 text-emerald-800 dark:text-emerald-200" title="Membre depuis">{{ m.joined_display }}</span>
{% endif %}
</div>
</div>
</div>
<details class="group border-t border-slate-200 dark:border-slate-700">
<summary class="px-4 py-3 cursor-pointer text-sm font-medium text-primary-600 dark:text-primary-400 hover:bg-slate-50 dark:hover:bg-slate-700/40 flex items-center justify-between list-none">
<span>Rapport détaillé</span>
<svg class="w-4 h-4 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</summary>
<div class="px-4 pb-4 pt-0 space-y-4 text-sm">
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-slate-600 dark:text-slate-300">
<div>
<dt class="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500">Pseudo serveur</dt>
<dd>{{ m.nick or '—' }}</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500">Arrivée (Discord)</dt>
<dd>{{ m.joined_display }}</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500">Invitation (DB)</dt>
<dd><code class="text-xs bg-slate-100 dark:bg-slate-700 px-1 rounded">{{ m.invite_code }}</code></dd>
</div>
<div>
<dt class="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500">Invité par</dt>
<dd>{{ m.inviter_name }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500">Date join enregistrée (welcome)</dt>
<dd>{{ m.invite_join_date }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-xs uppercase tracking-wide text-slate-400 dark:text-slate-500">Rôles</dt>
<dd class="text-xs leading-relaxed">{{ m.roles }}</dd>
</div>
</dl>
{% if m.sanctions %}
<div>
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-2">Sanctions</h3>
<ul class="space-y-2 max-h-48 overflow-y-auto">
{% for s in m.sanctions %}
<li class="rounded-lg border border-slate-200 dark:border-slate-600 px-3 py-2 bg-slate-50 dark:bg-slate-900/50">
<div class="flex flex-wrap items-baseline justify-between gap-2">
<span class="font-medium text-slate-800 dark:text-slate-200">{{ s.type }}</span>
<span class="text-xs text-slate-500">{{ s.created_at }}</span>
</div>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">{{ s.reason }}</p>
<p class="text-xs text-slate-500 mt-1">Par {{ s.staff_name }}{% if s.duration is not none %} · durée {{ s.duration }}s{% endif %}</p>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<p class="text-xs text-slate-500 dark:text-slate-400">Aucune sanction enregistrée en base pour ce membre.</p>
{% endif %}
</div>
</details>
</div>
{% endfor %}
</div>
<script>
(function() {
var input = document.getElementById('member-search');
if (!input) return;
input.addEventListener('input', function() {
var q = (input.value || '').toLowerCase().trim();
document.querySelectorAll('[data-member-card]').forEach(function(el) {
var blob = (el.getAttribute('data-search') || '').toLowerCase();
el.style.display = !q || blob.indexOf(q) !== -1 ? '' : 'none';
});
});
})();
</script>
{% endif %}
{% endblock %}