Torn Faction Online Tracker (Mobile Panel)

Tracks faction members' online times and shows a small collapsible panel on Torn PDA for easy chain scheduling.

// ==UserScript==
// @name         Torn Faction Online Tracker (Mobile Panel)
// @namespace    https://www.torn.com/
// @version      1.1
// @description  Tracks faction members' online times and shows a small collapsible panel on Torn PDA for easy chain scheduling.
// @author       Paul
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // ==== CONFIG ====
    const POLL_MINUTES = 5;        // how often to check (minutes)
    const LOOKBACK_HOURS = 24 * 7; // keep 7 days of data
    // =================

    let apiKey = GM_getValue('apiKey', '');
    if (!apiKey) {
        apiKey = prompt('Enter your Torn API key (with faction access):');
        if (apiKey) GM_setValue('apiKey', apiKey);
    }

    if (!apiKey) {
        alert('No API key set. Script stopped.');
        return;
    }

    const API_URL = `https://api.torn.com/faction/?selections=members&key=${apiKey}`;

    // --- UI Panel Setup ---
    const panel = document.createElement('div');
    panel.id = 'tornTrackerPanel';
    panel.style.cssText = `
        position: fixed;
        top: 100px;
        left: 0;
        z-index: 99999;
        background: rgba(20,20,20,0.9);
        color: #fff;
        font-size: 12px;
        padding: 8px;
        border-radius: 0 6px 6px 0;
        width: 180px;
        max-height: 240px;
        overflow-y: auto;
        display: none;
        box-shadow: 0 0 5px rgba(0,0,0,0.4);
    `;
    document.body.appendChild(panel);

    const toggleBtn = document.createElement('button');
    toggleBtn.textContent = '🔘';
    toggleBtn.style.cssText = `
        position: fixed;
        top: 100px;
        left: 0;
        z-index: 100000;
        background: #333;
        color: white;
        border: none;
        border-radius: 0 6px 6px 0;
        padding: 6px 8px;
        font-size: 16px;
    `;
    toggleBtn.onclick = () => {
        panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
    };
    document.body.appendChild(toggleBtn);

    function updatePanel(text) {
        panel.innerHTML = `<b>Faction Activity</b><br><pre>${text}</pre>`;
    }

    async function fetchFaction() {
        try {
            const res = await fetch(API_URL);
            const data = await res.json();
            if (data.error) throw new Error(data.error.error);
            return data.members || {};
        } catch (err) {
            updatePanel('❌ Error fetching faction');
            console.log('[Torn Tracker] Error:', err.message);
            return null;
        }
    }

    function recordMembers(members) {
        const now = Date.now();
        let logs = JSON.parse(GM_getValue('logs', '[]'));

        for (const [id, m] of Object.entries(members)) {
            const ts = m.last_action.timestamp;
            logs.push({ id, name: m.name, lastOnline: ts, recorded: now });
        }

        // Keep only recent logs
        const cutoff = now - LOOKBACK_HOURS * 3600 * 1000;
        logs = logs.filter(l => l.recorded >= cutoff);

        GM_setValue('logs', JSON.stringify(logs));
    }

    function analyze() {
        const logs = JSON.parse(GM_getValue('logs', '[]'));
        const hours = new Array(24).fill(0);
        for (const log of logs) {
            const h = new Date(log.lastOnline * 1000).getHours();
            hours[h]++;
        }

        const sorted = hours.map((c, h) => ({ h, c }))
            .sort((a, b) => b.c - a.c)
            .slice(0, 6);

        let txt = '';
        sorted.forEach(x => {
            txt += `${String(x.h).padStart(2, '0')}:00 — ${x.c}\\n`;
        });
        updatePanel(txt || 'No data yet');
    }

    async function run() {
        const members = await fetchFaction();
        if (members) {
            recordMembers(members);
            analyze();
        }
    }

    GM_registerMenuCommand('Analyze Now', analyze);
    run();
    setInterval(run, POLL_MINUTES * 60 * 1000);
})();