Merge pull request #97 from wagesj45/codex/update-error-handling-in-background.js

Add persistent error indicator
This commit is contained in:
Jordan Wages 2025-07-15 23:17:11 -05:00 committed by GitHub
commit 83b890a419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 49 additions and 7 deletions

View file

@ -22,7 +22,8 @@ message meets a specified criterion.
- **Rule enable/disable** temporarily turn a rule off without removing it. - **Rule enable/disable** temporarily turn a rule off without removing it.
- **Account & folder filters** limit rules to specific accounts or folders. - **Account & folder filters** limit rules to specific accounts or folders.
- **Context menu** apply AI rules from the message list or the message display action button. - **Context menu** apply AI rules from the message list or the message display action button.
- **Status icons** toolbar icons show when classification is in progress and briefly display success or error states. - **Status icons** toolbar icons show when classification is in progress and briefly display success states. If a failure occurs the icon turns red until you dismiss the notification.
- **Error notification** failed classification displays a notification with a button to clear the error and reset the icon.
- **View reasoning** inspect why rules matched via the Details popup. - **View reasoning** inspect why rules matched via the Details popup.
- **Cache management** clear cached results from the context menu or options page. - **Cache management** clear cached results from the context menu or options page.
- **Queue & timing stats** monitor processing time on the Maintenance tab. - **Queue & timing stats** monitor processing time on the Maintenance tab.
@ -81,6 +82,7 @@ Sortana is implemented entirely with standard WebExtension scripts—no custom e
open a compose window using the account that received the message. open a compose window using the account that received the message.
3. Save your settings. New mail will be evaluated automatically using the 3. Save your settings. New mail will be evaluated automatically using the
configured rules. configured rules.
4. If the toolbar icon shows a red X, click the notification's **Dismiss** button to clear the error.
### Example Filters ### Example Filters

View file

