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
+4 -4
View File
@@ -628,10 +628,10 @@ def process_query(query: str, session_id: str = "default"):
"chunks": final_chunks, "chunks": final_chunks,
"books_used": books_used, "books_used": books_used,
"retrieval": { "retrieval": {
"dense": len(dense_chunks), "dense": len(final_chunks),
"bm25": len(bm25_chunks), "bm25": len(final_chunks),
"fused": len(fused), "fused": len(final_chunks),
"both": sum(1 for c in fused if c.get("source") == "both"), "both": sum(1 for c in final_chunks if c.get("source") == "both"),
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
1 1
/var/lib/postgresql/data /var/lib/postgresql/data
1779841526 1780001008
5432 5432
/var/run/postgresql /var/run/postgresql
* *
+58 -57
View File
@@ -926,15 +926,15 @@ curl -X POST https://rag.afaqzaid.com/ingest-url \
</div> </div>
<script> <script>
// ADMIN_KEY removed — JWT-only auth // ADMIN_KEY removed - JWT-only auth
let JWT_TOKEN = localStorage.getItem('admin_jwt') || ''; let JWT_TOKEN = localStorage.getItem('admin_jwt') || '';
const RAG_KEY = 'zRf77E_brSEnA2BUi40uxLxT4uofAk661WMZb-cdv4M'; const RAG_KEY = 'zRf77E_brSEnA2BUi40uxLxT4uofAk661WMZb-cdv4M';
let keyVisible = false; 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'; if (!JWT_TOKEN) document.getElementById('login').style.display = 'flex';
// ── Core API ────────────────────────────────────────────────────────────────── // -- Core API ------------------------------------------------------------------
async function api(path, opts={}) { async function api(path, opts={}) {
const headers = {'Content-Type':'application/json', 'Authorization':'Bearer '+JWT_TOKEN}; const headers = {'Content-Type':'application/json', 'Authorization':'Bearer '+JWT_TOKEN};
if (JWT_TOKEN) headers['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) { if (r.status === 401) {
JWT_TOKEN = ''; localStorage.removeItem('admin_jwt'); JWT_TOKEN = ''; localStorage.removeItem('admin_jwt');
document.getElementById('login').style.display = 'flex'; 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); if (!r.ok) throw new Error(r.status + ' ' + path);
return r.json(); 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>`; 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() { async function doLogin() {
const email = document.getElementById('admin-email').value.trim(); const email = document.getElementById('admin-email').value.trim();
const pass = document.getElementById('admin-pass').value; const pass = document.getElementById('admin-pass').value;
@@ -980,7 +980,7 @@ async function doLogin() {
} catch(e) { errEl.textContent='Connection error'; errEl.classList.remove('hidden'); } } catch(e) { errEl.textContent='Connection error'; errEl.classList.remove('hidden'); }
} }
// ── Navigation ──────────────────────────────────────────────────────────────── // -- Navigation ----------------------------------------------------------------
function showPage(name, btn) { function showPage(name, btn) {
document.querySelectorAll('.page').forEach(p=>p.classList.add('hidden')); document.querySelectorAll('.page').forEach(p=>p.classList.add('hidden'));
document.querySelectorAll('.nav-btn').forEach(b=>{ 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 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'); } function closeSidebar() { document.getElementById('sidebar').classList.remove('open'); document.getElementById('sidebar-overlay').classList.remove('show'); }
// ── Helpers ─────────────────────────────────────────────────────────────────── // -- Helpers -------------------------------------------------------------------
function bar(label, val, max, color='#4edea3') { function bar(label, val, max, color='#4edea3') {
const pct = max>0 ? Math.round(val/max*100) : 0; const pct = max>0 ? Math.round(val/max*100) : 0;
return `<div class="flex items-center gap-3 mb-3"> 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>`; 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() { async function loadLibraryHealth() {
try { try {
const d = await api('/admin/library-health'); const d = await api('/admin/library-health');
@@ -1098,7 +1098,7 @@ async function loadLibraryHealth() {
</div> </div>
<div class="space-y-1"> <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"> ${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> <span class="text-red-400">${j.errors > 0 ? j.errors + ' errors' : ''}</span>
</div>`).join('')} </div>`).join('')}
</div> </div>
@@ -1122,7 +1122,7 @@ async function rebuildBM25() {
btn.disabled = true; btn.innerHTML = '<span class="msym text-sm animate-spin">refresh</span> Rebuilding...'; btn.disabled = true; btn.innerHTML = '<span class="msym text-sm animate-spin">refresh</span> Rebuilding...';
try { try {
const r = await api('/admin/rebuild-bm25', {method:'POST'}); 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'); } } catch(e) { toast('BM25 rebuild failed: ' + e.message, 'error'); }
btn.disabled = false; btn.innerHTML = '<span class="msym text-sm">refresh</span> Rebuild BM25'; 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...'; btn.disabled = true; btn.innerHTML = '<span class="msym text-sm">delete_sweep</span> Clearing...';
try { try {
const r = await api('/admin/clear-cache', {method:'POST'}); 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(); loadOverview();
} catch(e) { toast('Cache clear failed: ' + e.message, 'error'); } } catch(e) { toast('Cache clear failed: ' + e.message, 'error'); }
btn.disabled = false; btn.innerHTML = '<span class="msym text-sm">delete_sweep</span> Clear Cache'; 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> <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('')}`; </div>`).join('')}`;
// ── R2 / Storage section ────────────────────────────────────────────────── // -- R2 / Storage section --------------------------------------------------
document.getElementById('settings-r2').innerHTML = ` document.getElementById('settings-r2').innerHTML = `
<h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2"> <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 <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> <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"> <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> <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' : '—'], ${[['Typesense',d.typesense_host ? 'http://'+d.typesense_host+':8108' : '--'],
['MinIO',d.minio_endpoint||'—'], ['MinIO',d.minio_endpoint||'--'],
].map(([l,v])=> ].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>' '<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('')} ).join('')}
@@ -1443,7 +1443,7 @@ async function saveR2Config() {
R2_PUBLIC_URL: document.getElementById('r2-public').value.trim(), R2_PUBLIC_URL: document.getElementById('r2-public').value.trim(),
}; };
if (!raw.R2_ENDPOINT) { toast('Endpoint URL مطلوب'); return; } 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 !== '')); const payload = Object.fromEntries(Object.entries(raw).filter(([,v]) => v !== ''));
status.textContent = 'جاري الحفظ...'; status.classList.remove('hidden'); status.textContent = 'جاري الحفظ...'; status.classList.remove('hidden');
try { try {
@@ -1466,10 +1466,10 @@ async function testR2Connection() {
const status = document.getElementById('r2-status'); const status = document.getElementById('r2-status');
status.textContent = 'جاري الاختبار...'; status.classList.remove('hidden'); status.textContent = 'جاري الاختبار...'; status.classList.remove('hidden');
try { try {
// Test-only — never saves, never overwrites stored credentials // Test-only -- never saves, never overwrites stored credentials
const r = await api('/admin/r2-test'); const r = await api('/admin/r2-test');
if (r.ok) { if (r.ok) {
status.textContent = '✅ R2 متصل — ' + (r.detail || ''); status.textContent = '✅ R2 متصل -- ' + (r.detail || '');
status.style.color = '#4edea3'; status.style.color = '#4edea3';
} else { } else {
status.textContent = '❌ ' + (r.detail || 'فشل الاتصال'); status.textContent = '❌ ' + (r.detail || 'فشل الاتصال');
@@ -1543,12 +1543,13 @@ async function loadBranding() {
async function saveBranding() { async function saveBranding() {
try { 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})}); 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'); if (r.ok) {
toast('Branding saved!','success');
const p=document.getElementById('b-primary-hex')?.value; const p=document.getElementById('b-primary-hex')?.value;
const s=document.getElementById('b-secondary-hex')?.value; const s=document.getElementById('b-secondary-hex')?.value;
if(p) document.documentElement.style.setProperty('--primary',p); if(p) document.documentElement.style.setProperty('--primary',p);
if(s) document.documentElement.style.setProperty('--secondary',s); if(s) document.documentElement.style.setProperty('--secondary',s);
} else alert('Save failed'); } else { alert('Save failed'); }
} catch(e) { alert('Error: '+e.message); } } catch(e) { alert('Error: '+e.message); }
} }
@@ -1635,7 +1636,7 @@ async function loadClaude() {
function toggleApiKey() { function toggleApiKey() {
const el = document.getElementById('api-key-display'), btn = document.getElementById('key-toggle-btn'); const el = document.getElementById('api-key-display'), btn = document.getElementById('key-toggle-btn');
keyVisible = !keyVisible; keyVisible = !keyVisible;
el.textContent = keyVisible ? RAG_KEY : '••••••••••••••••••••••'; el.textContent = keyVisible ? RAG_KEY : '**********************';
btn.textContent = keyVisible ? 'Hide' : 'Show'; btn.textContent = keyVisible ? 'Hide' : 'Show';
} }
@@ -1655,7 +1656,7 @@ async function saveClaudeKey() {
} catch(e) { toast('Error: '+e.message); } } catch(e) { toast('Error: '+e.message); }
} }
// ── Auto-login on page load ─────────────────────────────────────────────────── // -- Auto-login on page load ---------------------------------------------------
if (JWT_TOKEN) { if (JWT_TOKEN) {
document.getElementById('login').style.display = 'none'; document.getElementById('login').style.display = 'none';
fetch('/auth/me', {headers:{'Authorization':'Bearer '+JWT_TOKEN}}) fetch('/auth/me', {headers:{'Authorization':'Bearer '+JWT_TOKEN}})
@@ -1667,10 +1668,10 @@ if (JWT_TOKEN) {
} else { } else {
loadOverview(); 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 => { 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'); 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 gaEnabled = false;
let analyticsEvents = []; let analyticsEvents = [];
@@ -1694,7 +1695,7 @@ const PB_META = {
custom: {label:'Custom Code', hint:'', icon:'&#9889;'}, custom: {label:'Custom Code', hint:'', icon:'&#9889;'},
}; };
// ── Load all tracking data ──────────────────────────────────────────────────── // -- Load all tracking data ----------------------------------------------------
async function loadTracking() { async function loadTracking() {
try { try {
const [ga, px] = await Promise.all([ const [ga, px] = await Promise.all([
@@ -1709,7 +1710,7 @@ async function loadTracking() {
const statusEl = document.getElementById('ga-status'); const statusEl = document.getElementById('ga-status');
if (ga.measurement_id) { if (ga.measurement_id) {
const on = ga.enabled; 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 { } else {
statusEl.innerHTML = '<span class="w-2 h-2 rounded-full bg-outline-variant inline-block"></span> Not configured'; 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); } } catch(e) { toast('Tracking: ' + e.message); console.error(e); }
} }
// ── GA4 ─────────────────────────────────────────────────────────────────────── // -- GA4 -----------------------------------------------------------------------
function updateGAToggleBtn(enabled) { function updateGAToggleBtn(enabled) {
const btn = document.getElementById('ga-toggle-btn'); const btn = document.getElementById('ga-toggle-btn');
if (!btn) return; if (!btn) return;
@@ -1744,7 +1745,7 @@ async function saveGAConfig() {
} catch(e) { toast('Save failed: ' + e.message); } } catch(e) { toast('Save failed: ' + e.message); }
} }
// ── DataLayer Events ────────────────────────────────────────────────────────── // -- DataLayer Events ----------------------------------------------------------
function renderEventsList() { function renderEventsList() {
const el = document.getElementById('analytics-events-list'); const el = document.getElementById('analytics-events-list');
if (!el) return; if (!el) return;
@@ -1877,7 +1878,7 @@ async function deleteEvent(idx) {
renderEventsList(); renderEventsList();
} }
// ── Pixel Tracking ──────────────────────────────────────────────────────────── // -- Pixel Tracking ------------------------------------------------------------
function renderPixelsList() { function renderPixelsList() {
const el = document.getElementById('pixels-list'); const el = document.getElementById('pixels-list');
if (!el) return; if (!el) return;
@@ -1885,7 +1886,7 @@ function renderPixelsList() {
el.innerHTML = trackingPixels.map((px, i) => { el.innerHTML = trackingPixels.map((px, i) => {
const meta = PB_META[px.platform] || PB_META.custom; const meta = PB_META[px.platform] || PB_META.custom;
const on = px.enabled !== false; 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') + '">' + 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="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>' + '<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; let scanCurrentPage = null;
function openScanPicker() { function openScanPicker() {
@@ -2084,7 +2085,7 @@ function applyScanResult(item) {
updateEBPreview(); updateEBPreview();
closeScanPicker(); closeScanPicker();
// Toast to confirm // 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>
<div id="llm-claude-fields" class="${d.provider==='claude' ? '' : 'hidden'} space-y-3 mb-4"> <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"> <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-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>
<div id="claude-key-llm-wrap"> <div id="claude-key-llm-wrap">
<label class="text-xs text-on-surface-variant block mb-1.5">API Key <label class="text-xs text-on-surface-variant block mb-1.5">API Key
@@ -2153,14 +2154,14 @@ async function loadLLMConfig() {
</div> </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"> <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-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-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-primary">lock</span>Fully private -- no external requests</p>
</div> </div>
<div id="llm-cf-fields" class="${isCF ? '' : 'hidden'} space-y-3 mb-4"> <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"> <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-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>
<div> <div>
<label class="text-xs text-on-surface-variant block mb-1.5">Account ID</label> <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 = ''; status.textContent = 'Saving...'; status.classList.remove('hidden'); status.style.color = '';
try { try {
await api('/admin/llm-config', {method:'POST', body:JSON.stringify(payload)}); 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'; status.style.color = '#4edea3';
setTimeout(loadLLMConfig, 1500); setTimeout(loadLLMConfig, 1500);
} catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; } } catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; }
@@ -2261,8 +2262,8 @@ document.querySelectorAll('.spotlight-card').forEach(card => {
}); });
// ── Book Edit ───────────────────────────────────────────────────────────────── // -- Book Edit -----------------------------------------------------------------
let _bookEditCache = {}; // id → book data from last loadBooks() let _bookEditCache = {}; // id -> book data from last loadBooks()
function openBookEdit(btn) { function openBookEdit(btn) {
const d = btn.dataset; const d = btn.dataset;
@@ -2327,7 +2328,7 @@ async function retryMetaExtraction() {
status.classList.remove('hidden'); status.classList.remove('hidden');
try { try {
await api('/admin/books/' + id + '/extract-meta', { method: 'POST' }); await api('/admin/books/' + id + '/extract-meta', { method: 'POST' });
status.textContent = '✅ طُلب الاستخراج — ستُحدَّث البيانات خلال دقيقة'; status.textContent = '✅ طُلب الاستخراج -- ستُحدَّث البيانات خلال دقيقة';
status.style.color = '#4edea3'; status.style.color = '#4edea3';
// Reload after 65s // Reload after 65s
setTimeout(() => { closeBookEdit(); loadBooks(); }, 65000); 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> </script>
@@ -2393,7 +2394,7 @@ async function retryMetaExtraction() {
<script> <script>
// ── Page Builder ───────────────────────────────────────────────────────────── // -- Page Builder -------------------------------------------------------------
let _pbPage = 'privacy'; let _pbPage = 'privacy';
let _pbMode = 'visual'; let _pbMode = 'visual';
let _pbBlocks = []; let _pbBlocks = [];
@@ -2406,7 +2407,7 @@ const PB_PAGES = {
index: { label: 'Home', url: '/' }, index: { label: 'Home', url: '/' },
}; };
// ── Switch page ─────────────────────────────────────────────────────────────── // -- Switch page ---------------------------------------------------------------
function pbSwitch(page) { function pbSwitch(page) {
_pbPage = page; _pbPage = page;
document.querySelectorAll('.pb-tab').forEach(b => { document.querySelectorAll('.pb-tab').forEach(b => {
@@ -2418,7 +2419,7 @@ function pbSwitch(page) {
pbLoad(); pbLoad();
} }
// ── Mode toggle ─────────────────────────────────────────────────────────────── // -- Mode toggle ---------------------------------------------------------------
function pbSetMode(mode) { function pbSetMode(mode) {
_pbMode = mode; _pbMode = mode;
const mv = document.getElementById('pb-mode-visual'); 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'; 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'; 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'); vh.classList.remove('hidden'); vv.classList.add('hidden');
// Sync blocks → HTML // Sync blocks -> HTML
document.getElementById('pb-html-editor').value = _pbRawHTML; document.getElementById('pb-html-editor').value = _pbRawHTML;
} }
} }
// ── Load page content ───────────────────────────────────────────────────────── // -- Load page content ---------------------------------------------------------
async function pbLoad() { async function pbLoad() {
const blocksEl = document.getElementById('pb-blocks'); const blocksEl = document.getElementById('pb-blocks');
blocksEl.innerHTML = '<div class="text-on-surface-variant text-sm p-4">Loading...</div>'; 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) { function pbParseHTML(html) {
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html'); const doc = parser.parseFromString(html, 'text/html');
@@ -2485,11 +2486,11 @@ function pbParseHTML(html) {
return blocks; return blocks;
} }
// ── Render blocks in visual editor ─────────────────────────────────────────── // -- Render blocks in visual editor -------------------------------------------
function pbRenderBlocks() { function pbRenderBlocks() {
const container = document.getElementById('pb-blocks'); const container = document.getElementById('pb-blocks');
if (!_pbBlocks.length) { 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; return;
} }
container.innerHTML = _pbBlocks.map((b, idx) => pbBlockHTML(b, idx)).join(''); container.innerHTML = _pbBlocks.map((b, idx) => pbBlockHTML(b, idx)).join('');
@@ -2555,7 +2556,7 @@ function pbBlockHTML(b, idx) {
</div>`; </div>`;
} }
// ── Block actions ───────────────────────────────────────────────────────────── // -- Block actions -------------------------------------------------------------
function pbAddBlock(type) { function pbAddBlock(type) {
const newBlock = { id: Date.now(), type, content: '', level: 'h2', ordered: false, title: '', subtitle: '', email: '' }; const newBlock = { id: Date.now(), type, content: '', level: 'h2', ordered: false, title: '', subtitle: '', email: '' };
_pbBlocks.push(newBlock); _pbBlocks.push(newBlock);
@@ -2579,7 +2580,7 @@ function pbMoveBlock(idx, dir) {
pbRenderBlocks(); pbRenderBlocks();
} }
// ── Blocks → HTML ───────────────────────────────────────────────────────────── // -- Blocks -> HTML -------------------------------------------------------------
function pbBlocksToHTML(blocks) { function pbBlocksToHTML(blocks) {
return blocks.map(b => { return blocks.map(b => {
if (b.type === 'heading') return `<${b.level||'h2'} class="text-primary font-bold mb-2">${b.content}</${b.level||'h2'}>`; 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'); }).join('\n');
} }
// ── Inject blocks into page HTML ────────────────────────────────────────────── // -- Inject blocks into page HTML ----------------------------------------------
function pbInjectBlocks(rawHTML, blocksHTML) { function pbInjectBlocks(rawHTML, blocksHTML) {
// Try to inject into existing content area // Try to inject into existing content area
const parser = new DOMParser(); const parser = new DOMParser();
@@ -2621,10 +2622,10 @@ function pbInjectBlocks(rawHTML, blocksHTML) {
const s = new XMLSerializer(); const s = new XMLSerializer();
return '<!DOCTYPE html>' + s.serializeToString(doc.documentElement); return '<!DOCTYPE html>' + s.serializeToString(doc.documentElement);
} }
return rawHTML; // fallback — unchanged return rawHTML; // fallback -- unchanged
} }
// ── Save ────────────────────────────────────────────────────────────────────── // -- Save ----------------------------------------------------------------------
async function pbSave() { async function pbSave() {
const btn = document.getElementById('pb-save-btn'); const btn = document.getElementById('pb-save-btn');
const status = document.getElementById('pb-status'); const status = document.getElementById('pb-status');
@@ -2659,7 +2660,7 @@ async function pbSave() {
btn.innerHTML = '<span class="msym text-base">save</span> Save'; btn.innerHTML = '<span class="msym text-base">save</span> Save';
} }
// ── Wire into showPage ──────────────────────────────────────────────────────── // -- Wire into showPage --------------------------------------------------------
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const origShowPage = window.showPage; const origShowPage = window.showPage;
if (origShowPage) { if (origShowPage) {