Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
617 lines
39 KiB
HTML
617 lines
39 KiB
HTML
|
|
<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Ontoref{% endblock title %}</title>
|
|
<!-- Apply saved theme + nav-mode before paint to avoid flash -->
|
|
<script>(function(){
|
|
var t=localStorage.getItem("ontoref-theme");
|
|
if(t)document.documentElement.setAttribute("data-theme",t);
|
|
var m=localStorage.getItem("ontoref-nav-mode");
|
|
if(m==="icons")document.documentElement.classList.add("nav-icons");
|
|
else if(m==="names")document.documentElement.classList.add("nav-names");
|
|
})()</script>
|
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
.badge-xs { height: 1rem; }
|
|
.badge-success, .badge-info, .badge-error, .badge-warning { color: #ffffff; }
|
|
html.nav-icons .nav-label { display: none !important; }
|
|
html.nav-icons .dropdown-content .nav-label { display: inline !important; }
|
|
html.nav-names .nav-icon { display: none !important; }
|
|
</style>
|
|
{% block head %}{% endblock head %}
|
|
</head>
|
|
<body class="min-h-screen bg-base-100 text-base-content">
|
|
|
|
<div class="navbar bg-base-200 shadow-lg px-4">
|
|
|
|
<!-- Mobile: burger -->
|
|
<div class="navbar-start md:hidden">
|
|
<div class="dropdown">
|
|
<button tabindex="0" class="btn btn-ghost btn-sm btn-circle" aria-label="Menu">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M4 6h16M4 12h16M4 18h16"/>
|
|
</svg>
|
|
</button>
|
|
<ul tabindex="0"
|
|
class="dropdown-content menu menu-sm bg-base-200 shadow-lg rounded-box z-50 w-56 mt-2 p-2 gap-0.5">
|
|
{% if slug %}
|
|
<li><a href="/ui/">← Projects</a></li>
|
|
<li class="divider my-0.5"></li>
|
|
{% endif %}
|
|
{% if not hide_project_nav %}
|
|
<li><a href="{{ base_url }}/" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
|
|
<span class="nav-label">Dashboard</span>
|
|
</a></li>
|
|
<!-- Reflect: Modes · Actions · Sessions -->
|
|
<li>
|
|
<details>
|
|
<summary class="gap-1.5 font-medium text-base-content/70">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
<span class="nav-label">Reflect</span>
|
|
</summary>
|
|
<ul>
|
|
<li><a href="{{ base_url }}/modes" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
<span class="nav-label">Modes</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/actions" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
<span class="nav-label">Actions</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/sessions" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
<span class="nav-label">Sessions</span>
|
|
</a></li>
|
|
</ul>
|
|
</details>
|
|
</li>
|
|
<!-- Track: Backlog · Q&A · Notifications -->
|
|
<li>
|
|
<details>
|
|
<summary class="gap-1.5 font-medium text-base-content/70">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
|
|
<span class="nav-label">Track</span>
|
|
</summary>
|
|
<ul>
|
|
<li><a href="{{ base_url }}/backlog" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
|
|
<span class="nav-label">Backlog</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/qa" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
<span class="nav-label">Q&A</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/notifications" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
|
|
<span class="nav-label">Notifications</span>
|
|
</a></li>
|
|
</ul>
|
|
</details>
|
|
</li>
|
|
<!-- Knowledge: Graph · Search · Compose -->
|
|
<li>
|
|
<details>
|
|
<summary class="gap-1.5 font-medium text-base-content/70">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
<span class="nav-label">Knowledge</span>
|
|
</summary>
|
|
<ul>
|
|
<li><a href="{{ base_url }}/graph" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
<span class="nav-label">Graph</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/search" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
<span class="nav-label">Search</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/compose" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
|
<span class="nav-label">Compose</span>
|
|
</a></li>
|
|
</ul>
|
|
</details>
|
|
</li>
|
|
<!-- Dev: API · Config -->
|
|
<li>
|
|
<details>
|
|
<summary class="gap-1.5 font-medium text-base-content/70">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
|
<span class="nav-label">Dev</span>
|
|
</summary>
|
|
<ul>
|
|
<li><a href="{{ base_url }}/api" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
|
<span class="nav-label">API</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/config" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
<span class="nav-label">Config</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/adrs" class="gap-1.5">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
<span class="nav-label">ADRs</span>
|
|
</a></li>
|
|
</ul>
|
|
</details>
|
|
</li>
|
|
<li class="divider my-0.5"></li>
|
|
{% endif %}
|
|
{% if not slug or current_role == "admin" %}
|
|
<li><a href="/ui/manage">Manage Projects</a></li>
|
|
{% endif %}
|
|
{% if slug %}
|
|
<li><a href="/ui/{{ slug }}/logout">Logout</a></li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
<!-- Mobile brand -->
|
|
<a href="/ui/" class="btn btn-ghost p-1 ml-1">
|
|
{% if logo or logo_dark %}
|
|
{% if logo %}<img id="proj-logo-light-m" src="{{ logo }}" alt="{{ slug }}" class="h-11 max-w-[8rem] object-contain object-left">{% endif %}
|
|
{% if logo_dark %}<img id="proj-logo-dark-m" src="{{ logo_dark }}" alt="{{ slug }}" class="h-11 max-w-[8rem] object-contain object-left">{% endif %}
|
|
{% else %}
|
|
<span class="text-base font-bold tracking-tight"><span style="color:#C0CCD8;">onto</span><span style="color:#E8A838;">ref</span></span>
|
|
{% endif %}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Desktop: brand + back link -->
|
|
<div class="navbar-start hidden md:flex items-center gap-2">
|
|
<a href="/ui/" class="btn btn-ghost p-1">
|
|
{% if logo or logo_dark %}
|
|
{% if logo %}<img id="proj-logo-light" src="{{ logo }}" alt="{{ slug }}" class="h-11 max-w-[9rem] object-contain object-left">{% endif %}
|
|
{% if logo_dark %}<img id="proj-logo-dark" src="{{ logo_dark }}" alt="{{ slug }}" class="h-11 max-w-[9rem] object-contain object-left">{% endif %}
|
|
{% else %}
|
|
<span class="text-lg font-bold tracking-tight"><span style="color:#C0CCD8;">onto</span><span style="color:#E8A838;">ref</span></span>
|
|
{% endif %}
|
|
</a>
|
|
{% if slug %}
|
|
<a href="/ui/" class="btn btn-xs btn-ghost text-base-content/50 gap-1">
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
</svg>
|
|
Projects
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Desktop: nav links (work area hover dropdowns) -->
|
|
<div class="navbar-center hidden md:flex">
|
|
{% if not hide_project_nav %}
|
|
<div class="flex items-center gap-0.5 text-sm">
|
|
|
|
<!-- Dashboard: standalone -->
|
|
<a href="{{ base_url }}/" class="btn btn-ghost btn-sm gap-1.5 {% block nav_dashboard %}{% endblock nav_dashboard %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
|
|
<span class="nav-label">Dashboard</span>
|
|
</a>
|
|
|
|
<!-- Reflect: Modes · Actions · Sessions -->
|
|
<div class="dropdown dropdown-hover">
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm gap-1.5 {% block nav_group_reflect %}{% endblock nav_group_reflect %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
<span class="nav-label">Reflect</span>
|
|
<svg class="w-3 h-3 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
</div>
|
|
<ul tabindex="0" class="dropdown-content menu bg-base-200 shadow-lg rounded-box z-50 w-44 p-2 mt-1">
|
|
<li><a href="{{ base_url }}/modes" class="gap-1.5 {% block nav_modes %}{% endblock nav_modes %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
<span class="nav-label">Modes</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/actions" class="gap-1.5 {% block nav_actions %}{% endblock nav_actions %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
<span class="nav-label">Actions</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/sessions" class="gap-1.5 {% block nav_sessions %}{% endblock nav_sessions %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
<span class="nav-label">Sessions</span>
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Track: Backlog · Q&A · Notifications -->
|
|
<div class="dropdown dropdown-hover">
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm gap-1.5 {% block nav_group_track %}{% endblock nav_group_track %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
|
|
<span class="nav-label">Track</span>
|
|
<svg class="w-3 h-3 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
</div>
|
|
<ul tabindex="0" class="dropdown-content menu bg-base-200 shadow-lg rounded-box z-50 w-44 p-2 mt-1">
|
|
<li><a href="{{ base_url }}/backlog" class="gap-1.5 {% block nav_backlog %}{% endblock nav_backlog %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/></svg>
|
|
<span class="nav-label">Backlog</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/qa" class="gap-1.5 {% block nav_qa %}{% endblock nav_qa %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
<span class="nav-label">Q&A</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/notifications" class="gap-1.5 {% block nav_notifications %}{% endblock nav_notifications %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
|
|
<span class="nav-label">Notifications</span>
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Knowledge: Graph · Search · Compose -->
|
|
<div class="dropdown dropdown-hover">
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm gap-1.5 {% block nav_group_knowledge %}{% endblock nav_group_knowledge %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
<span class="nav-label">Knowledge</span>
|
|
<svg class="w-3 h-3 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
</div>
|
|
<ul tabindex="0" class="dropdown-content menu bg-base-200 shadow-lg rounded-box z-50 w-44 p-2 mt-1">
|
|
<li><a href="{{ base_url }}/graph" class="gap-1.5 {% block nav_graph %}{% endblock nav_graph %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
<span class="nav-label">Graph</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/search" class="gap-1.5 {% block nav_search %}{% endblock nav_search %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
<span class="nav-label">Search</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/compose" class="gap-1.5 {% block nav_compose %}{% endblock nav_compose %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
|
<span class="nav-label">Compose</span>
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Dev: API · Config -->
|
|
<div class="dropdown dropdown-hover">
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm gap-1.5 {% block nav_group_dev %}{% endblock nav_group_dev %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
|
<span class="nav-label">Dev</span>
|
|
<svg class="w-3 h-3 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
</div>
|
|
<ul tabindex="0" class="dropdown-content menu bg-base-200 shadow-lg rounded-box z-50 w-44 p-2 mt-1">
|
|
<li><a href="{{ base_url }}/api" class="gap-1.5 {% block nav_api %}{% endblock nav_api %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
|
<span class="nav-label">API</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/config" class="gap-1.5 {% block nav_config %}{% endblock nav_config %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
<span class="nav-label">Config</span>
|
|
</a></li>
|
|
<li><a href="{{ base_url }}/adrs" class="gap-1.5 {% block nav_adrs %}{% endblock nav_adrs %}">
|
|
<svg class="nav-icon w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
<span class="nav-label">ADRs</span>
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- End: slug, user/session, MCP, manage (main page only), theme -->
|
|
<div class="navbar-end gap-1">
|
|
{% if slug %}
|
|
<span class="text-xs font-mono text-base-content/40 hidden sm:inline">{{ slug }}</span>
|
|
{% if current_role == "viewer" %}
|
|
<span class="badge badge-xs badge-ghost font-mono hidden sm:inline-flex">viewer</span>
|
|
{% endif %}
|
|
<!-- User icon → logout -->
|
|
<a href="/ui/{{ slug }}/logout" class="btn btn-xs btn-ghost hidden sm:inline-flex" title="Logout">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
</a>
|
|
{% endif %}
|
|
{% if mcp_active %}
|
|
<div class="dropdown dropdown-end hidden sm:block">
|
|
<button tabindex="0" class="btn btn-xs btn-ghost gap-1" title="MCP Server active">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2v-4M9 21H5a2 2 0 01-2-2v-4m0 0h18"/>
|
|
</svg>
|
|
<span class="text-xs font-mono">MCP</span>
|
|
<span class="inline-block w-1.5 h-1.5 rounded-full bg-success"></span>
|
|
</button>
|
|
<div tabindex="0"
|
|
class="dropdown-content bg-base-200 border border-base-content/10 rounded-box z-50 shadow-lg w-64 p-3 mt-1">
|
|
<p class="text-xs font-semibold text-base-content/60 mb-2 uppercase tracking-wide">MCP Tools</p>
|
|
<ul class="space-y-0.5">
|
|
{% for tool in mcp_tools %}
|
|
<li class="text-xs font-mono text-base-content/80 py-0.5 px-1 rounded hover:bg-base-300">{{ tool }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<div class="mt-2 pt-2 border-t border-base-content/10 text-[11px] text-base-content/40 space-y-0.5">
|
|
<div>HTTP: <code class="font-mono">/mcp</code></div>
|
|
<div>stdio: <code class="font-mono">--mcp-stdio</code></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<!-- Manage gear: only on main picker page (no project) -->
|
|
{% if not slug %}
|
|
<a href="/ui/manage" class="btn btn-xs btn-ghost hidden sm:inline-flex" title="Manage projects">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
</svg>
|
|
</a>
|
|
{% endif %}
|
|
<!-- Loopback status badge → dropdown with live health -->
|
|
<div class="dropdown dropdown-end hidden sm:block" id="loopback-wrap">
|
|
<button tabindex="0" id="loopback-btn"
|
|
class="btn btn-xs btn-ghost font-mono gap-1 text-base-content/50"
|
|
title="Daemon status">
|
|
<span id="loopback-dot" class="inline-block w-1.5 h-1.5 rounded-full bg-base-content/20"></span>
|
|
<span id="loopback-host">loopback</span>
|
|
</button>
|
|
<div tabindex="0"
|
|
class="dropdown-content bg-base-200 border border-base-content/10 rounded-box z-50 shadow-lg w-64 p-3 mt-1 text-xs">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="font-semibold text-base-content/60 uppercase tracking-wide">Daemon</span>
|
|
<button onclick="daemonPing()" class="btn btn-xs btn-ghost py-0 h-5 min-h-0 text-base-content/40">↺ ping</button>
|
|
</div>
|
|
<div id="loopback-details" class="space-y-1 text-base-content/70">
|
|
<div class="text-base-content/30 italic">loading…</div>
|
|
</div>
|
|
<div class="mt-2 pt-2 border-t border-base-content/10 flex justify-between items-center">
|
|
<code class="font-mono text-[11px] text-base-content/40" id="loopback-addr"></code>
|
|
<button onclick="navigator.clipboard.writeText(document.getElementById('loopback-addr').textContent)"
|
|
class="btn btn-xs btn-ghost py-0 h-5 min-h-0 text-base-content/30" title="Copy address">⎘</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Nav display mode toggle -->
|
|
<button id="nav-mode-btn" class="btn btn-ghost btn-sm btn-circle hidden sm:inline-flex" title="Toggle nav display (icons+labels / icons / labels)" aria-label="Toggle nav display">
|
|
<svg id="nav-mode-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/>
|
|
</svg>
|
|
</button>
|
|
<button id="theme-toggle" class="btn btn-ghost btn-sm btn-circle" title="Toggle theme" aria-label="Toggle theme">
|
|
<svg id="icon-sun" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M12 8a4 4 0 100 8 4 4 0 000-8z"/>
|
|
</svg>
|
|
<svg id="icon-moon" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 12.79A9 9 0 1111.21 3a7 7 0 009.79 9.79z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
|
{% block content %}{% endblock content %}
|
|
</main>
|
|
|
|
{% block scripts %}{% endblock scripts %}
|
|
<script>
|
|
(function () {
|
|
var html = document.documentElement;
|
|
|
|
// ── Theme ──────────────────────────────────────────────────────────────
|
|
var DARK = "dark", LIGHT = "light", THEME_KEY = "ontoref-theme";
|
|
var btn = document.getElementById("theme-toggle");
|
|
var iconSun = document.getElementById("icon-sun");
|
|
var iconMoon = document.getElementById("icon-moon");
|
|
function currentTheme() { return html.getAttribute("data-theme") === DARK ? DARK : LIGHT; }
|
|
|
|
var logoLight = document.getElementById("proj-logo-light");
|
|
var logoDark = document.getElementById("proj-logo-dark");
|
|
var logoLightM = document.getElementById("proj-logo-light-m");
|
|
var logoDarkM = document.getElementById("proj-logo-dark-m");
|
|
function applyLogos(theme) {
|
|
if (logoLight && !logoDark) {
|
|
logoLight.style.display = "";
|
|
} else {
|
|
if (logoLight) logoLight.style.display = (theme === DARK) ? "none" : "";
|
|
if (logoDark) logoDark.style.display = (theme === DARK) ? "" : "none";
|
|
}
|
|
if (logoLightM && !logoDarkM) {
|
|
logoLightM.style.display = "";
|
|
} else {
|
|
if (logoLightM) logoLightM.style.display = (theme === DARK) ? "none" : "";
|
|
if (logoDarkM) logoDarkM.style.display = (theme === DARK) ? "" : "none";
|
|
}
|
|
}
|
|
function applyTheme(theme) {
|
|
html.setAttribute("data-theme", theme);
|
|
localStorage.setItem(THEME_KEY, theme);
|
|
if (theme === DARK) { iconSun.classList.remove("hidden"); iconMoon.classList.add("hidden"); }
|
|
else { iconMoon.classList.remove("hidden"); iconSun.classList.add("hidden"); }
|
|
applyLogos(theme);
|
|
}
|
|
applyTheme(currentTheme());
|
|
btn.addEventListener("click", function () {
|
|
applyTheme(currentTheme() === DARK ? LIGHT : DARK);
|
|
});
|
|
|
|
// ── Nav display mode ───────────────────────────────────────────────────
|
|
var NAV_KEY = "ontoref-nav-mode";
|
|
var NAV_MODES = ["both", "icons", "names"];
|
|
var navBtn = document.getElementById("nav-mode-btn");
|
|
var navIcon = document.getElementById("nav-mode-icon");
|
|
|
|
// Icon paths per mode: both=list, icons=grid, names=text
|
|
var NAV_ICONS = {
|
|
both: "M4 6h16M4 12h16M4 18h7",
|
|
icons: "M4 6h4M4 12h4M4 18h4M10 6h10M10 12h10M10 18h10",
|
|
names: "M4 6h16M4 12h16M4 18h16"
|
|
};
|
|
|
|
function currentNavMode() { return localStorage.getItem(NAV_KEY) || "both"; }
|
|
function applyNavMode(mode) {
|
|
html.classList.remove("nav-icons", "nav-names");
|
|
if (mode === "icons") html.classList.add("nav-icons");
|
|
if (mode === "names") html.classList.add("nav-names");
|
|
localStorage.setItem(NAV_KEY, mode);
|
|
if (navIcon) navIcon.setAttribute("d", NAV_ICONS[mode] || NAV_ICONS.both);
|
|
if (navBtn) navBtn.title = "Nav: " + mode + " — click to change";
|
|
}
|
|
applyNavMode(currentNavMode());
|
|
if (navBtn) {
|
|
navBtn.addEventListener("click", function () {
|
|
var cur = currentNavMode();
|
|
var next = NAV_MODES[(NAV_MODES.indexOf(cur) + 1) % NAV_MODES.length];
|
|
applyNavMode(next);
|
|
});
|
|
}
|
|
|
|
// ── Last project ───────────────────────────────────────────────────────
|
|
{% if slug %}
|
|
try { localStorage.setItem("ontoref-last-project", "{{ slug }}"); } catch (_) {}
|
|
{% endif %}
|
|
|
|
})();
|
|
|
|
// ── Browser session auto-registration ─────────────────────────────────
|
|
{% if slug %}
|
|
(function () {
|
|
var SLUG = "{{ slug }}";
|
|
var SK = "ontoref-session:" + SLUG;
|
|
var TTL = 29 * 60 * 1000; // 29 min — shorter than server stale timeout
|
|
var HB_SECS = 60 * 1000; // heartbeat every 60s
|
|
|
|
function stored() {
|
|
try { return JSON.parse(sessionStorage.getItem(SK)); } catch(_) { return null; }
|
|
}
|
|
|
|
function currentPrefs() {
|
|
return {
|
|
theme: localStorage.getItem("ontoref-theme") || "dark",
|
|
nav_mode: localStorage.getItem("ontoref-nav-mode") || "both",
|
|
};
|
|
}
|
|
|
|
function syncProfile(token) {
|
|
fetch("/actors/" + encodeURIComponent(token) + "/profile", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ preferences: currentPrefs() }),
|
|
}).catch(function(){});
|
|
}
|
|
|
|
function register() {
|
|
var pid = Math.floor(Math.random() * 90000) + 10000;
|
|
fetch("/actors/register", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
actor_type: "browser",
|
|
hostname: window.location.hostname || "browser",
|
|
pid: pid,
|
|
project: SLUG,
|
|
role: "viewer",
|
|
preferences: currentPrefs(),
|
|
}),
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
try {
|
|
sessionStorage.setItem(SK, JSON.stringify({
|
|
token: data.token,
|
|
expires_at: Date.now() + TTL,
|
|
}));
|
|
} catch(_) {}
|
|
})
|
|
.catch(function(){});
|
|
}
|
|
|
|
function heartbeat() {
|
|
var s = stored();
|
|
if (!s) { register(); return; }
|
|
if (Date.now() > s.expires_at) { register(); return; }
|
|
fetch("/actors/" + encodeURIComponent(s.token) + "/touch", { method: "POST" })
|
|
.then(function(r) {
|
|
if (r.status === 404) register();
|
|
}).catch(function(){});
|
|
}
|
|
|
|
var s = stored();
|
|
if (!s || Date.now() > s.expires_at) {
|
|
register();
|
|
}
|
|
|
|
setInterval(heartbeat, HB_SECS);
|
|
})();
|
|
{% endif %}
|
|
|
|
// ── Daemon loopback status ─────────────────────────────────────────────
|
|
(function () {
|
|
var dot = document.getElementById("loopback-dot");
|
|
var host = document.getElementById("loopback-host");
|
|
var details = document.getElementById("loopback-details");
|
|
var addr = document.getElementById("loopback-addr");
|
|
|
|
if (!dot) return; // loopback widget not rendered (single-project without flag, etc.)
|
|
|
|
if (host) host.textContent = window.location.host;
|
|
if (addr) addr.textContent = window.location.origin;
|
|
|
|
function fmtUptime(secs) {
|
|
if (secs < 60) return secs + "s";
|
|
if (secs < 3600) return Math.floor(secs / 60) + "m " + (secs % 60) + "s";
|
|
var h = Math.floor(secs / 3600);
|
|
var m = Math.floor((secs % 3600) / 60);
|
|
return h + "h " + m + "m";
|
|
}
|
|
|
|
function row(label, value) {
|
|
return '<div class="flex justify-between"><span class="text-base-content/40">' +
|
|
label + '</span><span class="font-mono">' + value + '</span></div>';
|
|
}
|
|
|
|
function ping() {
|
|
fetch("/health")
|
|
.then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
|
|
.then(function (d) {
|
|
var total = (d.cache_hits || 0) + (d.cache_misses || 0);
|
|
var rate = total > 0 ? Math.round((d.cache_hits / total) * 100) + "%" : "—";
|
|
if (dot) {
|
|
dot.className = "inline-block w-1.5 h-1.5 rounded-full " +
|
|
(d.status === "ok" ? "bg-success" : "bg-warning");
|
|
}
|
|
if (details) {
|
|
details.innerHTML =
|
|
row("uptime", fmtUptime(d.uptime_secs || 0)) +
|
|
row("actors", d.active_actors || 0) +
|
|
row("cache", (d.cache_entries || 0) + " entries") +
|
|
row("hit rate", rate);
|
|
}
|
|
})
|
|
.catch(function () {
|
|
if (dot) dot.className = "inline-block w-1.5 h-1.5 rounded-full bg-error";
|
|
if (details) details.innerHTML = '<div class="text-error/70 italic">unreachable</div>';
|
|
});
|
|
}
|
|
|
|
window.daemonPing = ping;
|
|
ping();
|
|
setInterval(ping, 30000);
|
|
})();
|
|
</script>
|
|
|
|
<!-- Version footer -->
|
|
<div style="
|
|
position: fixed;
|
|
bottom: 0.75rem;
|
|
right: 1rem;
|
|
font-size: 0.7rem;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
user-select: none;
|
|
opacity: 0.55;
|
|
letter-spacing: 0.03em;
|
|
">
|
|
<a href="https://ontoref.dev" target="_blank" rel="noopener" style="text-decoration: none;">
|
|
<span style="color: #C0CCD8;">onto</span><span style="color: #E8A838; font-weight: 600;">ref</span>
|
|
</a>
|
|
<span style="color: #E8A838; font-weight: 600;">v{{ daemon_version | default(value="") }}</span>
|
|
<span style="color: #475569; margin: 0 0.25rem;">|</span>
|
|
<span style="color: #64748b;">2026</span>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|