// ═══════════════════════════════════════════════════════════ // stats-tracker.js // تتبع الاستخدام — إرسال بيانات مجهولة إلى Supabase // + طابور Offline في IndexedDB لمزامنة الأحداث عند عودة الإنترنت // ═══════════════════════════════════════════════════════════ (function(){ 'use strict'; // ═══ Fallback إن لم يكن Supabase جاهزًا ═══ if (!window.WaziriSB || !window.WaziriSB.client) { window.WaziriStats = { recordVisit: function(){}, recordSessionStart: function(){ return Promise.resolve(null); }, recordSessionEnd: function(){}, recordFileTransfer: function(){}, touchSession: function(){}, updateUserNameForAllVisits: function(){}, updateUserNameForAllSessions: function(){}, abandonOldSessions: function(){}, detectDevice: function(){ return {}; }, drainStatsQueue: function(){}, statsQueueCount: function(){ return Promise.resolve(0); } }; return; } var sb = window.WaziriSB.client; var CFG = window.WaziriConfig; var VISIT_KEY = 'waziri-visit-tracked-v1'; var cachedDevice = null; // ═══ أدوات ═══ function makeLocalUUID(){ try { if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID(); } catch(e) {} return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c){ var r = Math.random() * 16 | 0; var v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // ═══ كشف الجهاز ═══ function detectDevice() { if (cachedDevice) return cachedDevice; var ua = navigator.userAgent || ''; var device = 'Unknown', os = 'Unknown'; if (/Android/i.test(ua)) { if (/Samsung|SM-|GT-|SCH-|SHV-|SGH-/i.test(ua)) device = 'Samsung'; else if (/Xiaomi|Redmi|Mi\s|POCO/i.test(ua)) device = 'Xiaomi'; else if (/Huawei|HONOR/i.test(ua)) device = 'Huawei'; else if (/Oppo|CPH|Find/i.test(ua)) device = 'Oppo'; else if (/Vivo|V[0-9]{4}/i.test(ua)) device = 'Vivo'; else if (/OnePlus|ONEPLUS/i.test(ua)) device = 'OnePlus'; else if (/Realme|RMX/i.test(ua)) device = 'Realme'; else if (/Infinix/i.test(ua)) device = 'Infinix'; else if (/Tecno/i.test(ua)) device = 'Tecno'; else device = 'Android'; var m = ua.match(/Android\s([\d.]+)/); os = 'Android' + (m ? ' ' + m[1] : ''); } else if (/iPhone/i.test(ua)) { device = 'iPhone'; os = 'iOS'; } else if (/iPad/i.test(ua)) { device = 'iPad'; os = 'iPadOS'; } else if (/Windows NT 10/.test(ua)) { device = 'PC'; os = 'Windows 10/11'; } else if (/Windows NT 6\.1/.test(ua)) { device = 'PC'; os = 'Windows 7'; } else if (/Windows/i.test(ua)) { device = 'PC'; os = 'Windows'; } else if (/Macintosh|Mac OS X/.test(ua)) { device = 'Mac'; os = 'macOS'; } else if (/Linux/i.test(ua)) { device = 'PC'; os = 'Linux'; } else if (/CrOS/i.test(ua)) { device = 'Chromebook'; os = 'ChromeOS'; } var browser = 'Other'; if (/Edg\//.test(ua)) browser = 'Edge'; else if (/Chrome\//.test(ua) && !/Edg\//.test(ua)) browser = 'Chrome'; else if (/Firefox\//.test(ua)) browser = 'Firefox'; else if (/Safari\//.test(ua) && !/Chrome\//.test(ua)) browser = 'Safari'; var isInstalled = !!(window.matchMedia && window.matchMedia('(display-mode: standalone)').matches); cachedDevice = { device: device, os: os, browser: browser, isInstalled: isInstalled }; return cachedDevice; } // ═══════════════════════════════════════════════════════════ // Stats Queue (IndexedDB) — طابور الأحداث Offline // ═══════════════════════════════════════════════════════════ var STATS_QUEUE_DB = 'waziri-stats-queue'; var STATS_QUEUE_STORE = 'events'; var drainInProgress = false; function openStatsQueue(){ return new Promise(function(res, rej){ var r = indexedDB.open(STATS_QUEUE_DB, 1); r.onupgradeneeded = function(){ if(!r.result.objectStoreNames.contains(STATS_QUEUE_STORE)){ r.result.createObjectStore(STATS_QUEUE_STORE, { keyPath: 'qid', autoIncrement: true }); } }; r.onsuccess = function(){ res(r.result); }; r.onerror = function(){ rej(r.error); }; }); } function statsQueueAdd(event){ return openStatsQueue().then(function(db){ return new Promise(function(res, rej){ var tx = db.transaction(STATS_QUEUE_STORE, 'readwrite'); var req = tx.objectStore(STATS_QUEUE_STORE).add(event); req.onsuccess = function(){ res(req.result); }; req.onerror = function(){ rej(req.error); }; }); }).catch(function(){ /* صامت */ }); } function statsQueueGetAll(){ return openStatsQueue().then(function(db){ return new Promise(function(res){ var tx = db.transaction(STATS_QUEUE_STORE, 'readonly'); var req = tx.objectStore(STATS_QUEUE_STORE).getAll(); req.onsuccess = function(){ res(req.result || []); }; req.onerror = function(){ res([]); }; }); }).catch(function(){ return []; }); } function statsQueueDelete(qid){ return openStatsQueue().then(function(db){ return new Promise(function(res){ var tx = db.transaction(STATS_QUEUE_STORE, 'readwrite'); tx.objectStore(STATS_QUEUE_STORE).delete(qid); tx.oncomplete = function(){ res(); }; tx.onerror = function(){ res(); }; }); }).catch(function(){}); } function statsQueueCount(){ return openStatsQueue().then(function(db){ return new Promise(function(res){ var tx = db.transaction(STATS_QUEUE_STORE, 'readonly'); var req = tx.objectStore(STATS_QUEUE_STORE).count(); req.onsuccess = function(){ res(req.result || 0); }; req.onerror = function(){ res(0); }; }); }).catch(function(){ return 0; }); } // ═══ إرسال عنصر واحد من الطابور ═══ async function trySendQueuedItem(item){ try { if (item.type === 'session_start') { var r1 = await sb.from('sessions').upsert(item.payload, { onConflict: 'id', ignoreDuplicates: true }); return !r1.error; } if (item.type === 'session_end') { var r2 = await sb.from('sessions').update(item.payload).eq('id', item.sessionLocalId); return !r2.error; } if (item.type === 'file_transfer') { var r3 = await sb.from('file_transfers').upsert(item.payload, { onConflict: 'client_id', ignoreDuplicates: true }); return !r3.error; } return true; // نوع غير معروف → تجاهله } catch(e) { return false; } } // ═══ تفريغ الطابور ═══ async function drainStatsQueue(){ if (!navigator.onLine) return; if (drainInProgress) return; drainInProgress = true; try { var items = await statsQueueGetAll(); if (!items.length) return; for (var i = 0; i < items.length; i++) { var ok = await trySendQueuedItem(items[i]); if (ok) { await statsQueueDelete(items[i].qid); } else { // فشل → توقّف وحاول لاحقًا break; } } } catch(e) { // صامت } finally { drainInProgress = false; } } // ═══ 1) تسجيل زيارة ═══ function recordVisit() { try { if (sessionStorage.getItem(VISIT_KEY)) return; sessionStorage.setItem(VISIT_KEY, '1'); var d = detectDevice(); var name = CFG.getUserName() || 'مجهول'; var deviceId = CFG.getDeviceId(); sb.from('visits').insert({ device_type: d.device, os: d.os, browser: d.browser, is_installed: d.isInstalled, device_name: name, device_id: deviceId }).then(function(res){ if (res.error) console.warn('[Stats] visit error:', res.error.message); }).catch(function(e){ console.warn('[Stats] visit catch:', e.message); }); } catch (e) {} } // ═══ 2) بدء جلسة — يرجع UUID محليًا فورًا + يضيف للطابور عند الفشل ═══ function recordSessionStart(opts) { opts = opts || {}; var localId = makeLocalUUID(); try { var d = detectDevice(); var name = CFG.getUserName(); var deviceId = CFG.getDeviceId(); var payload = { id: localId, session_code: opts.sessionCode || null, mode: opts.mode || 'unknown', role: opts.role || null, device_type: d.device, os: d.os, browser: d.browser, device_name: name || opts.deviceName || null, user_name: name || opts.deviceName || null, device_id: deviceId, is_installed: d.isInstalled, last_activity_at: new Date().toISOString(), end_reason: null }; // محاولة فورية sb.from('sessions').upsert(payload, { onConflict: 'id', ignoreDuplicates: true }) .then(function(res){ if (res.error) { statsQueueAdd({ type: 'session_start', payload: payload, sessionLocalId: localId, time: Date.now() }); } }) .catch(function(){ statsQueueAdd({ type: 'session_start', payload: payload, sessionLocalId: localId, time: Date.now() }); }); } catch (e) { // حتى لو فشل التحضير → نرجع الـ ID } return Promise.resolve(localId); } // ═══ 3) تحديث نشاط الجلسة — fire-and-forget (لا يُطابَر) ═══ function touchSession(sessionId) { if (!sessionId) return; try { sb.from('sessions').update({ last_activity_at: new Date().toISOString() }).eq('id', sessionId).then(function(res){ if (res.error) console.warn('[Stats] touch error:', res.error.message); }).catch(function(){}); } catch(e) {} } // ═══ 4) إنهاء جلسة — يضاف للطابور عند الفشل ═══ function recordSessionEnd(sessionId, opts) { if (!sessionId) return; opts = opts || {}; try { var payload = { ended_at: new Date().toISOString(), duration_seconds: opts.durationSeconds || 0, files_sent_count: opts.filesSentCount || 0, files_sent_bytes: opts.filesSentBytes || 0, files_recv_count: opts.filesRecvCount || 0, files_recv_bytes: opts.filesRecvBytes || 0, end_reason: opts.endReason || 'completed' }; sb.from('sessions').update(payload).eq('id', sessionId) .then(function(res){ if (res.error) { statsQueueAdd({ type: 'session_end', payload: payload, sessionLocalId: sessionId, time: Date.now() }); } }) .catch(function(){ statsQueueAdd({ type: 'session_end', payload: payload, sessionLocalId: sessionId, time: Date.now() }); }); } catch (e) {} } // ═══ 5) تسجيل نقل ملف — يضاف للطابور عند الفشل ═══ function recordFileTransfer(opts) { opts = opts || {}; try { var clientId = makeLocalUUID(); var payload = { client_id: clientId, session_code: opts.sessionCode || null, user_name: opts.user_name || null, file_name: opts.fileName || null, file_type: opts.fileType || 'other', file_size: opts.fileSize || 0, direction: opts.direction || 'sent', mode: opts.mode || 'unknown' }; sb.from('file_transfers').upsert(payload, { onConflict: 'client_id', ignoreDuplicates: true }) .then(function(res){ if (res.error) { statsQueueAdd({ type: 'file_transfer', payload: payload, sessionLocalId: null, time: Date.now() }); } }) .catch(function(){ statsQueueAdd({ type: 'file_transfer', payload: payload, sessionLocalId: null, time: Date.now() }); }); } catch (e) {} } // ═══ 6) تحديث اسم المستخدم في كل الزيارات السابقة ═══ function updateUserNameForAllVisits(name) { if (!name) return; try { var deviceId = CFG.getDeviceId(); sb.from('visits').update({ device_name: name }) .eq('device_id', deviceId) .then(function(res){ if (res.error) console.warn('[Stats] update visits error:', res.error.message); }).catch(function(){}); } catch(e) {} } // ═══ 7) تحديث اسم المستخدم في كل الجلسات السابقة ═══ function updateUserNameForAllSessions(name) { if (!name) return; try { var deviceId = CFG.getDeviceId(); sb.from('sessions').update({ user_name: name, device_name: name }) .eq('device_id', deviceId) .then(function(res){ if (res.error) console.warn('[Stats] update sessions error:', res.error.message); }).catch(function(){}); } catch(e) {} } // ═══ 8) وسم الجلسات القديمة كـ "مهجورة" (عالمي + throttle) ═══ var ABANDON_THROTTLE_KEY = 'waziri-abandon-last-run'; var ABANDON_THROTTLE_MS = 5 * 60 * 1000; function abandonOldSessions() { try { var lastRun = parseInt(localStorage.getItem(ABANDON_THROTTLE_KEY) || '0', 10); if (Date.now() - lastRun < ABANDON_THROTTLE_MS) return; localStorage.setItem(ABANDON_THROTTLE_KEY, String(Date.now())); var thresholdIso = new Date(Date.now() - CFG.ABANDON_MS).toISOString(); var nowIso = new Date().toISOString(); sb.from('sessions').update({ ended_at: nowIso, end_reason: 'abandoned' }) .is('ended_at', null) .lt('last_activity_at', thresholdIso) .then(function(res){ if (res.error) console.warn('[Stats] abandon #1 error:', res.error.message); }).catch(function(){}); sb.from('sessions').update({ ended_at: nowIso, end_reason: 'abandoned' }) .is('ended_at', null) .is('last_activity_at', null) .lt('started_at', thresholdIso) .then(function(res){ if (res.error) console.warn('[Stats] abandon #2 error:', res.error.message); }).catch(function(){}); } catch(e) {} } // ═══════════════════════════════════════════════════════════ // Init: تفريغ الطابور عند الفرص المناسبة (صامت) // ═══════════════════════════════════════════════════════════ setTimeout(function(){ drainStatsQueue(); }, 3000); if (typeof window !== 'undefined') { window.addEventListener('online', function(){ setTimeout(function(){ drainStatsQueue(); }, 500); }); document.addEventListener('visibilitychange', function(){ if (document.visibilityState === 'visible') { setTimeout(function(){ drainStatsQueue(); }, 800); } }); } // ═══ الواجهة العامة ═══ window.WaziriStats = { recordVisit: recordVisit, recordSessionStart: recordSessionStart, recordSessionEnd: recordSessionEnd, recordFileTransfer: recordFileTransfer, touchSession: touchSession, updateUserNameForAllVisits: updateUserNameForAllVisits, updateUserNameForAllSessions: updateUserNameForAllSessions, abandonOldSessions: abandonOldSessions, detectDevice: detectDevice, drainStatsQueue: drainStatsQueue, statsQueueCount: statsQueueCount }; })();