facebook / modules /rotation.js
Althnayi's picture
Upload 27 files
2a196ac verified
Raw
History Blame Contribute Delete
77.8 kB
// ============================================================================
// 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}
/**
* 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 '<i class="fa-solid fa-house" style="color: #0d9488; margin-right: 5px;"></i>';
}
if (raw.includes('sale') || raw.includes('discount') || raw.includes('promo') || raw.includes('deal') || catStr.includes('sale') || catStr.includes('ecom')) {
return '<i class="fa-solid fa-tags" style="color: #f59e0b; margin-right: 5px;"></i>';
}
if (raw.includes('software') || raw.includes('saas') || raw.includes('tech') || raw.includes('code') || catStr.includes('tech') || catStr.includes('software')) {
return '<i class="fa-solid fa-laptop-code" style="color: #6366f1; margin-right: 5px;"></i>';
}
if (raw.includes('support') || raw.includes('help') || raw.includes('service') || catStr.includes('service') || catStr.includes('support')) {
return '<i class="fa-solid fa-headset" style="color: #ef4444; margin-right: 5px;"></i>';
}
if (raw.includes('crypto') || raw.includes('token') || raw.includes('coin') || raw.includes('wallet') || catStr.includes('crypto')) {
return '<i class="fa-solid fa-wallet" style="color: #8b5cf6; margin-right: 5px;"></i>';
}
return '<i class="fa-solid fa-file-invoice" style="color: #64748b; margin-right: 5px;"></i>';
}
// ============================================================================
// 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 = '<i class="fa-solid fa-circle-check"></i>Automated Content Rotation Active';
} else {
badge.className = "badge red";
badge.innerHTML = '<i class="fa-solid fa-ban"></i>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 = '<tr><td colspan="5" style="text-align:center; padding:20px; color:var(--text-muted);"><i class="fa-solid fa-triangle-exclamation"></i>Database archive is currently empty. Add rotation templates to start accumulating performance analytics.</td></tr>';
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 = `<i class="fa-solid fa-play" style="color: #64748b; margin-right: 5px;"></i>${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 = `<i class="fa-solid fa-circle-check" style="margin-right: 5px;"></i>${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 = `<i class="fa-solid fa-clock" style="margin-right: 5px;"></i>${p.stats ? (p.stats.pending || 0).toLocaleString('en-US') : 0}`;
const tdFailed = document.createElement('td');
tdFailed.style.color = 'var(--danger)';
tdFailed.innerHTML = `<i class="fa-solid fa-triangle-exclamation" style="margin-right: 5px;"></i>${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 = `<i class="fa-solid fa-play" style="margin-right:5px;"></i>${publishedSum.toLocaleString('en-US')}`;
if (totalSuccEl) totalSuccEl.innerHTML = `<i class="fa-solid fa-circle-check" style="margin-right:5px;"></i>${successfulSum.toLocaleString('en-US')}`;
if (totalRevEl) totalRevEl.innerHTML = `<i class="fa-solid fa-hourglass-half" style="margin-right:5px;"></i>${reviewedSum.toLocaleString('en-US')}`;
if (totalFailEl) totalFailEl.innerHTML = `<i class="fa-solid fa-triangle-exclamation" style="margin-right:5px;"></i>${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 = '<i class="fa-solid fa-chevron-left"></i>';
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 = '<i class="fa-solid fa-chevron-right"></i>';
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 = `<i class="fa-solid fa-triangle-exclamation"></i> 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 = `<i class="fa-solid fa-triangle-exclamation"></i> ${analysis.errors}`;
outputEl.style.color = "var(--danger)";
} else {
outputEl.innerHTML = `<i class="fa-solid fa-circle-check"></i>Spintax brackets verified successfully. <strong>Expected variations: ${analysis.variations.toLocaleString('en-US')} unique copy paths.</strong>`;
outputEl.style.color = "var(--success)";
}
} else {
outputEl.innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> 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 = '<i class="fa-solid fa-xmark"></i>';
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 = '<i class="fa-solid fa-folder-open"></i> 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 = '<i class="fa-solid fa-circle-check"></i>Active'; }
else if (p.status === 'frozen') { statColor = 'background: #e0f2fe; color: #0369a1;'; statText = '<i class="fa-solid fa-snowflake"></i>Frozen'; }
else if (p.status === 'draft') { statColor = 'background: #f1f5f9; color: #475569;'; statText = '<i class="fa-solid fa-file-pen"></i>Draft'; }
else if (p.status === 'inactive') { statColor = 'background: #fee2e2; color: #991b1b;'; statText = '<i class="fa-solid fa-circle-xmark"></i>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 = `<i class="fa-brands fa-whatsapp"></i>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 = `<i class="fa-solid fa-play" style="margin-right: 3px;"></i>Dispatched: ${useCount.toLocaleString('en-US')} times`;
const dateSpan = document.createElement('span');
const dateVal = p.entryDate || p.creationDate;
dateSpan.innerHTML = `<i class="fa-regular fa-calendar" style="margin-right: 3px;"></i>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 = `<i class="fa-solid fa-tags" style="margin-right: 3px;"></i>${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 = '<i class="fa-solid fa-file-lines"></i>';
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 = '<i class="fa-solid fa-pen-to-square"></i>';
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 = '<i class="fa-solid fa-trash"></i>';
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 = '<i class="fa-solid fa-xmark"></i>';
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 = '<i class="fa-solid fa-up-right-from-square"></i>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 = '<tr><td colspan="4" style="text-align:center; color:var(--text-muted); padding:15px;"><i class="fa-solid fa-folder-open"></i>No active dispatches associated with this template block found in the ledger.</td></tr>';
}
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);