Accueil restructuré (zones Discord/Twitch, stats, indicateur connexion) + Top 3 sanctions/modérateurs + BOT_STATUS dans config Flask

This commit is contained in:
Mow910
2026-02-01 12:55:38 +01:00
parent a8d2a0e063
commit 48531690fd
10 changed files with 612 additions and 141 deletions

View File

@@ -33,7 +33,7 @@ def _set_sqlite_pragma(dbapi_connection, connection_record):
except Exception: except Exception:
pass pass
def _tableExists(table_name:str, cursor:Cursor) -> bool: def _tableExists(table_name: str, cursor: Cursor) -> bool:
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)) cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
return cursor.fetchone() is not None return cursor.fetchone() is not None
@@ -72,7 +72,6 @@ def _doPostImportMigration(cursor:Cursor):
_dropTable('game_bundle_old', cursor) _dropTable('game_bundle_old', cursor)
if _tableExists('youtube_notification', cursor): if _tableExists('youtube_notification', cursor):
logging.info("Migration de la table youtube_notification: ajout des colonnes d'embed")
embed_columns = [ embed_columns = [
('embed_title', 'VARCHAR(256)'), ('embed_title', 'VARCHAR(256)'),
('embed_description', 'VARCHAR(2000)'), ('embed_description', 'VARCHAR(2000)'),
@@ -81,7 +80,7 @@ def _doPostImportMigration(cursor:Cursor):
('embed_author_name', 'VARCHAR(256)'), ('embed_author_name', 'VARCHAR(256)'),
('embed_author_icon', 'VARCHAR(512)'), ('embed_author_icon', 'VARCHAR(512)'),
('embed_thumbnail', 'BOOLEAN DEFAULT 1'), ('embed_thumbnail', 'BOOLEAN DEFAULT 1'),
('embed_image', 'BOOLEAN DEFAULT 1') ('embed_image', 'BOOLEAN DEFAULT 1'),
] ]
for col_name, col_type in embed_columns: for col_name, col_type in embed_columns:
if not _tableHaveColumn('youtube_notification', col_name, cursor): if not _tableHaveColumn('youtube_notification', col_name, cursor):
@@ -89,8 +88,7 @@ def _doPostImportMigration(cursor:Cursor):
cursor.execute(f'ALTER TABLE youtube_notification ADD COLUMN {col_name} {col_type}') cursor.execute(f'ALTER TABLE youtube_notification ADD COLUMN {col_name} {col_type}')
logging.info(f"Colonne {col_name} ajoutée à youtube_notification") logging.info(f"Colonne {col_name} ajoutée à youtube_notification")
except Exception as e: except Exception as e:
logging.error(f"Impossible d'ajouter la colonne {col_name}: {e}") logging.warning(f"Colonne youtube_notification.{col_name}: {e}")
raise
with webapp.app_context(): with webapp.app_context():
with open('database/schema.sql', 'r') as f: with open('database/schema.sql', 'r') as f:

View File

@@ -61,6 +61,7 @@ class AntiCheatCache(db.Model):
notes = db.Column(db.String(1024)) notes = db.Column(db.String(1024))
updated_at = db.Column(db.DateTime) updated_at = db.Column(db.DateTime)
class YouTubeNotification(db.Model): class YouTubeNotification(db.Model):
__tablename__ = 'youtube_notification' __tablename__ = 'youtube_notification'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View File

