(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' ? 'Gladiateur' : 'Légende'} ${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)}

${PVP_PER_SPEC.map(([key, label]) => ` `).join('')}
BracketCette saisonMax (xp)VictoiresDéfaites
${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(''); $('#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) => `

${esc(g.title)}

`).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(); })();