2026-03-13 00:18:14 +00:00
|
|
|
{% extends "base.html" %}
|
|
|
|
|
{% block title %}Backlog — Ontoref{% endblock title %}
|
|
|
|
|
{% block nav_backlog %}active{% endblock nav_backlog %}
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
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
2026-03-26 20:20:22 +00:00
|
|
|
{% block nav_group_track %}active{% endblock nav_group_track %}
|
2026-03-13 00:18:14 +00:00
|
|
|
|
|
|
|
|
{% 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>
|
|
|
|
|
|
2026-03-29 08:32:50 +01:00
|
|
|
<!-- Items table — always rendered so HTMX always has a swap target -->
|
|
|
|
|
<div id="backlog-items-container">
|
2026-03-13 00:18:14 +00:00
|
|
|
{% 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>
|
2026-03-29 08:32:50 +01:00
|
|
|
{% include "partials/backlog_tbody.html" %}
|
2026-03-13 00:18:14 +00:00
|
|
|
</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 %}
|
2026-03-29 08:32:50 +01:00
|
|
|
</div>
|
2026-03-13 00:18:14 +00:00
|
|
|
|
|
|
|
|
{% 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>
|
2026-03-29 08:32:50 +01:00
|
|
|
<form hx-post="{{ base_url }}/backlog/add"
|
|
|
|
|
hx-target="#backlog-items-container"
|
|
|
|
|
hx-swap="innerHTML"
|
|
|
|
|
hx-on::after-request="if(event.detail.successful){document.getElementById('add-modal').close();this.reset()}"
|
|
|
|
|
class="space-y-3">
|
2026-03-13 00:18:14 +00:00
|
|
|
<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>
|
|
|
|
|
|
2026-03-29 08:32:50 +01:00
|
|
|
<!-- Edit item modal -->
|
|
|
|
|
<dialog id="edit-modal" class="modal">
|
|
|
|
|
<div class="modal-box max-w-xl">
|
|
|
|
|
<h3 class="font-bold text-base mb-4">Edit Backlog Item <span id="edit-modal-id" class="font-mono text-xs text-base-content/40"></span></h3>
|
|
|
|
|
<form hx-post="{{ base_url }}/backlog/edit"
|
|
|
|
|
hx-target="#backlog-items-container"
|
|
|
|
|
hx-swap="innerHTML"
|
|
|
|
|
hx-on::after-request="if(event.detail.successful){document.getElementById('edit-modal').close()}"
|
|
|
|
|
class="space-y-3">
|
|
|
|
|
<input type="hidden" name="id" id="edit-id">
|
|
|
|
|
<div class="form-control">
|
|
|
|
|
<label class="label py-1"><span class="label-text text-sm">Title</span></label>
|
|
|
|
|
<input type="text" name="title" id="edit-title" required
|
|
|
|
|
class="input input-bordered input-sm w-full">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="grid grid-cols-3 gap-3">
|
|
|
|
|
<div class="form-control">
|
|
|
|
|
<label class="label py-1"><span class="label-text text-sm">Status</span></label>
|
|
|
|
|
<select name="status" id="edit-status" class="select select-bordered select-sm">
|
|
|
|
|
<option value="Open">Open</option>
|
|
|
|
|
<option value="InProgress">In Progress</option>
|
|
|
|
|
<option value="Done">Done</option>
|
|
|
|
|
<option value="Cancelled">Cancelled</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-control">
|
|
|
|
|
<label class="label py-1"><span class="label-text text-sm">Kind</span></label>
|
|
|
|
|
<select name="kind" id="edit-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" id="edit-priority" class="select select-bordered select-sm">
|
|
|
|
|
<option value="Medium">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" id="edit-detail" rows="3"
|
|
|
|
|
class="textarea textarea-bordered textarea-sm w-full"></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
|
|
|
<div class="form-control">
|
|
|
|
|
<label class="label py-1">
|
|
|
|
|
<span class="label-text text-sm">Related ADRs</span>
|
|
|
|
|
<span class="label-text-alt text-base-content/40">comma-separated</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input type="text" name="related_adrs" id="edit-related-adrs"
|
|
|
|
|
placeholder="adr-001-title, adr-002-title"
|
|
|
|
|
class="input input-bordered input-sm w-full font-mono text-xs">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-control">
|
|
|
|
|
<label class="label py-1">
|
|
|
|
|
<span class="label-text text-sm">Related Modes</span>
|
|
|
|
|
<span class="label-text-alt text-base-content/40">comma-separated</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input type="text" name="related_modes" id="edit-related-modes"
|
|
|
|
|
placeholder="develop, coder-workflow"
|
|
|
|
|
class="input input-bordered input-sm w-full font-mono text-xs">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-control">
|
|
|
|
|
<label class="label py-1"><span class="label-text text-sm">Graduates to</span></label>
|
|
|
|
|
<select name="graduates_to" id="edit-graduates-to" class="select select-bordered select-sm">
|
|
|
|
|
<option value="">— unchanged —</option>
|
|
|
|
|
<option value="Adr">ADR</option>
|
|
|
|
|
<option value="Mode">Mode</option>
|
|
|
|
|
<option value="StateTransition">State Transition</option>
|
|
|
|
|
<option value="PrItem">PR Item</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-action mt-2 flex justify-between">
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<span id="edit-delete-confirm" class="hidden items-center gap-2">
|
|
|
|
|
<span class="text-xs text-error font-medium">Delete this item?</span>
|
|
|
|
|
<button type="button" class="btn btn-xs btn-error" onclick="editModalDeleteConfirmed()">Yes, delete</button>
|
|
|
|
|
<button type="button" class="btn btn-xs btn-ghost" onclick="editDeleteCancel()">Cancel</button>
|
|
|
|
|
</span>
|
|
|
|
|
<button type="button" id="edit-delete-btn"
|
|
|
|
|
class="btn btn-sm btn-error btn-outline"
|
|
|
|
|
onclick="editDeleteAsk()">✕ Delete</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="btn btn-sm btn-ghost"
|
|
|
|
|
onclick="editModalSendToProject()">↗ Send to project</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<button type="button" onclick="document.getElementById('edit-modal').close()"
|
|
|
|
|
class="btn btn-sm btn-ghost">Cancel</button>
|
|
|
|
|
<button type="submit" class="btn btn-sm btn-primary">Save</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
|
|
|
</dialog>
|
|
|
|
|
|
2026-03-13 00:18:14 +00:00
|
|
|
{% endblock content %}
|
|
|
|
|
|
|
|
|
|
{% block scripts %}
|
|
|
|
|
<script>
|
2026-03-29 08:32:50 +01:00
|
|
|
// ── Edit modal ────────────────────────────────────────────────────────────────
|
|
|
|
|
function openEditModal(row) {
|
|
|
|
|
document.getElementById('edit-id').value = row.dataset.id;
|
|
|
|
|
document.getElementById('edit-modal-id').textContent = row.dataset.id;
|
|
|
|
|
document.getElementById('edit-title').value = row.dataset.title;
|
|
|
|
|
document.getElementById('edit-detail').value = row.dataset.detail;
|
|
|
|
|
document.getElementById('edit-related-adrs').value = row.dataset.relatedAdrs || '';
|
|
|
|
|
document.getElementById('edit-related-modes').value = row.dataset.relatedModes || '';
|
|
|
|
|
function sel(id, val) {
|
|
|
|
|
var el = document.getElementById(id);
|
|
|
|
|
for (var o of el.options) o.selected = (o.value === val);
|
|
|
|
|
}
|
|
|
|
|
sel('edit-kind', row.dataset.kind);
|
|
|
|
|
sel('edit-priority', row.dataset.priorityVal);
|
|
|
|
|
sel('edit-status', row.dataset.statusVal);
|
|
|
|
|
sel('edit-graduates-to', row.dataset.graduatesTo || '');
|
|
|
|
|
editDeleteCancel();
|
|
|
|
|
document.getElementById('edit-modal').showModal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function editDeleteAsk() {
|
|
|
|
|
document.getElementById('edit-delete-btn').classList.add('hidden');
|
|
|
|
|
var c = document.getElementById('edit-delete-confirm');
|
|
|
|
|
c.classList.remove('hidden');
|
|
|
|
|
c.classList.add('flex');
|
|
|
|
|
}
|
|
|
|
|
function editDeleteCancel() {
|
|
|
|
|
document.getElementById('edit-delete-confirm').classList.add('hidden');
|
|
|
|
|
document.getElementById('edit-delete-confirm').classList.remove('flex');
|
|
|
|
|
document.getElementById('edit-delete-btn').classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
function editModalDeleteConfirmed() {
|
|
|
|
|
var id = document.getElementById('edit-id').value;
|
|
|
|
|
document.getElementById('edit-modal').close();
|
|
|
|
|
editDeleteCancel();
|
|
|
|
|
htmx.ajax('POST', '{{ base_url | safe }}/backlog/delete', {
|
|
|
|
|
target: '#backlog-items-container',
|
|
|
|
|
swap: 'innerHTML',
|
|
|
|
|
values: { id: id }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function editModalSendToProject() {
|
|
|
|
|
var id = document.getElementById('edit-id').value;
|
|
|
|
|
var title = document.getElementById('edit-title').value;
|
|
|
|
|
var status = document.getElementById('edit-status').value;
|
|
|
|
|
var priority = document.getElementById('edit-priority').value;
|
|
|
|
|
var payload = JSON.stringify({ item_id: id, status: status, priority: priority });
|
|
|
|
|
var base = '{{ base_url | safe }}';
|
|
|
|
|
var url = base + '/notifications?kind=backlog_delegation'
|
|
|
|
|
+ '&title=' + encodeURIComponent(id + ': ' + title)
|
|
|
|
|
+ '&payload=' + encodeURIComponent(payload);
|
|
|
|
|
document.getElementById('edit-modal').close();
|
|
|
|
|
window.location.href = url;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 00:18:14 +00:00
|
|
|
// ── 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 %}
|