TUF Pro - Chart.js Analytics

Replaces heatmaps with Chart.js powered analytics comparing you to your Top 5 friends.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         TUF Pro - Chart.js Analytics
// @namespace    https://sharadcodes.github.io
// @author       sharadcodes
// @description  Replaces heatmaps with Chart.js powered analytics comparing you to your Top 5 friends.
// @match        *://*.takeuforward.org/*
// @supportURL   https://github.com/sharadcodes/UserScripts/issues
// @version      9.1
// @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})`);
    }

    // FIX: toISOString() returns UTC date — use local date instead
    function fmtLocal(d) {
        return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
    }

    // --- 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[fmtLocal(date)] = { 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 = fmtLocal(d);
                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: fmtLocal(d), 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 [y, m, day] = d.date.split('-').map(Number);
            return `${m}/${day}`;
        });
    }

    // --- 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 });

})();