= h($cmd['syntax']) ?>
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); ?>
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.
Recherche ou filtre par catégorie. Les commandes normales peuvent être utilisées sans !, sauf les actions indiquées.
= h($cmd['syntax']) ?>
Activité récente du bot et des services associés.
| User | Messages | Dernier |
|---|---|---|
| @= h($row['sender']) ?> | = h($row['total']) ?> | = h(formatLocal($row['last_seen'])) ?> |
| Aucune donnée. | ||
| User | Hops | Weakest | Maillon |
|---|---|---|---|
| @= h($row['sender']) ?> | = h($row['hop_count']) ?> | = h(formatSnr($row['weakest_snr'])) ?> | = h($row['weakest_name'] ?: '—') ?> |
| Aucun TRACE enregistré. | |||
| Heure | User | Canal | Commande | Hops | RX final |
|---|---|---|---|---|---|
| = h(formatLocal($row['received_at'])) ?> | @= h($row['sender']) ?> | #= h($row['channel_name']) ?> | = h($row['command'] ?: 'message') ?> | = h(routeText($row['hops'])) ?> | = h(formatSnr($row['snr'])) ?> |
| Aucune donnée. | |||||
Placé ici volontairement, en bas de page.