prepare(" SELECT name FROM sqlite_master WHERE type='table' AND name=? LIMIT 1 "); $stmt->execute([$table]); return (bool)$stmt->fetchColumn(); } function formatSnr($snr): string { if ($snr === null || $snr === '') return 'N/A'; return sprintf('%+.1f dB', (float)$snr); } function qualityClass($snr): string { if ($snr === null || $snr === '') return 'neutral'; $snr = (float)$snr; if ($snr >= 8) return 'excellent'; if ($snr >= 3) return 'good'; if ($snr >= 0) return 'ok'; if ($snr >= -5) return 'weak'; return 'critical'; } function qualityText($snr): string { if ($snr === null || $snr === '') return 'INCONNU'; $snr = (float)$snr; if ($snr >= 8) return 'EXCELLENT'; if ($snr >= 3) return 'GOOD'; if ($snr >= 0) return 'OK'; if ($snr >= -5) return 'WEAK'; return 'CRITICAL'; } function routeText($hops): string { $hops = (int)$hops; if ($hops <= 0) return 'Direct'; if ($hops === 1) return '1 hop'; return $hops . ' hops'; } function formatLocal($value): string { if (!$value) return '—'; try { $d = new DateTime((string)$value, new DateTimeZone('UTC')); $d->setTimezone(new DateTimeZone('America/Toronto')); return $d->format('Y-m-d H:i:s'); } catch (Throwable $e) { return (string)$value; } } function formatUptime($seconds): string { if ($seconds === null) return '—'; $seconds = max(0, (int)$seconds); $days = intdiv($seconds, 86400); $seconds %= 86400; $hours = intdiv($seconds, 3600); $seconds %= 3600; $minutes = intdiv($seconds, 60); $parts = []; if ($days) $parts[] = $days . 'j'; if ($hours) $parts[] = $hours . 'h'; if ($minutes) $parts[] = $minutes . 'm'; return $parts ? implode(' ', $parts) : '<1m'; } try { $db = new PDO( 'sqlite:' . $dbFile, null, null, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ] ); } catch (Throwable $e) { http_response_code(500); die('Base du bot indisponible.'); } $hasRadio = tableExists($db, 'radio_events'); $hasAi = tableExists($db, 'ai_usage'); $hasSmsOut = tableExists($db, 'sms_outgoing'); $hasSmsIn = tableExists($db, 'sms_incoming'); $hasTraceRuns = tableExists($db, 'trace_runs'); $hasTraceHops = tableExists($db, 'trace_hops'); $health = []; $botOnline = false; $context = stream_context_create([ 'http' => [ 'timeout' => 1.5, 'ignore_errors' => true, ] ]); $healthRaw = @file_get_contents($healthUrl, false, $context); if ($healthRaw !== false) { $decoded = json_decode($healthRaw, true); if (is_array($decoded)) { $health = $decoded; $botOnline = (($decoded['status'] ?? '') === 'ok'); } } $stats = [ 'messages_24h' => 0, 'users_24h' => 0, 'messages_1h' => 0, 'users_1h' => 0, ]; $recentEvents = []; $topUsers = []; $recentTraces = []; $aiToday = 0; $smsOut24 = 0; $smsIn24 = 0; if ($hasRadio) { $row = $db->query(" SELECT COUNT(*) AS total, COUNT(DISTINCT sender) AS users FROM radio_events WHERE received_at >= datetime('now','-24 hours') ")->fetch(); $stats['messages_24h'] = (int)($row['total'] ?? 0); $stats['users_24h'] = (int)($row['users'] ?? 0); $row = $db->query(" SELECT COUNT(*) AS total, COUNT(DISTINCT sender) AS users FROM radio_events WHERE received_at >= datetime('now','-1 hour') ")->fetch(); $stats['messages_1h'] = (int)($row['total'] ?? 0); $stats['users_1h'] = (int)($row['users'] ?? 0); $recentEvents = $db->query(" SELECT sender, channel_name, command, snr, hops, received_at FROM radio_events ORDER BY id DESC LIMIT 25 ")->fetchAll(); $topUsers = $db->query(" SELECT sender, COUNT(*) AS total, MAX(received_at) AS last_seen FROM radio_events WHERE received_at >= datetime('now','-24 hours') GROUP BY sender ORDER BY total DESC LIMIT 8 ")->fetchAll(); } if ($hasAi) { $aiToday = (int)$db->query(" SELECT COUNT(*) FROM ai_usage WHERE date(created_at,'localtime') = date('now','localtime') ")->fetchColumn(); } if ($hasSmsOut) { $smsOut24 = (int)$db->query(" SELECT COUNT(*) FROM sms_outgoing WHERE sent_at >= datetime('now','-24 hours') ")->fetchColumn(); } if ($hasSmsIn) { $smsIn24 = (int)$db->query(" SELECT COUNT(*) FROM sms_incoming WHERE received_at >= datetime('now','-24 hours') ")->fetchColumn(); } if ($hasTraceRuns) { $recentTraces = $db->query(" SELECT id, sender, hop_count, weakest_snr, weakest_name, created_at FROM trace_runs ORDER BY id DESC LIMIT 10 ")->fetchAll(); } $commands = [ [ 'group' => 'Radio', 'name' => 'test', 'syntax' => 'test', 'desc' => 'Test radio complet avec route, hops, SNR et maillon le plus faible.', 'badge' => 'recommandé' ], [ 'group' => 'Radio', 'name' => 'ping', 'syntax' => 'ping', 'desc' => 'Réponse rapide du bot avec le nombre de hops et le weakest SNR si disponible.', 'badge' => 'rapide' ], [ 'group' => 'Radio', 'name' => 'link', 'syntax' => 'link', 'desc' => 'Diagnostic détaillé du lien avec les répéteurs et le signal de chaque hop.', 'badge' => 'diagnostic' ], [ 'group' => 'Radio', 'name' => 'snr', 'syntax' => 'snr', 'desc' => 'Affiche les SNR du path et identifie le plus faible.', 'badge' => '' ], [ 'group' => 'Radio', 'name' => 'hop / hops', 'syntax' => 'hops', 'desc' => 'Affiche la route complète, les répéteurs connus et leur signal.', 'badge' => '' ], [ 'group' => 'Radio', 'name' => 'trace', 'syntax' => 'trace [USER]', 'desc' => 'Lance un TRACE MeshCore vers un utilisateur ou vers soi-même.', 'badge' => 'avancé' ], [ 'group' => 'Radio', 'name' => 'diag', 'syntax' => 'diag [USER]', 'desc' => 'Diagnostic complet du path et du maillon le plus faible.', 'badge' => 'avancé' ], [ 'group' => 'IA', 'name' => 'ai', 'syntax' => '!ai ta question', 'desc' => 'Pose une question à Gemini. Limite de 10 questions par jour par utilisateur.', 'badge' => 'IA' ], [ 'group' => 'IA', 'name' => 'aistatus', 'syntax' => 'aistatus', 'desc' => 'Affiche le quota IA utilisé et restant pour la journée.', 'badge' => '' ], [ 'group' => 'IA', 'name' => 'translate', 'syntax' => 'translate en ton texte', 'desc' => 'Traduit un texte vers la langue demandée.', 'badge' => '' ], [ 'group' => 'IA', 'name' => 'resume', 'syntax' => 'resume ton texte', 'desc' => 'Résume un texte en quelques phrases courtes.', 'badge' => '' ], [ 'group' => 'Météo', 'name' => 'meteo', 'syntax' => 'meteo', 'desc' => 'Affiche le menu des villes disponibles.', 'badge' => '' ], [ 'group' => 'Météo', 'name' => 'meteo 0-6', 'syntax' => 'meteo 0', 'desc' => '0 St-Jean · 1 Montréal · 2 Laval · 3 Sorel · 4 Longueuil · 5 Châteauguay · 6 Sherbrooke.', 'badge' => '' ], [ 'group' => 'SMS', 'name' => 'sms', 'syntax' => '!sms tonnumero tonmessage', 'desc' => 'Envoie un SMS via VoIP.ms.', 'badge' => 'action' ], [ 'group' => 'SMS', 'name' => 'reply', 'syntax' => '!reply tonmessage', 'desc' => 'Répond au dernier numéro SMS associé à ton utilisateur MeshCore.', 'badge' => 'action' ], [ 'group' => 'SMS', 'name' => 'smsstatus', 'syntax' => 'smsstatus', 'desc' => 'Affiche la conversation SMS active.', 'badge' => '' ], [ 'group' => 'Mailbox', 'name' => 'msg', 'syntax' => 'msg USER ton message', 'desc' => 'Dépose un message pour un utilisateur qui pourra le lire plus tard.', 'badge' => 'offline' ], [ 'group' => 'Mailbox', 'name' => 'inbox', 'syntax' => 'inbox', 'desc' => 'Affiche les messages en attente dans ta boîte.', 'badge' => '' ], [ 'group' => 'Fun', 'name' => 'dadjoke', 'syntax' => 'dadjoke', 'desc' => 'Dad joke aléatoire. Ajoute en pour une version anglaise.', 'badge' => '' ], [ 'group' => 'Fun', 'name' => 'joke', 'syntax' => 'joke', 'desc' => 'Blague aléatoire FR/EN.', 'badge' => '' ], [ 'group' => 'Fun', 'name' => 'fact', 'syntax' => 'fact', 'desc' => 'Fait aléatoire FR/EN.', 'badge' => '' ], [ 'group' => 'Fun', 'name' => 'coin', 'syntax' => 'coin', 'desc' => 'Pile ou face.', 'badge' => '' ], [ 'group' => 'Fun', 'name' => 'dice', 'syntax' => 'dice 20', 'desc' => 'Lance un dé du nombre de faces demandé.', 'badge' => '' ], [ 'group' => 'Fun', 'name' => 'choose', 'syntax' => 'choose pizza burger sushi', 'desc' => 'Choisit une option au hasard.', 'badge' => '' ], [ 'group' => 'Système', 'name' => 'status', 'syntax' => 'status', 'desc' => 'Affiche le statut général et l’uptime du bot.', 'badge' => '' ], [ 'group' => 'Système', 'name' => 'info', 'syntax' => 'info', 'desc' => 'Affiche les informations générales et l’adresse du dashboard.', 'badge' => '' ], [ 'group' => 'Système', 'name' => 'help', 'syntax' => 'help', 'desc' => 'Affiche l’aide abrégée directement dans MeshCore.', 'badge' => '' ], [ 'group' => 'Système', 'name' => 'advert', 'syntax' => '!advert', 'desc' => 'Envoie un advert FLOOD. Utilise !advert local pour ZERO-HOP.', 'badge' => 'action' ], ]; $groups = []; foreach ($commands as $cmd) { $groups[$cmd['group']] = true; } $groups = array_keys($groups); ?> MeshCore Bot — CECREVIER
📡
MeshCore Bot
CECREVIER · MeshCore Gateway

Commandes d’abord.
Diagnostic ensuite.

Cette page sert de guide rapide pour utiliser le bot MeshCore. Les commandes les plus utiles sont immédiatement visibles, puis les statistiques et l’état du système sont affichés plus bas.

#bots
#test
Gemini
10 questions IA / jour / user
SMS VoIP.ms
TRACE / weakest SNR

Commandes

Recherche ou filtre par catégorie. Les commandes normales peuvent être utilisées sans !, sauf les actions indiquées.

Aperçu

Activité récente du bot et des services associés.

Messages MeshCore / 24h
dans la dernière heure
Utilisateurs / 24h
dans la dernière heure
Requêtes IA aujourd’hui
Tous utilisateurs confondus
SMS / 24h
envoyés · reçus

🔥 Utilisateurs actifs

24h
User Messages Dernier
@
Aucune donnée.

🛰️ Dernières traces

weakest path
User Hops Weakest Maillon
@
Aucun TRACE enregistré.

📨 Activité récente

25 derniers événements
Heure User Canal Commande Hops RX final
@ #
Aucune donnée.

Status système

Placé ici volontairement, en bas de page.

Uptime
Modèle IA
Quota IA /jour/user
Canaux