website/pages/test2.html

459 lines
No EOL
15 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Kuby API Dashboard</title>
<style>
:root {
--bg: #0b1220;
--panel: #111a2e;
--panel2: #0f172a;
--text: #e5e7eb;
--muted: #9ca3af;
--accent: #60a5fa;
--ok: #34d399;
--bad: #f87171;
--border: rgba(255,255,255,.08);
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
background: radial-gradient(1200px 700px at 15% 10%, rgba(96,165,250,.18), transparent 60%),
radial-gradient(900px 600px at 90% 15%, rgba(52,211,153,.14), transparent 55%),
var(--bg);
color: var(--text);
}
header {
padding: 24px 20px 10px;
border-bottom: 1px solid var(--border);
background: linear-gradient(180deg, rgba(255,255,255,.03), transparent);
}
h1 { margin: 0 0 6px; font-size: 20px; letter-spacing: .2px; }
.sub { color: var(--muted); font-size: 13px; }
.wrap { max-width: 1100px; margin: 0 auto; padding: 18px 20px 34px; }
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 14px;
}
@media (min-width: 900px) {
.grid { grid-template-columns: 1fr 1fr; }
.span2 { grid-column: span 2; }
}
.card {
background: rgba(17,26,46,.75);
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px;
box-shadow: 0 10px 30px rgba(0,0,0,.25);
backdrop-filter: blur(8px);
}
.card h2 {
margin: 0 0 10px;
font-size: 15px;
color: #f3f4f6;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
input, button, select {
background: rgba(15,23,42,.85);
border: 1px solid var(--border);
color: var(--text);
border-radius: 10px;
padding: 10px 10px;
font-size: 14px;
outline: none;
}
input::placeholder { color: rgba(156,163,175,.7); }
button {
cursor: pointer;
background: linear-gradient(180deg, rgba(96,165,250,.22), rgba(96,165,250,.06));
border-color: rgba(96,165,250,.35);
}
button:hover { border-color: rgba(96,165,250,.55); }
button:disabled { opacity: .6; cursor: not-allowed; }
.pill {
font-size: 12px;
padding: 5px 9px;
border-radius: 999px;
border: 1px solid var(--border);
color: var(--muted);
background: rgba(15,23,42,.7);
white-space: nowrap;
}
.pill.ok { color: #a7f3d0; border-color: rgba(52,211,153,.35); background: rgba(52,211,153,.10); }
.pill.bad { color: #fecaca; border-color: rgba(248,113,113,.35); background: rgba(248,113,113,.10); }
pre {
margin: 10px 0 0;
background: rgba(15,23,42,.75);
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px;
overflow: auto;
max-height: 360px;
font-size: 12px;
color: #d1d5db;
}
.muted { color: var(--muted); font-size: 13px; }
.list {
margin-top: 10px;
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
@media (min-width: 700px) {
.list { grid-template-columns: 1fr 1fr; }
}
.item {
display: flex;
gap: 12px;
align-items: center;
padding: 10px;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(15,23,42,.60);
}
.avatar, .icon {
width: 44px; height: 44px;
border-radius: 12px;
background: rgba(255,255,255,.06);
border: 1px solid var(--border);
object-fit: cover;
flex: 0 0 auto;
}
.item .meta { min-width: 0; }
.name { font-weight: 650; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.small { font-size: 12px; color: var(--muted); }
.right { margin-left: auto; display:flex; gap:8px; align-items:center; }
a.link {
color: #bfdbfe;
text-decoration: none;
border-bottom: 1px dashed rgba(191,219,254,.35);
}
.err { color: #fecaca; }
.oktxt { color: #a7f3d0; }
</style>
</head>
<body>
<header>
<div class="wrap">
<h1>Kuby API Dashboard</h1>
<div class="sub">
Affiche les réponses de l’API Flask: <code>/api/status</code>, <code>/api/user/&​lt;id&​gt;</code>, <code>/api/guilds</code>, <code>/api/guild/&​lt;id&​gt;</code>
</div>
</div>
</header>
<div class="wrap">
<div class="grid">
<section class="card">
<h2>
Statut du bot
<span id="statusPill" class="pill">non chargé</span>
</h2>
<div class="row">
<button id="btnStatus">Charger /api/status</button>
<span class="muted">Affiche le statut global.</span>
</div>
<pre id="statusOut">{}</pre>
</section>
<section class="card">
<h2>
Rechercher un utilisateur
<span id="userPill" class="pill">/api/user/&​lt;id&​gt;</span>
</h2>
<div class="row">
<input id="userId" type="number" placeholder="User ID (ex: 123...)" />
<button id="btnUser">Charger</button>
</div>
<div id="userCard" class="list" style="grid-template-columns: 1fr; display:none;">
<div class="item">
<img id="userAvatar" class="avatar" alt="avatar" />
<div class="meta">
<div id="userName" class="name">—</div>
<div id="userAvatarUrl" class="small">—</div>
</div>
<div class="right">
<a id="userAvatarLink" class="link" href="#" target="_blank" rel="noreferrer">Ouvrir avatar</a>
</div>
</div>
</div>
<pre id="userOut">{}</pre>
</section>
<section class="card span2">
<h2>
Serveurs du bot
<span id="guildsPill" class="pill">/api/guilds</span>
</h2>
<div class="row">
<button id="btnGuilds">Charger la liste</button>
<span class="muted">Clique un serveur pour charger ses détails.</span>
</div>
<div id="guildsList" class="list"></div>
<pre id="guildsOut">{}</pre>
</section>
<section class="card span2">
<h2>
Détails d’un serveur
<span id="guildPill" class="pill">/api/guild/&​lt;id&​gt;</span>
</h2>
<div class="row">
<input id="guildId" type="number" placeholder="Guild ID (ex: 123...)" />
<button id="btnGuild">Charger</button>
<span id="guildHint" class="muted">Astuce: tu peux aussi cliquer un serveur dans la liste au-dessus.</span>
</div>
<div id="guildCard" class="list" style="grid-template-columns: 1fr; display:none;"></div>
<pre id="guildOut">{}</pre>
</section>
</div>
</div>
<script>
// Change ici si ton API est ailleurs (ex: http://localhost:5000)
const API_BASE = "http://127.0.0.1:5000";
const $ = (id) => document.getElementById(id);
function pretty(obj) {
return JSON.stringify(obj, null, 2);
}
async function apiGet(path) {
const url = API_BASE + path;
const res = await fetch(url, { method: "GET" });
let data = null;
try {
data = await res.json();
} catch (e) {
// si la réponse n'est pas JSON
const text = await res.text();
throw new Error(`Réponse non JSON (${res.status}) depuis ${url}: ${text}`);
}
if (!res.ok) {
const msg = (data && (data.error || data.message)) ? (data.error || data.message) : `HTTP ${res.status}`;
const err = new Error(msg);
err.status = res.status;
err.data = data;
throw err;
}
return data;
}
function setPill(el, kind, text) {
el.classList.remove("ok", "bad");
if (kind === "ok") el.classList.add("ok");
if (kind === "bad") el.classList.add("bad");
el.textContent = text;
}
// STATUS
$("btnStatus").addEventListener("click", async () => {
$("btnStatus").disabled = true;
setPill($("statusPill"), "", "chargement...");
try {
const data = await apiGet("/api/status");
$("statusOut").textContent = pretty(data);
setPill($("statusPill"), data.status === "online" ? "ok" : "", data.status || "ok");
} catch (e) {
$("statusOut").textContent = pretty({ error: e.message, status: e.status || null, data: e.data || null });
setPill($("statusPill"), "bad", "erreur");
} finally {
$("btnStatus").disabled = false;
}
});
// USER
$("btnUser").addEventListener("click", async () => {
const id = $("userId").value?.trim();
$("userCard").style.display = "none";
$("btnUser").disabled = true;
setPill($("userPill"), "", "chargement...");
try {
if (!id) throw new Error("Tu dois renseigner un user_id.");
const data = await apiGet(`/api/user/${encodeURIComponent(id)}`);
$("userOut").textContent = pretty(data);
$("userName").textContent = data.username ?? "—";
$("userAvatar").src = data.avatar_url ?? "https://cdn.discordapp.com/embed/avatars/0.png";
$("userAvatarUrl").textContent = data.avatar_url ?? "—";
$("userAvatarLink").href = data.avatar_url ?? "#";
$("userCard").style.display = "grid";
setPill($("userPill"), "ok", "ok");
} catch (e) {
$("userOut").textContent = pretty({ error: e.message, status: e.status || null, data: e.data || null });
setPill($("userPill"), "bad", "erreur");
} finally {
$("btnUser").disabled = false;
}
});
// GUILDS LIST
$("btnGuilds").addEventListener("click", async () => {
$("btnGuilds").disabled = true;
setPill($("guildsPill"), "", "chargement...");
$("guildsList").innerHTML = "";
try {
const data = await apiGet("/api/guilds");
$("guildsOut").textContent = pretty(data);
for (const g of data) {
const div = document.createElement("div");
div.className = "item";
div.style.cursor = "pointer";
div.title = "Cliquer pour charger /api/guild/<id>";
const img = document.createElement("img");
img.className = "icon";
img.alt = "icon";
img.src = g.icon_url || "https://cdn.discordapp.com/embed/avatars/0.png";
const meta = document.createElement("div");
meta.className = "meta";
const name = document.createElement("div");
name.className = "name";
name.textContent = g.name || "Sans nom";
const small = document.createElement("div");
small.className = "small";
small.textContent = `ID: ${g.id} | Membres: ${g.member_count ?? "?"}`;
meta.appendChild(name);
meta.appendChild(small);
const right = document.createElement("div");
right.className = "right";
const pill = document.createElement("span");
pill.className = "pill";
pill.textContent = "voir détails";
right.appendChild(pill);
div.appendChild(img);
div.appendChild(meta);
div.appendChild(right);
div.addEventListener("click", async () => {
$("guildId").value = g.id;
await loadGuild(g.id);
});
$("guildsList").appendChild(div);
}
setPill($("guildsPill"), "ok", `${data.length} serveurs`);
} catch (e) {
$("guildsOut").textContent = pretty({ error: e.message, status: e.status || null, data: e.data || null });
setPill($("guildsPill"), "bad", "erreur");
} finally {
$("btnGuilds").disabled = false;
}
});
async function loadGuild(id) {
$("guildCard").style.display = "none";
$("btnGuild").disabled = true;
setPill($("guildPill"), "", "chargement...");
try {
if (!id) throw new Error("Tu dois renseigner un guild_id.");
const data = await apiGet(`/api/guild/${encodeURIComponent(id)}`);
$("guildOut").textContent = pretty(data);
// carte “propre” comme user
const html = `
<div class="item">
<img class="icon" alt="icon" src="${(data.icon_url || "https://cdn.discordapp.com/embed/avatars/0.png")}" />
<div class="meta">
<div class="name">${escapeHtml(data.name || "Sans nom")}</div>
<div class="small">ID: ${data.id} | Membres: ${data.member_count ?? "?"} | Owner: ${data.owner_id ?? "?"}</div>
<div class="small">${data.description ? escapeHtml(data.description) : ""}</div>
</div>
<div class="right">
${data.icon_url ? `<a class="link" href="${data.icon_url}" target="_blank" rel="noreferrer">Ouvrir icône</a>` : ""}
</div>
</div>
<div class="item" style="align-items:flex-start;">
<div class="meta" style="width:100%;">
<div class="name">Channels (${(data.channels || []).length})</div>
<div class="small">${renderChannels(data.channels || [])}</div>
</div>
</div>
<div class="item" style="align-items:flex-start;">
<div class="meta" style="width:100%;">
<div class="name">Roles (${(data.roles || []).length})</div>
<div class="small">${renderRoles(data.roles || [])}</div>
</div>
</div>
`;
$("guildCard").innerHTML = html;
$("guildCard").style.display = "grid";
setPill($("guildPill"), "ok", "ok");
} catch (e) {
$("guildOut").textContent = pretty({ error: e.message, status: e.status || null, data: e.data || null });
setPill($("guildPill"), "bad", "erreur");
} finally {
$("btnGuild").disabled = false;
}
}
$("btnGuild").addEventListener("click", async () => {
const id = $("guildId").value?.trim();
await loadGuild(id);
});
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&​amp;")
.replaceAll("<", "&​lt;")
.replaceAll(">", "&​gt;")
.replaceAll('"', "&​quot;")
.replaceAll("'", "&​#039;");
}
function renderChannels(channels) {
if (!channels.length) return "Aucun channel (ou non accessible).";
// limiter pour pas exploser l’UI
const shown = channels.slice(0, 40);
const more = channels.length > shown.length ? ` (+${channels.length - shown.length})` : "";
return shown
.map(c => {
const cat = c.category ? ` / ${escapeHtml(c.category)}` : "";
return `#${escapeHtml(c.name)} (${escapeHtml(c.type)})${cat}`;
})
.join("<br/>") + more;
}
function renderRoles(roles) {
if (!roles.length) return "Aucun rôle (ou non accessible).";
// trier par position desc si pas déjà
const sorted = [...roles].sort((a,b) => (b.position ?? 0) - (a.position ?? 0));
const shown = sorted.slice(0, 40);
const more = sorted.length > shown.length ? ` (+${sorted.length - shown.length})` : "";
return shown
.map(r => `${escapeHtml(r.name)} — membres: ${r.member_count ?? "?"} (pos ${r.position ?? "?"})`)
.join("<br/>") + more;
}
// Auto-load status au démarrage (optionnel)
$("btnStatus").click();
</script>
</body>
</html>