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 @@
+
+
+
+
@@ -925,6 +960,17 @@ window.TURNSTONE_KB_SHORTCUTS = [
+
+
+
Memory Detail
+
+
+
+
+
+
+
+
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; }