From 42202b4e6f813f83a573ba2631b6f15fd42bad8e Mon Sep 17 00:00:00 2001 From: jackrescuer-gif Date: Sun, 19 Apr 2026 22:35:36 +0300 Subject: [PATCH] feat: git project drill-down from Projects view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'Open ›' button to each project row in Projects view; clicking it filters Sessions view to show only that project's sessions - Show breadcrumb bar with project name and 'Clear filter' button that returns to Projects view - Group renderProjects by git-root key (not name) to correctly handle repos with identical directory names - Clear all other active filters (search, date, tag) when drilling down to avoid silently empty results - Clear gitProjectFilter on any sidebar navigation via setView() - Use data-* attributes for project key/name in Open button to avoid JSON.stringify in inline onclick handlers (XSS hardening) - Document that drill-down always uses git-root key independent of groupingMode --- src/frontend/app.js | 90 ++++++++++++++++++++++++++++++++++------ src/frontend/calendar.js | 3 ++ src/frontend/styles.css | 50 ++++++++++++++++++++++ 3 files changed, 131 insertions(+), 12 deletions(-) diff --git a/src/frontend/app.js b/src/frontend/app.js index 3c6c318..316950f 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -11,6 +11,7 @@ let layout = localStorage.getItem('codedash-layout') || 'grid'; // 'grid' or 'li let groupingMode = normalizeGroupingMode(localStorage.getItem('codedash-grouping-mode')); let searchQuery = ''; let toolFilter = null; // null, 'claude', 'codex' +let gitProjectFilter = null; // null or { key, name } — drill-down from Projects view let tagFilter = ''; let dateFrom = ''; let dateTo = ''; @@ -447,10 +448,17 @@ function generateAllTitles() { // ── Data loading ─────────────────────────────────────────────── +var _loadSessionsInFlight = false; + async function loadSessions() { + if (_loadSessionsInFlight) return; + _loadSessionsInFlight = true; try { var resp = await fetch('/api/sessions'); allSessions = await resp.json(); + // Invalidate analytics cache so stale aggregates are not shown + _analyticsHtmlCache = null; + _analyticsCacheUrl = null; applyFilters(); // Progressive loading: if server is still loading cursor vscdb sessions, auto-refresh if (resp.headers.get('X-Loading') === '1') { @@ -458,6 +466,8 @@ async function loadSessions() { } } catch (e) { document.getElementById('content').innerHTML = '
Failed to load sessions. Is the server running?
'; + } finally { + _loadSessionsInFlight = false; } } @@ -649,6 +659,12 @@ function applyFilters() { if (!toolMatch) continue; } + // Git project drill-down filter (always uses git-root key, independent of groupingMode) + if (gitProjectFilter) { + var sessionProjectKey = getRepoInfo(s.project, s.git_root).key; + if (sessionProjectKey !== gitProjectFilter.key) continue; + } + // Tag filter if (tagFilter) { var sessionTags = tags[s.id] || []; @@ -975,9 +991,28 @@ function render() { // Stats if (stats) { - stats.textContent = sessions.length + ' sessions' + - (toolFilter ? ' (' + toolFilter + ')' : '') + - (tagFilter ? ' [' + tagFilter + ']' : ''); + var statsText = sessions.length + ' sessions'; + if (toolFilter) statsText += ' (' + toolFilter + ')'; + if (tagFilter) statsText += ' [' + tagFilter + ']'; + stats.textContent = statsText; + } + + // Project filter breadcrumb + var existingBreadcrumb = document.getElementById('gitProjectBreadcrumb'); + if (gitProjectFilter && currentView === 'sessions') { + if (!existingBreadcrumb) { + var bc = document.createElement('div'); + bc.id = 'gitProjectBreadcrumb'; + bc.className = 'git-project-breadcrumb'; + var toolbar = document.querySelector('.toolbar'); + if (toolbar) toolbar.parentNode.insertBefore(bc, toolbar.nextSibling); + } + document.getElementById('gitProjectBreadcrumb').innerHTML = + 'Project:' + + '' + escHtml(gitProjectFilter.name) + '' + + ''; + } else if (existingBreadcrumb) { + existingBreadcrumb.remove(); } // Route to view @@ -1172,15 +1207,15 @@ function renderQACard(s, idx) { } function renderProjects(container, sessions) { - var byGit = {}; + var byGit = {}; // key → { name, list } sessions.forEach(function(s) { - var name = getGitProjectName(s.project, s.git_root); - if (!byGit[name]) byGit[name] = []; - byGit[name].push(s); + var info = getRepoInfo(s.project, s.git_root); + if (!byGit[info.key]) byGit[info.key] = { name: info.name, list: [] }; + byGit[info.key].list.push(s); }); var sorted = Object.entries(byGit).sort(function(a, b) { - return b[1][0].last_ts - a[1][0].last_ts; + return b[1].list[0].last_ts - a[1].list[0].last_ts; }); if (sorted.length === 0) { @@ -1195,9 +1230,10 @@ function renderProjects(container, sessions) { html += ''; html += '
'; sorted.forEach(function(entry) { - var name = entry[0]; - var list = entry[1].slice().sort(function(a, b) { return b.last_ts - a.last_ts; }); - var color = getProjectColor(name); + var projKey = entry[0]; + var projName = entry[1].name; + var list = entry[1].list.slice().sort(function(a, b) { return b.last_ts - a.last_ts; }); + var color = getProjectColor(projName); var totalMsgs = list.reduce(function(s, e) { return s + (e.messages || 0); }, 0); var totalCost = list.reduce(function(s, e) { return s + estimateCost(e.file_size); }, 0); var costLabel = totalCost > 0 ? ' · ~$' + totalCost.toFixed(2) : ''; @@ -1205,8 +1241,9 @@ function renderProjects(container, sessions) { html += '
'; html += '
'; html += ''; - html += '' + escHtml(name) + ''; + html += '' + escHtml(projName) + ''; html += '' + list.length + ' sessions · ' + totalMsgs + ' msgs' + escHtml(costLabel) + ''; + html += ''; html += ''; html += '
'; html += '
'; @@ -1381,6 +1418,34 @@ function openProject(name) { applyFilters(); } +function drillIntoGitProject(key, name) { + gitProjectFilter = { key: key, name: name }; + currentView = 'sessions'; + // Reset other filters so they don't silently suppress results + searchQuery = ''; + tagFilter = ''; + dateFrom = ''; + dateTo = ''; + var searchBox = document.querySelector('.search-box'); + if (searchBox) searchBox.value = ''; + var tagSel = document.getElementById('tagFilter'); + if (tagSel) tagSel.value = ''; + updateDateBtn(); + document.querySelectorAll('.sidebar-item').forEach(function(el) { + el.classList.toggle('active', el.getAttribute('data-view') === 'sessions'); + }); + applyFilters(); +} + +function clearGitProjectFilter() { + gitProjectFilter = null; + currentView = 'projects'; + document.querySelectorAll('.sidebar-item').forEach(function(el) { + el.classList.toggle('active', el.getAttribute('data-view') === 'projects'); + }); + applyFilters(); +} + // ── Themes ───────────────────────────────────────────────────── function setTheme(theme) { @@ -1980,6 +2045,7 @@ function dismissUpdate() { loadTerminals(); checkForUpdates(); setInterval(checkForUpdates, 10000); // check every 10s + setInterval(loadSessions, 60000); // refresh sessions + invalidate analytics cache every 60s startActivePolling(); // Apply saved theme diff --git a/src/frontend/calendar.js b/src/frontend/calendar.js index 648f994..d1587f5 100644 --- a/src/frontend/calendar.js +++ b/src/frontend/calendar.js @@ -201,6 +201,9 @@ function setView(view) { currentView = view; } + // Clear project drill-down filter on any sidebar navigation + gitProjectFilter = null; + // Update sidebar active state document.querySelectorAll('.sidebar-item').forEach(function(el) { el.classList.toggle('active', el.getAttribute('data-view') === view); diff --git a/src/frontend/styles.css b/src/frontend/styles.css index d810a4a..f804389 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -1604,6 +1604,23 @@ body { flex: 1; } +.git-project-open-btn { + background: none; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-muted); + font-size: 11px; + padding: 2px 8px; + cursor: pointer; + flex-shrink: 0; + transition: background 0.15s, color 0.15s; +} +.git-project-open-btn:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + .git-project-group .group-chevron { font-size: 10px; color: var(--text-muted); @@ -1611,6 +1628,39 @@ body { } .git-project-group.collapsed .group-chevron { transform: rotate(-90deg); } +/* ── Git project filter breadcrumb ──────────────────────────── */ + +.git-project-breadcrumb { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 16px; + background: var(--bg-card); + border-bottom: 1px solid var(--border); + font-size: 13px; +} +.bc-label { + color: var(--text-muted); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.bc-name { + font-weight: 700; + color: var(--accent); +} +.bc-clear { + margin-left: auto; + background: none; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-muted); + font-size: 11px; + padding: 2px 8px; + cursor: pointer; +} +.bc-clear:hover { color: var(--text); border-color: var(--text-muted); } + /* ── QA session list ────────────────────────────────────────── */ .qa-list {