Improved error feedbacks

This commit is contained in:
Pierre committed 2026-10-04 12:39:38 +02:00
1 parent ece851f5a0
commit cfb5334666
12 files changed
+253 -13

No files matched your search

+2
View File
@@ -6,3 +6,5 @@
.log-message.level-WARNING{color:var(--vscode-editorWarning-foreground,#f3cd80)}.log-message.level-ERROR{color:var(--vscode-editorError-foreground,#ffacac)}
.block-title{font-weight:600;margin-bottom:6px;color:var(--fg);overflow-wrap:anywhere}
#error-count{font-weight:700;font-size:14px}#error-count.has-errors{color:var(--vscode-editorError-foreground,#ffacac);border-color:currentColor;background:var(--vscode-inputValidation-errorBackground,#5b3039)}
+1 -1
View File
@@ -3,7 +3,7 @@
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src {{CSP}}; script-src 'nonce-{{NONCE}}';">
<link rel="stylesheet" href="{{CSS}}"><title>AppDaemon · Logs</title></head>
<body>
<header><div><h1>AppDaemon <span>Logs</span></h1><p id="status" role="status">Reading logs…</p></div><div class="actions"><button id="import">Open history</button><button id="copy">Copy selected log</button><button id="pause">Pause</button></div></header>
<header><div><h1>AppDaemon <span>Logs</span></h1><p id="status" role="status">Reading logs…</p></div><div class="actions"><button id="error-count" title="Show errors across all apps in loaded history">0 errors</button><button id="import">Open history</button><button id="clear" title="Empty configured live log files; imported history files are kept">Clear logs</button><button id="copy">Copy selected log</button><button id="pause">Pause</button></div></header>
<nav aria-label="Filters"><input id="search" type="search" placeholder="Search apps, entities and messages…" aria-label="Search"><select id="period" aria-label="Time range"><option value="15">15 minutes</option><option value="60">1 hour</option><option value="1440">24 hours</option><option value="0" selected>All loaded</option></select><select id="level" aria-label="Level"><option value="all">All levels</option><option value="INFO">INFO</option><option value="WARNING">WARNING</option><option value="ERROR">ERROR</option><option value="DEBUG">DEBUG</option></select></nav>
<div id="warnings" role="status"></div>
<main><section class="apps" aria-label="Applications"><div class="section-title"><h2>Applications</h2><span id="count"></span><select id="sort" aria-label="Sort apps"><option value="latest" selected>Latest log first</option><option value="oldest">Oldest log first</option><option value="name">App name</option></select></div><div id="apps"></div></section><section class="causes" aria-label="Log details"><div class="section-title"><h2>Log details</h2><div><button id="graph-mode" aria-pressed="true">Graph</button><button id="tree-mode" aria-pressed="false">Tree</button></div></div><div id="detail"><div class="empty">Select a log to view its details.<p>The base AppDaemon log format does not include causal links.</p></div></div></section></main>
+46 -2
View File
@@ -21,6 +21,7 @@
const appFlashTokens = new Map();
let receivedInitialData = false;
let pointerSelecting = false;
let errorView;
const $ = id => document.getElementById(id);
const node = (tag, text, cls) => { const e = document.createElement(tag); if (text !== undefined) e.textContent = text; if (cls) e.className = cls; return e; };
const normalLevel = e => e.level === 'CRITICAL' ? 'ERROR' : e.level;
@@ -135,6 +136,7 @@
if (!lastLogs.has(e.app) || e.time > lastLogs.get(e.app).time) lastLogs.set(e.app, e);
});
const timeOf = (app, logs) => (lastLogs.get(app) || logs[logs.length - 1]).time;
$('error-count').setAttribute('aria-pressed', String($('level').value === 'ERROR'));
[...groups].sort(([a, al], [b, bl]) => {
const order = $('sort').value;
if (order === 'name') return a.localeCompare(b);
@@ -193,7 +195,15 @@
const selectedGroup = visibleEventGroups.get(e.event_id) || [e];
container.replaceChildren(logMessages(selectedGroup, 'selected-message'), node('div', `${e.app} · ${e.timestamp} · ${selectedGroup.length} log lines`, 'metadata'));
const raw = node('button', 'Open raw log'); raw.onclick = () => vscode.postMessage({ type: 'open', id: e.event_id }); container.append(raw);
if (e.source) { const source = node('button', 'Open source'); source.onclick = () => vscode.postMessage({ type: 'source', id: e.event_id }); container.append(source); }
const sourceEntries = selectedGroup.filter(entry => entry.source);
const sourceLocations = new Set();
sourceEntries.forEach(entry => {
const location = `${entry.source.path}:${entry.source.line}`;
if (sourceLocations.has(location)) return;
sourceLocations.add(location);
const source = node('button', 'Open source'); source.title = location;
source.onclick = () => vscode.postMessage({ type: 'source', id: entry.event_id }); container.append(source);
});
if (origin && origin !== selected) { const back = node('button', 'Back to selected log'); back.onclick = () => choose(origin); container.append(back); }
const byId = new Map(entries.map(e => [e.event_id, e]));
const mirroredEvaluations = new Map();
@@ -315,6 +325,10 @@
$('status').title = traceCount
? 'Showing AppDaemon logs and additional records from configured history files.'
: 'Reading the configured AppDaemon text logs. Causal links require additional metadata that the base log format does not contain.';
const errorCount = entries.filter(e => (e.kind === 'log' || e.kind.startsWith('initialization_')) && normalLevel(e) === 'ERROR').length;
$('error-count').textContent = `${errorCount} error${errorCount === 1 ? '' : 's'}`;
$('error-count').classList.toggle('has-errors', errorCount > 0);
$('error-count').title = 'Errors and critical logs across all apps in loaded history. Click to show them.';
$('warnings').textContent = (message.warnings || []).join(' · '); renderApps(); renderDetail();
}
function refreshLastLogAges() {
@@ -325,6 +339,12 @@
function selectionActive() { return pointerSelecting || Boolean(window.getSelection()?.toString()); }
window.addEventListener('message', event => {
if (event.data?.type === 'copied') { $('copy').textContent = 'Copied'; setTimeout(() => $('copy').textContent = 'Copy selected log', 1200); return; }
if (event.data?.type === 'clear-result') {
$('clear').disabled = false;
$('clear').textContent = 'Clear logs';
if (event.data.failures?.length) $('warnings').textContent = event.data.failures.join(' · ');
return;
}
if (event.data?.type !== 'entries') return;
latest = event.data;
if (!paused && !selectionActive()) apply(latest);
@@ -341,9 +361,33 @@
}
});
$('copy').onclick = () => { const e = entries.find(e => e.event_id === selected); copy(window.getSelection()?.toString() || (e ? (visibleEventGroups.get(e.event_id) || [e]).map(copyText).join('\n') : '')); };
['search', 'period', 'level', 'sort'].forEach(id => $(id).addEventListener('input', () => { save(); renderApps(); }));
['search', 'period', 'level', 'sort'].forEach(id => $(id).addEventListener('input', () => {
errorView = undefined; selected = undefined; origin = undefined;
save(); renderApps(); renderDetail();
}));
$('pause').onclick = () => { paused = !paused; $('pause').textContent = paused ? 'Resume' : 'Pause'; if (!paused && latest) apply(latest); else if (paused) $('status').textContent = 'Display paused · New events are still being collected'; };
$('error-count').onclick = () => {
const leaving = $('level').value === 'ERROR';
if (leaving) {
$('search').value = errorView?.search || '';
$('period').value = errorView?.period || '0';
$('level').value = errorView?.level || 'all';
if (errorView) { expanded.clear(); errorView.expanded.forEach(app => expanded.add(app)); }
errorView = undefined;
} else {
errorView = { search: $('search').value, period: $('period').value, level: $('level').value, expanded: [...expanded] };
$('search').value = ''; $('period').value = '0'; $('level').value = 'ERROR';
entries.filter(e => normalLevel(e) === 'ERROR').forEach(e => expanded.add(e.app));
}
selected = undefined; origin = undefined;
save(); renderApps(); renderDetail();
if (!leaving) {
const first = document.querySelector('#apps .row');
if (first) { first.click(); first.closest('.rows').scrollTop = 0; first.focus({ preventScroll: true }); }
}
};
$('import').onclick = () => vscode.postMessage({ type: 'import' });
$('clear').onclick = () => { selected = undefined; origin = undefined; save(); $('clear').disabled = true; $('clear').textContent = 'Clearing…'; vscode.postMessage({ type: 'clear' }); };
$('graph-mode').onclick = () => { mode = 'graph'; save(); renderDetail(); };
$('tree-mode').onclick = () => { mode = 'tree'; save(); renderDetail(); };
$('sort').value = saved.sort || 'latest';