TUF Pro - Analytics

Comparing you to your Top 5 friends.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         TUF Pro - Analytics
// @namespace    https://sharadcodes.github.io
// @author       sharadcodes
// @description  Comparing you to your Top 5 friends.
// @match        *://*.takeuforward.org/*
// @supportURL   https://github.com/sharadcodes/UserScripts/issues
// @version      1.2
// @license      MIT
// @run-at       document-idle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'tuf_friends_list';
    let currentUserData = null;
    let allFriendsData = [];
    let tabChartInstance = null; // track tab chart separately for proper cleanup

    // FIX: Plain RGB strings — Chart.js cannot resolve CSS variable references
    const COLORS = {
        me:      'rgb(99, 102, 241)',  // indigo
        palette: [
            'rgb(59,  130, 246)',  // blue
            'rgb(139, 92,  246)',  // purple
            'rgb(16,  185, 129)',  // emerald
            'rgb(236, 72,  153)',  // pink
            'rgb(251, 146, 60)',   // amber
        ]
    };

    const TOP3_COLORS = ['rgb(239, 68, 68)', 'rgb(34, 197, 94)', 'rgb(59, 130, 246)'];

    function colorFor(user, myId, idx) {
        if (idx < 3) return TOP3_COLORS[idx];
        return user.id === myId ? COLORS.me : COLORS.palette[(idx - 3) % COLORS.palette.length];
    }

    function colorAlpha(rgb, a) {
        // FIX: build rgba properly — no string-replacing CSS vars
        return rgb.replace('rgb(', 'rgba(').replace(')', `, ${a})`);
    }

    // --- Data Management ---
    const getFriends = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    const saveFriend = (id) => {
        const f = getFriends();
        if (!f.includes(id.trim())) {
            localStorage.setItem(STORAGE_KEY, JSON.stringify([...f, id.trim()]));
            return true;
        }
        return false;
    };
    const removeFriend = (id) => {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(getFriends().filter(x => x !== id)));
    };

    async function fetchStats(username) {
        const year = new Date().getFullYear();
        try {
            const res = await fetch(`https://backend-go.takeuforward.org/api/v1/streak/heatmap/${username}?year=${year}`);
            if (!res.ok) throw new Error('HTTP ' + res.status);
            const json = await res.json();
            // FIX: guard against unexpected API shape
            const months = json?.data?.months || {};

            let practice = 0, theory = 0;
            const dailyDataMap = {};

            Object.keys(months).forEach(mStr => {
                const mIdx = parseInt(mStr) - 1;
                const monthData = months[mStr];
                Object.keys(monthData).forEach(dayStr => {
                    const dayVal = monthData[dayStr];
                    if (typeof dayVal !== 'object' || dayVal === null) return;
                    const p = dayVal.dsa_practice_problem || 0;
                    const t = dayVal.dsa_theory || 0;
                    practice += p;
                    theory   += t;
                    const date = new Date(year, mIdx, parseInt(dayStr));
                    dailyDataMap[date.toISOString().split('T')[0]] = { practice: p, theory: t };
                });
            });

            const today = new Date();
            // FIX: Array(30).fill(obj) shares one object reference — use Array.from instead
            const last30Days = Array.from({ length: 30 }, (_, i) => {
                const d = new Date(today);
                d.setDate(d.getDate() - (29 - i));
                const key = d.toISOString().split('T')[0];
                const entry = dailyDataMap[key] || { practice: 0, theory: 0 };
                return { date: key, practice: entry.practice, theory: entry.theory, total: entry.practice + entry.theory };
            });

            return {
                id: username,
                total: json?.data?.total || 0,
                practice, theory,
                last30Total: last30Days.reduce((s, d) => s + d.total, 0),
                last30Days,
                error: false,
            };
        } catch {
            return {
                id: username, total: 0, practice: 0, theory: 0, last30Total: 0,
                // FIX: use Array.from so each day object is independent
                last30Days: Array.from({ length: 30 }, (_, i) => {
                    const d = new Date();
                    d.setDate(d.getDate() - (29 - i));
                    return { date: d.toISOString().split('T')[0], practice: 0, theory: 0, total: 0 };
                }),
                error: true,
            };
        }
    }

    // --- Modal ---
    function createModal(buildFn) {
        const overlay = document.createElement('div');
        overlay.id = 'tuf-modal-overlay';
        Object.assign(overlay.style, {
            position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.75)',
            zIndex: '9999', display: 'flex', alignItems: 'center', justifyContent: 'center',
            backdropFilter: 'blur(6px)',
        });

        const modal = document.createElement('div');
        Object.assign(modal.style, {
            background: 'var(--color-bg, #fff)', color: 'var(--profile-text-primary)',
            padding: '28px 28px 24px', borderRadius: '16px',
            width: '95%', maxWidth: '960px', maxHeight: '90vh', overflowY: 'auto',
            border: '1px solid rgba(0,0,0,0.1)', position: 'relative',
            boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
        });

        // FIX: clicking backdrop closes modal
        overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        Object.assign(closeBtn.style, {
            position: 'absolute', top: '12px', right: '16px',
            fontSize: '26px', lineHeight: '1', background: 'none',
            border: 'none', cursor: 'pointer', color: '#999',
            padding: '4px 8px', borderRadius: '6px',
        });
        closeBtn.addEventListener('click', () => overlay.remove());

        buildFn(modal);
        modal.appendChild(closeBtn);
        overlay.appendChild(modal);
        document.body.appendChild(overlay);
    }

    // --- Shared Chart defaults ---
    function baseTooltip() {
        return {
            backgroundColor: 'rgba(15,15,15,0.85)',
            padding: 10,
            titleFont: { size: 12, weight: 'bold' },
            bodyFont: { size: 11 },
            borderColor: 'rgba(255,255,255,0.15)',
            borderWidth: 1,
            displayColors: true,
        };
    }

    function baseTick(size = 9) {
        return { font: { size }, color: '#888' };
    }

    // Day labels: show all dates
    function sparseLabels(days) {
        return days.map(d => {
            const dt = new Date(d.date);
            return `${dt.getMonth() + 1}/${dt.getDate()}`;
        });
    }

    // --- Analytics Tab (Quick Chart) ---
    function renderChartTab(container, myId) {
        // FIX: if Analytics opened before Leaderboard, auto-fetch data
        if (!currentUserData) {
            container.innerHTML = '<div style="padding:16px;text-align:center;font-size:12px;color:#888;">Loading…</div>';
            const friends = getFriends();
            Promise.all([...new Set([myId, ...friends])].map(fetchStats)).then(data => {
                currentUserData = data.find(u => u.id === myId);
                allFriendsData  = data.filter(u => u.id !== myId && !u.error);
                renderChartTab(container, myId);
            });
            return;
        }

        // FIX: destroy previous tab chart instance before rebuilding
        if (tabChartInstance) { tabChartInstance.destroy(); tabChartInstance = null; }

        // FIX: sort by last-30-days total, not annual total
        const chartData = [currentUserData, ...allFriendsData.slice(0, 5)]
            .sort((a, b) => b.last30Total - a.last30Total);

        container.innerHTML = `
            <div style="padding:8px;height:100%;display:flex;flex-direction:column;gap:8px;">
                <div style="display:flex;justify-content:space-between;align-items:center;">
                    <span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#888;">Last 30 Days</span>
                    <button id="view-detailed" style="font-size:10px;font-weight:700;color:#6366F1;background:none;border:none;cursor:pointer;padding:0;text-decoration:none;">Deep Dive ↗</button>
                </div>
                <div style="flex:1;position:relative;min-height:0;">
                    <canvas id="tuf-quick-chart" style="width:100%;height:100%;display:block;"></canvas>
                </div>
            </div>
        `;

        // FIX: requestAnimationFrame ensures the canvas is laid out before Chart.js reads its size
        requestAnimationFrame(() => {
            const ctx = document.getElementById('tuf-quick-chart');
            if (!ctx) return;
            tabChartInstance = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: sparseLabels(chartData[0].last30Days),
                    datasets: chartData.map((user, idx) => {
                        const c = colorFor(user, myId, idx);
                        return {
                            label: user.id === myId ? `${user.id} (You)` : user.id,
                            data: user.last30Days.map(d => d.total),
                            borderColor: c,
                            backgroundColor: colorAlpha(c, 0.07),
                            borderWidth: 2,
                            pointRadius: 0,
                            pointHoverRadius: 5,
                            fill: true,
                            tension: 0.35,
                        };
                    }),
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    interaction: { mode: 'index', intersect: false },
                    animation: { duration: 400 },
                    plugins: {
                        legend: {
                            position: 'bottom',
                            labels: { font: { size: 9, weight: 'bold' }, padding: 8, usePointStyle: true, pointStyle: 'circle' },
                        },
                        tooltip: baseTooltip(),
                    },
                    scales: {
                        y: { beginAtZero: true, ticks: baseTick(8), grid: { color: 'rgba(0,0,0,0.04)' } },
                        // FIX: maxRotation: 0 keeps labels horizontal so they don't overlap
                        x: { ticks: { ...baseTick(8), maxRotation: 0 }, grid: { display: false } },
                    },
                },
            });
        });

        document.getElementById('view-detailed').onclick = () => renderModalChart(chartData, myId);
    }

    // --- Deep Dive Modal ---
    function renderModalChart(chartData, myId) {
        createModal(modal => {
            modal.innerHTML = `
                <div style="margin-bottom:18px;padding-right:40px;">
                    <h2 style="font-size:20px;font-weight:800;margin:0 0 2px;">Last 30 Days — Deep Dive</h2>
                    <p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#888;margin:0;">
                        You vs top ${chartData.length - 1} friends
                    </p>
                </div>

                <div id="tuf-legend" style="display:flex;flex-wrap:wrap;gap:10px;padding:10px 12px;
                    background:rgba(0,0,0,0.03);border-radius:10px;margin-bottom:18px;"></div>

                <div id="tuf-stats" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:18px;"></div>

                <div style="display:grid;grid-template-columns:minmax(0,1fr);gap:14px;margin-bottom:14px;">
                    <div style="background:rgba(0,0,0,0.03);border-radius:12px;padding:14px;">
                        <p style="font-size:11px;font-weight:700;text-transform:uppercase;color:#888;margin:0 0 8px;">Daily Trend</p>
                        <div style="height:200px;position:relative;">
                            <canvas id="tuf-trend-chart"></canvas>
                        </div>
                    </div>
                </div>

                <div style="background:rgba(0,0,0,0.03);border-radius:12px;padding:14px;">
                    <p style="font-size:11px;font-weight:700;text-transform:uppercase;color:#888;margin:0 0 8px;">Practice vs Theory — All Users</p>
                    <div id="tuf-progress-table" style="max-height:250px;overflow-y:auto;"></div>
                </div>
            `;

            // Legend with 30-day totals
            const legend = modal.querySelector('#tuf-legend');
            chartData.forEach((u, i) => {
                const c = colorFor(u, myId, i);
                const el = document.createElement('div');
                el.style.cssText = 'display:flex;align-items:center;gap:6px;font-size:11px;font-weight:700;';
                el.style.color = c;
                el.innerHTML = `
                    <span style="width:10px;height:10px;border-radius:50%;background:${c};flex-shrink:0;"></span>
                    ${u.id}${u.id === myId ? ' (You)' : ''}
                    <span style="color:#aaa;font-weight:400;">· ${u.last30Total}</span>`;
                legend.appendChild(el);
            });

            // Key stats
            const stats = modal.querySelector('#tuf-stats');
            chartData.forEach((u, i) => {
                const c = colorFor(u, myId, i);
                const practice = u.last30Days.reduce((s, d) => s + d.practice, 0);
                const theory = u.last30Days.reduce((s, d) => s + d.theory, 0);
                const ratio = theory > 0 ? (practice / theory).toFixed(2) : practice.toFixed(2);
                const el = document.createElement('div');
                el.style.cssText = 'background:rgba(0,0,0,0.03);border-radius:10px;padding:12px;';
                el.innerHTML = `
                    <div style="font-size:11px;font-weight:700;color:${u.id === myId ? c : '#666'};margin-bottom:4px;">
                        ${u.id}${u.id === myId ? ' (You)' : ''}
                    </div>
                    <div style="font-size:20px;font-weight:800;color:${c};margin-bottom:2px;">${u.last30Total}</div>
                    <div style="font-size:9px;color:#888;">Practice: ${practice} · Theory: ${theory}</div>
                    <div style="font-size:9px;color:#888;">P/T Ratio: ${ratio}</div>
                `;
                stats.appendChild(el);
            });

            // FIX: requestAnimationFrame so canvases are sized before Chart.js reads them
            requestAnimationFrame(() => {
                // Chart 1: trend lines
                const trendCtx = modal.querySelector('#tuf-trend-chart');
                if (trendCtx) {
                    new Chart(trendCtx, {
                        type: 'line',
                        data: {
                            labels: sparseLabels(chartData[0].last30Days),
                            datasets: chartData.map((user, idx) => {
                                const c = colorFor(user, myId, idx);
                                return {
                                    label: user.id,
                                    data: user.last30Days.map(d => d.total),
                                    borderColor: c,
                                    backgroundColor: colorAlpha(c, 0.06),
                                    borderWidth: user.id === myId ? 2.5 : 1.5,
                                    pointRadius: 0,
                                    pointHoverRadius: 5,
                                    fill: user.id === myId,
                                    tension: 0.4,
                                };
                            }),
                        },
                        options: {
                            responsive: true, maintainAspectRatio: false,
                            interaction: { mode: 'index', intersect: false },
                            plugins: { legend: { display: false }, tooltip: baseTooltip() },
                            scales: {
                                y: { beginAtZero: true, ticks: baseTick(9), grid: { color: 'rgba(0,0,0,0.04)' } },
                                x: { ticks: { ...baseTick(8), maxRotation: 0 }, grid: { display: false } },
                            },
                        },
                    });
                }

                // Progress bar table for practice vs theory
                const progressTable = modal.querySelector('#tuf-progress-table');
                chartData.forEach((u, i) => {
                    const c = colorFor(u, myId, i);
                    const practice = u.last30Days.reduce((s, d) => s + d.practice, 0);
                    const theory = u.last30Days.reduce((s, d) => s + d.theory, 0);
                    const total = practice + theory;
                    const practicePct = total > 0 ? ((practice / total) * 100).toFixed(1) : 0;
                    const theoryPct = total > 0 ? ((theory / total) * 100).toFixed(1) : 0;

                    const row = document.createElement('div');
                    row.style.cssText = 'display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,0.05);';
                    row.innerHTML = `
                        <div style="font-size:11px;font-weight:700;color:${u.id === myId ? c : '#666'};min-width:80px;flex-shrink:0;">
                            ${u.id}${u.id === myId ? ' (You)' : ''}
                        </div>
                        <div style="flex:1;height:8px;background:rgba(0,0,0,0.1);border-radius:4px;overflow:hidden;display:flex;">
                            <div style="width:${practicePct}%;background:rgb(99, 102, 241);"></div>
                            <div style="width:${theoryPct}%;background:rgb(251, 146, 60);"></div>
                        </div>
                        <div style="font-size:10px;color:#888;min-width:80px;text-align:right;flex-shrink:0;">
                            P:${practice} T:${theory}
                        </div>
                    `;
                    progressTable.appendChild(row);
                });
            });
        });
    }

    // --- Leaderboard Tab ---
    async function renderRanksTab(content, myId) {
        content.innerHTML = '<div style="padding:16px;text-align:center;font-size:12px;color:#888;">Loading…</div>';

        const friends = getFriends();
        const data    = await Promise.all([...new Set([myId, ...friends])].map(fetchStats));
        currentUserData = data.find(u => u.id === myId);
        allFriendsData  = data.filter(u => u.id !== myId && !u.error);
        data.sort((a, b) => b.total - a.total);

        const top3 = data.slice(0, 3);
        const rest  = data.slice(3);

        let html = `
            <div style="display:flex;gap:6px;margin-bottom:8px;padding:0 2px;">
                <input id="tuf-add-input" type="text" placeholder="Add friend by ID…"
                    style="flex:1;background:transparent;border:1px solid rgba(0,0,0,0.15);border-radius:6px;
                        padding:5px 8px;font-size:11px;outline:none;color:inherit;">
                <button id="tuf-add-btn"
                    style="background:#6366F1;color:#fff;border:none;border-radius:6px;
                        padding:5px 12px;font-size:11px;font-weight:700;cursor:pointer;flex-shrink:0;">Add</button>
            </div>
        `;

        // FIX: empty state when no friends
        if (data.length <= 1 && friends.length === 0) {
            html += `<div style="padding:28px 8px;text-align:center;color:#aaa;font-size:12px;line-height:1.7;">
                No friends yet.<br>Add a friend ID above to compare scores.
            </div>`;
        } else {
            // Podium
            if (top3.length > 0) {
                const podium = [];
                if (top3[1]) podium.push({ ...top3[1], r: 2, h: 52, medal: '#9CA3AF' });
                if (top3[0]) podium.push({ ...top3[0], r: 1, h: 76, medal: '#EAB308' });
                if (top3[2]) podium.push({ ...top3[2], r: 3, h: 36, medal: '#F97316' });

                html += `<div style="display:flex;justify-content:center;align-items:flex-end;gap:6px;
                    border-bottom:1px solid rgba(0,0,0,0.08);padding:6px 4px 8px;margin-bottom:6px;">`;
                podium.forEach(x => {
                    const isMe = x.id === myId;
                    // FIX: full username via title tooltip, CSS ellipsis for overflow
                    html += `
                    <div style="display:flex;flex-direction:column;align-items:center;flex:1;position:relative;">
                        ${!isMe ? `<button class="tuf-del" data-id="${x.id}" title="Remove"
                            style="position:absolute;top:-2px;right:0;font-size:14px;line-height:1;
                                background:rgba(239,68,68,.1);border:none;cursor:pointer;
                                color:#EF4444;padding:2px 5px;border-radius:4px;">×</button>` : ''}
                        <div style="font-size:9px;font-weight:700;max-width:60px;overflow:hidden;
                            text-overflow:ellipsis;white-space:nowrap;text-align:center;
                            color:${isMe ? '#6366F1' : 'inherit'};"
                            title="${x.id}">${x.id}</div>
                        <div style="font-size:11px;font-weight:800;color:${x.medal};">${x.total}</div>
                        <div style="width:100%;height:${x.h}px;border-radius:4px 4px 0 0;
                            background:${isMe ? 'rgba(99,102,241,.1)' : 'rgba(0,0,0,.05)'};
                            border-top:2px solid ${isMe ? '#6366F1' : x.medal};
                            display:flex;align-items:flex-start;justify-content:center;padding-top:4px;">
                            <span style="font-size:9px;font-weight:800;opacity:.3;">${x.r}</span>
                        </div>
                    </div>`;
                });
                html += `</div>`;
            }

            // Remaining list
            if (rest.length > 0) {
                html += `<div style="overflow-y:auto;flex:1;display:flex;flex-direction:column;gap:2px;">`;
                rest.forEach((u, i) => {
                    const isMe = u.id === myId;
                    html += `
                    <div style="display:flex;justify-content:space-between;align-items:center;
                        padding:5px 8px;border-radius:7px;
                        background:${isMe ? 'rgba(99,102,241,.08)' : 'transparent'};
                        border-left:${isMe ? '2px solid #6366F1' : '2px solid transparent'};">
                        <span style="font-size:11px;font-weight:700;
                            color:${isMe ? '#6366F1' : 'inherit'};
                            white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:130px;"
                            title="${u.id}">${i + 4}. ${u.id}</span>
                        <div style="display:flex;align-items:center;gap:8px;flex-shrink:0;">
                            <span style="font-size:11px;font-weight:800;color:#6366F1;">${u.total}</span>
                            ${!isMe ? `<button class="tuf-del" data-id="${u.id}" title="Remove"
                                style="font-size:13px;background:rgba(239,68,68,.1);border:none;
                                    cursor:pointer;color:#EF4444;padding:1px 5px;border-radius:4px;
                                    line-height:1.4;">×</button>` : ''}
                        </div>
                    </div>`;
                });
                html += `</div>`;
            }
        }

        content.innerHTML = `<div style="display:flex;flex-direction:column;height:100%;gap:0;overflow:hidden;">${html}</div>`;

        // FIX: Enter key triggers add
        const input = content.querySelector('#tuf-add-input');
        const addFn = () => {
            const v = input.value.trim();
            if (v && saveFriend(v)) { input.value = ''; renderRanksTab(content, myId); }
        };
        content.querySelector('#tuf-add-btn').addEventListener('click', addFn);
        input.addEventListener('keydown', e => { if (e.key === 'Enter') addFn(); });

        // FIX: delete buttons always visible (not opacity-0)
        content.querySelectorAll('.tuf-del').forEach(b =>
            b.addEventListener('click', e => {
                removeFriend(e.currentTarget.dataset.id);
                renderRanksTab(content, myId);
            })
        );
    }

    // --- Bootstrap ---
    function init(target) {
        const myId = window.location.pathname.split('/')[2];

        target.innerHTML = `
            <div id="tuf-box" style="display:flex;flex-direction:column;height:100%;width:100%;">
                <div style="display:flex;border-bottom:1px solid rgba(0,0,0,0.08);margin-bottom:6px;">
                    <button id="t-lb"
                        style="flex:1;padding:7px 0;font-size:10px;font-weight:700;text-transform:uppercase;
                            letter-spacing:.05em;background:none;border:none;cursor:pointer;
                            color:#6366F1;border-bottom:2px solid #6366F1;">Leaderboard</button>
                    <button id="t-ac"
                        style="flex:1;padding:7px 0;font-size:10px;font-weight:700;text-transform:uppercase;
                            letter-spacing:.05em;background:none;border:none;cursor:pointer;
                            color:#aaa;border-bottom:2px solid transparent;">Analytics</button>
                </div>
                <div id="tuf-content"
                    style="flex:1;display:flex;flex-direction:column;
                        min-height:240px;max-height:340px;overflow:hidden;"></div>
            </div>
        `;

        const content = target.querySelector('#tuf-content');
        const btnLb   = target.querySelector('#t-lb');
        const btnAc   = target.querySelector('#t-ac');

        const activateTab = (which) => {
            const isLb = which === 'lb';
            btnLb.style.color        = isLb ? '#6366F1' : '#aaa';
            btnLb.style.borderBottom = isLb ? '2px solid #6366F1' : '2px solid transparent';
            btnAc.style.color        = isLb ? '#aaa' : '#6366F1';
            btnAc.style.borderBottom = isLb ? '2px solid transparent' : '2px solid #6366F1';
            if (isLb) renderRanksTab(content, myId);
            else      renderChartTab(content, myId);
        };

        btnLb.addEventListener('click', () => activateTab('lb'));
        btnAc.addEventListener('click', () => activateTab('ac'));
        activateTab('lb');
    }

    new MutationObserver(() => {
        const cell = document.querySelector('[style*="grid-area: skills"] .profile-skills-container');
        if (cell && !document.getElementById('tuf-box')) { cell.innerHTML = ''; init(cell); }
    }).observe(document.body, { childList: true, subtree: true });

})();