Sync cezen-portal/ from live working copy (V1.0 GA polish + search-icon ordering fix)
Brings the ISO-packaged portal copy current with the live cezen-portal/ working directory, which had drifted across the entire V1.0 GA polish initiative (Phases 1-10) plus this session's topnav icon-order bugfix. - 52 existing files updated, 2 new pages added (about.html, health.html) - cgit/cezen-portal/ is now byte-identical to cezen-portal/ - Backend files (rag_ingest.py, cezen_license.py) already in sync; main.py intentionally left as-is (cgit is ahead there with workstation-category commercial-fit logic not yet backported to the live working copy) Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
This commit is contained in:
1 parent
7540da04a8
commit
b3eec34158
57 files changed
+2832
-1354
No files matched your search
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Model Catalog — Nexus One AI</title>
|
||||
<link rel="stylesheet" href="style.css?v=4">
|
||||
<link rel="stylesheet" href="style.css?v=12">
|
||||
<style>
|
||||
.mm-tabs { display:flex; gap:4px; margin-bottom:24px; background:rgba(255,255,255,.03); border:1px solid var(--bdr); border-radius:10px; padding:4px; width:fit-content; }
|
||||
.mm-tab { padding:7px 18px; border-radius:7px; font-size:13px; font-weight:600; cursor:pointer; color:var(--lt); border:none; background:none; font-family:inherit; transition:all .15s; }
|
||||
@@ -126,6 +126,7 @@
|
||||
<a href="dashboard.html">Dashboard</a>
|
||||
<a href="analytics.html">Usage Analytics</a>
|
||||
<a href="audit.html">Governance & Audit</a>
|
||||
<a href="health.html">Health Center</a>
|
||||
<a href="feedback.html">Feedback & Ratings</a>
|
||||
<span class="nav-drop-cat">MANAGE /</span>
|
||||
<a href="users.html">Users</a>
|
||||
@@ -165,7 +166,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<a href="notifications.html" style="position:relative">🔔</a>
|
||||
<a href="notifications.html" class="nav-icon-btn" style="position:relative" aria-label="Notifications"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M15 17h5l-1.4-1.4A2 2 0 0 1 18 14.2V11a6 6 0 1 0-12 0v3.2a2 2 0 0 1-.6 1.4L4 17h5"/><path d="M10 17a2 2 0 0 0 4 0"/></svg></span></a>
|
||||
<span class="badge" data-brand="tier">Basic Tier</span>
|
||||
<div id="nav-org-logo" class="nav-org-logo"></div>
|
||||
</header>
|
||||
@@ -183,11 +184,12 @@
|
||||
<div class="mm-tabs">
|
||||
<button class="mm-tab active" onclick="switchTab('installed', this)">Installed</button>
|
||||
<button class="mm-tab" onclick="switchTab('available', this)">Pull from Registry</button>
|
||||
<button class="mm-tab" onclick="switchTab('upload', this)">📁 Upload / Offline</button>
|
||||
<button class="mm-tab" onclick="switchTab('upload', this)">Upload / Offline</button>
|
||||
</div>
|
||||
|
||||
<!-- INSTALLED -->
|
||||
<div id="tab-installed">
|
||||
<input type="text" id="installed-filter" class="mm-filter-input" placeholder="Filter installed models by name…" oninput="filterInstalled()" style="width:100%;max-width:340px;margin-bottom:14px;padding:9px 12px;border:1px solid var(--bdr);border-radius:8px;font-size:13px;font-family:inherit">
|
||||
<div class="mm-section-wrap" id="installed-list">
|
||||
<div style="padding:24px;color:var(--lt)">Loading installed models…</div>
|
||||
</div>
|
||||
@@ -221,7 +223,7 @@
|
||||
|
||||
<div class="upload-zone" id="upload-zone" onclick="document.getElementById('gguf-file').click()">
|
||||
<input type="file" id="gguf-file" accept=".gguf" onchange="onFileSelected(this)">
|
||||
<div class="upload-zone-icon">📦</div>
|
||||
<div class="upload-zone-icon">GGUF</div>
|
||||
<div class="upload-zone-label">Click to select a .gguf file</div>
|
||||
<div class="upload-zone-hint">Or drag and drop here · Files can be multi-GB — allow time to transfer</div>
|
||||
</div>
|
||||
@@ -274,7 +276,7 @@
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Nexus One AI · Powered by Cezen · Basic Tier</p>
|
||||
<p>Nexus One AI · Powered by Cezen · <span data-brand="tier">Basic Tier</span></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -290,6 +292,11 @@ const MOCK_INSTALLED = [
|
||||
];
|
||||
|
||||
let installedNames = new Set();
|
||||
let installedModels = [];
|
||||
|
||||
function escHtmlLocal(s) {
|
||||
return (s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
||||
}
|
||||
|
||||
function switchTab(name, btn) {
|
||||
document.querySelectorAll('.mm-tab').forEach(t => t.classList.remove('active'));
|
||||
@@ -322,12 +329,27 @@ async function loadInstalled() {
|
||||
data = { models: MOCK_INSTALLED, installed_names: MOCK_INSTALLED.map(m=>m.name) };
|
||||
}
|
||||
installedNames = new Set(data.installed_names || []);
|
||||
installedModels = data.models || [];
|
||||
renderInstalledList(installedModels);
|
||||
}
|
||||
|
||||
function filterInstalled() {
|
||||
const q = (document.getElementById('installed-filter').value || '').trim().toLowerCase();
|
||||
const filtered = q ? installedModels.filter(m => (m.name || '').toLowerCase().includes(q)) : installedModels;
|
||||
renderInstalledList(filtered, q);
|
||||
}
|
||||
|
||||
function renderInstalledList(models, activeQuery) {
|
||||
const el = document.getElementById('installed-list');
|
||||
if (!data.models || !data.models.length) {
|
||||
el.innerHTML = '<div style="padding:24px;color:var(--lt)">No models installed yet. Go to "Available to Pull" to download your first model.</div>';
|
||||
if (!installedModels.length) {
|
||||
el.innerHTML = '<div class="cezen-list-empty">No models installed yet — <a onclick="document.querySelector(\'.mm-tab[onclick*=available]\').click()">pull your first model from the registry</a>.</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.models.map(m => {
|
||||
if (!models.length) {
|
||||
el.innerHTML = `<div class="cezen-list-empty">No installed models match "${escHtmlLocal(activeQuery || '')}".</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = models.map(m => {
|
||||
const statusColor = m.status==='loaded' ? '#15803D' : '#6B7280';
|
||||
const statusLabel = m.status==='loaded' ? '● loaded' : '○ idle';
|
||||
const qLevel = m.details?.quantization_level || '';
|
||||
@@ -354,12 +376,12 @@ async function loadAvailable() {
|
||||
const grid = document.getElementById('available-grid');
|
||||
grid.innerHTML = models.map(m => `
|
||||
<div class="mm-card ${m.installed ? 'installed' : ''}">
|
||||
${m.installed ? '<div class="installed-badge">✓ Installed</div>' : ''}
|
||||
${m.installed ? '<div class="installed-badge">Installed</div>' : ''}
|
||||
<div class="mm-card-name">${m.label}</div>
|
||||
<div class="mm-card-use">${m.use}</div>
|
||||
<div class="mm-card-meta">
|
||||
<span class="mm-tag size">💾 ${m.size_gb} GB download</span>
|
||||
<span class="mm-tag vram">🎮 ~${m.vram_gb} GB VRAM</span>
|
||||
<span class="mm-tag size">${m.size_gb} GB download</span>
|
||||
<span class="mm-tag vram">~${m.vram_gb} GB VRAM</span>
|
||||
</div>
|
||||
<div class="vram-bar-wrap"><div class="vram-bar" style="width:${Math.round(m.vram_gb/96*100)}%"></div></div>
|
||||
<div style="font-size:10px;color:var(--lt);margin:4px 0 12px;text-align:right">${m.vram_gb} of 96 GB VRAM</div>
|
||||
@@ -384,7 +406,7 @@ async function pullModel(name, btn) {
|
||||
body: JSON.stringify({model: name})
|
||||
});
|
||||
const data = await res.json();
|
||||
notice.textContent = `⏳ ${data.message}`;
|
||||
notice.textContent = `Processing: ${data.message}`;
|
||||
notice.classList.add('show');
|
||||
setTimeout(() => { notice.classList.remove('show'); loadInstalled(); }, 8000);
|
||||
} catch(e) {
|
||||
@@ -427,7 +449,7 @@ function onFileSelected(input) {
|
||||
selectedFile = input.files[0];
|
||||
if (!selectedFile) return;
|
||||
const info = document.getElementById('file-selected-info');
|
||||
info.textContent = `📦 ${selectedFile.name} (${(selectedFile.size / 1e9).toFixed(2)} GB)`;
|
||||
info.textContent = `${selectedFile.name} (${(selectedFile.size / 1e9).toFixed(2)} GB)`;
|
||||
info.classList.add('show');
|
||||
const nameInput = document.getElementById('upload-model-name');
|
||||
if (!nameInput.value)
|
||||
@@ -484,7 +506,7 @@ function doUpload() {
|
||||
xhr.addEventListener('load', async () => {
|
||||
prog.classList.remove('show');
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
showUploadMsg(`✓ File uploaded. Registering "${modelName}" with Ollama…`, 'ok');
|
||||
showUploadMsg(`File uploaded. Registering "${modelName}" with Ollama...`, 'ok');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`${_API}/models/load`, {
|
||||
@@ -493,7 +515,7 @@ function doUpload() {
|
||||
body: JSON.stringify({ filename: selectedFile.name, model_name: modelName })
|
||||
});
|
||||
if (res.ok) {
|
||||
showUploadMsg(`✓ Model "${modelName}" is ready to use!`, 'ok');
|
||||
showUploadMsg(`Model "${modelName}" is ready to use.`, 'ok');
|
||||
loadInstalled(); loadUploads();
|
||||
} else {
|
||||
const d = await res.json().catch(()=>({}));
|
||||
@@ -535,8 +557,8 @@ async function loadUploads() {
|
||||
<div class="uf-row">
|
||||
<div class="uf-name">${f.filename}</div>
|
||||
<div class="uf-size">${f.size_gb} GB</div>
|
||||
<button class="uf-load-btn" onclick="openLoadModal('${f.filename}')">Register →</button>
|
||||
<button class="uf-del-btn" onclick="deleteUpload('${f.filename}', this)">✕</button>
|
||||
<button class="uf-load-btn" onclick="openLoadModal('${f.filename}')">Register</button>
|
||||
<button class="uf-del-btn" onclick="deleteUpload('${f.filename}', this)">Delete</button>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch {
|
||||
@@ -592,6 +614,6 @@ document.getElementById('modal-load')?.addEventListener('click', e => {
|
||||
loadInstalled();
|
||||
</script>
|
||||
<script src="auth.js"></script>
|
||||
<script src="branding.js"></script>
|
||||
<script src="branding.js?v=3"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user