v1.8.6: branding — colors apply instantly, logo upload with size control, fix tracking width

This commit is contained in:
2026-05-28 19:48:25 +00:00
parent 881c3617da
commit 8e7b1f5938
17 changed files with 175 additions and 16 deletions
+20 -2
View File
@@ -127,6 +127,7 @@ class BrandingUpdate(BaseModel):
site_subtitle: str = None site_subtitle: str = None
primary_color: str = None primary_color: str = None
secondary_color: str = None secondary_color: str = None
logo_size: str = None
logo_url: str = None logo_url: str = None
class ClaudeKeyUpdate(BaseModel): class ClaudeKeyUpdate(BaseModel):
@@ -669,10 +670,9 @@ def admin_settings(_=Depends(require_admin)):
"qdrant_url": QDRANT_URL, "qdrant_url": QDRANT_URL,
"claude_api_configured": bool(os.getenv("CLAUDE_API_KEY", "")), "claude_api_configured": bool(os.getenv("CLAUDE_API_KEY", "")),
"cache_ttl_hours": 24, "cache_ttl_hours": 24,
"complexity_threshold_words": 30,
"confidence_threshold": 0.20, # SCORE_THRESH in brain.py "confidence_threshold": 0.20, # SCORE_THRESH in brain.py
"chunk_size": 150, "chunk_size": 150,
"chunk_overlap": 200, "chunk_overlap": 2, # sentences (≈30 words)
# ── Cloudflare R2 ───────────────────────────────────────────────────── # ── Cloudflare R2 ─────────────────────────────────────────────────────
"r2_endpoint": os.getenv("R2_ENDPOINT", ""), "r2_endpoint": os.getenv("R2_ENDPOINT", ""),
"r2_bucket": os.getenv("R2_BUCKET", ""), "r2_bucket": os.getenv("R2_BUCKET", ""),
@@ -753,6 +753,24 @@ def test_r2_connection(_=Depends(require_admin)):
except Exception as e: except Exception as e:
return {"ok": False, "detail": str(e)[:120]} return {"ok": False, "detail": str(e)[:120]}
# ── Upload Logo ────────────────────────────────────────────────────────────────
@app.post("/admin/upload-logo")
async def upload_logo(payload: dict, _=Depends(require_admin)):
import base64, os, uuid, mimetypes
data = payload.get("data","")
mime_type = payload.get("mime_type","image/png")
filename = payload.get("filename","logo.png")
if not data:
raise HTTPException(status_code=400, detail="No image data")
ext = mimetypes.guess_extension(mime_type) or ".png"
name = f"logo_{uuid.uuid4().hex[:8]}{ext}"
path = f"/opt/rag/static/uploads/{name}"
os.makedirs("/opt/rag/static/uploads", exist_ok=True)
with open(path, "wb") as f:
f.write(base64.b64decode(data))
return {"url": f"/static/uploads/{name}"}
@app.get("/admin/branding") @app.get("/admin/branding")
def get_branding(_=Depends(require_admin)): def get_branding(_=Depends(require_admin)):
with get_db() as conn: with get_db() as conn:
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.
@@ -52149,3 +52149,83 @@ $3
del del
$13 $13
unacked_mutex unacked_mutex
*5
$3
SET
$13
unacked_mutex
$32
0ac306665acd11f199e682dacbd0314b
$4
PXAT
$13
1779997512054
*2
$3
del
$13
unacked_mutex
*5
$3
SET
$13
unacked_mutex
$32
466013d05acd11f199e682dacbd0314b
$4
PXAT
$13
1779997612069
*2
$3
del
$13
unacked_mutex
*5
$3
SET
$13
unacked_mutex
$32
81fcc8c05acd11f199e682dacbd0314b
$4
PXAT
$13
1779997712082
*2
$3
del
$13
unacked_mutex
*5
$3
SET
$13
unacked_mutex
$32
bd99e12e5acd11f199e682dacbd0314b
$4
PXAT
$13
1779997812097
*2
$3
del
$13
unacked_mutex
*5
$3
SET
$13
unacked_mutex
$32
f936c5125acd11f199e682dacbd0314b
$4
PXAT
$13
1779997912111
*2
$3
del
$13
unacked_mutex
+67 -12
View File
@@ -484,12 +484,23 @@ a[role=button]:active {
<input id="b-subtitle" class="w-full bg-surface-high border border-outline-variant rounded-xl px-4 py-3 text-sm text-on-surface outline-none focus:border-primary transition-colors"/> <input id="b-subtitle" class="w-full bg-surface-high border border-outline-variant rounded-xl px-4 py-3 text-sm text-on-surface outline-none focus:border-primary transition-colors"/>
</div> </div>
<div> <div>
<label class="text-xs text-on-surface-variant mb-2 block">Logo URL</label> <label class="text-xs text-on-surface-variant mb-2 block">Logo</label>
<input id="b-logo" placeholder="https://example.com/logo.png" class="w-full bg-surface-high border border-outline-variant rounded-xl px-4 py-3 text-sm text-on-surface outline-none focus:border-primary transition-colors"/> <div class="flex gap-2 mb-2">
</div> <input id="b-logo" placeholder="https://example.com/logo.png"
<div id="logo-preview" class="hidden"> class="flex-1 bg-surface-high border border-outline-variant rounded-xl px-4 py-3 text-sm text-on-surface outline-none focus:border-primary transition-colors"
<label class="text-xs text-on-surface-variant mb-2 block">Preview</label> oninput="updateLogoPreview()"/>
<img id="logo-img" class="h-12 rounded-xl" /> <label class="glass glass-hover px-3 py-2 rounded-xl text-xs text-primary font-bold cursor-pointer flex items-center gap-1 shrink-0">
<span class="msym text-sm">upload</span> Upload
<input type="file" accept="image/*" class="hidden" onchange="uploadLogo(this)"/>
</label>
</div>
<img id="b-logo-preview" class="hidden rounded-xl mb-2 border border-outline-variant" style="max-width:200px"/>
<div class="flex items-center gap-3">
<label class="text-xs text-on-surface-variant shrink-0">Size</label>
<input type="range" id="b-logo-size" min="20" max="120" value="40" step="4"
class="flex-1 accent-primary" oninput="updateLogoPreview()"/>
<span id="b-logo-size-val" class="text-xs text-on-surface-variant w-10 text-right">40px</span>
</div>
</div> </div>
</div> </div>
<div class="glass rounded-2xl p-6 space-y-5"> <div class="glass rounded-2xl p-6 space-y-5">
@@ -515,7 +526,7 @@ a[role=button]:active {
<div id="preview-secondary" class="flex-1 h-10 rounded-lg"></div> <div id="preview-secondary" class="flex-1 h-10 rounded-lg"></div>
</div> </div>
</div> </div>
<p class="text-xs text-on-surface-variant">Changes apply on next page load after saving.</p> <p class="text-xs text-on-surface-variant">Colors apply instantly after saving.</p>
</div> </div>
</div> </div>
</section> </section>
@@ -589,7 +600,6 @@ curl -X POST https://rag.afaqzaid.com/ingest-url \
<div id="settings-llm" class="glass rounded-2xl p-6 mt-6"></div> <div id="settings-llm" class="glass rounded-2xl p-6 mt-6"></div>
<div id="settings-r2" class="glass rounded-2xl p-6 mt-6"></div> <div id="settings-r2" class="glass rounded-2xl p-6 mt-6"></div>
</section> </section>
</div>
</section> </section>
<!-- CLAUDE --> <!-- CLAUDE -->
@@ -1300,13 +1310,13 @@ async function loadSettings() {
const d = await api('/admin/settings'); const d = await api('/admin/settings');
document.getElementById('settings-ai').innerHTML = ` document.getElementById('settings-ai').innerHTML = `
<h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2"><span class="msym text-base">smart_toy</span>AI Configuration</h3> <h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2"><span class="msym text-base">smart_toy</span>AI Configuration</h3>
${[['Local LLM',d.ollama_model,''],['Claude API',d.claude_api_configured?'✓ Configured':'✗ Not set',d.claude_api_configured?'text-emerald-400':'text-red-400'],['Complexity Threshold',d.complexity_threshold_words+' words',''],['Confidence Threshold',d.confidence_threshold,''],['Cache TTL',d.cache_ttl_hours+'h','']].map(([l,v,cls])=>` ${[['Local LLM',d.ollama_model,''],['Claude API',d.claude_api_configured?'✓ Configured':'✗ Not set',d.claude_api_configured?'text-emerald-400':'text-red-400'],['Confidence Threshold',d.confidence_threshold,''],['Cache TTL',d.cache_ttl_hours+'h','']].map(([l,v,cls])=>`
<div class="flex justify-between items-center py-3 border-b border-outline-variant/20 last:border-0"> <div class="flex justify-between items-center py-3 border-b border-outline-variant/20 last:border-0">
<span class="text-sm text-on-surface-variant">${l}</span><span class="text-xs font-mono glass px-2 py-1 rounded-lg ${cls||'text-primary'}">${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 ${cls||'text-primary'}">${v}</span>
</div>`).join('')}`; </div>`).join('')}`;
document.getElementById('settings-system').innerHTML = ` document.getElementById('settings-system').innerHTML = `
<h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2"><span class="msym text-base">build</span>Ingestion Settings</h3> <h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2"><span class="msym text-base">build</span>Ingestion Settings</h3>
${[['Chunk Size',d.chunk_size+' words'],['Chunk Overlap',d.chunk_overlap+' words'],['Qdrant URL',d.qdrant_url],['Ollama URL',d.ollama_url]].map(([l,v])=>` ${[['Chunk Size',d.chunk_size+' words'],['Chunk Overlap',d.chunk_overlap+' sentences'],['Qdrant URL',d.qdrant_url],['Ollama URL',d.ollama_url]].map(([l,v])=>`
<div class="flex justify-between items-center py-3 border-b border-outline-variant/20 last:border-0"> <div class="flex justify-between items-center py-3 border-b border-outline-variant/20 last:border-0">
<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('')}`;
@@ -1431,6 +1441,39 @@ async function testR2Connection() {
} catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; } } catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; }
} }
function updateLogoPreview() {
const url = document.getElementById('b-logo')?.value;
const size = document.getElementById('b-logo-size')?.value || 40;
document.getElementById('b-logo-size-val').textContent = size + 'px';
const img = document.getElementById('b-logo-preview');
if (url) { img.src = url; img.style.height = size + 'px'; img.classList.remove('hidden'); }
else img.classList.add('hidden');
}
async function uploadLogo(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
try {
const base64 = e.target.result.split(',')[1];
const mime = file.type;
const r = await fetch('/admin/upload-logo', {
method: 'POST',
headers: {'Content-Type':'application/json','Authorization':'Bearer '+JWT_TOKEN},
body: JSON.stringify({data: base64, mime_type: mime, filename: file.name})
});
const d = await r.json();
if (d.url) {
document.getElementById('b-logo').value = d.url;
updateLogoPreview();
toast('Logo uploaded!', 'success');
}
} catch(e) { toast('Upload failed: '+e.message); }
};
reader.readAsDataURL(file);
}
async function loadBranding() { async function loadBranding() {
try { try {
const d = await api('/admin/branding'); const d = await api('/admin/branding');
@@ -1443,6 +1486,13 @@ async function loadBranding() {
document.getElementById('b-secondary-hex').value = d.secondary_color||'#95d3ba'; document.getElementById('b-secondary-hex').value = d.secondary_color||'#95d3ba';
document.getElementById('preview-primary').style.background = d.primary_color||'#4edea3'; document.getElementById('preview-primary').style.background = d.primary_color||'#4edea3';
document.getElementById('preview-secondary').style.background = d.secondary_color||'#95d3ba'; document.getElementById('preview-secondary').style.background = d.secondary_color||'#95d3ba';
// Apply colors live to admin CSS variables
if(d.primary_color) document.documentElement.style.setProperty('--primary', d.primary_color);
if(d.secondary_color) document.documentElement.style.setProperty('--secondary', d.secondary_color);
// Logo
if(d.logo_url) document.getElementById('b-logo').value = d.logo_url;
if(d.logo_size) document.getElementById('b-logo-size').value = d.logo_size;
if(d.logo_url) { const img=document.getElementById('b-logo-preview'); img.src=d.logo_url; img.style.height=(d.logo_size||40)+'px'; img.classList.remove('hidden'); }
if (d.logo_url) { document.getElementById('logo-img').src=d.logo_url; document.getElementById('logo-preview').classList.remove('hidden'); } if (d.logo_url) { document.getElementById('logo-img').src=d.logo_url; document.getElementById('logo-preview').classList.remove('hidden'); }
document.getElementById('b-primary').oninput = function(){ document.getElementById('b-primary-hex').value=this.value; document.getElementById('preview-primary').style.background=this.value; }; document.getElementById('b-primary').oninput = function(){ document.getElementById('b-primary-hex').value=this.value; document.getElementById('preview-primary').style.background=this.value; };
document.getElementById('b-secondary').oninput = function(){ document.getElementById('b-secondary-hex').value=this.value; document.getElementById('preview-secondary').style.background=this.value; }; document.getElementById('b-secondary').oninput = function(){ document.getElementById('b-secondary-hex').value=this.value; document.getElementById('preview-secondary').style.background=this.value; };
@@ -1454,8 +1504,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, 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'); 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); } } catch(e) { alert('Error: '+e.message); }
} }
+8 -2
View File
@@ -371,8 +371,14 @@ let currentUser = null;
// ── Branding ────────────────────────────────────────────────────────────────── // ── Branding ──────────────────────────────────────────────────────────────────
fetch('/branding').then(r=>r.json()).then(b=>{ fetch('/branding').then(r=>r.json()).then(b=>{
if(b.site_name){document.title=b.site_name;document.getElementById('brand-name').textContent=b.site_name;} if(b.site_name){document.title=b.site_name;const bn=document.getElementById('brand-name');if(bn)bn.textContent=b.site_name;}
if(b.site_subtitle) document.getElementById('brand-sub').textContent=b.site_subtitle; if(b.site_subtitle){const bs=document.getElementById('brand-sub');if(bs)bs.textContent=b.site_subtitle;}
if(b.primary_color) document.documentElement.style.setProperty('--primary', b.primary_color);
if(b.secondary_color) document.documentElement.style.setProperty('--secondary', b.secondary_color);
if(b.logo_url){
const logo=document.getElementById('brand-logo');
if(logo){logo.src=b.logo_url;logo.style.height=(b.logo_size||'40')+'px';logo.classList.remove('hidden');}
}
}).catch(()=>{}); }).catch(()=>{});
// ── Counter animation ───────────────────────────────────────────────────────── // ── Counter animation ─────────────────────────────────────────────────────────