v1.8.9: fix JS syntax errors — em dash in comment, broken try/catch in saveBranding

This commit is contained in:
2026-05-28 20:46:31 +00:00
parent 43a30c50f1
commit b6452a9c0e
17 changed files with 67 additions and 66 deletions
+62 -61
View File
@@ -926,15 +926,15 @@ curl -X POST https://rag.afaqzaid.com/ingest-url \
</div>
<script>
// ADMIN_KEY removed — JWT-only auth
// ADMIN_KEY removed - JWT-only auth
let JWT_TOKEN = localStorage.getItem('admin_jwt') || '';
const RAG_KEY = 'zRf77E_brSEnA2BUi40uxLxT4uofAk661WMZb-cdv4M';
let keyVisible = false;
// Hide login immediately if token present — no flash
// Hide login immediately if token present -- no flash
if (!JWT_TOKEN) document.getElementById('login').style.display = 'flex';
// ── Core API ──────────────────────────────────────────────────────────────────
// -- Core API ------------------------------------------------------------------
async function api(path, opts={}) {
const headers = {'Content-Type':'application/json', 'Authorization':'Bearer '+JWT_TOKEN};
if (JWT_TOKEN) headers['Authorization'] = 'Bearer ' + JWT_TOKEN;
@@ -942,7 +942,7 @@ async function api(path, opts={}) {
if (r.status === 401) {
JWT_TOKEN = ''; localStorage.removeItem('admin_jwt');
document.getElementById('login').style.display = 'flex';
throw new Error('Session expired — please login again');
throw new Error('Session expired -- please login again');
}
if (!r.ok) throw new Error(r.status + ' ' + path);
return r.json();
@@ -961,7 +961,7 @@ function showErr(id, msg) {
if (el) el.innerHTML = `<div class="glass border border-red-900/40 rounded-xl p-4 text-red-400 text-sm flex items-center gap-2"><span class="msym">error</span>${msg}</div>`;
}
// ── Auth ──────────────────────────────────────────────────────────────────────
// -- Auth ----------------------------------------------------------------------
async function doLogin() {
const email = document.getElementById('admin-email').value.trim();
const pass = document.getElementById('admin-pass').value;
@@ -980,7 +980,7 @@ async function doLogin() {
} catch(e) { errEl.textContent='Connection error'; errEl.classList.remove('hidden'); }
}
// ── Navigation ────────────────────────────────────────────────────────────────
// -- Navigation ----------------------------------------------------------------
function showPage(name, btn) {
document.querySelectorAll('.page').forEach(p=>p.classList.add('hidden'));
document.querySelectorAll('.nav-btn').forEach(b=>{
@@ -999,7 +999,7 @@ function showPage(name, btn) {
function openSidebar() { document.getElementById('sidebar').classList.add('open'); document.getElementById('sidebar-overlay').classList.add('show'); }
function closeSidebar() { document.getElementById('sidebar').classList.remove('open'); document.getElementById('sidebar-overlay').classList.remove('show'); }
// ── Helpers ───────────────────────────────────────────────────────────────────
// -- Helpers -------------------------------------------------------------------
function bar(label, val, max, color='#4edea3') {
const pct = max>0 ? Math.round(val/max*100) : 0;
return `<div class="flex items-center gap-3 mb-3">
@@ -1022,7 +1022,7 @@ function badge(llm) {
return `<span class="text-[10px] font-bold px-2 py-0.5 rounded-full ${map[llm]||'bg-blue-900/40 text-blue-400'}">${llm}</span>`;
}
// ── Page Loaders ──────────────────────────────────────────────────────────────
// -- Page Loaders --------------------------------------------------------------
async function loadLibraryHealth() {
try {
const d = await api('/admin/library-health');
@@ -1098,7 +1098,7 @@ async function loadLibraryHealth() {
</div>
<div class="space-y-1">
${d.pending_jobs.map(j => `<div class="bg-yellow-900/20 border border-yellow-900/30 rounded-lg px-3 py-2 text-xs flex justify-between">
<span class="text-on-surface-variant">${j.status} — ${j.done}/${j.total} done</span>
<span class="text-on-surface-variant">${j.status} -- ${j.done}/${j.total} done</span>
<span class="text-red-400">${j.errors > 0 ? j.errors + ' errors' : ''}</span>
</div>`).join('')}
</div>
@@ -1122,7 +1122,7 @@ async function rebuildBM25() {
btn.disabled = true; btn.innerHTML = '<span class="msym text-sm animate-spin">refresh</span> Rebuilding...';
try {
const r = await api('/admin/rebuild-bm25', {method:'POST'});
toast(`BM25 rebuilt — ${r.chunks_indexed} chunks indexed`, 'success');
toast(`BM25 rebuilt -- ${r.chunks_indexed} chunks indexed`, 'success');
} catch(e) { toast('BM25 rebuild failed: ' + e.message, 'error'); }
btn.disabled = false; btn.innerHTML = '<span class="msym text-sm">refresh</span> Rebuild BM25';
}
@@ -1132,7 +1132,7 @@ async function clearCache() {
btn.disabled = true; btn.innerHTML = '<span class="msym text-sm">delete_sweep</span> Clearing...';
try {
const r = await api('/admin/clear-cache', {method:'POST'});
toast(`Cache cleared — ${r.cleared} entries removed`, 'success');
toast(`Cache cleared -- ${r.cleared} entries removed`, 'success');
loadOverview();
} catch(e) { toast('Cache clear failed: ' + e.message, 'error'); }
btn.disabled = false; btn.innerHTML = '<span class="msym text-sm">delete_sweep</span> Clear Cache';
@@ -1359,7 +1359,7 @@ async function loadSettings() {
<span class="text-sm text-on-surface-variant">${l}</span><span class="text-xs font-mono glass px-2 py-1 rounded-lg text-primary truncate max-w-[140px]">${v}</span>
</div>`).join('')}`;
// ── R2 / Storage section ──────────────────────────────────────────────────
// -- R2 / Storage section --------------------------------------------------
document.getElementById('settings-r2').innerHTML = `
<h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2">
<span class="msym text-base">cloud</span>Cloudflare R2 Storage
@@ -1422,8 +1422,8 @@ async function loadSettings() {
<p id="r2-status" class="text-xs text-center mt-3 hidden"></p>
<div class="mt-5 pt-4 border-t border-outline-variant/20">
<p class="text-xs text-on-surface-variant mb-3 font-semibold uppercase tracking-widest">Service Endpoints</p>
${[['Typesense',d.typesense_host ? 'http://'+d.typesense_host+':8108' : '—'],
['MinIO',d.minio_endpoint||'—'],
${[['Typesense',d.typesense_host ? 'http://'+d.typesense_host+':8108' : '--'],
['MinIO',d.minio_endpoint||'--'],
].map(([l,v])=>
'<div class="flex justify-between items-center py-2"><span class="text-xs text-on-surface-variant">'+l+'</span><span class="text-xs font-mono glass px-2 py-1 rounded-lg text-primary">'+v+'</span></div>'
).join('')}
@@ -1443,7 +1443,7 @@ async function saveR2Config() {
R2_PUBLIC_URL: document.getElementById('r2-public').value.trim(),
};
if (!raw.R2_ENDPOINT) { toast('Endpoint URL مطلوب'); return; }
// Only send non-empty values — never overwrite saved keys with blank
// Only send non-empty values -- never overwrite saved keys with blank
const payload = Object.fromEntries(Object.entries(raw).filter(([,v]) => v !== ''));
status.textContent = 'جاري الحفظ...'; status.classList.remove('hidden');
try {
@@ -1466,10 +1466,10 @@ async function testR2Connection() {
const status = document.getElementById('r2-status');
status.textContent = 'جاري الاختبار...'; status.classList.remove('hidden');
try {
// Test-only — never saves, never overwrites stored credentials
// Test-only -- never saves, never overwrites stored credentials
const r = await api('/admin/r2-test');
if (r.ok) {
status.textContent = '✅ R2 متصل — ' + (r.detail || '');
status.textContent = '✅ R2 متصل -- ' + (r.detail || '');
status.style.color = '#4edea3';
} else {
status.textContent = '❌ ' + (r.detail || 'فشل الاتصال');
@@ -1543,12 +1543,13 @@ async function loadBranding() {
async function saveBranding() {
try {
const r = await fetch('/admin/branding', {method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({site_name:document.getElementById('b-site-name').value, site_subtitle:document.getElementById('b-subtitle').value, logo_url:document.getElementById('b-logo').value, logo_size:document.getElementById('b-logo-size')?.value||'40', primary_color:document.getElementById('b-primary-hex').value, secondary_color:document.getElementById('b-secondary-hex').value})});
if (r.ok) toast('Branding saved!','success');
const p=document.getElementById('b-primary-hex')?.value;
const s=document.getElementById('b-secondary-hex')?.value;
if(p) document.documentElement.style.setProperty('--primary',p);
if(s) document.documentElement.style.setProperty('--secondary',s);
} else alert('Save failed');
if (r.ok) {
toast('Branding saved!','success');
const p=document.getElementById('b-primary-hex')?.value;
const s=document.getElementById('b-secondary-hex')?.value;
if(p) document.documentElement.style.setProperty('--primary',p);
if(s) document.documentElement.style.setProperty('--secondary',s);
} else { alert('Save failed'); }
} catch(e) { alert('Error: '+e.message); }
}
@@ -1635,7 +1636,7 @@ async function loadClaude() {
function toggleApiKey() {
const el = document.getElementById('api-key-display'), btn = document.getElementById('key-toggle-btn');
keyVisible = !keyVisible;
el.textContent = keyVisible ? RAG_KEY : '••••••••••••••••••••••';
el.textContent = keyVisible ? RAG_KEY : '**********************';
btn.textContent = keyVisible ? 'Hide' : 'Show';
}
@@ -1655,7 +1656,7 @@ async function saveClaudeKey() {
} catch(e) { toast('Error: '+e.message); }
}
// ── Auto-login on page load ───────────────────────────────────────────────────
// -- Auto-login on page load ---------------------------------------------------
if (JWT_TOKEN) {
document.getElementById('login').style.display = 'none';
fetch('/auth/me', {headers:{'Authorization':'Bearer '+JWT_TOKEN}})
@@ -1667,10 +1668,10 @@ if (JWT_TOKEN) {
} else {
loadOverview();
}
}).catch(() => { loadOverview(); }); // network error — stay logged in
}).catch(() => { loadOverview(); }); // network error -- stay logged in
}
// ── Nav init ──────────────────────────────────────────────────────────────────
// -- Nav init ------------------------------------------------------------------
document.querySelectorAll('.nav-btn').forEach(b => {
if (!b.classList.contains('active')) b.classList.add('text-on-surface-variant','hover:text-primary','hover:bg-primary/5');
});
@@ -1678,7 +1679,7 @@ document.querySelector('.nav-btn.active')?.classList.add('bg-primary/10','text-p
// ══════════════════════════════════════════════════════════════════════════════
// TRACKING — Google Analytics 4 + DataLayer Events + Pixel Tracking
// TRACKING -- Google Analytics 4 + DataLayer Events + Pixel Tracking
// ══════════════════════════════════════════════════════════════════════════════
let gaEnabled = false;
let analyticsEvents = [];
@@ -1694,7 +1695,7 @@ const PB_META = {
custom: {label:'Custom Code', hint:'', icon:'&#9889;'},
};
// ── Load all tracking data ────────────────────────────────────────────────────
// -- Load all tracking data ----------------------------------------------------
async function loadTracking() {
try {
const [ga, px] = await Promise.all([
@@ -1709,7 +1710,7 @@ async function loadTracking() {
const statusEl = document.getElementById('ga-status');
if (ga.measurement_id) {
const on = ga.enabled;
statusEl.innerHTML = '<span class="w-2 h-2 rounded-full ' + (on?'bg-emerald-400':'bg-yellow-400') + ' inline-block"></span> <span class="' + (on?'text-emerald-400':'text-yellow-400') + '">' + (on?'Active':'Disabled') + ' — ' + ga.measurement_id + '</span>';
statusEl.innerHTML = '<span class="w-2 h-2 rounded-full ' + (on?'bg-emerald-400':'bg-yellow-400') + ' inline-block"></span> <span class="' + (on?'text-emerald-400':'text-yellow-400') + '">' + (on?'Active':'Disabled') + ' -- ' + ga.measurement_id + '</span>';
} else {
statusEl.innerHTML = '<span class="w-2 h-2 rounded-full bg-outline-variant inline-block"></span> Not configured';
}
@@ -1720,7 +1721,7 @@ async function loadTracking() {
} catch(e) { toast('Tracking: ' + e.message); console.error(e); }
}
// ── GA4 ───────────────────────────────────────────────────────────────────────
// -- GA4 -----------------------------------------------------------------------
function updateGAToggleBtn(enabled) {
const btn = document.getElementById('ga-toggle-btn');
if (!btn) return;
@@ -1744,7 +1745,7 @@ async function saveGAConfig() {
} catch(e) { toast('Save failed: ' + e.message); }
}
// ── DataLayer Events ──────────────────────────────────────────────────────────
// -- DataLayer Events ----------------------------------------------------------
function renderEventsList() {
const el = document.getElementById('analytics-events-list');
if (!el) return;
@@ -1877,7 +1878,7 @@ async function deleteEvent(idx) {
renderEventsList();
}
// ── Pixel Tracking ────────────────────────────────────────────────────────────
// -- Pixel Tracking ------------------------------------------------------------
function renderPixelsList() {
const el = document.getElementById('pixels-list');
if (!el) return;
@@ -1885,7 +1886,7 @@ function renderPixelsList() {
el.innerHTML = trackingPixels.map((px, i) => {
const meta = PB_META[px.platform] || PB_META.custom;
const on = px.enabled !== false;
const label = px.platform === 'custom' ? (px.name || 'Custom') : (px.pixel_id || '—');
const label = px.platform === 'custom' ? (px.name || 'Custom') : (px.pixel_id || '--');
return '<div class="glass rounded-xl p-4 flex items-center gap-4 ' + (on?'':'opacity-50') + '">' +
'<div class="w-10 h-10 rounded-xl bg-purple-900/30 flex items-center justify-center shrink-0 text-xl">' + meta.icon + '</div>' +
'<div class="flex-1 min-w-0"><div class="flex items-center gap-2 mb-0.5"><span class="text-sm font-bold capitalize">' + px.platform + '</span>' +
@@ -1986,7 +1987,7 @@ async function pbDelete(idx) {
}
// ── Page Scanner ─────────────────────────────────────────────────────────────
// -- Page Scanner -------------------------------------------------------------
let scanCurrentPage = null;
function openScanPicker() {
@@ -2084,7 +2085,7 @@ function applyScanResult(item) {
updateEBPreview();
closeScanPicker();
// Toast to confirm
toast('Element applied — review and save', 'success');
toast('Element applied -- review and save', 'success');
}
@@ -2118,9 +2119,9 @@ async function loadLLMConfig() {
</div>
<div id="llm-claude-fields" class="${d.provider==='claude' ? '' : 'hidden'} space-y-3 mb-4">
<div class="glass rounded-xl p-4 text-xs text-on-surface-variant space-y-1 mb-3">
<p class="flex items-center gap-2"><span class="msym text-base text-purple-400">bolt</span>Anthropic Claude — fast, high quality</p>
<p class="flex items-center gap-2"><span class="msym text-base text-purple-400">bolt</span>Anthropic Claude -- fast, high quality</p>
<p class="flex items-center gap-2"><span class="msym text-base text-primary">all_inclusive</span>claude-sonnet-4 recommended for Arabic</p>
<p class="flex items-center gap-2"><span class="msym text-base text-on-surface-variant">storage</span>API key required — console.anthropic.com</p>
<p class="flex items-center gap-2"><span class="msym text-base text-on-surface-variant">storage</span>API key required -- console.anthropic.com</p>
</div>
<div id="claude-key-llm-wrap">
<label class="text-xs text-on-surface-variant block mb-1.5">API Key
@@ -2153,14 +2154,14 @@ async function loadLLMConfig() {
</div>
<div id="llm-local-info" class="${isLocal ? '' : 'hidden'} glass rounded-xl p-4 mb-4 text-xs text-on-surface-variant space-y-1">
<p class="flex items-center gap-2"><span class="msym text-base text-emerald-400">check_circle</span>Running qwen2:7b on your VPS CPU</p>
<p class="flex items-center gap-2"><span class="msym text-base text-yellow-400">schedule</span>~30-50s per query — streaming masks latency</p>
<p class="flex items-center gap-2"><span class="msym text-base text-primary">lock</span>Fully private — no external requests</p>
<p class="flex items-center gap-2"><span class="msym text-base text-yellow-400">schedule</span>~30-50s per query -- streaming masks latency</p>
<p class="flex items-center gap-2"><span class="msym text-base text-primary">lock</span>Fully private -- no external requests</p>
</div>
<div id="llm-cf-fields" class="${isCF ? '' : 'hidden'} space-y-3 mb-4">
<div class="glass rounded-xl p-4 text-xs text-on-surface-variant space-y-1 mb-3">
<p class="flex items-center gap-2"><span class="msym text-base text-blue-400">bolt</span>GPU inference — 3-5s per query</p>
<p class="flex items-center gap-2"><span class="msym text-base text-blue-400">bolt</span>GPU inference -- 3-5s per query</p>
<p class="flex items-center gap-2"><span class="msym text-base text-primary">all_inclusive</span>~900 free queries/day on free tier</p>
<p class="flex items-center gap-2"><span class="msym text-base text-on-surface-variant">storage</span>Your PDFs stay local — only text prompt sent</p>
<p class="flex items-center gap-2"><span class="msym text-base text-on-surface-variant">storage</span>Your PDFs stay local -- only text prompt sent</p>
</div>
<div>
<label class="text-xs text-on-surface-variant block mb-1.5">Account ID</label>
@@ -2229,7 +2230,7 @@ async function saveLLMConfig() {
status.textContent = 'Saving...'; status.classList.remove('hidden'); status.style.color = '';
try {
await api('/admin/llm-config', {method:'POST', body:JSON.stringify(payload)});
status.textContent = '✅ Saved — restart API container to apply';
status.textContent = '✅ Saved -- restart API container to apply';
status.style.color = '#4edea3';
setTimeout(loadLLMConfig, 1500);
} catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; }
@@ -2261,8 +2262,8 @@ document.querySelectorAll('.spotlight-card').forEach(card => {
});
// ── Book Edit ─────────────────────────────────────────────────────────────────
let _bookEditCache = {}; // id → book data from last loadBooks()
// -- Book Edit -----------------------------------------------------------------
let _bookEditCache = {}; // id -> book data from last loadBooks()
function openBookEdit(btn) {
const d = btn.dataset;
@@ -2327,7 +2328,7 @@ async function retryMetaExtraction() {
status.classList.remove('hidden');
try {
await api('/admin/books/' + id + '/extract-meta', { method: 'POST' });
status.textContent = '✅ طُلب الاستخراج — ستُحدَّث البيانات خلال دقيقة';
status.textContent = '✅ طُلب الاستخراج -- ستُحدَّث البيانات خلال دقيقة';
status.style.color = '#4edea3';
// Reload after 65s
setTimeout(() => { closeBookEdit(); loadBooks(); }, 65000);
@@ -2337,7 +2338,7 @@ async function retryMetaExtraction() {
}
}
// Book data passed via data attributes — no cache needed
// Book data passed via data attributes -- no cache needed
</script>
@@ -2393,7 +2394,7 @@ async function retryMetaExtraction() {
<script>
// ── Page Builder ─────────────────────────────────────────────────────────────
// -- Page Builder -------------------------------------------------------------
let _pbPage = 'privacy';
let _pbMode = 'visual';
let _pbBlocks = [];
@@ -2406,7 +2407,7 @@ const PB_PAGES = {
index: { label: 'Home', url: '/' },
};
// ── Switch page ───────────────────────────────────────────────────────────────
// -- Switch page ---------------------------------------------------------------
function pbSwitch(page) {
_pbPage = page;
document.querySelectorAll('.pb-tab').forEach(b => {
@@ -2418,7 +2419,7 @@ function pbSwitch(page) {
pbLoad();
}
// ── Mode toggle ───────────────────────────────────────────────────────────────
// -- Mode toggle ---------------------------------------------------------------
function pbSetMode(mode) {
_pbMode = mode;
const mv = document.getElementById('pb-mode-visual');
@@ -2434,12 +2435,12 @@ function pbSetMode(mode) {
mh.className = 'px-3 py-1.5 rounded-lg text-xs font-bold bg-primary/20 text-primary border border-primary/30 flex items-center gap-1';
mv.className = 'px-3 py-1.5 rounded-lg text-xs font-bold glass text-on-surface-variant flex items-center gap-1';
vh.classList.remove('hidden'); vv.classList.add('hidden');
// Sync blocks → HTML
// Sync blocks -> HTML
document.getElementById('pb-html-editor').value = _pbRawHTML;
}
}
// ── Load page content ─────────────────────────────────────────────────────────
// -- Load page content ---------------------------------------------------------
async function pbLoad() {
const blocksEl = document.getElementById('pb-blocks');
blocksEl.innerHTML = '<div class="text-on-surface-variant text-sm p-4">Loading...</div>';
@@ -2455,7 +2456,7 @@ async function pbLoad() {
}
}
// ── Parse HTML → blocks ───────────────────────────────────────────────────────
// -- Parse HTML -> blocks -------------------------------------------------------
function pbParseHTML(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
@@ -2485,11 +2486,11 @@ function pbParseHTML(html) {
return blocks;
}
// ── Render blocks in visual editor ───────────────────────────────────────────
// -- Render blocks in visual editor -------------------------------------------
function pbRenderBlocks() {
const container = document.getElementById('pb-blocks');
if (!_pbBlocks.length) {
container.innerHTML = '<div class="glass rounded-2xl p-6 text-center text-on-surface-variant text-sm">No blocks yet — add one below</div>';
container.innerHTML = '<div class="glass rounded-2xl p-6 text-center text-on-surface-variant text-sm">No blocks yet -- add one below</div>';
return;
}
container.innerHTML = _pbBlocks.map((b, idx) => pbBlockHTML(b, idx)).join('');
@@ -2555,7 +2556,7 @@ function pbBlockHTML(b, idx) {
</div>`;
}
// ── Block actions ─────────────────────────────────────────────────────────────
// -- Block actions -------------------------------------------------------------
function pbAddBlock(type) {
const newBlock = { id: Date.now(), type, content: '', level: 'h2', ordered: false, title: '', subtitle: '', email: '' };
_pbBlocks.push(newBlock);
@@ -2579,7 +2580,7 @@ function pbMoveBlock(idx, dir) {
pbRenderBlocks();
}
// ── Blocks → HTML ─────────────────────────────────────────────────────────────
// -- Blocks -> HTML -------------------------------------------------------------
function pbBlocksToHTML(blocks) {
return blocks.map(b => {
if (b.type === 'heading') return `<${b.level||'h2'} class="text-primary font-bold mb-2">${b.content}</${b.level||'h2'}>`;
@@ -2597,7 +2598,7 @@ function pbBlocksToHTML(blocks) {
}).join('\n');
}
// ── Inject blocks into page HTML ──────────────────────────────────────────────
// -- Inject blocks into page HTML ----------------------------------------------
function pbInjectBlocks(rawHTML, blocksHTML) {
// Try to inject into existing content area
const parser = new DOMParser();
@@ -2621,10 +2622,10 @@ function pbInjectBlocks(rawHTML, blocksHTML) {
const s = new XMLSerializer();
return '<!DOCTYPE html>' + s.serializeToString(doc.documentElement);
}
return rawHTML; // fallback — unchanged
return rawHTML; // fallback -- unchanged
}
// ── Save ──────────────────────────────────────────────────────────────────────
// -- Save ----------------------------------------------------------------------
async function pbSave() {
const btn = document.getElementById('pb-save-btn');
const status = document.getElementById('pb-status');
@@ -2659,7 +2660,7 @@ async function pbSave() {
btn.innerHTML = '<span class="msym text-base">save</span> Save';
}
// ── Wire into showPage ────────────────────────────────────────────────────────
// -- Wire into showPage --------------------------------------------------------
document.addEventListener('DOMContentLoaded', () => {
const origShowPage = window.showPage;
if (origShowPage) {