// ============================================================================ // File: modules/rotation.js // ============================================================================ (global => { 'use strict'; let previousPostsData = []; let selectedPrevPostIds = new Set(); let isRotationActive = false; let editPrevPostMedia = []; let addPrevPostMedia = []; let currentMetricsPage = 1; const METRICS_ITEMS_PER_PAGE = 5; const STATE_INTEGRITY_SALT = "FritreeRotationMatrixDynamicIntegrityVerificationSalt_2026"; function sanitizeRotationInput(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Post schema integrity validator */ function isValidPostSchema(p) { return p && typeof p === 'object' && typeof p.id === 'string'; } /** * Semantic Text Analyzer to apply context-specific Font Awesome icons to dynamically generated templates */ function getIconForTemplateText(text, categories) { const raw = (text || '').toLowerCase(); const catStr = (categories || []).join(' ').toLowerCase(); if (raw.includes('apartment') || raw.includes('house') || raw.includes('villa') || raw.includes('rent') || catStr.includes('estate') || catStr.includes('realestate')) { return ''; } if (raw.includes('sale') || raw.includes('discount') || raw.includes('promo') || raw.includes('deal') || catStr.includes('sale') || catStr.includes('ecom')) { return ''; } if (raw.includes('software') || raw.includes('saas') || raw.includes('tech') || raw.includes('code') || catStr.includes('tech') || catStr.includes('software')) { return ''; } if (raw.includes('support') || raw.includes('help') || raw.includes('service') || catStr.includes('service') || catStr.includes('support')) { return ''; } if (raw.includes('crypto') || raw.includes('token') || raw.includes('coin') || raw.includes('wallet') || catStr.includes('crypto')) { return ''; } return ''; } // ============================================================================ // Anti-Tamper State Checksum Validation (SHA-256 Intrusion Guard) // ============================================================================ async function calculateStateChecksum() { const structuralConcat = previousPostsData.map(p => `${p.id}:${p.status}`).sort().join('||'); if (typeof FritreeCrypto !== 'undefined' && typeof FritreeCrypto.signReceipt === 'function') { return await FritreeCrypto.signReceipt("ROT_MATRIX", structuralConcat.length, "verify", structuralConcat, STATE_INTEGRITY_SALT); } return "lite_hash_" + structuralConcat.length; } async function saveDynamicStateChecksum() { const computedSignature = await calculateStateChecksum(); await FritreeStorage.set('local_rotation_matrix_integrity_sig', computedSignature); } async function runSymmetricDataIntegrityCheck() { if (previousPostsData.length === 0) return true; const savedSignature = await FritreeStorage.get('local_rotation_matrix_integrity_sig', ''); if (!savedSignature) return true; const actualSignature = await calculateStateChecksum(); return savedSignature === actualSignature; } async function savePrevPostsToLocal() { await FritreeStorage.set('local_previous_posts', previousPostsData); await saveDynamicStateChecksum(); } // ============================================================================ // Module Initializer & UI Binders (LTR Standard with Instant Auto-Save) // ============================================================================ async function initRotationModule() { try { const rawPrevPosts = await FritreeStorage.get('local_previous_posts', []); previousPostsData = Array.isArray(rawPrevPosts) ? rawPrevPosts.filter(isValidPostSchema) : []; const cachedSelected = await FritreeStorage.get('local_selected_prev_posts', []); selectedPrevPostIds = new Set(Array.isArray(cachedSelected) ? cachedSelected.filter(id => typeof id === 'string') : []); const activeState = await FritreeStorage.get('local_is_rotation_active', 'false'); isRotationActive = (activeState === 'true'); // Audit the integrity signature with auto-healing enabled const isIntegrityValid = await runSymmetricDataIntegrityCheck(); if (!isIntegrityValid) { console.log("[Fritree Crypto] Auto-healing Content Rotation Library signature to preserve modifications."); await savePrevPostsToLocal(); } recalculateAllPostScores(); const openModalBtn = document.getElementById('btn-open-previous-posts-modal'); const disableRotationBtn = document.getElementById('btn-disable-prev-posts-mode'); const searchInput = document.getElementById('prev-posts-search'); const filterSelect = document.getElementById('prev-posts-filter-status'); const sortSelect = document.getElementById('prev-posts-sort-by'); const selectAllCheck = document.getElementById('prev-posts-select-all'); const filterKeywordInput = document.getElementById('prev-posts-filter-keyword'); const filterCategoryInput = document.getElementById('prev-posts-filter-category'); const filterNotesInput = document.getElementById('prev-posts-filter-notes'); const filterFollowUpDateInput = document.getElementById('prev-posts-filter-followup-date'); const filterMinScoreInput = document.getElementById('prev-posts-filter-min-score'); const filterMaxCharsInput = document.getElementById('prev-posts-filter-max-chars'); // Set up inline navigation focus triggers if (openModalBtn) openModalBtn.addEventListener('click', showPreviousPostsModal); if (disableRotationBtn) disableRotationBtn.addEventListener('click', disableRotationModeGlobally); if (searchInput) searchInput.addEventListener('input', renderPreviousPostsGrid); if (filterSelect) filterSelect.addEventListener('change', renderPreviousPostsGrid); if (sortSelect) sortSelect.addEventListener('change', renderPreviousPostsGrid); if (filterKeywordInput) filterKeywordInput.addEventListener('input', renderPreviousPostsGrid); if (filterCategoryInput) filterCategoryInput.addEventListener('input', renderPreviousPostsGrid); if (filterNotesInput) filterNotesInput.addEventListener('input', renderPreviousPostsGrid); if (filterFollowUpDateInput) filterFollowUpDateInput.addEventListener('change', renderPreviousPostsGrid); if (filterMinScoreInput) filterMinScoreInput.addEventListener('input', renderPreviousPostsGrid); if (filterMaxCharsInput) filterMaxCharsInput.addEventListener('input', renderPreviousPostsGrid); if (selectAllCheck) { selectAllCheck.addEventListener('change', async (e) => { const isChecked = e.target.checked; document.querySelectorAll('.prev-post-item-checkbox').forEach(chk => { chk.checked = isChecked; const id = chk.value; if (isChecked) selectedPrevPostIds.add(id); else selectedPrevPostIds.delete(id); }); await FritreeStorage.set('local_selected_prev_posts', Array.from(selectedPrevPostIds)); updateRotationIndicatorUI(); }); } const addCurrentManualBtn = document.getElementById('btn-add-current-to-prev-library'); const bulkFreezeBtn = document.getElementById('btn-prev-bulk-freeze'); const bulkUnfreezeBtn = document.getElementById('btn-prev-bulk-unfreeze'); const bulkDraftBtn = document.getElementById('btn-prev-bulk-draft'); const bulkActiveBtn = document.getElementById('btn-prev-bulk-active'); const bulkArchiveBtn = document.getElementById('btn-prev-bulk-archive'); const bulkDeleteBtn = document.getElementById('btn-prev-bulk-delete'); if (addCurrentManualBtn) addCurrentManualBtn.addEventListener('click', handleSaveCurrentToLibraryManually); if (bulkFreezeBtn) bulkFreezeBtn.addEventListener('click', () => bulkPrevPostsAction('freeze')); if (bulkUnfreezeBtn) bulkUnfreezeBtn.addEventListener('click', () => bulkPrevPostsAction('unfreeze')); if (bulkDraftBtn) bulkDraftBtn.addEventListener('click', () => bulkPrevPostsAction('draft')); if (bulkActiveBtn) bulkActiveBtn.addEventListener('click', () => bulkPrevPostsAction('active')); if (bulkArchiveBtn) bulkArchiveBtn.addEventListener('click', () => bulkPrevPostsAction('inactive')); if (bulkDeleteBtn) bulkDeleteBtn.addEventListener('click', () => bulkPrevPostsAction('delete')); const validateAddSpintaxBtn = document.getElementById('btn-validate-add-spintax'); if (validateAddSpintaxBtn) validateAddSpintaxBtn.addEventListener('click', spintaxValidatorAddPost); const editDropzone = document.getElementById('edit-prev-post-dropzone'); const editFileInput = document.getElementById('edit-prev-post-file-input'); const closeEditModalBtn = document.getElementById('edit-prev-post-close-btn'); const saveEditedBtn = document.getElementById('btn-save-edited-prev-post'); if (editDropzone && editFileInput) { editDropzone.addEventListener('click', () => editFileInput.click()); editFileInput.addEventListener('change', (e) => handleEditPrevPostFiles(e.target.files)); editDropzone.addEventListener('dragover', (e) => { e.preventDefault(); editDropzone.style.borderColor = 'var(--primary)'; }); editDropzone.addEventListener('dragleave', () => { editDropzone.style.borderColor = 'var(--border)'; }); editDropzone.addEventListener('drop', (e) => { e.preventDefault(); editDropzone.style.borderColor = 'var(--border)'; if (e.dataTransfer.files.length > 0) handleEditPrevPostFiles(e.dataTransfer.files); }); } if (closeEditModalBtn) { closeEditModalBtn.addEventListener('click', () => { const modal = document.getElementById('edit-prev-post-modal'); if (modal) modal.style.display = 'none'; }); } if (saveEditedBtn) saveEditedBtn.addEventListener('click', saveEditedPreviousPost); const addDropzone = document.getElementById('add-prev-post-dropzone'); const addFileInput = document.getElementById('add-prev-post-file-input'); const submitNewPrevPostBtn = document.getElementById('btn-submit-new-prev-post'); if (addDropzone && addFileInput) { addDropzone.addEventListener('click', () => addFileInput.click()); addFileInput.addEventListener('change', (e) => handleAddPrevPostFiles(e.target.files)); addDropzone.addEventListener('dragover', (e) => { e.preventDefault(); addDropzone.style.borderColor = 'var(--primary)'; }); addDropzone.addEventListener('dragleave', () => { addDropzone.style.borderColor = 'var(--border)'; }); addDropzone.addEventListener('drop', (e) => { e.preventDefault(); addDropzone.style.borderColor = 'var(--border)'; if (e.dataTransfer.files.length > 0) handleAddPrevPostFiles(e.dataTransfer.files); }); } if (submitNewPrevPostBtn) submitNewPrevPostBtn.addEventListener('click', handleAddNewRotationPost); const enableRotationChk = document.getElementById('chk-enable-rotation'); const modeSelect = document.getElementById('sel-rotation-mode'); if (enableRotationChk) { enableRotationChk.addEventListener('change', async (e) => { const details = document.getElementById('rotation-config-details'); if (details) details.style.display = e.target.checked ? 'grid' : 'none'; updateRotationBadgeInModal(); // Instant automated synchronization on state change await saveAndApplyRotationSettings(); }); } if (modeSelect) { modeSelect.addEventListener('change', async () => { // Instant automated synchronization on strategy change await saveAndApplyRotationSettings(); }); } bindModalTabs(); startRotationActiveSchedulers(); updateRotationIndicatorUI(); updateRotationBadgeInModal(); } catch (e) { console.error("[Rotation Library] Failed to load rotation matrix configuration:", e); } } function bindModalTabs() { const tabs = document.querySelectorAll('.rot-tab-btn'); tabs.forEach(tab => { tab.addEventListener('click', () => { tabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); const targetPage = tab.dataset.page; document.querySelectorAll('.rot-modal-page').forEach(page => { page.style.display = page.id === targetPage ? 'block' : 'none'; }); if (targetPage === 'rot-page-manage') { renderPreviousPostsGrid(); } else if (targetPage === 'rot-page-evaluation') { renderEvaluationDashboard(); } }); }); } /** * Inline Navigation: Smoothly scrolls to the integrated dashboard panel * and applies a subtle glowing outline to guide the user visually. */ function showPreviousPostsModal() { const inlinePanel = document.getElementById('previous-posts-inline-panel'); if (inlinePanel) { inlinePanel.scrollIntoView({ behavior: 'smooth', block: 'center' }); inlinePanel.style.outline = '3px solid #0d9488'; inlinePanel.style.borderRadius = 'var(--radius-lg)'; setTimeout(() => { inlinePanel.style.transition = 'outline 0.8s ease'; inlinePanel.style.outline = '3px solid transparent'; setTimeout(() => { inlinePanel.style.outline = 'none'; inlinePanel.style.transition = 'none'; }, 800); }, 1200); } const defaultTab = document.querySelector('.rot-tab-btn[data-page="rot-page-manage"]'); if (defaultTab) defaultTab.click(); renderPreviousPostsGrid(); applyPreviousPostsSettingsToUI(); } function hidePreviousPostsModal() { console.log("[Rotation Library] Inline workspace mode active. Separation modal skipped."); } async function applyPreviousPostsSettingsToUI() { const enableRotationChk = document.getElementById('chk-enable-rotation'); const modeSelect = document.getElementById('sel-rotation-mode'); const details = document.getElementById('rotation-config-details'); if (enableRotationChk) enableRotationChk.checked = isRotationActive; const cachedMode = await FritreeStorage.get('local_rotation_mode', 'balanced'); if (modeSelect) modeSelect.value = cachedMode; if (details) details.style.display = isRotationActive ? 'grid' : 'none'; updateRotationBadgeInModal(); } function updateRotationBadgeInModal() { const enableRotationChk = document.getElementById('chk-enable-rotation'); const badge = document.getElementById('rotation-status-badge'); if (!badge || !enableRotationChk) return; if (enableRotationChk.checked) { badge.className = "badge green"; badge.innerHTML = 'Automated Content Rotation Active'; } else { badge.className = "badge red"; badge.innerHTML = 'Automated Content Rotation Disabled'; } } /** * Saves rotation parameters, applies indicators on Facebook page immediately */ async function saveAndApplyRotationSettings() { const enableRotationChk = document.getElementById('chk-enable-rotation'); const modeSelect = document.getElementById('sel-rotation-mode'); isRotationActive = enableRotationChk ? enableRotationChk.checked : false; const mode = modeSelect ? modeSelect.value : 'balanced'; await FritreeStorage.set('local_is_rotation_active', isRotationActive ? 'true' : 'false'); await FritreeStorage.set('local_rotation_mode', mode); updateRotationIndicatorUI(); if (typeof window.addLog === 'function') { window.addLog('Rotation configuration updated and synchronized successfully.', 'success'); } } async function updateRotationIndicatorUI() { const indicatorPanel = document.getElementById('prev-posts-indicator-panel'); const composerPanel = document.getElementById('default-composer-panel'); const postTextarea = document.getElementById('post-text'); if (isRotationActive) { if (indicatorPanel) indicatorPanel.style.display = 'block'; if (composerPanel) composerPanel.style.opacity = '0.4'; if (postTextarea) postTextarea.disabled = true; const activeCount = previousPostsData.filter(p => selectedPrevPostIds.has(p.id) && p.status === 'active').length; const countLabel = document.getElementById('lbl-active-prev-posts-count'); if (countLabel) countLabel.textContent = activeCount.toLocaleString('en-US'); const rotationMode = await FritreeStorage.get('local_rotation_mode', 'balanced'); const modeLabel = document.getElementById('lbl-active-prev-posts-mode'); let modeDisplay = 'Balanced Queue'; if (rotationMode === 'weighted') modeDisplay = 'Priority-Weighted Distribution'; if (rotationMode === 'high_perf') modeDisplay = 'Performance Focus'; if (rotationMode === 'newest') modeDisplay = 'Chronological Order (Newest)'; if (rotationMode === 'oldest') modeDisplay = 'Chronological Order (Oldest)'; if (rotationMode === 'random') modeDisplay = 'Random Dispatch Mode'; if (modeLabel) modeLabel.textContent = modeDisplay; } else { if (indicatorPanel) indicatorPanel.style.display = 'none'; if (composerPanel) composerPanel.style.opacity = '1'; if (postTextarea) postTextarea.disabled = false; } } async function disableRotationModeGlobally() { isRotationActive = false; await FritreeStorage.set('local_is_rotation_active', 'false'); updateRotationIndicatorUI(); applyPreviousPostsSettingsToUI(); if (typeof window.addLog === 'function') { window.addLog('Automated content rotation disabled. Standard composer controls restored.', 'warn'); } } // ============================================================================ // Routing Strategy Engine Algorithms (Selection Matrix) // ============================================================================ function selectNextRotationPost(mode, availableIds) { if (!availableIds || availableIds.length === 0) return null; const candidates = previousPostsData.filter(p => availableIds.includes(p.id) && p.status === 'active'); if (candidates.length === 0) return null; switch (mode) { case 'newest': return [...candidates].sort((a, b) => new Date(b.entryDate || b.creationDate) - new Date(a.entryDate || a.creationDate))[0]; case 'oldest': return [...candidates].sort((a, b) => new Date(a.entryDate || a.creationDate) - new Date(b.entryDate || b.creationDate))[0]; case 'high_perf': return [...candidates].sort((a, b) => (b.score || 0) - (a.score || 0))[0]; case 'weighted': let totalPriority = candidates.reduce((acc, curr) => acc + (parseInt(curr.priority) || 50), 0); let thresh = Math.random() * totalPriority; let cumulative = 0; for (let c of candidates) { cumulative += (parseInt(c.priority) || 50); if (thresh <= cumulative) return c; } return candidates[0]; case 'balanced': return [...candidates].sort((a, b) => { const usageA = a.stats ? (a.stats.usage || 0) : (a.usageCount || 0); const usageB = b.stats ? (b.stats.usage || 0) : (b.usageCount || 0); return usageA - usageB; })[0]; case 'random': default: return candidates[Math.floor(Math.random() * candidates.length)]; } } // ============================================================================ // Performance Metrics Calculations & Evaluation // ============================================================================ function recalculateAllPostScores() { if (previousPostsData.length === 0) return; const metrics = previousPostsData.map(p => { const usage = p.stats ? (p.stats.usage || 0) : (p.usageCount || 0); const successCount = p.stats ? ((p.stats.success || 0) + (p.stats.pending || 0)) : (p.usageCount || 0); const ratio = usage > 0 ? (successCount / usage) : 0; return { id: p.id, ratio, usage }; }); const maxUsage = Math.max(...metrics.map(m => m.usage), 1); previousPostsData.forEach(p => { const metric = metrics.find(m => m.id === p.id); if (!p.stats) { p.stats = { usage: p.usageCount || 0, success: p.usageCount || 0, pending: 0, failed: 0 }; } const successWeightedPoints = (metric ? metric.ratio : 0) * 70; const consistencyWeightedPoints = (metric ? (metric.usage / maxUsage) : 0) * 30; p.score = Math.min(100, Math.max(0, Math.round(successWeightedPoints + consistencyWeightedPoints))); }); } function renderEvaluationDashboard() { const container = document.getElementById('rot-evaluation-list'); const paginationContainer = document.getElementById('rot-evaluation-pagination'); if (!container) return; container.innerHTML = ''; recalculateAllPostScores(); const sorted = [...previousPostsData].sort((a, b) => (b.score || 0) - (a.score || 0)); if (previousPostsData.length === 0) { container.innerHTML = 'Database archive is currently empty. Add rotation templates to start accumulating performance analytics.'; if (paginationContainer) paginationContainer.innerHTML = ''; updateEvaluationTotalsRow(sorted); return; } const startIndex = (currentMetricsPage - 1) * METRICS_ITEMS_PER_PAGE; const endIndex = startIndex + METRICS_ITEMS_PER_PAGE; const paginatedItems = sorted.slice(startIndex, endIndex); paginatedItems.forEach(p => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; tr.title = "Click to inspect active trace logs and proof links associated with this template."; tr.addEventListener('click', () => { openVariantTraceModal(p.id); }); const textIcon = getIconForTemplateText(p.text, p.categories); const tdTitle = document.createElement('td'); tdTitle.style.fontWeight = 'bold'; tdTitle.innerHTML = `${textIcon}${p.title || p.text.substring(0, 20) + '...'}`; const tdUsage = document.createElement('td'); tdUsage.innerHTML = `${p.stats ? (p.stats.usage || 0).toLocaleString('en-US') : (p.usageCount || 0).toLocaleString('en-US')}`; const tdSuccess = document.createElement('td'); tdSuccess.style.color = 'var(--success)'; tdSuccess.innerHTML = `${p.stats ? (p.stats.success || 0).toLocaleString('en-US') : (p.usageCount || 0).toLocaleString('en-US')}`; const tdPending = document.createElement('td'); tdPending.style.color = 'var(--warning)'; tdPending.innerHTML = `${p.stats ? (p.stats.pending || 0).toLocaleString('en-US') : 0}`; const tdFailed = document.createElement('td'); tdFailed.style.color = 'var(--danger)'; tdFailed.innerHTML = `${p.stats ? (p.stats.failed || 0).toLocaleString('en-US') : 0}`; tr.appendChild(tdTitle); tr.appendChild(tdUsage); tr.appendChild(tdSuccess); tr.appendChild(tdPending); tr.appendChild(tdFailed); container.appendChild(tr); }); updateEvaluationTotalsRow(sorted); renderEvaluationPagination(sorted.length, paginationContainer); } function updateEvaluationTotalsRow(items) { let publishedSum = 0; let successfulSum = 0; let reviewedSum = 0; let failedSum = 0; items.forEach(p => { if (p.stats) { publishedSum += (p.stats.usage || 0); successfulSum += (p.stats.success || 0); reviewedSum += (p.stats.pending || 0); failedSum += (p.stats.failed || 0); } }); const totalPubEl = document.getElementById('rot-total-published'); const totalSuccEl = document.getElementById('rot-total-successful'); const totalRevEl = document.getElementById('rot-total-reviewed'); const totalFailEl = document.getElementById('rot-total-failed'); if (totalPubEl) totalPubEl.innerHTML = `${publishedSum.toLocaleString('en-US')}`; if (totalSuccEl) totalSuccEl.innerHTML = `${successfulSum.toLocaleString('en-US')}`; if (totalRevEl) totalRevEl.innerHTML = `${reviewedSum.toLocaleString('en-US')}`; if (totalFailEl) totalFailEl.innerHTML = `${failedSum.toLocaleString('en-US')}`; } function renderEvaluationPagination(totalItems, container) { if (!container) return; container.innerHTML = ''; const totalPages = Math.ceil(totalItems / METRICS_ITEMS_PER_PAGE); if (totalPages <= 1) return; const prevBtn = document.createElement('button'); prevBtn.className = 'btn-secondary'; prevBtn.style.padding = '4px 10px'; prevBtn.innerHTML = ''; prevBtn.disabled = currentMetricsPage === 1; prevBtn.addEventListener('click', () => { if (currentMetricsPage > 1) { currentMetricsPage--; renderEvaluationDashboard(); } }); container.appendChild(prevBtn); const pageIndicator = document.createElement('span'); pageIndicator.style.cssText = 'font-weight:bold; font-size:12px; align-self:center; margin: 0 10px; color:#64748b;'; pageIndicator.textContent = `Page ${currentMetricsPage.toLocaleString('en-US')} of ${totalPages.toLocaleString('en-US')}`; container.appendChild(pageIndicator); const nextBtn = document.createElement('button'); nextBtn.className = 'btn-secondary'; nextBtn.style.padding = '4px 10px'; nextBtn.innerHTML = ''; nextBtn.disabled = currentMetricsPage === totalPages; nextBtn.addEventListener('click', () => { if (currentMetricsPage < totalPages) { currentMetricsPage++; renderEvaluationDashboard(); } }); container.appendChild(nextBtn); } // ============================================================================ // Add New Post & Spintax Validation (Dynamic Builders) // ============================================================================ function spintaxValidatorAddPost() { const textVal = document.getElementById('add-prev-post-text').value; const outputEl = document.getElementById('add-spintax-analysis-output'); if (!outputEl) return; if (!textVal) { outputEl.innerHTML = ` Error: Message text copy is empty! Please write spintax content first.`; outputEl.style.color = "var(--danger)"; return; } if (typeof FritreeRules !== 'undefined' && typeof FritreeRules.analyzeSpintax === 'function') { const analysis = FritreeRules.analyzeSpintax(textVal); if (!analysis.valid && analysis.errors) { outputEl.innerHTML = ` ${analysis.errors}`; outputEl.style.color = "var(--danger)"; } else { outputEl.innerHTML = `Spintax brackets verified successfully. Expected variations: ${analysis.variations.toLocaleString('en-US')} unique copy paths.`; outputEl.style.color = "var(--success)"; } } else { outputEl.innerHTML = ` Spintax analysis engine is temporarily offline.`; } } function handleAddPrevPostFiles(files) { for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.size > 25 * 1024 * 1024) { alert(`File ${file.name} is too large! Maximum allowed upload size is 25MB.`); continue; } if (!file.type.startsWith('image/')) continue; const reader = new FileReader(); reader.onload = async () => { const compressedBlob = typeof FritreeLibrary !== 'undefined' ? await FritreeLibrary.compressImageBlob(file) : file; const thumbBlob = typeof FritreeLibrary !== 'undefined' ? await FritreeLibrary.generateImageThumbnail(compressedBlob) : file; const binaryHash = 'rot_media_add_' + Date.now() + "_" + Math.random().toString(36).substr(2,4); await FritreeStorage.set(`media_blob_${binaryHash}`, compressedBlob); await FritreeStorage.set(`media_thumb_${binaryHash}`, thumbBlob); addPrevPostMedia.push({ id: binaryHash, type: "image/jpeg", blob: compressedBlob, thumbBlob: thumbBlob, filename: file.name }); renderAddPrevPostPreviews(); }; reader.readAsDataURL(file); } const addFileInput = document.getElementById('add-prev-post-file-input'); if (addFileInput) addFileInput.value = ''; } function renderAddPrevPostPreviews() { const container = document.getElementById('add-prev-post-previews'); if (!container) return; container.innerHTML = ''; for (let i = 0; i < addPrevPostMedia.length; i++) { const media = addPrevPostMedia[i]; const box = document.createElement('div'); box.className = 'preview-box'; const el = document.createElement('img'); const previewUrl = URL.createObjectURL(media.thumbBlob || media.blob); el.src = previewUrl; box.dataset.objurl = previewUrl; el.style.cssText = 'width: 100%; height: 100%; object-fit: cover;'; const closeBtn = document.createElement('button'); closeBtn.setAttribute('type', 'button'); closeBtn.innerHTML = ''; closeBtn.addEventListener('click', () => { if (box.dataset.objurl) URL.revokeObjectURL(box.dataset.objurl); FritreeStorage.remove(`media_blob_${media.id}`); FritreeStorage.remove(`media_thumb_${media.id}`); addPrevPostMedia.splice(i, 1); renderAddPrevPostPreviews(); }); box.appendChild(el); box.appendChild(closeBtn); container.appendChild(box); } } async function handleAddNewRotationPost() { const titleVal = document.getElementById('add-prev-post-title').value.trim(); const textVal = document.getElementById('add-prev-post-text').value; const statusVal = document.getElementById('add-prev-post-status').value; const keywordsVal = document.getElementById('add-prev-post-keywords').value; const phonesVal = document.getElementById('add-prev-post-phones').value; const noteVal = document.getElementById('add-prev-post-notes').value.trim(); const categoryVal = document.getElementById('add-prev-post-categories').value; const altTextVal = document.getElementById('add-prev-post-alt-text').value; const priorityVal = parseInt(document.getElementById('add-prev-post-priority').value) || 50; const entryDateVal = document.getElementById('add-prev-post-entry-date').value; const expireDateVal = document.getElementById('add-prev-post-expire-date').value; const clientNameVal = document.getElementById('add-prev-post-client-name').value.trim(); const enableWaFollowUp = document.getElementById('add-prev-post-wa-followup').checked; const followUpMessage = document.getElementById('add-prev-post-followup-msg').value; const followUpDate = document.getElementById('add-prev-post-followup-date').value; if (!textVal && addPrevPostMedia.length === 0) { alert('Save aborted: Cannot record a blank template block. Provide a text copy or attach media files.'); return; } const keywordsArray = keywordsVal.split(',').map(s => s.trim().toLowerCase()).filter(s => s); const phonesArray = phonesVal.split(',').map(s => s.trim().replace(/[^0-9+]/g, '')).filter(s => s); const categoryArray = categoryVal.split(',').map(s => s.trim()).filter(s => s); const newPost = { id: 'rot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4), title: titleVal || (textVal ? textVal.substring(0, 30) + '...' : 'Manual Archived Dispatch'), text: textVal, altText: altTextVal, priority: priorityVal, mediaReferences: addPrevPostMedia.map(m => ({ id: m.id, type: m.type, filename: m.filename })), status: statusVal, keywords: keywordsArray, categories: categoryArray, notes: noteVal, enableWaFollowUp: enableWaFollowUp, clientName: clientNameVal, phoneNumbers: phonesArray, followUpDate: followUpDate, followUpMessage: followUpMessage, entryDate: entryDateVal, expireDate: expireDateVal, followUpSent: false, date: new Date().toISOString() }; previousPostsData.unshift(newPost); await savePrevPostsToLocal(); document.getElementById('add-prev-post-title').value = ''; document.getElementById('add-prev-post-text').value = ''; document.getElementById('add-prev-post-keywords').value = ''; document.getElementById('add-prev-post-phones').value = ''; document.getElementById('add-prev-post-notes').value = ''; document.getElementById('add-prev-post-categories').value = ''; document.getElementById('add-prev-post-entry-date').value = ''; document.getElementById('add-prev-post-expire-date').value = ''; document.getElementById('add-prev-post-client-name').value = ''; document.getElementById('add-prev-post-wa-followup').checked = false; document.getElementById('add-prev-post-followup-msg').value = ''; document.getElementById('add-prev-post-followup-date').value = ''; document.getElementById('add-prev-post-alt-text').value = ''; document.getElementById('add-prev-post-priority').value = '50'; addPrevPostMedia = []; const previewsContainer = document.getElementById('add-prev-post-previews'); if (previewsContainer) previewsContainer.innerHTML = ''; triggerOnScreenCelebration(); const manageTab = document.querySelector('.rot-tab-btn[data-page="rot-page-manage"]'); if (manageTab) manageTab.click(); if (typeof window.addLog === 'function') { window.addLog(`Successfully saved new rotation template block: "${newPost.title}".`, 'success'); } } // ============================================================================ // Rotation Grid Layout Renderer (RTL to LTR Alignment) // ============================================================================ function renderPreviousPostsGrid() { const container = document.getElementById('prev-posts-grid-container'); if (!container) return; container.innerHTML = ''; const queryInput = document.getElementById('prev-posts-search'); const statusSelect = document.getElementById('prev-posts-filter-status'); const sortSelect = document.getElementById('prev-posts-sort-by'); const keywordInput = document.getElementById('prev-posts-filter-keyword'); const categoryInput = document.getElementById('prev-posts-filter-category'); const notesInput = document.getElementById('prev-posts-filter-notes'); const followupDateInput = document.getElementById('prev-posts-filter-followup-date'); const minScoreInput = document.getElementById('prev-posts-filter-min-score'); const maxCharsInput = document.getElementById('prev-posts-filter-max-chars'); const query = queryInput ? queryInput.value.toLowerCase().trim() : ''; const statusFilter = statusSelect ? statusSelect.value : 'all'; const sortBy = sortSelect ? sortSelect.value : 'newest'; const keywordFilter = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; const categoryFilter = categoryInput ? categoryInput.value.toLowerCase().trim() : ''; const notesFilter = notesInput ? notesInput.value.toLowerCase().trim() : ''; const followupDateFilter = followupDateInput ? followupDateInput.value : ''; const minScoreFilter = minScoreInput ? parseInt(minScoreInput.value) || 0 : 0; const maxCharsFilter = maxCharsInput ? parseInt(maxCharsInput.value) || 0 : 0; const filtered = previousPostsData.filter(p => { const textContent = p.text || ''; const matchSearch = textContent.toLowerCase().includes(query) || (p.title && p.title.toLowerCase().includes(query)); let matchStatus = true; if (statusFilter !== 'all') { matchStatus = p.status === statusFilter; } let matchKeyword = true; if (keywordFilter) { matchKeyword = p.keywords?.some(k => k.toLowerCase().includes(keywordFilter)); } let matchCategory = true; if (categoryFilter) { matchCategory = p.categories?.some(c => c.toLowerCase().includes(categoryFilter)); } let matchNotes = true; if (notesFilter) { matchNotes = p.teamNotes?.toLowerCase().includes(notesFilter); } let matchFollowUpDate = true; if (followupDateFilter) { matchFollowUpDate = p.followUpDate === followupDateFilter; } let matchMinScore = true; if (minScoreFilter) { matchMinScore = (p.score || 0) >= minScoreFilter; } let matchMaxChars = true; if (maxCharsFilter) { matchMaxChars = textContent.length <= maxCharsFilter; } return matchSearch && matchStatus && matchKeyword && matchCategory && matchNotes && matchFollowUpDate && matchMinScore && matchMaxChars; }); if (sortBy === 'newest') filtered.sort((a,b) => new Date(b.creationDate || b.entryDate) - new Date(a.creationDate || a.entryDate)); else if (sortBy === 'oldest') filtered.sort((a,b) => new Date(a.creationDate || a.entryDate) - new Date(b.creationDate || b.entryDate)); else if (sortBy === 'most_pub') filtered.sort((a,b) => (b.stats?.usage || 0) - (a.stats?.usage || 0)); else if (sortBy === 'least_pub') filtered.sort((a,b) => (a.stats?.usage || 0) - (b.stats?.usage || 0)); else if (sortBy === 'highest_score') filtered.sort((a,b) => (b.score || 0) - (a.score || 0)); else if (sortBy === 'lowest_score') filtered.sort((a,b) => (a.score || 0) - (b.score || 0)); if (filtered.length === 0) { const emptyEl = document.createElement('div'); emptyEl.style.cssText = 'text-align: center; color: #64748b; padding: 20px; font-size: 12px;'; emptyEl.innerHTML = ' Rotation matrix is currently empty or contains no template blocks matching active filters.'; container.appendChild(emptyEl); return; } filtered.forEach(p => { const item = document.createElement('div'); item.style.cssText = 'display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px; background: white; border: 1px solid #e2e8f0; border-radius: 15px; transition: none; direction: ltr; text-align: left;'; const chk = document.createElement('input'); chk.type = 'checkbox'; chk.className = 'prev-post-item-checkbox'; chk.value = p.id; chk.checked = selectedPrevPostIds.has(p.id); chk.style.cssText = 'width: 18px; height: 18px; cursor: pointer; accent-color: var(--primary); flex-shrink: 0;'; if (chk.checked) item.style.borderColor = '#93c5fd'; chk.addEventListener('change', async () => { if (chk.checked) { selectedPrevPostIds.add(p.id); item.style.borderColor = '#93c5fd'; } else { selectedPrevPostIds.delete(p.id); item.style.borderColor = '#e2e8f0'; } await FritreeStorage.set('local_selected_prev_posts', Array.from(selectedPrevPostIds)); updateRotationIndicatorUI(); }); const contentDiv = document.createElement('div'); contentDiv.style.cssText = 'flex: 1; display: flex; flex-direction: column; gap: 4px; overflow: hidden; padding-left: 10px; text-align: left;'; const textIcon = getIconForTemplateText(p.text, p.categories); const titleSpan = document.createElement('span'); titleSpan.style.cssText = 'font-weight: bold; font-size: 13px; color: #1e293b; text-align: left; display: flex; align-items: center; gap: 6px; flex-wrap: wrap;'; titleSpan.innerHTML = `${textIcon} ${p.title || 'Manual Archived Dispatch'}`; const statBadge = document.createElement('span'); let statColor = 'background: #f1f5f9; color: #475569;'; let statText = 'Draft'; if (p.status === 'active') { statColor = 'background: #dcfce7; color: #166534;'; statText = 'Active'; } else if (p.status === 'frozen') { statColor = 'background: #e0f2fe; color: #0369a1;'; statText = 'Frozen'; } else if (p.status === 'draft') { statColor = 'background: #f1f5f9; color: #475569;'; statText = 'Draft'; } else if (p.status === 'inactive') { statColor = 'background: #fee2e2; color: #991b1b;'; statText = 'Deactivated'; } statBadge.style.cssText = `${statColor} padding: 2px 6px; border-radius: 4px; font-size: 9px; font-weight: bold; text-transform: uppercase; margin-left: 5px; display: inline-flex; align-items: center; gap: 3px;`; statBadge.innerHTML = statText; titleSpan.appendChild(statBadge); if (p.enableWaFollowUp && p.followUpDate) { const fuBadge = document.createElement('span'); fuBadge.style.cssText = 'background: #fdf2f8; color: #9d174d; padding: 2px 6px; border-radius: 4px; font-size: 9px; font-weight: bold; margin-left: 5px;'; fuBadge.innerHTML = `Follow-Up: ${p.followUpDate}`; titleSpan.appendChild(fuBadge); } const previewText = document.createElement('span'); previewText.style.cssText = 'font-size: 12px; color: #64748b; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; max-width: 350px; text-align: left; display: block;'; previewText.textContent = p.text || '[Media Attachment Assets Only]'; const statsRow = document.createElement('div'); statsRow.style.cssText = 'display: flex; gap: 12px; font-size: 11px; color: #94a3b8; margin-top: 2px; flex-wrap: wrap; justify-content: flex-start;'; const usageSpan = document.createElement('span'); const useCount = p.stats ? (p.stats.usage || 0) : (p.usageCount || 0); usageSpan.innerHTML = `Dispatched: ${useCount.toLocaleString('en-US')} times`; const dateSpan = document.createElement('span'); const dateVal = p.entryDate || p.creationDate; dateSpan.innerHTML = `Created: ${new Date(dateVal).toLocaleDateString('en-US')}`; statsRow.appendChild(usageSpan); statsRow.appendChild(dateSpan); if (p.categories && p.categories.length > 0) { const catSpan = document.createElement('span'); catSpan.innerHTML = `${p.categories.join(', ')}`; statsRow.appendChild(catSpan); } contentDiv.appendChild(titleSpan); contentDiv.appendChild(previewText); contentDiv.appendChild(statsRow); const mediaContainer = document.createElement('div'); mediaContainer.style.cssText = 'display: flex; gap: 4px; align-items: center; justify-content: center; min-width: 60px;'; if (p.mediaReferences && p.mediaReferences.length > 0) { p.mediaReferences.slice(0, 2).forEach(async m => { const thumbBlob = await FritreeStorage.get(`media_thumb_${m.id}`); if (thumbBlob) { const img = document.createElement('img'); img.src = URL.createObjectURL(thumbBlob); img.style.cssText = 'width: 32px; height: 32px; object-fit: cover; border-radius: 6px; border: 1px solid #cbd5e1;'; mediaContainer.appendChild(img); } }); } else { const noMedia = document.createElement('span'); noMedia.style.cssText = 'font-size: 20px; color: #e2e8f0;'; noMedia.innerHTML = ''; mediaContainer.appendChild(noMedia); } const tdActs = document.createElement('div'); tdActs.style.cssText = 'display: flex; gap: 4px; align-items: center;'; const editBtn = document.createElement('button'); editBtn.className = 'action-btn view'; editBtn.title = "Modify Template Parameters"; editBtn.innerHTML = ''; editBtn.addEventListener('click', (e) => { e.stopPropagation(); openEditPrevPostModal(p.id); }); const delBtn = document.createElement('button'); delBtn.className = 'action-btn delete'; delBtn.title = "Wipe Template Permanently"; delBtn.innerHTML = ''; delBtn.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('Are you sure you want to permanently delete this rotation template block?')) { if (p.mediaReferences) { p.mediaReferences.forEach(m => { FritreeStorage.remove(`media_blob_${m.id}`); FritreeStorage.remove(`media_thumb_${m.id}`); }); } previousPostsData = previousPostsData.filter(x => x.id !== p.id); selectedPrevPostIds.delete(p.id); savePrevPostsToLocal(); renderPreviousPostsGrid(); updateRotationIndicatorUI(); } }); tdActs.appendChild(editBtn); tdActs.appendChild(delBtn); item.appendChild(chk); item.appendChild(contentDiv); item.appendChild(mediaContainer); item.appendChild(tdActs); container.appendChild(item); }); } async function bulkPrevPostsAction(action) { const container = document.getElementById('prev-posts-grid-container'); if (!container) return; const checkedBoxes = container.querySelectorAll('.prev-post-item-checkbox:checked'); if (checkedBoxes.length === 0) { alert('Please select at least one template block first to perform bulk actions.'); return; } if (action === 'delete') { if (!confirm(`Are you sure you want to permanently delete ${checkedBoxes.length} selected template block(s)?`)) return; checkedBoxes.forEach(chk => { const post = previousPostsData.find(x => x.id === chk.value); if (post && post.mediaReferences) { post.mediaReferences.forEach(m => { FritreeStorage.remove(`media_blob_${chk.value}_${m.id}`); FritreeStorage.remove(`media_thumb_${chk.value}_${m.id}`); }); } previousPostsData = previousPostsData.filter(x => x.id !== chk.value); selectedPrevPostIds.delete(chk.value); }); } else { checkedBoxes.forEach(chk => { const post = previousPostsData.find(x => x.id === chk.value); if (post) { if (action === 'draft') post.status = 'draft'; else if (action === 'active') post.status = 'active'; else if (action === 'inactive') post.status = 'inactive'; else if (action === 'freeze') post.status = 'frozen'; else if (action === 'unfreeze') post.status = 'active'; } }); } await savePrevPostsToLocal(); await FritreeStorage.set('local_selected_prev_posts', Array.from(selectedPrevPostIds)); renderPreviousPostsGrid(); updateRotationIndicatorUI(); const selectAllCheck = document.getElementById('prev-posts-select-all'); if (selectAllCheck) selectAllCheck.checked = false; if (typeof window.addLog === 'function') { window.addLog(`Bulk action [${action}] executed successfully.`, 'warn'); } } async function incrementPrevPostUsage(id, status = 'success') { const post = previousPostsData.find(x => x.id === id); if (post) { if (!post.stats) { post.stats = { usage: 0, success: 0, fail: 0, pending: 0, realInteractions: 0 }; } post.stats.usage = (post.stats.usage || 0) + 1; post.usageCount = (post.usageCount || 0) + 1; if (status === 'success') post.stats.success++; else if (status === 'pending') post.stats.pending++; else if (status === 'failed') post.stats.failed++; recalculateAllPostScores(); await savePrevPostsToLocal(); const inlinePanel = document.getElementById('previous-posts-inline-panel'); if (inlinePanel) { renderPreviousPostsGrid(); renderEvaluationDashboard(); } } } // ============================================================================ // Edit existing rotation Template block (Modal Forms editor) // ============================================================================ async function openEditPrevPostModal(id) { const post = previousPostsData.find(p => p.id === id); if (!post) return; document.getElementById('edit-prev-post-id').value = post.id; document.getElementById('edit-prev-post-title').value = post.title || ''; document.getElementById('edit-prev-post-text').value = post.text || ''; document.getElementById('edit-prev-post-status').value = post.status || 'active'; document.getElementById('edit-prev-post-keywords').value = post.keywords ? post.keywords.join(', ') : ''; document.getElementById('edit-prev-post-phones').value = post.phoneNumbers ? post.phoneNumbers.join(', ') : ''; document.getElementById('edit-prev-post-notes').value = post.teamNotes || ''; document.getElementById('edit-prev-post-categories').value = post.categories ? post.categories.join(', ') : ''; document.getElementById('edit-prev-post-entry-date').value = post.entryDate ? post.entryDate.slice(0, 16) : ''; document.getElementById('edit-prev-post-expire-date').value = post.expireDate ? post.expireDate.slice(0, 16) : ''; document.getElementById('edit-prev-post-client-name').value = post.clientName || ''; document.getElementById('edit-prev-post-wa-followup').checked = !!post.enableWaFollowUp; document.getElementById('edit-prev-post-followup-msg').value = post.followUpMessage || ''; document.getElementById('edit-prev-post-followup-date').value = post.followUpDate || ''; editPrevPostMedia = []; if (post.mediaReferences) { for (let i = 0; i < post.mediaReferences.length; i++) { const ref = post.mediaReferences[i]; const rawBlob = await FritreeStorage.get(`media_blob_${ref.id}`); const thumbBlob = await FritreeStorage.get(`media_thumb_${ref.id}`); editPrevPostMedia.push({ id: ref.id, type: ref.type, blob: rawBlob, thumbBlob: thumbBlob, filename: ref.filename }); } } renderEditPrevPostPreviews(); const modal = document.getElementById('edit-prev-post-modal'); if (modal) modal.style.display = 'flex'; } function handleEditPrevPostFiles(files) { for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.size > 25 * 1024 * 1024) { alert(`File ${file.name} is too large! Maximum allowed upload size is 25MB.`); continue; } if (!file.type.startsWith('image/')) continue; const reader = new FileReader(); reader.onload = async () => { const compressedBlob = typeof FritreeLibrary !== 'undefined' ? await FritreeLibrary.compressImageBlob(file) : file; const thumbBlob = typeof FritreeLibrary !== 'undefined' ? await FritreeLibrary.generateImageThumbnail(compressedBlob) : file; const binaryHash = 'rot_media_edit_' + Date.now() + "_" + Math.random().toString(36).substr(2,4); await FritreeStorage.set(`media_blob_${binaryHash}`, compressedBlob); await FritreeStorage.set(`media_thumb_${binaryHash}`, thumbBlob); editPrevPostMedia.push({ id: binaryHash, type: "image/jpeg", blob: compressedBlob, thumbBlob: thumbBlob, filename: file.name }); renderEditPrevPostPreviews(); }; reader.readAsDataURL(file); } const editFileInput = document.getElementById('edit-prev-post-file-input'); if (editFileInput) editFileInput.value = ''; } async function renderEditPrevPostPreviews() { const container = document.getElementById('edit-prev-post-previews'); if (!container) return; container.innerHTML = ''; for (let i = 0; i < editPrevPostMedia.length; i++) { const media = editPrevPostMedia[i]; const box = document.createElement('div'); box.className = 'preview-box'; const el = document.createElement('img'); if (media.thumbBlob || media.blob) { const previewUrl = URL.createObjectURL(media.thumbBlob || media.blob); el.src = previewUrl; box.dataset.objurl = previewUrl; } else { el.src = 'icon.png'; } el.style.width = '100%'; el.style.height = '100%'; el.style.objectFit = 'cover'; const closeBtn = document.createElement('button'); closeBtn.setAttribute('type', 'button'); closeBtn.innerHTML = ''; closeBtn.addEventListener('click', () => { if (box.dataset.objurl) URL.revokeObjectURL(box.dataset.objurl); FritreeStorage.remove(`media_blob_${media.id}`); FritreeStorage.remove(`media_thumb_${media.id}`); editPrevPostMedia.splice(i, 1); renderEditPrevPostPreviews(); }); box.appendChild(el); box.appendChild(closeBtn); container.appendChild(box); } } async function saveEditedPreviousPost() { const id = document.getElementById('edit-prev-post-id').value; const post = previousPostsData.find(p => p.id === id); if (!post) return; const titleVal = document.getElementById('edit-prev-post-title').value.trim(); const textVal = document.getElementById('edit-prev-post-text').value; const statusVal = document.getElementById('edit-prev-post-status').value; const keywordsVal = document.getElementById('edit-prev-post-keywords').value; const phonesVal = document.getElementById('edit-prev-post-phones').value; const noteVal = document.getElementById('edit-prev-post-notes').value.trim(); const categoryVal = document.getElementById('edit-prev-post-categories').value; const entryDateVal = document.getElementById('edit-prev-post-entry-date').value; const expireDateVal = document.getElementById('edit-prev-post-expire-date').value; const clientNameVal = document.getElementById('edit-prev-post-client-name').value.trim(); const enableWaFollowUp = document.getElementById('edit-prev-post-wa-followup').checked; const followUpMessage = document.getElementById('edit-prev-post-followup-msg').value; const followUpDate = document.getElementById('edit-prev-post-followup-date').value; if (!textVal && editPrevPostMedia.length === 0) { alert('Save aborted: Cannot save a blank template. Provide a text copy or attach media files.'); return; } const keywordsArray = keywordsVal.split(',').map(s => s.trim().toLowerCase()).filter(s => s); const phonesArray = phonesVal.split(',').map(s => s.trim().replace(/[^0-9+]/g, '')).filter(s => s); const categoryArray = categoryVal.split(',').map(s => s.trim()).filter(s => s); post.title = titleVal || (textVal ? textVal.substring(0, 30) + '...' : 'Manual Archived Dispatch'); post.text = textVal; post.mediaReferences = editPrevPostMedia.map(m => ({ id: m.id, type: m.type, filename: m.filename })); post.status = statusVal; post.keywords = keywordsArray; post.categories = categoryArray; post.notes = noteVal; post.enableWaFollowUp = enableWaFollowUp; post.clientName = clientNameVal; post.phoneNumbers = phonesArray; post.followUpDate = followUpDate; post.followUpMessage = followUpMessage; post.entryDate = entryDateVal; post.expireDate = expireDateVal; post.followUpSent = false; await savePrevPostsToLocal(); renderPreviousPostsGrid(); document.querySelectorAll('#edit-prev-post-previews .preview-box').forEach(box => { if (box.dataset.objurl) URL.revokeObjectURL(box.dataset.objurl); }); const modal = document.getElementById('edit-prev-post-modal'); if (modal) modal.style.display = 'none'; if (typeof window.addLog === 'function') { window.addLog(`Successfully updated rotation template block parameters: "${post.title}".`, 'success'); } } // ============================================================================ // Handles manual/automatic save composer states to library database // ============================================================================ async function handleSaveCurrentToLibraryManually() { const text = document.getElementById('post-text').value; const images = window.FritreeFacebook ? await window.FritreeFacebook.getUploadedImages() : []; if (!text && images.length === 0) { alert('Standard composer is empty. Write text or attach media to save as template.'); return; } const title = prompt('Enter a descriptive administrative title to save current composer state as template block:'); if (!title) return; await autoSaveCurrentPostToLibrary(text, images, title); renderPreviousPostsGrid(); } async function autoSaveCurrentPostToLibrary(text, images, title = '') { if (!text && images.length === 0) return; const summaryTitle = title || (text ? text.substring(0, 25) + '...' : 'Media Only Template'); const hasDuplicate = previousPostsData.some(p => p.text === text && p.text.length > 0); if (hasDuplicate && !title) return; const newPost = { id: 'rot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4), title: sanitizeRotationInput(summaryTitle), text: text, mediaReferences: [], status: 'active', keywords: [], phoneNumbers: [], enableWaFollowUp: false, followUpMessage: '', followUpDate: '', followUpSent: false, entryDate: new Date().toISOString(), expireDate: '', clientName: '', teamNotes: '', categories: [], stats: { usage: 0, success: 0, pending: 0, failed: 0 }, score: 50, creationDate: new Date().toISOString() }; if (images && images.length > 0) { for (let i = 0; i < images.length; i++) { const imgObj = images[i]; const binaryHash = 'rot_media_' + Date.now() + "_" + Math.random().toString(36).substr(2,4); let rawBlob = null; if (imgObj.id) { rawBlob = await FritreeStorage.get(`media_blob_${imgObj.id}`); } else if (imgObj.data) { rawBlob = typeof FritreeStorage.b64ToBlob === 'function' ? FritreeStorage.b64ToBlob(imgObj.data, imgObj.type) : imgObj.data; } if (rawBlob) { const thumbBlob = typeof FritreeLibrary !== 'undefined' ? await FritreeLibrary.generateImageThumbnail(rawBlob) : rawBlob; await FritreeStorage.set(`media_blob_${binaryHash}`, rawBlob); await FritreeStorage.set(`media_thumb_${binaryHash}`, thumbBlob); newPost.mediaReferences.push({ id: binaryHash, type: imgObj.type || "image/jpeg", filename: imgObj.name || "image.jpg" }); } } } previousPostsData.unshift(newPost); recalculateAllPostScores(); await savePrevPostsToLocal(); if (typeof window.addLog === 'function') { window.addLog(`Composer state automatically archived in Rotation Library under descriptive title: "${newPost.title}".`, 'success'); } } // ============================================================================ // Background Alarms Schedulers & Expiration Checks // ============================================================================ function startRotationActiveSchedulers() { setInterval(async () => { await checkAndTriggerFollowUps(); await checkAndProcessExpiredRotationPosts(); }, 30000); } async function checkAndProcessExpiredRotationPosts() { let changed = false; const now = Date.now(); previousPostsData.forEach(p => { if (p.status === 'active' && p.expireDate) { const expTime = new Date(p.expireDate).getTime(); if (now >= expTime) { p.status = 'frozen'; changed = true; if (typeof window.addLog === 'function') { window.addLog(`Auto-Freeze Alert: Rotation template block "${p.title}" exceeded its expiration limit and has been deactivated.`, 'warn'); } } } }); if (changed) { await savePrevPostsToLocal(); const inlinePanel = document.getElementById('previous-posts-inline-panel'); if (inlinePanel) { renderPreviousPostsGrid(); renderEvaluationDashboard(); } } } async function checkAndTriggerFollowUps() { let changed = false; const now = Date.now(); for (let i = 0; i < previousPostsData.length; i++) { const p = previousPostsData[i]; if (p.status === 'active' && p.enableWaFollowUp && p.followUpDate && !p.followUpSent) { const fDate = new Date(p.followUpDate); if (fDate.getTime() <= now) { if (p.phoneNumbers && p.phoneNumbers.length > 0 && p.followUpMessage) { let customizedMsg = p.followUpMessage; if (p.clientName) { customizedMsg = customizedMsg.replace(/{client_name}/g, p.clientName); } else { customizedMsg = customizedMsg.replace(/{client_name}/g, 'Valued Customer'); } const targetRecipients = p.phoneNumbers.map(num => ({ phone: num, name: p.clientName || 'Recipient' })); console.log(`[Scheduler] Dispatching automated WhatsApp follow-up for client template block: ${p.clientName || 'Recipient'}`); chrome.runtime.sendMessage({ action: 'start_wa_campaign', recipients: targetRecipients, text: customizedMsg, media: null, delayConfig: { minSeconds: 15, maxSeconds: 45 } }); p.followUpSent = true; changed = true; } } } } if (changed) { await savePrevPostsToLocal(); const inlinePanel = document.getElementById('previous-posts-inline-panel'); if (inlinePanel) { renderPreviousPostsGrid(); renderEvaluationDashboard(); } } } async function openVariantTraceModal(postId) { const tbody = document.getElementById('variant-trace-tbody'); const metaInfo = document.getElementById('variant-trace-meta-info'); if (!tbody || !metaInfo) return; tbody.innerHTML = ''; const post = previousPostsData.find(p => p.id === postId); if (!post) return; metaInfo.textContent = `Delivery Trace Map for: "${post.title}" | Cumulative Performance Rating: ${(post.score || 0).toLocaleString('en-US')}%`; const campaignHistoryData = await FritreeStorage.get('campaignHistoryData', []); let matchCount = 0; campaignHistoryData.forEach(camp => { if (camp.logs && Array.isArray(camp.logs)) { camp.logs.forEach(log => { if (log.variantName === postId) { matchCount++; const tr = document.createElement('tr'); const tdCamp = document.createElement('td'); tdCamp.style.fontWeight = "bold"; tdCamp.textContent = camp.name; const tdTarget = document.createElement('td'); tdTarget.textContent = log.groupId || "Unknown"; const tdStatus = document.createElement('td'); const badge = document.createElement('span'); badge.className = `status-badge ${log.status === 'success' ? 'success' : (log.status === 'pending' ? 'pending' : 'failed')}`; badge.textContent = log.status === 'success' ? 'Dispatched' : (log.status === 'pending' ? 'Moderation' : 'Failed'); tdStatus.appendChild(badge); const tdLink = document.createElement('td'); if (log.postUrl) { const link = document.createElement('a'); link.href = log.postUrl; link.target = '_blank'; link.style.cssText = 'color:#1877f2; font-weight:bold; text-decoration:none;'; link.innerHTML = 'View'; tdLink.appendChild(link); } else { tdLink.textContent = log.error || 'N/A'; } tr.appendChild(tdCamp); tr.appendChild(tdTarget); tr.appendChild(tdStatus); tr.appendChild(tdLink); tbody.appendChild(tr); } }); } }); if (matchCount === 0) { tbody.innerHTML = 'No active dispatches associated with this template block found in the ledger.'; } const modal = document.getElementById('variant-trace-modal'); if (modal) modal.style.display = 'flex'; } /** * Celebrator effect designed to complete instantly */ function triggerOnScreenCelebration() { console.log("[Fritree Celebration] Action completed instantly."); } // Exports global.FritreeRotation = { init: initRotationModule, isRotationActive: () => isRotationActive, getSelectedIds: () => Array.from(selectedPrevPostIds), getPosts: () => previousPostsData, disableRotation: () => { isRotationActive = false; const indicatorPanel = document.getElementById('prev-posts-indicator-panel'); const composerPanel = document.getElementById('default-composer-panel'); const postTextarea = document.getElementById('post-text'); if (indicatorPanel) indicatorPanel.style.display = 'none'; if (composerPanel) composerPanel.style.opacity = '1'; if (postTextarea) postTextarea.disabled = false; }, incrementUsage: incrementPrevPostUsage, saveDraft: autoSaveCurrentPostToLibrary, recalculateScores: recalculateAllPostScores, selectNextPost: selectNextRotationPost, triggerFollowUps: checkAndTriggerFollowUps, celebrate: triggerOnScreenCelebration, showTrace: openVariantTraceModal }; if (document.readyState === 'complete' || document.readyState === 'interactive') { initRotationModule(); } else { document.addEventListener('DOMContentLoaded', () => initRotationModule()); } })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);