Add session error log and transient error icon
This commit is contained in:
parent
af6702bceb
commit
9269225a0c
4 changed files with 135 additions and 15 deletions
|
|
@ -24,8 +24,9 @@ with JSON indicating whether the 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 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.
|
||||
- **Status icons** – toolbar icons show when classification is in progress and briefly display success states. If a failure occurs the icon turns red briefly before returning to normal.
|
||||
- **Error notification** – failed classification displays a notification in Thunderbird.
|
||||
- **Session error log** – the Errors tab (visible only when errors occur) shows errors recorded since the last add-on start.
|
||||
- **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.
|
||||
|
|
@ -88,7 +89,7 @@ Sortana is implemented entirely with standard WebExtension scripts—no custom e
|
|||
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.
|
||||
4. If the toolbar icon shows a red X, it will clear after a few seconds. Open the Errors tab in Options to review the latest failures.
|
||||
|
||||
### Example Filters
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ let queue = Promise.resolve();
|
|||
let queuedCount = 0;
|
||||
let processing = false;
|
||||
let iconTimer = null;
|
||||
let errorTimer = null;
|
||||
let timingStats = { count: 0, mean: 0, m2: 0, total: 0, last: -1 };
|
||||
let currentStart = 0;
|
||||
let logGetTiming = true;
|
||||
|
|
@ -33,8 +34,11 @@ let userTheme = 'auto';
|
|||
let currentTheme = 'light';
|
||||
let detectSystemTheme;
|
||||
let errorPending = false;
|
||||
let errorLog = [];
|
||||
let showDebugTab = false;
|
||||
const ERROR_NOTIFICATION_ID = 'sortana-error';
|
||||
const ERROR_ICON_TIMEOUT = 4500;
|
||||
const MAX_ERROR_LOG = 50;
|
||||
|
||||
function normalizeRules(rules) {
|
||||
return Array.isArray(rules) ? rules.map(r => {
|
||||
|
|
@ -108,11 +112,33 @@ function showTransientIcon(factory, delay = 1500) {
|
|||
|
||||
async function clearError() {
|
||||
errorPending = false;
|
||||
await storage.local.set({ errorPending: false });
|
||||
clearTimeout(errorTimer);
|
||||
await browser.notifications.clear(ERROR_NOTIFICATION_ID);
|
||||
updateActionIcon();
|
||||
}
|
||||
|
||||
function recordError(context, err) {
|
||||
const message = err instanceof Error ? err.message : String(err || 'Unknown error');
|
||||
const detail = err instanceof Error ? err.stack : '';
|
||||
errorLog.unshift({
|
||||
time: Date.now(),
|
||||
context,
|
||||
message,
|
||||
detail
|
||||
});
|
||||
if (errorLog.length > MAX_ERROR_LOG) {
|
||||
errorLog.length = MAX_ERROR_LOG;
|
||||
}
|
||||
errorPending = true;
|
||||
updateActionIcon();
|
||||
clearTimeout(errorTimer);
|
||||
errorTimer = setTimeout(() => {
|
||||
errorPending = false;
|
||||
updateActionIcon();
|
||||
}, ERROR_ICON_TIMEOUT);
|
||||
browser.runtime.sendMessage({ type: 'sortana:errorLogUpdated', count: errorLog.length }).catch(() => {});
|
||||
}
|
||||
|
||||
function refreshMenuIcons() {
|
||||
browser.menus.update('apply-ai-rules-list', { icons: iconPaths('eye') });
|
||||
browser.menus.update('apply-ai-rules-display', { icons: iconPaths('eye') });
|
||||
|
|
@ -382,16 +408,14 @@ async function processMessage(id) {
|
|||
const elapsed = Date.now() - currentStart;
|
||||
currentStart = 0;
|
||||
updateTimingStats(elapsed);
|
||||
await storage.local.set({ classifyStats: timingStats, errorPending: true });
|
||||
errorPending = true;
|
||||
await storage.local.set({ classifyStats: timingStats });
|
||||
logger.aiLog("failed to apply AI rules", { level: 'error' }, e);
|
||||
setIcon(ICONS.error());
|
||||
recordError("Failed to apply AI rules", e);
|
||||
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' }]
|
||||
message: 'Failed to apply AI rules'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -451,7 +475,7 @@ async function clearCacheForMessages(idsInput) {
|
|||
}
|
||||
|
||||
try {
|
||||
const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "aiRules", "theme", "errorPending", "showDebugTab"]);
|
||||
const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "tokenReduction", "aiRules", "theme", "showDebugTab"]);
|
||||
logger.setDebug(store.debugLogging);
|
||||
await AiClassifier.setConfig(store);
|
||||
userTheme = store.theme || 'auto';
|
||||
|
|
@ -465,7 +489,6 @@ async function clearCacheForMessages(idsInput) {
|
|||
if (store.aiParams && typeof store.aiParams.max_tokens !== 'undefined') {
|
||||
maxTokens = parseInt(store.aiParams.max_tokens) || maxTokens;
|
||||
}
|
||||
errorPending = store.errorPending === true;
|
||||
showDebugTab = store.showDebugTab === true;
|
||||
const savedStats = await storage.local.get('classifyStats');
|
||||
if (savedStats.classifyStats && typeof savedStats.classifyStats === 'object') {
|
||||
|
|
@ -524,10 +547,6 @@ async function clearCacheForMessages(idsInput) {
|
|||
if (changes.showDebugTab) {
|
||||
showDebugTab = changes.showDebugTab.newValue === true;
|
||||
}
|
||||
if (changes.errorPending) {
|
||||
errorPending = changes.errorPending.newValue === true;
|
||||
updateActionIcon();
|
||||
}
|
||||
if (changes.theme) {
|
||||
userTheme = changes.theme.newValue || 'auto';
|
||||
currentTheme = userTheme === 'auto' ? await detectSystemTheme() : userTheme;
|
||||
|
|
@ -720,6 +739,8 @@ async function clearCacheForMessages(idsInput) {
|
|||
}
|
||||
} else if (msg?.type === "sortana:getQueueCount") {
|
||||
return { count: queuedCount + (processing ? 1 : 0) };
|
||||
} else if (msg?.type === "sortana:getErrorLog") {
|
||||
return { errors: errorLog.slice() };
|
||||
} else if (msg?.type === "sortana:getTiming") {
|
||||
const t = timingStats;
|
||||
const std = t.count > 1 ? Math.sqrt(t.m2 / (t.count - 1)) : 0;
|
||||
|
|
@ -751,6 +772,7 @@ async function clearCacheForMessages(idsInput) {
|
|||
// Catch any unhandled rejections
|
||||
window.addEventListener("unhandledrejection", ev => {
|
||||
logger.aiLog("Unhandled promise rejection", { level: 'error' }, ev.reason);
|
||||
recordError("Unhandled promise rejection", ev.reason);
|
||||
});
|
||||
|
||||
browser.notifications.onClicked.addListener(id => {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
<li class="is-active" data-tab="settings"><a><span class="icon is-small"><img data-icon="settings" data-size="16" src="../resources/img/settings-light-16.png" alt=""></span><span>Settings</span></a></li>
|
||||
<li data-tab="rules"><a><span class="icon is-small"><img data-icon="clipboarddata" data-size="16" src="../resources/img/clipboarddata-light-16.png" alt=""></span><span>Rules</span></a></li>
|
||||
<li data-tab="maintenance"><a><span class="icon is-small"><img data-icon="gear" data-size="16" src="../resources/img/gear-light-16.png" alt=""></span><span>Maintenance</span></a></li>
|
||||
<li id="errors-tab-button" class="is-hidden" data-tab="errors"><a><span class="icon is-small"><img data-icon="x" data-size="16" src="../resources/img/x-light-16.png" alt=""></span><span>Errors</span></a></li>
|
||||
<li id="debug-tab-button" class="is-hidden" data-tab="debug"><a><span class="icon is-small"><img data-icon="average" data-size="16" src="../resources/img/average-light-16.png" alt=""></span><span>Debug</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -285,6 +286,32 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="errors-tab" class="tab-content is-hidden">
|
||||
<h2 class="title is-4">
|
||||
<span class="icon is-small"><img data-icon="x" data-size="16" src="../resources/img/x-light-16.png" alt=""></span>
|
||||
<span>Session Errors</span>
|
||||
</h2>
|
||||
<div id="errors-empty" class="notification is-success is-light">
|
||||
No errors have been recorded since the last start.
|
||||
</div>
|
||||
<div id="errors-panel" class="is-hidden">
|
||||
<div class="box mb-4">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<p class="title is-5 mb-1">Error Log</p>
|
||||
<p class="subtitle is-6">Visible only for this session.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<span class="tag is-danger is-light" id="errors-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="errors-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="debug-tab" class="tab-content is-hidden">
|
||||
<h2 class="title is-4">
|
||||
<span class="icon is-small"><img data-icon="average" data-size="16" src="../resources/img/average-light-16.png" alt=""></span>
|
||||
|
|
|
|||
|
|
@ -196,6 +196,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
|
||||
const debugTabToggle = document.getElementById('show-debug-tab');
|
||||
const debugTabBtn = document.getElementById('debug-tab-button');
|
||||
const errorTabBtn = document.getElementById('errors-tab-button');
|
||||
const errorsEmpty = document.getElementById('errors-empty');
|
||||
const errorsPanel = document.getElementById('errors-panel');
|
||||
const errorsList = document.getElementById('errors-list');
|
||||
const errorsCount = document.getElementById('errors-count');
|
||||
function updateDebugTab() {
|
||||
const visible = debugTabToggle.checked;
|
||||
debugTabBtn.classList.toggle('is-hidden', !visible);
|
||||
|
|
@ -204,6 +209,71 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
debugTabToggle.addEventListener('change', () => { updateDebugTab(); markDirty(); });
|
||||
updateDebugTab();
|
||||
|
||||
function formatErrorTime(value) {
|
||||
try {
|
||||
return new Date(value).toLocaleString();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function renderErrors(entries = []) {
|
||||
const hasErrors = entries.length > 0;
|
||||
errorTabBtn.classList.toggle('is-hidden', !hasErrors);
|
||||
errorsEmpty.classList.toggle('is-hidden', hasErrors);
|
||||
errorsPanel.classList.toggle('is-hidden', !hasErrors);
|
||||
errorsList.innerHTML = '';
|
||||
errorsCount.textContent = String(entries.length);
|
||||
if (!hasErrors) {
|
||||
return;
|
||||
}
|
||||
entries.forEach(entry => {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'message is-danger is-light mb-4';
|
||||
const header = document.createElement('div');
|
||||
header.className = 'message-header';
|
||||
const title = document.createElement('p');
|
||||
title.textContent = entry.context || 'Error';
|
||||
const time = document.createElement('span');
|
||||
time.className = 'is-size-7 has-text-weight-normal';
|
||||
time.textContent = formatErrorTime(entry.time);
|
||||
header.appendChild(title);
|
||||
header.appendChild(time);
|
||||
const body = document.createElement('div');
|
||||
body.className = 'message-body';
|
||||
const summary = document.createElement('p');
|
||||
summary.className = 'mb-2';
|
||||
summary.textContent = entry.message || 'Unknown error';
|
||||
body.appendChild(summary);
|
||||
if (entry.detail) {
|
||||
const detail = document.createElement('pre');
|
||||
detail.className = 'is-family-monospace is-size-7';
|
||||
detail.textContent = entry.detail;
|
||||
body.appendChild(detail);
|
||||
}
|
||||
card.appendChild(header);
|
||||
card.appendChild(body);
|
||||
errorsList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadErrors() {
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({ type: 'sortana:getErrorLog' });
|
||||
renderErrors(response?.errors || []);
|
||||
} catch (e) {
|
||||
renderErrors([]);
|
||||
}
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((msg) => {
|
||||
if (msg?.type === 'sortana:errorLogUpdated') {
|
||||
loadErrors();
|
||||
}
|
||||
});
|
||||
|
||||
await loadErrors();
|
||||
|
||||
updateDiffDisplay();
|
||||
|
||||
[htmlToggle, stripUrlToggle, altTextToggle, collapseWhitespaceToggle, tokenReductionToggle].forEach(toggle => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue