fc / backup-service.js
Althnayi's picture
Upload 56 files
cca0cf3 verified
Raw
History Blame Contribute Delete
45 kB
/**
* @fileoverview Encrypted Migration & Workspace Backup Service (AES-256-GCM / PBKDF2 / ZIP)
* @module services/backup-service
* @description خدمة تصدير واستيراد النسخ الاحتياطية الشاملة والمستقلة لبيئة العمل وتشفير الوسائط وحزم البيانات.
*/
(global => {
'use strict';
/**
* تعريفات وحدات وأقسام بيئة العمل القابلة للتصدير والاستيراد
*/
const MODULE_DEFINITIONS = Object.freeze([
Object.freeze({
key: 'rotation_library',
title: 'مكتبة المنشورات وتدوير المحتوى',
icon: 'fa-arrows-spin',
color: '#0d9488',
desc: 'تصدير واستيراد كل منشوراتك المؤرشفة، الكلمات المفتاحية، الصور المرفقة، وحالة التدوير.'
}),
Object.freeze({
key: 'rotation_groups',
title: 'حزم ومجموعات المنشورات المخصصة',
icon: 'fa-layer-group',
color: '#8b5cf6',
desc: 'تصدير واستيراد الحزم والمجموعات المخصصة بالمنشورات المحددة للتدوير.'
}),
Object.freeze({
key: 'rotation_followups',
title: 'رسائل المتابعة التلقائية للواتساب',
icon: 'fa-brands fa-whatsapp',
color: '#00a884',
desc: 'تصدير واستيراد رسائل ومواعيد وتكرار المتابعة المرتبطة بالعملاء والمنشورات.'
}),
Object.freeze({
key: 'campaign_history',
title: 'أرشيف سجلات الحملات والنتائج',
icon: 'fa-box-archive',
color: '#3b82f6',
desc: 'تصدير واستيراد السجل الكامل لنتائج النشر والبث وتفاصيل الوجهات.'
}),
Object.freeze({
key: 'scheduled_campaigns',
title: 'طابور الحملات المجدولة المستقبلية',
icon: 'fa-calendar-days',
color: '#f59e0b',
desc: 'تصدير واستيراد طابور الحملات المجدولة ومواعيدها ومرفقاتها.'
}),
Object.freeze({
key: 'facebook_registry',
title: 'مجموعات فيسبوك، رادار السلامة، والتصنيفات',
icon: 'fa-users-rectangle',
color: '#1877f2',
desc: 'تصدير واستيراد قائمة جروبات الفيسبوك، الشرائح، الحجر الصحي، والتصنيفات.'
}),
Object.freeze({
key: 'whatsapp_queue',
title: 'طابور مستلمي وأرقام واتساب',
icon: 'fa-address-book',
color: '#10b981',
desc: 'تصدير واستيراد قائمة أرقام ومستلمي الواتساب في طابور الانتظار.'
}),
Object.freeze({
key: 'protection_pacing',
title: 'إعدادات التخفي وفترات تبريد الاتصال',
icon: 'fa-shield-halved',
color: '#6366f1',
desc: 'تصدير واستيراد إعدادات دروع التخفي ومحاكاة السلوك وفترات الانتظار.'
}),
Object.freeze({
key: 'contacts_manager',
title: 'سجل جهات الاتصال والعملاء والوسوم',
icon: 'fa-address-book',
color: '#0284c7',
desc: 'تصدير واستيراد كل بيانات العميل، مراحل المبيعات، الوسوم، والحقول المخصصة.'
}),
Object.freeze({
key: 'calendar_agenda',
title: 'تقويم المهام والأجندة الذكية والتذكيرات',
icon: 'fa-calendar-check',
color: '#1d4ed8',
desc: 'تصدير واستيراد كل أحداث ومهام الأجندة والمواعيد المجدولة.'
}),
Object.freeze({
key: 'fb_recent_published',
title: 'سجل التعبئة السريعة لمنشورات فيسبوك',
icon: 'fa-clock-rotate-left',
color: '#059669',
desc: 'تصدير واستيراد سجل التعبئة السريعة والمنشورات الحديثة.'
}),
Object.freeze({
key: 'store_rates_cache',
title: 'إعدادات المتجر وأسعار الصرف المؤرشفة',
icon: 'fa-store',
color: '#7c3aed',
desc: 'تصدير واستيراد أسعار الصرف المؤرشفة محلياً وبيانات المتجر.'
})
]);
/**
* تحويل Blob إلى نص Base64
*/
async function blobToBase64(blob) {
if (global.FritreeStorage && typeof global.FritreeStorage.blobToB64 === 'function') {
return await global.FritreeStorage.blobToB64(blob);
}
if (blob instanceof Uint8Array) {
blob = new Blob([blob]);
}
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return `data:${blob.type || 'image/jpeg'};base64,${btoa(binary)}`;
}
/**
* تحويل نص Base64 إلى Blob
*/
function base64ToBlob(base64Data) {
if (global.FritreeStorage && typeof global.FritreeStorage.b64ToBlob === 'function') {
return global.FritreeStorage.b64ToBlob(base64Data);
}
const parts = base64Data.split(",");
const mimeType = parts[0].split(":")[1].split(";")[0];
const byteCharacters = atob(parts[1] || parts[0]);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: mimeType });
}
/**
* بناء واجهة قسم النسخ الاحتياطي والتصدير والاستيراد (#oboufoudrv)
*/
function buildBackupPaneLayout() {
const contentArea = document.getElementById('s7ihkirolg');
if (!contentArea) return false;
const host = document.getElementById('fritree-dynamic-panes-host') || contentArea;
let pane = document.getElementById('oboufoudrv');
if (pane) {
if (pane.parentElement !== host) {
host.appendChild(pane);
}
return true;
}
pane = document.createElement('section');
pane.className = 'rjiai07g77';
pane.id = 'oboufoudrv';
pane.innerHTML = `
<div class="d5tp7way8h zcjbe1otmg" style="margin: 0; padding: clamp(14px, 1.8vw, 24px); border-radius: var(--radius-xl); border: 1px solid var(--border); direction: rtl; text-align: right; background: var(--bg-card); box-sizing: border-box;">
<!-- ترويسة مركز النسخ الاحتياطي -->
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 14px; margin-bottom: 18px; flex-wrap: wrap; gap: 10px;">
<div>
<h2 style="font-size: clamp(15px, 1.2vw, 18px); font-weight: 900; display: flex; align-items: center; gap: 8px; margin: 0; color: var(--text-main);">
<i class="fa-solid fa-database" style="color: #059669;"></i>
<span>مركز النسخ الاحتياطي وتصدير واستيراد بيئة العمل</span>
</h2>
<p style="font-size: 11px; color: var(--text-muted); margin-top: 4px; font-weight: 700;">
<i class="fa-solid fa-shield-halved"></i>
<span>تشفير متقدم AES-256-GCM مع دعم تقييد النسخ بكود الجهاز الموثق أو تصديرها كنسخ عامة.</span>
</p>
</div>
</div>
<!-- شبكة التصدير الشامل والاستيراد المباشر -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(clamp(280px, 35vw, 1fr), 1fr)); gap: 14px; margin-bottom: 20px;">
<!-- 1. تصدير النسخة الشاملة -->
<div style="border: 1px solid var(--border); border-radius: var(--radius-lg); padding: clamp(14px, 1.4vw, 18px); background: var(--bg-subtle); display: flex; flex-direction: column; gap: 10px; box-sizing: border-box;">
<h3 style="font-size: 13.5px; font-weight: 900; margin: 0; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--border); padding-bottom: 8px; color: var(--text-main);">
<i class="fa-solid fa-file-zipper" style="color: var(--primary);"></i> <span>تصدير حزمة بيئة العمل الشاملة (.zip)</span>
</h3>
<p style="font-size: 11px; color: var(--text-muted); line-height: 1.5; margin: 0; font-weight: 600;">
يتم تشفير وحزم كافة الأقسام (المنشورات، جهات الاتصال، التقويم، الحملات، الإعدادات، والوسائط) داخل ملف ZIP مشفر.
</p>
<div class="d0uttwsjz2" style="margin: 0;">
<label style="font-size: 11px; font-weight: 700;"><i class="fa-solid fa-key"></i> <span>كلمة مرور التشفير (اختياري):</span></label>
<input type="password" id="backup-master-password" class="xy26ymifmf" placeholder="اتركها فارغة إذا كان الملف بدون كلمة سر..." style="padding: 7px 10px;">
</div>
<div class="d0uttwsjz2" style="margin: 0;">
<label style="font-size: 11px; font-weight: 700;"><i class="fa-solid fa-lock"></i> <span>تقييد بكود جهاز محدد (128 حرف - اختياري):</span></label>
<input type="text" id="backup-target-device-id" class="xy26ymifmf" placeholder="اتركه فارغاً لتصدير نسخة مفتوحة..." style="padding: 7px 10px; font-size: 10px; direction: ltr; text-align: left; font-family: var(--font-code);">
</div>
<button type="button" id="btn-export-master-backup" class="itdnt14mss" style="padding: 10px; font-size: 12px; font-weight: 800; margin-top: auto; border-radius: var(--radius-md);">
<i class="fa-solid fa-download"></i> <span>تصدير وتنزيل الحزمة الشاملة الآن</span>
</button>
</div>
<!-- 2. استيراد النسخة الاحتياطية -->
<div style="border: 1px solid var(--border); border-radius: var(--radius-lg); padding: clamp(14px, 1.4vw, 18px); background: var(--bg-subtle); display: flex; flex-direction: column; gap: 10px; box-sizing: border-box;">
<h3 style="font-size: 13.5px; font-weight: 900; margin: 0; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--border); padding-bottom: 8px; color: var(--text-main);">
<i class="fa-solid fa-file-import" style="color: var(--wa-brand);"></i> <span>استيراد واستعادة ملف النسخة الاحتياطية</span>
</h3>
<div class="jbhynmzpf2" id="backup-import-dropzone" style="padding: 20px; text-align: center; cursor: pointer;">
<i class="fa-solid fa-cloud-arrow-up fa-2x" style="color: var(--wa-brand); margin-bottom: 6px;"></i>
<strong style="font-size: 12px; color: var(--text-main);"><i class="fa-solid fa-folder-open"></i> <span>اسحب وأفلت ملف الـ ZIP المشفر هنا</span></strong>
<span style="font-size: 10.5px; color: var(--text-muted);">أو اضغط لاختيار الملف من جهازك</span>
<input type="file" id="backup-import-file-input" accept=".zip" style="display: none;">
</div>
<div class="d0uttwsjz2" style="margin: 0;">
<label style="font-size: 11px; font-weight: 700;"><i class="fa-solid fa-key"></i> <span>كلمة المرور لفك التشفير:</span></label>
<input type="password" id="backup-import-password" class="xy26ymifmf" placeholder="اكتب كلمة المرور في حال كان الملف محمياً..." style="padding: 7px 10px;">
</div>
</div>
</div>
<!-- شبكة تصدير الأقسام الفردية المستقلة -->
<div style="border-top: 1px solid var(--border); padding-top: 16px;">
<h3 style="font-size: 13.5px; font-weight: 900; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; color: var(--text-main);">
<i class="fa-solid fa-cubes" style="color: #8b5cf6;"></i> <span>تصدير واستيراد الأقسام والوحدات المستقلة</span>
</h3>
<div id="backup-modules-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(clamp(250px, 24vw, 320px), 1fr)); gap: 12px; box-sizing: border-box;"></div>
</div>
</div>
`;
host.appendChild(pane);
renderModulesGrid();
bindBackupEvents();
return true;
}
/**
* رسم شبكة بطاقات الأقسام المستقلة
*/
function renderModulesGrid() {
const container = document.getElementById('backup-modules-grid');
if (!container) return;
container.innerHTML = '';
MODULE_DEFINITIONS.forEach(mod => {
const card = document.createElement('div');
card.style.cssText = `
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 14px;
background: var(--bg-card);
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
box-shadow: var(--shadow-xs);
box-sizing: border-box;
`;
card.innerHTML = `
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<strong style="font-size: 12.5px; display: flex; align-items: center; gap: 6px; color: var(--text-main);">
<i class="fa-solid ${mod.icon}" style="color: ${mod.color}; font-size: 14px;"></i>
<span>${mod.title}</span>
</strong>
</div>
<p style="font-size: 11px; color: var(--text-muted); line-height: 1.4; margin-bottom: 8px; font-weight: 600;">
${mod.desc}
</p>
<span id="module-count-${mod.key}" style="font-size: 10px; font-weight: 700; background: var(--bg-subtle); padding: 2px 8px; border-radius: var(--radius-pill); border: 1px solid var(--border); display: inline-block; color: var(--text-secondary); font-family: var(--font-code);">
جاري الحساب...
</span>
</div>
<button type="button" class="ipxi2jz4g0 btn-export-single-mod" data-mod-key="${mod.key}" style="padding: 6px 10px; font-size: 10.5px; font-weight: 800; margin-top: 6px; width: 100%; min-height: 30px;">
<i class="fa-solid fa-file-export"></i> <span>تصدير هذا القسم فقط</span>
</button>
`;
container.appendChild(card);
});
refreshModuleBadges();
}
/**
* تجميع بيانات قسم محدد من التخزين المشفر
* @param {string} moduleKey
* @returns {Promise<{ payload: Object, mediaBlobsToExport: Array<Object> }>}
*/
async function collectModuleData(moduleKey) {
const payload = {
moduleKey: moduleKey,
timestamp: new Date().toISOString(),
version: "1.3.0-ModularFull",
algo: "PBKDF2-SHA256-AES-GCM"
};
const mediaBlobsToExport = [];
switch (moduleKey) {
case 'rotation_library':
payload.contentLibraryData = await global.FritreeStorage.get('local_content_library', []);
payload.contentCategoriesData = await global.FritreeStorage.get('local_content_categories', []);
payload.previousPostsData = await global.FritreeStorage.get('local_previous_posts', []);
payload.selectedPrevPostIds = await global.FritreeStorage.get('local_selected_prev_posts', []);
payload.isRotationActive = await global.FritreeStorage.get('local_is_rotation_active', 'false');
payload.rotationMode = await global.FritreeStorage.get('local_rotation_mode', 'balanced');
if (payload.previousPostsData && Array.isArray(payload.previousPostsData)) {
for (const post of payload.previousPostsData) {
if (post.mediaReferences && Array.isArray(post.mediaReferences)) {
for (const ref of post.mediaReferences) {
const fullBlob = await global.FritreeStorage.get(`media_blob_${ref.id}`);
const thumbBlob = await global.FritreeStorage.get(`media_thumb_${ref.id}`);
if (fullBlob) {
mediaBlobsToExport.push({ id: ref.id, suffix: 'blob', blob: fullBlob, filename: ref.filename || `${ref.id}_blob.jpg` });
}
if (thumbBlob) {
mediaBlobsToExport.push({ id: ref.id, suffix: 'thumb', blob: thumbBlob, filename: ref.filename || `${ref.id}_thumb.jpg` });
}
}
}
}
}
break;
case 'rotation_groups':
payload.postGroupsData = await global.FritreeStorage.get('local_post_groups', []);
break;
case 'rotation_followups':
payload.followupPosts = (await global.FritreeStorage.get('local_previous_posts', [])).filter(p => p.enableWaFollowUp);
break;
case 'campaign_history':
payload.campaignHistoryData = await global.FritreeStorage.get('campaignHistoryData', []);
break;
case 'scheduled_campaigns':
payload.scheduledCampaignsData = await global.FritreeStorage.get('scheduledCampaignsData', []);
break;
case 'facebook_registry':
payload.localGroups = await global.FritreeStorage.get('local_groups', []);
payload.selectedGroupIds = await global.FritreeStorage.get('local_selected_groups', []);
payload.savedTags = await global.FritreeStorage.get('local_saved_tags', {});
payload.groupClassifications = await global.FritreeStorage.get('local_fb_group_classifications', {});
payload.quarantinedGroups = await global.FritreeStorage.get('local_fb_quarantined_groups', []);
payload.quarantineTimestamps = await global.FritreeStorage.get('local_fb_quarantine_timestamps', {});
payload.customCooldowns = await global.FritreeStorage.get('local_fb_custom_group_cooldowns', {});
payload.whitelistedGroups = await global.FritreeStorage.get('local_fb_whitelisted_groups', []);
payload.blacklistedGroups = await global.FritreeStorage.get('local_fb_blacklisted_groups', []);
break;
case 'whatsapp_queue':
payload.waRecipients = await global.FritreeStorage.get('local_wa_recipients_queue', []);
break;
case 'protection_pacing':
payload.sleepIntervals = await global.FritreeStorage.get('local_sleep_intervals', {});
payload.protectionSettings = await global.FritreeStorage.get('local_protection_settings', {});
payload.shieldConfigMatrix = await global.FritreeStorage.get('local_shield_config_matrix', {});
break;
case 'contacts_manager':
payload.contactsDatabase = await global.FritreeStorage.get('local_contacts_database', []);
break;
case 'calendar_agenda':
payload.calendarEvents = await global.FritreeStorage.get('local_calendar_events_db', []);
break;
case 'fb_recent_published':
payload.fbRecentPublishedPosts = await global.FritreeStorage.get('fbRecentPublishedPosts', []);
if (payload.fbRecentPublishedPosts && Array.isArray(payload.fbRecentPublishedPosts)) {
for (const item of payload.fbRecentPublishedPosts) {
if (item.images && Array.isArray(item.images)) {
for (const img of item.images) {
const rawBlob = await global.FritreeStorage.get(`media_blob_recent_${item.id}_${img.id}`);
if (rawBlob) {
mediaBlobsToExport.push({ id: `recent_${item.id}_${img.id}`, suffix: 'blob', blob: rawBlob, filename: img.name || `${img.id}.jpg` });
}
}
}
}
}
break;
case 'store_rates_cache':
payload.cachedRates = await global.FritreeStorage.get('cached_internet_rates', {});
payload.ratesLastUpdated = await global.FritreeStorage.get('rates_last_updated', '');
break;
}
return { payload, mediaBlobsToExport };
}
/**
* استعادة وفك تشفير وحفظ بيانات قسم محدد في التخزين المشفر
*/
async function restoreModuleData(payload, zipFiles = [], passphrase = '', currentId = '') {
if (payload.contentCategoriesData) await global.FritreeStorage.set('local_content_categories', payload.contentCategoriesData);
if (payload.contentLibraryData) await global.FritreeStorage.set('local_content_library', payload.contentLibraryData);
if (payload.previousPostsData) {
const existingPosts = await global.FritreeStorage.get('local_previous_posts', []);
const mergedPosts = [...existingPosts];
for (const importedPost of payload.previousPostsData) {
if (!importedPost.id) {
importedPost.id = 'rot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
}
const duplicateIndex = mergedPosts.findIndex(p => p.id === importedPost.id);
if (duplicateIndex !== -1) {
mergedPosts[duplicateIndex] = { ...mergedPosts[duplicateIndex], ...importedPost };
} else {
mergedPosts.push(importedPost);
}
}
await global.FritreeStorage.set('local_previous_posts', mergedPosts);
}
if (payload.postGroupsData) await global.FritreeStorage.set('local_post_groups', payload.postGroupsData);
if (payload.campaignHistoryData) await global.FritreeStorage.set('campaignHistoryData', payload.campaignHistoryData);
if (payload.scheduledCampaignsData) await global.FritreeStorage.set('scheduledCampaignsData', payload.scheduledCampaignsData);
if (payload.localGroups) await global.FritreeStorage.set('local_groups', payload.localGroups);
if (payload.selectedGroupIds) await global.FritreeStorage.set('local_selected_groups', payload.selectedGroupIds);
if (payload.savedTags) await global.FritreeStorage.set('local_saved_tags', payload.savedTags);
if (payload.groupClassifications) await global.FritreeStorage.set('local_fb_group_classifications', payload.groupClassifications);
if (payload.quarantinedGroups) await global.FritreeStorage.set('local_fb_quarantined_groups', payload.quarantinedGroups);
if (payload.quarantineTimestamps) await global.FritreeStorage.set('local_fb_quarantine_timestamps', payload.quarantineTimestamps);
if (payload.customCooldowns) await global.FritreeStorage.set('local_fb_custom_group_cooldowns', payload.customCooldowns);
if (payload.whitelistedGroups) await global.FritreeStorage.set('local_fb_whitelisted_groups', payload.whitelistedGroups);
if (payload.blacklistedGroups) await global.FritreeStorage.set('local_fb_blacklisted_groups', payload.blacklistedGroups);
if (payload.waRecipients) await global.FritreeStorage.set('local_wa_recipients_queue', payload.waRecipients);
if (payload.sleepIntervals) await global.FritreeStorage.set('local_sleep_intervals', payload.sleepIntervals);
if (payload.protectionSettings) await global.FritreeStorage.set('local_protection_settings', payload.protectionSettings);
if (payload.shieldConfigMatrix) await global.FritreeStorage.set('local_shield_config_matrix', payload.shieldConfigMatrix);
if (payload.contactsDatabase && Array.isArray(payload.contactsDatabase)) {
const existingContacts = await global.FritreeStorage.get('local_contacts_database', []);
const mergedContacts = [...existingContacts];
for (const impCnt of payload.contactsDatabase) {
if (!impCnt.id) impCnt.id = 'cnt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
const dupIdx = mergedContacts.findIndex(c => c.id === impCnt.id || (c.phone && impCnt.phone && c.phone === impCnt.phone));
if (dupIdx !== -1) {
mergedContacts[dupIdx] = { ...mergedContacts[dupIdx], ...impCnt };
} else {
mergedContacts.push(impCnt);
}
}
await global.FritreeStorage.set('local_contacts_database', mergedContacts);
}
if (payload.calendarEvents && Array.isArray(payload.calendarEvents)) {
const existingEvents = await global.FritreeStorage.get('local_calendar_events_db', []);
const mergedEvents = [...existingEvents];
for (const impEvt of payload.calendarEvents) {
if (!impEvt.id) impEvt.id = 'evt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
const dupIdx = mergedEvents.findIndex(e => e.id === impEvt.id);
if (dupIdx !== -1) {
mergedEvents[dupIdx] = { ...mergedEvents[dupIdx], ...impEvt };
} else {
mergedEvents.push(impEvt);
}
}
await global.FritreeStorage.set('local_calendar_events_db', mergedEvents);
}
if (payload.fbRecentPublishedPosts) await global.FritreeStorage.set('fbRecentPublishedPosts', payload.fbRecentPublishedPosts);
if (payload.cachedRates) await global.FritreeStorage.set('cached_internet_rates', payload.cachedRates);
if (payload.ratesLastUpdated) await global.FritreeStorage.set('rates_last_updated', payload.ratesLastUpdated);
// فك تشفير واستعادة الصور والوسائط المرفقة
const imageFiles = zipFiles.filter(file => file.name.startsWith("images/") && file.name.endsWith(".enc"));
for (const file of imageFiles) {
const fileParts = file.name.slice("images/".length).replace(".enc", "").split("_");
const suffix = fileParts.pop();
const mediaId = fileParts.join("_");
const encryptedImgText = new TextDecoder().decode(file.data);
const decryptedImgB64 = await global.FritreeCrypto.decryptBackupString(encryptedImgText, passphrase, currentId);
if (decryptedImgB64) {
const imgBlob = base64ToBlob(decryptedImgB64);
await global.FritreeStorage.set(`media_${suffix}_${mediaId}`, imgBlob);
}
}
}
/**
* حساب إحصائيات وعدد العناصر بكل قسم
*/
async function refreshModuleBadges() {
const stats = {};
for (const m of MODULE_DEFINITIONS) {
let countText = "0 عناصر";
switch (m.key) {
case 'rotation_library':
const posts = await global.FritreeStorage.get('local_previous_posts', []);
countText = `${posts.length.toLocaleString('en-US')} منشورات بالمكتبة`;
break;
case 'rotation_groups':
const groups = await global.FritreeStorage.get('local_post_groups', []);
countText = `${groups.length.toLocaleString('en-US')} حزمة مخصصة`;
break;
case 'rotation_followups':
const followups = (await global.FritreeStorage.get('local_previous_posts', [])).filter(p => p.enableWaFollowUp);
countText = `${followups.length.toLocaleString('en-US')} رسالة متابعة`;
break;
case 'campaign_history':
const history = await global.FritreeStorage.get('campaignHistoryData', []);
countText = `${history.length.toLocaleString('en-US')} سجل حملات`;
break;
case 'scheduled_campaigns':
const sched = await global.FritreeStorage.get('scheduledCampaignsData', []);
countText = `${sched.length.toLocaleString('en-US')} حملات مجدولة`;
break;
case 'facebook_registry':
const fbGroups = await global.FritreeStorage.get('local_groups', []);
countText = `${fbGroups.length.toLocaleString('en-US')} مجموعة متصلة`;
break;
case 'whatsapp_queue':
const waQ = await global.FritreeStorage.get('local_wa_recipients_queue', []);
countText = `${waQ.length.toLocaleString('en-US')} مستلم بالطابور`;
break;
case 'protection_pacing':
countText = "مصفوفة فترات الأمان والتبريد";
break;
case 'contacts_manager':
const contacts = await global.FritreeStorage.get('local_contacts_database', []);
countText = `${contacts.length.toLocaleString('en-US')} جهة اتصال بالسجل`;
break;
case 'calendar_agenda':
const calEvents = await global.FritreeStorage.get('local_calendar_events_db', []);
countText = `${calEvents.length.toLocaleString('en-US')} أحداث ومهام مجدولة`;
break;
case 'fb_recent_published':
const recentFb = await global.FritreeStorage.get('fbRecentPublishedPosts', []);
countText = `${recentFb.length.toLocaleString('en-US')} منشورات سريعة`;
break;
case 'store_rates_cache':
countText = "أسعار الصرف وسجل الكاش المعتمد";
break;
}
stats[m.key] = countText;
const badge = document.getElementById(`module-count-${m.key}`);
if (badge) {
badge.textContent = countText;
}
}
return stats;
}
/**
* تصدير قسم منفرد وتشفيره وحزمه في ملف ZIP
*/
async function exportSingleModule(moduleKey, passphrase = '', targetAccountId = '') {
const { payload, mediaBlobsToExport } = await collectModuleData(moduleKey);
const plainText = JSON.stringify(payload);
const targetId = targetAccountId ? targetAccountId.trim() : null;
const encryptedMetaBase64 = await global.FritreeCrypto.encryptBackupString(plainText, passphrase, targetId);
if (!encryptedMetaBase64) {
throw new Error("فشل تشفير بيانات القسم المحدد.");
}
const zipFiles = [];
const textEncoder = new TextEncoder();
zipFiles.push({
name: "module_data.json.enc",
data: textEncoder.encode(encryptedMetaBase64)
});
if (mediaBlobsToExport.length > 0) {
for (const img of mediaBlobsToExport) {
const imgB64 = await blobToBase64(img.blob);
const encryptedImgB64 = await global.FritreeCrypto.encryptBackupString(imgB64, passphrase, targetId);
if (encryptedImgB64) {
zipFiles.push({
name: `images/${img.id}_${img.suffix}.enc`,
data: textEncoder.encode(encryptedImgB64)
});
}
}
}
const zipBytes = global.FritreeZipArchive.createZip(zipFiles);
const blob = new Blob([zipBytes], { type: 'application/zip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const now = new Date();
const pad = num => String(num).padStart(2, '0');
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`;
a.download = `قسم_مشفر_${moduleKey}_${timestamp}.zip`;
a.click();
URL.revokeObjectURL(url);
}
/**
* تصدير النسخة الاحتياطية الشاملة لبيئة العمل
*/
async function exportMasterFullBackup(selectedModuleKeys, passphrase = '', targetAccountId = '') {
const masterPayload = {
timestamp: new Date().toISOString(),
version: "1.3.0-MasterFull",
algo: "PBKDF2-SHA256-AES-GCM",
modules: {}
};
const allMediaBlobs = [];
for (const key of selectedModuleKeys) {
const { payload, mediaBlobsToExport } = await collectModuleData(key);
masterPayload.modules[key] = payload;
if (mediaBlobsToExport.length > 0) {
allMediaBlobs.push(...mediaBlobsToExport);
}
}
const plainText = JSON.stringify(masterPayload);
const targetId = targetAccountId ? targetAccountId.trim() : null;
const encryptedMetadataBase64 = await global.FritreeCrypto.encryptBackupString(plainText, passphrase, targetId);
if (!encryptedMetadataBase64) {
throw new Error("فشل تشفير بيئة العمل الشاملة.");
}
const zipFiles = [];
const textEncoder = new TextEncoder();
zipFiles.push({
name: "workspace_data.json.enc",
data: textEncoder.encode(encryptedMetadataBase64)
});
for (const img of allMediaBlobs) {
const imgB64 = await blobToBase64(img.blob);
const encryptedImgB64 = await global.FritreeCrypto.encryptBackupString(imgB64, passphrase, targetId);
if (encryptedImgB64) {
zipFiles.push({
name: `images/${img.id}_${img.suffix}.enc`,
data: textEncoder.encode(encryptedImgB64)
});
}
}
const zipBytes = global.FritreeZipArchive.createZip(zipFiles);
const blob = new Blob([zipBytes], { type: 'application/zip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const now = new Date();
const pad = num => String(num).padStart(2, '0');
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`;
const fileSuffix = targetId ? "مقيد_بمعرف" : "عام_مفتوح";
a.download = `حزمة_بيئة_عمل_فريتري_شاملة_${fileSuffix}_${timestamp}.zip`;
a.click();
URL.revokeObjectURL(url);
}
/**
* استيراد ملف ZIP وفك تشفيره
*/
async function importBackupZipFile(arrayBuffer, passphrase = '') {
const zipFiles = global.FritreeZipArchive.readZip(arrayBuffer);
const metaFile = zipFiles.find(f => f.name === "workspace_data.json.enc" || f.name === "module_data.json.enc");
if (!metaFile) {
throw new Error("فشل الاستيراد: صيغة الملف غير صالحة أو غير متوافقة.");
}
const encryptedText = new TextDecoder().decode(metaFile.data);
const currentId = await global.FritreeCrypto.getOrGenerateAccountId();
const decryptedText = await global.FritreeCrypto.decryptBackupString(encryptedText, passphrase, currentId);
if (!decryptedText) {
throw new Error("فشل فك التشفير: كلمة المرور غير صحيحة أو الملف مقفول لجهاز آخر.");
}
const data = JSON.parse(decryptedText);
if (data.modules && typeof data.modules === 'object') {
for (const [mKey, mPayload] of Object.entries(data.modules)) {
await restoreModuleData(mPayload, zipFiles, passphrase, currentId);
}
} else {
await restoreModuleData(data, zipFiles, passphrase, currentId);
}
await refreshModuleBadges();
return true;
}
/**
* ربط أحداث واجهة النسخ الاحتياطي
*/
function bindBackupEvents() {
const btnExportMaster = document.getElementById('btn-export-master-backup');
if (btnExportMaster) {
btnExportMaster.onclick = async () => {
const pass = (document.getElementById('backup-master-password')?.value || '').trim();
const targetId = (document.getElementById('backup-target-device-id')?.value || '').trim();
const keys = MODULE_DEFINITIONS.map(m => m.key);
btnExportMaster.disabled = true;
btnExportMaster.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>جاري التشفير والحزم...</span>';
try {
await exportMasterFullBackup(keys, pass, targetId);
alert('تم بنجاح تصدير وتنزيل حزمة بيئة العمل الشاملة المشفرة!');
} catch (e) {
alert('خطأ أثناء التصدير: ' + e.message);
} finally {
btnExportMaster.disabled = false;
btnExportMaster.innerHTML = '<i class="fa-solid fa-download"></i> <span>تصدير وتنزيل الحزمة الشاملة الآن</span>';
}
};
}
const dropzone = document.getElementById('backup-import-dropzone');
const fileInput = document.getElementById('backup-import-file-input');
if (dropzone && fileInput) {
dropzone.onclick = () => fileInput.click();
fileInput.onchange = async (e) => {
if (e.target.files.length > 0) {
await handleIncomingZipFile(e.target.files[0]);
}
};
dropzone.ondragover = (e) => {
e.preventDefault();
dropzone.style.borderColor = 'var(--primary)';
dropzone.style.background = 'var(--primary-light)';
};
dropzone.ondragleave = () => {
dropzone.style.borderColor = 'var(--border-strong)';
dropzone.style.background = 'var(--bg-subtle)';
};
dropzone.ondrop = async (e) => {
e.preventDefault();
dropzone.style.borderColor = 'var(--border-strong)';
dropzone.style.background = 'var(--bg-subtle)';
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
await handleIncomingZipFile(e.dataTransfer.files[0]);
}
};
}
document.querySelectorAll('.btn-export-single-mod').forEach(btn => {
btn.onclick = async () => {
const modKey = btn.getAttribute('data-mod-key');
if (!modKey) return;
const pass = prompt("اكتب كلمة مرور لتشفير هذا القسم (اختياري، اضغط موافق بدون كتابة للتصدير العام):") || '';
try {
await exportSingleModule(modKey, pass);
alert('تم بنجاح تصدير ملف القسم المشفر!');
} catch (e) {
alert('خطأ أثناء تصدير القسم: ' + e.message);
}
};
});
}
async function handleIncomingZipFile(file) {
if (!file || !file.name.endsWith('.zip')) {
alert('يرجى اختيار ملف بصيغة ZIP صالحة!');
return;
}
const pass = (document.getElementById('backup-import-password')?.value || '').trim();
try {
const buffer = await file.arrayBuffer();
await importBackupZipFile(buffer, pass);
alert('تم استيراد واستعادة بيئة العمل بنجاح تام!');
if (global.syncWorkspaceUI) global.syncWorkspaceUI();
} catch (e) {
alert('فشل استيراد الملف: ' + e.message);
}
}
/**
* تصدير خدمة النسخ الاحتياطي والتصدير/الاستيراد
*/
global.FritreeBackupService = {
MODULE_DEFINITIONS,
buildPane: buildBackupPaneLayout,
refreshBadges: refreshModuleBadges,
exportSingleModule,
exportMasterBackup: exportMasterFullBackup,
importZipFile: importBackupZipFile,
init: function() {
buildBackupPaneLayout();
return true;
}
};
global.FritreeMigration = global.FritreeBackupService;
if (typeof document !== 'undefined') {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
global.FritreeBackupService.init();
} else {
document.addEventListener('DOMContentLoaded', () => global.FritreeBackupService.init());
}
}
})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);