(function () {
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const FR_CLASS_TO_EN = {
'Guerrier': 'Warrior', 'Paladin': 'Paladin', 'Chasseur': 'Hunter', 'Voleur': 'Rogue',
'Prêtre': 'Priest', 'Chevalier de la mort': 'Death Knight', 'Chaman': 'Shaman', 'Mage': 'Mage',
'Démoniste': 'Warlock', 'Moine': 'Monk', 'Druide': 'Druid', 'Chasseur de démons': 'Demon Hunter',
'Évocateur': 'Evoker'
};
const PVP_GENERAL = [['ARENA_2v2', '2v2'], ['ARENA_3v3', '3v3'], ['RATED_BG', 'RBG']];
const PVP_PER_SPEC = [['SOLO_SHUFFLE', 'Solo Shuffle'], ['BLITZ', 'Blitz']];
const SPECS_BY_CLASS = {
'Death Knight': [
{ key: 'Blood', fr: 'Sang', icon: 'spell_deathknight_bloodpresence.jpg' },
{ key: 'Frost', fr: 'Givre', icon: 'spell_deathknight_frostpresence.jpg' },
{ key: 'Unholy', fr: 'Impie', icon: 'spell_deathknight_unholypresence.jpg' }
],
'Demon Hunter': [
{ key: 'Havoc', fr: 'Dévastation', icon: 'ability_demonhunter_specdps.jpg' },
{ key: 'Vengeance', fr: 'Vengeance', icon: 'ability_demonhunter_spectank.jpg' },
{ key: 'Devoration', fr: 'Dévoration', icon: 'classicon_demonhunter_void.jpg' }
],
'Druid': [
{ key: 'Balance', fr: 'Équilibre', icon: 'spell_nature_starfall.jpg' },
{ key: 'Feral', fr: 'Farouche', icon: 'ability_druid_catform.jpg' },
{ key: 'Guardian', fr: 'Gardien', icon: 'ability_racial_bearform.jpg' },
{ key: 'Restoration', fr: 'Restauration', icon: 'spell_nature_healingtouch.jpg' }
],
'Evoker': [
{ key: 'Devastation', fr: 'Dévastation', icon: 'classicon_evoker_devastation.jpg' },
{ key: 'Preservation', fr: 'Préservation', icon: 'classicon_evoker_preservation.jpg' },
{ key: 'Augmentation', fr: 'Augmentation', icon: 'classicon_evoker_augmentation.jpg' }
],
'Hunter': [
{ key: 'Beast Mastery', fr: 'Maîtrise des bêtes', icon: 'ability_hunter_bestialdiscipline.jpg' },
{ key: 'Marksmanship', fr: 'Précision', icon: 'ability_hunter_focusedaim.jpg' },
{ key: 'Survival', fr: 'Survie', icon: 'ability_hunter_camouflage.jpg' }
],
'Mage': [
{ key: 'Arcane', fr: 'Arcanes', icon: 'spell_nature_magicimmunity.jpg' },
{ key: 'Fire', fr: 'Feu', icon: 'spell_fire_firebolt02.jpg' },
{ key: 'Frost', fr: 'Givre', icon: 'spell_frost_frostbolt02.jpg' }
],
'Monk': [
{ key: 'Brewmaster', fr: 'Maître brasseur', icon: 'spell_monk_brewmaster_spec.jpg' },
{ key: 'Mistweaver', fr: 'Tisse-brume', icon: 'spell_monk_mistweaver_spec.jpg' },
{ key: 'Windwalker', fr: 'Marche-vent', icon: 'spell_monk_windwalker_spec.jpg' }
],
'Paladin': [
{ key: 'Holy', fr: 'Sacré', icon: 'spell_holy_holybolt.jpg' },
{ key: 'Protection', fr: 'Protection', icon: 'ability_paladin_shieldofthetemplar.jpg' },
{ key: 'Retribution', fr: 'Vindicte', icon: 'spell_holy_auraoflight.jpg' }
],
'Priest': [
{ key: 'Discipline', fr: 'Discipline', icon: 'spell_holy_powerwordshield.jpg' },
{ key: 'Holy', fr: 'Sacré', icon: 'spell_holy_guardianspirit.jpg' },
{ key: 'Shadow', fr: 'Ombre', icon: 'spell_shadow_shadowwordpain.jpg' }
],
'Rogue': [
{ key: 'Assassination', fr: 'Assassinat', icon: 'ability_rogue_deadlybrew.jpg' },
{ key: 'Outlaw', fr: 'Hors-la-loi', icon: 'ability_rogue_waylay.jpg' },
{ key: 'Subtlety', fr: 'Finesse', icon: 'ability_stealth.jpg' }
],
'Shaman': [
{ key: 'Elemental', fr: 'Élémentaire', icon: 'spell_nature_lightning.jpg' },
{ key: 'Enhancement', fr: 'Amélioration', icon: 'spell_shaman_improvedstormstrike.jpg' },
{ key: 'Restoration', fr: 'Restauration', icon: 'spell_nature_healingtouch.jpg' }
],
'Warlock': [
{ key: 'Affliction', fr: 'Affliction', icon: 'spell_shadow_deathcoil.jpg' },
{ key: 'Demonology', fr: 'Démonologie', icon: 'spell_shadow_metamorphosis.jpg' },
{ key: 'Destruction', fr: 'Destruction', icon: 'spell_shadow_rainoffire.jpg' }
],
'Warrior': [
{ key: 'Arms', fr: 'Armes', icon: 'ability_warrior_innerrage.jpg' },
{ key: 'Fury', fr: 'Fureur', icon: 'ability_warrior_savageblow.jpg' },
{ key: 'Protection', fr: 'Protection', icon: 'ability_warrior_defensivestance.jpg' }
]
};
const ADMIN_CLASSES = [
{ name: 'Warrior', fr: 'Guerrier', logo: 'War.png' },
{ name: 'Paladin', fr: 'Paladin', logo: 'Pal.png' },
{ name: 'Hunter', fr: 'Chasseur', logo: 'Hunt.png' },
{ name: 'Rogue', fr: 'Voleur', logo: 'Rogue.png' },
{ name: 'Priest', fr: 'Prêtre', logo: 'Priest.png' },
{ name: 'Death Knight', fr: 'Chevalier de la mort', logo: 'DK.png' },
{ name: 'Shaman', fr: 'Chaman', logo: 'Sham.png' },
{ name: 'Mage', fr: 'Mage', logo: 'Mage.png' },
{ name: 'Warlock', fr: 'Démoniste', logo: 'Lock.png' },
{ name: 'Monk', fr: 'Moine', logo: 'monk.png' },
{ name: 'Druid', fr: 'Druide', logo: 'Druid.png' },
{ name: 'Demon Hunter', fr: 'Chasseur de démons', logo: 'dh.png' },
{ name: 'Evoker', fr: 'Évocateur', logo: 'evoker.png' }
];
let pvpEditId = null;
let state = { logged: false };
async function checkLogin() {
try {
await api.get('/api/admin/me');
state.logged = true;
} catch {
state.logged = false;
}
render();
}
function render() {
$('#login-view').hidden = state.logged;
$('#admin-view').hidden = !state.logged;
$('#logout-btn').hidden = !state.logged;
if (state.logged) {
loadChars();
loadGoals();
loadSettings();
loadSessions();
loadLogs();
}
}
const SESSION_BRACKETS = [['SOLO_SHUFFLE', 'Solo Shuffle'], ['BLITZ', 'Blitz'], ['ARENA_2v2', '2v2'], ['ARENA_3v3', '3v3'], ['RATED_BG', 'RBG']];
async function loadSessions() {
const grid = $('#session-grid');
if (!grid) return;
try {
const data = await api.get('/api/admin/sessions');
const today = {};
const week = {};
(data.today || []).forEach((x) => { today[x.bracket] = x; });
(data.week || []).forEach((x) => { week[x.bracket] = x; });
grid.innerHTML = SESSION_BRACKETS.map(([key, label]) => {
const t = today[key] || { wins: 0, losses: 0 };
const w = week[key] || { wins: 0, losses: 0 };
const total = (t.wins || 0) + (t.losses || 0);
const winrate = total > 0 ? `${Math.round((t.wins / total) * 100)}%` : '—';
return `
${label}
${t.wins}V · ${t.losses}D ${winrate}
7 derniers jours : ${w.wins}V · ${w.losses}D
`;
}).join('');
grid.querySelectorAll('.session-win, .session-loss').forEach((b) => {
b.addEventListener('click', async () => {
await api.post('/api/admin/sessions', { bracket: b.dataset.bracket, won: Number(b.dataset.won) });
loadSessions();
});
});
$('#session-undo').hidden = !(data.last && data.last.length);
} catch {
grid.innerHTML = 'Impossible de charger le suivi de session.
';
}
}
async function sessionUndo() {
try {
await api.post('/api/admin/sessions/undo');
loadSessions();
} catch {}
}
$('#login-form').addEventListener('submit', async (e) => {
e.preventDefault();
$('#login-error').hidden = true;
try {
await api.post('/api/admin/login', { password: $('#login-password').value });
state.logged = true;
$('#login-password').value = '';
render();
} catch (err) {
$('#login-error').textContent = err.message;
$('#login-error').hidden = false;
}
});
$('#logout-btn').addEventListener('click', async () => {
await api.post('/api/admin/logout');
state.logged = false;
render();
});
// Tabs
$$('.tab-btn').forEach((btn) => {
btn.addEventListener('click', () => {
$$('.tab-btn').forEach((b) => b.classList.remove('active'));
$$('.tab-panel').forEach((p) => p.classList.remove('active'));
btn.classList.add('active');
$(`#tab-${btn.dataset.tab}`).classList.add('active');
});
});
// ---- Characters ----
async function loadChars() {
const chars = await api.get('/api/admin/characters');
window.__adminChars = chars;
$('#chars-tbody').innerHTML = chars.map((c) => `
${esc(c.name)}${c.note ? ` ${esc(c.note)}` : ''} |
|
|
${esc(c.realm)} (${c.region}) |
|
|
|
|
${(c.titles || []).map((t) => `
${t.title === 'glad' ? 'Glad' : 'Leg'}
${t.season ? `${esc(t.season)}` : ''}
`).join('') || ' Aucun'}
|
|
${esc(fmtDate(c.last_updated))} |
|
`).join('');
}
$('#add-char-btn').addEventListener('click', () => {
$('#add-char-form').classList.toggle('hidden');
});
$('#nc-save').addEventListener('click', async () => {
try {
await api.post('/api/admin/characters', {
region: $('#nc-region').value,
realm: $('#nc-realm').value,
name: $('#nc-name').value,
realm_slug: $('#nc-realm-slug').value || null,
class: FR_CLASS_TO_EN[$('#nc-class').value] || null,
note: $('#nc-note').value
});
$('#nc-name').value = ''; $('#nc-realm').value = ''; $('#nc-realm-slug').value = ''; $('#nc-class').value = ''; $('#nc-note').value = '';
$('#add-char-form').classList.add('hidden');
await loadChars();
await refreshAll();
} catch (e) {
alert(e.message);
}
});
$('#chars-tbody').addEventListener('change', async (e) => {
const tr = e.target.closest('tr');
if (!tr) return;
if (e.target.matches('[data-field="active"]')) {
await api.put(`/api/admin/characters/${tr.dataset.id}`, { active: e.target.checked ? 1 : 0 });
} else if (e.target.matches('[data-field="class"]')) {
await api.put(`/api/admin/characters/${tr.dataset.id}`, { class: e.target.value || null });
} else if (e.target.matches('[data-field="level"]')) {
const v = e.target.value.trim();
await api.put(`/api/admin/characters/${tr.dataset.id}`, { level: v === '' ? null : Number(v) });
} else if (e.target.matches('[data-field="check_pvp_url"]')) {
await api.put(`/api/admin/characters/${tr.dataset.id}`, { check_pvp_url: e.target.value });
} else if (e.target.matches('[data-field="pvp_sets"]')) {
await api.put(`/api/admin/characters/${tr.dataset.id}`, { pvp_sets: e.target.value });
} else if (e.target.matches('[data-field="pve_sets"]')) {
await api.put(`/api/admin/characters/${tr.dataset.id}`, { pve_sets: e.target.value });
}
});
$('#chars-tbody').addEventListener('click', async (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const tr = btn.closest('tr');
const id = tr.dataset.id;
if (btn.dataset.action === 'delete') {
if (!confirm('Supprimer ce personnage ?')) return;
await api.del(`/api/admin/characters/${id}`);
await loadChars();
} else if (btn.dataset.action === 'refresh') {
btn.textContent = '…';
await api.post('/api/admin/refresh', { id });
btn.textContent = 'Maj';
await loadChars();
await loadGoals();
await loadLogs();
} else if (btn.dataset.action === 'pvp') {
openPvpModal(id, tr.querySelector('.char-cell-name')?.textContent || '');
} else if (btn.dataset.action === 'add-title') {
const kind = tr.querySelector('[data-title-kind]')?.value || 'glad';
const season = (tr.querySelector('[data-title-season]')?.value || '').trim();
await api.post(`/api/admin/characters/${id}/titles`, { title: kind, season });
await loadChars();
} else if (btn.dataset.action === 'del-title') {
if (!confirm('Retirer ce titre ?')) return;
await api.del(`/api/admin/characters/${id}/titles/${btn.dataset.titleId}`);
await loadChars();
}
});
// ---- Ratings PvP modal ----
function openPvpModal(id, name) {
const chars = window.__adminChars || [];
const c = chars.find((x) => String(x.id) === String(id));
pvpEditId = id;
$('#pvp-modal-title').textContent = `Ratings PvP — ${name}`;
let pm = {};
try { pm = c && c.pvp_manual ? JSON.parse(c.pvp_manual || '{}') : {}; } catch {}
const classSpecs = (c && SPECS_BY_CLASS[c.class]) || [];
const cell = (spec, key, slot, val) => {
const ds = spec ? `data-spec="${spec}" ` : '';
return ``;
};
$('#pvp-general-body').innerHTML = PVP_GENERAL.map(([key, label]) => `
| ${label} |
${cell(null, key, 'season', pm[key]?.season)} |
${cell(null, key, 'max', pm[key]?.max)} |
${cell(null, key, 'won', pm[key]?.won)} |
${cell(null, key, 'lost', pm[key]?.lost)} |
`).join('');
$('#pvp-specs').innerHTML = classSpecs.map((s) => {
const sp = (pm.specs && pm.specs[s.key]) || {};
return `
${esc(s.fr)}
| Bracket | Cette saison | Max (xp) | Victoires | Défaites |
${PVP_PER_SPEC.map(([key, label]) => `
| ${label} |
${cell(s.key, key, 'season', sp[key]?.season)} |
${cell(s.key, key, 'max', sp[key]?.max)} |
${cell(s.key, key, 'won', sp[key]?.won)} |
${cell(s.key, key, 'lost', sp[key]?.lost)} |
`).join('')}
`;
}).join('');
$('#pvp-modal-backdrop').hidden = false;
$('#pvp-save-msg').textContent = '';
document.body.style.overflow = 'hidden';
}
$('#pvp-save').addEventListener('click', async () => {
const out = { specs: {} };
const apply = (input) => {
const key = input.dataset.bracket;
const slot = input.dataset.slot;
const v = input.value.trim();
return { key, slot, v: v === '' ? 0 : Number(v) };
};
for (const input of $$('#pvp-general-body input')) {
const { key, slot, v } = apply(input);
if (!out[key]) out[key] = {};
out[key][slot] = v;
}
for (const input of $$('#pvp-specs input')) {
const spec = input.dataset.spec;
const { key, slot, v } = apply(input);
if (!out.specs[spec]) out.specs[spec] = {};
if (!out.specs[spec][key]) out.specs[spec][key] = {};
out.specs[spec][key][slot] = v;
}
try {
await api.put(`/api/admin/characters/${pvpEditId}`, { pvp_manual: JSON.stringify(out) });
$('#pvp-save-msg').textContent = 'Enregistré.';
$('#pvp-save-msg').style.color = 'var(--green)';
setTimeout(() => closePvpModal(), 600);
} catch (err) {
$('#pvp-save-msg').textContent = err.message;
$('#pvp-save-msg').style.color = 'var(--red)';
}
});
function closePvpModal() {
$('#pvp-modal-backdrop').hidden = true;
document.body.style.overflow = '';
}
$('#pvp-modal-close').addEventListener('click', closePvpModal);
$('#pvp-modal-backdrop').addEventListener('click', (e) => { if (e.target === e.currentTarget) closePvpModal(); });
// ---- Goals ----
async function loadGoals() {
const goals = await api.get('/api/admin/goals');
$('#admin-goals').innerHTML = goals.map((g) => `
`).join('');
}
$('#admin-goals').addEventListener('change', async (e) => {
const field = e.target.dataset.gfield;
if (!field) return;
const card = e.target.closest('.goal-card');
const key = card.dataset.key;
let value;
if (field === 'enabled') value = e.target.checked ? 1 : 0;
else if (field === 'target' || field === 'display_max') value = e.target.value === '' ? null : Number(e.target.value);
else value = e.target.value;
try {
await api.put(`/api/admin/goals/${key}`, { [field]: value });
} catch (err) {
alert(err.message);
}
});
// ---- Settings ----
function renderClassSelect(el, selected, onSave) {
const sel = new Set((selected || '').split(',').map((s) => s.trim()).filter(Boolean));
el.innerHTML = ADMIN_CLASSES.map((c) => `
`).join('');
el.querySelectorAll('.class-chip').forEach((chip) => {
chip.addEventListener('click', () => {
const name = chip.dataset.name;
if (chip.classList.toggle('on')) sel.add(name); else sel.delete(name);
onSave([...sel].join(','));
});
});
}
function renderSingleClassSelect(el, selected, onSave) {
let current = (selected || '').trim();
el.innerHTML = ADMIN_CLASSES.map((c) => `
`).join('');
el.querySelectorAll('.class-chip').forEach((chip) => {
chip.addEventListener('click', () => {
const name = chip.dataset.name;
current = current === name ? '' : name;
el.querySelectorAll('.class-chip').forEach((x) => x.classList.toggle('on', x.dataset.name === current));
onSave(current);
});
});
}
async function loadSettings() {
const s = await api.get('/api/admin/settings');
const fetchEl = $('#fetch-status');
if (fetchEl) {
const interval = s.fetch_interval_hours ? `${s.fetch_interval_hours} h` : '6 h';
fetchEl.innerHTML = s.last_fetch_at
? `Maj auto toutes les ${esc(interval)} · Dernière : ${esc(fmtDate(s.last_fetch_at))}`
: 'Aucune mise à jour automatique';
}
$('#st-interval').value = s.fetch_interval_hours || '';
$('#st-blizz-id').value = s.blizzard_client_id || '';
$('#st-blizz-secret').value = s.blizzard_client_secret || '';
$('#st-build').value = s.wow_tools_build || '';
$('#st-manual-bosses').value = s.manual_mythic_bosses || '';
$('#st-manual-ach').value = s.manual_achievements || '';
$('#st-manual-ach-total').value = s.manual_achievements_total || '';
$('#st-manual-glad-wins').value = s.manual_glad_wins || '';
renderSingleClassSelect($('#st-manual-glad-class-select'), s.manual_glad_class, async (v) => {
await api.put('/api/admin/settings', { manual_glad_class: v });
});
$('#st-raid-name').value = s.mythic_raid_name || '';
$('#st-raid-total').value = s.mythic_raid_total || '';
$('#st-twitch-url').value = s.twitch_url || '';
$('#st-youtube-url').value = s.youtube_url || '';
$('#st-tiktok-url').value = s.tiktok_url || '';
$('#st-glad-season').value = s.glad_title_season || '';
$('#st-legend-season').value = s.legend_title_season || '';
renderClassSelect($('#st-manual-class90-select'), s.manual_class90_classes, async (v) => {
await api.put('/api/admin/settings', { manual_class90_classes: v });
});
renderClassSelect($('#st-manual-ss2300-select'), s.manual_ss2300_classes, async (v) => {
await api.put('/api/admin/settings', { manual_ss2300_classes: v });
});
const gladWinsInput = $('#st-manual-glad-wins');
gladWinsInput.dataset.savedValue = gladWinsInput.value;
gladWinsInput.addEventListener('change', async () => {
const v = gladWinsInput.value;
try {
await api.put('/api/admin/settings', { manual_glad_wins: v });
gladWinsInput.dataset.savedValue = v;
$('#settings-error').textContent = 'Victoires gladiateur enregistrées.';
$('#settings-error').style.color = 'var(--green)';
} catch (e) {
gladWinsInput.value = gladWinsInput.dataset.savedValue;
$('#settings-error').textContent = e.message;
$('#settings-error').style.color = '';
}
$('#settings-error').hidden = false;
});
$('#st-last-fetch').textContent = s.last_fetch_at ? `Dernière mise à jour : ${fmtDate(s.last_fetch_at)}` : 'Aucune mise à jour effectuée.';
$('#settings-error').hidden = true;
}
async function saveSettings() {
$('#settings-error').hidden = true;
try {
await api.put('/api/admin/settings', {
fetch_interval_hours: $('#st-interval').value,
blizzard_client_id: $('#st-blizz-id').value,
blizzard_client_secret: $('#st-blizz-secret').value,
wow_tools_build: $('#st-build').value,
manual_mythic_bosses: $('#st-manual-bosses').value,
manual_achievements: $('#st-manual-ach').value,
manual_achievements_total: $('#st-manual-ach-total').value,
manual_glad_wins: $('#st-manual-glad-wins').value,
mythic_raid_name: $('#st-raid-name').value,
mythic_raid_total: $('#st-raid-total').value,
twitch_url: $('#st-twitch-url').value,
youtube_url: $('#st-youtube-url').value,
tiktok_url: $('#st-tiktok-url').value,
glad_title_season: $('#st-glad-season').value,
legend_title_season: $('#st-legend-season').value
});
$('#settings-error').textContent = 'Paramètres enregistrés.';
$('#settings-error').hidden = false;
$('#settings-error').style.color = 'var(--green)';
await loadSettings();
} catch (e) {
$('#settings-error').textContent = e.message;
$('#settings-error').hidden = false;
}
}
$('#st-save').addEventListener('click', saveSettings);
$('#st-save-blizz').addEventListener('click', saveSettings);
$('#st-refresh').addEventListener('click', async () => {
$('#st-refresh').textContent = 'Mise à jour en cours…';
await api.post('/api/admin/refresh', {});
$('#st-refresh').textContent = 'Lancer une mise à jour complète';
await loadSettings();
await loadLogs();
await loadGoals();
});
$('#pw-save').addEventListener('click', async () => {
try {
await api.post('/api/admin/password', { current: $('#pw-current').value, next: $('#pw-next').value });
$('#pw-current').value = ''; $('#pw-next').value = '';
alert('Mot de passe changé.');
} catch (e) {
alert(e.message);
}
});
async function refreshAll() {
await api.post('/api/admin/refresh', {});
await loadChars();
await loadSettings();
await loadLogs();
await loadGoals();
}
$('#refresh-all-btn').addEventListener('click', async () => {
const btn = $('#refresh-all-btn');
btn.disabled = true;
btn.textContent = 'Mise à jour en cours…';
try {
await refreshAll();
} finally {
btn.disabled = false;
btn.textContent = 'Tout actualiser';
}
});
// ---- Logs ----
async function loadLogs() {
const logs = await api.get('/api/admin/logs');
$('#logs-tbody').innerHTML = logs.map((l) => `
| ${esc(fmtDate(l.at))} |
#${l.character_id ?? '—'} |
${esc(l.source)} |
${esc(l.status)} |
${esc(shortDetail(l.detail))} |
`).join('') || '| Aucun log. |
';
}
function shortDetail(d) {
try {
const j = JSON.parse(d || '{}');
return Object.entries(j).map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`).join(' · ');
} catch {
return d || '';
}
}
function fmtDate(iso) {
if (!iso) return '—';
const d = new Date(iso);
if (isNaN(d)) return iso;
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
}
const SOCIAL_SVG = {
youtube: '',
tiktok: ''
};
function renderSocials(el, socials) {
const buttons = [];
if (socials.youtube) buttons.push(`${SOCIAL_SVG.youtube}`);
if (socials.tiktok) buttons.push(`${SOCIAL_SVG.tiktok}`);
el.innerHTML = buttons.join('');
}
async function updateLiveStatus(statusEl, twitchUrl) {
const channel = twitchUrl.replace(/\/+$/, '').split('/').pop() || 'wycz';
const link = ``;
try {
const res = await fetch(`https://decapi.me/twitch/uptime/${encodeURIComponent(channel)}`, { mode: 'cors' });
const text = (await res.text()).trim();
const isLive = text && !/offline|hors ligne|hors-ligne|not live/i.test(text);
statusEl.innerHTML = isLive
? `${link} Live en cours`
: `${link} Hors ligne`;
} catch {
statusEl.innerHTML = `${link} Twitch`;
}
}
async function loadHeader() {
try {
const overview = await api.get('/api/overview');
renderSocials($('#socials'), { youtube: (overview.youtube && overview.youtube.url) || '', tiktok: (overview.tiktok && overview.tiktok.url) || '' });
updateLiveStatus($('#header-live'), (overview.twitch && overview.twitch.url) || 'https://twitch.tv/wycz');
} catch {
$('#socials').innerHTML = '';
}
}
const sessionUndoBtn = $('#session-undo');
if (sessionUndoBtn) sessionUndoBtn.addEventListener('click', sessionUndo);
loadHeader();
checkLogin();
})();