From 9ce9efc82e4ac50bd5d86244c4649d740428a571 Mon Sep 17 00:00:00 2001
From: Jino Jose
Date: Tue, 30 Jun 2026 10:51:41 +0530
Subject: [PATCH] Package portal and close UI API gaps
---
ansible/roles/cezen-backend/files/main.py | 167 +++
ansible/roles/cezen-nginx/tasks/main.yml | 6 +-
cezen-portal/admin.html | 621 +++++++++
cezen-portal/agents.html | 1156 +++++++++++++++++
cezen-portal/analytics.html | 595 +++++++++
cezen-portal/api-playground.html | 644 ++++++++++
cezen-portal/apikeys.html | 386 ++++++
cezen-portal/appliance.html | 386 ++++++
cezen-portal/audit.html | 299 +++++
cezen-portal/auth.js | 200 +++
cezen-portal/benchmark.html | 403 ++++++
cezen-portal/branding.js | 359 ++++++
cezen-portal/change-password.html | 113 ++
cezen-portal/chat-multi.html | 1192 ++++++++++++++++++
cezen-portal/chatrooms.html | 721 +++++++++++
cezen-portal/connectors.html | 748 +++++++++++
cezen-portal/console.html | 266 ++++
cezen-portal/dashboard.html | 810 ++++++++++++
cezen-portal/documents.html | 1071 ++++++++++++++++
cezen-portal/evals.html | 852 +++++++++++++
cezen-portal/faq.html | 393 ++++++
cezen-portal/feedback.html | 404 ++++++
cezen-portal/glossary.html | 290 +++++
cezen-portal/guardrails.html | 567 +++++++++
cezen-portal/index.html | 880 +++++++++++++
cezen-portal/knowledge.html | 686 ++++++++++
cezen-portal/login.html | 214 ++++
cezen-portal/meeting.html | 946 ++++++++++++++
cezen-portal/model-compare.html | 789 ++++++++++++
cezen-portal/models-admin.html | 605 +++++++++
cezen-portal/models.html | 271 ++++
cezen-portal/notifications.html | 442 +++++++
cezen-portal/prompt-studio.html | 713 +++++++++++
cezen-portal/prompts.html | 454 +++++++
cezen-portal/quickstart.html | 564 +++++++++
cezen-portal/rag-quality.html | 565 +++++++++
cezen-portal/router.html | 777 ++++++++++++
cezen-portal/schedules.html | 722 +++++++++++
cezen-portal/security.html | 318 +++++
cezen-portal/settings.html | 457 +++++++
cezen-portal/style.css | 1400 +++++++++++++++++++++
cezen-portal/system-status.json | 13 +
cezen-portal/teams.html | 555 ++++++++
cezen-portal/tool-chromadb.html | 190 +++
cezen-portal/tool-dcgm.html | 212 ++++
cezen-portal/tool-fastapi.html | 196 +++
cezen-portal/tool-jupyter.html | 202 +++
cezen-portal/tool-langchain.html | 180 +++
cezen-portal/tool-nvtop.html | 201 +++
cezen-portal/tool-ollama.html | 198 +++
cezen-portal/tool-openwebui.html | 196 +++
cezen-portal/tool-qlora.html | 193 +++
cezen-portal/training.html | 730 +++++++++++
cezen-portal/troubleshooting.html | 422 +++++++
cezen-portal/update-system-status.sh | 78 ++
cezen-portal/usecases.html | 660 ++++++++++
cezen-portal/users.html | 379 ++++++
cezen-portal/whats-new.html | 322 +++++
cezen-portal/workflows.html | 978 ++++++++++++++
59 files changed, 29355 insertions(+), 2 deletions(-)
create mode 100644 cezen-portal/admin.html
create mode 100644 cezen-portal/agents.html
create mode 100644 cezen-portal/analytics.html
create mode 100644 cezen-portal/api-playground.html
create mode 100644 cezen-portal/apikeys.html
create mode 100644 cezen-portal/appliance.html
create mode 100644 cezen-portal/audit.html
create mode 100644 cezen-portal/auth.js
create mode 100644 cezen-portal/benchmark.html
create mode 100644 cezen-portal/branding.js
create mode 100644 cezen-portal/change-password.html
create mode 100644 cezen-portal/chat-multi.html
create mode 100644 cezen-portal/chatrooms.html
create mode 100644 cezen-portal/connectors.html
create mode 100644 cezen-portal/console.html
create mode 100644 cezen-portal/dashboard.html
create mode 100644 cezen-portal/documents.html
create mode 100644 cezen-portal/evals.html
create mode 100644 cezen-portal/faq.html
create mode 100644 cezen-portal/feedback.html
create mode 100644 cezen-portal/glossary.html
create mode 100644 cezen-portal/guardrails.html
create mode 100644 cezen-portal/index.html
create mode 100644 cezen-portal/knowledge.html
create mode 100644 cezen-portal/login.html
create mode 100644 cezen-portal/meeting.html
create mode 100644 cezen-portal/model-compare.html
create mode 100644 cezen-portal/models-admin.html
create mode 100644 cezen-portal/models.html
create mode 100644 cezen-portal/notifications.html
create mode 100644 cezen-portal/prompt-studio.html
create mode 100644 cezen-portal/prompts.html
create mode 100644 cezen-portal/quickstart.html
create mode 100644 cezen-portal/rag-quality.html
create mode 100644 cezen-portal/router.html
create mode 100644 cezen-portal/schedules.html
create mode 100644 cezen-portal/security.html
create mode 100644 cezen-portal/settings.html
create mode 100644 cezen-portal/style.css
create mode 100644 cezen-portal/system-status.json
create mode 100644 cezen-portal/teams.html
create mode 100644 cezen-portal/tool-chromadb.html
create mode 100644 cezen-portal/tool-dcgm.html
create mode 100644 cezen-portal/tool-fastapi.html
create mode 100644 cezen-portal/tool-jupyter.html
create mode 100644 cezen-portal/tool-langchain.html
create mode 100644 cezen-portal/tool-nvtop.html
create mode 100644 cezen-portal/tool-ollama.html
create mode 100644 cezen-portal/tool-openwebui.html
create mode 100644 cezen-portal/tool-qlora.html
create mode 100644 cezen-portal/training.html
create mode 100644 cezen-portal/troubleshooting.html
create mode 100755 cezen-portal/update-system-status.sh
create mode 100644 cezen-portal/usecases.html
create mode 100644 cezen-portal/users.html
create mode 100644 cezen-portal/whats-new.html
create mode 100644 cezen-portal/workflows.html
diff --git a/ansible/roles/cezen-backend/files/main.py b/ansible/roles/cezen-backend/files/main.py
index f1a22ad..c238201 100644
--- a/ansible/roles/cezen-backend/files/main.py
+++ b/ansible/roles/cezen-backend/files/main.py
@@ -2630,6 +2630,32 @@ async def guardrail_log(limit: int = 100, admin: dict = Depends(admin_only)):
db.close()
return [dict(r) for r in rows]
+@app.get("/api/guardrails/flags")
+async def guardrail_flags(limit: int = 20, admin: dict = Depends(admin_only)):
+ db = get_db()
+ rows = db.execute(
+ """SELECT gl.*, gr.label
+ FROM guardrail_log gl
+ LEFT JOIN guardrail_rules gr ON gr.id=gl.rule_id
+ ORDER BY gl.logged_at DESC LIMIT ?""",
+ (limit,)
+ ).fetchall()
+ db.close()
+ flags = []
+ for r in rows:
+ action = r["action"] or ""
+ severity = "high" if action == "block" else "medium" if action == "redact" else "low"
+ flags.append({
+ "id": r["id"],
+ "severity": severity,
+ "excerpt": r["pattern"],
+ "user": r["username"],
+ "ts": r["logged_at"],
+ "rule": r["label"] or r["pattern"] or r["rule_type"],
+ "action": action,
+ })
+ return {"flags": flags}
+
# ── Model Benchmarking ────────────────────────────────────────────────────────
class BenchmarkRun(BaseModel):
@@ -3294,6 +3320,80 @@ async def delete_doc_job(job_id: int, user: dict = Depends(current_user)):
# ── Multimodal Chat ────────────────────────────────────────────────────────────
+@app.post("/api/chat")
+async def chat_completion(body: dict, user: dict = Depends(current_user)):
+ import urllib.request as _ur
+ model = body.get("model") or "llama3"
+ message = (body.get("message") or body.get("prompt") or "").strip()
+ system_prompt = (body.get("system_prompt") or "").strip()
+ if not message:
+ raise HTTPException(400, "message required")
+
+ messages = []
+ if system_prompt:
+ messages.append({"role": "system", "content": system_prompt})
+ messages.append({"role": "user", "content": message})
+
+ routed = _select_router_model(message, model, {"content_type": "text", "user_role": user.get("role", "")})
+ options = {}
+ if body.get("temperature") is not None:
+ options["temperature"] = float(body.get("temperature"))
+ if body.get("max_tokens") is not None:
+ options["num_predict"] = int(body.get("max_tokens"))
+ elif routed.get("max_tokens"):
+ options["num_predict"] = int(routed["max_tokens"])
+
+ payload = {"model": routed["model"], "messages": messages, "stream": False}
+ if options:
+ payload["options"] = options
+
+ started = datetime.now(timezone.utc)
+ try:
+ req = _ur.Request(
+ f"{OLLAMA_URL}/api/chat",
+ data=json.dumps(payload).encode(),
+ headers={"Content-Type": "application/json"},
+ method="POST"
+ )
+ with _ur.urlopen(req, timeout=300) as resp:
+ data = json.loads(resp.read())
+ response_text = data.get("message", {}).get("content", "")
+ except Exception as e:
+ db = get_db()
+ try:
+ db.execute(
+ "INSERT INTO query_log (user_id,username,model,prompt_tokens,completion_tokens,duration_ms,status,logged_at) VALUES (?,?,?,?,?,?,?,?)",
+ (user["sub"], user["username"], routed["model"], _estimate_tokens(message), 0, 0, "error", utcnow())
+ )
+ db.commit()
+ finally:
+ db.close()
+ raise HTTPException(502, f"Ollama error: {e}")
+
+ duration_ms = int((datetime.now(timezone.utc) - started).total_seconds() * 1000)
+ prompt_tokens = _estimate_tokens(system_prompt + " " + message)
+ completion_tokens = _estimate_tokens(response_text)
+ db = get_db()
+ try:
+ db.execute(
+ "INSERT INTO query_log (user_id,username,model,prompt_tokens,completion_tokens,duration_ms,status,logged_at) VALUES (?,?,?,?,?,?,?,?)",
+ (user["sub"], user["username"], routed["model"], prompt_tokens, completion_tokens, duration_ms, "ok", utcnow())
+ )
+ db.commit()
+ finally:
+ db.close()
+ return {
+ "response": response_text,
+ "model": routed["model"],
+ "route": routed.get("route", "default"),
+ "usage": {
+ "prompt_tokens": prompt_tokens,
+ "completion_tokens": completion_tokens,
+ "total_tokens": prompt_tokens + completion_tokens,
+ },
+ "duration_ms": duration_ms,
+ }
+
@app.post("/api/multimodal/chat")
async def multimodal_chat(body: dict, user: dict = Depends(current_user)):
"""
@@ -4191,6 +4291,73 @@ async def list_schedule_runs(job_id: int, user=Depends(current_user)):
db.close()
return [dict(r) for r in rows]
+@app.get("/api/jobs/failed")
+async def failed_jobs(limit: int = 10, admin: dict = Depends(admin_only)):
+ db = get_db()
+ items = []
+ for r in db.execute(
+ "SELECT id,orig_name,error_msg,created_at,finished_at FROM doc_jobs WHERE status='error' ORDER BY COALESCE(finished_at,created_at) DESC LIMIT ?",
+ (limit,)
+ ).fetchall():
+ items.append({"id": f"doc:{r['id']}", "type": "doc", "name": r["orig_name"], "error": r["error_msg"] or "Document job failed", "ts": r["finished_at"] or r["created_at"]})
+ for r in db.execute(
+ "SELECT id,agent_name,error_msg,created_at,finished_at FROM agent_runs WHERE status='error' ORDER BY COALESCE(finished_at,created_at) DESC LIMIT ?",
+ (limit,)
+ ).fetchall():
+ items.append({"id": f"agent:{r['id']}", "type": "agent", "name": r["agent_name"], "error": r["error_msg"] or "Agent run failed", "ts": r["finished_at"] or r["created_at"]})
+ for r in db.execute(
+ "SELECT id,job_id,job_name,error_msg,started_at,finished_at FROM scheduled_job_runs WHERE status='error' ORDER BY COALESCE(finished_at,started_at) DESC LIMIT ?",
+ (limit,)
+ ).fetchall():
+ items.append({"id": f"sched:{r['job_id']}", "type": "sched", "name": r["job_name"], "error": r["error_msg"] or "Scheduled job failed", "ts": r["finished_at"] or r["started_at"]})
+ for r in db.execute(
+ "SELECT id,name,status,created_at,finished_at FROM training_jobs WHERE status='error' ORDER BY COALESCE(finished_at,created_at) DESC LIMIT ?",
+ (limit,)
+ ).fetchall():
+ items.append({"id": f"training:{r['id']}", "type": "train", "name": r["name"], "error": "Training job failed", "ts": r["finished_at"] or r["created_at"]})
+ db.close()
+ items.sort(key=lambda x: x.get("ts") or "", reverse=True)
+ return {"jobs": items[:limit]}
+
+@app.post("/api/jobs/{job_key:path}/retry")
+async def retry_failed_job(job_key: str, admin: dict = Depends(admin_only)):
+ if ":" not in job_key:
+ raise HTTPException(400, "job id must include type prefix")
+ kind, raw_id = job_key.split(":", 1)
+ try:
+ jid = int(raw_id)
+ except ValueError:
+ raise HTTPException(400, "invalid job id")
+
+ db = get_db()
+ try:
+ if kind == "doc":
+ row = db.execute("SELECT id FROM doc_jobs WHERE id=?", (jid,)).fetchone()
+ if not row:
+ raise HTTPException(404, "Document job not found")
+ db.execute("UPDATE doc_jobs SET status='pending', error_msg=NULL, finished_at=NULL WHERE id=?", (jid,))
+ db.commit()
+ asyncio.create_task(_run_doc_job(jid))
+ return {"ok": True, "message": "Document job queued"}
+ if kind == "agent":
+ row = db.execute("SELECT id FROM agent_runs WHERE id=?", (jid,)).fetchone()
+ if not row:
+ raise HTTPException(404, "Agent run not found")
+ db.execute("UPDATE agent_runs SET status='pending', error_msg=NULL, finished_at=NULL WHERE id=?", (jid,))
+ db.commit()
+ asyncio.create_task(_execute_agent(jid))
+ return {"ok": True, "message": "Agent run queued"}
+ if kind == "sched":
+ row = db.execute("SELECT id FROM scheduled_jobs WHERE id=?", (jid,)).fetchone()
+ if not row:
+ raise HTTPException(404, "Scheduled job not found")
+ loop = asyncio.get_event_loop()
+ loop.run_in_executor(None, _run_scheduled_job_sync, jid)
+ return {"ok": True, "message": "Scheduled job triggered"}
+ finally:
+ db.close()
+ raise HTTPException(400, f"Retry not supported for job type: {kind}")
+
# ── Notifications ─────────────────────────────────────────────────────────────
diff --git a/ansible/roles/cezen-nginx/tasks/main.yml b/ansible/roles/cezen-nginx/tasks/main.yml
index be59f70..1fca52d 100644
--- a/ansible/roles/cezen-nginx/tasks/main.yml
+++ b/ansible/roles/cezen-nginx/tasks/main.yml
@@ -3,7 +3,9 @@
- name: Install Nginx
apt:
- name: nginx
+ name:
+ - nginx
+ - rsync
state: present
update_cache: yes
@@ -17,7 +19,7 @@
- name: Sync portal static files
synchronize:
- src: "{{ playbook_dir }}/../../../cezen-portal/"
+ src: "{{ playbook_dir }}/../cezen-portal/"
dest: /opt/cezen/portal/
delete: yes
recursive: yes
diff --git a/cezen-portal/admin.html b/cezen-portal/admin.html
new file mode 100644
index 0000000..4b3d235
--- /dev/null
+++ b/cezen-portal/admin.html
@@ -0,0 +1,621 @@
+
+
+
+
+
+ Admin Guide — Nexus One AI Portal
+
+
+
+
+
+
+
+
Admin Guide
+
System Administration Reference
+
For the person managing the Nexus One AI server. Commands to restart services, manage users, add models, check health, and pull logs — all in one place.
+
+
+
+
+
+ 🔐
This page is for system administrators only. All commands below require SSH access to the server. If you're an end user looking for help, visit
Troubleshooting or
FAQ instead.
+
+
+
+
Quick reference — most common admin tasks
+
+
+
+
Restart all services
+
sudo systemctl restart ollama open-webui chromadb jupyter
+
Copy
+
+
+
+
Check all service status
+
sudo systemctl status ollama open-webui chromadb jupyter
+
Copy
+
+
+
+
Check disk space
+
df -h /
+
Copy
+
+
+
+
Check GPU status
+
nvidia-smi
+
Copy
+
+
+
+
List installed models
+
ollama list
+
Copy
+
+
+
+
View live system logs
+
sudo journalctl -f -u ollama -u open-webui
+
Copy
+
+
+
+
+
+
Service management
+
All Nexus One AI services run as systemd units. Replace SERVICE below with: ollama, open-webui, chromadb, or jupyter.
+
+
+
+
+
+
Start a service
+
Use when a service is stopped and needs to be brought up.
+
+
+
sudo systemctl start SERVICE
+
Copy
+
+
+
+
+
+
Stop a service
+
Gracefully stops the service. Active users will be disconnected.
+
+
+
sudo systemctl stop SERVICE
+
Copy
+
+
+
+
+
+
Restart a service
+
Stop then start. Use when a service is misbehaving or after a config change.
+
+
+
sudo systemctl restart SERVICE
+
Copy
+
+
+
+
+
+
Check service status
+
Shows whether the service is running, the last few log lines, and any errors.
+
+
+
sudo systemctl status SERVICE
+
Copy
+
+
+
+
+
+
Enable auto-start on boot
+
Ensures the service starts automatically if the server is rebooted.
+
+
+
sudo systemctl enable SERVICE
+
Copy
+
+
+
+
+
+
Restart all Cezen services
+
Restart everything at once. Useful after a server update or if multiple services are unresponsive.
+
+
+
sudo systemctl restart ollama open-webui chromadb jupyter
+
Copy
+
+
+
+
+
+
+
Service ports and endpoints
+
+
+
+
+ Service
+ Port
+ URL
+ Notes
+
+
+
+
+ Open WebUI
+ 3000
+ http://ai.local:3001
+ Main user-facing chat interface
+
+
+ Ollama API
+ 11434
+ http://ai.local:11434
+ Model inference API — do not expose externally
+
+
+ ChromaDB
+ 8000
+ http://ai.local:8000
+ Vector database API — do not expose externally
+
+
+ Jupyter
+ 8888
+ http://ai.local:8888
+ Notebook server — restrict to developer users only
+
+
+ FastAPI
+ 8080
+ http://ai.local:8080
+ Custom API layer — if deployed
+
+
+
+
+
+
+
Model management
+
+
+
+
+
List installed models
+
Shows all models currently available on the system with their sizes.
+
+
+
+
+
+
+
Pull (install) a new model
+
Downloads and installs a model. Examples: llama3.1:8b, mistral:7b, gemma2:9b. Requires the model files to be available on your network or internet (if connected).
+
+
+
ollama pull MODEL_NAME
+
Copy
+
+
+
+
+
+
Remove a model
+
Deletes a model from disk to free up space. Make sure no users are actively using it.
+
+
+
ollama rm MODEL_NAME
+
Copy
+
+
+
+
+
+
Show model info
+
Displays the model's parameters, context length, and configuration.
+
+
+
ollama show MODEL_NAME
+
Copy
+
+
+
+
+
+
Check what's currently loaded in GPU
+
Shows running Ollama processes and their VRAM usage.
+
+
+
+
+
+
+
Model storage location
+
All model files are stored here. Monitor this directory for disk usage.
+
+
+
ls -lh /usr/share/ollama/.ollama/models/
+
Copy
+
+
+
+
+
+
+
User management (Open WebUI)
+
User accounts are managed through the Open WebUI admin panel at http://ai.local:3001/admin/users . Log in with your admin account and use the panel for most user tasks. The commands below are for bulk operations or when the UI is unavailable.
+
+
+
+
+
+
Open user admin panel
+
The primary way to create, disable, and manage user accounts. Open this URL in a browser while connected to the internal network.
+
+
+
http://ai.local:3001/admin/users
+
Copy
+
+
+
+
+
+
Open WebUI database location
+
User data is stored in a SQLite database at this path. Back this up regularly.
+
+
+
/opt/open-webui/data/webui.db
+
Copy
+
+
+
+
+
+
Back up Nexus One AI data
+
Creates a local archive of Cezen data, settings, audit logs, and uploaded working files.
+
+
+
sudo bash /opt/cezen/scripts/cezen-backup.sh backup
+
Copy
+
+
+
+
+
+
+
System health & monitoring
+
+
+
+
+
GPU status snapshot
+
Shows GPU utilisation, VRAM used/free, temperature, and power draw for all GPUs.
+
+
+
+
+
+
+
Live GPU monitor
+
Refreshes every second. Good for watching GPU load while a model is running. Press Ctrl+C to exit.
+
+
+
watch -n1 nvidia-smi
+
Copy
+
+
+
+
+
+
Full GPU health diagnostic
+
Runs DCGM's built-in diagnostic suite. Takes ~2 minutes. Red results indicate hardware issues to escalate.
+
+
+
dcgmi diag -r 1
+
Copy
+
+
+
+
+
+
Disk space usage
+
Check overall disk space. Watch /: models, databases, and logs all grow over time.
+
+
+
+
+
+
+
Find what's using the most disk space
+
Shows the 10 largest directories under /opt — useful when disk space runs low.
+
+
+
sudo du -sh /opt/* | sort -rh | head -10
+
Copy
+
+
+
+
+
+
CPU and memory overview
+
Live system resource usage — CPU load, RAM, swap. Press Q to exit.
+
+
+
+
+
+
+
Check server uptime
+
Shows how long the server has been running and current load averages.
+
+
+
+
+
+
+
+
Log access
+
+
+
+
+
Live logs — all Cezen services
+
Streams live log output from Ollama and Open WebUI together. Useful for diagnosing live issues. Press Ctrl+C to exit.
+
+
+
sudo journalctl -f -u ollama -u open-webui
+
Copy
+
+
+
+
+
+
Last 100 lines — Ollama
+
Recent Ollama log output. Check here first when models aren't loading or inference is failing.
+
+
+
sudo journalctl -u ollama -n 100 --no-pager
+
Copy
+
+
+
+
+
+
Last 100 lines — Open WebUI
+
Recent Open WebUI log output. Check here when users can't log in or the interface is not loading.
+
+
+
sudo journalctl -u open-webui -n 100 --no-pager
+
Copy
+
+
+
+
+
+
Logs since last boot
+
All logs from the current boot session — useful after a server restart to check everything came up cleanly.
+
+
+
sudo journalctl -b -u ollama -u open-webui -u chromadb -u jupyter
+
Copy
+
+
+
+
+
+
Search logs for errors
+
Filters log output to show only error-level entries — faster than reading everything.
+
+
+
sudo journalctl -u ollama -u open-webui -p err --no-pager
+
Copy
+
+
+
+
+
+
+
Common procedures
+
+
+
+ 🔄
+ Safe server reboot
+
+
+
Before rebooting, notify users and allow active sessions to finish. All systemd services with enabled status will restart automatically on boot.
+
+ Notify users that the system will restart in 10 minutes
+ Wait for active model requests to complete: watch ollama ps Copy
+ Reboot the server: sudo reboot Copy
+ After reboot, verify all services are running: sudo systemctl status ollama open-webui chromadb jupyter Copy
+ Open http://ai.local:3001 and confirm the chat interface loads
+
+
+
+
+
+
+ 🧠
+ Adding a new AI model
+
+
+
Models are added via the Ollama CLI. On an air-gapped system, copy the model file to the server first.
+
+ Check available disk space: df -h / Copy — ensure at least 2× the model size is free
+ Pull the model (if internet-connected): ollama pull llama3.1:8b Copy
+ For air-gapped install, copy the GGUF file then: ollama create mymodel -f /path/to/Modelfile Copy
+ Verify it appears: ollama list Copy
+ Test it loads: ollama run MODEL_NAME "Hello" Copy
+ The model will appear automatically in Open WebUI's model selector — no restart needed
+
+
+
+
+
+
+ 🗑️
+ Freeing up disk space
+
+
+
Disk space is most commonly consumed by model files, ChromaDB data, and Jupyter notebooks. Check each before deleting anything.
+
+ Find what's using the most space: sudo du -sh /opt/* | sort -rh | head -10 Copy
+ List models with sizes: ollama list Copy
+ Remove unused models (confirm with users first): ollama rm MODEL_NAME Copy
+ Clear old journal logs (keeps last 7 days): sudo journalctl --vacuum-time=7d Copy
+ Verify space recovered: df -h / Copy
+
+
+
+
+
+
+ 🔑
+ Resetting an admin password (Open WebUI)
+
+
+
If the admin password is lost, reset it via the Open WebUI CLI while the service is stopped.
+
+ Stop Open WebUI: sudo systemctl stop open-webui Copy
+ Run the reset command: sudo open-webui reset-admin-password --email admin@example.com --password newpassword Copy
+ Restart Open WebUI: sudo systemctl start open-webui Copy
+ Log in with the new password and immediately change it to something secure
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cezen-portal/agents.html b/cezen-portal/agents.html
new file mode 100644
index 0000000..351059b
--- /dev/null
+++ b/cezen-portal/agents.html
@@ -0,0 +1,1156 @@
+
+
+
+
+
+ Agent Builder — Nexus One AI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🤖
+
Agent Builder
+
+ Chain prompts and AI steps into automated pipelines. Each step's output feeds the next — build multi-step workflows that run with one click.
+
+
+ 📄 Report Generator
+ 🏷️ Classify & Summarise
+ ❓ Q&A Extractor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🛡️ Governance & Approval
+ Controls how autonomously this agent can act
+
+
+
+
+
Autonomy Level
+
+
+ 🔍
+ Observe
+ Runs in background, logs only — no output surfaced to users
+
+
+ 💡
+ Advise
+ Shows recommendations — human decides whether to act
+
+
+ ✋
+ Act with Approval
+ Runs pipeline, waits for human sign-off before finalising
+
+
+ ⚡
+ Act Automatically
+ Runs and applies output without confirmation — use with care
+
+
+
+
+
+
+
+
+ Approvers
+
+ Admins only
+ Any logged-in user
+ Agent owner only
+
+
+
+ Approval Timeout
+
+ 1 hour — then auto-reject
+ 4 hours
+ 24 hours
+ 3 days
+ No timeout
+
+
+
+
+
+
+
+
+
+ Max Auto-Runs Per Day
+
+
+
+ Cool-down Between Runs
+
+ None
+ 1 minute
+ 5 minutes
+ 1 hour
+
+
+
+
+
+
+
+
Permissions — Tools This Agent May Use
+
+
+
+
+
📚 Read Knowledge Base
+
RAG queries against allowed collections
+
+
+
+
+
+
📄 Read Uploaded Documents
+
Access files uploaded by users
+
+
+
+
+
+
📤 Export / Save Output
+
Write results to files or exports
+
+
+
+
+
+
🔔 Send Notifications
+
In-portal alerts and email notices
+
+
+
+
+
+
🌐 Call External APIs
+
HTTP requests to configured connectors
+
+
+
+
+
+
⏱️ Trigger Scheduled Jobs
+
Chain into the scheduler
+
+
+
+
+
+
+
+
+
+
+
📋 Log All Inputs & Outputs
+
Every run recorded in audit log
+
+
+
+
+
+
🔒 PII Detection
+
Warn before processing personal data
+
+
+
+
+
+
🛡️ Apply Guardrails Policy
+
Content filter rules from Guardrails page
+
+
+
+
+
+
⚠️ Mark as Sensitive
+
Outputs redacted from non-admin audit views
+
+
+
+
+
+
+ Allowed Knowledge Base Collections (comma-separated, or * for all)
+
+
+
+
+
+
+
+
+
+
+
+ + Add Step
+
+
+
+
+
+ 💾 Save Agent
+ 🗑 Delete
+
+
+
+
+
+
+
+
+ Input (available as {{input}} in all steps)
+
+
+
▶ Run Pipeline
+
+
+
+
+
+
✋
+
Waiting for Approval
+
This agent requires human sign-off before the output is finalised. A pending approval has been added to the queue above.
+
+ ✓ Approve & Finalise
+ ✕ Reject
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cezen-portal/analytics.html b/cezen-portal/analytics.html
new file mode 100644
index 0000000..fe27e0c
--- /dev/null
+++ b/cezen-portal/analytics.html
@@ -0,0 +1,595 @@
+
+
+
+
+
+ Usage Analytics — Nexus One AI
+
+
+
+
+
+
+
+
+
Admin · Analytics
+
Usage Analytics
+
Track AI usage across users, models, features, and time — all on-premises, all private.
+
+
+
+
+
+
+ Last 7 days
+ Last 30 days
+ Last 90 days
+ ⬇️ Export CSV
+ 🏢 Org-wide view
+
+
+
+
+
+
+
+
🔢
+
—
+
Total Queries
+
+
+
+
🪙
+
—
+
Tokens Processed
+
+
+
+
⚡
+
—
+
Avg Response Time
+
+
+
+
👥
+
—
+
Active Users
+
+
+
+
📄
+
—
+
Documents Processed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Usage by Feature
+
queries routed per tool
+
+
+
+
+
+
+
Token Budget
+
this period
+
+
+
+
+
+
+
+
+
+
Peak Activity Hours
+
average queries per hour of day (local time)
+
+
+
+
+
+
+
+
+
+
+
Top Users
+
admin view
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cezen-portal/api-playground.html b/cezen-portal/api-playground.html
new file mode 100644
index 0000000..9970a1f
--- /dev/null
+++ b/cezen-portal/api-playground.html
@@ -0,0 +1,644 @@
+
+
+
+
+
+ API Playground — Nexus One AI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auth
+ Params
+ Body
+ Docs
+
+
+
+
+ API Key
+
+ Paste
+ Load saved
+
+
Your API key is sent as Authorization: Bearer <key> on every request.
+
+
+
+
+
+ ⚡ Format JSON
+
+
+
+
+
+
Select an endpoint from the left to see documentation.
+
+
+
+
+
+
+
+
+
+
🔌
+
Select an endpoint and click Send
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cezen-portal/apikeys.html b/cezen-portal/apikeys.html
new file mode 100644
index 0000000..346976c
--- /dev/null
+++ b/cezen-portal/apikeys.html
@@ -0,0 +1,386 @@
+
+
+
+
+
+ API Keys — Nexus One AI
+
+
+
+
+
+
+
+
+
Admin · Integrations
+
API Key Manager
+
Generate API keys for programmatic access to Nexus One AI from scripts, pipelines, and external tools.
+
+
+
+
+
+
+
🔒 Pro Tier & Above
+
API Key management is available on Mid and Advanced tiers. Upgrade to enable programmatic access for pipelines and integrations.
+
+
+
+
+
+
+
+
🔑 Your API Keys
+
Keys authenticate requests to /api/* via Authorization: Bearer <key>. Each key is shown only once on creation.
+
+ Generate New Key
+
+
+
+
+
+
+
+
📖 How to Use
+
Pass your API key in the Authorization header on every request.
+
+
cURL example
+curl -H "Authorization: Bearer czk_your_key_here" \
+ https://ai.local/api/prompts
+
+ Keys can also be verified at /api/apikeys/verify — useful for custom middleware or integrations that need to validate a key before proxying requests to Ollama or Open WebUI.
+
+
+
+
+
+
+
+
+
+
+
Generate API Key
+ Key Name (e.g. "CI pipeline", "RAG script")
+
+ Expiry
+
+ Never
+ 30 days
+ 90 days
+ 180 days
+ 1 year
+
+
+
+
+
+
+
+
+
🔑 Your New API Key
+
This key will not be shown again. Copy it now and store it securely.
+
+
SECRET KEY — COPY NOW
+
+
+
📋 Copy Key
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cezen-portal/appliance.html b/cezen-portal/appliance.html
new file mode 100644
index 0000000..47ea2f6
--- /dev/null
+++ b/cezen-portal/appliance.html
@@ -0,0 +1,386 @@
+
+
+
+
+
+ Appliance Ops — Nexus One AI
+
+
+
+
+
+
+
+
+
Admin · System
+
Appliance Ops
+
Readiness, license tier, audit summary, and local data protection.
+
+
+
+
+ Refresh
+ Create Backup
+ Export Readiness JSON
+
+
+
+
+ Readiness
+ —
+ Loading
+
+
+
+
+
+ Audit
+ —
+ — failures in 7 days
+
+
+
+
+
+
+
+
+
+
+
+ Backups
+
+
+ Name Created Size
+ Loading…
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cezen-portal/audit.html b/cezen-portal/audit.html
new file mode 100644
index 0000000..fe2d5b8
--- /dev/null
+++ b/cezen-portal/audit.html
@@ -0,0 +1,299 @@
+
+
+
+
+
+ Audit Log — Nexus One AI
+
+
+
+
+
+
+
+
+
Admin · Audit Log
+
Audit Log
+
Full history of authentication events, model operations, and administrative actions.
+
+
+
+
+
+
+ All Actions
+ Login
+ Logout
+ Login Failed
+ Create User
+ Delete User
+ Reset Password
+ Change Password
+ Model Pull
+ Model Delete
+ Session Revoke
+
+
+ All Results
+ Success
+ Failure
+ Info
+
+
+ Clear
+ ↺ Refresh
+ ↓ Export CSV
+
+
+
+
+
+
+ Time
+ User
+ Action
+ Target
+ IP
+ Result
+ Detail
+
+
+
+ Loading…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cezen-portal/auth.js b/cezen-portal/auth.js
new file mode 100644
index 0000000..ebd33fd
--- /dev/null
+++ b/cezen-portal/auth.js
@@ -0,0 +1,200 @@
+/**
+ * Nexus One AI — Shared Auth Guard
+ * Include in every portal page. Checks session, injects user info into nav.
+ *
+ * Usage: (place just before
tag.
+ */
+
+// ── Session timeout constants ────────────────────────────────────────────────
+const SESSION_HOURS = 8; // must match JWT_EXPIRE_HRS in main.py
+const WARN_BEFORE_MIN = 10; // show warning this many minutes before expiry
+const EXTEND_INTERVAL = 60000; // poll session state every 60 seconds
+
+(async function () {
+ // Don't guard the login page itself
+ if (window.location.pathname.endsWith('login.html')) return;
+
+ let user = null;
+ try {
+ const res = await fetch('/api/auth/me', { credentials: 'include' });
+ if (!res.ok) throw new Error('unauthenticated');
+ user = await res.json();
+ } catch {
+ window.location.href = '/login.html?next=' + encodeURIComponent(window.location.pathname);
+ return;
+ }
+
+ // Force password change
+ if (user.must_change_password && !window.location.pathname.endsWith('change-password.html')) {
+ window.location.href = '/change-password.html';
+ return;
+ }
+
+ // Admin-only pages
+ if (document.body.dataset.role === 'admin' && user.role !== 'admin') {
+ window.location.href = '/index.html';
+ return;
+ }
+
+ // ── Inject user chip + logout into nav ─────────────────────────────────────
+ const topnav = document.querySelector('.topnav');
+ if (topnav) {
+ document.querySelectorAll('[data-admin-only]').forEach(el => {
+ if (user.role !== 'admin') el.style.display = 'none';
+ });
+
+ const chip = document.createElement('div');
+ chip.className = 'nav-user-chip';
+ chip.innerHTML = `
+ ${user.username.charAt(0).toUpperCase()}
+
+ ${user.username}
+ ${user.role === 'admin' ? 'Administrator ' : 'User '}
+
+ Sign out
+ `;
+ topnav.appendChild(chip);
+
+ document.getElementById('cezen-logout').addEventListener('click', async () => {
+ await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
+ window.location.href = '/login.html';
+ });
+ }
+
+ // Expose user globally for page scripts
+ window.cezenUser = user;
+ document.dispatchEvent(new CustomEvent('cezenAuthReady', { detail: user }));
+
+ // ── Online users pill in navbar (admin only) ────────────────────────────────
+ if (user.role === 'admin' && topnav) {
+ const pill = document.createElement('a');
+ pill.id = 'cezen-online-pill';
+ pill.href = '/dashboard.html#sessions';
+ pill.title = 'View active sessions';
+ pill.style.cssText = [
+ 'display:inline-flex;align-items:center;gap:5px;',
+ 'background:rgba(21,128,61,.08);border:1px solid rgba(21,128,61,.2);',
+ 'color:#15803D;border-radius:20px;padding:3px 10px 3px 8px;',
+ 'font-size:11.5px;font-weight:600;text-decoration:none;',
+ 'margin-right:2px;cursor:pointer;transition:.15s;flex-shrink:0;',
+ ].join('');
+ pill.innerHTML = '… online';
+ // Insert before user chip (last child)
+ topnav.insertBefore(pill, topnav.lastElementChild);
+
+ async function refreshOnlineCount() {
+ try {
+ const d = await fetch('/api/users/sessions', { credentials: 'include' }).then(r => r.json());
+ const n = (d.sessions || []).length;
+ document.getElementById('cezen-online-count').textContent = n;
+ pill.title = (d.sessions || []).map(s => s.username).join(', ') || 'No active sessions';
+ } catch { /* silently ignore */ }
+ }
+ refreshOnlineCount();
+ setInterval(refreshOnlineCount, 30000);
+ }
+
+ // ── Session timeout banner ──────────────────────────────────────────────────
+ // Record login time in sessionStorage so we can track elapsed time
+ const loginKey = 'cezen_login_ts';
+ if (!sessionStorage.getItem(loginKey)) {
+ sessionStorage.setItem(loginKey, Date.now().toString());
+ }
+
+ // Inject banner CSS + element
+ const style = document.createElement('style');
+ style.textContent = `
+ #cezen-timeout-banner {
+ display: none;
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background: #1E3A5F;
+ color: white;
+ border-radius: 12px;
+ padding: 14px 18px;
+ font-size: 13px;
+ font-family: inherit;
+ z-index: 9999;
+ box-shadow: 0 8px 32px rgba(0,0,0,.35);
+ max-width: 320px;
+ border-left: 4px solid #F59E0B;
+ animation: cezenFadeIn .3s ease;
+ }
+ @keyframes cezenFadeIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:none; } }
+ #cezen-timeout-banner.expired { border-color: #EF4444; }
+ #cezen-timeout-banner strong { display:block; margin-bottom:4px; font-size:14px; }
+ #cezen-timeout-banner .tb-actions { display:flex; gap:8px; margin-top:10px; }
+ #cezen-timeout-banner .tb-btn {
+ flex:1; padding:6px 0; border:none; border-radius:7px; font-size:12px;
+ font-weight:700; cursor:pointer; font-family:inherit;
+ }
+ #cezen-timeout-banner .tb-extend { background:#0D9488; color:white; }
+ #cezen-timeout-banner .tb-logout { background:rgba(255,255,255,.15); color:white; }
+ `;
+ document.head.appendChild(style);
+
+ const banner = document.createElement('div');
+ banner.id = 'cezen-timeout-banner';
+ banner.innerHTML = `
+ ⏱ Session expiring soon
+ Your session expires in
+
+ Stay signed in
+ Sign out
+
+ `;
+ document.body.appendChild(banner);
+
+ document.getElementById('cezen-extend').addEventListener('click', async () => {
+ // Touching /api/auth/me re-validates the cookie; for a true refresh we'd
+ // need a /api/auth/refresh endpoint — for now just reset the local timer.
+ sessionStorage.setItem(loginKey, Date.now().toString());
+ banner.style.display = 'none';
+ });
+
+ document.getElementById('cezen-tb-logout').addEventListener('click', async () => {
+ await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
+ window.location.href = '/login.html';
+ });
+
+ function fmt(ms) {
+ const m = Math.floor(ms / 60000);
+ const s = Math.floor((ms % 60000) / 1000);
+ if (m > 0) return `${m}m ${s}s`;
+ return `${s}s`;
+ }
+
+ function checkTimeout() {
+ const loginTs = parseInt(sessionStorage.getItem(loginKey) || Date.now(), 10);
+ const expireMs = SESSION_HOURS * 3600 * 1000;
+ const elapsed = Date.now() - loginTs;
+ const remaining = expireMs - elapsed;
+ const warnMs = WARN_BEFORE_MIN * 60 * 1000;
+
+ if (remaining <= 0) {
+ // Expired — redirect to login
+ banner.classList.add('expired');
+ banner.style.display = 'block';
+ document.getElementById('cezen-timeout-msg').innerHTML = 'Your session has expired.';
+ document.getElementById('cezen-extend').style.display = 'none';
+ setTimeout(() => { window.location.href = '/login.html'; }, 3000);
+ return;
+ }
+
+ if (remaining <= warnMs) {
+ banner.style.display = 'block';
+ const cd = document.getElementById('cezen-countdown');
+ if (cd) cd.textContent = fmt(remaining);
+ } else {
+ banner.style.display = 'none';
+ }
+ }
+
+ // Run every second for accurate countdown; heavier session validation every minute
+ setInterval(checkTimeout, 1000);
+ checkTimeout();
+})();
diff --git a/cezen-portal/benchmark.html b/cezen-portal/benchmark.html
new file mode 100644
index 0000000..ddb19f8
--- /dev/null
+++ b/cezen-portal/benchmark.html
@@ -0,0 +1,403 @@
+
+
+
+
+
+ Model Benchmark — Nexus One AI
+
+
+
+
+
+
+
+
+
Admin · Evaluation
+
Model Benchmarking
+
Compare response quality and inference speed across models side by side using your own test prompts.
+
+
+
+
+
+
+
📊 Pro Tier & Above
+
Model benchmarking requires the hardware throughput of Mid or Advanced tier to produce meaningful latency comparisons.
+
+
+
+
+
+
+
🤖 Select Models
+
Choose up to 5 models to compare. Models must be loaded in Ollama.
+
+
+
+ Loading models…
+
+ + Add
+
+
+
+
+
+
💬 Test Prompts
+
Add prompts to run against each model. Up to 10 prompts.
+
+
+ + Add prompt
+ 📋 Load presets
+ Max tokens:
+
+
+
+
+
+
+
+ ▶ Run Benchmark
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cezen-portal/branding.js b/cezen-portal/branding.js
new file mode 100644
index 0000000..717f0b3
--- /dev/null
+++ b/cezen-portal/branding.js
@@ -0,0 +1,359 @@
+/**
+ * branding.js — Nexus One AI White-Label Branding + Tier Feature Gating
+ * Fetches /api/settings/branding (public, no auth) and:
+ * 1. Applies org name, logo, accent color, and page title across all pages.
+ * 2. Locks nav items that require a higher tier, showing an upgrade prompt.
+ * Inject BEFORE
+
+
Nexus One AI
+
Set New Password
+
Your password needs to be changed before you can continue. Choose a strong password you haven't used before.
+
+
+
Password changed! Redirecting…
+
+
+ New Password
+
+
+
+ Confirm Password
+
+
+
+
Change Password
+
+
+ ✓ At least 8 characters ✓ Mix letters & numbers
+
+
+
+
+
+