diff --git a/index.html b/index.html
index 4552599..360bee7 100644
--- a/index.html
+++ b/index.html
@@ -11,6 +11,7 @@
+
@@ -426,6 +427,12 @@
Done
+
+
+
diff --git a/src/kanban.js b/src/kanban.js
index 5f284f1..a5e97d1 100644
--- a/src/kanban.js
+++ b/src/kanban.js
@@ -219,6 +219,21 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
+
+ updateArchiveAllButton();
+ }
+
+ // Show the Done column's "Archive all" button only when signed in and there
+ // is at least one GitHub issue card available to archive.
+ function updateArchiveAllButton() {
+ const btn = document.getElementById('archive-all-done-btn');
+ const doneColumn = document.getElementById('done');
+ if (!btn || !doneColumn) {
+ return;
+ }
+ const authed = Boolean(window.GitHub?.isGitHubAuthed?.());
+ const issueCount = doneColumn.querySelectorAll('.bg-white.border[data-issue-number]').length;
+ btn.classList.toggle('hidden', !(authed && issueCount > 0));
}
// Expose updateColumnCounts globally for modal use
@@ -310,6 +325,28 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
+ // Archive every GitHub issue currently in the Done column at once.
+ document.addEventListener('click', function(e) {
+ if (!e.target.closest('#archive-all-done-btn')) {
+ return;
+ }
+ const doneColumn = document.getElementById('done');
+ if (!doneColumn) {
+ return;
+ }
+ const issueCards = Array.from(doneColumn.querySelectorAll('.bg-white.border[data-issue-number]'));
+ if (issueCards.length === 0) {
+ return;
+ }
+ const plural = issueCards.length === 1 ? 'issue' : 'issues';
+ if (!window.confirm(`Archive all ${issueCards.length} ${plural} in Done?`)) {
+ return;
+ }
+ issueCards.forEach(card => {
+ window.GitHub.archiveGitHubIssue(card.getAttribute('data-issue-number'), card);
+ });
+ });
+
// Add click to open issue modal functionality
document.addEventListener('click', function(e) {
// Only handle clicks on issue cards with GitHub issue numbers
@@ -476,6 +513,7 @@ document.addEventListener('DOMContentLoaded', function() {
const testAPI = {
createTaskElement,
updateColumnCounts,
+ updateArchiveAllButton,
hideModal,
addTaskBtn,
addTaskModal,
diff --git a/src/logger.js b/src/logger.js
new file mode 100644
index 0000000..ad49e61
--- /dev/null
+++ b/src/logger.js
@@ -0,0 +1,17 @@
+// Quiets Dashban's routine console output.
+//
+// The app modules log a lot of informational/lifecycle detail via console.log.
+// Loading this script first (before the other src modules) replaces console.log
+// with a version that stays silent unless debugging is explicitly enabled by
+// setting `window.DASHBAN_DEBUG = true` (e.g. from devtools). console.error and
+// console.warn are left untouched, so genuine problems still surface.
+(function () {
+ 'use strict';
+
+ const original = console.log.bind(console);
+ console.log = function () {
+ if (globalThis.DASHBAN_DEBUG) {
+ original.apply(null, arguments);
+ }
+ };
+})();
diff --git a/src/status-cards.js b/src/status-cards.js
index 3bf4b5c..a057843 100644
--- a/src/status-cards.js
+++ b/src/status-cards.js
@@ -217,12 +217,25 @@ document.addEventListener('DOMContentLoaded', function() {
}
async function fetchCoverageStatus() {
+ // Remember which repo this request is for. If the user switches repos
+ // while the request is in flight, a slow/late response must not overwrite
+ // the now-current repo's coverage (switching to a repo with no coverage
+ // and back was leaving dashban stuck on "unknown").
+ const { OWNER, REPO } = getCurrentRepoConfig();
+ const isStale = () => {
+ const current = getCurrentRepoConfig();
+ return current.OWNER !== OWNER || current.REPO !== REPO;
+ };
+
try {
// The coverage badge URL already carries a query (?branch=main), so
// join the cache-buster with & rather than ?.
const badgeUrl = `${buildBadgeUrl('coverage')}&t=${Date.now()}`;
const svgText = await fetch(badgeUrl).then(r => r.text());
+ if (isStale()) {
+ return;
+ }
const coverage = parseCoverageFromSVG(svgText);
const coverageData = {
@@ -234,7 +247,10 @@ document.addEventListener('DOMContentLoaded', function() {
updateCoverageStatusUI(coverageData);
} catch (error) {
console.error('Error fetching coverage status:', error);
-
+
+ if (isStale()) {
+ return;
+ }
const fallbackData = {
coverage: 'unknown',
updatedAt: new Date(),
diff --git a/tests/kanban.test.js b/tests/kanban.test.js
index e5f3945..ad047c9 100644
--- a/tests/kanban.test.js
+++ b/tests/kanban.test.js
@@ -868,6 +868,142 @@ describe('Kanban Board Core Functionality', () => {
});
});
+ describe('archive all in the Done column', () => {
+ function addArchiveAllButton() {
+ const btn = document.createElement('button');
+ btn.id = 'archive-all-done-btn';
+ btn.className = 'hidden';
+ document.body.appendChild(btn);
+ return btn;
+ }
+
+ function addDoneIssueCard(number) {
+ const card = document.createElement('div');
+ card.className = 'bg-white border';
+ card.setAttribute('data-issue-number', String(number));
+ document.getElementById('done').appendChild(card);
+ return card;
+ }
+
+ beforeEach(() => {
+ global.window.GitHub.githubAuth = { isAuthenticated: true, mode: 'clerk', user: { login: 'octocat' } };
+ global.window.GitHub.archiveGitHubIssue = jest.fn();
+ global.window.confirm = jest.fn(() => true);
+ });
+
+ describe('updateArchiveAllButton', () => {
+ test('shows the button when signed in and Done has issue cards', () => {
+ const btn = addArchiveAllButton();
+ addDoneIssueCard(1);
+ api.updateArchiveAllButton();
+ expect(btn.classList.contains('hidden')).toBe(false);
+ });
+
+ test('hides the button when Done has no issue cards', () => {
+ const btn = addArchiveAllButton();
+ api.updateArchiveAllButton();
+ expect(btn.classList.contains('hidden')).toBe(true);
+ });
+
+ test('hides the button when not signed in', () => {
+ const btn = addArchiveAllButton();
+ addDoneIssueCard(1);
+ global.window.GitHub.githubAuth = { isAuthenticated: false, mode: null, user: null };
+ api.updateArchiveAllButton();
+ expect(btn.classList.contains('hidden')).toBe(true);
+ });
+
+ test('returns quietly when the button is absent', () => {
+ expect(() => api.updateArchiveAllButton()).not.toThrow();
+ });
+
+ test('returns quietly when the Done column is absent', () => {
+ const btn = addArchiveAllButton();
+ document.getElementById('done').remove();
+ expect(() => api.updateArchiveAllButton()).not.toThrow();
+ expect(btn.classList.contains('hidden')).toBe(true);
+ });
+
+ test('treats a missing GitHub integration as signed out', () => {
+ const btn = addArchiveAllButton();
+ addDoneIssueCard(1);
+ const saved = global.window.GitHub;
+ delete global.window.GitHub;
+ try {
+ api.updateArchiveAllButton();
+ expect(btn.classList.contains('hidden')).toBe(true);
+ } finally {
+ global.window.GitHub = saved;
+ }
+ });
+
+ test('treats GitHub without isGitHubAuthed as signed out', () => {
+ const btn = addArchiveAllButton();
+ addDoneIssueCard(1);
+ const saved = global.window.GitHub;
+ global.window.GitHub = {};
+ try {
+ api.updateArchiveAllButton();
+ expect(btn.classList.contains('hidden')).toBe(true);
+ } finally {
+ global.window.GitHub = saved;
+ }
+ });
+ });
+
+ describe('archive-all click handler', () => {
+ test('archives every issue card in Done after confirmation', () => {
+ addArchiveAllButton();
+ const c1 = addDoneIssueCard(1);
+ const c2 = addDoneIssueCard(2);
+
+ document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));
+
+ expect(global.window.confirm).toHaveBeenCalledWith('Archive all 2 issues in Done?');
+ expect(global.window.GitHub.archiveGitHubIssue).toHaveBeenCalledWith('1', c1);
+ expect(global.window.GitHub.archiveGitHubIssue).toHaveBeenCalledWith('2', c2);
+ });
+
+ test('uses the singular noun for a single issue', () => {
+ addArchiveAllButton();
+ addDoneIssueCard(7);
+
+ document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));
+
+ expect(global.window.confirm).toHaveBeenCalledWith('Archive all 1 issue in Done?');
+ expect(global.window.GitHub.archiveGitHubIssue).toHaveBeenCalledWith('7', expect.any(Object));
+ });
+
+ test('does nothing when the user cancels', () => {
+ global.window.confirm = jest.fn(() => false);
+ addArchiveAllButton();
+ addDoneIssueCard(1);
+
+ document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));
+
+ expect(global.window.GitHub.archiveGitHubIssue).not.toHaveBeenCalled();
+ });
+
+ test('does not prompt when Done has no issue cards', () => {
+ addArchiveAllButton();
+
+ document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));
+
+ expect(global.window.confirm).not.toHaveBeenCalled();
+ expect(global.window.GitHub.archiveGitHubIssue).not.toHaveBeenCalled();
+ });
+
+ test('does nothing when the Done column is missing', () => {
+ addArchiveAllButton();
+ document.getElementById('done').remove();
+
+ document.getElementById('archive-all-done-btn').dispatchEvent(new Event('click', { bubbles: true }));
+
+ expect(global.window.confirm).not.toHaveBeenCalled();
+ });
+ });
+ });
+
describe('double-click edit functionality', () => {
test('should handle double-click events on tasks', () => {
const taskElement = document.createElement('div');
diff --git a/tests/logger.test.js b/tests/logger.test.js
new file mode 100644
index 0000000..06f5857
--- /dev/null
+++ b/tests/logger.test.js
@@ -0,0 +1,59 @@
+/**
+ * Tests for src/logger.js — the console.log gate that keeps the browser console
+ * quiet unless window.DASHBAN_DEBUG is set.
+ */
+describe('logger (console.log gate)', () => {
+ let originalLog;
+ let originalError;
+ let originalWarn;
+
+ beforeEach(() => {
+ jest.resetModules();
+ originalLog = console.log;
+ originalError = console.error;
+ originalWarn = console.warn;
+ delete globalThis.DASHBAN_DEBUG;
+ });
+
+ afterEach(() => {
+ console.log = originalLog;
+ console.error = originalError;
+ console.warn = originalWarn;
+ delete globalThis.DASHBAN_DEBUG;
+ });
+
+ test('suppresses console.log by default (debugging off)', () => {
+ const sink = jest.fn();
+ console.log = sink; // stand in for the real console.log
+ require('../src/logger.js'); // wraps console.log, capturing `sink` as the original
+
+ console.log('hidden');
+
+ expect(sink).not.toHaveBeenCalled();
+ });
+
+ test('forwards console.log when DASHBAN_DEBUG is enabled', () => {
+ const sink = jest.fn();
+ console.log = sink;
+ require('../src/logger.js');
+
+ globalThis.DASHBAN_DEBUG = true;
+ console.log('shown', 1);
+
+ expect(sink).toHaveBeenCalledWith('shown', 1);
+ });
+
+ test('leaves console.error and console.warn untouched', () => {
+ const errSink = jest.fn();
+ const warnSink = jest.fn();
+ console.error = errSink;
+ console.warn = warnSink;
+ require('../src/logger.js');
+
+ console.error('boom');
+ console.warn('careful');
+
+ expect(errSink).toHaveBeenCalledWith('boom');
+ expect(warnSink).toHaveBeenCalledWith('careful');
+ });
+});
diff --git a/tests/status-cards.test.js b/tests/status-cards.test.js
index 7778198..30b7ac3 100644
--- a/tests/status-cards.test.js
+++ b/tests/status-cards.test.js
@@ -966,11 +966,40 @@ describe('Status Cards Functions', () => {
test('should handle coverage fetch errors', async () => {
global.fetch.mockRejectedValue(new Error('Coverage fetch error'));
-
+
await statusAPI.fetchCoverageStatus();
-
+
expect(console.error).toHaveBeenCalledWith('Error fetching coverage status:', expect.any(Error));
});
+
+ test('drops a stale coverage response when the repo changed mid-request', async () => {
+ setupGitHubAuth('super3', 'dashban');
+ const coverageEl = document.querySelector('[data-coverage-status]');
+ coverageEl.innerHTML = 'SENTINEL';
+ // The user switches repos while the request is in flight.
+ global.fetch.mockImplementation(async () => {
+ global.window.GitHubAuth.GITHUB_CONFIG.repo = 'other-repo';
+ return { text: async () => '' };
+ });
+
+ await statusAPI.fetchCoverageStatus();
+
+ expect(coverageEl.innerHTML).toBe('SENTINEL');
+ });
+
+ test('drops a stale coverage error when the repo changed mid-request', async () => {
+ setupGitHubAuth('super3', 'dashban');
+ const coverageEl = document.querySelector('[data-coverage-status]');
+ coverageEl.innerHTML = 'SENTINEL';
+ global.fetch.mockImplementation(async () => {
+ global.window.GitHubAuth.GITHUB_CONFIG.repo = 'other-repo';
+ throw new Error('network');
+ });
+
+ await statusAPI.fetchCoverageStatus();
+
+ expect(coverageEl.innerHTML).toBe('SENTINEL');
+ });
});
describe('refreshAllStatuses', () => {