Add Broker client and writer quick dialogs

This commit is contained in:
2026-09-13 14:40:51 +02:00
parent 9f6ebf2053
commit 7ccc8799e9
9 changed files with 274 additions and 32 deletions
+60 -26
View File
@@ -153,9 +153,9 @@ static const char s_index_html[] =
"<div class=\"status-item\"><span class=\"label\">Broker client</span>"
"<span id=\"client-id\" class=\"value\">—</span></div>\n"
"<div class=\"status-item\"><span class=\"label\">Active writer</span>"
"<span id=\"writer-id\" class=\"value\">None</span></div>\n"
"<button type=\"button\" id=\"quick-writer\" class=\"quick-trigger\" aria-label=\"Active writer management\" aria-controls=\"serial-settings\" aria-expanded=\"false\" aria-haspopup=\"dialog\" disabled><span id=\"writer-id\" class=\"value\">None</span></button></div>\n"
"<div class=\"status-item\"><span class=\"label\">Broker clients</span>"
"<span id=\"broker-clients\" class=\"value\">—</span></div>\n"
"<button type=\"button\" id=\"quick-broker\" class=\"quick-trigger\" aria-label=\"Live broker clients\" aria-controls=\"serial-settings\" aria-expanded=\"false\" aria-haspopup=\"dialog\" disabled><span id=\"broker-clients\" class=\"value\">—</span></button></div>\n"
"<div class=\"status-item wide\"><span class=\"label\">Wi-Fi</span>"
"<button type=\"button\" id=\"quick-network\" class=\"quick-trigger\" aria-label=\"Wi-Fi quick settings\" aria-controls=\"serial-settings\" aria-expanded=\"false\" aria-haspopup=\"dialog\" disabled><span id=\"wifi-summary\" class=\"value\">Loading…</span></button></div>\n"
"<div class=\"status-item wide\"><span class=\"label\">Serial</span>"
@@ -186,12 +186,12 @@ static const char s_index_html[] =
"<div id=\"terminal\" class=\"terminal-host\"></div>\n"
"<div id=\"admin-terminal\" class=\"terminal-host\" hidden></div>\n"
"<section id=\"serial-settings\" class=\"settings-page\" aria-label=\"Serial settings\" hidden>"
"<div id=\"quick-header\" hidden><h2 id=\"quick-title\">Quick settings</h2><button id=\"quick-close\" class=\"button\" type=\"button\">Close quick settings</button> <a id=\"quick-full\" href=\"#serial-settings\">Open full settings page</a><p class=\"connection-detail\">Opening and selecting never applies changes. Apply changes RAM; Save persists working device values, not drafts. Closing discards drafts; admitted work is not cancelled. Password changes are available only on the full Network page.</p></div>"
"<div id=\"quick-header\" hidden><h2 id=\"quick-title\">Quick settings</h2><button id=\"quick-close\" class=\"button\" type=\"button\">Close quick settings</button> <a id=\"quick-full\" href=\"#serial-settings\">Open full settings page</a><p id=\"quick-help\" class=\"connection-detail\">Opening and selecting never applies changes. Apply changes RAM; Save persists working device values, not drafts. Closing discards drafts; admitted work is not cancelled. Password changes are available only on the full Network page.</p></div>"
"<div id=\"settings-navigation\" class=\"serial-actions\"><button id=\"settings-serial\" class=\"button\" type=\"button\" aria-pressed=\"true\">Serial settings</button>"
"<button id=\"settings-accounts\" class=\"button\" type=\"button\" aria-pressed=\"false\">Accounts</button>"
"<button id=\"settings-network\" class=\"button\" type=\"button\" aria-pressed=\"false\">Network</button>"
"<button id=\"settings-display\" class=\"button\" type=\"button\" aria-pressed=\"false\">Display</button><button id=\"settings-broker\" class=\"button\" type=\"button\" aria-pressed=\"false\">Broker</button></div>"
"<div id=\"broker-settings\" hidden><h2>Broker clients and writer</h2><p class=\"connection-detail\">One writer, multiple isolated observers. Viewing, refreshing and selecting do not change the lease or either terminal. Assignment revokes the previous writer, without recalling bytes already accepted by UART. Any intervening lease transition rejects stale confirmation, even release and reacquire by the same writer.</p><p class=\"connection-detail\">Pending and high-water are bounded output bytes; dropped counts cover this connection or the last shell counter clear. No UART data is consumed. Refresh clears selection. No persistence or disconnect controls.</p><button id=\"broker-refresh\" class=\"button\" type=\"button\">Refresh</button><p id=\"broker-detail\" class=\"connection-detail\" role=\"status\"></p><dl id=\"broker-values\" class=\"settings-values\"></dl><div class=\"settings-edit\"><label>Assign writer to<select id=\"broker-target\"><option value=\"\">Select a connected client</option><option id=\"broker-option-0\" hidden disabled></option><option id=\"broker-option-1\" hidden disabled></option><option id=\"broker-option-2\" hidden disabled></option><option id=\"broker-option-3\" hidden disabled></option><option id=\"broker-option-4\" hidden disabled></option><option id=\"broker-option-5\" hidden disabled></option><option id=\"broker-option-6\" hidden disabled></option><option id=\"broker-option-7\" hidden disabled></option></select></label></div><div class=\"serial-actions\"><button id=\"broker-assign\" class=\"button\" type=\"button\">Assign writer…</button><button id=\"broker-result\" class=\"button\" type=\"button\">Check Operation Result</button></div><p id=\"broker-operation-detail\" class=\"connection-detail\" role=\"status\">Explicit confirmation required. Navigation or timeout does not cancel admitted work. Check Result after uncertainty; no automatic mutation retry.</p></div>\n"
"<div id=\"broker-settings\" hidden><h2>Broker clients and writer</h2><p class=\"connection-detail\">One writer, multiple isolated observers. Viewing, refreshing and selecting do not change the lease or either terminal. Assignment revokes the previous writer, without recalling bytes already accepted by UART. Any intervening lease transition rejects stale confirmation, even release and reacquire by the same writer.</p><p class=\"connection-detail\">Pending and high-water are bounded output bytes; dropped counts cover this connection or the last shell counter clear. No UART data is consumed. Refresh retains explicit selection without renewing its lease token. Stale selections require choosing the blank option then the target again. No persistence or disconnect controls.</p><button id=\"broker-refresh\" class=\"button\" type=\"button\">Refresh</button><p id=\"broker-detail\" class=\"connection-detail\" role=\"status\"></p><dl id=\"broker-values\" class=\"settings-values\"></dl><div class=\"settings-edit\"><label>Assign writer to<select id=\"broker-target\"><option value=\"\">Select a connected client</option><option id=\"broker-option-0\" hidden disabled></option><option id=\"broker-option-1\" hidden disabled></option><option id=\"broker-option-2\" hidden disabled></option><option id=\"broker-option-3\" hidden disabled></option><option id=\"broker-option-4\" hidden disabled></option><option id=\"broker-option-5\" hidden disabled></option><option id=\"broker-option-6\" hidden disabled></option><option id=\"broker-option-7\" hidden disabled></option></select></label></div><div class=\"serial-actions\"><button id=\"broker-assign\" class=\"button\" type=\"button\">Assign writer…</button><button id=\"broker-result\" class=\"button\" type=\"button\">Check Operation Result</button></div><p id=\"broker-operation-detail\" class=\"connection-detail\" role=\"status\">Explicit confirmation required. Navigation or timeout does not cancel admitted work. Check Result after uncertainty; no automatic mutation retry.</p></div>\n"
"<div id=\"display-settings\" hidden><h2>Display</h2>\n"
"<p class=\"connection-detail\">Working OLED inactivity settings, not saved NVS values. Zero disables a transition. Each timeout is 086400 seconds; when both are enabled, Off must be later than Dim.</p>\n"
"<p class=\"connection-detail\">Apply and Defaults change RAM only. Save persists the working snapshot, not browser drafts. Load discards drafts and uses stored settings, or defaults if storage is absent/incompatible; it does not change NVS. Reset saves defaults and applies them. Refresh discards drafts. Intervening configuration edits reject stale operations: Refresh and review before retrying.</p>\n"
@@ -398,11 +398,14 @@ static const char s_app_js[] =
" element('serial-result').disabled = busy;\n"
"}\n"
"let quick = null, quickLeaveTimer = 0, quickSuppressFocus = false, quickDismissed = null;\n"
"const quickDomains = ['serial','network','broker','writer'];\n"
"const quickTitle = domain => ({serial:'Serial quick settings',network:'Wi-Fi quick settings',broker:'Live broker clients',writer:'Active writer management'})[domain];\n"
"function resetQuick() {\n"
" window.clearTimeout(quickLeaveTimer); quickLeaveTimer = 0; quick = null;\n"
" settingsHost.setAttribute('data-quick', 'false'); settingsHost.setAttribute('role', 'region');\n"
" element('quick-header').hidden = true; settingsHost.setAttribute('aria-label', 'Settings');\n"
" for (const domain of ['serial','network']) element('quick-' + domain).setAttribute('aria-expanded', 'false');\n"
" stopBrokerLive();\n"
" for (const domain of quickDomains) element('quick-' + domain).setAttribute('aria-expanded', 'false');\n"
"}\n"
"function closeQuick(focus = false) {\n"
" if (!quick) return;\n"
@@ -416,13 +419,15 @@ static const char s_app_js[] =
" if (activate) quickDismissed = null;\n"
" if (quick?.domain === domain) { if (activate) element('quick-close').focus(); return; }\n"
" if (quick) closeQuick();\n"
" // Full-page drafts retain their sole DOM owner; only explicit activation navigates there.\n"
" if (selected === 'settings') { if (activate) { selectSettingsDomain(domain); element('settings-' + domain).focus(); } return; }\n"
" // Broker contexts never navigate away from full-page drafts; Serial/Wi-Fi retain their explicit navigation.\n"
" const brokerContext = domain === 'broker' || domain === 'writer';\n"
" if (selected === 'settings') { if (activate) { if (!brokerContext) selectSettingsDomain(domain); element('settings-' + (brokerContext ? settingsDomain : domain)).focus(); } return; }\n"
" const previous = selected, trigger = element('quick-' + domain);\n"
" selectTerminal('settings', false); selectSettingsDomain(domain, false); clearNetworkSecret();\n"
" selectTerminal('settings', false); selectSettingsDomain(brokerContext ? 'broker' : domain, false); clearNetworkSecret();\n"
" quick = {domain, previous, trigger}; settingsHost.setAttribute('data-quick', 'true');\n"
" settingsHost.setAttribute('role', 'dialog'); settingsHost.setAttribute('aria-label', domain === 'serial' ? 'Serial quick settings' : 'Wi-Fi quick settings');\n"
" element('quick-title').textContent = domain === 'serial' ? 'Serial quick settings' : 'Wi-Fi quick settings';\n"
" settingsHost.setAttribute('role', 'dialog'); settingsHost.setAttribute('aria-label', quickTitle(domain));\n"
" element('quick-title').textContent = quickTitle(domain);\n"
" element('quick-help').textContent = brokerContext ? 'Live clients share one writer and isolated observation. Opening, refreshing and selecting never transfers the lease. Assign writer requires separate confirmation. Closing does not cancel admitted work; after uncertainty use Check Result, never automatic retry.' : 'Opening and selecting never applies changes. Apply changes RAM; Save persists working device values, not drafts. Closing discards drafts; admitted work is not cancelled. Password changes are available only on the full Network page.';\n"
" element('quick-header').hidden = false; trigger.setAttribute('aria-expanded', 'true');\n"
" terminalHost.hidden = previous !== 'serial'; adminHost.hidden = previous !== 'admin';\n"
" refreshSettings(); if (activate) element('quick-close').focus();\n"
@@ -432,8 +437,9 @@ static const char s_app_js[] =
" window.clearTimeout(quickLeaveTimer); if (!quick) return;\n"
" quickLeaveTimer = window.setTimeout(() => { quickLeaveTimer = 0; if (quick && !quickContains(document.activeElement) && !settingsHost.matches(':hover') && !quick.trigger.matches(':hover')) closeQuick(); }, 250);\n"
"}\n"
"for (const domain of ['serial','network']) {\n"
"for (const domain of quickDomains) {\n"
" const trigger = element('quick-' + domain);\n"
" if (domain === 'broker' || domain === 'writer') trigger.setAttribute('title', 'Open client/writer management. When a full Settings page is open, use its Broker navigation or return to a terminal first; contextual triggers preserve that page and its drafts.');\n"
" trigger.addEventListener('pointerenter', event => { if (event.pointerType === 'mouse') openQuick(domain); });\n"
" trigger.addEventListener('focus', () => openQuick(domain));\n"
" trigger.addEventListener('click', () => openQuick(domain, true));\n"
@@ -694,7 +700,16 @@ static const char s_app_js[] =
"element('display-result').addEventListener('click', () => displayOperation(null));\n"
"for (const action of displayActions) element('display-' + action).addEventListener('click', () => displayOperation(action));\n"
"const brokerDetail = element('broker-detail');\n"
"let brokerClients = [], brokerWriter = 0;\n"
"let brokerClients = [], brokerWriter = 0, brokerSelection = null, brokerLiveTimer = 0;\n"
"function stopBrokerLive() { window.clearTimeout(brokerLiveTimer); brokerLiveTimer = 0; }\n"
"function scheduleBrokerLive() {\n"
" stopBrokerLive();\n"
" if (!quick || settingsDomain !== 'broker' || brokerAbort || brokerAuto || brokerOperationPending) return;\n"
" brokerLiveTimer = window.setTimeout(() => { brokerLiveTimer = 0; refreshBroker(); }, 5000);\n"
"}\n"
"function brokerSelectionCurrent() {\n"
" return brokerSelection && !brokerSelection.stale && brokerSelection.generation === brokerGeneration && brokerClients.some(c => c.id === brokerSelection.target && c.type === brokerSelection.type && c.name_hex === brokerSelection.name_hex);\n"
"}\n"
"let brokerAbort = null, brokerGeneration = 0, brokerOperationAction = '';\n"
"const brokerActions = ['assign'];\n"
"const brokerUint = v => Number.isInteger(v) && v >= 0 && v <= 4294967295;\n"
@@ -739,18 +754,25 @@ static const char s_app_js[] =
" auto.deadline = window.setTimeout(() => expireBrokerAuto(auto), 15000);\n"
" scheduleBrokerCheck();\n"
"}\n"
"function brokerButton(id, disabled) {\n"
" const button = element('broker-' + id);\n"
" button.setAttribute('aria-disabled', String(disabled));\n"
" // Preserve keyboard focus during live updates; handlers independently gate all actions.\n"
" button.disabled = disabled && document.activeElement !== button;\n"
"}\n"
"function brokerButtons() {\n"
" const busy = !!brokerAbort || !!brokerAuto;\n"
" const target = Number(element('broker-target').value);\n"
" element('broker-assign').disabled = busy || brokerOperationPending || !brokerGeneration || brokerGeneration === 4294967295 || !brokerClients.some(c => c.id === target) || target === brokerWriter;\n"
" element('broker-target').disabled = busy || brokerOperationPending || !brokerGeneration;\n"
" element('broker-refresh').disabled = busy;\n"
" element('broker-result').disabled = busy;\n"
" brokerButton('assign', busy || brokerOperationPending || !brokerSelectionCurrent() || !brokerGeneration || brokerGeneration === 4294967295 || !brokerClients.some(c => c.id === target) || target === brokerWriter);\n"
" brokerButton('target', (!!brokerAbort && !brokerAbort.brokerRead) || !!brokerAuto || brokerOperationPending || !brokerGeneration);\n"
" brokerButton('refresh', busy);\n"
" brokerButton('result', busy);\n"
"}\n"
"function clearBroker() {\n"
" if (!brokerAuto && brokerOperationPending) element('broker-operation-detail').textContent = brokerOutcomeWarning + 'Outcome pending or unknown. Check Result on return; navigation does not cancel work.';\n"
" stopBrokerAuto(true);\n"
" if (brokerAbort) brokerAbort.abort(); brokerAbort = null; brokerGeneration = 0;\n"
" stopBrokerLive(); brokerSelection = null;\n"
" brokerClients = []; brokerWriter = 0; element('broker-target').value = '';\n"
" element('broker-values').textContent = '';\n"
" for (let i = 0; i < 8; ++i) { const o = element('broker-option-' + i); o.textContent = ''; o.hidden = o.disabled = true; }\n"
@@ -758,7 +780,10 @@ static const char s_app_js[] =
"}\n"
"async function refreshBroker() {\n"
" if (settingsDomain !== 'broker' || selected !== 'settings' || accountRole !== 'admin' || !sessionVerified || suspended || unloading || navigating || loggingOut || brokerAbort || brokerAuto) return;\n"
" const controller = new AbortController(), generation = workGeneration; brokerAbort = controller; brokerButtons();\n"
" stopBrokerLive();\n"
" const controller = new AbortController(), generation = workGeneration; controller.brokerRead = true; brokerAbort = controller; brokerButtons();\n"
" const deadline = window.setTimeout(() => { if (brokerAbort !== controller) return; controller.abort(); brokerAbort = null; brokerGeneration = 0; if (brokerSelection) brokerSelection.stale = true; brokerDetail.textContent = 'Live refresh timed out. Selection stale; Refresh and reselect. Automatic refresh stopped.'; brokerButtons(); }, 5000);\n"
" controller.signal.addEventListener('abort', () => window.clearTimeout(deadline), {once:true});\n"
" const current = () => brokerAbort === controller && selected === 'settings' && settingsDomain === 'broker';\n"
" brokerDetail.textContent = 'Reading broker clients... Previous snapshot is stale until refreshed.';\n"
" try {\n"
@@ -766,7 +791,8 @@ static const char s_app_js[] =
" const {status, payload: v} = await api('/api/settings/broker', generation, {signal: controller.signal, limit: 2048, current});\n"
" if (status !== 200 || !v || Object.keys(v).length !== 3 || !Number.isInteger(v.generation) || v.generation < 1 || v.generation > 4294967295 || !brokerValid(v)) throw new Error('Invalid snapshot');\n"
" brokerGeneration = v.generation; brokerClients = v.clients; brokerWriter = v.writer;\n"
" element('broker-target').value = '';\n"
" if (brokerSelection && !brokerSelectionCurrent()) brokerSelection.stale = true;\n"
" if (brokerSelection && !brokerClients.some(c => c.id === brokerSelection.target)) brokerSelection.missing = true;\n"
" const list = element('broker-values'); list.textContent = '';\n"
" for (let i = 0; i < 8; ++i) {\n"
" const c = brokerClients[i], o = element('broker-option-' + i); o.hidden = o.disabled = !c;\n"
@@ -778,10 +804,11 @@ static const char s_app_js[] =
" list.appendChild(dt); list.appendChild(dd);\n"
" }\n"
" }\n"
" brokerDetail.textContent = (brokerOperationPending ? 'Outcome pending or unknown. ' : '') + 'Writer: ' + (brokerWriter || 'None') + '. ' + brokerClients.length + ' connected clients. Refresh clears selection; review and explicitly confirm assignment.' + (brokerGeneration === 4294967295 ? ' Confirmation generation exhausted; use the admin shell.' : '');\n"
" element('broker-target').value = brokerSelection && !brokerSelection.missing && brokerClients.some(c => c.id === brokerSelection.target) ? String(brokerSelection.target) : '';\n"
" brokerDetail.textContent = (brokerOperationPending ? 'Outcome pending or unknown. ' : '') + 'Writer: ' + (brokerWriter || 'None') + '. ' + brokerClients.length + ' connected clients. ' + (brokerSelection?.stale ? 'Selection stale: lease or client changed. Choose the blank option then explicitly select again before confirmation. ' : 'Refresh preserves selection, never renews its confirmation token. ') + (quick ? 'Live refresh every 5 seconds after completion. ' : '') + (brokerGeneration === 4294967295 ? 'Confirmation generation exhausted; use the admin shell.' : '');\n"
" } catch (error) {\n"
" if (live(generation) && current()) { brokerGeneration = 0; brokerDetail.textContent = (error.status ? error.message : 'Broker snapshot unavailable or invalid.') + ' Select Refresh to retry.'; }\n"
" } finally { if (current()) { brokerAbort = null; brokerButtons(); } }\n"
" if (live(generation) && current()) { brokerGeneration = 0; if (brokerSelection) brokerSelection.stale = true; brokerDetail.textContent = (error.status ? error.message : 'Broker snapshot unavailable or invalid.') + ' Selection stale. Select Refresh and reselect to retry.'; }\n"
" } finally { window.clearTimeout(deadline); if (current()) { brokerAbort = null; brokerButtons(); if (brokerGeneration) scheduleBrokerLive(); } }\n"
"}\n"
"async function brokerOperation(action, automatic = false) {\n"
" if (settingsDomain !== 'broker') return;\n"
@@ -792,19 +819,21 @@ static const char s_app_js[] =
" let body;\n"
" if (action) {\n"
" const target = Number(element('broker-target').value), client = brokerClients.find(c => c.id === target);\n"
" if (action !== 'assign' || !client || !brokerGeneration || brokerGeneration === 4294967295 || target === brokerWriter) return;\n"
" const value = {action, generation: brokerGeneration, target};\n"
" if (!brokerSelectionCurrent()) { detail.textContent = 'Selection stale or missing. Refresh, explicitly select and confirm again. No assignment submitted.'; return; }\n"
" if (action !== 'assign' || !client || !brokerGeneration || brokerGeneration === 4294967295 || target === brokerWriter || target !== brokerSelection.target) return;\n"
" const value = {action, generation: brokerSelection.generation, target};\n"
" if (!window.confirm('Assign the writer lease to ' + brokerLabel(client) + '? Current writer: ' + (brokerWriter || 'None') + '. The previous writer becomes an observer. Already queued UART bytes are not recalled.')) return;\n"
" body = JSON.stringify(value);\n"
" if (new TextEncoder().encode(body).length > 256) return;\n"
" }\n"
" stopBrokerLive(); if (action && brokerSelection) brokerSelection.stale = true;\n"
" const controller = new AbortController(), generation = workGeneration; brokerAbort = controller;\n"
" const auto = automatic ? brokerAuto : null;\n"
" const current = () => {\n"
" if (auto && brokerAuto === auto && performance.now() >= auto.until) expireBrokerAuto(auto);\n"
" return brokerAbort === controller && selected === 'settings' && settingsDomain === 'broker';\n"
" };\n"
" element('broker-refresh').disabled = true; brokerButtons();\n"
" brokerButtons();\n"
" detail.textContent = brokerOutcomeWarning + (action ? 'Assigning... Submitting once; completion will be checked automatically.' : 'Reading latest result for this login...');\n"
" brokerDetail.textContent = 'Snapshot stale: operation pending or outcome not yet checked.';\n"
" try {\n"
@@ -848,7 +877,12 @@ static const char s_app_js[] =
"element('broker-refresh').addEventListener('click', refreshBroker);\n"
"element('broker-result').addEventListener('click', () => brokerOperation(null));\n"
"for (const action of brokerActions) element('broker-' + action).addEventListener('click', () => brokerOperation(action));\n"
"element('broker-target').addEventListener('change', brokerButtons);\n"
"element('broker-target').addEventListener('change', () => {\n"
" const client = brokerClients.find(c => c.id === Number(element('broker-target').value));\n"
" brokerSelection = client && (!brokerAbort || brokerAbort.brokerRead) && !brokerOperationPending && brokerGeneration ? {target:client.id, type:client.type, name_hex:client.name_hex, generation:brokerGeneration, stale:false} : null;\n"
" if (brokerSelection) brokerDetail.textContent = 'Explicit selection: ' + brokerLabel(client) + '. Confirm assignment separately; refresh never renews this lease token.';\n"
" brokerButtons();\n"
"});\n"
"let settingsDomain = 'serial', accounts = [], accountsAbort = null, accountId = 0, accountPending = false, accountAwaitingAck = false, accountWarning = '';\n"
"let keysAbort = null, accountKeys = [], keysIdentity = '';\n"
"function keyIdentity() { const t = accounts[Number(element('account-target').value)]; return t ? JSON.stringify([t.username,t.user_id,t.auth_generation]) : ''; }\n"
@@ -1549,7 +1583,7 @@ static const char s_app_js[] =
" if (accountRole !== 'admin') { closeAdmin(); selectTerminal('serial'); }\n"
" terminalHost.hidden = (quick?.previous || selected) !== 'serial'; adminHost.hidden = (quick?.previous || selected) !== 'admin';\n"
" settingsHost.hidden = selected !== 'settings' || loggingOut || suspended;\n"
" for (const domain of ['serial','network']) element('quick-' + domain).disabled = accountRole !== 'admin' || loggingOut || suspended;\n"
" for (const domain of quickDomains) element('quick-' + domain).disabled = accountRole !== 'admin' || loggingOut || suspended;\n"
" scheduleFit();\n"
" const deadline = Date.now() + payload.expires_in * 1000;\n"
" sessionDeadline = sessionDeadline ? Math.min(sessionDeadline, deadline) : deadline;\n"