@@ -3,6 +3,7 @@ import discord
import logging import logging
import random import random
from webapp import webapp
from database import db from database import db
from database.helpers import ConfigurationHelper from database.helpers import ConfigurationHelper
from database.models import Configuration, Humeur, Commande from database.models import Configuration, Humeur, Commande
@@ -28,6 +29,8 @@ from protondb import searhProtonDb
class DiscordBot(discord.Client): class DiscordBot(discord.Client):
async def on_ready(self): async def on_ready(self):
logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})') logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})')
webapp.config["BOT_STATUS"]["discord_connected"] = True
webapp.config["BOT_STATUS"]["discord_guild_count"] = len(self.guilds)
for c in self.get_all_channels() : for c in self.get_all_channels() :
logging.info(f'{c.id} {c.name}') logging.info(f'{c.id} {c.name}')
@@ -38,6 +41,9 @@ class DiscordBot(discord.Client):
self.loop.create_task(self.updateHumbleBundle()) self.loop.create_task(self.updateHumbleBundle())
self.loop.create_task(self.updateYouTube()) self.loop.create_task(self.updateYouTube())
async def on_disconnect(self):
webapp.config["BOT_STATUS"]["discord_connected"] = False
async def updateStatus(self): async def updateStatus(self):
while not self.is_closed(): while not self.is_closed():
humeurs = Humeur.query.all() humeurs = Humeur.query.all()
@@ -56,7 +62,6 @@ class DiscordBot(discord.Client):
async def updateYouTube(self): async def updateYouTube(self):
while not self.is_closed(): while not self.is_closed():
await checkYouTubeVideos() await checkYouTubeVideos()
# Vérification toutes les 5 minutes (comme pour Twitch)
await asyncio.sleep(5*60) await asyncio.sleep(5*60)
def getAllTextChannel(self) -> list[TextChannel]: def getAllTextChannel(self) -> list[TextChannel]:

View File

@@ -14,8 +14,11 @@ USER_SCOPE = [AuthScope.CHAT_READ, AuthScope.CHAT_EDIT]
async def _onReady(ready_event: EventData): async def _onReady(ready_event: EventData):
logging.info('Bot Twitch prêt') logging.info('Bot Twitch prêt')
channel = ConfigurationHelper().getValue('twitch_channel')
webapp.config["BOT_STATUS"]["twitch_connected"] = True
webapp.config["BOT_STATUS"]["twitch_channel_name"] = channel
with webapp.app_context(): with webapp.app_context():
await ready_event.chat.join_room(ConfigurationHelper().getValue('twitch_channel')) await ready_event.chat.join_room(channel)
asyncio.get_event_loop().create_task(twitchBot._checkOnlineStreamers()) asyncio.get_event_loop().create_task(twitchBot._checkOnlineStreamers())

View File

@@ -2,4 +2,12 @@ from flask import Flask
webapp = Flask(__name__) webapp = Flask(__name__)
# État des bots (mis à jour par les bots, lu par le panneau)
webapp.config["BOT_STATUS"] = {
"discord_connected": False,
"discord_guild_count": 0,
"twitch_connected": False,
"twitch_channel_name": None,
}
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation, youtube from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation, youtube

View File

@@ -1,6 +1,16 @@
from flask import render_template from flask import render_template
from webapp import webapp from webapp import webapp
from database.models import ModerationEvent
@webapp.route("/") @webapp.route("/")
def index(): def index():
return render_template("index.html") status = webapp.config["BOT_STATUS"]
sanctions_count = ModerationEvent.query.count()
return render_template(
"index.html",
discord_connected=status["discord_connected"],
discord_guild_count=status["discord_guild_count"],
sanctions_count=sanctions_count,
twitch_connected=status["twitch_connected"],
twitch_channel_name=status["twitch_channel_name"],
)

View File

@@ -3,16 +3,58 @@ from webapp import webapp
from database import db from database import db
from database.models import ModerationEvent from database.models import ModerationEvent
def _top_sanctioned():
return (
db.session.query(
ModerationEvent.discord_id,
db.func.max(ModerationEvent.username).label("username"),
db.func.count(ModerationEvent.id).label("count"),
)
.group_by(ModerationEvent.discord_id)
.order_by(db.func.count(ModerationEvent.id).desc())
.limit(3)
.all()
)
def _top_moderators():
return (
db.session.query(
ModerationEvent.staff_id,
db.func.max(ModerationEvent.staff_name).label("staff_name"),
db.func.count(ModerationEvent.id).label("count"),
)
.group_by(ModerationEvent.staff_id)
.order_by(db.func.count(ModerationEvent.id).desc())
.limit(3)
.all()
)
@webapp.route("/moderation") @webapp.route("/moderation")
def moderation(): def moderation():
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all() events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
return render_template("moderation.html", events=events, event=None) top_sanctioned = _top_sanctioned()
top_moderators = _top_moderators()
return render_template(
"moderation.html",
events=events,
event=None,
top_sanctioned=top_sanctioned,
top_moderators=top_moderators,
)
@webapp.route("/moderation/edit/<int:event_id>") @webapp.route("/moderation/edit/<int:event_id>")
def open_edit_moderation_event(event_id): def open_edit_moderation_event(event_id):
event = ModerationEvent.query.get_or_404(event_id) event = ModerationEvent.query.get_or_404(event_id)
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all() events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
return render_template("moderation.html", events=events, event=event) top_sanctioned = _top_sanctioned()
top_moderators = _top_moderators()
return render_template(
"moderation.html",
events=events,
event=event,
top_sanctioned=top_sanctioned,
top_moderators=top_moderators,
)
@webapp.route("/moderation/update/<int:event_id>", methods=['POST']) @webapp.route("/moderation/update/<int:event_id>", methods=['POST'])
def update_moderation_event(event_id): def update_moderation_event(event_id):

