333 lines
14 KiB
HTML
Raw Normal View History

2026-03-13 00:18:14 +00:00
{% extends "base.html" %}
{% block title %}Backlog — Ontoref{% endblock title %}
{% block nav_backlog %}active{% endblock nav_backlog %}
{% 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, '&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 %}