feat: update frontend to use device_id-based session keys and API URLs
- buildTileHTML: prefer session.deviceId over legacy integer remoteId for data-remote-id - buildSidebarHTML: prefer session.deviceId over legacy integer remoteId for data-remote-id - getVisibleSessions: check s.sessionKey (device_id:name) in hidden_sessions list for backward compatibility with both old (plain name) and new (device_id:name) formats - openSession: rename _remoteId → _deviceId to reflect value is now a device_id string - createNewSession: rename internal remoteId variable → deviceId for clarity All federation API URLs continue to work via opts.remoteId (now contains deviceId value). Backward compatibility maintained: fallback to remoteId when deviceId not present (old server). Tests: updated test_open_session_fires_bell_clear_for_remote to check _deviceId guard; added 5 new tests covering all structural changes.
This commit is contained in:
+20
-15
@@ -444,7 +444,9 @@ function buildTileHTML(session, index, mobile) {
|
||||
}
|
||||
const lastLines = allLines.slice(_lineCount).join('\n');
|
||||
|
||||
const remoteIdAttr = session.remoteId != null ? ` data-remote-id="${escapeHtml(session.remoteId)}"` : '';
|
||||
// Prefer deviceId (device_id string from backend) over legacy integer remoteId
|
||||
var _effRemoteId = session.deviceId != null ? session.deviceId : session.remoteId;
|
||||
const remoteIdAttr = _effRemoteId != null ? ` data-remote-id="${escapeHtml(_effRemoteId)}"` : '';
|
||||
return (
|
||||
`<article class="${classes}" data-session="${escapedName}" data-session-key="${escapeHtml(session.sessionKey || name)}"${remoteIdAttr} tabindex="0" role="listitem" aria-label="${escapedName}">` +
|
||||
`<div class="tile-header">` +
|
||||
@@ -498,8 +500,10 @@ function buildSidebarHTML(session, currentSession) {
|
||||
}
|
||||
const lastLines = allLines.slice(-20).join('\n');
|
||||
|
||||
// Prefer deviceId (device_id string from backend) over legacy integer remoteId
|
||||
var _sidebarEffRemoteId = session.deviceId != null ? session.deviceId : (session.remoteId != null ? session.remoteId : '');
|
||||
return (
|
||||
`<article class="${classes}" data-session="${escapedName}" data-remote-id="${escapeHtml(session.remoteId != null ? session.remoteId : '')}" tabindex="0" role="listitem">` +
|
||||
`<article class="${classes}" data-session="${escapedName}" data-remote-id="${escapeHtml(_sidebarEffRemoteId)}" tabindex="0" role="listitem">` +
|
||||
`<div class="sidebar-item-header">` +
|
||||
`<span class="sidebar-item-name">${escapedName}</span>` +
|
||||
badgeHtml +
|
||||
@@ -539,7 +543,7 @@ function getVisibleSessions(sessions) {
|
||||
return (sessions || []).filter(function(s) {
|
||||
// Skip status entries (unreachable, auth_failed) — rendered separately as status tiles
|
||||
if (s.status) return false;
|
||||
if (hidden.length > 0 && hidden.includes(s.name)) {
|
||||
if (hidden.length > 0 && (hidden.includes(s.sessionKey || s.name) || hidden.includes(s.name))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1227,11 +1231,12 @@ async function openSession(name, opts = {}) {
|
||||
if (fab) fab.classList.add('hidden');
|
||||
|
||||
// Always spawn ttyd for this session — ensures correct session after service restart or page restore
|
||||
var _remoteId = opts.remoteId != null ? opts.remoteId : '';
|
||||
// _deviceId holds the device_id string (was integer remoteId index in old protocol)
|
||||
var _deviceId = opts.remoteId != null ? opts.remoteId : '';
|
||||
try {
|
||||
if (_remoteId !== '') {
|
||||
if (_deviceId !== '') {
|
||||
// Remote session: route connect POST through same-origin federation proxy
|
||||
await api('POST', '/api/federation/' + encodeURIComponent(_remoteId) + '/connect/' + encodeURIComponent(name));
|
||||
await api('POST', '/api/federation/' + encodeURIComponent(_deviceId) + '/connect/' + encodeURIComponent(name));
|
||||
} else {
|
||||
await api('POST', '/api/sessions/' + encodeURIComponent(name) + '/connect');
|
||||
}
|
||||
@@ -1241,18 +1246,18 @@ async function openSession(name, opts = {}) {
|
||||
}
|
||||
|
||||
// Persist active_remote_id so restoreState() can reopen remote sessions after page refresh
|
||||
api('PATCH', '/api/state', { active_session: name, active_remote_id: _remoteId || null }).catch(function() {});
|
||||
api('PATCH', '/api/state', { active_session: name, active_remote_id: _deviceId || null }).catch(function() {});
|
||||
|
||||
// Fire-and-forget bell-clear for remote sessions — acknowledge bells on the remote server
|
||||
if (_remoteId !== '') {
|
||||
api('POST', '/api/federation/' + encodeURIComponent(_remoteId) + '/sessions/' + encodeURIComponent(name) + '/bell/clear').catch(function() {});
|
||||
if (_deviceId !== '') {
|
||||
api('POST', '/api/federation/' + encodeURIComponent(_deviceId) + '/sessions/' + encodeURIComponent(name) + '/bell/clear').catch(function() {});
|
||||
}
|
||||
|
||||
// Wait for animation to finish (may already be done if /connect was slow)
|
||||
await animDone;
|
||||
|
||||
// Mount terminal NOW — /connect has completed, new ttyd is serving the correct session
|
||||
if (window._openTerminal) window._openTerminal(name, _remoteId, getDisplaySettings().fontSize);
|
||||
if (window._openTerminal) window._openTerminal(name, _deviceId, getDisplaySettings().fontSize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2087,9 +2092,9 @@ function showFabSessionInput() {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function createNewSession(name, remoteId) {
|
||||
remoteId = remoteId || '';
|
||||
var deviceId = remoteId || ''; // Accept device_id string (was integer index in old protocol)
|
||||
try {
|
||||
var endpoint = remoteId ? '/api/federation/' + encodeURIComponent(remoteId) + '/sessions' : '/api/sessions';
|
||||
var endpoint = deviceId ? '/api/federation/' + encodeURIComponent(deviceId) + '/sessions' : '/api/sessions';
|
||||
const res = await api('POST', endpoint, { name });
|
||||
const data = await res.json();
|
||||
const sessionName = data.name || name;
|
||||
@@ -2122,8 +2127,8 @@ async function createNewSession(name, remoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute expectedKey: for remote sessions, use 'remoteId:sessionName' (sessionKey format)
|
||||
var expectedKey = remoteId ? (remoteId + ':' + sessionName) : sessionName;
|
||||
// Compute expectedKey: for remote sessions, use 'deviceId:sessionName' (sessionKey format)
|
||||
var expectedKey = deviceId ? (deviceId + ':' + sessionName) : sessionName;
|
||||
|
||||
// Poll until the session appears in _currentSessions (max 30s, every 2s)
|
||||
var attempts = 0;
|
||||
@@ -2138,7 +2143,7 @@ async function createNewSession(name, remoteId) {
|
||||
clearInterval(pollForSession);
|
||||
removeLoadingTile();
|
||||
showToast('Session \'' + sessionName + '\' ready');
|
||||
openSession(sessionName, { remoteId: remoteId });
|
||||
openSession(sessionName, { remoteId: deviceId });
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(pollForSession);
|
||||
removeLoadingTile();
|
||||
|
||||
@@ -2580,9 +2580,9 @@ def test_open_session_fires_bell_clear_for_remote() -> None:
|
||||
assert "/bell/clear" in body, (
|
||||
"openSession must POST to /api/federation/{remoteId}/sessions/{name}/bell/clear"
|
||||
)
|
||||
# Must guard bell-clear with a _remoteId !== '' check
|
||||
assert "_remoteId !== ''" in body, (
|
||||
"openSession must guard bell-clear POST with _remoteId !== '' check"
|
||||
# Must guard bell-clear with a _deviceId !== '' check (renamed from _remoteId in task-11)
|
||||
assert "_deviceId !== ''" in body, (
|
||||
"openSession must guard bell-clear POST with _deviceId !== '' check"
|
||||
)
|
||||
|
||||
|
||||
@@ -2770,3 +2770,105 @@ def test_update_favicon_badge_filters_through_visible_sessions() -> None:
|
||||
"updateFaviconBadge must filter sessions through getVisibleSessions() "
|
||||
"to exclude hidden sessions from the bell activity check"
|
||||
)
|
||||
|
||||
|
||||
# ─── Task 11: Frontend — Change Session Key Format to `device_id:name` ────────
|
||||
|
||||
|
||||
def test_build_tile_html_prefers_device_id_for_data_remote_id() -> None:
|
||||
"""buildTileHTML must use session.deviceId (falling back to session.remoteId) for data-remote-id.
|
||||
|
||||
The backend now sends deviceId (device_id string) alongside remoteId on every session.
|
||||
The tile's data-remote-id must prefer deviceId so the click handler passes the correct
|
||||
device_id to openSession(), enabling federation API calls with the new device_id format.
|
||||
"""
|
||||
match = re.search(
|
||||
r"function buildTileHTML\s*\(.*?\)\s*\{(.*?)(?=\nfunction |\n// |\n/\*\*)",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "buildTileHTML function not found"
|
||||
body = match.group(1)
|
||||
assert "session.deviceId" in body, (
|
||||
"buildTileHTML must use session.deviceId for data-remote-id attribute "
|
||||
"(with fallback to session.remoteId for backward compatibility with old servers)"
|
||||
)
|
||||
|
||||
|
||||
def test_build_sidebar_html_prefers_device_id_for_data_remote_id() -> None:
|
||||
"""buildSidebarHTML must use session.deviceId (falling back to session.remoteId) for data-remote-id.
|
||||
|
||||
The sidebar click handler reads data-remote-id and passes it to openSession().
|
||||
Must prefer deviceId so federation API calls use the new device_id:name format.
|
||||
"""
|
||||
match = re.search(
|
||||
r"function buildSidebarHTML\s*\(.*?\)\s*\{(.*?)(?=\nfunction |\n// |\n/\*\*)",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "buildSidebarHTML function not found"
|
||||
body = match.group(1)
|
||||
assert "session.deviceId" in body, (
|
||||
"buildSidebarHTML must use session.deviceId for data-remote-id attribute "
|
||||
"(with fallback to session.remoteId for backward compatibility with old servers)"
|
||||
)
|
||||
|
||||
|
||||
def test_get_visible_sessions_checks_session_key_in_hidden() -> None:
|
||||
"""getVisibleSessions must check sessionKey (as well as name) against the hidden_sessions list.
|
||||
|
||||
hidden_sessions may contain either:
|
||||
- old format: plain session name (e.g. 'dev')
|
||||
- new format: device_id:name key (e.g. 'abc-123:dev')
|
||||
|
||||
Both formats must be matched for backward compatibility.
|
||||
"""
|
||||
match = re.search(
|
||||
r"function getVisibleSessions\s*\(\w+\)\s*\{(.*?)(?=\n(?:function|//|window\.))",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "getVisibleSessions function not found"
|
||||
body = match.group(1)
|
||||
assert "sessionKey" in body, (
|
||||
"getVisibleSessions must check s.sessionKey (in addition to s.name) against hidden_sessions "
|
||||
"for backward compatibility: hidden_sessions may contain plain names OR device_id:name keys"
|
||||
)
|
||||
|
||||
|
||||
def test_open_session_uses_device_id_variable_for_federation() -> None:
|
||||
"""openSession must use _deviceId variable (from opts.remoteId) for federation API calls.
|
||||
|
||||
The local variable _remoteId is renamed to _deviceId to reflect that the value
|
||||
is now a device_id string, not an integer remote instance index.
|
||||
"""
|
||||
match = re.search(
|
||||
r"async function openSession\s*\(.*?\)\s*\{(.*?)^\}",
|
||||
_JS,
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
assert match, "openSession function not found"
|
||||
body = match.group(1)
|
||||
assert "_deviceId" in body, (
|
||||
"openSession must use _deviceId variable (assigned from opts.remoteId) "
|
||||
"for federation API URL construction — reflects that the value is now a device_id string"
|
||||
)
|
||||
|
||||
|
||||
def test_create_new_session_uses_device_id_variable_internally() -> None:
|
||||
"""createNewSession must use a deviceId variable internally for federation endpoint construction.
|
||||
|
||||
The internal variable is renamed from 'remoteId' to 'deviceId' to reflect that
|
||||
the value is now a device_id string (e.g. 'abc-123'), not an integer index.
|
||||
"""
|
||||
match = re.search(
|
||||
r"async function createNewSession\s*\([\w,\s]+\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "createNewSession function not found"
|
||||
body = match.group(1)
|
||||
assert "var deviceId" in body, (
|
||||
"createNewSession must declare 'var deviceId' internally (assigned from the remoteId parameter) "
|
||||
"to reflect that the value is now a device_id string used in federation API URLs"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user