View File

@@ -1,7 +1,97 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Bienvenue sur l'interface d'administration de Mamie.</h1> <div class="text-center py-10">
<p>Nous devons définir ce que nous souhaitons afficher sur la page d'accueil. Peut-être l'historique des dernières <h1 class="text-3xl sm:text-4xl font-bold text-slate-800 dark:text-white mb-3">
modifications ? de la modération ?</p> Panneau d'administration
</h1>
<p class="text-base text-slate-600 dark:text-slate-400 max-w-xl mx-auto">
Gérez les fonctionnalités de votre bot Discord et Twitch depuis cette interface.
</p>
</div>
{# Zone Discord #}
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-8">
<div class="p-4 sm:p-6 border-b border-slate-200 dark:border-slate-700 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full {% if discord_connected %}bg-emerald-500 ring-4 ring-emerald-500/30{% else %}bg-slate-400 ring-4 ring-slate-400/30{% endif %}" title="{% if discord_connected %}Bot Discord connecté{% else %}Bot Discord déconnecté{% endif %}"></span>
<h2 class="text-xl font-semibold text-slate-800 dark:text-white">Discord</h2>
</div>
<span class="text-sm text-slate-500 dark:text-slate-400">
{% if discord_connected %}Connecté{% else %}Déconnecté{% endif %}
</span>
</div>
<div class="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Serveurs connectés</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ discord_guild_count }}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Sanctions enregistrées</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ sanctions_count }}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600 sm:col-span-2 lg:col-span-2 flex items-center justify-center">
<div class="flex flex-wrap gap-3 justify-center">
<a href="/live-alert" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Alertes Live</a>
<a href="/youtube" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Notification YouTube</a>
<a href="/humeurs" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Humeurs</a>
<a href="/protondb" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">ProtonDB</a>
<a href="/commandes" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Commandes</a>
<a href="/moderation" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Modération</a>
</div>
</div>
</div>
</div>
{# Zone Twitch #}
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-8">
<div class="p-4 sm:p-6 border-b border-slate-200 dark:border-slate-700 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full {% if twitch_connected %}bg-emerald-500 ring-4 ring-emerald-500/30{% else %}bg-slate-400 ring-4 ring-slate-400/30{% endif %}" title="{% if twitch_connected %}Bot Twitch connecté{% else %}Bot Twitch déconnecté{% endif %}"></span>
<h2 class="text-xl font-semibold text-slate-800 dark:text-white">Twitch</h2>
</div>
<span class="text-sm text-slate-500 dark:text-slate-400">
{% if twitch_connected %}Connecté{% else %}Déconnecté{% endif %}
</span>
</div>
<div class="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Canal connecté</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{% if twitch_channel_name %}{{ twitch_channel_name }}{% else %}—{% endif %}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Sanctions</p>
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1"></p>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">À venir</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600 sm:col-span-2 lg:col-span-2 flex items-center justify-center">
<p class="text-sm text-slate-500 dark:text-slate-400">Intégrations du bot Twitch à venir.</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<div>
<h3 class="font-medium text-slate-800 dark:text-white mb-2">À propos</h3>
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">
Mamie Henriette est un bot open source pour Discord et Twitch, développé par la communauté.
Cette interface vous permet de configurer et gérer toutes les fonctionnalités.
</p>
<div class="flex flex-wrap gap-3">
<a href="https://github.com/skylanix/MamieHenriette" target="_blank" class="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-800 dark:bg-slate-700 text-white rounded text-sm hover:bg-slate-700 dark:hover:bg-slate-600 transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
GitHub
</a>
<a href="https://discord.com/invite/UwAPqMJnx3" target="_blank" class="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-600 rounded text-sm hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"></path></svg>
Discord
</a>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -1,110 +1,215 @@
{% extends "template.html" %} {% extends "template.html" %}
{% block content %} {% block content %}
<h1>Modération Discord</h1> <div class="mb-6">
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Modération</h1>
<p class="text-sm text-slate-600 dark:text-slate-400">
Historique des actions de modération sur le serveur Discord.
</p>
</div>
<p> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
Historique des actions de modération effectuées sur le serveur Discord. <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Top 3 sanctions</h2>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Utilisateurs les plus sanctionnés</p>
</div>
<div class="divide-y divide-slate-200 dark:divide-slate-700">
{% for row in top_sanctioned %}
<div class="px-5 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-slate-200 dark:bg-slate-600 flex items-center justify-center text-sm font-bold text-slate-700 dark:text-slate-300">{{ loop.index }}</span>
<div class="min-w-0">
<span class="block text-sm font-medium text-slate-800 dark:text-white truncate">{{ row.username or '—' }}</span>
<span class="block text-xs text-slate-500 dark:text-slate-400 font-mono truncate">{{ row.discord_id }}</span>
</div>
</div>
<span class="flex-shrink-0 text-sm font-semibold text-slate-600 dark:text-slate-300">{{ row.count }} sanction{{ 's' if row.count > 1 else '' }}</span>
</div>
{% else %}
<div class="px-5 py-6 text-center text-sm text-slate-500 dark:text-slate-400">Aucune sanction enregistrée</div>
{% endfor %}
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Top 3 modérateurs</h2>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Staff ayant effectué le plus d'actions</p>
</div>
<div class="divide-y divide-slate-200 dark:divide-slate-700">
{% for row in top_moderators %}
<div class="px-5 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-slate-200 dark:bg-slate-600 flex items-center justify-center text-sm font-bold text-slate-700 dark:text-slate-300">{{ loop.index }}</span>
<div class="min-w-0">
<span class="block text-sm font-medium text-slate-800 dark:text-white truncate">{{ row.staff_name or '—' }}</span>
<span class="block text-xs text-slate-500 dark:text-slate-400 font-mono truncate">{{ row.staff_id }}</span>
</div>
</div>
<span class="flex-shrink-0 text-sm font-semibold text-slate-600 dark:text-slate-300">{{ row.count }} action{{ 's' if row.count > 1 else '' }}</span>
</div>
{% else %}
<div class="px-5 py-6 text-center text-sm text-slate-500 dark:text-slate-400">Aucune action enregistrée</div>
{% endfor %}
</div>
</div>
</div>
Le bot enregistre automatiquement les avertissements, exclusions et bannissements. <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
<details class="group">
<table> <summary class="flex items-center justify-between px-5 py-4 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
<thead> <span class="font-medium text-slate-800 dark:text-white">Commandes de modération disponibles</span>
<tr> <svg class="w-5 h-5 text-slate-400 group-open:rotate-180 transition-transform" 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>
<th>Commande</th> </summary>
<th>Description</th> <div class="border-t border-slate-200 dark:border-slate-700">
</tr> <div class="divide-y divide-slate-200 dark:divide-slate-700 text-sm">
</thead> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
<tbody> <code class="text-slate-700 dark:text-slate-300 font-mono">!averto @user raison</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Avertit un utilisateur</span>
<td><strong>!averto @utilisateur raison</strong><br><small>Alias : !warn, !av, !avertissement</small></td> </div>
<td>Avertit un utilisateur et enregistre l'avertissement dans la base de données</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!delaverto id</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Retire un avertissement</span>
<td><strong>!delaverto id</strong><br><small>Alias : !removewarn, !delwarn</small></td> </div>
<td>Retire un avertissement en utilisant son numéro d'ID</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!warnings [@user]</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Liste les événements de modération</span>
<td><strong>!warnings</strong> ou <strong>!warnings @utilisateur</strong><br><small>Alias : !listevent, !listwarn</small></td> </div>
<td>Affiche la liste des événements de modération (tous ou pour un utilisateur spécifique)</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!inspect @user</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Informations sur un utilisateur</span>
<td><strong>!inspect @utilisateur</strong> ou <strong>!inspect id</strong></td> </div>
<td>Affiche des informations détaillées sur un utilisateur : création du compte, date d'arrivée, historique de modération</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!kick @user raison</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Expulse un utilisateur</span>
<td><strong>!kick @utilisateur raison</strong></td> </div>
<td>Expulse un utilisateur du serveur</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!ban @user raison</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Bannit un utilisateur</span>
<td><strong>!ban @utilisateur raison</strong></td> </div>
<td>Bannit définitivement un utilisateur du serveur</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!unban id</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Révoque un bannissement</span>
<td><strong>!unban discord_id</strong> ou <strong>!unban #sanction_id raison</strong></td> </div>
<td>Révoque le bannissement d'un utilisateur et lui envoie une invitation</td> <div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
</tr> <code class="text-slate-700 dark:text-slate-300 font-mono">!banlist</code>
<tr> <span class="text-slate-500 dark:text-slate-400">Liste des utilisateurs bannis</span>
<td><strong>!banlist</strong></td> </div>
<td>Affiche la liste des utilisateurs actuellement bannis du serveur</td> </div>
</tr> </div>
<tr> </details>
<td><strong>!aide</strong><br><small>Alias : !help</small></td> </div>
<td>Affiche l'aide avec toutes les commandes disponibles</td>
</tr>
</tbody>
</table>
</p>
{% if not event %} {% if not event %}
<h2>Événements de modération</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
<table class="moderation"> <div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
<thead> <h2 class="text-lg font-medium text-slate-800 dark:text-white">Événements de modération</h2>
<tr> </div>
<th>Type</th> <div class="overflow-x-auto">
<th>Utilisateur</th> <table class="w-full">
<th>Discord ID</th> <thead>
<th>Date & Heure</th> <tr class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
<th>Raison</th> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Type</th>
<th>Staff</th> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Utilisateur</th>
<th>#</th> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Date</th>
</tr> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Raison</th>
</thead> <th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Staff</th>
<tbody> <th class="px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Actions</th>
{% for mod_event in events %} </tr>
<tr> </thead>
<td>{{ mod_event.type }}</td> <tbody class="divide-y divide-slate-200 dark:divide-slate-700">
<td>{{ mod_event.username }}</td> {% for mod_event in events %}
<td>{{ mod_event.discord_id }}</td> <tr class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
<td>{{ mod_event.created_at.strftime('%d/%m/%Y %H:%M') if mod_event.created_at else 'N/A' }}</td> <td class="px-4 py-3">
<td>{{ mod_event.reason }}</td> {% if mod_event.type == 'ban' %}
<td>{{ mod_event.staff_name }}</td> <span class="text-xs font-medium text-red-600 dark:text-red-400">Ban</span>
<td> {% elif mod_event.type == 'kick' %}
<a href="{{ url_for('open_edit_moderation_event', event_id = mod_event.id) }}" class="icon"></a> <span class="text-xs font-medium text-orange-600 dark:text-orange-400">Kick</span>
<a href="{{ url_for('delete_moderation_event', event_id = mod_event.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')" class="icon">🗑</a> {% elif mod_event.type == 'warn' or mod_event.type == 'warning' %}
</td> <span class="text-xs font-medium text-yellow-600 dark:text-yellow-400">Warn</span>
</tr> {% elif mod_event.type == 'unban' %}
{% endfor %} <span class="text-xs font-medium text-green-600 dark:text-green-400">Unban</span>
</tbody> {% else %}
</table> <span class="text-xs font-medium text-slate-600 dark:text-slate-400">{{ mod_event.type }}</span>
{% endif %}
</td>
<td class="px-4 py-3">
<div class="flex flex-col">
<span class="text-sm font-medium text-slate-800 dark:text-white">{{ mod_event.username }}</span>
<span class="text-xs text-slate-500 dark:text-slate-400 font-mono">{{ mod_event.discord_id }}</span>
</div>
</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400 whitespace-nowrap">
{{ mod_event.created_at.strftime('%d/%m/%Y %H:%M') if mod_event.created_at else 'N/A' }}
</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400 max-w-xs">
<div class="line-clamp-2">{{ mod_event.reason }}</div>
</td>
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400">
{{ mod_event.staff_name }}
</td>
<td class="px-4 py-3">
<div class="flex items-center justify-end gap-2">
<a href="{{ url_for('open_edit_moderation_event', event_id = mod_event.id) }}" class="text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors">
Modifier
</a>
<a href="{{ url_for('delete_moderation_event', event_id = mod_event.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')" class="text-sm text-slate-500 hover:text-red-600 dark:hover:text-red-400 transition-colors">
Supprimer
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
Aucun événement de modération
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %} {% endif %}
{% if event %} {% if event %}
<h2>Editer un événement</h2> <div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5">
<form action="{{ url_for('update_moderation_event', event_id = event.id) }}" method="POST"> <h2 class="text-lg font-medium text-slate-800 dark:text-white mb-5">Modifier l'événement</h2>
<label for="type">Type</label>
<input name="type" type="text" value="{{ event.type }}" disabled />
<label for="username">Utilisateur</label>
<input name="username" type="text" value="{{ event.username }}" disabled />
<label for="discord_id">Discord ID</label>
<input name="discord_id" type="text" value="{{ event.discord_id }}" disabled />
<label for="reason">Raison</label>
<input name="reason" type="text" value="{{ event.reason }}" required="required" />
<label for="staff_name">Staff</label>
<input name="staff_name" type="text" value="{{ event.staff_name }}" disabled />
<input type="Submit" value="Modifier">
<a href="{{ url_for('moderation') }}">Annuler</a>
</form>
{% endif %}
<form action="{{ url_for('update_moderation_event', event_id = event.id) }}" method="POST" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Type</label>
<input type="text" value="{{ event.type }}" disabled class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Staff</label>
<input type="text" value="{{ event.staff_name }}" disabled class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Utilisateur</label>
<input type="text" value="{{ event.username }}" disabled class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Discord ID</label>
<input type="text" value="{{ event.discord_id }}" disabled class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 cursor-not-allowed font-mono">
</div>
</div>
<div>
<label for="reason" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Raison</label>
<input type="text" name="reason" id="reason" value="{{ event.reason }}" required class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
</div>
<div class="flex items-center justify-end gap-3">
<a href="{{ url_for('moderation') }}" class="px-4 py-2 text-slate-700 dark:text-slate-300 text-sm font-medium rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
Annuler
</a>
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
Enregistrer
</button>
</div>
</form>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,40 +1,249 @@
<!DOCTYPE html> <!DOCTYPE html>
<html color-mode="user"> <html lang="fr" class="h-full">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes">
<title>Mamie Henriette</title> <title>Mamie Henriette</title>
<link rel="stylesheet" href="/static/css/mvp.css" /> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/static/css/style.css" /> <script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
},
accent: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
}
}
}
}
}
</script>
<link rel="icon" href="/static/ico/favicon.ico" type="image/x-icon"> <link rel="icon" href="/static/ico/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/static/ico/favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="/static/ico/favicon.ico" type="image/x-icon">
<style>
/* Animations personnalisées */
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Scrollbar personnalisée */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.dark ::-webkit-scrollbar-thumb { background: #475569; }
.dark ::-webkit-scrollbar-thumb:hover { background: #64748b; }
</style>
</head> </head>
<body> <body class="h-full bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200">
<header> <!-- Navbar -->
<nav> <nav class="fixed top-0 left-0 right-0 z-50 bg-white dark:bg-gray-800 shadow-md border-b border-gray-200 dark:border-gray-700">
<a href="/"><img src="/static/ico/favicon.ico"></a> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ul> <div class="flex items-center justify-between h-16">
<li><a href="/live-alert">Alerte live</a></li> <!-- Logo -->
<li><a href="/youtube">YouTube</a></li> <a href="/" class="flex items-center gap-3 group">
<li><a href="/commandes">Commandes</a></li> <img src="/static/ico/favicon.ico" alt="Mamie Henriette" class="w-10 h-10 rounded-full ring-2 ring-slate-200 dark:ring-slate-600 group-hover:ring-slate-300 dark:group-hover:ring-slate-500 transition-all">
<li><a href="/humeurs">Humeurs</a></li> <span class="font-bold text-xl text-slate-800 dark:text-white hidden sm:block">Mamie Henriette</span>
<li><a href="/moderation">Modération</a></li> </a>
<li><a href="/protondb">ProtonDB</a></li>
<li><a href="/configurations">Configurations</a></li> <!-- Navigation Desktop -->
</ul> <div class="hidden md:flex items-center gap-1">
</nav> <!-- Discord (sous-menus) -->
</header> <div class="relative group">
<main> <button type="button" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-1.5">
{% block content %}{% endblock %} <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
Discord
<svg class="w-4 h-4 transition-transform group-hover: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>
</button>
<div class="absolute left-0 top-full pt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[200px]">
<a href="/humeurs" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Humeur
</a>
<a href="/live-alert" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
Notification Twitch
</a>
<a href="/youtube" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
Notification YouTube
</a>
<a href="/protondb" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
ProtonDB
</a>
<div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
<a href="/commandes" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
Commandes
</a>
<a href="/moderation" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
Modération
</a>
</div>
</div>
</div>
<!-- Twitch (futur bot) -->
<div class="relative group">
<button type="button" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-1.5">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M11.571 4.714h1.715v5.143H11.57l-.002-5.143zm3.43 0H16.714v5.143H15V4.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0H6zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714v9.429z"/></svg>
Twitch
<svg class="w-4 h-4 transition-transform group-hover: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>
</button>
<div class="absolute left-0 top-full pt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[200px]">
<a href="/live-alert" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
Alerte live
</a>
<span class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-400 dark:text-gray-500 italic">
Bot Twitch — à venir
</span>
</div>
</div>
</div>
<!-- Configuration locale -->
<a href="/configurations" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
Configuration
</a>
</div>
<!-- Actions -->
<div class="flex items-center gap-2">
<!-- Dark Mode Toggle -->
<button onclick="toggleDarkMode()" class="p-2 rounded-lg text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all" title="Mode sombre">
<svg class="w-5 h-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
<svg class="w-5 h-5 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
</button>
<!-- Mobile Menu Button -->
<button onclick="toggleMobileMenu()" class="md:hidden p-2 rounded-lg text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="px-4 py-3 space-y-1">
<a href="/live-alert" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
Alerte live
</a>
<a href="/youtube" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
YouTube
</a>
<a href="/commandes" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
Commandes
</a>
<a href="/humeurs" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Humeurs
</a>
<a href="/moderation" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
Modération
</a>
<a href="/protondb" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
ProtonDB
</a>
<a href="/configurations" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
Configurations
</a>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="pt-20 pb-12 min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="fade-in">
{% block content %}{% endblock %}
</div>
</div>
</main> </main>
<footer>
<hr> <!-- Footer -->
<p><a href="https://github.com/skylanix/MamieHenriette" target="_blank">MamieHenriette</a> créé par la communauté <a href="https://discord.com/invite/UwAPqMJnx3" target="_blank">Discord</a> de <a href="https://www.youtube.com/@513v3" target="_blank">573v3</a> - Projet open source sous licence <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">AGPLv3</a></p> <footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<img src="/static/ico/favicon.ico" alt="" class="w-6 h-6 rounded-full">
<span>Mamie Henriette</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">
Créé par la communauté
<a href="https://discord.com/invite/UwAPqMJnx3" target="_blank" class="text-primary-600 dark:text-primary-400 hover:underline">Discord</a>
de
<a href="https://www.youtube.com/@513v3" target="_blank" class="text-primary-600 dark:text-primary-400 hover:underline">573v3</a>
</p>
<div class="flex items-center gap-4">
<a href="https://github.com/skylanix/MamieHenriette" target="_blank" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
</a>
<span class="text-xs text-gray-400 dark:text-gray-500">AGPLv3</span>
</div>
</div>
</div>
</footer> </footer>
<script>
// Dark mode
function toggleDarkMode() {
document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
}
// Initialize dark mode from preference
if (localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
// Mobile menu
function toggleMobileMenu() {
document.getElementById('mobile-menu').classList.toggle('hidden');
}
</script>
</body> </body>
</html> </html>