aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.python-version1
-rw-r--r--__pycache__/app.cpython-313.pycbin0 -> 15762 bytes
-rw-r--r--__pycache__/config.cpython-313.pycbin0 -> 1494 bytes
-rw-r--r--app.py365
-rw-r--r--banner.jpgbin0 -> 382245 bytes
-rw-r--r--cache.json1
-rw-r--r--config.php73
-rw-r--r--config.py74
-rw-r--r--crash.jpgbin0 -> 6481 bytes
-rw-r--r--debug.php231
-rw-r--r--game.jpegbin0 -> 2795333 bytes
-rw-r--r--general.jpgbin0 -> 73705 bytes
-rw-r--r--illit.jpgbin0 -> 29759 bytes
-rw-r--r--index.css395
-rw-r--r--index.php363
-rw-r--r--info.pngbin0 -> 1270596 bytes
-rw-r--r--pyproject.toml11
-rw-r--r--requirements.txt3
-rw-r--r--uv.lock264
19 files changed, 1114 insertions, 667 deletions
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..24ee5b1
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.13
diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc
new file mode 100644
index 0000000..c01cd98
--- /dev/null
+++ b/__pycache__/app.cpython-313.pyc
Binary files differ
diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc
new file mode 100644
index 0000000..eda5147
--- /dev/null
+++ b/__pycache__/config.cpython-313.pyc
Binary files differ
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..5395f2e
--- /dev/null
+++ b/app.py
@@ -0,0 +1,365 @@
+import json
+import random
+import time
+from pathlib import Path
+from urllib.parse import quote
+
+import requests
+from flask import Flask, Response, render_template_string, request, send_from_directory
+from flask_cors import CORS
+
+from config import BASE_DIR, CONFIG
+
+app = Flask(__name__)
+CORS(app)
+
+ALLOWED_STATIC_EXTENSIONS = {
+ ".css",
+ ".png",
+ ".jpg",
+ ".jpeg",
+ ".gif",
+ ".webp",
+ ".bmp",
+ ".svg",
+ ".ico",
+}
+
+
+def safe_url(filename):
+ return quote(str(filename or ""), safe="/")
+
+
+def generate_stars(count, opacity):
+ shadows = []
+ for _ in range(count):
+ x = random.randint(0, 2000)
+ y = random.randint(0, 2000)
+ shadows.append(f"{x}px {y}px rgba(255, 255, 255, {opacity})")
+ return ", ".join(shadows)
+
+
+def fetch_teamspeak_data(cfg):
+ cache_file = Path(cfg["cache_file"])
+ if cache_file.exists() and (time.time() - cache_file.stat().st_mtime) < cfg["cache_time"]:
+ try:
+ return json.loads(cache_file.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ pass
+
+ base_url = f"https://{cfg['host']}:{cfg['port']}/{cfg['server_id']}"
+ headers = {"x-api-key": cfg["api_key"]}
+
+ try:
+ channels_res = requests.get(f"{base_url}/channellist", headers=headers, timeout=2)
+ clients_res = requests.get(f"{base_url}/clientlist?-groups", headers=headers, timeout=2)
+ channels_json = channels_res.json()
+ clients_json = clients_res.json()
+ except requests.RequestException:
+ return {"channels": [], "clients": []}
+ except ValueError:
+ return {"channels": [], "clients": []}
+
+ data = {
+ "channels": channels_json.get("body") or [],
+ "clients": clients_json.get("body") or [],
+ }
+
+ try:
+ cache_file.write_text(json.dumps(data), encoding="utf-8")
+ except OSError:
+ pass
+
+ return data
+
+
+def same_id(left, right):
+ return str(left or 0) == str(right or 0)
+
+
+def sort_ts_channels(channels, pid=0):
+ ordered = []
+ by_order = {}
+
+ for channel in channels:
+ if same_id(channel.get("pid", 0), pid):
+ by_order[str(channel.get("channel_order", 0))] = channel
+
+ current_order = "0"
+ max_iterations = len(channels) + 1
+ iterations = 0
+
+ while current_order in by_order and iterations < max_iterations:
+ channel = by_order[current_order]
+ ordered.append(channel)
+ ordered.extend(sort_ts_channels(channels, channel.get("cid")))
+ current_order = str(channel.get("cid"))
+ iterations += 1
+
+ return ordered
+
+
+def build_context():
+ start_time = time.perf_counter()
+ ts_data = fetch_teamspeak_data(CONFIG["server"])
+ api_ping = round((time.perf_counter() - start_time) * 1000)
+
+ raw_channels = ts_data.get("channels") or []
+ for channel in raw_channels:
+ channel["channel_name"] = channel.get("channel_name").replace("[Cspacer]", "")
+ raw_clients = ts_data.get("clients") or []
+ is_online = bool(raw_channels)
+
+ channels = sort_ts_channels(raw_channels, 0)
+
+ clients_by_channel = {}
+ total_real_clients = 0
+
+ if is_online:
+ for client in raw_clients:
+ if int(client.get("client_type") or 0) == 1:
+ continue
+ clients_by_channel.setdefault(str(client.get("cid")), []).append(client)
+ total_real_clients += 1
+
+ return {
+ "config": CONFIG,
+ "api_ping": api_ping,
+ "channels": channels,
+ "clients_by_channel": clients_by_channel,
+ "is_online": is_online,
+ "safe_url": safe_url,
+ "small_stars": generate_stars(150, 0.4),
+ "medium_stars": generate_stars(50, 0.6),
+ "large_stars": generate_stars(15, 0.8),
+ "total_real_clients": total_real_clients,
+ }
+
+
+FRAGMENT_TEMPLATE = """
+{% if is_online %}
+ <div class="stat-item">
+ <span class="stat-label">Users Online</span>
+ <span class="stat-value">{{ total_real_clients }}</span>
+ </div>
+ <div class="channel-list">
+ {% for channel in channels %}
+ {% set c_name = channel.get('channel_name', '') %}
+ {% set cid = channel.get('cid')|string %}
+ {% set channel_clients = clients_by_channel.get(cid, []) %}
+ {% set bg_img = config.channel_banners.get(c_name, config.generic_images.get('background', '')) %}
+ {% set icon_img = config.channel_icons.get(c_name, config.generic_images.get('icon', '')) %}
+ {% set max_clients = channel.get('channel_maxclients') %}
+
+ {% if bg_img %}
+ {% set bg_style = "background-image: linear-gradient(90deg, rgba(9, 18, 32, 0.96) 0%, rgba(9, 18, 32, 0.78) 45%, rgba(9, 18, 32, 0.94) 100%), url('" ~ safe_url(bg_img) ~ "');" %}
+ {% else %}
+ {% set bg_style = "" %}
+ {% endif %}
+
+ <article class="channel">
+ <header class="channel-header" style="{{ bg_style }}">
+ <div class="channel-title-group">
+ {% if icon_img %}
+ <img src="{{ safe_url(icon_img) }}" class="channel-icon" alt="" aria-hidden="true">
+ {% endif %}
+ <h2>{{ c_name }}</h2>
+ </div>
+ {# Only show client count for non-spacer channels #}
+ {% if '[spacer' not in c_name %}
+ <span class="channel-meta">
+ {{ channel_clients|length }} / {{ '∞' if max_clients == -1 or max_clients == '-1' else max_clients }}
+ </span>
+ {% endif %}
+ </header>
+
+ {# Only show client list for non-spacer channels #}
+ {% if '[spacer' not in c_name|lower and '[cspacer' not in c_name|lower and channel_clients %}
+ <ul class="client-list">
+ {% for client in channel_clients %}
+ <li class="client">
+ {% set role_icon = namespace(path='') %}
+ {% for group_id in (client.get('client_servergroups', '')|string).split(',') %}
+ {% if not role_icon.path and group_id in config.role_icons %}
+ {% set role_icon.path = config.role_icons[group_id] %}
+ {% endif %}
+ {% endfor %}
+
+ {% if role_icon.path %}
+ <img src="{{ safe_url(role_icon.path) }}" class="role-icon" alt="Role">
+ {% else %}
+ <span class="client-no-role-icon"></span>
+ {% endif %}
+ <span>{{ client.get('client_nickname', '') }}</span>
+ </li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ </article>
+ {% endfor %}
+ </div>
+{% else %}
+ <div class="empty-server">Unable to reach the TeamSpeak server.</div>
+{% endif %}
+"""
+
+PAGE_TEMPLATE = """
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="{{ config.ui.subtitle }}">
+ <title>{{ config.ui.title }}</title>
+ <link rel="icon" type="image/png" href="logo.png">
+ <link rel="stylesheet" href="index.css">
+ <style>
+ :root {
+ --bg-color: {{ config.ui.background_color }};
+ --panel-bg: {{ config.ui.panel_color }};
+ --accent: {{ config.ui.accent_color }};
+ --text-main: {{ config.ui.text_color }};
+ }
+
+ {% if config.ui.enable_stars %}
+ .stars-s { box-shadow: {{ small_stars }}; }
+ .stars-m { box-shadow: {{ medium_stars }}; }
+ .stars-l { box-shadow: {{ large_stars }}; }
+ {% endif %}
+ </style>
+</head>
+<body>
+
+{% if config.ui.enable_stars %}
+<div id="space-bg" aria-hidden="true">
+ <div class="star-layer stars-s"></div>
+ <div class="star-layer stars-m"></div>
+ <div class="star-layer stars-l"></div>
+</div>
+{% endif %}
+
+<main class="app-container">
+ {% if is_online %}<div class="progress-bar" id="refresh-bar"></div>{% endif %}
+
+ {% if config.ui.banner_url %}
+ <img src="{{ safe_url(config.ui.banner_url) }}" alt="Header banner" class="banner">
+ {% endif %}
+
+ <div class="content-wrapper">
+ <header class="header">
+ <span class="eyebrow">TeamSpeak Server Status</span>
+ <h1>{{ config.ui.title }}</h1>
+ <p>{{ config.ui.subtitle }}</p>
+ {% if is_online %}
+ <div class="status-badge status-online"><span class="pulse-dot"></span>SERVER ONLINE</div>
+ {% else %}
+ <div class="status-badge status-offline"><span class="pulse-dot"></span> SERVER OFFLINE</div>
+ {% endif %}
+ </header>
+
+ <a href="ts3server://{{ config.server.ts_domain }}" class="btn-primary">{{ config.buttons.join }}</a>
+ <p class="help-text">{{ config.buttons.download_text }} <a href="{{ config.buttons.download_url }}" target="_blank" rel="noopener">{{ config.buttons.download_link }}</a></p>
+
+ <section id="ajax-container" aria-live="polite">
+ {{ html_fragment|safe }}
+ </section>
+
+ {% if config.external_links %}
+ <nav class="footer-links">
+ {% for name, url in config.external_links.items() %}
+ <a href="{{ url }}" target="_blank" rel="noopener">{{ name }}</a>
+ {% endfor %}
+ </nav>
+ {% endif %}
+
+ <footer class="credits">
+ {{ config.footer.text }} <a href="{{ config.footer.author_url }}" target="_blank" rel="noopener">{{ config.footer.author }}</a> — Refresh scan: <span id="timer-text">{{ config.server.cache_time }}</span>s
+ </footer>
+ </div>
+</main>
+
+<script>
+ {% if is_online %}
+ document.addEventListener("DOMContentLoaded", () => {
+ let timeLeft = {{ config.server.cache_time|int }};
+ const totalTime = {{ config.server.cache_time|int }};
+
+ const progressBar = document.getElementById('refresh-bar');
+ const timerText = document.getElementById('timer-text');
+ const container = document.getElementById('ajax-container');
+
+ function resetProgressBar() {
+ progressBar.style.transition = 'none';
+ progressBar.style.width = '100%';
+ void progressBar.offsetWidth;
+ progressBar.style.transition = `width ${totalTime}s linear`;
+ progressBar.style.width = '0%';
+ }
+
+ resetProgressBar();
+
+ setInterval(() => {
+ timeLeft--;
+ timerText.innerText = timeLeft;
+
+ if (timeLeft <= 0) {
+ container.style.opacity = '0.5';
+ fetch(window.location.href.split('?')[0] + '?ajax=1', { cache: 'no-store', headers: {'X-Requested-With': 'XMLHttpRequest'} })
+ .then(r => {
+ if (!r.ok) throw new Error("HTTP error " + r.status);
+ return r.text();
+ })
+ .then(html => {
+ container.innerHTML = html;
+ container.style.opacity = '1';
+
+ timeLeft = totalTime;
+ timerText.innerText = timeLeft;
+
+ resetProgressBar();
+ })
+ .catch(err => {
+ console.error("Refresh failed:", err);
+ container.style.opacity = '1';
+ timeLeft = totalTime;
+ resetProgressBar();
+ });
+ }
+ }, 1000);
+ });
+ {% endif %}
+</script>
+</body>
+</html>
+"""
+
+
+def render_fragment(context):
+ return render_template_string(FRAGMENT_TEMPLATE, **context)
+
+
+@app.route("/")
+def index():
+ context = build_context()
+ html_fragment = render_fragment(context)
+
+ if request.args.get("ajax") == "1":
+ return Response(html_fragment, mimetype="text/html; charset=utf-8")
+
+ return render_template_string(PAGE_TEMPLATE, html_fragment=html_fragment, **context)
+
+
+@app.route("/<path:filename>")
+def static_asset(filename):
+ path = (BASE_DIR / filename).resolve()
+ if (
+ path.is_file()
+ and BASE_DIR in path.parents
+ and path.suffix.lower() in ALLOWED_STATIC_EXTENSIONS
+ ):
+ return send_from_directory(BASE_DIR, filename)
+ return Response("Not found", status=404)
+
+
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=5000)
diff --git a/banner.jpg b/banner.jpg
new file mode 100644
index 0000000..31b5611
--- /dev/null
+++ b/banner.jpg
Binary files differ
diff --git a/cache.json b/cache.json
new file mode 100644
index 0000000..c2336bd
--- /dev/null
+++ b/cache.json
@@ -0,0 +1 @@
+{"channels": [{"channel_name": "[Cspacer]INFORMATION", "channel_needed_subscribe_power": "0", "channel_order": "0", "cid": "13", "pid": "0", "total_clients": "0"}, {"channel_name": "[Cspacer]GAME SERVERS", "channel_needed_subscribe_power": "0", "channel_order": "13", "cid": "17", "pid": "0", "total_clients": "0"}, {"channel_name": "[Cspacer]\ud83d\udd07 AFK NPCs \ud83d\udd07", "channel_needed_subscribe_power": "0", "channel_order": "17", "cid": "4", "pid": "0", "total_clients": "1"}, {"channel_name": "General", "channel_needed_subscribe_power": "0", "channel_order": "4", "cid": "1", "pid": "0", "total_clients": "0"}, {"channel_name": "CRASHING OUT", "channel_needed_subscribe_power": "0", "channel_order": "1", "cid": "9", "pid": "0", "total_clients": "0"}, {"channel_name": "Wonhee HQ", "channel_needed_subscribe_power": "0", "channel_order": "9", "cid": "10", "pid": "0", "total_clients": "1"}], "clients": [{"cid": "4", "clid": "71", "client_channel_group_id": "8", "client_channel_group_inherited_channel_id": "4", "client_database_id": "1", "client_nickname": "serveradmin", "client_servergroups": "2", "client_type": "1"}, {"cid": "10", "clid": "1", "client_channel_group_id": "8", "client_channel_group_inherited_channel_id": "10", "client_database_id": "29", "client_nickname": "The Real Wonhee", "client_servergroups": "8", "client_type": "0"}]} \ No newline at end of file
diff --git a/config.php b/config.php
deleted file mode 100644
index 6a40406..0000000
--- a/config.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-/**
- * Configuration de l'Explorateur TeamSpeak
- * Renommez ce fichier en 'config.php' et remplissez vos informations.
- */
-
-return [
- // Paramètres de connexion au serveur
- 'server' => [
- 'host' => '127.0.0.1', // IP du serveur (ex: 192.168.1.50)
- 'port' => '10080', // Port HTTP WebQuery (TS6)
- 'api_key' => 'VOTRE_CLE_API_ICI',
- 'server_id' => '1',
- 'ts_domain' => 'ts.votre-domaine.com', // Utilisé pour le bouton de connexion
- 'cache_time' => 30, // Actualisation en secondes (anti-spam)
- 'cache_file' => __DIR__ . '/cache.json'
- ],
-
- // Design et Textes principaux
- 'ui' => [
- 'titre' => 'Mon Serveur TeamSpeak',
- 'sous_titre' => 'Bienvenue sur notre espace de discussion.',
- 'couleur_fond' => '#070a10', // Couleur de fond principale
- 'couleur_panneau' => '#131823', // Couleur des blocs de canaux
- 'couleur_accent' => '#3b82f6', // Couleur des boutons et de la barre de temps
- 'couleur_texte' => '#e2e8f0',
- 'banniere_url' => '', // Laissez vide ('') pour ne pas afficher d'en-tête
- 'activer_etoiles' => false, // Passez à true pour activer une animation spatiale en arrière-plan
- ],
-
- // Textes et liens de l'interface
- 'boutons' => [
- 'rejoindre' => 'Se connecter au serveur',
- 'telecharger_texte' => 'Le logiciel n\'est pas installé sur votre PC ?',
- 'telecharger_lien' => 'Télécharger TeamSpeak',
- 'telecharger_url' => 'https://www.teamspeak.com/fr/downloads/' // Lien officiel au cas où il change
- ],
-
- // Liens utiles (Ajoutez ou retirez autant de lignes que vous le souhaitez)
- 'liens_externes' => [
- 'Notre Site Web' => 'https://www.votre-site.com',
- 'Discord / Forum'=> 'https://discord.gg/votre-lien'
- ],
-
- // Pied de page
- 'footer' => [
- 'texte' => 'Hébergé par',
- 'auteur' => 'VotrePseudo',
- 'lien_auteur' => 'https://github.com/votre-pseudo'
- ],
-
- // Personnalisation des canaux (Nom exact du canal => nom du fichier image)
- 'bannieres_canaux' => [
- // 'Accueil' => 'accueil_fond.png',
- // 'Général' => 'general_fond.png',
- ],
-
- 'icones_canaux' => [
- // 'Accueil' => 'accueil_icone.png',
- // 'Général' => 'general_icone.png',
- ],
-
- 'images_generiques' => [
- 'fond' => '', // Image appliquée par défaut aux canaux inconnus
- 'icone' => '' // Icône appliquée par défaut aux canaux inconnus
- ],
-
- // Groupes de serveur (Rôles) : 'ID_DU_GROUPE' => 'Image'
- 'icones_roles' => [
- // '6' => 'admin.png',
- // '9' => 'vip.png',
- ]
-]; \ No newline at end of file
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..936b159
--- /dev/null
+++ b/config.py
@@ -0,0 +1,74 @@
+"""TeamSpeak Web Explorer configuration."""
+
+from pathlib import Path
+
+BASE_DIR = Path(__file__).resolve().parent
+
+CONFIG = {
+ # Server connection settings
+ "server": {
+ "host": "chat.moekyun.me", # Server IP or hostname, e.g. 192.168.1.50
+ "port": "8443", # HTTP WebQuery port (TS6)
+ "api_key": "",
+ "server_id": "1",
+ "ts_domain": "chat.moekyun.me", # Used by the connect button
+ "cache_time": 30, # Refresh interval in seconds (anti-spam)
+ "cache_file": BASE_DIR / "cache.json",
+ },
+
+ # Main design and copy
+ "ui": {
+ "title": "Overwatch Diddly Server",
+ "subtitle": "Jump into voice, organize channels, and see who is online.",
+ "background_color": "#07111f", # Main background color
+ "panel_color": "#101b2d", # Main panel and channel block color
+ "accent_color": "#2589ff", # Buttons, highlights, and refresh timer
+ "text_color": "#e8f2ff",
+ "banner_url": "banner.jpg", # Leave empty ('') to hide the header banner
+ "enable_stars": True, # Set to True to enable a subtle animated space background
+ },
+
+ # Interface buttons and links
+ "buttons": {
+ "join": "Connect to TeamSpeak",
+ "download_text": "Teamspeak is not installed yet?",
+ "download_link": "Download Teamspeak",
+ "download_url": "https://www.teamspeak.com/en/downloads/", # Official download link
+ },
+
+ # Useful links. Add or remove as many rows as you want.
+ "external_links": {
+ },
+
+ # Footer
+ "footer": {
+ "text": "another moekyun service",
+ "author": "",
+ "author_url": "",
+ },
+
+ # Channel customization. Exact channel name => image filename.
+ "channel_banners": {
+ "INFORMATION": "info.png",
+ "GAME SERVERS": "game.jpeg",
+ "General": "general.jpg",
+ "CRASHING OUT": "crash.png",
+ "Wonhee HQ": "illit.jpg",
+ },
+
+ "channel_icons": {
+ # "Lobby": "lobby_icon.png",
+ # "General": "general_icon.png",
+ },
+
+ "generic_images": {
+ "background": "", # Default image applied to unknown channels
+ "icon": "", # Default icon applied to unknown channels
+ },
+
+ # Server groups / roles: 'GROUP_ID' => 'Image'
+ "role_icons": {
+ # "6": "admin.png",
+ # "9": "vip.png",
+ },
+}
diff --git a/crash.jpg b/crash.jpg
new file mode 100644
index 0000000..bdf4a00
--- /dev/null
+++ b/crash.jpg
Binary files differ
diff --git a/debug.php b/debug.php
deleted file mode 100644
index 5f8e39c..0000000
--- a/debug.php
+++ /dev/null
@@ -1,231 +0,0 @@
-<?php
-/**
- * Outil de Diagnostic Avancé - Explorateur Web TeamSpeak 6
- * Auteur : Guraz
- * ⚠️ ATTENTION : NE LAISSEZ PAS CE FICHIER SUR UN SERVEUR EN PRODUCTION ⚠️
- */
-
-// LA FONCTION MANQUANTE EST LÀ 👇
-function e($string) { return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8'); }
-
-$config_file = __DIR__ . '/config.php';
-$has_config = file_exists($config_file);
-
-if ($has_config) {
- $config = require $config_file;
- $cfg = $config['server'];
- $api_key_masked = empty($cfg['api_key']) ? "NON DÉFINIE" : substr($cfg['api_key'], 0, 6) . str_repeat('•', 20) . substr($cfg['api_key'], -4);
-}
-
-// Utilitaires de diagnostic
-function check_system_reqs() {
- $reqs = [];
- $reqs['PHP Version'] = [
- 'status' => version_compare(PHP_VERSION, '7.4.0', '>='),
- 'value' => PHP_VERSION,
- 'msg' => 'PHP 7.4 ou supérieur recommandé.'
- ];
- $reqs['allow_url_fopen'] = [
- 'status' => ini_get('allow_url_fopen'),
- 'value' => ini_get('allow_url_fopen') ? 'Activé' : 'Désactivé',
- 'msg' => 'Requis pour communiquer avec l\'API distante.'
- ];
- $reqs['Permissions Dossier'] = [
- 'status' => is_writable(__DIR__),
- 'value' => is_writable(__DIR__) ? 'Écriture OK' : 'Lecture seule',
- 'msg' => 'Requis pour créer le fichier cache.json.'
- ];
- return $reqs;
-}
-
-function test_tcp_connection($host, $port) {
- $start = microtime(true);
- $fp = @fsockopen($host, $port, $errno, $errstr, 2);
- $time = round((microtime(true) - $start) * 1000);
-
- if (!$fp) {
- return ['status' => false, 'msg' => "Connexion refusée ({$errstr})", 'time' => '-'];
- } else {
- fclose($fp);
- return ['status' => true, 'msg' => 'Port ouvert et accessible', 'time' => $time . ' ms'];
- }
-}
-
-function test_endpoint($url, $api_key, $endpoint_name) {
- $options = [
- 'http' => [
- 'header' => "x-api-key: {$api_key}\r\n",
- 'method' => 'GET',
- 'timeout' => 3,
- 'ignore_errors' => true
- ]
- ];
- $context = stream_context_create($options);
-
- $start_time = microtime(true);
- $response = @file_get_contents($url, false, $context);
- $ping = round((microtime(true) - $start_time) * 1000);
-
- $http_code = "Inconnu";
- if (isset($http_response_header) && is_array($http_response_header)) {
- if (preg_match('#HTTP/[0-9\.]+\s+([0-9]+)#', $http_response_header[0], $matches)) {
- $http_code = intval($matches[1]);
- }
- }
-
- $html = "<div class='endpoint-card'><div class='endpoint-header'>";
- $html .= "<h2>➔ /{$endpoint_name}</h2><div class='endpoint-meta'>";
-
- if ($response === false) {
- $html .= "<span class='badge error'>Échec HTTP (Timeout)</span></div></div>";
- $html .= "<div class='endpoint-body'><p style='color: #f87171;'>Impossible de récupérer les données HTTP.</p></div>";
- } else {
- $data = json_decode($response, true);
- $ts_code = isset($data['status']['code']) ? $data['status']['code'] : 'N/A';
-
- if ($http_code == 200 && $ts_code === 0) {
- $html .= "<span class='badge success'>HTTP 200</span> <span class='badge success'>TS Code: 0 (OK)</span>";
- } elseif ($ts_code === 5120) {
- $html .= "<span class='badge warning'>HTTP {$http_code}</span> <span class='badge warning'>TS Code: 5120 (Out of Scope)</span>";
- } elseif ($http_code == 401 || $ts_code === 2568) {
- $html .= "<span class='badge error'>HTTP {$http_code}</span> <span class='badge error'>TS Code: {$ts_code} (Clé Invalide)</span>";
- } else {
- $html .= "<span class='badge error'>HTTP {$http_code}</span> <span class='badge error'>TS Code: {$ts_code}</span>";
- }
- $html .= "<span class='badge info'>{$ping} ms</span></div></div>";
-
- if ($ts_code === 5120 && $endpoint_name == 'serverinfo') {
- $html .= "<div class='help-box'>ℹ️ <b>Note :</b> Erreur 5120 normale si votre clé API est configurée en lecture seule (scope=read).</div>";
- }
-
- $pretty_json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
- $html .= "<div class='endpoint-body'><pre><code>" . htmlspecialchars($pretty_json) . "</code></pre></div>";
- }
- $html .= "</div>";
- return $html;
-}
-?>
-<!DOCTYPE html>
-<html lang="fr">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Diagnostic API TS6</title>
- <style>
- :root { --bg: #0f172a; --panel: #1e293b; --text: #f8fafc; --text-muted: #94a3b8; --border: #334155; }
- body { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; background-color: var(--bg); color: var(--text); margin: 0; padding: 2rem; line-height: 1.5; }
- .container { max-width: 900px; margin: 0 auto; }
-
- .alert-danger { background-color: rgba(239, 68, 68, 0.2); border: 1px solid #ef4444; color: #fca5a5; padding: 1rem; border-radius: 8px; text-align: center; font-weight: bold; margin-bottom: 2rem; }
- .section-title { font-size: 1.5rem; color: #38bdf8; border-bottom: 2px solid var(--border); padding-bottom: 0.5rem; margin-top: 2rem; margin-bottom: 1rem; }
-
- .grid-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
- .card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
- .card h3 { margin-top: 0; font-size: 1.1rem; color: #e2e8f0; margin-bottom: 1rem; }
-
- .item-row { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.05); padding: 0.5rem 0; }
- .item-row:last-child { border-bottom: none; }
- .item-label { color: var(--text-muted); font-size: 0.9rem; }
- .item-val { font-weight: bold; }
- .val-ok { color: #10b981; } .val-err { color: #ef4444; } .val-warn { color: #f59e0b; }
-
- .endpoint-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 1.5rem; overflow: hidden; }
- .endpoint-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; background: rgba(0,0,0,0.2); border-bottom: 1px solid var(--border); }
- .endpoint-header h2 { margin: 0; font-size: 1.1rem; color: #eab308; }
- .endpoint-meta { display: flex; gap: 0.5rem; }
-
- .badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold; }
- .badge.success { background: rgba(16, 185, 129, 0.2); color: #6ee7b7; border: 1px solid #10b981; }
- .badge.error { background: rgba(239, 68, 68, 0.2); color: #fca5a5; border: 1px solid #ef4444; }
- .badge.warning { background: rgba(245, 158, 11, 0.2); color: #fcd34d; border: 1px solid #f59e0b; }
- .badge.info { background: rgba(56, 189, 248, 0.2); color: #7dd3fc; border: 1px solid #38bdf8; }
-
- .help-box { margin: 1rem 1.5rem 0 1.5rem; padding: 0.75rem; background: rgba(56, 189, 248, 0.1); border-left: 4px solid #38bdf8; font-size: 0.9rem; color: #bae6fd; }
- .endpoint-body pre { margin: 0; padding: 1.5rem; overflow-x: auto; font-size: 0.85rem; color: #a7f3d0; max-height: 400px; }
- </style>
-</head>
-<body>
-
-<div class="container">
- <div class="alert-danger">
- ⚠️ DANGER : NE LAISSEZ JAMAIS CE FICHIER SUR VOTRE SERVEUR WEB PUBLIC.<br>
- Supprimez 'debug.php' de votre hébergement après utilisation.
- </div>
-
- <?php if (!$has_config): ?>
- <div class="card" style="border-color: #ef4444;">
- <h3 style="color: #fca5a5;">❌ Fichier config.php introuvable</h3>
- <p>Veuillez créer votre fichier <code>config.php</code> avant de lancer le diagnostic.</p>
- </div>
- <?php else: ?>
-
- <h2 class="section-title">🔍 Diagnostic de l'Environnement</h2>
-
- <div class="grid-cards">
- <div class="card">
- <h3>⚙️ Configuration Détectée</h3>
- <div class="item-row"><span class="item-label">Hôte API</span><span class="item-val"><?php echo e($cfg['host']); ?></span></div>
- <div class="item-row"><span class="item-label">Port API</span><span class="item-val"><?php echo e($cfg['port']); ?></span></div>
- <div class="item-row"><span class="item-label">ID Serveur</span><span class="item-val"><?php echo e($cfg['server_id']); ?></span></div>
- <div class="item-row"><span class="item-label">Clé API</span><span class="item-val" style="color: #fb923c;"><?php echo e($api_key_masked); ?></span></div>
- </div>
-
- <div class="card">
- <h3>💻 Serveur Web (PHP)</h3>
- <?php foreach (check_system_reqs() as $name => $req): ?>
- <div class="item-row">
- <span class="item-label" title="<?php echo e($req['msg']); ?>"><?php echo e($name); ?></span>
- <span class="item-val <?php echo $req['status'] ? 'val-ok' : 'val-err'; ?>">
- <?php echo $req['status'] ? '✓ ' : '✗ '; ?><?php echo e($req['value']); ?>
- </span>
- </div>
- <?php endforeach; ?>
- </div>
-
- <div class="card">
- <h3>📡 Réseau & Cache</h3>
- <?php
- $net = test_tcp_connection($cfg['host'], $cfg['port']);
- $cache_exists = file_exists($cfg['cache_file']);
- ?>
- <div class="item-row">
- <span class="item-label">Ping TCP (<?php echo e($cfg['port']); ?>)</span>
- <span class="item-val <?php echo $net['status'] ? 'val-ok' : 'val-err'; ?>" title="<?php echo e($net['msg']); ?>">
- <?php echo $net['status'] ? '✓ ' : '✗ '; ?><?php echo e($net['time']); ?>
- </span>
- </div>
- <div class="item-row">
- <span class="item-label">Fichier Cache</span>
- <span class="item-val <?php echo $cache_exists ? 'val-ok' : 'val-warn'; ?>">
- <?php echo $cache_exists ? '✓ Présent' : 'Non généré'; ?>
- </span>
- </div>
- <?php if ($cache_exists): ?>
- <div class="item-row">
- <span class="item-label">Âge du Cache</span>
- <span class="item-val"><?php echo (time() - filemtime($cfg['cache_file'])) . ' sec'; ?></span>
- </div>
- <div class="item-row">
- <span class="item-label">Taille du Cache</span>
- <span class="item-val"><?php echo round(filesize($cfg['cache_file']) / 1024, 1) . ' KB'; ?></span>
- </div>
- <?php endif; ?>
- </div>
- </div>
-
- <h2 class="section-title">🔌 Tests de l'API TeamSpeak</h2>
-
- <?php
- $base_url = "http://{$cfg['host']}:{$cfg['port']}/{$cfg['server_id']}";
-
- // Exécution des tests API
- echo test_endpoint("{$base_url}/channellist", $cfg['api_key'], "channellist");
- echo test_endpoint("{$base_url}/clientlist?-groups", $cfg['api_key'], "clientlist?-groups");
- echo test_endpoint("{$base_url}/serverinfo", $cfg['api_key'], "serverinfo");
- ?>
-
- <?php endif; ?>
-</div>
-
-</body>
-</html> \ No newline at end of file
diff --git a/game.jpeg b/game.jpeg
new file mode 100644
index 0000000..d651107
--- /dev/null
+++ b/game.jpeg
Binary files differ
diff --git a/general.jpg b/general.jpg
new file mode 100644
index 0000000..f2f5bc0
--- /dev/null
+++ b/general.jpg
Binary files differ
diff --git a/illit.jpg b/illit.jpg
new file mode 100644
index 0000000..60f3655
--- /dev/null
+++ b/illit.jpg
Binary files differ
diff --git a/index.css b/index.css
new file mode 100644
index 0000000..26229e2
--- /dev/null
+++ b/index.css
@@ -0,0 +1,395 @@
+:root {
+ --cl-black: hsl(140, 1%, 6%);
+ --cl-gray-0: hsl(140, 2%, 8%);
+ --cl-gray-1: hsl(140, 2%, 12%);
+ --cl-gray-2: hsl(140, 4%, 16%);
+ --cl-gray-3: hsl(140, 4%, 24%);
+ --cl-gray-4: hsl(140, 4%, 36%);
+ --cl-gray-5: hsl(140, 4%, 44%);
+ --cl-gray-6: hsl(80, 8%, 52%);
+ --cl-gray-7: hsl(70, 8%, 58%);
+ --cl-gray-8: hsl(60, 16%, 66%);
+ --cl-gray-9: hsl(40, 32%, 78%);
+ --cl-gray-10: hsl(30, 32%, 84%);
+ --cl-white: hsl(26, 64%, 88%);
+ --cl-red-6: hsl(4, 83%, 67%);
+ --cl-red-7: hsl(4, 75%, 75%);
+ --cl-orange-6: hsl(26, 84%, 62%);
+ --cl-orange-7: hsl(26, 84%, 74%);
+ --cl-yellow: hsl(37, 80%, 69%);
+ --cl-green-6: hsl(120, 41%, 64%);
+ --cl-green-7: hsl(120, 42%, 75%);
+ --cl-cyan-6: hsl(160, 41%, 64%);
+ --cl-cyan-7: hsl(160, 32%, 75%);
+ --cl-blue-6: hsl(200, 55%, 64%);
+ --cl-blue-8: hsl(201, 55%, 80%);
+ --cl-magenta-7: hsl(320, 59%, 72%);
+ --cl-magenta-8: hsl(320, 61%, 80%);
+}
+
+* {
+ box-sizing: border-box;
+ border-radius: 0px !important;
+}
+
+body {
+ min-height: 100vh;
+ margin: 5vh 10vw;
+ display: flex;
+ justify-content: center;
+ font-family: "Iosevka", "Roboto Mono", monospace;
+ background-color: var(--cl-black);
+ color: var(--cl-white);
+}
+
+/* Neutralize the old space background elements */
+body::before,
+#space-bg,
+.star-layer,
+.stars-s,
+.stars-m,
+.stars-l {
+ display: none;
+}
+
+.app-container {
+ position: relative;
+ width: 100%;
+ max-width: 760px;
+ height: fit-content;
+ background-color: var(--cl-gray-0);
+ border: 2px solid var(--cl-gray-2);
+}
+
+.app-container::before {
+ display: none;
+}
+
+.banner {
+ width: 100%;
+ height: auto;
+ display: block;
+ border-bottom: 2px solid var(--cl-gray-2);
+}
+
+.content-wrapper {
+ position: relative;
+ padding: 2rem;
+}
+
+.progress-bar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 10;
+ width: 100%;
+ height: 4px;
+ background-color: var(--cl-orange-6);
+}
+
+.header {
+ margin-bottom: 1.75rem;
+ text-align: left;
+}
+
+.eyebrow {
+ display: block;
+ margin-bottom: 0.65rem;
+ color: var(--cl-gray-6);
+ font-size: 0.85rem;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
+.eyebrow::before,
+.eyebrow::after {
+ display: none;
+}
+
+.header h1 {
+ margin: 0 0 0.5rem;
+ color: var(--cl-white);
+ font-size: 2.2rem;
+ font-weight: bold;
+}
+
+.header p {
+ max-width: 100%;
+ margin: 0;
+ color: var(--cl-gray-7);
+ font-size: 1rem;
+ line-height: 1.6;
+}
+
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ margin-top: 1rem;
+ padding: 4px 12px;
+ font-size: 0.85rem;
+ font-weight: bold;
+ text-transform: uppercase;
+ border: 2px solid;
+}
+
+.status-online {
+ color: var(--cl-green-6);
+ border-color: var(--cl-green-6);
+ background-color: transparent;
+}
+
+.status-offline {
+ color: var(--cl-red-6);
+ border-color: var(--cl-red-6);
+ background-color: transparent;
+}
+
+.pulse-dot {
+ width: 10px;
+ height: 10px;
+ margin-right: 0.75rem;
+ background-color: currentColor;
+}
+
+.server-stats {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ border: 2px solid var(--cl-gray-2);
+ background-color: var(--cl-gray-1);
+}
+
+.stat-item {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-bottom: 10px;
+ padding: 0.75rem;
+ border: 2px solid var(--cl-gray-2);
+ background-color: var(--cl-gray-0);
+}
+
+.stat-label {
+ margin-bottom: 0.5rem;
+ color: var(--cl-gray-6);
+ font-size: 0.75rem;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
+.stat-value {
+ color: var(--cl-white);
+ font-size: 1.25rem;
+ font-weight: bold;
+}
+
+.btn-primary {
+ display: block;
+ width: 100%;
+ margin-bottom: 1rem;
+ padding: 12px;
+ color: var(--cl-orange-6);
+ text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ font-size: 1rem;
+ font-weight: bold;
+ border: 2px solid var(--cl-orange-6);
+ background-color: transparent;
+}
+
+.btn-primary::before {
+ content: "[ ";
+}
+
+.btn-primary::after {
+ content: " ]";
+}
+
+.btn-primary:hover {
+ color: var(--cl-black);
+ background-color: var(--cl-magenta-7);
+ border-color: transparent;
+}
+
+.help-text {
+ margin: 1rem 0 2rem;
+ color: var(--cl-gray-6);
+ text-align: left;
+ font-size: 0.9rem;
+}
+
+.help-text a {
+ color: var(--cl-orange-6);
+ text-decoration: none;
+ border-bottom: 2px solid var(--cl-orange-6);
+}
+
+.help-text a:hover {
+ color: var(--cl-black);
+ background-color: var(--cl-magenta-7);
+ border-color: transparent;
+}
+
+.channel-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.channel {
+ border: 2px solid var(--cl-gray-2);
+ background-color: var(--cl-gray-1);
+}
+
+.channel-header {
+ padding: 0.75rem 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ color: var(--cl-white);
+ border-bottom: 2px solid var(--cl-gray-2);
+ background-color: var(--cl-gray-2);
+}
+
+.channel-title-group {
+ display: flex;
+ align-items: center;
+ margin: 0;
+}
+
+.channel-title-group::before {
+ content: ">";
+ margin-right: 0.75rem;
+ color: var(--cl-orange-6);
+ font-weight: bold;
+}
+
+.channel-title-group h2 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: bold;
+}
+
+.channel-icon {
+ display: none;
+}
+
+.channel-meta {
+ padding: 4px 8px;
+ color: var(--cl-cyan-6);
+ font-size: 0.85rem;
+ font-weight: bold;
+ border: 2px solid var(--cl-cyan-6);
+}
+
+.client-list {
+ margin: 0;
+ padding: 0.5rem;
+ list-style: none;
+}
+
+.client {
+ padding: 0.5rem 1rem;
+ display: flex;
+ align-items: center;
+ color: var(--cl-gray-8);
+ font-size: 0.95rem;
+ border: 2px solid transparent;
+}
+
+.client::before {
+ content: "-";
+ margin-right: 1rem;
+ color: var(--cl-gray-5);
+}
+
+.client:hover {
+ color: var(--cl-white);
+ background-color: var(--cl-gray-2);
+ border-color: var(--cl-gray-4);
+}
+
+.role-icon,
+.client-no-role-icon {
+ display: none;
+}
+
+.empty-server {
+ padding: 2rem 1rem;
+ color: var(--cl-gray-6);
+ text-align: center;
+ font-size: 1rem;
+ border: 2px dashed var(--cl-gray-4);
+ background-color: var(--cl-gray-1);
+}
+
+.footer-links {
+ margin-top: 2.5rem;
+ padding-top: 1.5rem;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ gap: 1rem;
+ border-top: 2px solid var(--cl-gray-2);
+}
+
+.footer-links a {
+ color: var(--cl-orange-6);
+ font-size: 0.9rem;
+ font-weight: bold;
+ text-decoration: none;
+ border: 2px solid transparent;
+ padding: 2px 4px;
+}
+
+.footer-links a:hover {
+ color: var(--cl-black);
+ background-color: var(--cl-magenta-7);
+}
+
+.credits {
+ margin-top: 1.5rem;
+ color: var(--cl-gray-6);
+ text-align: left;
+ font-size: 0.85rem;
+}
+
+.credits a {
+ color: var(--cl-orange-6);
+ text-decoration: none;
+ border-bottom: 2px solid var(--cl-orange-6);
+}
+
+.credits a:hover {
+ color: var(--cl-black);
+ background-color: var(--cl-magenta-7);
+ border-color: transparent;
+}
+
+#ajax-container {
+ transition: none;
+}
+
+@media (max-width: 640px) {
+ body {
+ margin: 2vh 4vw;
+ }
+
+ .content-wrapper {
+ padding: 1.25rem;
+ }
+
+ .server-stats {
+ grid-template-columns: 1fr;
+ }
+
+ .channel-header {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 0.75rem;
+ }
+}
diff --git a/index.php b/index.php
deleted file mode 100644
index 60d3239..0000000
--- a/index.php
+++ /dev/null
@@ -1,363 +0,0 @@
-<?php
-/**
- * Explorateur Web - TeamSpeak 6
- */
-
-$config_file = __DIR__ . '/config.php';
-if (!file_exists($config_file)) {
- die("Erreur : Le fichier 'config.php' est introuvable. Veuillez le créer à partir de 'config.example.php'.");
-}
-$config = require $config_file;
-
-// Utilitaires
-function e($string) { return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8'); }
-function safeUrl($filename) { return str_replace('%2F', '/', rawurlencode($filename)); }
-
-function genererEtoiles($nombre, $opacite) {
- $ombres = [];
- for ($i = 0; $i < $nombre; $i++) {
- $x = rand(0, 2000); $y = rand(0, 2000);
- $ombres[] = "{$x}px {$y}px rgba(255, 255, 255, {$opacite})";
- }
- return implode(", ", $ombres);
-}
-
-// Récupération API (Strictement en LECTURE SEULE - scope=read)
-function fetch_teamspeak_data($cfg) {
- $cache_file = $cfg['cache_file'];
- if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cfg['cache_time']) {
- $cached_data = file_get_contents($cache_file);
- if ($cached_data) return json_decode($cached_data, true);
- }
-
- $options = ['http' => ['header' => "x-api-key: {$cfg['api_key']}\r\n", 'method' => 'GET', 'timeout' => 2, 'ignore_errors' => true]];
- $context = stream_context_create($options);
-
- $base_url = "http://{$cfg['host']}:{$cfg['port']}/{$cfg['server_id']}";
- $channels_res = @file_get_contents("$base_url/channellist", false, $context);
- $clients_res = @file_get_contents("$base_url/clientlist?-groups", false, $context);
-
- if ($channels_res !== false && $clients_res !== false) {
- $data = [
- 'channels' => json_decode($channels_res, true)['body'] ?? [],
- 'clients' => json_decode($clients_res, true)['body'] ?? []
- ];
- file_put_contents($cache_file, json_encode($data));
- return $data;
- }
- return ['channels' => [], 'clients' => []];
-}
-
-// Tri hiérarchique
-function sort_ts_channels(array $channels, $pid = 0) {
- $ordered = []; $by_order = [];
- foreach ($channels as $c) { if (($c['pid'] ?? 0) == $pid) $by_order[$c['channel_order'] ?? 0] = $c; }
- $current_order = 0; $max_iterations = count($channels) + 1; $i = 0;
- while (isset($by_order[$current_order]) && $i < $max_iterations) {
- $channel = $by_order[$current_order]; $ordered[] = $channel;
- $sub_channels = sort_ts_channels($channels, $channel['cid']);
- foreach ($sub_channels as $sc) $ordered[] = $sc;
- $current_order = $channel['cid']; $i++;
- }
- return $ordered;
-}
-
-// Traitement des données
-$start_time = microtime(true);
-$ts_data = fetch_teamspeak_data($config['server']);
-$api_ping = round((microtime(true) - $start_time) * 1000); // Calcule le temps de réponse de l'API locale
-
-$raw_channels = $ts_data['channels'];
-$raw_clients = $ts_data['clients'];
-$is_online = !empty($raw_channels);
-
-$channels = sort_ts_channels($raw_channels, 0);
-
-$clients_by_channel = [];
-$total_real_clients = 0;
-
-if ($is_online) {
- foreach ($raw_clients as $client) {
- if (($client['client_type'] ?? 0) == 1) continue; // On ignore les bots/ServerQuery
- $clients_by_channel[$client['cid']][] = $client;
- $total_real_clients++;
- }
-}
-
-// Rendu du fragment AJAX
-ob_start();
-if ($is_online): ?>
-
- <div class="server-stats">
- <div class="stat-item">
- <span class="stat-label">En Ligne</span>
- <span class="stat-value"><?php echo $total_real_clients; ?></span>
- </div>
- <div class="stat-item">
- <span class="stat-label">Salons</span>
- <span class="stat-value"><?php echo count($channels); ?></span>
- </div>
- <div class="stat-item">
- <span class="stat-label">Latence API</span>
- <span class="stat-value"><?php echo $api_ping; ?> ms</span>
- </div>
- </div>
-
- <div class="channel-list">
- <?php foreach ($channels as $channel):
- $c_name = $channel['channel_name'];
- if (strpos($c_name, '[spacer') !== false) continue;
-
- $cid = $channel['cid'];
- $channel_clients = $clients_by_channel[$cid] ?? [];
-
- $bg_img = $config['bannieres_canaux'][$c_name] ?? ($config['images_generiques']['fond'] ?? '');
- $icon_img = $config['icones_canaux'][$c_name] ?? ($config['images_generiques']['icone'] ?? '');
-
- $bg_style = "";
- if (!empty($bg_img)) {
- $bg_style = "background-image: linear-gradient(90deg, rgba(19, 24, 35, 0.95) 0%, rgba(19, 24, 35, 0.7) 40%, rgba(19, 24, 35, 0.9) 100%), url('" . safeUrl($bg_img) . "');";
- }
- ?>
- <article class="channel">
- <header class="channel-header" style="<?php echo $bg_style; ?>">
- <div class="channel-title-group">
- <?php if (!empty($icon_img)): ?>
- <img src="<?php echo safeUrl($icon_img); ?>" class="channel-icon" alt="" aria-hidden="true">
- <?php endif; ?>
- <h2><?php echo e($c_name); ?></h2>
- </div>
- <span class="channel-meta">
- <?php echo count($channel_clients); ?> / <?php echo ($channel['channel_maxclients'] == -1) ? '∞' : e($channel['channel_maxclients']); ?>
- </span>
- </header>
-
- <?php if (!empty($channel_clients)): ?>
- <ul class="client-list">
- <?php foreach ($channel_clients as $client): ?>
- <li class="client">
- <?php
- $role_icon = '<span class="client-no-role-icon"></span>';
- if (isset($client['client_servergroups'])) {
- $groups = explode(',', $client['client_servergroups']);
- foreach ($groups as $g_id) {
- if (isset($config['icones_roles'][$g_id])) {
- $role_icon = '<img src="' . safeUrl($config['icones_roles'][$g_id]) . '" class="role-icon" alt="Rôle">';
- break;
- }
- }
- }
- echo $role_icon;
- ?>
- <span><?php echo e($client['client_nickname']); ?></span>
- </li>
- <?php endforeach; ?>
- </ul>
- <?php endif; ?>
- </article>
- <?php endforeach; ?>
- </div>
-<?php else: ?>
- <div class="empty-server">Impossible d'établir le contact avec le serveur central.</div>
-<?php endif;
-$html_fragment = ob_get_clean();
-
-// Gestion de la réponse AJAX
-if (isset($_GET['ajax']) && $_GET['ajax'] === '1') {
- header('Content-Type: text/html; charset=utf-8');
- echo $html_fragment;
- exit;
-}
-?>
-<!DOCTYPE html>
-<html lang="fr">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="<?php echo e($config['ui']['sous_titre']); ?>">
- <title><?php echo e($config['ui']['titre']); ?></title>
- <link rel="icon" type="image/png" href="logo.png">
- <style>
- :root {
- --bg-color: <?php echo e($config['ui']['couleur_fond']); ?>;
- --panel-bg: <?php echo e($config['ui']['couleur_panneau']); ?>;
- --accent: <?php echo e($config['ui']['couleur_accent']); ?>;
- --text-main: <?php echo e($config['ui']['couleur_texte']); ?>;
- --text-muted: #64748b;
- --channel-bg: #1e2433;
- }
-
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; padding: 2rem; display: flex; justify-content: center; min-height: 100vh; box-sizing: border-box; }
-
- <?php if ($config['ui']['activer_etoiles'] ?? false): ?>
- #space-bg { position: fixed; inset: 0; z-index: -1; background-color: var(--bg-color); overflow: hidden;}
- .star-layer { position: absolute; background: transparent; }
- .star-layer::after { content: ""; position: absolute; top: 2000px; background: inherit; box-shadow: inherit; }
- .stars-s { width: 1px; height: 1px; box-shadow: <?php echo genererEtoiles(150, 0.4); ?>; animation: animStar 250s linear infinite; }
- .stars-m { width: 2px; height: 2px; box-shadow: <?php echo genererEtoiles(50, 0.6); ?>; animation: animStar 180s linear infinite; }
- .stars-l { width: 2px; height: 2px; box-shadow: <?php echo genererEtoiles(15, 0.8); ?>; animation: animStar 100s linear infinite; }
- @keyframes animStar { from { transform: translateY(0); } to { transform: translateY(-2000px); } }
- <?php endif; ?>
-
- .app-container { background: var(--panel-bg); border-radius: 12px; width: 100%; max-width: 650px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7); border: 1px solid rgba(255, 255, 255, 0.04); position: relative; overflow: hidden; height: fit-content; }
-
- .banner { width: 100%; height: auto; display: block; border-bottom: 2px solid var(--accent); opacity: 0.95; }
- .content-wrapper { padding: 2rem; }
-
- /* Barre de progression fluide */
- .progress-bar { position: absolute; top: 0; left: 0; height: 2px; background-color: var(--accent); width: 100%; z-index: 10; }
-
- .header { text-align: center; margin-bottom: 2rem; }
- .header h1 { margin: 0 0 0.25rem 0; font-size: 1.5rem; color: #f8fafc; }
- .header p { color: var(--text-muted); margin: 0; font-size: 0.9rem; }
-
- .status-badge { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; margin-top: 15px; letter-spacing: 1px; }
- .status-online { color: #a7f3d0; background-color: rgba(6, 78, 59, 0.4); border: 1px solid rgba(16, 185, 129, 0.2); }
- .status-offline { color: #fca5a5; background-color: rgba(127, 29, 29, 0.4); border: 1px solid rgba(239, 68, 68, 0.2); }
- .pulse-dot { width: 6px; height: 6px; background-color: #10b981; border-radius: 50%; margin-right: 8px; animation: pulse 2s infinite; }
- .status-offline .pulse-dot { background-color: #ef4444; animation: none; }
- @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } }
-
- /* Stats du serveur adaptées à la lecture seule */
- .server-stats { display: flex; justify-content: space-around; background: rgba(0, 0, 0, 0.2); padding: 12px; border-radius: 8px; margin-bottom: 1.5rem; border: 1px solid rgba(255, 255, 255, 0.03); }
- .stat-item { display: flex; flex-direction: column; align-items: center; }
- .stat-label { font-size: 0.65rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
- .stat-value { font-size: 1rem; font-weight: 700; color: var(--text-main); font-variant-numeric: tabular-nums; }
-
- .btn-primary { display: block; padding: 12px; background-color: var(--accent); color: #000; text-align: center; text-decoration: none; border-radius: 6px; font-weight: 800; text-transform: uppercase; font-size: 0.85rem; transition: filter 0.2s; margin-bottom: 0.5rem; }
- .btn-primary:hover { filter: brightness(1.1); }
-
- .help-text { text-align: center; font-size: 0.8rem; color: var(--text-muted); margin: 0.5rem 0 2rem 0; }
- .help-text a { color: var(--text-main); text-decoration: none; border-bottom: 1px solid rgba(255,255,255,0.2); transition: border-color 0.2s; }
- .help-text a:hover { border-color: var(--accent); color: var(--accent); }
-
- .channel-list { display: flex; flex-direction: column; gap: 6px; }
- .channel { border-radius: 12px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.03); background-color: var(--channel-bg); }
- .channel-header { padding: 8px 14px; display: flex; justify-content: space-between; align-items: center; color: #f1f5f9; background-size: cover; background-position: center; background-repeat: no-repeat; }
-
- .channel-title-group { display: flex; align-items: center; margin: 0; font-size: 0.9rem; font-weight: 600; }
- .channel-title-group h2 { font-size: 1rem; margin: 0; font-weight: 600; }
- .channel-icon { width: 20px; height: 20px; margin-right: 10px; border-radius: 4px; object-fit: contain; }
- .channel-meta { font-size: 0.8rem; color: rgba(255,255,255,0.7); }
-
- .client-list { list-style: none; padding: 4px 0 8px 0; margin: 0; }
- .client { padding: 4px 15px 4px 38px; font-size: 0.85rem; color: #cbd5e1; display: flex; align-items: center; font-weight: 500; }
- .role-icon { width: 16px; height: 16px; object-fit: contain; margin-right: 10px; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.5)); }
- .client-no-role-icon { display: inline-block; width: 8px; height: 8px; border: 2px solid #475569; border-radius: 50%; margin-right: 10px; }
-
- .empty-server { text-align: center; color: var(--text-muted); padding: 3rem 0; font-size: 0.9rem; }
-
- .footer-links { text-align: center; padding-top: 1.5rem; margin-top: 2rem; border-top: 1px solid rgba(255, 255, 255, 0.05); }
- .footer-links a { color: var(--text-muted); text-decoration: none; font-size: 0.85rem; margin: 0 10px; transition: color 0.2s; }
- .footer-links a:hover { color: var(--text-main); }
-
- .credits { text-align: center; margin-top: 1rem; font-size: 0.75rem; color: #475569; }
- .credits a { color: var(--text-muted); font-weight: 600; text-decoration: none; transition: color 0.2s; }
- .credits a:hover { color: var(--accent); }
-
- #ajax-container { transition: opacity 0.3s ease-in-out; }
- </style>
-</head>
-<body>
-
-<?php if ($config['ui']['activer_etoiles'] ?? false): ?>
-<div id="space-bg" aria-hidden="true">
- <div class="star-layer stars-s"></div>
- <div class="star-layer stars-m"></div>
- <div class="star-layer stars-l"></div>
-</div>
-<?php endif; ?>
-
-<main class="app-container">
- <?php if ($is_online): ?><div class="progress-bar" id="refresh-bar"></div><?php endif; ?>
-
- <?php if (!empty($config['ui']['banniere_url'])): ?>
- <img src="<?php echo safeUrl($config['ui']['banniere_url']); ?>" alt="Bannière d'en-tête" class="banner">
- <?php endif; ?>
-
- <div class="content-wrapper">
- <header class="header">
- <h1><?php echo e($config['ui']['titre']); ?></h1>
- <p><?php echo e($config['ui']['sous_titre']); ?></p>
- <?php if ($is_online): ?>
- <div class="status-badge status-online"><span class="pulse-dot"></span> TRANSMISSION ACTIVE</div>
- <?php else: ?>
- <div class="status-badge status-offline"><span class="pulse-dot"></span> SIGNAL PERDU</div>
- <?php endif; ?>
- </header>
-
- <a href="ts3server://<?php echo e($config['server']['ts_domain']); ?>" class="btn-primary"><?php echo e($config['boutons']['rejoindre']); ?></a>
- <p class="help-text"><?php echo e($config['boutons']['telecharger_texte']); ?> <a href="<?php echo e($config['boutons']['telecharger_url']); ?>" target="_blank" rel="noopener"><?php echo e($config['boutons']['telecharger_lien']); ?></a></p>
-
- <section id="ajax-container" aria-live="polite">
- <?php echo $html_fragment; ?>
- </section>
-
- <?php if (!empty($config['liens_externes'])): ?>
- <nav class="footer-links">
- <?php foreach ($config['liens_externes'] as $nom => $url): ?>
- <a href="<?php echo e($url); ?>" target="_blank" rel="noopener"><?php echo e($nom); ?></a>
- <?php endforeach; ?>
- </nav>
- <?php endif; ?>
-
- <footer class="credits">
- <?php echo e($config['footer']['texte']); ?> <a href="<?php echo e($config['footer']['lien_auteur']); ?>" target="_blank" rel="noopener"><?php echo e($config['footer']['auteur']); ?></a> — Scan radar : <span id="timer-text"><?php echo $config['server']['cache_time']; ?></span>s
- </footer>
- </div>
-</main>
-
-<script>
- <?php if ($is_online): ?>
- document.addEventListener("DOMContentLoaded", () => {
- let timeLeft = <?php echo (int)$config['server']['cache_time']; ?>;
- const totalTime = <?php echo (int)$config['server']['cache_time']; ?>;
-
- const progressBar = document.getElementById('refresh-bar');
- const timerText = document.getElementById('timer-text');
- const container = document.getElementById('ajax-container');
-
- // Animation fluide de la barre de temps
- function resetProgressBar() {
- progressBar.style.transition = 'none';
- progressBar.style.width = '100%';
- void progressBar.offsetWidth;
- progressBar.style.transition = `width ${totalTime}s linear`;
- progressBar.style.width = '0%';
- }
-
- resetProgressBar();
-
- setInterval(() => {
- timeLeft--;
- timerText.innerText = timeLeft;
-
- if (timeLeft <= 0) {
- container.style.opacity = '0.5';
- fetch(window.location.href.split('?')[0] + '?ajax=1', { cache: 'no-store', headers: {'X-Requested-With': 'XMLHttpRequest'} })
- .then(r => {
- if (!r.ok) throw new Error("HTTP error " + r.status);
- return r.text();
- })
- .then(html => {
- container.innerHTML = html;
- container.style.opacity = '1';
-
- timeLeft = totalTime;
- timerText.innerText = timeLeft;
-
- resetProgressBar();
- })
- .catch(err => {
- console.error("Erreur de rafraîchissement :", err);
- container.style.opacity = '1';
- timeLeft = totalTime;
- resetProgressBar();
- });
- }
- }, 1000);
- });
- <?php endif; ?>
-</script>
-</body>
-</html> \ No newline at end of file
diff --git a/info.png b/info.png
new file mode 100644
index 0000000..fd27228
--- /dev/null
+++ b/info.png
Binary files differ
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ffeca58
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,11 @@
+[project]
+name = "ts6-web-explorer"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.13"
+dependencies = [
+ "flask>=3.1.3",
+ "flask-cors>=6.0.5",
+ "requests>=2.34.2",
+]
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..0062c2f
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+Flask>=3.0,<4
+flask-cors>=4.0,<5
+requests>=2.31,<3
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..91d187d
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,264 @@
+version = 1
+revision = 3
+requires-python = ">=3.13"
+
+[[package]]
+name = "blinker"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.6.17"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
+ { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
+ { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
+ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
+ { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
+ { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
+ { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
+ { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
+ { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
+ { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "flask"
+version = "3.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "blinker" },
+ { name = "click" },
+ { name = "itsdangerous" },
+ { name = "jinja2" },
+ { name = "markupsafe" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
+]
+
+[[package]]
+name = "flask-cors"
+version = "6.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/47/03/4e464a50860f9adf08b5c1d3479cb8ea1f12af2aa69535c7042c6e628135/flask_cors-6.0.5.tar.gz", hash = "sha256:30c5031552cd59f620ac0c8211dac45b345d3b2df310e7721879e4f46ef9c601", size = 101386, upload-time = "2026-06-08T20:20:17.765Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/55/5bb1a2d918e9f02f131e47a59032bae70e48050e986e941511fd737a935c/flask_cors-6.0.5-py3-none-any.whl", hash = "sha256:68fcf75693e961f3af26683b23c4b9a8fb6b64de17d20d0c37b95e8de7ab2ed8", size = 16692, upload-time = "2026-06-08T20:20:16.247Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
+[[package]]
+name = "itsdangerous"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+]
+
+[[package]]
+name = "ts6-web-explorer"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "flask" },
+ { name = "flask-cors" },
+ { name = "requests" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "flask", specifier = ">=3.1.3" },
+ { name = "flask-cors", specifier = ">=6.0.5" },
+ { name = "requests", specifier = ">=2.34.2" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
+]
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage