diff --git a/turnstone/console/static/admin.js b/turnstone/console/static/admin.js index 3691afc5..2fcfa03f 100644 --- a/turnstone/console/static/admin.js +++ b/turnstone/console/static/admin.js @@ -63,6 +63,7 @@ function showAdmin() { "ws-templates": "admin.ws_templates", usage: "admin.usage", audit: "admin.audit", + memories: "admin.memories", settings: "admin.users", }; if (perms) { @@ -191,6 +192,7 @@ function switchAdminTab(tab) { "ws-templates", "usage", "audit", + "memories", "settings", ]; for (var p = 0; p < panels.length; p++) { @@ -212,6 +214,7 @@ function switchAdminTab(tab) { _populateAuditUserFilter(); loadGovAudit(); } + if (tab === "memories") loadAdminMemories(); if (tab === "settings") loadSettings(); // Update breadcrumb with active tab label @@ -1589,6 +1592,7 @@ function _installTrap(overlayId, boxId, trapRef) { hideCreateWsTemplateModal(); else if (overlayId === "edit-wst-overlay") hideEditWsTemplateModal(); else if (overlayId === "wst-history-overlay") hideWstHistoryModal(); + else if (overlayId === "memory-detail-overlay") hideMemoryDetailModal(); } }; } @@ -1667,6 +1671,7 @@ document.addEventListener("keydown", function (e) { ["create-wst-overlay", hideCreateWsTemplateModal], ["edit-wst-overlay", hideEditWsTemplateModal], ["wst-history-overlay", hideWstHistoryModal], + ["memory-detail-overlay", hideMemoryDetailModal], ]; for (var gi = 0; gi < govOverlays.length; gi++) { var govEl = document.getElementById(govOverlays[gi][0]); diff --git a/turnstone/console/static/governance.js b/turnstone/console/static/governance.js index 3e47deab..1a2b6c6c 100644 --- a/turnstone/console/static/governance.js +++ b/turnstone/console/static/governance.js @@ -1636,3 +1636,251 @@ function _populateAuditUserFilter() { } sel.innerHTML = html; } + +// --------------------------------------------------------------------------- +// Memories tab +// --------------------------------------------------------------------------- + +var _adminMemories = []; +var _memDetailTrap = null; +var _memDetailTrigger = null; +var _memSearchTimer = null; +var _memSearchBound = false; + +function loadAdminMemories() { + clearTimeout(_memSearchTimer); + // Bind search debounce on first load + if (!_memSearchBound) { + var searchEl = document.getElementById("mem-search"); + if (searchEl) { + searchEl.addEventListener("input", function () { + clearTimeout(_memSearchTimer); + _memSearchTimer = setTimeout(loadAdminMemories, 300); + }); + } + _memSearchBound = true; + } + + var memType = document.getElementById("mem-filter-type").value; + var scope = document.getElementById("mem-filter-scope").value; + var query = (document.getElementById("mem-search").value || "").trim(); + + var url; + if (query) { + url = + "/v1/api/admin/memories/search?q=" + + encodeURIComponent(query) + + (memType ? "&type=" + encodeURIComponent(memType) : "") + + (scope ? "&scope=" + encodeURIComponent(scope) : ""); + } else { + url = + "/v1/api/admin/memories?limit=200" + + (memType ? "&type=" + encodeURIComponent(memType) : "") + + (scope ? "&scope=" + encodeURIComponent(scope) : ""); + } + + authFetch(url) + .then(function (r) { + if (!r.ok) throw new Error("Failed to load memories"); + return r.json(); + }) + .then(function (data) { + _adminMemories = data.memories || []; + _renderAdminMemories(_adminMemories, data.total || _adminMemories.length); + }) + .catch(function () { + document.getElementById("admin-memories-table").innerHTML = + '
Failed to load memories
'; + }); +} + +function _renderAdminMemories(items, total) { + var el = document.getElementById("admin-memories-table"); + if (!items.length) { + el.innerHTML = '
No memories found
'; + return; + } + + var html = ""; + for (var i = 0; i < items.length; i++) { + var m = items[i]; + + // Type badge + var typeCls = "scope-badge mem-type-" + escapeHtml(m.type); + var typeBadge = + '' + escapeHtml(m.type) + ""; + + // Scope badge + var scopeLabel = m.scope; + if (m.scope_id) scopeLabel += ":" + m.scope_id; + var scopeCls = "scope-badge mem-scope-" + escapeHtml(m.scope); + var scopeBadge = + '' + escapeHtml(scopeLabel) + ""; + + // Description (truncated) + var desc = m.description || ""; + if (desc.length > 60) desc = desc.substring(0, 57) + "…"; + + html += + '
' + + '' + + escapeHtml(m.name) + + "" + + '' + + typeBadge + + "" + + '' + + scopeBadge + + "" + + '' + + escapeHtml(desc) + + "" + + '' + + _relativeTime(m.updated) + + "" + + '' + + '' + + '' + + "" + + "
"; + } + + el.innerHTML = html; + + // Bind view buttons + var viewBtns = el.querySelectorAll("[data-view-memory]"); + for (var v = 0; v < viewBtns.length; v++) { + viewBtns[v].addEventListener("click", function () { + showMemoryDetailModal(this.getAttribute("data-view-memory")); + }); + } + + // Bind delete buttons + var delBtns = el.querySelectorAll("[data-delete-memory]"); + for (var d = 0; d < delBtns.length; d++) { + delBtns[d].addEventListener("click", function () { + var mid = this.getAttribute("data-delete-memory"); + var mname = this.getAttribute("data-delete-name"); + deleteAdminMemory(mid, mname); + }); + } +} + +function showMemoryDetailModal(memoryId) { + _memDetailTrigger = document.activeElement; + var ov = document.getElementById("memory-detail-overlay"); + ov.style.display = "flex"; + document.getElementById("memory-detail-body").innerHTML = + '
Loading…
'; + + // Disable delete button and clear stale handler while loading + var delBtn = document.getElementById("mem-detail-delete"); + delBtn.disabled = true; + delBtn.onclick = null; + + // Focus close button for keyboard accessibility + var closeBtn = ov.querySelector(".modal-cancel"); + if (closeBtn) closeBtn.focus(); + + authFetch("/v1/api/admin/memories/" + encodeURIComponent(memoryId)) + .then(function (r) { + if (!r.ok) throw new Error("Not found"); + return r.json(); + }) + .then(function (m) { + var scopeLabel = m.scope; + if (m.scope_id) scopeLabel += ":" + m.scope_id; + + var html = + '
' + + '
Name' + + escapeHtml(m.name) + + "
" + + '
Type' + + '' + + escapeHtml(m.type) + + "
" + + '
Scope' + + '' + + escapeHtml(scopeLabel) + + "
" + + '
Created' + + _relativeTime(m.created) + + "
" + + '
Updated' + + _relativeTime(m.updated) + + "
" + + '
Accessed' + + (m.access_count || 0) + + " times
" + + "
" + + '
Description
' + + '
' + + escapeHtml(m.description || "(none)") + + "
" + + '
Content
' + + '
' +
+        escapeHtml(m.content) +
+        "
"; + + document.getElementById("memory-detail-body").innerHTML = html; + + // Wire delete button now that data is loaded + delBtn.disabled = false; + delBtn.onclick = function () { + deleteAdminMemory(m.memory_id, m.name); + }; + }) + .catch(function () { + document.getElementById("memory-detail-body").innerHTML = + '
Failed to load memory
'; + }); + + _memDetailTrap = _installTrap("memory-detail-overlay", "memory-detail-box"); +} + +function hideMemoryDetailModal() { + document.getElementById("memory-detail-overlay").style.display = "none"; + _memDetailTrap = _removeTrap(_memDetailTrap); + if (_memDetailTrigger && _memDetailTrigger.focus) _memDetailTrigger.focus(); + _memDetailTrigger = null; +} + +function deleteAdminMemory(memoryId, memoryName) { + if (!confirm("Delete memory '" + memoryName + "'?")) return; + + authFetch("/v1/api/admin/memories/" + encodeURIComponent(memoryId), { + method: "DELETE", + }) + .then(function (r) { + if (!r.ok) + return r.json().then(function (d) { + throw new Error(d.error || "Failed"); + }); + return r.json(); + }) + .then(function () { + showToast("Memory deleted"); + // Close detail modal if open + if ( + document.getElementById("memory-detail-overlay").style.display !== + "none" + ) { + hideMemoryDetailModal(); + } + loadAdminMemories(); + }) + .catch(function (e) { + showToast("Error: " + e.message); + }); +} diff --git a/turnstone/console/static/index.html b/turnstone/console/static/index.html index 3260c223..4ebeaf43 100644 --- a/turnstone/console/static/index.html +++ b/turnstone/console/static/index.html @@ -102,6 +102,7 @@ +
@@ -347,6 +348,40 @@
+ + + + + diff --git a/turnstone/console/static/style.css b/turnstone/console/static/style.css index 8e39978e..d229e644 100644 --- a/turnstone/console/static/style.css +++ b/turnstone/console/static/style.css @@ -1168,7 +1168,8 @@ #create-role-overlay, #edit-role-overlay, #user-roles-overlay, #create-policy-overlay, #edit-policy-overlay, #create-template-overlay, #edit-template-overlay, -#create-wst-overlay, #edit-wst-overlay, #wst-history-overlay { +#create-wst-overlay, #edit-wst-overlay, #wst-history-overlay, +#memory-detail-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); @@ -1326,6 +1327,91 @@ .audit-danger { color: var(--red); border-color: var(--red-glow); } .audit-success { color: var(--green); border-color: var(--green-glow); } +/* ========================================================================== + Memories tab + ========================================================================== */ +#admin-memories .admin-colheaders, +#admin-memories .admin-row { + grid-template-columns: 1.5fr 80px 110px 1fr 100px 80px; +} + +/* Filter toolbar */ +.admin-toolbar-filters { + display: flex; + gap: 8px; + margin-left: auto; + align-items: center; +} +.admin-toolbar-filters select, +.admin-toolbar-filters input[type="search"] { + border: 1px solid var(--border); + border-radius: 3px; + padding: 4px 8px; + font: inherit; + font-size: 12px; +} +.admin-toolbar-filters select { + min-width: 0; +} +.admin-toolbar-filters input[type="search"] { + width: 180px; + background: var(--bg); + color: var(--fg); +} +.admin-toolbar-filters input[type="search"]::placeholder { + color: var(--fg-dim); +} + +/* Memory type badges */ +.mem-type-project { background: var(--bg-highlight); color: var(--cyan); border-color: var(--cyan-glow, var(--border)); } +.mem-type-user { background: var(--bg-highlight); color: var(--green); border-color: var(--green-glow, var(--border)); } +.mem-type-feedback { background: var(--bg-highlight); color: var(--yellow); border-color: var(--yellow-glow, var(--border)); } +.mem-type-reference { background: var(--bg-highlight); color: var(--magenta); border-color: var(--magenta-glow, var(--border)); } + +/* Memory scope badges */ +.mem-scope-global { background: var(--bg-highlight); color: var(--fg-dim); } +.mem-scope-workstream { background: var(--bg-highlight); color: var(--cyan); border-color: var(--cyan-glow, var(--border)); } +.mem-scope-user { background: var(--bg-highlight); color: var(--green); border-color: var(--green-glow, var(--border)); } + +/* Memory detail modal */ +.mem-detail-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px 16px; +} +.mem-detail-field { + display: flex; + flex-direction: column; + gap: 2px; +} +.mem-detail-label { + font-size: 10px; + font-family: var(--font-display); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--fg-dim); +} +.mem-detail-desc { + color: var(--fg); + font-size: 13px; + padding: 4px 0; +} +.memory-content-block { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 12px; + max-height: 400px; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--font-mono); + font-size: 13px; + color: var(--fg); + margin: 4px 0 0; +} + /* ========================================================================== Governance: Usage dashboard ========================================================================== */ @@ -1495,6 +1581,13 @@ grid-template-columns: 60px 1fr 100px; } .admin-col-auser, .admin-col-adetail { display: none; } + #admin-memories .admin-colheaders, #admin-memories .admin-row { + grid-template-columns: 1fr 70px 90px 80px; + } + .admin-col-mdesc, .admin-col-mupdated { display: none; } + .admin-toolbar-filters { flex-wrap: wrap; } + .admin-toolbar-filters input[type="search"] { width: 120px; } + .mem-detail-grid { grid-template-columns: 1fr 1fr; } .usage-readout-value { font-size: 18px; } .usage-bar-row { grid-template-columns: 70px 1fr 50px; } .perm-grid { grid-template-columns: 1fr; }