feat: add formatLastSeen and buildOfflineTileHTML functions
- Add formatLastSeen(ms) to format millisecond timestamps into relative strings (Xs ago, Xm ago, Xh ago, Xd ago, Never for null) - Add buildOfflineTileHTML(source) to render unreachable device tiles with Offline badge and last-seen timestamp - Both functions added after buildAuthTileHTML in app.js - Both exported via module.exports - 7 new tests covering export, CSS class, device name, badge, last-seen, HTML escaping, and null handling
This commit is contained in:
@@ -541,6 +541,37 @@ function buildAuthTileHTML(source) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a millisecond timestamp into a relative 'last seen' string.
|
||||
* @param {number|null} ms - Millisecond timestamp
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatLastSeen(ms) {
|
||||
if (ms == null) return 'Never';
|
||||
var diff = Math.floor((Date.now() - ms) / 1000);
|
||||
if (diff < 60) return diff + 's ago';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||
return Math.floor(diff / 86400) + 'd ago';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the HTML string for an offline (unreachable) source tile.
|
||||
* @param {{ name: string, url: string, lastSeenAt: number|null }} source
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildOfflineTileHTML(source) {
|
||||
var escapedName = escapeHtml(source.name || '');
|
||||
var lastSeen = formatLastSeen(source.lastSeenAt);
|
||||
return (
|
||||
'<article class="source-tile source-tile--offline">' +
|
||||
'<span class="source-tile__name">' + escapedName + '</span>' +
|
||||
'<span class="source-tile__badge">Offline</span>' +
|
||||
'<span class="source-tile__last-seen">Last seen ' + escapeHtml(lastSeen) + '</span>' +
|
||||
'</article>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sessions with hidden session names removed.
|
||||
* Only hides LOCAL sessions (those with empty/absent sourceUrl) matching the
|
||||
@@ -2219,6 +2250,8 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
renderFilterBar,
|
||||
// Federation tiles
|
||||
buildAuthTileHTML,
|
||||
buildOfflineTileHTML,
|
||||
formatLastSeen,
|
||||
// Test-only helpers
|
||||
_setCurrentSessions,
|
||||
_setViewMode,
|
||||
|
||||
@@ -2864,3 +2864,43 @@ test('buildAuthTileHTML escapes HTML in device name', () => {
|
||||
assert.ok(html.includes('<script>'), 'escaped script tag should appear in html');
|
||||
});
|
||||
|
||||
// --- buildOfflineTileHTML ---
|
||||
|
||||
test('buildOfflineTileHTML is exported as a function', () => {
|
||||
assert.strictEqual(typeof app.buildOfflineTileHTML, 'function');
|
||||
});
|
||||
|
||||
test('buildOfflineTileHTML returns article with source-tile--offline class', () => {
|
||||
const html = app.buildOfflineTileHTML({ name: 'Dev Server', url: 'http://dev:8088', lastSeenAt: null });
|
||||
assert.ok(html.startsWith('<article'), 'html should start with <article');
|
||||
assert.ok(html.includes('source-tile--offline'), 'html should include source-tile--offline class');
|
||||
});
|
||||
|
||||
test('buildOfflineTileHTML includes device name', () => {
|
||||
const html = app.buildOfflineTileHTML({ name: 'Dev Server', url: 'http://dev:8088', lastSeenAt: null });
|
||||
assert.ok(html.includes('Dev Server'), 'html should include the device name');
|
||||
});
|
||||
|
||||
test('buildOfflineTileHTML includes Offline badge', () => {
|
||||
const html = app.buildOfflineTileHTML({ name: 'Dev Server', url: 'http://dev:8088', lastSeenAt: null });
|
||||
assert.ok(html.includes('Offline'), 'html should include Offline text');
|
||||
assert.ok(html.includes('source-tile__badge'), 'html should include source-tile__badge class');
|
||||
});
|
||||
|
||||
test('buildOfflineTileHTML shows relative last-seen time', () => {
|
||||
const fiveMinAgo = Date.now() - 5 * 60 * 1000;
|
||||
const html = app.buildOfflineTileHTML({ name: 'Dev Server', url: 'http://dev:8088', lastSeenAt: fiveMinAgo });
|
||||
assert.ok(html.includes('Last seen'), 'html should include Last seen text');
|
||||
});
|
||||
|
||||
test('buildOfflineTileHTML escapes device name', () => {
|
||||
const html = app.buildOfflineTileHTML({ name: '<b>bad</b>', url: '', lastSeenAt: null });
|
||||
assert.ok(!html.includes('<b>bad</b>'), 'raw <b>bad</b> must not appear in html');
|
||||
assert.ok(html.includes('<b>bad</b>'), 'device name should be HTML-escaped');
|
||||
});
|
||||
|
||||
test('buildOfflineTileHTML shows "Never" when lastSeenAt is null', () => {
|
||||
const html = app.buildOfflineTileHTML({ name: 'Dev Server', url: '', lastSeenAt: null });
|
||||
assert.ok(html.includes('Never'), 'html should include Never when lastSeenAt is null');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user