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
334 lines
14 KiB
HTML
334 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Backlog — Ontoref{% endblock title %}
|
|
{% block nav_backlog %}active{% endblock nav_backlog %}
|
|
{% block nav_group_track %}active{% endblock nav_group_track %}
|
|
|
|
{% block content %}
|
|
<div class="mb-5 flex items-center justify-between">
|
|
<h1 class="text-xl font-bold">Backlog</h1>
|
|
<button onclick="document.getElementById('add-modal').showModal()"
|
|
class="btn btn-sm btn-primary gap-1.5">
|
|
<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="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
New item
|
|
</button>
|
|
</div>
|
|
|
|
{% if not has_backlog %}
|
|
<div class="flex flex-col items-center justify-center py-16 text-base-content/40 text-sm">
|
|
<p>No <code class="font-mono">reflection/backlog.ncl</code> found in this project.</p>
|
|
</div>
|
|
{% else %}
|
|
|
|
<!-- Dashboard summary -->
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6">
|
|
<div class="stat bg-base-200 rounded-lg p-4">
|
|
<div class="stat-title text-xs">Open</div>
|
|
<div class="stat-value text-2xl text-info">{{ stats.open }}</div>
|
|
</div>
|
|
<div class="stat bg-base-200 rounded-lg p-4">
|
|
<div class="stat-title text-xs">In Progress</div>
|
|
<div class="stat-value text-2xl text-warning">{{ stats.inprog }}</div>
|
|
</div>
|
|
<div class="stat bg-base-200 rounded-lg p-4">
|
|
<div class="stat-title text-xs">Done</div>
|
|
<div class="stat-value text-2xl text-success">{{ stats.done }}</div>
|
|
</div>
|
|
<div class="stat bg-base-200 rounded-lg p-4">
|
|
<div class="stat-title text-xs">Critical</div>
|
|
<div class="stat-value text-2xl text-error">{{ stats.critical }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="flex flex-wrap gap-2 mb-4" id="filter-bar">
|
|
<button class="btn btn-xs filter-btn active-filter" data-filter="all">All ({{ stats.total }})</button>
|
|
<button class="btn btn-xs filter-btn" data-filter="Open">Open ({{ stats.open }})</button>
|
|
<button class="btn btn-xs filter-btn" data-filter="InProgress">In Progress ({{ stats.inprog }})</button>
|
|
<button class="btn btn-xs filter-btn" data-filter="Done">Done ({{ stats.done }})</button>
|
|
<div class="ml-auto flex gap-1.5">
|
|
<button class="btn btn-xs priority-btn active-priority" data-priority="all">All priorities</button>
|
|
<button class="btn btn-xs priority-btn" data-priority="Critical">Critical</button>
|
|
<button class="btn btn-xs priority-btn" data-priority="High">High</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Items table -->
|
|
{% if items %}
|
|
<div class="overflow-x-auto rounded-lg border border-base-content/10">
|
|
<table class="table table-sm w-full">
|
|
<thead>
|
|
<tr class="text-xs text-base-content/40 uppercase tracking-wider">
|
|
<th class="w-20">ID</th>
|
|
<th class="w-24">Status</th>
|
|
<th class="w-20">Priority</th>
|
|
<th class="w-16">Kind</th>
|
|
<th>Title</th>
|
|
<th class="w-24 text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="backlog-tbody">
|
|
{% for it in items %}
|
|
<tr class="backlog-row hover:bg-base-200/50"
|
|
data-status="{{ it.status }}"
|
|
data-priority="{{ it.priority }}">
|
|
<td class="font-mono text-xs text-base-content/50">{{ it.id }}</td>
|
|
<td>
|
|
<span class="badge badge-xs
|
|
{% if it.status == "Open" %}badge-info
|
|
{% elif it.status == "InProgress" %}badge-warning
|
|
{% elif it.status == "Done" %}badge-success
|
|
{% else %}badge-ghost{% endif %}">{{ it.status }}</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-xs
|
|
{% if it.priority == "Critical" %}badge-error
|
|
{% elif it.priority == "High" %}badge-warning
|
|
{% else %}badge-ghost{% endif %}">{{ it.priority }}</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-xs badge-ghost">{{ it.kind }}</span>
|
|
</td>
|
|
<td>
|
|
<div class="font-medium text-sm leading-tight">{{ it.title }}</div>
|
|
{% if it.detail %}
|
|
<div class="text-xs text-base-content/50 leading-tight mt-0.5 line-clamp-1">{{ it.detail }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-right">
|
|
<div class="dropdown dropdown-end">
|
|
<button tabindex="0" class="btn btn-xs btn-ghost">▾</button>
|
|
<ul tabindex="0"
|
|
class="dropdown-content menu menu-xs bg-base-200 shadow rounded-box z-50 w-40 p-1">
|
|
{% if it.status != "InProgress" %}
|
|
<li>
|
|
<form method="post" action="{{ base_url }}/backlog/status">
|
|
<input type="hidden" name="id" value="{{ it.id }}">
|
|
<input type="hidden" name="status" value="InProgress">
|
|
<button type="submit" class="w-full text-left">→ In Progress</button>
|
|
</form>
|
|
</li>
|
|
{% endif %}
|
|
{% if it.status != "Done" %}
|
|
<li>
|
|
<form method="post" action="{{ base_url }}/backlog/status">
|
|
<input type="hidden" name="id" value="{{ it.id }}">
|
|
<input type="hidden" name="status" value="Done">
|
|
<button type="submit" class="w-full text-left">✓ Done</button>
|
|
</form>
|
|
</li>
|
|
{% endif %}
|
|
{% if it.status != "Open" %}
|
|
<li>
|
|
<form method="post" action="{{ base_url }}/backlog/status">
|
|
<input type="hidden" name="id" value="{{ it.id }}">
|
|
<input type="hidden" name="status" value="Open">
|
|
<button type="submit" class="w-full text-left">↩ Reopen</button>
|
|
</form>
|
|
</li>
|
|
{% endif %}
|
|
<li>
|
|
<form method="post" action="{{ base_url }}/backlog/status">
|
|
<input type="hidden" name="id" value="{{ it.id }}">
|
|
<input type="hidden" name="status" value="Cancelled">
|
|
<button type="submit" class="w-full text-left text-error">✕ Cancel</button>
|
|
</form>
|
|
</li>
|
|
<li class="border-t border-base-content/10 mt-1 pt-1">
|
|
<a href="{{ base_url }}/notifications?kind=backlog_delegation&title={{ it.id | urlencode }}%3A%20{{ it.title | urlencode }}&payload=%7B%22item_id%22%3A%22{{ it.id | urlencode }}%22%2C%22status%22%3A%22{{ it.status | urlencode }}%22%2C%22priority%22%3A%22{{ it.priority | urlencode }}%22%7D"
|
|
class="w-full text-left">↗ Send to project</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-10 text-base-content/30 text-sm border border-base-content/10 rounded-lg">
|
|
No backlog items yet.
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
|
|
{% if slug %}
|
|
<!-- Cross-project backlog panel (multi-project mode only) -->
|
|
<div class="mt-8 border border-base-content/10 rounded-lg p-4">
|
|
<h2 class="text-sm font-semibold text-base-content/60 uppercase tracking-wide mb-3">Cross-project backlog</h2>
|
|
<div class="flex gap-2 mb-4">
|
|
<input id="peer-slug-input" type="text" placeholder="peer-project-slug"
|
|
class="input input-bordered input-sm flex-1 font-mono max-w-xs">
|
|
<button id="btn-load-peer" class="btn btn-sm btn-ghost gap-1.5">
|
|
<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="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>
|
|
Load
|
|
</button>
|
|
</div>
|
|
<div id="peer-backlog">
|
|
<p class="text-xs text-base-content/30 italic">Enter a project slug and click Load to view its backlog.</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Add item modal -->
|
|
<dialog id="add-modal" class="modal">
|
|
<div class="modal-box max-w-lg">
|
|
<h3 class="font-bold text-base mb-4">New Backlog Item</h3>
|
|
<form method="post" action="{{ base_url }}/backlog/add" class="space-y-3">
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">Title</span></label>
|
|
<input type="text" name="title" required placeholder="Short description of the item"
|
|
class="input input-bordered input-sm w-full">
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">Kind</span></label>
|
|
<select name="kind" class="select select-bordered select-sm">
|
|
<option value="Todo">Todo</option>
|
|
<option value="Wish">Wish</option>
|
|
<option value="Idea">Idea</option>
|
|
<option value="Bug">Bug</option>
|
|
<option value="Debt">Debt</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">Priority</span></label>
|
|
<select name="priority" class="select select-bordered select-sm">
|
|
<option value="Medium" selected>Medium</option>
|
|
<option value="High">High</option>
|
|
<option value="Critical">Critical</option>
|
|
<option value="Low">Low</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">Detail</span></label>
|
|
<textarea name="detail" rows="3" placeholder="Optional detail or acceptance criteria"
|
|
class="textarea textarea-bordered textarea-sm w-full"></textarea>
|
|
</div>
|
|
<div class="modal-action mt-2">
|
|
<button type="button" onclick="document.getElementById('add-modal').close()"
|
|
class="btn btn-sm btn-ghost">Cancel</button>
|
|
<button type="submit" class="btn btn-sm btn-primary">Add item</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
|
|
{% endblock content %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// ── Filter logic ──────────────────────────────────────────────────────────────
|
|
let activeStatus = 'all';
|
|
let activePriority = 'all';
|
|
|
|
function applyFilters() {
|
|
document.querySelectorAll('.backlog-row').forEach(row => {
|
|
const s = row.dataset.status;
|
|
const p = row.dataset.priority;
|
|
const showStatus = activeStatus === 'all' || s === activeStatus;
|
|
const showPriority = activePriority === 'all' || p === activePriority;
|
|
row.classList.toggle('hidden', !(showStatus && showPriority));
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active-filter', 'btn-primary'));
|
|
btn.classList.add('active-filter', 'btn-primary');
|
|
activeStatus = btn.dataset.filter;
|
|
applyFilters();
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.priority-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.priority-btn').forEach(b => b.classList.remove('active-priority', 'btn-accent'));
|
|
btn.classList.add('active-priority', 'btn-accent');
|
|
activePriority = btn.dataset.priority;
|
|
applyFilters();
|
|
});
|
|
});
|
|
|
|
// Initialise active button styles
|
|
document.querySelector('[data-filter="all"]').classList.add('btn-primary');
|
|
document.querySelector('[data-priority="all"]').classList.add('btn-accent');
|
|
|
|
// ── Cross-project backlog ──────────────────────────────────────────────────
|
|
(function () {
|
|
var peerInput = document.getElementById('peer-slug-input');
|
|
var peerBtn = document.getElementById('btn-load-peer');
|
|
var peerDiv = document.getElementById('peer-backlog');
|
|
if (!peerBtn) return;
|
|
|
|
function statusBadge(s) {
|
|
var cls = s === 'Open' ? 'badge-info' : s === 'InProgress' ? 'badge-warning' : s === 'Done' ? 'badge-success' : 'badge-ghost';
|
|
return '<span class="badge badge-xs ' + cls + '">' + esc(s) + '</span>';
|
|
}
|
|
function priorityBadge(p) {
|
|
var cls = p === 'Critical' ? 'badge-error' : p === 'High' ? 'badge-warning' : 'badge-ghost';
|
|
return '<span class="badge badge-xs ' + cls + '">' + esc(p) + '</span>';
|
|
}
|
|
function esc(s) {
|
|
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
async function loadPeer() {
|
|
var slug = peerInput.value.trim();
|
|
if (!slug) { peerInput.focus(); return; }
|
|
|
|
peerDiv.innerHTML = '<div class="flex items-center gap-2 text-xs text-base-content/40 py-2">'
|
|
+ '<span class="loading loading-spinner loading-xs"></span> Loading…</div>';
|
|
|
|
var url = '/backlog-json?slug=' + encodeURIComponent(slug);
|
|
var data;
|
|
try {
|
|
var res = await fetch(url);
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
data = await res.json();
|
|
} catch (err) {
|
|
peerDiv.innerHTML = '<p class="text-xs text-error">Failed to load backlog for <code>' + esc(slug) + '</code>: ' + esc(String(err)) + '</p>';
|
|
return;
|
|
}
|
|
|
|
var items = data.items || [];
|
|
if (items.length === 0) {
|
|
peerDiv.innerHTML = '<p class="text-xs text-base-content/40 italic">No backlog items for <code>' + esc(slug) + '</code>.</p>';
|
|
return;
|
|
}
|
|
|
|
var rows = items.map(function (it) {
|
|
return '<tr>'
|
|
+ '<td class="font-mono text-xs text-base-content/50">' + esc(it.id) + '</td>'
|
|
+ '<td>' + statusBadge(it.status) + '</td>'
|
|
+ '<td>' + priorityBadge(it.priority) + '</td>'
|
|
+ '<td class="text-sm">' + esc(it.title) + '</td>'
|
|
+ '</tr>';
|
|
}).join('');
|
|
|
|
peerDiv.innerHTML = '<div class="overflow-x-auto rounded-lg border border-base-content/10">'
|
|
+ '<table class="table table-xs w-full">'
|
|
+ '<thead><tr class="text-xs text-base-content/40 uppercase tracking-wider">'
|
|
+ '<th class="w-20">ID</th><th class="w-24">Status</th><th class="w-20">Priority</th><th>Title</th>'
|
|
+ '</tr></thead>'
|
|
+ '<tbody>' + rows + '</tbody>'
|
|
+ '</table></div>'
|
|
+ '<p class="text-xs text-base-content/30 mt-2 font-mono">' + items.length + ' items from ' + esc(slug) + '</p>';
|
|
}
|
|
|
|
peerBtn.addEventListener('click', loadPeer);
|
|
peerInput.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') loadPeer();
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock scripts %}
|