Add enterprise appliance experience Phase 1 foundation

This commit is contained in:
Jino Jose committed 2026-07-12 12:13:23 +05:30
1 parent b3eec34158
commit ebaa2e7219
69 files changed
+6832 -18

No files matched your search

+5 -3
View File
@@ -292,12 +292,13 @@ async function api(path, opts={}) {
function renderReadiness(d) {
readinessData = d;
const score = d.readiness?.score ?? 0;
const status = d.readiness?.status || 'unknown';
const status = d.state || d.readiness?.status || 'unavailable';
const entitlement = d.entitlement || d.license?.entitlement || {};
document.getElementById('readiness-score').textContent = score + '%';
const statusEl = document.getElementById('readiness-status');
statusEl.textContent = status.replace(/_/g, ' ');
statusEl.className = 'pill ' + (status === 'ready' ? 'ok' : status === 'limited' ? 'warn' : 'bad');
if (window.CezenExperience) window.CezenExperience.renderState(statusEl, {state: status, severity: d.severity, summary: status.replace(/_/g, ' ')});
else statusEl.textContent = status.replace(/_/g, ' ');
statusEl.title = window.CezenExperience ? window.CezenExperience.formatFreshness(d.observed_at) : '';
document.getElementById('license-tier').textContent = d.license?.label || '—';
document.getElementById('license-position').textContent = `${d.license?.positioning || '—'} · status: ${licenseStatusLabel(entitlement.license_status || d.license?.license_status || 'missing')}`;
@@ -457,6 +458,7 @@ function exportReadiness() {
loadAll();
</script>
<script src="auth.js"></script>
<script src="experience.js?v=1"></script>
<script src="branding.js?v=3"></script>
</body>
</html>
+16
View File
@@ -50,6 +50,22 @@
max: 'Server Max',
};
var PRODUCT_VOCABULARY = Object.freeze({
companyAttribution: 'Powered by Cezen',
workstation: 'Nexus One AI Workstation',
server: 'Nexus One AI Server',
tiers: Object.freeze({ starter: 'Server S', basic: 'Server M', pro: 'Server L', max: 'Server Max' }),
capabilityReasons: Object.freeze({
licensed: 'Included in the current license',
not_licensed: 'Not included in the current license',
not_provisioned: 'Licensed but not installed on this appliance',
unsupported_hardware: 'Not supported by the detected hardware',
degraded: 'Temporarily degraded',
unavailable: 'Currently unavailable'
})
});
window.NexusOneVocabulary = PRODUCT_VOCABULARY;
var SERVICE_PORTS = {
'3001': { name: 'open-webui', label: 'Open WebUI' },
'11434': { name: 'ollama', label: 'Ollama' },
+70
View File
@@ -0,0 +1,70 @@
/** Shared, dependency-free customer experience primitives for Nexus One AI. */
(function (global) {
'use strict';
var STATES = ['loading', 'empty', 'ready', 'degraded', 'failed', 'restricted', 'unavailable', 'stale'];
var SEVERITIES = ['info', 'warning', 'critical'];
function canonicalState(value) {
value = String(value || '').toLowerCase().replace(/[_ ]+/g, '-');
var aliases = { ok: 'ready', healthy: 'ready', limited: 'degraded', warning: 'degraded', error: 'failed', critical: 'failed', not_ready: 'failed' };
value = aliases[value] || value;
return STATES.indexOf(value) >= 0 ? value : 'unavailable';
}
function announce(message, priority) {
var id = priority === 'assertive' ? 'cezen-live-assertive' : 'cezen-live-polite';
var region = document.getElementById(id);
if (!region) {
region = document.createElement('div');
region.id = id;
region.className = 'sr-only';
region.setAttribute('aria-live', priority === 'assertive' ? 'assertive' : 'polite');
region.setAttribute('aria-atomic', 'true');
document.body.appendChild(region);
}
region.textContent = '';
global.setTimeout(function () { region.textContent = String(message || ''); }, 20);
}
function renderState(element, payload) {
if (!element) return;
payload = payload || {};
var state = canonicalState(payload.state || payload.status);
var severity = SEVERITIES.indexOf(payload.severity) >= 0 ? payload.severity : (state === 'failed' ? 'critical' : state === 'degraded' || state === 'stale' ? 'warning' : 'info');
element.classList.remove.apply(element.classList, STATES.map(function (item) { return 'cx-state-' + item; }));
element.classList.add('cx-state', 'cx-state-' + state);
element.setAttribute('data-state', state);
element.setAttribute('data-severity', severity);
element.setAttribute('role', severity === 'critical' ? 'alert' : 'status');
element.textContent = payload.summary || state.replace(/-/g, ' ');
}
function formatFreshness(observedAt, now) {
if (!observedAt) return 'Freshness unknown';
var observed = new Date(observedAt).getTime();
var current = now ? new Date(now).getTime() : Date.now();
if (!isFinite(observed)) return 'Freshness unknown';
var seconds = Math.max(0, Math.round((current - observed) / 1000));
if (seconds < 60) return 'Observed ' + seconds + ' seconds ago';
if (seconds < 3600) return 'Observed ' + Math.floor(seconds / 60) + ' minutes ago';
return 'Observed ' + Math.floor(seconds / 3600) + ' hours ago';
}
function focusFirstError(container) {
var target = (container || document).querySelector('[aria-invalid="true"], .field-error, .cx-error');
if (!target) return false;
if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '-1');
target.focus();
return true;
}
global.CezenExperience = Object.freeze({
states: STATES.slice(),
canonicalState: canonicalState,
announce: announce,
renderState: renderState,
formatFreshness: formatFreshness,
focusFirstError: focusFirstError
});
})(window);
+6
View File
@@ -245,6 +245,11 @@ function renderSummary(status) {
const dot = document.getElementById('hc-summary-dot');
const title = document.getElementById('hc-summary-title');
const sub = document.getElementById('hc-summary-sub');
if (window.CezenExperience) {
const mapped = status === 'ok' ? 'ready' : status === 'warning' ? 'degraded' : status === 'critical' ? 'failed' : 'unavailable';
title.className = 'hc-summary-title cx-state cx-state-' + mapped;
title.setAttribute('role', mapped === 'failed' ? 'alert' : 'status');
}
if (status === 'ok') {
dot.className = 'hc-summary-dot healthy';
title.textContent = 'Healthy — all systems normal';
@@ -284,6 +289,7 @@ loadHealth();
setInterval(loadHealth, 30000);
</script>
<script src="auth.js"></script>
<script src="experience.js?v=1"></script>
<script src="branding.js?v=3"></script>
</body>
</html>
+21
View File
@@ -817,11 +817,32 @@ async function fetchSystemInfo() {
}
}
async function fetchApplianceReadiness() {
try {
const response = await fetch('/api/system/readiness-report', { credentials: 'include' });
if (!response.ok) return;
const data = await response.json();
const label = document.getElementById('status-overall-label');
const checked = document.getElementById('status-last-checked');
if (label) {
label.textContent = data.customer_impact || (data.state === 'ready' ? 'Appliance ready' : 'Appliance needs attention');
label.setAttribute('role', data.severity === 'critical' ? 'alert' : 'status');
}
if (checked && window.CezenExperience) checked.textContent = window.CezenExperience.formatFreshness(data.observed_at);
if (data.next_actions && data.next_actions.length && window.CezenExperience) window.CezenExperience.announce(label.textContent, data.severity === 'critical' ? 'assertive' : 'polite');
} catch (e) {
/* Existing service checks remain visible when readiness aggregation is unavailable. */
}
}
fetchSystemInfo();
fetchMetrics();
fetchApplianceReadiness();
setInterval(fetchMetrics, 30000);
setInterval(fetchApplianceReadiness, 30000);
</script>
<script src="auth.js"></script>
<script src="experience.js?v=1"></script>
<script src="branding.js?v=3"></script>
<script>
function renderLocalIcons() {
+1
View File
@@ -640,6 +640,7 @@ checkStepCompletion();
</script>
<script src="auth.js"></script>
<script src="experience.js?v=1"></script>
<script src="branding.js?v=3"></script>
</body>
</html>
+20
View File
@@ -159,6 +159,26 @@ a { color: inherit; text-decoration: none; }
font-size: .6875rem; font-weight: 600; letter-spacing: .02em;
line-height: 1.6;
}
/* Shared enterprise state and accessibility primitives. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
box-shadow: var(--shadow-focus);
}
.cx-state { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border); border-radius: var(--radius-pill); padding: 3px 9px; font-weight: 600; }
.cx-state::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.cx-state-ready { color: var(--green); background: var(--green-lt); border-color: var(--green-border); }
.cx-state-degraded, .cx-state-stale { color: var(--amber); background: var(--amber-lt); border-color: var(--amber-border); }
.cx-state-failed { color: var(--red); background: var(--red-lt); border-color: var(--red-border); }
.cx-state-loading, .cx-state-empty, .cx-state-restricted, .cx-state-unavailable { color: var(--text-secondary); background: var(--surface2); }
.cx-alert { border: 1px solid var(--border); border-radius: var(--radius-md); padding: 12px 14px; }
.cx-alert[data-severity="critical"] { border-color: var(--red-border); background: var(--red-lt); }
.cx-alert[data-severity="warning"] { border-color: var(--amber-border); background: var(--amber-lt); }
.cx-error { color: var(--red); font-weight: 600; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
}
.badge-pill.violet { background: var(--brand-lt); color: var(--brand-text); border: 1px solid var(--brand-border); }
.badge-pill.green { background: var(--green-lt); color: var(--green); border: 1px solid var(--green-border); }
.badge-pill.amber { background: var(--amber-lt); color: var(--amber); border: 1px solid var(--amber-border); }
+10
View File
@@ -369,6 +369,15 @@
</div>
</div>
<div class="ts-issue" data-tags="setup interrupted resume recovery install">
<div class="ts-issue-header" onclick="toggleIssue(this)"><div class="ts-issue-title">Setup or installation was interrupted</div><span class="ts-chevron">›</span></div>
<div class="ts-issue-body"><p>Return to the setup page. Nexus One AI preserves a local operation record and shows whether setup can resume, should be retried, or needs support.</p><ol><li>Do not delete <code>/opt/cezen/setup-operation.json</code>.</li><li>Review <code>/var/log/cezen-install.log</code> for the last completed phase.</li><li>Use the displayed retry or recovery action. If it remains in <strong>Needs attention</strong>, contact support with the operation/correlation ID.</li></ol></div>
</div>
<div class="ts-issue" data-tags="readiness stale degraded unavailable offline">
<div class="ts-issue-header" onclick="toggleIssue(this)"><div class="ts-issue-title">Readiness is degraded, unavailable, or stale</div><span class="ts-chevron">›</span></div>
<div class="ts-issue-body"><p>Open <a href="health.html">Health Center</a> and review affected capabilities and observation time. Stale means the last observation is too old to claim current health; it does not automatically mean the appliance has failed.</p><p>This guidance is stored locally and does not require public internet access.</p></div>
</div>
</div>
<footer>
@@ -418,6 +427,7 @@ document.querySelector('.ts-issue-header').click();
</script>
<script src="auth.js"></script>
<script src="experience.js?v=1"></script>
<script src="branding.js?v=3"></script>
</body>
</html>