@ -30,6 +30,8 @@ let TurndownService = null;
let userTheme = 'auto'; let userTheme = 'auto';
let currentTheme = 'light'; let currentTheme = 'light';
let detectSystemTheme; let detectSystemTheme;
let errorPending = false;
const ERROR_NOTIFICATION_ID = 'sortana-error';
function normalizeRules(rules) { function normalizeRules(rules) {
return Array.isArray(rules) ? rules.map(r => { return Array.isArray(rules) ? rules.map(r => {
@ -68,7 +70,8 @@ const ICONS = {
logo: () => 'resources/img/logo.png', logo: () => 'resources/img/logo.png',
circledots: () => iconPaths('circledots'), circledots: () => iconPaths('circledots'),
circle: () => iconPaths('circle'), circle: () => iconPaths('circle'),
average: () => iconPaths('average') average: () => iconPaths('average'),
error: () => iconPaths('x')
}; };
function setIcon(path) { function setIcon(path) {
@ -82,19 +85,31 @@ function setIcon(path) {
function updateActionIcon() { function updateActionIcon() {
let path = ICONS.logo(); let path = ICONS.logo();
if (processing || queuedCount > 0) { if (errorPending) {
path = ICONS.error();
} else if (processing || queuedCount > 0) {
path = ICONS.circledots(); path = ICONS.circledots();
} }
setIcon(path); setIcon(path);
} }
function showTransientIcon(factory, delay = 1500) { function showTransientIcon(factory, delay = 1500) {
if (errorPending) {
return;
}
clearTimeout(iconTimer); clearTimeout(iconTimer);
const path = typeof factory === 'function' ? factory() : factory; const path = typeof factory === 'function' ? factory() : factory;
setIcon(path); setIcon(path);
iconTimer = setTimeout(updateActionIcon, delay); iconTimer = setTimeout(updateActionIcon, delay);
} }
async function clearError() {
errorPending = false;
await storage.local.set({ errorPending: false });
await browser.notifications.clear(ERROR_NOTIFICATION_ID);
updateActionIcon();
}
function refreshMenuIcons() { function refreshMenuIcons() {
browser.menus.update('apply-ai-rules-list', { icons: iconPaths('eye') }); browser.menus.update('apply-ai-rules-list', { icons: iconPaths('eye') });
browser.menus.update('apply-ai-rules-display', { icons: iconPaths('eye') }); browser.menus.update('apply-ai-rules-display', { icons: iconPaths('eye') });
@ -307,9 +322,17 @@ async function processMessage(id) {
const elapsed = Date.now() - currentStart; const elapsed = Date.now() - currentStart;
currentStart = 0; currentStart = 0;
updateTimingStats(elapsed); updateTimingStats(elapsed);
await storage.local.set({ classifyStats: timingStats }); await storage.local.set({ classifyStats: timingStats, errorPending: true });
errorPending = true;
logger.aiLog("failed to apply AI rules", { level: 'error' }, e); logger.aiLog("failed to apply AI rules", { level: 'error' }, e);
showTransientIcon(ICONS.average); setIcon(ICONS.error());
browser.notifications.create(ERROR_NOTIFICATION_ID, {
type: 'basic',
iconUrl: browser.runtime.getURL('resources/img/logo.png'),
title: 'Sortana Error',
message: 'Failed to apply AI rules',
buttons: [{ title: 'Dismiss' }]
});
} }
} }
async function applyAiRules(idsInput) { async function applyAiRules(idsInput) {
@ -368,7 +391,7 @@ async function clearCacheForMessages(idsInput) {
} }
try { try {
const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "aiRules", "theme"]); const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "aiRules", "theme", "errorPending"]);
logger.setDebug(store.debugLogging); logger.setDebug(store.debugLogging);
await AiClassifier.setConfig(store); await AiClassifier.setConfig(store);
userTheme = store.theme || 'auto'; userTheme = store.theme || 'auto';
@ -378,6 +401,7 @@ async function clearCacheForMessages(idsInput) {
stripUrlParams = store.stripUrlParams === true; stripUrlParams = store.stripUrlParams === true;
altTextImages = store.altTextImages === true; altTextImages = store.altTextImages === true;
collapseWhitespace = store.collapseWhitespace === true; collapseWhitespace = store.collapseWhitespace === true;
errorPending = store.errorPending === true;
const savedStats = await storage.local.get('classifyStats'); const savedStats = await storage.local.get('classifyStats');
if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') { if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') {
Object.assign(timingStats, savedStats.classifyStats); Object.assign(timingStats, savedStats.classifyStats);
@ -409,6 +433,10 @@ async function clearCacheForMessages(idsInput) {
collapseWhitespace = changes.collapseWhitespace.newValue === true; collapseWhitespace = changes.collapseWhitespace.newValue === true;
logger.aiLog("collapseWhitespace updated from storage change", { debug: true }, collapseWhitespace); logger.aiLog("collapseWhitespace updated from storage change", { debug: true }, collapseWhitespace);
} }
if (changes.errorPending) {
errorPending = changes.errorPending.newValue === true;
updateActionIcon();
}
if (changes.theme) { if (changes.theme) {
userTheme = changes.theme.newValue || 'auto'; userTheme = changes.theme.newValue || 'auto';
currentTheme = userTheme === 'auto' ? await detectSystemTheme() : userTheme; currentTheme = userTheme === 'auto' ? await detectSystemTheme() : userTheme;
@ -634,6 +662,18 @@ async function clearCacheForMessages(idsInput) {
logger.aiLog("Unhandled promise rejection", { level: 'error' }, ev.reason); logger.aiLog("Unhandled promise rejection", { level: 'error' }, ev.reason);
}); });
browser.notifications.onClicked.addListener(id => {
if (id === ERROR_NOTIFICATION_ID) {
clearError();
}
});
browser.notifications.onButtonClicked.addListener((id) => {
if (id === ERROR_NOTIFICATION_ID) {
clearError();
}
});
browser.runtime.onInstalled.addListener(async ({ reason }) => { browser.runtime.onInstalled.addListener(async ({ reason }) => {
if (reason === "install") { if (reason === "install") {
await browser.runtime.openOptionsPage(); await browser.runtime.openOptionsPage();