clean up deprecated buttons

This commit is contained in:
Pierre committed 2026-10-04 13:43:50 +02:00
1 parent 99422d8cfc
commit 5614ad049d
12 files changed
+36 -161

No files matched your search

+6 -6
View File
@@ -17,11 +17,11 @@ By default, it reads the existing AppDaemon text logs from the workspace:
- `logs/error.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
@@ -61,8 +61,8 @@ settings file.
- Home Assistant entity completion and hover information in Python/YAML.
- Navigation to AppDaemon app and virtual-sensor definitions.
- AppDaemon errors as VS Code diagnostics and an output channel.
- Status bar actions for app reload, Home Assistant restart, production mode,
error handling and apps defined in the active YAML file.
- Status bar actions for app reload, Home Assistant restart, log exploration,
and apps defined in the active YAML file.
| Shortcut | Action |
| --- | --- |
@@ -72,7 +72,7 @@ settings file.
Commands are available under **AppDaemon:** in the Command Palette: **Go to
App**, **Restart Apps in Current File**, **Reload Apps**, **Restart Home
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
+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="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>
<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>
+7 -29
View File
@@ -1,19 +1,10 @@
/* Shared UI: VS Code transport or a standalone review fixture. No log HTML is executed. */
(() => {
const inVSCode = typeof acquireVsCodeApi === 'function';
const vscode = inVSCode ? acquireVsCodeApi() : {
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 vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : {
postMessage() {}, getState() {}, setState() {}
};
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 visibleEventGroups = 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));
} catch { return entry.timestamp; }
};
const copyText = e => `${formatTime(e, true)} ${e.level} ${e.app}: ${cleanMessage(e)}`;
function timeSince(time) {
const seconds = Math.max(0, Math.floor((Date.now() - Number(time)) / 1000));
if (seconds < 60) return `${seconds} second${seconds === 1 ? '' : 's'} ago`;
@@ -70,7 +60,6 @@
if (days < 365) return `${months} mo ago`;
return `${Math.floor(days / 365)} yr ago`;
}
function copy(text) { if (text) vscode.postMessage({ type: 'copy', text }); }
function logMessages(items, cls) {
const container = node('div', undefined, cls);
items.forEach((entry, index) => {
@@ -361,7 +350,7 @@
? `${logCount} log lines + ${traceCount} ${traceLabel} · Live updates · Up to ${message.limit || 10000} records`
: `${logCount} log lines · Live updates · Up to ${message.limit || 10000} records`;
$('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.';
const errorCount = entries.filter(e => (e.kind === 'log' || e.kind.startsWith('initialization_')) && normalLevel(e) === 'ERROR').length;
$('error-count').textContent = `${errorCount} error${errorCount === 1 ? '' : 's'}`;
@@ -376,7 +365,6 @@
setInterval(refreshLastLogAges, 1000);
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';
@@ -385,25 +373,16 @@
}
if (event.data?.type !== 'entries') return;
latest = event.data;
if (!paused && !selectionActive()) apply(latest);
else if (!paused) $('status').textContent = 'Updates buffered while text is selected';
if (!selectionActive()) apply(latest);
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('pointerup', () => { pointerSelecting = false; });
document.addEventListener('selectionchange', () => { if (!paused && !selectionActive() && latest && latest !== appliedMessage) setTimeout(() => { if (!paused && !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') : '')); };
document.addEventListener('selectionchange', () => { if (!selectionActive() && latest && latest !== appliedMessage) setTimeout(() => { if (!selectionActive() && latest !== appliedMessage) apply(latest); }, 0); });
['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) {
@@ -424,7 +403,6 @@
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(); };
-4
View File
@@ -113,10 +113,6 @@
"title": "AppDaemon: Refresh Entity List",
"icon": "$(sync)"
},
{
"command": "appdaemon.handleErrors",
"title": "AppDaemon: Handle Errors"
},
{
"command": "appdaemon.clearErrors",
"title": "AppDaemon: Clear Error Diagnostics"
-5
View File
@@ -14,8 +14,6 @@ function normalizeUrl(raw: string): string {
export class ADClient {
private productionMode = false;
private _onProductionModeChanged = new vscode.EventEmitter<boolean>();
readonly onProductionModeChanged = this._onProductionModeChanged.event;
private log: vscode.OutputChannel;
constructor(log: vscode.OutputChannel) {
@@ -120,7 +118,6 @@ export class ADClient {
async setProductionMode(mode: boolean): Promise<void> {
await this.request('POST', '/api/appdaemon/service/admin/production_mode/set', { mode });
this.productionMode = mode;
this._onProductionModeChanged.fire(mode);
}
/**
@@ -135,7 +132,6 @@ export class ADClient {
if (key.includes('production_mode')) {
const val = (state[key] as { state?: unknown })?.state;
this.productionMode = val === true || val === 'True' || val === 'true';
this._onProductionModeChanged.fire(this.productionMode);
return this.productionMode;
}
}
@@ -155,6 +151,5 @@ export class ADClient {
}
dispose() {
this._onProductionModeChanged.dispose();
}
}
-14
View File
@@ -11,10 +11,6 @@ export class ErrorViewer {
private diagnosticCollection: vscode.DiagnosticCollection;
private watcher: fs.FSWatcher | undefined;
private lastSize = 0;
private errorCount = 0;
private _onErrorCountChanged = new vscode.EventEmitter<number>();
readonly onErrorCountChanged = this._onErrorCountChanged.event;
constructor() {
this.outputChannel = vscode.window.createOutputChannel('AppDaemon Errors');
@@ -30,12 +26,6 @@ export class ErrorViewer {
clearDiagnostics() {
this.diagnosticCollection.clear();
this.errorCount = 0;
this._onErrorCountChanged.fire(0);
}
getErrorCount(): number {
return this.errorCount;
}
// ── file watching ────────────────────────────────────────────────────────
@@ -115,12 +105,9 @@ export class ErrorViewer {
this.outputChannel.appendLine(trimmed);
if (/\b(ERROR|CRITICAL|WARNING)\b/.test(trimmed)) {
this.errorCount++;
this.parseDiagnostic(trimmed);
}
}
this._onErrorCountChanged.fire(this.errorCount);
} finally {
fs.closeSync(fd);
}
@@ -183,6 +170,5 @@ export class ErrorViewer {
}
this.outputChannel.dispose();
this.diagnosticCollection.dispose();
this._onErrorCountChanged.dispose();
}
}
+1 -16
View File
@@ -347,12 +347,9 @@ export async function activate(context: vscode.ExtensionContext) {
errorViewer = new ErrorViewer();
const logExplorer = new LogExplorer(context, count => statusBar.setLogErrorCount(count));
context.subscriptions.push(logExplorer, vscode.commands.registerCommand('appdaemon.exploreLogs', () => logExplorer.show()));
statusBar = new StatusBarManager(adClient);
statusBar = new StatusBarManager();
logExplorer.startPolling();
// Wire error count → status bar badge
errorViewer.onErrorCountChanged((count) => statusBar.setErrorCount(count));
// ── Contextual per-file restart button ───────────────────────────────────
updateContextualButton(vscode.window.activeTextEditor);
context.subscriptions.push(
@@ -544,18 +541,6 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('appdaemon.showErrors', () => {
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 () => {
const entities = await haClient.fetchEntities();
vscode.window.showInformationMessage(`AppDaemon: ${entities.length} entities loaded`);
+2 -16
View File
@@ -14,7 +14,6 @@ export class LogExplorer implements vscode.Disposable {
private timer?: ReturnType<typeof setInterval>;
private entries = new Map<string, LogEntry>();
private cursors = new Map<string, Cursor>();
private imported = new Set<string>();
private warnings = new Set<string>();
private busy = false;
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());
this.messageListener?.dispose();
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 === 'open' && typeof message.id === 'string') {
const entry = this.entries.get(message.id);
@@ -92,11 +87,6 @@ export class LogExplorer implements vscode.Disposable {
editor.revealRange(new vscode.Range(position, position));
} 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(); }
}, undefined, this.context.subscriptions);
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']);
return [...new Set(configured.map(p => path.resolve(folder, p)))];
}
private files(): string[] {
return [...new Set([...this.configuredFiles(), ...this.imported])];
}
private clearLogs() {
const folder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const cleared = new Set<string>(this.imported);
const cleared = new Set<string>();
const failures: string[] = [];
if (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); } }
failures.forEach(failure => this.warnings.add(`Unable to clear ${failure}`));
this.publish();
@@ -151,7 +137,7 @@ export class LogExplorer implements vscode.Disposable {
try {
const encoding = vscode.workspace.getConfiguration('appdaemon').get<string>('logEncoding', 'auto');
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; }
try {
const stat = fs.statSync(file);
+1 -49
View File
@@ -1,15 +1,12 @@
import * as vscode from 'vscode';
import { ADClient } from './adClient';
export class StatusBarManager {
private restartADItem: vscode.StatusBarItem;
private restartHAItem: vscode.StatusBarItem;
private productionModeItem: vscode.StatusBarItem;
private errorItem: vscode.StatusBarItem;
private contextualItem: vscode.StatusBarItem;
private logsItem: vscode.StatusBarItem;
constructor(private adClient: ADClient) {
constructor() {
// Reload AppDaemon apps
this.restartADItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
this.restartADItem.command = 'appdaemon.restartAD';
@@ -24,19 +21,6 @@ export class StatusBarManager {
this.restartHAItem.tooltip = 'Restart Home Assistant';
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.
this.logsItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 96);
this.logsItem.command = 'appdaemon.exploreLogs';
@@ -47,9 +31,6 @@ export class StatusBarManager {
// Contextual per-file restart button (hidden by default)
this.contextualItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 101);
this.contextualItem.command = 'appdaemon.restartCurrentFileApps';
// React to production mode changes
adClient.onProductionModeChanged(() => this.updateProductionModeDisplay());
}
/**
@@ -69,33 +50,6 @@ export class StatusBarManager {
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) {
this.logsItem.backgroundColor = count > 0 ? new vscode.ThemeColor('statusBarItem.errorBackground') : undefined;
this.logsItem.tooltip = count > 0
@@ -106,8 +60,6 @@ export class StatusBarManager {
dispose() {
this.restartADItem.dispose();
this.restartHAItem.dispose();
this.productionModeItem.dispose();
this.errorItem.dispose();
this.logsItem.dispose();
this.contextualItem.dispose();
}
+2 -5
View File
@@ -61,20 +61,17 @@ test('groups plain app.log lines between explicit initialization markers',()=>{
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));
messages=[];const webview={postMessage:m=>messages.push(m)};e.panel={webview};e.activeWebview=webview;
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(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.imported.add(archive);e.poll();
e.poll();
assert.equal(counts.at(-1),1);
e.clearLogs();
assert.equal(fs.statSync(file).size,0);
assert.equal(fs.statSync(errorFile).size,0);
assert.match(fs.readFileSync(archive,'utf8'),/archived/);
assert.equal(counts.at(-1),0);
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();
+16 -16
View File
@@ -20,7 +20,9 @@ function setup() {
test('English interface, latest timestamps, sorting and separate initialization groups',()=>{
const {dom,d,errors}=setup();
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.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$/);
@@ -38,25 +40,25 @@ test('English interface, latest timestamps, sorting and separate initialization
assert.equal(d.getElementById('group-init'),null);
assert.deepEqual(errors,[]);dom.window.close();
});
test('copies selected logs and entire initialization groups through VS Code transport',()=>{
const {dom,d,sent,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();
assert.equal(sent.at(-1).type,'copy');assert.match(sent.at(-1).text,/Température déjà disponible/);
test('selects individual logs and entire initialization groups',()=>{
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();
assert.match(d.getElementById('detail').textContent,/Température déjà disponible/);
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(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);
assert.deepEqual(errors,[]);dom.window.close();
});
test('retains selected text during live updates and copies it with Ctrl+C',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'));
test('retains selected text during live updates and leaves Ctrl+C to the browser',async()=>{
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 original=text.textContent;
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/);
text.dispatchEvent(new dom.window.KeyboardEvent('keydown',{key:'c',ctrlKey:true,bubbles:true,cancelable:true}));
assert.equal(sent.at(-1).text,original);
const copy=new dom.window.KeyboardEvent('keydown',{key:'c',ctrlKey:true,bubbles:true,cancelable:true});
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));
assert.match(d.querySelector('[data-app="alpha"]').textContent,/New log/);
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();
});
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 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'};
@@ -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.equal(combined.querySelectorAll('.link-hint').length,1);
assert.equal(rows.children[1].querySelector('.row-message').textContent,'Later event');
combined.click();d.getElementById('copy').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/);
combined.click();
assert.match(d.getElementById('detail').textContent,/Callback trigger by sensor.kitchen_last_motion/);
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'};
send([...entries,trace]);
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();
});
Binary file not shown.