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 =
+ '';
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 += '
';
@@ -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 {