import { useCollection, useProxy, useInterval, useNotify } from '@deplixo/sdk'; import Dashboard from './components/Dashboard.jsx'; import ChartView from './components/ChartView.jsx'; import History from './components/History.jsx'; import Alerts from './components/Alerts.jsx'; import Settings from './components/Settings.jsx'; import { DEFAULT_SETTINGS, sortedSnapshots, findMe, generateDemoLeaderboard, tryFetchBotRix, analyzeStall, fmtTime, } from './components/lib.js'; const TABS = [ { id: 'dashboard', label: '📊 اللوحة' }, { id: 'chart', label: '📈 الرسوم' }, { id: 'alerts', label: '🔔 التنبيهات' }, { id: 'history', label: '🗂️ السجل' }, { id: 'settings', label: '⚙️ الإعدادات' }, ]; function App() { const [tab, setTab] = useState('dashboard'); const [busy, setBusy] = useState(false); const [failStreak, setFailStreak] = useState(0); const proxy = useProxy(); const notify = useNotify(); const snapshotsCol = useCollection('botrix-snapshots'); const alertsCol = useCollection('botrix-alerts'); const settingsCol = useCollection('botrix-settings'); // Resolve current settings (single-doc pattern in a collection). const settingsDoc = (settingsCol.items || [])[0]; const settings = { ...DEFAULT_SETTINGS, ...(settingsDoc?.value || {}) }; const snapshots = snapshotsCol.items || []; const sorted = sortedSnapshots(snapshots); const latest = sorted.length ? sorted[sorted.length - 1].snapshot : null; const saveSettings = async (next) => { if (settingsDoc) await settingsCol.update(settingsDoc.id, next); else await settingsCol.add(next); pushAlert('warn', '⚙️ تم تحديث الإعدادات'); }; const pushAlert = async (level, message) => { try { await alertsCol.add({ level, message, ts: Date.now() }); if (typeof notify === 'function') { try { notify(message); } catch {} } } catch {} }; const runCheck = async () => { if (busy) return; setBusy(true); try { const prevSnap = latest; let snap = null; // 1) Try real BotRix fetch. const proxyFn = typeof proxy === 'function' ? proxy : proxy?.fetch; if (proxyFn) { try { snap = await tryFetchBotRix(proxyFn, settings.channel); } catch { snap = null; } } if (!snap) { // Track fail streak and alert if exceeded. setFailStreak((n) => { const next = n + 1; if (next === settings.failThreshold) { pushAlert('bad', `⚠️ فشل قراءة BotRix ${next} مرات متتالية — الخدمة قد تكون معطلة`); } return next; }); // Fallback to demo/simulated snapshot so the app remains usable. snap = generateDemoLeaderboard(prevSnap, settings.targetUser); } else { setFailStreak(0); } // Save snapshot. await snapshotsCol.add({ snapshot: snap }); // Analyze changes against previous. if (prevSnap) { const prevMe = findMe(prevSnap.entries, settings.targetUser); const curMe = findMe(snap.entries, settings.targetUser); if (prevMe && curMe) { // Rank change. if (curMe.rank < prevMe.rank) { await pushAlert('ok', `🏆 صعود ${settings.targetUser} من #${prevMe.rank} إلى #${curMe.rank}`); } else if (curMe.rank > prevMe.rank) { await pushAlert('bad', `⚠️ نزول ${settings.targetUser} من #${prevMe.rank} إلى #${curMe.rank}`); } // Rival proximity (rank #rivalRank). const prevRival = prevSnap.entries.find((e) => e.rank === settings.rivalRank); const curRival = snap.entries.find((e) => e.rank === settings.rivalRank); if (prevRival && curRival) { const prevGap = Math.abs(prevRival.points - prevMe.points); const curGap = Math.abs(curRival.points - curMe.points); if (prevGap > 20 && curGap <= 20) { await pushAlert('warn', `🔥 المركز #${settings.rivalRank} أصبح قريبًا جدًا (فرق ${curGap} نقطة)`); } } // Overtaken / overtook by any top-10 competitor. const wasAhead = new Set(prevSnap.entries.filter((e) => e.points < prevMe.points).map((e) => e.name)); const nowBehind = new Set(snap.entries.filter((e) => e.points > curMe.points).map((e) => e.name)); for (const name of nowBehind) { if (wasAhead.has(name)) { await pushAlert('bad', `🚨 ${name} تجاوز ${settings.targetUser}`); } } const wasBehind = new Set(prevSnap.entries.filter((e) => e.points > prevMe.points).map((e) => e.name)); const nowAhead = new Set(snap.entries.filter((e) => e.points < curMe.points).map((e) => e.name)); for (const name of nowAhead) { if (wasBehind.has(name)) { await pushAlert('ok', `🟢 ${settings.targetUser} تجاوز ${name}`); } } } } // Stall detection uses the FULL updated series. const updated = [...sorted, { snapshot: snap }]; const stall = analyzeStall(updated, settings.targetUser, settings.stallThreshold); // Fire once when transition into stalled. const prevStall = analyzeStall(sorted, settings.targetUser, settings.stallThreshold); if (stall.status === 'stalled' && prevStall.status !== 'stalled') { await pushAlert('bad', `🔴 احتمال توقف جمع النقاط. آخر زيادة: ${fmtTime(stall.lastGrowthTs)} — أول قراءة بدون تغيير: ${fmtTime(stall.firstStallTs)}`); } else if (stall.status === 'ok' && prevStall.status === 'stalled') { await pushAlert('ok', '🟢 عادت النقاط للارتفاع من جديد'); } } catch (err) { await pushAlert('bad', `خطأ أثناء الفحص: ${err?.message || err}`); } finally { setBusy(false); } }; // Automatic polling. useInterval(() => { runCheck(); }, Math.max(1, settings.intervalMinutes) * 60 * 1000); const alerts = alertsCol.items || []; const stall = analyzeStall(sorted, settings.targetUser, settings.stallThreshold); const statusPill = stall.status === 'ok' ? 'ok' : stall.status === 'stalled' ? 'bad' : stall.status === 'warn' ? 'warn' : ''; return (
قناة KICK: {settings.channel} • تحديث كل {settings.intervalMinutes} دقيقة