diff --git a/vscode-appdaemon/media/logExplorer.css b/vscode-appdaemon/media/logExplorer.css index fc97ca9..b767e2b 100644 --- a/vscode-appdaemon/media/logExplorer.css +++ b/vscode-appdaemon/media/logExplorer.css @@ -8,3 +8,7 @@ .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)} + +.last-log{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +summary{display:grid;grid-template-columns:auto minmax(0,1fr) repeat(3,max-content);align-items:center;column-gap:10px;row-gap:5px} +summary .last-log{grid-column:1/-1;min-width:0} diff --git a/vscode-appdaemon/media/logExplorer.js b/vscode-appdaemon/media/logExplorer.js index 0503954..aaab556 100644 --- a/vscode-appdaemon/media/logExplorer.js +++ b/vscode-appdaemon/media/logExplorer.js @@ -56,6 +56,19 @@ const years = Math.floor(days / 365); return `${years} year${years === 1 ? '' : 's'} ago`; } + function shortTimeSince(time) { + const seconds = Math.max(0, Math.floor((Date.now() - Number(time)) / 1000)); + if (seconds < 60) return `${seconds} sec ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes} min ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours} hr ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days} d ago`; + const months = Math.floor(days / 30); + 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); @@ -133,7 +146,7 @@ if (!groups.size) container.append(node('div', 'No logs match these filters. Try a wider time range or clear the search.', 'empty')); const lastLogs = new Map(); entries.filter(e => e.kind === 'log' || e.kind.startsWith('initialization_')).forEach(e => { - if (!lastLogs.has(e.app) || e.time > lastLogs.get(e.app).time) lastLogs.set(e.app, e); + 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')); @@ -146,15 +159,21 @@ if (recentApps.has(app)) detail.classList.add('new-log-app'); const summary = node('summary'), label = node('div', undefined, 'app-label'); const last = lastLogs.get(app); + let preview; label.append(node('span', app, 'name')); if (last) { - const age = node('span', `Last log: ${timeSince(last.time)}`, 'last-log last-age'); + const line = node('span', undefined, `last-log log-message level-${normalLevel(last)}`); + const age = node('span', shortTimeSince(last.time), 'last-age'); age.dataset.time = String(last.time); age.title = `${formatTime(last, true)} · Exact: ${last.timestamp}`; - label.append(age); - } else label.append(node('span', 'No log in loaded history', 'last-log')); + const message = cleanMessage(last); + line.title = message; + line.append(age, document.createTextNode(' : '), node('span', message.split(/\r?\n/, 1)[0].trim() || '—', 'last-message')); + preview = line; + } else preview = node('span', 'No log in loaded history', 'last-log'); summary.append(label); ['INFO', 'WARNING', 'ERROR'].forEach(level => summary.append(node('span', `${level} ${logs.filter(e => normalLevel(e) === level).length}`, `badge ${level}`))); + summary.append(preview); detail.append(summary); function fill() { detail.querySelector('.rows')?.remove(); @@ -332,7 +351,7 @@ $('warnings').textContent = (message.warnings || []).join(' · '); renderApps(); renderDetail(); } function refreshLastLogAges() { - document.querySelectorAll('.last-age[data-time]').forEach(label => { label.textContent = `Last log: ${timeSince(label.dataset.time)}`; }); + document.querySelectorAll('.last-age[data-time]').forEach(label => { label.textContent = shortTimeSince(label.dataset.time); }); document.querySelectorAll('.relative-time[data-time]').forEach(label => { label.textContent = timeSince(label.dataset.time); }); } setInterval(refreshLastLogAges, 1000); diff --git a/vscode-appdaemon/tests/logUi.test.js b/vscode-appdaemon/tests/logUi.test.js index 950d902..b029020 100644 --- a/vscode-appdaemon/tests/logUi.test.js +++ b/vscode-appdaemon/tests/logUi.test.js @@ -23,6 +23,10 @@ test('English interface, latest timestamps, sorting and separate initialization assert.equal(d.getElementById('copy').textContent,'Copy selected log'); 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$/); + const summary=d.querySelector('[data-app="alpha"] summary'); + assert.deepEqual([...summary.children].map(child=>child.className),['app-label','badge INFO','badge WARNING','badge ERROR','last-log']); + assert.equal(summary.querySelector('.app-label .last-log'),null); d.getElementById('sort').value='oldest';d.getElementById('sort').dispatchEvent(new dom.window.Event('input')); assert.equal(d.querySelector('[data-app]').dataset.app,'alpha'); const alpha=d.querySelector('[data-app="alpha"]');alpha.open=true;alpha.dispatchEvent(new dom.window.Event('toggle')); @@ -109,10 +113,12 @@ test('pulses an app when a new log arrives and refreshes its last-log age',async assert.ok(app.classList.contains('new-log-app')); app.open=true;app.dispatchEvent(new dom.window.Event('toggle')); assert.ok(app.querySelector('.recent-log')); - assert.equal(app.querySelector('.last-age').textContent,'Last log: 1 second ago'); + assert.equal(app.querySelector('.last-age').textContent,'1 sec ago'); + assert.equal(app.querySelector('summary .last-log').textContent,'1 sec ago : Fresh log'); dom.window.Date.now=()=>nextTime+65000; await new Promise(resolve=>setTimeout(resolve,1100)); - assert.equal(app.querySelector('.last-age').textContent,'Last log: 1 minute ago'); + assert.equal(app.querySelector('.last-age').textContent,'1 min ago'); + assert.equal(app.querySelector('summary .last-log').textContent,'1 min ago : Fresh log'); dom.window.close(); }); diff --git a/vscode-appdaemon/vscode-appdaemon-0.2.0.vsix b/vscode-appdaemon/vscode-appdaemon-0.2.0.vsix index 4002bd3..f27ab5e 100644 Binary files a/vscode-appdaemon/vscode-appdaemon-0.2.0.vsix and b/vscode-appdaemon/vscode-appdaemon-0.2.0.vsix differ