clean up deprecated buttons
This commit is contained in:
1 parent
99422d8cfc
commit
5614ad049d
12 files changed
+36
-161
No files matched your search
@@ -17,11 +17,11 @@ By default, it reads the existing AppDaemon text logs from the workspace:
|
|||||||
- `logs/error.log`
|
- `logs/error.log`
|
||||||
- `logs/virtualsensors.log`
|
- `logs/virtualsensors.log`
|
||||||
|
|
||||||
The explorer groups lines by the app name in the standard AppDaemon log format. It provides INFO/WARNING/ERROR counts, expandable app histories, relative timestamps, sorting by latest activity or app name, text/time/level filters, live updates, copy/paste, raw-log navigation, and archived-log import. Duplicate error lines found in multiple files are merged. The log reader handles file rotation, truncation, multiline exceptions, UTF-8, and legacy Windows-1252 bytes.
|
The explorer groups lines by the app name in the standard AppDaemon log format. It provides INFO/WARNING/ERROR counts, expandable app histories, relative timestamps, sorting by latest activity or app name, text/time/level filters, live updates, and raw-log navigation. Duplicate error lines found in multiple files are merged. The log reader handles file rotation, truncation, multiline exceptions, UTF-8, and legacy Windows-1252 bytes.
|
||||||
|
|
||||||
**AD Logs** turns red when the loaded logs contain an ERROR or CRITICAL entry. **Clear logs** empties the configured live log files and clears the current explorer history; imported archive files are left on disk.
|
**AD Logs** turns red when the loaded logs contain an ERROR or CRITICAL entry. **Clear logs** empties the configured log files and clears the current explorer history.
|
||||||
|
|
||||||
The standard AppDaemon text logs do not contain causal links or reliable initialization boundaries. The details panel will say so for ordinary entries. Existing messages that already contain trace or initialization metadata can still be grouped or linked; the explorer does not create that metadata. **Open history** can also load archived files. No AppDaemon Python changes are needed for this raw-log view.
|
The standard AppDaemon text logs do not contain causal links or reliable initialization boundaries. The details panel will say so for ordinary entries. Existing messages that already contain trace or initialization metadata can still be grouped or linked; the explorer does not create that metadata. No AppDaemon Python changes are needed for this raw-log view.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ settings file.
|
|||||||
- Home Assistant entity completion and hover information in Python/YAML.
|
- Home Assistant entity completion and hover information in Python/YAML.
|
||||||
- Navigation to AppDaemon app and virtual-sensor definitions.
|
- Navigation to AppDaemon app and virtual-sensor definitions.
|
||||||
- AppDaemon errors as VS Code diagnostics and an output channel.
|
- AppDaemon errors as VS Code diagnostics and an output channel.
|
||||||
- Status bar actions for app reload, Home Assistant restart, production mode,
|
- Status bar actions for app reload, Home Assistant restart, log exploration,
|
||||||
error handling and apps defined in the active YAML file.
|
and apps defined in the active YAML file.
|
||||||
|
|
||||||
| Shortcut | Action |
|
| Shortcut | Action |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -72,7 +72,7 @@ settings file.
|
|||||||
Commands are available under **AppDaemon:** in the Command Palette: **Go to
|
Commands are available under **AppDaemon:** in the Command Palette: **Go to
|
||||||
App**, **Restart Apps in Current File**, **Reload Apps**, **Restart Home
|
App**, **Restart Apps in Current File**, **Reload Apps**, **Restart Home
|
||||||
Assistant**, **Toggle Production Mode**, **Show Error Log**, **Refresh Entity
|
Assistant**, **Toggle Production Mode**, **Show Error Log**, **Refresh Entity
|
||||||
List**, **Handle Errors**, **Clear Error Diagnostics**, and **Explore Logs**.
|
List**, **Clear Error Diagnostics**, and **Explore Logs**.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src {{CSP}}; script-src 'nonce-{{NONCE}}';">
|
<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>
|
<link rel="stylesheet" href="{{CSS}}"><title>AppDaemon · Logs</title></head>
|
||||||
<body>
|
<body>
|
||||||
<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>
|
<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="clear" title="Empty configured log files">Clear logs</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>
|
<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>
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
/* Shared UI: VS Code transport or a standalone review fixture. No log HTML is executed. */
|
/* Shared UI: VS Code transport or a standalone review fixture. No log HTML is executed. */
|
||||||
(() => {
|
(() => {
|
||||||
const inVSCode = typeof acquireVsCodeApi === 'function';
|
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : {
|
||||||
const vscode = inVSCode ? acquireVsCodeApi() : {
|
postMessage() {}, getState() {}, setState() {}
|
||||||
async postMessage(message) {
|
|
||||||
if (message.type !== 'copy') return;
|
|
||||||
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(message.text);
|
|
||||||
else {
|
|
||||||
const area = document.createElement('textarea'); area.value = message.text;
|
|
||||||
document.body.append(area); area.select(); document.execCommand?.('copy'); area.remove();
|
|
||||||
}
|
|
||||||
window.dispatchEvent(new MessageEvent('message', { data: { type: 'copied' } }));
|
|
||||||
}, getState() {}, setState() {}
|
|
||||||
};
|
};
|
||||||
const saved = vscode.getState() || {};
|
const saved = vscode.getState() || {};
|
||||||
let entries = [], latest, appliedMessage, displayZone = 'Europe/Berlin', paused = false, selected = saved.selected, mode = saved.mode || 'graph', origin = saved.origin || saved.selected;
|
let entries = [], latest, appliedMessage, displayZone = 'Europe/Berlin', selected = saved.selected, mode = saved.mode || 'graph', origin = saved.origin || saved.selected;
|
||||||
const expanded = new Set(saved.expanded || []);
|
const expanded = new Set(saved.expanded || []);
|
||||||
const visibleEventGroups = new Map();
|
const visibleEventGroups = new Map();
|
||||||
const logScrollPositions = new Map();
|
const logScrollPositions = new Map();
|
||||||
@@ -42,7 +33,6 @@
|
|||||||
return new Intl.DateTimeFormat(undefined, { timeZone: displayZone, day: '2-digit', month: 'short', ...(includeYear ? { year: 'numeric' } : {}), hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', timeZoneName: 'shortOffset' }).format(new Date(entry.time));
|
return new Intl.DateTimeFormat(undefined, { timeZone: displayZone, day: '2-digit', month: 'short', ...(includeYear ? { year: 'numeric' } : {}), hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', timeZoneName: 'shortOffset' }).format(new Date(entry.time));
|
||||||
} catch { return entry.timestamp; }
|
} catch { return entry.timestamp; }
|
||||||
};
|
};
|
||||||
const copyText = e => `${formatTime(e, true)} ${e.level} ${e.app}: ${cleanMessage(e)}`;
|
|
||||||
function timeSince(time) {
|
function timeSince(time) {
|
||||||
const seconds = Math.max(0, Math.floor((Date.now() - Number(time)) / 1000));
|
const seconds = Math.max(0, Math.floor((Date.now() - Number(time)) / 1000));
|
||||||
if (seconds < 60) return `${seconds} second${seconds === 1 ? '' : 's'} ago`;
|
if (seconds < 60) return `${seconds} second${seconds === 1 ? '' : 's'} ago`;
|
||||||
@@ -70,7 +60,6 @@
|
|||||||
if (days < 365) return `${months} mo ago`;
|
if (days < 365) return `${months} mo ago`;
|
||||||
return `${Math.floor(days / 365)} yr ago`;
|
return `${Math.floor(days / 365)} yr ago`;
|
||||||
}
|
}
|
||||||
function copy(text) { if (text) vscode.postMessage({ type: 'copy', text }); }
|
|
||||||
function logMessages(items, cls) {
|
function logMessages(items, cls) {
|
||||||
const container = node('div', undefined, cls);
|
const container = node('div', undefined, cls);
|
||||||
items.forEach((entry, index) => {
|
items.forEach((entry, index) => {
|
||||||
@@ -361,7 +350,7 @@
|
|||||||
? `${logCount} log lines + ${traceCount} ${traceLabel} · Live updates · Up to ${message.limit || 10000} records`
|
? `${logCount} log lines + ${traceCount} ${traceLabel} · Live updates · Up to ${message.limit || 10000} records`
|
||||||
: `${logCount} log lines · Live updates · Up to ${message.limit || 10000} records`;
|
: `${logCount} log lines · Live updates · Up to ${message.limit || 10000} records`;
|
||||||
$('status').title = traceCount
|
$('status').title = traceCount
|
||||||
? 'Showing AppDaemon logs and additional records from configured history files.'
|
? 'Showing AppDaemon logs and additional records from configured log files.'
|
||||||
: 'Reading the configured AppDaemon text logs. Causal links require additional metadata that the base log format does not contain.';
|
: '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;
|
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').textContent = `${errorCount} error${errorCount === 1 ? '' : 's'}`;
|
||||||
@@ -376,7 +365,6 @@
|
|||||||
setInterval(refreshLastLogAges, 1000);
|
setInterval(refreshLastLogAges, 1000);
|
||||||
function selectionActive() { return pointerSelecting || Boolean(window.getSelection()?.toString()); }
|
function selectionActive() { return pointerSelecting || Boolean(window.getSelection()?.toString()); }
|
||||||
window.addEventListener('message', event => {
|
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') {
|
if (event.data?.type === 'clear-result') {
|
||||||
$('clear').disabled = false;
|
$('clear').disabled = false;
|
||||||
$('clear').textContent = 'Clear logs';
|
$('clear').textContent = 'Clear logs';
|
||||||
@@ -385,25 +373,16 @@
|
|||||||
}
|
}
|
||||||
if (event.data?.type !== 'entries') return;
|
if (event.data?.type !== 'entries') return;
|
||||||
latest = event.data;
|
latest = event.data;
|
||||||
if (!paused && !selectionActive()) apply(latest);
|
if (!selectionActive()) apply(latest);
|
||||||
else if (!paused) $('status').textContent = 'Updates buffered while text is selected';
|
else $('status').textContent = 'Updates buffered while text is selected';
|
||||||
});
|
});
|
||||||
document.addEventListener('pointerdown', event => { pointerSelecting = Boolean(event.target.closest?.('.row, .selected-message, .node, .values')); });
|
document.addEventListener('pointerdown', event => { pointerSelecting = Boolean(event.target.closest?.('.row, .selected-message, .node, .values')); });
|
||||||
document.addEventListener('pointerup', () => { pointerSelecting = false; });
|
document.addEventListener('pointerup', () => { pointerSelecting = false; });
|
||||||
document.addEventListener('selectionchange', () => { if (!paused && !selectionActive() && latest && latest !== appliedMessage) setTimeout(() => { if (!paused && !selectionActive() && latest !== appliedMessage) apply(latest); }, 0); });
|
document.addEventListener('selectionchange', () => { if (!selectionActive() && latest && latest !== appliedMessage) setTimeout(() => { if (!selectionActive() && latest !== appliedMessage) apply(latest); }, 0); });
|
||||||
document.addEventListener('keydown', event => {
|
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c' && !event.target.closest?.('input, textarea, [contenteditable=true]')) {
|
|
||||||
const entry = entries.find(e => e.event_id === selected);
|
|
||||||
const text = window.getSelection()?.toString() || (entry ? (visibleEventGroups.get(entry.event_id) || [entry]).map(copyText).join('\n') : '');
|
|
||||||
if (text && (inVSCode || !window.getSelection()?.toString())) { event.preventDefault(); copy(text); }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('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', () => {
|
['search', 'period', 'level', 'sort'].forEach(id => $(id).addEventListener('input', () => {
|
||||||
errorView = undefined; selected = undefined; origin = undefined;
|
errorView = undefined; selected = undefined; origin = undefined;
|
||||||
save(); renderApps(); renderDetail();
|
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 = () => {
|
$('error-count').onclick = () => {
|
||||||
const leaving = $('level').value === 'ERROR';
|
const leaving = $('level').value === 'ERROR';
|
||||||
if (leaving) {
|
if (leaving) {
|
||||||
@@ -424,7 +403,6 @@
|
|||||||
if (first) { first.click(); first.closest('.rows').scrollTop = 0; first.focus({ preventScroll: true }); }
|
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' }); };
|
$('clear').onclick = () => { selected = undefined; origin = undefined; save(); $('clear').disabled = true; $('clear').textContent = 'Clearing…'; vscode.postMessage({ type: 'clear' }); };
|
||||||
$('graph-mode').onclick = () => { mode = 'graph'; save(); renderDetail(); };
|
$('graph-mode').onclick = () => { mode = 'graph'; save(); renderDetail(); };
|
||||||
$('tree-mode').onclick = () => { mode = 'tree'; save(); renderDetail(); };
|
$('tree-mode').onclick = () => { mode = 'tree'; save(); renderDetail(); };
|
||||||
|
|||||||
@@ -113,10 +113,6 @@
|
|||||||
"title": "AppDaemon: Refresh Entity List",
|
"title": "AppDaemon: Refresh Entity List",
|
||||||
"icon": "$(sync)"
|
"icon": "$(sync)"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"command": "appdaemon.handleErrors",
|
|
||||||
"title": "AppDaemon: Handle Errors"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"command": "appdaemon.clearErrors",
|
"command": "appdaemon.clearErrors",
|
||||||
"title": "AppDaemon: Clear Error Diagnostics"
|
"title": "AppDaemon: Clear Error Diagnostics"
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ function normalizeUrl(raw: string): string {
|
|||||||
|
|
||||||
export class ADClient {
|
export class ADClient {
|
||||||
private productionMode = false;
|
private productionMode = false;
|
||||||
private _onProductionModeChanged = new vscode.EventEmitter<boolean>();
|
|
||||||
readonly onProductionModeChanged = this._onProductionModeChanged.event;
|
|
||||||
private log: vscode.OutputChannel;
|
private log: vscode.OutputChannel;
|
||||||
|
|
||||||
constructor(log: vscode.OutputChannel) {
|
constructor(log: vscode.OutputChannel) {
|
||||||
@@ -120,7 +118,6 @@ export class ADClient {
|
|||||||
async setProductionMode(mode: boolean): Promise<void> {
|
async setProductionMode(mode: boolean): Promise<void> {
|
||||||
await this.request('POST', '/api/appdaemon/service/admin/production_mode/set', { mode });
|
await this.request('POST', '/api/appdaemon/service/admin/production_mode/set', { mode });
|
||||||
this.productionMode = mode;
|
this.productionMode = mode;
|
||||||
this._onProductionModeChanged.fire(mode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,7 +132,6 @@ export class ADClient {
|
|||||||
if (key.includes('production_mode')) {
|
if (key.includes('production_mode')) {
|
||||||
const val = (state[key] as { state?: unknown })?.state;
|
const val = (state[key] as { state?: unknown })?.state;
|
||||||
this.productionMode = val === true || val === 'True' || val === 'true';
|
this.productionMode = val === true || val === 'True' || val === 'true';
|
||||||
this._onProductionModeChanged.fire(this.productionMode);
|
|
||||||
return this.productionMode;
|
return this.productionMode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,6 +151,5 @@ export class ADClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._onProductionModeChanged.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,10 +11,6 @@ export class ErrorViewer {
|
|||||||
private diagnosticCollection: vscode.DiagnosticCollection;
|
private diagnosticCollection: vscode.DiagnosticCollection;
|
||||||
private watcher: fs.FSWatcher | undefined;
|
private watcher: fs.FSWatcher | undefined;
|
||||||
private lastSize = 0;
|
private lastSize = 0;
|
||||||
private errorCount = 0;
|
|
||||||
|
|
||||||
private _onErrorCountChanged = new vscode.EventEmitter<number>();
|
|
||||||
readonly onErrorCountChanged = this._onErrorCountChanged.event;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.outputChannel = vscode.window.createOutputChannel('AppDaemon Errors');
|
this.outputChannel = vscode.window.createOutputChannel('AppDaemon Errors');
|
||||||
@@ -30,12 +26,6 @@ export class ErrorViewer {
|
|||||||
|
|
||||||
clearDiagnostics() {
|
clearDiagnostics() {
|
||||||
this.diagnosticCollection.clear();
|
this.diagnosticCollection.clear();
|
||||||
this.errorCount = 0;
|
|
||||||
this._onErrorCountChanged.fire(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getErrorCount(): number {
|
|
||||||
return this.errorCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── file watching ────────────────────────────────────────────────────────
|
// ── file watching ────────────────────────────────────────────────────────
|
||||||
@@ -115,12 +105,9 @@ export class ErrorViewer {
|
|||||||
this.outputChannel.appendLine(trimmed);
|
this.outputChannel.appendLine(trimmed);
|
||||||
|
|
||||||
if (/\b(ERROR|CRITICAL|WARNING)\b/.test(trimmed)) {
|
if (/\b(ERROR|CRITICAL|WARNING)\b/.test(trimmed)) {
|
||||||
this.errorCount++;
|
|
||||||
this.parseDiagnostic(trimmed);
|
this.parseDiagnostic(trimmed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._onErrorCountChanged.fire(this.errorCount);
|
|
||||||
} finally {
|
} finally {
|
||||||
fs.closeSync(fd);
|
fs.closeSync(fd);
|
||||||
}
|
}
|
||||||
@@ -183,6 +170,5 @@ export class ErrorViewer {
|
|||||||
}
|
}
|
||||||
this.outputChannel.dispose();
|
this.outputChannel.dispose();
|
||||||
this.diagnosticCollection.dispose();
|
this.diagnosticCollection.dispose();
|
||||||
this._onErrorCountChanged.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,12 +347,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
errorViewer = new ErrorViewer();
|
errorViewer = new ErrorViewer();
|
||||||
const logExplorer = new LogExplorer(context, count => statusBar.setLogErrorCount(count));
|
const logExplorer = new LogExplorer(context, count => statusBar.setLogErrorCount(count));
|
||||||
context.subscriptions.push(logExplorer, vscode.commands.registerCommand('appdaemon.exploreLogs', () => logExplorer.show()));
|
context.subscriptions.push(logExplorer, vscode.commands.registerCommand('appdaemon.exploreLogs', () => logExplorer.show()));
|
||||||
statusBar = new StatusBarManager(adClient);
|
statusBar = new StatusBarManager();
|
||||||
logExplorer.startPolling();
|
logExplorer.startPolling();
|
||||||
|
|
||||||
// Wire error count → status bar badge
|
|
||||||
errorViewer.onErrorCountChanged((count) => statusBar.setErrorCount(count));
|
|
||||||
|
|
||||||
// ── Contextual per-file restart button ───────────────────────────────────
|
// ── Contextual per-file restart button ───────────────────────────────────
|
||||||
updateContextualButton(vscode.window.activeTextEditor);
|
updateContextualButton(vscode.window.activeTextEditor);
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
@@ -544,18 +541,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
vscode.commands.registerCommand('appdaemon.showErrors', () => {
|
vscode.commands.registerCommand('appdaemon.showErrors', () => {
|
||||||
errorViewer.show();
|
errorViewer.show();
|
||||||
}),
|
}),
|
||||||
vscode.commands.registerCommand('appdaemon.handleErrors', async () => {
|
|
||||||
const pick = await vscode.window.showErrorMessage(
|
|
||||||
'AppDaemon errors detected',
|
|
||||||
'Show log',
|
|
||||||
'Clear'
|
|
||||||
);
|
|
||||||
if (pick === 'Show log') {
|
|
||||||
errorViewer.show();
|
|
||||||
} else if (pick === 'Clear') {
|
|
||||||
errorViewer.clearDiagnostics();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
vscode.commands.registerCommand('appdaemon.refreshEntities', async () => {
|
vscode.commands.registerCommand('appdaemon.refreshEntities', async () => {
|
||||||
const entities = await haClient.fetchEntities();
|
const entities = await haClient.fetchEntities();
|
||||||
vscode.window.showInformationMessage(`AppDaemon: ${entities.length} entities loaded`);
|
vscode.window.showInformationMessage(`AppDaemon: ${entities.length} entities loaded`);
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export class LogExplorer implements vscode.Disposable {
|
|||||||
private timer?: ReturnType<typeof setInterval>;
|
private timer?: ReturnType<typeof setInterval>;
|
||||||
private entries = new Map<string, LogEntry>();
|
private entries = new Map<string, LogEntry>();
|
||||||
private cursors = new Map<string, Cursor>();
|
private cursors = new Map<string, Cursor>();
|
||||||
private imported = new Set<string>();
|
|
||||||
private warnings = new Set<string>();
|
private warnings = new Set<string>();
|
||||||
private busy = false;
|
private busy = false;
|
||||||
private sourceDocuments?: Promise<SourceDocument[]>;
|
private sourceDocuments?: Promise<SourceDocument[]>;
|
||||||
@@ -58,10 +57,6 @@ export class LogExplorer implements vscode.Disposable {
|
|||||||
.replace('{{CSS}}', media('logExplorer.css').toString()).replace('{{JS}}', media('logExplorer.js').toString());
|
.replace('{{CSS}}', media('logExplorer.css').toString()).replace('{{JS}}', media('logExplorer.js').toString());
|
||||||
this.messageListener?.dispose();
|
this.messageListener?.dispose();
|
||||||
this.messageListener = webview.onDidReceiveMessage(async message => {
|
this.messageListener = webview.onDidReceiveMessage(async message => {
|
||||||
if (message.type === 'copy' && typeof message.text === 'string' && message.text.length <= 1024 * 1024) {
|
|
||||||
try { await vscode.env.clipboard.writeText(message.text); webview.postMessage({ type: 'copied' }); }
|
|
||||||
catch (error) { vscode.window.showWarningMessage(`Unable to copy: ${String(error)}`); }
|
|
||||||
}
|
|
||||||
if (message.type === 'ready') { this.publish(); }
|
if (message.type === 'ready') { this.publish(); }
|
||||||
if (message.type === 'open' && typeof message.id === 'string') {
|
if (message.type === 'open' && typeof message.id === 'string') {
|
||||||
const entry = this.entries.get(message.id);
|
const entry = this.entries.get(message.id);
|
||||||
@@ -92,11 +87,6 @@ export class LogExplorer implements vscode.Disposable {
|
|||||||
editor.revealRange(new vscode.Range(position, position));
|
editor.revealRange(new vscode.Range(position, position));
|
||||||
} catch (error) { vscode.window.showWarningMessage(`Source unavailable: ${String(error)}`); }
|
} catch (error) { vscode.window.showWarningMessage(`Source unavailable: ${String(error)}`); }
|
||||||
}
|
}
|
||||||
if (message.type === 'import') {
|
|
||||||
const files = await vscode.window.showOpenDialog({ canSelectMany: true, filters: { Logs: ['log', '1', '2', '3'] } });
|
|
||||||
files?.forEach(uri => this.imported.add(uri.fsPath));
|
|
||||||
this.poll();
|
|
||||||
}
|
|
||||||
if (message.type === 'clear') { this.clearLogs(); }
|
if (message.type === 'clear') { this.clearLogs(); }
|
||||||
}, undefined, this.context.subscriptions);
|
}, undefined, this.context.subscriptions);
|
||||||
this.activeWebview = webview;
|
this.activeWebview = webview;
|
||||||
@@ -116,12 +106,9 @@ export class LogExplorer implements vscode.Disposable {
|
|||||||
const configured = config.get<string[]>('logExplorerPaths', ['logs/appdaemon.log', 'logs/error.log', 'logs/virtualsensors.log']);
|
const configured = config.get<string[]>('logExplorerPaths', ['logs/appdaemon.log', 'logs/error.log', 'logs/virtualsensors.log']);
|
||||||
return [...new Set(configured.map(p => path.resolve(folder, p)))];
|
return [...new Set(configured.map(p => path.resolve(folder, p)))];
|
||||||
}
|
}
|
||||||
private files(): string[] {
|
|
||||||
return [...new Set([...this.configuredFiles(), ...this.imported])];
|
|
||||||
}
|
|
||||||
private clearLogs() {
|
private clearLogs() {
|
||||||
const folder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
const folder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
const cleared = new Set<string>(this.imported);
|
const cleared = new Set<string>();
|
||||||
const failures: string[] = [];
|
const failures: string[] = [];
|
||||||
if (folder) {
|
if (folder) {
|
||||||
const realRoot = fs.realpathSync(folder);
|
const realRoot = fs.realpathSync(folder);
|
||||||
@@ -138,7 +125,6 @@ export class LogExplorer implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.imported.clear();
|
|
||||||
for (const [id, entry] of this.entries) { if (entry.file && cleared.has(entry.file)) { this.entries.delete(id); } }
|
for (const [id, entry] of this.entries) { if (entry.file && cleared.has(entry.file)) { this.entries.delete(id); } }
|
||||||
failures.forEach(failure => this.warnings.add(`Unable to clear ${failure}`));
|
failures.forEach(failure => this.warnings.add(`Unable to clear ${failure}`));
|
||||||
this.publish();
|
this.publish();
|
||||||
@@ -151,7 +137,7 @@ export class LogExplorer implements vscode.Disposable {
|
|||||||
try {
|
try {
|
||||||
const encoding = vscode.workspace.getConfiguration('appdaemon').get<string>('logEncoding', 'auto');
|
const encoding = vscode.workspace.getConfiguration('appdaemon').get<string>('logEncoding', 'auto');
|
||||||
const zone = vscode.workspace.getConfiguration('appdaemon').get<string>('logTimezone', 'Europe/Berlin');
|
const zone = vscode.workspace.getConfiguration('appdaemon').get<string>('logTimezone', 'Europe/Berlin');
|
||||||
for (const file of this.files()) {
|
for (const file of this.configuredFiles()) {
|
||||||
if (!fs.existsSync(file)) { continue; }
|
if (!fs.existsSync(file)) { continue; }
|
||||||
try {
|
try {
|
||||||
const stat = fs.statSync(file);
|
const stat = fs.statSync(file);
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { ADClient } from './adClient';
|
|
||||||
|
|
||||||
export class StatusBarManager {
|
export class StatusBarManager {
|
||||||
private restartADItem: vscode.StatusBarItem;
|
private restartADItem: vscode.StatusBarItem;
|
||||||
private restartHAItem: vscode.StatusBarItem;
|
private restartHAItem: vscode.StatusBarItem;
|
||||||
private productionModeItem: vscode.StatusBarItem;
|
|
||||||
private errorItem: vscode.StatusBarItem;
|
|
||||||
private contextualItem: vscode.StatusBarItem;
|
private contextualItem: vscode.StatusBarItem;
|
||||||
private logsItem: vscode.StatusBarItem;
|
private logsItem: vscode.StatusBarItem;
|
||||||
|
|
||||||
constructor(private adClient: ADClient) {
|
constructor() {
|
||||||
// Reload AppDaemon apps
|
// Reload AppDaemon apps
|
||||||
this.restartADItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
this.restartADItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
||||||
this.restartADItem.command = 'appdaemon.restartAD';
|
this.restartADItem.command = 'appdaemon.restartAD';
|
||||||
@@ -24,19 +21,6 @@ export class StatusBarManager {
|
|||||||
this.restartHAItem.tooltip = 'Restart Home Assistant';
|
this.restartHAItem.tooltip = 'Restart Home Assistant';
|
||||||
this.restartHAItem.show();
|
this.restartHAItem.show();
|
||||||
|
|
||||||
// Production mode toggle
|
|
||||||
this.productionModeItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 98);
|
|
||||||
this.productionModeItem.command = 'appdaemon.toggleProductionMode';
|
|
||||||
this.updateProductionModeDisplay();
|
|
||||||
this.productionModeItem.show();
|
|
||||||
|
|
||||||
// Error log shortcut
|
|
||||||
this.errorItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 97);
|
|
||||||
this.errorItem.command = 'appdaemon.showErrors';
|
|
||||||
this.errorItem.text = '$(check) AD Errors';
|
|
||||||
this.errorItem.tooltip = 'Show AppDaemon Error Log';
|
|
||||||
this.errorItem.show();
|
|
||||||
|
|
||||||
// Log explorer shortcut, available even when no editor is open.
|
// Log explorer shortcut, available even when no editor is open.
|
||||||
this.logsItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 96);
|
this.logsItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 96);
|
||||||
this.logsItem.command = 'appdaemon.exploreLogs';
|
this.logsItem.command = 'appdaemon.exploreLogs';
|
||||||
@@ -47,9 +31,6 @@ export class StatusBarManager {
|
|||||||
// Contextual per-file restart button (hidden by default)
|
// Contextual per-file restart button (hidden by default)
|
||||||
this.contextualItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 101);
|
this.contextualItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 101);
|
||||||
this.contextualItem.command = 'appdaemon.restartCurrentFileApps';
|
this.contextualItem.command = 'appdaemon.restartCurrentFileApps';
|
||||||
|
|
||||||
// React to production mode changes
|
|
||||||
adClient.onProductionModeChanged(() => this.updateProductionModeDisplay());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,33 +50,6 @@ export class StatusBarManager {
|
|||||||
this.contextualItem.show();
|
this.contextualItem.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProductionModeDisplay() {
|
|
||||||
const isProduction = this.adClient.isProductionMode();
|
|
||||||
if (isProduction) {
|
|
||||||
this.productionModeItem.text = '$(eye-closed) Prod ON';
|
|
||||||
this.productionModeItem.tooltip = 'Production Mode: ON — click to disable';
|
|
||||||
this.productionModeItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
|
|
||||||
} else {
|
|
||||||
this.productionModeItem.text = '$(eye) Prod OFF';
|
|
||||||
this.productionModeItem.tooltip = 'Production Mode: OFF — click to enable';
|
|
||||||
this.productionModeItem.backgroundColor = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrorCount(count: number) {
|
|
||||||
if (count > 0) {
|
|
||||||
this.errorItem.text = `$(warning) AD Errors (${count})`;
|
|
||||||
this.errorItem.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
|
|
||||||
this.errorItem.tooltip = `${count} error(s) — click to show or clear`;
|
|
||||||
this.errorItem.command = 'appdaemon.handleErrors';
|
|
||||||
} else {
|
|
||||||
this.errorItem.text = '$(check) AD Errors';
|
|
||||||
this.errorItem.backgroundColor = undefined;
|
|
||||||
this.errorItem.tooltip = 'No AppDaemon errors — click to view log';
|
|
||||||
this.errorItem.command = 'appdaemon.showErrors';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLogErrorCount(count: number) {
|
setLogErrorCount(count: number) {
|
||||||
this.logsItem.backgroundColor = count > 0 ? new vscode.ThemeColor('statusBarItem.errorBackground') : undefined;
|
this.logsItem.backgroundColor = count > 0 ? new vscode.ThemeColor('statusBarItem.errorBackground') : undefined;
|
||||||
this.logsItem.tooltip = count > 0
|
this.logsItem.tooltip = count > 0
|
||||||
@@ -106,8 +60,6 @@ export class StatusBarManager {
|
|||||||
dispose() {
|
dispose() {
|
||||||
this.restartADItem.dispose();
|
this.restartADItem.dispose();
|
||||||
this.restartHAItem.dispose();
|
this.restartHAItem.dispose();
|
||||||
this.productionModeItem.dispose();
|
|
||||||
this.errorItem.dispose();
|
|
||||||
this.logsItem.dispose();
|
this.logsItem.dispose();
|
||||||
this.contextualItem.dispose();
|
this.contextualItem.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,20 +61,17 @@ test('groups plain app.log lines between explicit initialization markers',()=>{
|
|||||||
assert.equal(grouped[2].group_inferred,undefined);
|
assert.equal(grouped[2].group_inferred,undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clears configured live files, keeps imported archives, and updates the log error count',()=>{
|
test('clears configured log files and updates the log error count',()=>{
|
||||||
const counts=[];const e=new LogExplorer({},count=>counts.push(count));
|
const counts=[];const e=new LogExplorer({},count=>counts.push(count));
|
||||||
messages=[];const webview={postMessage:m=>messages.push(m)};e.panel={webview};e.activeWebview=webview;
|
messages=[];const webview={postMessage:m=>messages.push(m)};e.panel={webview};e.activeWebview=webview;
|
||||||
const errorFile=path.join(root,'logs/error.log');
|
const errorFile=path.join(root,'logs/error.log');
|
||||||
const archive=path.join(root,'logs/history.log.1');
|
|
||||||
fs.writeFileSync(file,'27-09-26 10:12:13 ERROR app: failed\n');
|
fs.writeFileSync(file,'27-09-26 10:12:13 ERROR app: failed\n');
|
||||||
fs.writeFileSync(errorFile,'27-09-26 10:12:13 ERROR app: failed\n');
|
fs.writeFileSync(errorFile,'27-09-26 10:12:13 ERROR app: failed\n');
|
||||||
fs.writeFileSync(archive,'27-09-26 10:12:14 INFO app: archived\n');
|
e.poll();
|
||||||
e.imported.add(archive);e.poll();
|
|
||||||
assert.equal(counts.at(-1),1);
|
assert.equal(counts.at(-1),1);
|
||||||
e.clearLogs();
|
e.clearLogs();
|
||||||
assert.equal(fs.statSync(file).size,0);
|
assert.equal(fs.statSync(file).size,0);
|
||||||
assert.equal(fs.statSync(errorFile).size,0);
|
assert.equal(fs.statSync(errorFile).size,0);
|
||||||
assert.match(fs.readFileSync(archive,'utf8'),/archived/);
|
|
||||||
assert.equal(counts.at(-1),0);
|
assert.equal(counts.at(-1),0);
|
||||||
assert.deepEqual(messages.filter(message=>message.type==='entries').at(-1).entries,[]);
|
assert.deepEqual(messages.filter(message=>message.type==='entries').at(-1).entries,[]);
|
||||||
fs.appendFileSync(file,'27-09-26 10:12:15 CRITICAL app: new failure\n');e.poll();
|
fs.appendFileSync(file,'27-09-26 10:12:15 CRITICAL app: new failure\n');e.poll();
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ function setup() {
|
|||||||
test('English interface, latest timestamps, sorting and separate initialization groups',()=>{
|
test('English interface, latest timestamps, sorting and separate initialization groups',()=>{
|
||||||
const {dom,d,errors}=setup();
|
const {dom,d,errors}=setup();
|
||||||
assert.equal(d.documentElement.lang,'en');
|
assert.equal(d.documentElement.lang,'en');
|
||||||
assert.equal(d.getElementById('copy').textContent,'Copy selected log');
|
assert.equal(d.getElementById('copy'),null);
|
||||||
|
assert.equal(d.getElementById('import'),null);
|
||||||
|
assert.equal(d.getElementById('pause'),null);
|
||||||
assert.deepEqual([...d.querySelectorAll('[data-app]')].map(e=>e.dataset.app),['beta','alpha']);
|
assert.deepEqual([...d.querySelectorAll('[data-app]')].map(e=>e.dataset.app),['beta','alpha']);
|
||||||
assert.match(d.querySelector('[data-app="alpha"] .last-age').title,/10:00:10/);
|
assert.match(d.querySelector('[data-app="alpha"] .last-age').title,/10:00:10/);
|
||||||
assert.match(d.querySelector('[data-app="alpha"] summary .last-log').textContent,/ : Température déjà disponible$/);
|
assert.match(d.querySelector('[data-app="alpha"] summary .last-log').textContent,/ : Température déjà disponible$/);
|
||||||
@@ -38,25 +40,25 @@ test('English interface, latest timestamps, sorting and separate initialization
|
|||||||
assert.equal(d.getElementById('group-init'),null);
|
assert.equal(d.getElementById('group-init'),null);
|
||||||
assert.deepEqual(errors,[]);dom.window.close();
|
assert.deepEqual(errors,[]);dom.window.close();
|
||||||
});
|
});
|
||||||
test('copies selected logs and entire initialization groups through VS Code transport',()=>{
|
test('selects individual logs and entire initialization groups',()=>{
|
||||||
const {dom,d,sent,errors}=setup();const alpha=d.querySelector('[data-app="alpha"]');alpha.open=true;alpha.dispatchEvent(new dom.window.Event('toggle'));
|
const {dom,d,errors}=setup();const alpha=d.querySelector('[data-app="alpha"]');alpha.open=true;alpha.dispatchEvent(new dom.window.Event('toggle'));
|
||||||
[...alpha.querySelectorAll('.row')].find(e=>e.textContent.includes('Température déjà disponible')).click();d.getElementById('copy').click();
|
[...alpha.querySelectorAll('.row')].find(e=>e.textContent.includes('Température déjà disponible')).click();
|
||||||
assert.equal(sent.at(-1).type,'copy');assert.match(sent.at(-1).text,/Température déjà disponible/);
|
assert.match(d.getElementById('detail').textContent,/Température déjà disponible/);
|
||||||
const group=alpha.querySelector('.event-log-group');
|
const group=alpha.querySelector('.event-log-group');
|
||||||
group.click();d.getElementById('copy').click();
|
group.click();
|
||||||
assert.match(d.getElementById('detail').textContent,/Preparing sensors\s+Ready/);
|
assert.match(d.getElementById('detail').textContent,/Preparing sensors\s+Ready/);
|
||||||
assert.match(sent.at(-1).text,/Preparing sensors/);assert.match(sent.at(-1).text,/Ready/);assert.doesNotMatch(sent.at(-1).text,/Restarting/);
|
assert.doesNotMatch(d.getElementById('detail').textContent,/Restarting/);
|
||||||
const input=d.getElementById('search'),paste=new dom.window.KeyboardEvent('keydown',{key:'v',ctrlKey:true,bubbles:true,cancelable:true});input.dispatchEvent(paste);assert.equal(paste.defaultPrevented,false);
|
const input=d.getElementById('search'),paste=new dom.window.KeyboardEvent('keydown',{key:'v',ctrlKey:true,bubbles:true,cancelable:true});input.dispatchEvent(paste);assert.equal(paste.defaultPrevented,false);
|
||||||
assert.deepEqual(errors,[]);dom.window.close();
|
assert.deepEqual(errors,[]);dom.window.close();
|
||||||
});
|
});
|
||||||
test('retains selected text during live updates and copies it with Ctrl+C',async()=>{
|
test('retains selected text during live updates and leaves Ctrl+C to the browser',async()=>{
|
||||||
const {dom,d,entries,sent,errors,send}=setup();const alpha=d.querySelector('[data-app="alpha"]');alpha.open=true;alpha.dispatchEvent(new dom.window.Event('toggle'));
|
const {dom,d,entries,errors,send}=setup();const alpha=d.querySelector('[data-app="alpha"]');alpha.open=true;alpha.dispatchEvent(new dom.window.Event('toggle'));
|
||||||
const text=alpha.querySelector('.row-message'),range=d.createRange();range.selectNodeContents(text);dom.window.getSelection().addRange(range);
|
const text=alpha.querySelector('.row-message'),range=d.createRange();range.selectNodeContents(text);dom.window.getSelection().addRange(range);
|
||||||
const original=text.textContent;
|
const original=text.textContent;
|
||||||
send([...entries,{...entries[3],event_id:'new',time:30,message:'New log'}]);
|
send([...entries,{...entries[3],event_id:'new',time:30,message:'New log'}]);
|
||||||
assert.equal(dom.window.getSelection().toString(),original);assert.match(d.getElementById('status').textContent,/buffered/);
|
assert.equal(dom.window.getSelection().toString(),original);assert.match(d.getElementById('status').textContent,/buffered/);
|
||||||
text.dispatchEvent(new dom.window.KeyboardEvent('keydown',{key:'c',ctrlKey:true,bubbles:true,cancelable:true}));
|
const copy=new dom.window.KeyboardEvent('keydown',{key:'c',ctrlKey:true,bubbles:true,cancelable:true});
|
||||||
assert.equal(sent.at(-1).text,original);
|
text.dispatchEvent(copy);assert.equal(copy.defaultPrevented,false);
|
||||||
dom.window.getSelection().removeAllRanges();d.dispatchEvent(new dom.window.Event('selectionchange'));await new Promise(resolve=>setTimeout(resolve,15));
|
dom.window.getSelection().removeAllRanges();d.dispatchEvent(new dom.window.Event('selectionchange'));await new Promise(resolve=>setTimeout(resolve,15));
|
||||||
assert.match(d.querySelector('[data-app="alpha"]').textContent,/New log/);
|
assert.match(d.querySelector('[data-app="alpha"]').textContent,/New log/);
|
||||||
assert.deepEqual(errors,[]);dom.window.close();
|
assert.deepEqual(errors,[]);dom.window.close();
|
||||||
@@ -67,7 +69,7 @@ test('log content is never interpreted as HTML',()=>{
|
|||||||
assert.equal(d.querySelectorAll('#apps img,#apps script').length,0);assert.match(app.textContent,/<img/);assert.deepEqual(errors,[]);dom.window.close();
|
assert.equal(d.querySelectorAll('#apps img,#apps script').length,0);assert.match(app.textContent,/<img/);assert.deepEqual(errors,[]);dom.window.close();
|
||||||
});
|
});
|
||||||
test('shows same-event log lines together with one causes link and keeps newer events below',()=>{
|
test('shows same-event log lines together with one causes link and keeps newer events below',()=>{
|
||||||
const {dom,d,entries,send,sent}=setup();
|
const {dom,d,entries,send}=setup();
|
||||||
const trigger={...entries[3],event_id:'motion-callback',time:entries[3].time+1000,message:'Callback trigger by sensor.kitchen_last_motion(7 => 0)',trace_id:'motion-event'};
|
const trigger={...entries[3],event_id:'motion-callback',time:entries[3].time+1000,message:'Callback trigger by sensor.kitchen_last_motion(7 => 0)',trace_id:'motion-event'};
|
||||||
const evaluation={...entries[3],event_id:'motion-evaluation',time:entries[3].time+1000,message:'[kitchen_occupancy][Evaluating] Triggered by (sensor.kitchen_last_motion[0] <= 5)[True]',trace_id:'motion-event',cause_ids:['state-event']};
|
const evaluation={...entries[3],event_id:'motion-evaluation',time:entries[3].time+1000,message:'[kitchen_occupancy][Evaluating] Triggered by (sensor.kitchen_last_motion[0] <= 5)[True]',trace_id:'motion-event',cause_ids:['state-event']};
|
||||||
const newer={...entries[3],event_id:'later-log',time:entries[3].time+2000,message:'Later event'};
|
const newer={...entries[3],event_id:'later-log',time:entries[3].time+2000,message:'Later event'};
|
||||||
@@ -79,9 +81,7 @@ test('shows same-event log lines together with one causes link and keeps newer e
|
|||||||
assert.match(combined.querySelector('.row-message').textContent,/Triggered by \(sensor.kitchen_last_motion/);
|
assert.match(combined.querySelector('.row-message').textContent,/Triggered by \(sensor.kitchen_last_motion/);
|
||||||
assert.equal(combined.querySelectorAll('.link-hint').length,1);
|
assert.equal(combined.querySelectorAll('.link-hint').length,1);
|
||||||
assert.equal(rows.children[1].querySelector('.row-message').textContent,'Later event');
|
assert.equal(rows.children[1].querySelector('.row-message').textContent,'Later event');
|
||||||
combined.click();d.getElementById('copy').click();
|
combined.click();
|
||||||
assert.match(sent.at(-1).text,/Callback trigger by sensor.kitchen_last_motion/);
|
|
||||||
assert.match(sent.at(-1).text,/Triggered by \(sensor.kitchen_last_motion/);
|
|
||||||
assert.match(d.getElementById('detail').textContent,/Callback trigger by sensor.kitchen_last_motion/);
|
assert.match(d.getElementById('detail').textContent,/Callback trigger by sensor.kitchen_last_motion/);
|
||||||
dom.window.close();
|
dom.window.close();
|
||||||
});
|
});
|
||||||
@@ -157,7 +157,7 @@ test('distinguishes log lines from supplemental records when configured',()=>{
|
|||||||
const trace={...entries[3],event_id:'trace-evaluation',kind:'evaluation',message:'Condition was evaluated'};
|
const trace={...entries[3],event_id:'trace-evaluation',kind:'evaluation',message:'Condition was evaluated'};
|
||||||
send([...entries,trace]);
|
send([...entries,trace]);
|
||||||
assert.match(d.getElementById('status').textContent,/5 log lines \+ 1 trace event/);
|
assert.match(d.getElementById('status').textContent,/5 log lines \+ 1 trace event/);
|
||||||
assert.match(d.getElementById('status').title,/configured history files/);
|
assert.match(d.getElementById('status').title,/configured log files/);
|
||||||
dom.window.close();
|
dom.window.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user