diff --git a/lib/hq/remote_ui/assets/app.css b/lib/hq/remote_ui/assets/app.css index 4891684..2a1a00c 100644 --- a/lib/hq/remote_ui/assets/app.css +++ b/lib/hq/remote_ui/assets/app.css @@ -755,6 +755,10 @@ textarea { .server-token-form { display: grid; gap: 10px; + margin-top: 10px; +} + +.server-list-item .server-token-form { margin: 0 12px 12px 46px; } @@ -776,6 +780,15 @@ textarea { gap: 0; } +.server-list-item > .detail-row { + align-items: start; +} + +.server-list-item .chip { + width: max-content; + margin-top: 4px; +} + .server-section-label > div { display: grid; gap: 2px; @@ -830,6 +843,34 @@ textarea { .server-connection-grid { grid-template-columns: 1fr; } + + .remote-server-list-item > .detail-row { + grid-template-columns: auto minmax(0, 1fr); + } + + .remote-server-list-item > .detail-row > .server-row-actions { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + width: 100%; + } + + .remote-server-list-item .server-row-actions .inline-icon-button { + justify-content: center; + min-width: 0; + padding-inline: 8px; + } + + .remote-server-list-item .server-row-actions .inline-icon-button span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .remote-server-list-item .server-token-form { + margin-left: 0; + margin-right: 0; + } } #prompt-input { diff --git a/lib/hq/remote_ui/assets/app.js b/lib/hq/remote_ui/assets/app.js index c117640..c50ef14 100644 --- a/lib/hq/remote_ui/assets/app.js +++ b/lib/hq/remote_ui/assets/app.js @@ -1809,10 +1809,11 @@ function renderNow() { const running = state.agents.filter((agent) => agent.running); const unread = state.agents.filter((agent) => agent.unread && !agent.awaiting_input && !agent.blocked && !agent.running); const scheduleSection = renderScheduleNowSection(); + const tokenRecovery = renderActiveServerTokenRecovery(); setHeader("Needs attention", connectionText(), "HQ"); setMainHeaderMore("now"); - if (waiting.length === 0 && blocked.length === 0 && running.length === 0 && unread.length === 0 && !scheduleSection) { + if (waiting.length === 0 && blocked.length === 0 && running.length === 0 && unread.length === 0 && !scheduleSection && !tokenRecovery) { replaceView(`

Nothing to do, hooray!

@@ -1843,6 +1844,7 @@ function renderNow() { replaceView(` ${summary} + ${tokenRecovery} ${scheduleSection} ${nowSection} ${unreadSection} @@ -1850,6 +1852,23 @@ function renderNow() { `); } +function renderActiveServerTokenRecovery() { + const server = activeServer(); + if (!server || server.local || remoteServerToken(server.key)) return ""; + if (!String(connectionText()).includes("rejected broker credentials")) return ""; + + return ` +
+
+ Remote token required + ${escapeHtml(server.name || server.key)} +
+

Enter this server's token to save it for this browser.

+ ${renderServerTokenForm(server)} +
+ `; +} + function renderScheduleNowSection() { const daemon = state.scheduleDaemon || {}; const schedules = state.schedules || []; @@ -2509,7 +2528,7 @@ function renderServerRow(server) { ? `${escapeHtml(tokenLabel)}` : ""; return ` -
+
diff --git a/test/remote_server_test.rb b/test/remote_server_test.rb index 055a3f1..95af5e0 100644 --- a/test/remote_server_test.rb +++ b/test/remote_server_test.rb @@ -2501,8 +2501,14 @@ def assert_remote_ui_routes_load_without_auth js[:body].include?("saveRemoteServerToken") && js[:body].include?("brokerGetWithHeaders") && js[:body].include?("Remote token saved for this browser") && - js[:body].include?("Token needed here"), + js[:body].include?("Token needed here") && + js[:body].include?("function renderActiveServerTokenRecovery") && + js[:body].include?("Remote token required"), "expected Settings to let existing remote servers save a browser-local token") + assert(css[:body].include?(".remote-server-list-item > .detail-row > .server-row-actions") && + css[:body].include?("grid-template-columns: repeat(3, minmax(0, 1fr))") && + css[:body].include?(".remote-server-list-item .server-row-actions .inline-icon-button span"), + "expected remote server row actions to fit small screens") assert(js[:body].include?("Restart the local Remote server to enable ad hoc peer switching"), "expected stale broker errors to explain that the local Remote server must be restarted") assert(!js[:body].include?("Connect local peer"),