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