Jesús Pérez 75892a8eea
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
feat: browser-style panel nav, repo file routing, migration 0007
graph, search, api_catalog pages: back/forward history stack (PanelNav/dpNav).
  File artifact paths open in external tabs via card.repo (Gitea source URL) or
  card.docs (cargo docs for .rs) — openFile/openFileInPanel removed from all pages.
  Tera | safe required for URL values inside <script> blocks (auto-escape of slashes).

  card.ncl: repo field added.
  insert_brand_ctx: injects card_repo/card_docs into Tera context.
  #[onto_api] proc-macro: source_file = file!() emitted; ApiRouteEntry.source_file
  populated in primary catalog handler.

  migration 0007-card-repo-field: check card.ncl for repo field; skip if absent.
2026-03-29 08:32:50 +01:00

425 lines
19 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 — always rendered so HTMX always has a swap target -->
<div id="backlog-items-container">
{% 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>
{% include "partials/backlog_tbody.html" %}
</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 %}
</div>
{% 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 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">
<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>
<!-- 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>
{% endblock content %}
{% block scripts %}
<script>
// ── 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;
}
// ── 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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 %}