115 lines
5.8 KiB
HTML
115 lines
5.8 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Substrate Views — Ontoref{% endblock title %}
|
|
{% block nav_views %}active{% endblock nav_views %}
|
|
{% block nav_group_knowledge %}active{% endblock nav_group_knowledge %}
|
|
|
|
{% block content %}
|
|
<div class="mb-4 flex items-center gap-3">
|
|
<h1 class="text-xl font-bold">Substrate Views</h1>
|
|
<span class="badge badge-ghost badge-sm">ADR-046</span>
|
|
<span id="views-count" class="badge badge-ghost badge-sm font-mono hidden">0</span>
|
|
<div class="flex-1"></div>
|
|
<span class="text-xs text-base-content/50 font-mono">composed recipe = prior ⊕ ops</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 h-[calc(100vh-200px)] min-h-[440px]">
|
|
<!-- Left: declared views by discipline -->
|
|
<div class="lg:col-span-1 flex flex-col bg-base-200 rounded-lg overflow-hidden border border-base-content/10">
|
|
<div class="p-3 border-b border-base-content/10 flex-shrink-0 text-sm font-bold text-base-content/70">Disciplines</div>
|
|
<div id="views-list" class="flex-1 overflow-y-auto p-2 flex flex-col gap-1">
|
|
<span class="loading loading-spinner loading-sm m-3"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: mounted recipe for the selected discipline -->
|
|
<div class="lg:col-span-2 flex flex-col bg-base-200 rounded-lg overflow-hidden border border-base-content/10">
|
|
<div id="mount-panel" class="flex-1 overflow-y-auto p-4 text-sm text-base-content/60">
|
|
Select a discipline to mount its substrate view.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const listEl = document.getElementById("views-list");
|
|
const countEl = document.getElementById("views-count");
|
|
const panelEl = document.getElementById("mount-panel");
|
|
|
|
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, (c) =>
|
|
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
|
|
const SLUG = {{ slug | json_encode() | safe }};
|
|
|
|
async function loadViews() {
|
|
try {
|
|
const url = SLUG ? `/api/views?slug=${encodeURIComponent(SLUG)}` : "/api/views";
|
|
const res = await fetch(url);
|
|
const data = await res.json();
|
|
const views = data.views || [];
|
|
countEl.textContent = views.length;
|
|
countEl.classList.remove("hidden");
|
|
if (!views.length) {
|
|
listEl.innerHTML = '<div class="text-sm text-base-content/50 p-2">No views declared.</div>';
|
|
return;
|
|
}
|
|
listEl.innerHTML = views.map((v) =>
|
|
`<button class="view-item btn btn-ghost btn-sm justify-start w-full font-mono text-left gap-1.5" data-d="${esc(v.discipline)}">
|
|
<span class="font-bold">${esc(v.discipline)}</span>
|
|
${v.context ? `<span class="badge badge-ghost badge-xs">${esc(v.context)}</span>` : ""}
|
|
${v.learns_from ? `<span class="badge badge-success badge-xs">learns</span>` : ""}
|
|
</button>`).join("");
|
|
listEl.querySelectorAll(".view-item").forEach((b) =>
|
|
b.addEventListener("click", () => mount(b.dataset.d)));
|
|
} catch (e) {
|
|
listEl.innerHTML = `<div class="alert alert-error text-xs">${esc(e)}</div>`;
|
|
}
|
|
}
|
|
|
|
async function mount(discipline) {
|
|
panelEl.innerHTML = '<span class="loading loading-spinner loading-sm"></span>';
|
|
const slugParam = SLUG ? `?slug=${encodeURIComponent(SLUG)}` : "";
|
|
const res = await fetch(`/api/views/${encodeURIComponent(discipline)}/mount${slugParam}`);
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
panelEl.innerHTML = `<div class="alert alert-error text-sm">${esc(err.error || res.statusText)}` +
|
|
(err.contexts ? ` — contexts: ${esc(err.contexts.join(", "))}` : "") + `</div>`;
|
|
return;
|
|
}
|
|
const m = await res.json();
|
|
const badge = m.learning_active
|
|
? `<span class="badge badge-success badge-sm">tuned · ${m.adjustments_applied} ops</span>`
|
|
: `<span class="badge badge-ghost badge-sm">prior</span>`;
|
|
const levels = (m.levels || []).map((l) => {
|
|
const delta = l.learned_delta ? ` <span class="text-success">Δ${l.learned_delta}</span>` : "";
|
|
const resolve = l.persistence === "Inline" ? "local" : `witness:${esc(l.verify)}`;
|
|
return `<tr>
|
|
<td class="font-mono">${esc(l.ref)}</td>
|
|
<td class="text-xs">${esc(l.axis)}/${esc(l.persistence)}</td>
|
|
<td class="text-xs">${esc(l.eligibility)}</td>
|
|
<td class="font-mono text-right">${esc(l.salience)}${delta}</td>
|
|
<td class="text-xs">${resolve}</td>
|
|
</tr>`;
|
|
}).join("");
|
|
const actions = (m.actions || []).map((a) =>
|
|
`<li><span class="font-mono">${esc(a.id)}</span> <span class="badge badge-xs">${esc(a.access)}</span> → ${esc(a.level)}` +
|
|
(a.operation ? ` <span class="badge badge-outline badge-xs">op:${esc(a.operation)}</span>` : "") + `</li>`).join("");
|
|
panelEl.innerHTML = `
|
|
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
|
<h2 class="font-bold font-mono text-base">${esc(m.view)}</h2>${badge}
|
|
${m.learns_from ? `<span class="badge badge-ghost badge-xs font-mono">${esc(m.learns_from)}</span>` : ""}
|
|
</div>
|
|
<div class="text-xs text-base-content/50 mb-3">discipline=${esc(m.discipline)}${m.context ? ` · context=${esc(m.context)}` : ""}</div>
|
|
<table class="table table-xs">
|
|
<thead><tr><th>level</th><th>axis/persist</th><th>elig</th><th class="text-right">salience</th><th>resolve</th></tr></thead>
|
|
<tbody>${levels}</tbody>
|
|
</table>
|
|
<div class="mt-4 text-sm font-bold">Actions</div>
|
|
<ul class="text-sm list-disc ml-5 mt-1">${actions || '<li class="text-base-content/50">none</li>'}</ul>
|
|
<div class="mt-4 text-sm"><span class="font-bold">Surfaces:</span> <span class="font-mono">${esc((m.surfaces || []).join(", ")) || "—"}</span></div>`;
|
|
}
|
|
|
|
loadViews();
|
|
})();
|
|
</script>
|
|
{% endblock content %}
|