Persist error icon and add notification

This commit is contained in:
Jordan Wages 2025-07-15 23:16:43 -05:00
commit 65ff743676
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.
- **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.
- **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.
- **Cache management** clear cached results from the context menu or options page.
- **Queue & timing stats** monitor processing time on the Maintenance tab.
@ -76,11 +77,12 @@ Sortana is implemented entirely with standard WebExtension scripts—no custom e
deleting or archiving a message when it matches. Drag rules to
reorder them, check *Only apply to unread messages* to skip read mail,
set optional minimum or maximum message age limits, select the accounts or
folders a rule should apply to, uncheck *Enabled* to temporarily disable a rule, and
folders a rule should apply to, uncheck *Enabled* to temporarily disable a rule, and
check *Stop after match* to halt further processing. Forward and reply actions
open a compose window using the account that received the message.
3. Save your settings. New mail will be evaluated automatically using the
configured rules.
4. If the toolbar icon shows a red X, click the notification's **Dismiss** button to clear the error.
### Example Filters

View file

@ -30,6 +30,8 @@ let TurndownService = null;
let userTheme = 'auto';
let currentTheme = 'light';
let detectSystemTheme;
let errorPending = false;
const ERROR_NOTIFICATION_ID = 'sortana-error';
function normalizeRules(rules) {
return Array.isArray(rules) ? rules.map(r => {
@ -68,7 +70,8 @@ const ICONS = {
logo: () => 'resources/img/logo.png',
circledots: () => iconPaths('circledots'),
circle: () => iconPaths('circle'),
average: () => iconPaths('average')
average: () => iconPaths('average'),
error: () => iconPaths('x')
};
function setIcon(path) {
@ -82,19 +85,31 @@ function setIcon(path) {
function updateActionIcon() {
let path = ICONS.logo();
if (processing || queuedCount > 0) {
if (errorPending) {
path = ICONS.error();
} else if (processing || queuedCount > 0) {
path = ICONS.circledots();
}
setIcon(path);
}
function showTransientIcon(factory, delay = 1500) {
if (errorPending) {
return;
}
clearTimeout(iconTimer);
const path = typeof factory === 'function' ? factory() : factory;
setIcon(path);
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() {
browser.menus.update('apply-ai-rules-list', { 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;
currentStart = 0;
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);
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) {
@ -368,7 +391,7 @@ async function clearCacheForMessages(idsInput) {
}
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);
await AiClassifier.setConfig(store);
userTheme = store.theme || 'auto';
@ -378,6 +401,7 @@ async function clearCacheForMessages(idsInput) {
stripUrlParams = store.stripUrlParams === true;
altTextImages = store.altTextImages === true;
collapseWhitespace = store.collapseWhitespace === true;
errorPending = store.errorPending === true;
const savedStats = await storage.local.get('classifyStats');
if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') {
Object.assign(timingStats, savedStats.classifyStats);
@ -409,6 +433,10 @@ async function clearCacheForMessages(idsInput) {
collapseWhitespace = changes.collapseWhitespace.newValue === true;
logger.aiLog("collapseWhitespace updated from storage change", { debug: true }, collapseWhitespace);
}
if (changes.errorPending) {
errorPending = changes.errorPending.newValue === true;
updateActionIcon();
}
if (changes.theme) {
userTheme = changes.theme.newValue || 'auto';
currentTheme = userTheme === 'auto' ? await detectSystemTheme() : userTheme;
@ -634,6 +662,18 @@ async function clearCacheForMessages(idsInput) {
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 }) => {
if (reason === "install") {
await browser.runtime.openOptionsPage();