TaskDecay v1.0.0 MVP — smart task review & archive Chrome extension

This commit is contained in:
Bun Bun
2026-06-17 06:48:47 +00:00
commit cc9aee68ee
38 changed files with 4710 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
import { fetchAllTasks, getTokenForSource, completeTask, deleteTask } from './apis/index.js';
import { getSettings, setSettings, getStoredTasks, setStoredTasks, setLastSync, addDismissedTask, addArchivedTask } from './storage.js';
import { getReviewTasks, getAutoArchiveCandidates } from './scoring.js';
// Background service worker for TaskDecay
// Handles periodic sync, badge count, and alarm-based review reminders
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('taskdecay-sync', { periodInMinutes: 60 });
chrome.alarms.create('taskdecay-review-reminder', { periodInMinutes: 60 * 24 });
updateBadge();
});
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'taskdecay-sync') {
await syncTasks();
} else if (alarm.name === 'taskdecay-review-reminder') {
await checkReviewReminder();
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const msg = message as any;
if (msg.type === 'sync') {
syncTasks().then(sendResponse).catch(e => sendResponse({ error: (e as Error).message }));
return true;
}
if (msg.type === 'getReviewTasks') {
getReviewData().then(sendResponse).catch(e => sendResponse({ error: (e as Error).message }));
return true;
}
if (msg.type === 'dismiss') {
addDismissedTask(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: (e as Error).message }));
return true;
}
if (msg.type === 'archive') {
archiveTask(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: (e as Error).message }));
return true;
}
if (msg.type === 'complete') {
completeTaskById(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: (e as Error).message }));
return true;
}
if (msg.type === 'delete') {
deleteTaskById(msg.taskId).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ error: (e as Error).message }));
return true;
}
if (msg.type === 'openReview') {
chrome.tabs.create({ url: chrome.runtime.getURL('src/review.html') });
sendResponse({ ok: true });
return true;
}
});
async function syncTasks() {
const settings = await getSettings();
const tasks = await fetchAllTasks(settings.providers);
await setStoredTasks(tasks);
await setLastSync(new Date().toISOString());
await updateBadge();
return { count: tasks.length, syncedAt: new Date().toISOString() };
}
async function getReviewData() {
const settings = await getSettings();
const tasks = await getStoredTasks();
const review = getReviewTasks(tasks, settings);
const autoArchive = getAutoArchiveCandidates(tasks, settings);
return { review, autoArchiveCount: autoArchive.length, total: tasks.length };
}
async function archiveTask(taskId: string) {
const settings = await getSettings();
const tasks = await getStoredTasks();
const task = tasks.find(t => t.id === taskId);
if (!task) throw new Error('Task not found');
const token = getTokenForSource(task.source, settings.providers);
if (!token) throw new Error('No token for source');
await completeTask(task, token);
await addArchivedTask(taskId);
await updateBadge();
}
async function completeTaskById(taskId: string) {
const settings = await getSettings();
const tasks = await getStoredTasks();
const task = tasks.find(t => t.id === taskId);
if (!task) throw new Error('Task not found');
const token = getTokenForSource(task.source, settings.providers);
if (!token) throw new Error('No token for source');
await completeTask(task, token);
await addArchivedTask(taskId);
await updateBadge();
}
async function deleteTaskById(taskId: string) {
const settings = await getSettings();
const tasks = await getStoredTasks();
const task = tasks.find(t => t.id === taskId);
if (!task) throw new Error('Task not found');
const token = getTokenForSource(task.source, settings.providers);
if (!token) throw new Error('No token for source');
await deleteTask(task, token);
await addArchivedTask(taskId);
await updateBadge();
}
async function checkReviewReminder() {
const settings = await getSettings();
if (!settings.lastReviewDate) return;
const last = new Date(settings.lastReviewDate);
const now = new Date();
const daysSince = (now.getTime() - last.getTime()) / (1000 * 60 * 60 * 24);
if (daysSince >= settings.reviewIntervalDays) {
const tasks = await getStoredTasks();
const review = getReviewTasks(tasks, settings);
if (review.length > 0) {
chrome.notifications.create('review-reminder', {
type: 'basic',
iconUrl: 'icon128.png',
title: 'TaskDecay Review Ready',
message: `${review.length} tasks need your attention. Open the review session to clean your backlog.`,
buttons: [{ title: 'Start Review' }],
});
}
}
}
async function updateBadge() {
try {
const settings = await getSettings();
const tasks = await getStoredTasks();
const review = getReviewTasks(tasks, settings);
const count = review.length;
chrome.action.setBadgeText({ text: count > 0 ? String(count) : '' });
chrome.action.setBadgeBackgroundColor({ color: '#e74c3c' });
} catch {
chrome.action.setBadgeText({ text: '' });
}
}
chrome.notifications.onButtonClicked.addListener((notificationId) => {
if (notificationId === 'review-reminder') {
chrome.tabs.create({ url: chrome.runtime.getURL('src/review.html') });
}
});