feat(reconciliation): implement deterministic state reconciliation engine and post-commit hook

- Implement ReconciliationEngine in server/src/handlers/reconciliation.rs
- Auto-transition ADRs to implemented, resolve tech debts, and cascade task unblocking
- Ingest git commit events via POST /api/git/commit and scripts/git-reconcile.py post-commit hook
- Add universal pagination, search filtering, and keyboard navigation to dashboard
- Implement non-destructive task TTL expiry sweeper and gate verification
- Implements: ADR-0102, ADR-0103
This commit is contained in:
Riz Ashraf committed 2026-10-07 19:00:44 +01:00
1 parent ec977c24dd
commit 8f924b793a
16 files changed
+2622 -163

No files matched your search

+665 -47
View File
@@ -157,10 +157,10 @@ function toggleFilter(type) {
updateGraphData();
}
function renderFilters() {
const container2 = document.getElementById("graph-filters");
if (!container2)
const container = document.getElementById("graph-filters");
if (!container)
return;
container2.innerHTML = Array.from(allTypes).sort().map((type) => {
container.innerHTML = Array.from(allTypes).sort().map((type) => {
const isActive = activeFilters.has(type);
const color = getColorForType(type).bg || getColorForType(type).background;
return `<button class="filter-chip ${isActive ? "active" : ""}" onclick="toggleFilter('${type}')">
@@ -227,8 +227,8 @@ async function loadGraph() {
rawRelations = data.relations || [];
updateGraphData();
if (!network) {
const container2 = document.getElementById("network-container");
if (!container2)
const container = document.getElementById("network-container");
if (!container)
return;
const options = {
nodes: { shape: "dot", size: 16, font: { size: 12 } },
@@ -257,7 +257,7 @@ async function loadGraph() {
},
interaction: { hover: true, tooltipDelay: 100, zoomView: false }
};
network = new vis.Network(container2, { nodes: nodesData, edges: edgesData }, options);
network = new vis.Network(container, { nodes: nodesData, edges: edgesData }, options);
network.once("stabilizationIterationsDone", function() {
network.setOptions({ physics: { enabled: false } });
});
@@ -267,7 +267,7 @@ async function loadGraph() {
network.on("dragEnd", function() {
network.setOptions({ physics: { enabled: false } });
});
container2.addEventListener("wheel", function(event) {
container.addEventListener("wheel", function(event) {
event.preventDefault();
const direction = event.deltaY > 0 ? -0.15 : 0.15;
zoomGraph(direction);
@@ -283,9 +283,9 @@ async function loadGraph() {
}
} catch (err) {
console.error("Failed to load graph", err);
const container2 = document.getElementById("network-container");
if (container2) {
container2.innerHTML = '<div style="color:#e74c3c;padding:20px;font-weight:bold;">Error loading graph: ' + err.message + "</div>";
const container = document.getElementById("network-container");
if (container) {
container.innerHTML = '<div style="color:#e74c3c;padding:20px;font-weight:bold;">Error loading graph: ' + err.message + "</div>";
}
}
}
@@ -414,19 +414,54 @@ function buildTaskTreeHTML(tasks, parentId, depth = 0) {
});
return html;
}
var cachedTasks = [];
var taskFilterQuery = "";
var taskStatusFilter = "";
function handleTaskFilter(val) {
taskFilterQuery = val.toLowerCase().trim();
applyTaskFilters();
}
function handleTaskStatusFilter(val) {
taskStatusFilter = val.toLowerCase().trim();
applyTaskFilters();
}
function applyTaskFilters() {
const taskContainer = document.getElementById("task-tree-container");
if (!taskContainer)
return;
let filtered = cachedTasks;
if (taskFilterQuery) {
filtered = filtered.filter((t) => t.title && t.title.toLowerCase().includes(taskFilterQuery) || t.description && t.description.toLowerCase().includes(taskFilterQuery) || t.id && t.id.toLowerCase().includes(taskFilterQuery) || t.assigned_agent && t.assigned_agent.toLowerCase().includes(taskFilterQuery));
}
if (taskStatusFilter) {
if (taskStatusFilter === "blocked") {
filtered = filtered.filter((t) => {
const deps = t.dependencies || [];
return deps.some((dId) => {
const dt = cachedTasks.find((x) => x.id === dId);
return dt && dt.status !== "completed" && dt.status !== "done";
});
});
} else {
filtered = filtered.filter((t) => (t.status || "pending").toLowerCase() === taskStatusFilter);
}
}
const rootHtml = buildTaskTreeHTML(filtered, null, 0);
if (!rootHtml) {
if (cachedTasks.length === 0) {
taskContainer.innerHTML = '<div class="enterprise-empty-state">No active tasks.</div>';
} else {
taskContainer.innerHTML = '<div class="enterprise-empty-state">No tasks matching current filter.</div>';
}
} else {
taskContainer.innerHTML = rootHtml;
}
}
async function loadTasks() {
try {
const res = await fetch("/api/tasks");
const tasks = await res.json();
const taskContainer = document.getElementById("task-tree-container");
if (!taskContainer)
return;
const rootHtml = buildTaskTreeHTML(tasks, null, 0);
if (!rootHtml) {
taskContainer.innerHTML = '<div style="color:var(--text-secondary); padding:20px; text-align:center;">No active tasks.</div>';
} else {
taskContainer.innerHTML = rootHtml;
}
cachedTasks = await res.json();
applyTaskFilters();
} catch (err) {
console.error("Failed to load tasks", err);
}
@@ -462,14 +497,14 @@ async function handleSearch(e) {
signal: activeSearchAbortController.signal
});
const data = await res.json();
const container2 = document.getElementById("search-results");
if (!container2)
const container = document.getElementById("search-results");
if (!container)
return;
if (!data.results || data.results.length === 0) {
container2.innerHTML = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>';
container.innerHTML = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>';
return;
}
container2.innerHTML = data.results.map((r) => `
container.innerHTML = data.results.map((r) => `
<div class="search-result-item" onclick="switchTab('graph-tab'); if(rawEntities['${r.id}']){ showInspector('${r.id}'); if(network){ network.selectNodes(['${r.id}']); network.focus('${r.id}', {scale:1.2, animation:true}); } }" style="cursor:pointer;">
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div>
@@ -775,39 +810,341 @@ function setupWS() {
setupSSE();
};
}
async function loadGenericList(endpoint, containerId, formatter) {
var listControllers = new Map;
function applyListFiltering(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
let result = state.items.slice();
const q = (state.searchQuery || "").trim().toLowerCase();
if (q) {
result = result.filter((item) => {
if (state.options.searchFields) {
const fields = state.options.searchFields(item);
return fields.some((f) => f && String(f).toLowerCase().includes(q));
}
return JSON.stringify(item).toLowerCase().includes(q);
});
}
if (state.options.filters) {
for (const filterDef of state.options.filters) {
const activeVal = state.activeFilters[filterDef.id];
if (activeVal !== undefined && activeVal !== "") {
result = result.filter((item) => filterDef.predicate(item, activeVal));
}
}
}
const factor = state.sortOrder === "desc" ? -1 : 1;
const extractor = state.options.sortExtractor || ((item) => {
if (state.options.timestampExtractor) {
const ts = state.options.timestampExtractor(item);
if (ts != null)
return ts;
}
return item.timestamp || item.created_at || item.id || 0;
});
result.sort((a, b) => {
const valA = extractor(a);
const valB = extractor(b);
if (typeof valA === "number" && typeof valB === "number") {
return (valA - valB) * factor;
}
return String(valA).localeCompare(String(valB)) * factor;
});
state.filteredItems = result;
}
function renderListCardsAndPagination(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
const totalItems = state.filteredItems.length;
const pageSize = state.pageSize;
const totalPages = pageSize <= 0 ? 1 : Math.max(1, Math.ceil(totalItems / pageSize));
if (state.currentPage > totalPages) {
state.currentPage = totalPages;
}
if (state.currentPage < 1) {
state.currentPage = 1;
}
const startIndex = totalItems === 0 ? 0 : (state.currentPage - 1) * (pageSize > 0 ? pageSize : totalItems) + 1;
const endIndex = pageSize <= 0 ? totalItems : Math.min(state.currentPage * pageSize, totalItems);
const metaEl = document.getElementById(`${containerId}-meta`);
if (metaEl) {
const itemNoun = state.options.itemNoun || "items";
let summaryText = "";
if (totalItems === 0) {
summaryText = `0 ${itemNoun}`;
} else {
summaryText = `Showing <strong>${startIndex}–${endIndex}</strong> of <strong>${totalItems}</strong> ${itemNoun}`;
}
if (totalItems !== state.items.length) {
summaryText += ` <span style="color:var(--text-secondary);">(filtered from ${state.items.length} total)</span>`;
}
metaEl.innerHTML = `<div>${summaryText}</div><div>Page <strong>${state.currentPage}</strong> of <strong>${totalPages}</strong></div>`;
}
const cardsEl = document.getElementById(`${containerId}-cards`);
if (cardsEl) {
if (totalItems === 0) {
const itemNoun = state.options.itemNoun || "items";
if (state.items.length === 0) {
cardsEl.innerHTML = `<div class="enterprise-empty-state">No ${itemNoun} recorded.</div>`;
} else {
cardsEl.innerHTML = `
<div class="enterprise-empty-state">
<div>\uD83D\uDD0D No matching ${itemNoun} found for current filter.</div>
<button class="enterprise-btn" style="margin-top:12px;" onclick="onListResetFilters('${containerId}')">Reset Filters</button>
</div>`;
}
} else {
const pageSlice = pageSize <= 0 ? state.filteredItems : state.filteredItems.slice((state.currentPage - 1) * pageSize, state.currentPage * pageSize);
cardsEl.innerHTML = pageSlice.map((item) => {
const ts = state.options.timestampExtractor ? state.options.timestampExtractor(item) : item.timestamp || item.created_at || null;
const dateStr = ts ? new Date(ts * 1000).toLocaleString() : "";
const borderCol = state.options.cardBorderColor || "#9b59b6";
return `
<div class="task-card" style="border-left-color: ${borderCol};">
${dateStr ? `<div style="font-size:0.75em; color:var(--text-secondary); float:right;">${dateStr}</div>` : ""}
${state.formatter(item)}
</div>`;
}).join("");
}
}
const paginationEl = document.getElementById(`${containerId}-pagination`);
if (paginationEl) {
if (totalPages <= 1) {
paginationEl.style.display = "none";
paginationEl.innerHTML = "";
} else {
paginationEl.style.display = "flex";
let pagHtml = "";
pagHtml += `<button onclick="onListPageChange('${containerId}', 1)" ${state.currentPage === 1 ? "disabled" : ""} title="First Page">⏮</button>`;
pagHtml += `<button onclick="onListPageChange('${containerId}', ${state.currentPage - 1})" ${state.currentPage === 1 ? "disabled" : ""} title="Previous Page">◀</button>`;
const cur = state.currentPage;
const startP = Math.max(1, cur - 2);
const endP = Math.min(totalPages, cur + 2);
if (startP > 1) {
pagHtml += `<button onclick="onListPageChange('${containerId}', 1)">1</button>`;
if (startP > 2)
pagHtml += `<span style="padding:0 4px; color:var(--text-secondary);">…</span>`;
}
for (let p = startP;p <= endP; p++) {
pagHtml += `<button class="${p === cur ? "active" : ""}" onclick="onListPageChange('${containerId}', ${p})">${p}</button>`;
}
if (endP < totalPages) {
if (endP < totalPages - 1)
pagHtml += `<span style="padding:0 4px; color:var(--text-secondary);">…</span>`;
pagHtml += `<button onclick="onListPageChange('${containerId}', ${totalPages})">${totalPages}</button>`;
}
pagHtml += `<button onclick="onListPageChange('${containerId}', ${state.currentPage + 1})" ${state.currentPage === totalPages ? "disabled" : ""} title="Next Page">▶</button>`;
pagHtml += `<button onclick="onListPageChange('${containerId}', ${totalPages})" ${state.currentPage === totalPages ? "disabled" : ""} title="Last Page">⏭</button>`;
paginationEl.innerHTML = pagHtml;
}
}
const clearBtn = document.getElementById(`${containerId}-clear`);
if (clearBtn) {
clearBtn.style.display = state.searchQuery ? "block" : "none";
}
const sortBtn = document.getElementById(`${containerId}-sort-btn`);
if (sortBtn) {
sortBtn.innerHTML = state.sortOrder === "desc" ? "⬇️ Newest First" : "⬆️ Oldest First";
}
}
function renderListStructure(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
const container = document.getElementById(containerId);
if (!container)
return;
const itemNoun = state.options.itemNoun || "items";
let filtersHtml = "";
if (state.options.filters && state.options.filters.length > 0) {
for (const f of state.options.filters) {
const activeVal = state.activeFilters[f.id] || "";
filtersHtml += `
<select class="enterprise-select" id="${containerId}-filter-${f.id}" onchange="onListFilterChange('${containerId}', '${f.id}', this.value)" title="Filter by ${f.label}">
${f.options.map((opt) => `<option value="${escapeHtml(opt.value)}" ${opt.value === activeVal ? "selected" : ""}>${escapeHtml(opt.label)}</option>`).join("")}
</select>`;
}
}
const curPageSize = state.pageSize;
const pageSizeOptions = [
{ label: "10 / page", value: "10" },
{ label: "25 / page", value: "25" },
{ label: "50 / page", value: "50" },
{ label: "100 / page", value: "100" },
{ label: "All items", value: "-1" }
];
const pageSizeHtml = `
<select class="enterprise-select" id="${containerId}-pagesize" onchange="onListPageSizeChange('${containerId}', this.value)" title="Items per page">
${pageSizeOptions.map((opt) => `<option value="${opt.value}" ${parseInt(opt.value, 10) === curPageSize ? "selected" : ""}>${opt.label}</option>`).join("")}
</select>`;
container.innerHTML = `
<div class="enterprise-toolbar" id="${containerId}-toolbar">
<div class="enterprise-search-wrap">
<span class="enterprise-search-icon">\uD83D\uDD0D</span>
<input
type="text"
class="enterprise-search-input"
id="${containerId}-search"
placeholder="Filter ${itemNoun} (/)..."
value="${escapeHtml(state.searchQuery)}"
oninput="onListSearchInput('${containerId}', this.value)"
/>
<button
class="enterprise-clear-btn"
id="${containerId}-clear"
onclick="onListClearSearch('${containerId}')"
title="Clear search"
style="display:${state.searchQuery ? "block" : "none"};"
>✕</button>
</div>
<div class="enterprise-filters-group">
${filtersHtml}
<button
class="enterprise-btn"
id="${containerId}-sort-btn"
onclick="onListToggleSort('${containerId}')"
title="Toggle sort direction"
>
${state.sortOrder === "desc" ? "⬇️ Newest First" : "⬆️ Oldest First"}
</button>
${pageSizeHtml}
<button
class="enterprise-btn"
onclick="onListResetFilters('${containerId}')"
title="Reset all filters and sorting"
>
\uD83D\uDD04 Reset
</button>
</div>
</div>
<div class="enterprise-meta-bar" id="${containerId}-meta"></div>
<div class="enterprise-cards-list" id="${containerId}-cards"></div>
<div class="enterprise-pagination" id="${containerId}-pagination"></div>
`;
renderListCardsAndPagination(containerId);
}
function onListSearchInput(containerId, value) {
const state = listControllers.get(containerId);
if (!state)
return;
if (state.debounceTimer)
clearTimeout(state.debounceTimer);
state.searchQuery = value;
state.debounceTimer = setTimeout(() => {
state.currentPage = 1;
applyListFiltering(containerId);
renderListCardsAndPagination(containerId);
}, 150);
}
function onListClearSearch(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
state.searchQuery = "";
const inputEl = document.getElementById(`${containerId}-search`);
if (inputEl) {
inputEl.value = "";
inputEl.focus();
}
state.currentPage = 1;
applyListFiltering(containerId);
renderListCardsAndPagination(containerId);
}
function onListFilterChange(containerId, filterId, value) {
const state = listControllers.get(containerId);
if (!state)
return;
state.activeFilters[filterId] = value;
state.currentPage = 1;
applyListFiltering(containerId);
renderListCardsAndPagination(containerId);
}
function onListToggleSort(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
state.sortOrder = state.sortOrder === "desc" ? "asc" : "desc";
applyListFiltering(containerId);
renderListCardsAndPagination(containerId);
}
function onListPageSizeChange(containerId, value) {
const state = listControllers.get(containerId);
if (!state)
return;
state.pageSize = parseInt(value, 10);
state.currentPage = 1;
renderListCardsAndPagination(containerId);
}
function onListPageChange(containerId, page) {
const state = listControllers.get(containerId);
if (!state)
return;
state.currentPage = page;
renderListCardsAndPagination(containerId);
const container = document.getElementById(containerId);
if (container) {
container.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
function onListResetFilters(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
state.searchQuery = "";
state.activeFilters = {};
state.sortOrder = state.options.defaultSort || "desc";
state.pageSize = state.options.pageSize || 25;
state.currentPage = 1;
applyListFiltering(containerId);
renderListStructure(containerId);
}
async function loadGenericList(endpoint, containerId, formatter, options) {
const container = document.getElementById(containerId);
if (!container)
return;
const resolvedOptions = options || {};
const existingState = listControllers.get(containerId);
const state = existingState || {
endpoint,
containerId,
formatter,
options: resolvedOptions,
items: [],
filteredItems: [],
searchQuery: "",
activeFilters: {},
sortOrder: resolvedOptions.defaultSort || "desc",
currentPage: 1,
pageSize: resolvedOptions.pageSize || 25,
debounceTimer: null
};
state.options = resolvedOptions;
state.formatter = formatter;
listControllers.set(containerId, state);
try {
const res = await fetch(endpoint);
if (!res.ok) {
if (container2) {
container2.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${endpoint} (${res.status} ${res.statusText})</div>`;
}
container.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${escapeHtml(endpoint)} (${res.status} ${escapeHtml(res.statusText)})</div>`;
return;
}
const data = await res.json();
const container2 = document.getElementById(containerId);
if (!container2)
return;
if (!data || data.length === 0) {
container2.innerHTML = '<div style="color:var(--text-secondary); padding:20px; text-align:center;">No items recorded.</div>';
return;
state.items = Array.isArray(data) ? data : [];
applyListFiltering(containerId);
const toolbarExists = document.getElementById(`${containerId}-toolbar`);
if (!toolbarExists) {
renderListStructure(containerId);
} else {
renderListCardsAndPagination(containerId);
}
container2.innerHTML = data.map((item) => {
const date = item.timestamp ? new Date(item.timestamp * 1000).toLocaleString() : "";
return `<div class="task-card" style="border-left-color: #9b59b6;">
<div style="font-size:0.75em; color:var(--text-secondary); float:right;">${date}</div>
${formatter(item)}
</div>`;
}).join("");
} catch (err) {
console.error(`Failed to load ${endpoint}`, err);
if (container) {
container.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${endpoint} (${escapeHtml(String(err))})</div>`;
}
container.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${escapeHtml(endpoint)} (${escapeHtml(String(err))})</div>`;
}
}
function loadLedger() {
loadGenericList("/api/ledger?limit=100", "ledger-container", (item) => {
loadGenericList("/api/ledger?limit=500", "ledger-container", (item) => {
const vcs = (item.vcs_type || (item.revision && item.revision.startsWith("r") ? "svn" : "git")).toUpperCase();
const vcsColor = vcs === "SVN" ? "#e67e22" : "#3498db";
const rev = item.revision || item.git_commit;
@@ -848,6 +1185,48 @@ function loadLedger() {
${authorBadge}
</div>
`;
}, {
itemNoun: "Ledger Entries",
cardBorderColor: "#3498db",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.file_path,
item.description,
item.author,
item.repo_name,
item.branch,
item.git_commit,
item.revision,
Array.isArray(item.symbols) ? item.symbols.join(" ") : ""
],
filters: [
{
id: "vcs",
label: "VCS",
options: [
{ label: "All VCS", value: "" },
{ label: "Git", value: "git" },
{ label: "SVN", value: "svn" }
],
predicate: (item, val) => {
const vcs = (item.vcs_type || (item.revision && item.revision.startsWith("r") ? "svn" : "git")).toLowerCase();
return vcs === val.toLowerCase();
}
},
{
id: "kind",
label: "Change Kind",
options: [
{ label: "All Types", value: "" },
{ label: "+ Added", value: "added" },
{ label: "Modified", value: "modified" },
{ label: "- Deleted", value: "deleted" },
{ label: "→ Renamed", value: "renamed" }
],
predicate: (item, val) => (item.change_kind || "modified").toLowerCase() === val.toLowerCase()
}
]
});
}
function loadTerminal() {
@@ -880,6 +1259,38 @@ function loadTerminal() {
</details>
` : ""}
`;
}, {
itemNoun: "Commands",
cardBorderColor: "#1abc9c",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.command,
item.cwd,
item.repo_name,
item.invoker,
item.shell,
item.stdout_summary,
item.error_output
],
filters: [
{
id: "status",
label: "Exit Code",
options: [
{ label: "All Results", value: "" },
{ label: "Success (0)", value: "0" },
{ label: "Failed (!= 0)", value: "err" }
],
predicate: (item, val) => {
if (val === "0")
return item.exit_code === 0;
if (val === "err")
return item.exit_code !== 0;
return true;
}
}
]
});
}
function loadTechDebt() {
@@ -923,6 +1334,50 @@ function loadTechDebt() {
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">\uD83C\uDFF7️ ${escapeHtml(item.namespace || "global")}</span>
</div>
`;
}, {
itemNoun: "Tech Debts",
cardBorderColor: "#e67e22",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.id,
item.description,
item.ideal_solution,
item.workaround,
item.file_path,
item.repo_name,
item.severity
],
filters: [
{
id: "status",
label: "Status",
options: [
{ label: "All Statuses", value: "" },
{ label: "Open", value: "open" },
{ label: "Resolved", value: "resolved" }
],
predicate: (item, val) => {
if (val === "resolved")
return !!item.is_resolved;
if (val === "open")
return !item.is_resolved;
return true;
}
},
{
id: "severity",
label: "Severity",
options: [
{ label: "All Severities", value: "" },
{ label: "Critical", value: "critical" },
{ label: "High", value: "high" },
{ label: "Medium", value: "medium" },
{ label: "Low", value: "low" }
],
predicate: (item, val) => String(item.severity || "").toLowerCase() === val.toLowerCase()
}
]
});
loadGenericList("/api/error_fixes", "errorfixes-container", (item) => `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
@@ -945,7 +1400,34 @@ function loadTechDebt() {
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}</span>
${item.git_branch ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(item.git_branch)}</span>` : ""}
</div>
`);
`, {
itemNoun: "Error Fixes",
cardBorderColor: "#2ecc71",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.signature,
item.solution,
item.toolchain,
item.error_category,
item.repo_name,
item.stack_trace
],
filters: [
{
id: "category",
label: "Category",
options: [
{ label: "All Categories", value: "" },
{ label: "Compilation", value: "compilation" },
{ label: "Runtime", value: "runtime" },
{ label: "Test", value: "test" },
{ label: "Configuration", value: "configuration" }
],
predicate: (item, val) => String(item.error_category || "").toLowerCase() === val.toLowerCase()
}
]
});
}
function loadADRs() {
loadGenericList("/api/adrs", "adrs-container", (item) => {
@@ -980,6 +1462,41 @@ function loadADRs() {
<div style="font-size:0.9em; margin-bottom:8px;"><em>Consequence:</em> ${escapeHtml(item.consequence || "")}</div>
${item.supersedes ? `<div style="font-size:0.9em; font-weight:bold; color:var(--warning-color);">Supersedes: ${escapeHtml(item.supersedes)}</div>` : ""}
`;
}, {
itemNoun: "ADRs",
cardBorderColor: "#3498db",
defaultSort: "desc",
sortExtractor: (item) => {
const match = String(item.id || "").match(/\d+/);
if (match)
return parseInt(match[0], 10);
return item.timestamp || 0;
},
searchFields: (item) => [
item.id,
item.title,
item.context,
item.decision,
item.consequence,
item.status,
item.git_branch,
item.task_id
],
filters: [
{
id: "status",
label: "Status",
options: [
{ label: "All Statuses", value: "" },
{ label: "Accepted", value: "accepted" },
{ label: "Implemented", value: "implemented" },
{ label: "Proposed", value: "proposed" },
{ label: "Draft", value: "draft" },
{ label: "Superseded", value: "superseded" }
],
predicate: (item, val) => String(item.status || "").toLowerCase() === val.toLowerCase()
}
]
});
}
function loadMemos() {
@@ -1016,6 +1533,18 @@ function loadMemos() {
</div>
` : ""}
`;
}, {
itemNoun: "Memos",
cardBorderColor: "#9b59b6",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.author,
item.content,
item.summary,
item.namespace,
item.git_branch
]
});
loadGenericList("/api/session_summaries", "summaries-container", (item) => `
<div style="display:flex; justify-content:space-between; align-items:center;">
@@ -1026,7 +1555,18 @@ function loadMemos() {
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4; margin-top:8px;">${escapeHtml(item.summary || "")}</div>
${item.tasks_completed && item.tasks_completed.length > 0 ? `<div style="margin-top:8px; font-size:0.85em;"><strong>Tasks Completed:</strong><ul style="margin:4px 0 0 16px; padding:0;">${item.tasks_completed.map((t) => `<li>${escapeHtml(t)}</li>`).join("")}</ul></div>` : ""}
${item.next_steps && item.next_steps.length > 0 ? `<div style="margin-top:6px; font-size:0.85em;"><strong>Next Steps:</strong><ul style="margin:4px 0 0 16px; padding:0;">${item.next_steps.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}</ul></div>` : ""}
`);
`, {
itemNoun: "Summaries",
cardBorderColor: "#34495e",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.session_id,
item.summary,
item.namespace,
item.git_branch
]
});
loadGenericList("/api/milestones", "milestones-container", (item) => {
const statusLower = (item.status || "pending").toLowerCase();
let statusBg = "rgba(52, 152, 219, 0.15)";
@@ -1072,6 +1612,30 @@ function loadMemos() {
</div>
` : ""}
`;
}, {
itemNoun: "Milestones",
cardBorderColor: "#f39c12",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp || item.target_date,
searchFields: (item) => [
item.title,
item.name,
item.description,
item.status
],
filters: [
{
id: "status",
label: "Status",
options: [
{ label: "All Statuses", value: "" },
{ label: "Pending", value: "pending" },
{ label: "In Progress", value: "in_progress" },
{ label: "Completed", value: "completed" }
],
predicate: (item, val) => String(item.status || "pending").toLowerCase() === val.toLowerCase()
}
]
});
}
function loadSnippets() {
@@ -1095,6 +1659,42 @@ function loadSnippets() {
${tags.map((t) => `<span style="background:var(--card-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; border:1px solid var(--border-color); color:var(--text-secondary);">#${escapeHtml(t)}</span>`).join(" ")}
</div>
`;
}, {
itemNoun: "Snippets",
cardBorderColor: "#27ae60",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.name,
item.description,
item.code,
item.language,
item.origin_file,
Array.isArray(item.tags) ? item.tags.join(" ") : ""
],
filters: [
{
id: "language",
label: "Language",
options: [
{ label: "All Languages", value: "" },
{ label: "Rust", value: "rust" },
{ label: "TypeScript / JS", value: "typescript" },
{ label: "Python", value: "python" },
{ label: "SQL", value: "sql" },
{ label: "Shell / Bash", value: "bash" },
{ label: "Markdown / Text", value: "markdown" }
],
predicate: (item, val) => {
const lang = String(item.language || "").toLowerCase();
if (val === "typescript")
return lang === "typescript" || lang === "javascript" || lang === "ts" || lang === "js";
if (val === "bash")
return lang === "bash" || lang === "sh" || lang === "pwsh" || lang === "powershell";
return lang === val.toLowerCase();
}
}
]
});
}
function loadAllExtras() {
@@ -1177,6 +1777,15 @@ document.addEventListener("keydown", function(e) {
searchInput.focus();
searchInput.select();
}
return;
}
if (e.key === "/" && document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA") {
const activeSearch = document.querySelector(".tab-content.active .enterprise-search-input, .tab-content.active #task-filter-input");
if (activeSearch) {
e.preventDefault();
activeSearch.focus();
activeSearch.select();
}
}
});
function refreshActiveTab() {
@@ -1225,6 +1834,8 @@ Object.assign(window, {
completeTask,
buildTaskTreeHTML,
loadTasks,
handleTaskFilter,
handleTaskStatusFilter,
highlightText,
handleSearch,
escapeHtml,
@@ -1234,6 +1845,13 @@ Object.assign(window, {
handleIncomingActivity,
setupWS,
loadGenericList,
onListSearchInput,
onListClearSearch,
onListFilterChange,
onListToggleSort,
onListPageSizeChange,
onListPageChange,
onListResetFilters,
loadTerminal,
loadLedger,
loadTechDebt,