diff --git a/app/static/js/repeater-manage.js b/app/static/js/repeater-manage.js index 6f84e7a..f14479d 100644 --- a/app/static/js/repeater-manage.js +++ b/app/static/js/repeater-manage.js @@ -88,19 +88,20 @@ function esc(s) { // Tools configuration // ================================================================ +// title/desc hold catalog KEYS, resolved with t() at render time. const TOOLS = [ - { key: 'status', icon: 'bi-bar-chart-line', title: 'Status', - desc: 'Battery, radio and packet statistics', adminOnly: false }, - { key: 'telemetry', icon: 'bi-activity', title: 'Telemetry', - desc: 'Sensor channels (Cayenne LPP)', adminOnly: false }, - { key: 'neighbors', icon: 'bi-people', title: 'Neighbors', - desc: 'Zero-hop repeaters heard', adminOnly: false }, - { key: 'cli', icon: 'bi-terminal', title: 'CLI', - desc: 'Send text commands to the repeater', adminOnly: true }, - { key: 'settings', icon: 'bi-gear', title: 'Settings', - desc: 'Configure repeater parameters', adminOnly: true }, - { key: 'actions', icon: 'bi-lightning', title: 'Actions', - desc: 'Advert, clock sync, reboot', adminOnly: true }, + { key: 'status', icon: 'bi-bar-chart-line', title: 'rptmgmt.tools.status', + desc: 'rptmgmt.tools.status_desc', adminOnly: false }, + { key: 'telemetry', icon: 'bi-activity', title: 'rptmgmt.tools.telemetry', + desc: 'rptmgmt.tools.telemetry_desc', adminOnly: false }, + { key: 'neighbors', icon: 'bi-people', title: 'rptmgmt.tools.neighbors', + desc: 'rptmgmt.tools.neighbors_desc', adminOnly: false }, + { key: 'cli', icon: 'bi-terminal', title: 'rptmgmt.tools.cli', + desc: 'rptmgmt.tools.cli_desc', adminOnly: true }, + { key: 'settings', icon: 'bi-gear', title: 'rptmgmt.tools.settings', + desc: 'rptmgmt.tools.settings_desc', adminOnly: true }, + { key: 'actions', icon: 'bi-lightning', title: 'rptmgmt.tools.actions', + desc: 'rptmgmt.tools.actions_desc', adminOnly: true }, ]; // ================================================================ @@ -118,7 +119,7 @@ let _passwordModal = null; function showLoading(text) { document.getElementById('loadingState').style.display = ''; - document.getElementById('loadingText').textContent = text || 'Loading…'; + document.getElementById('loadingText').textContent = text || t('common.loading'); document.getElementById('errorState').style.display = 'none'; document.getElementById('panelContent').style.display = 'none'; } @@ -126,7 +127,7 @@ function showLoading(text) { function showError(text) { document.getElementById('loadingState').style.display = 'none'; document.getElementById('errorState').style.display = ''; - document.getElementById('errorText').textContent = text || 'Something went wrong.'; + document.getElementById('errorText').textContent = text || t('rptmgmt.error_generic'); document.getElementById('panelContent').style.display = 'none'; } @@ -188,8 +189,8 @@ function renderTools() { ${locked ? 'title="Admin login required"' : ''}>
-
${esc(tool.title)}${locked ? ' ' : ''}
-

${esc(tool.desc)}

+
${esc(t(tool.title))}${locked ? ' ' : ''}
+

${esc(t(tool.desc))}

@@ -197,7 +198,7 @@ function renderTools() { const tile = col.querySelector('.tool-tile'); tile.addEventListener('click', () => { if (locked) { - showNotification('Admin login required for this tool', 'warning'); + showNotification(t('rptmgmt.toast.admin_required'), 'warning'); return; } openToolPane(tool); @@ -226,7 +227,7 @@ function openToolPane(tool) { icon.style.height = '32px'; icon.style.fontSize = '1rem'; icon.innerHTML = ``; - document.getElementById('paneTitle').textContent = tool.title; + document.getElementById('paneTitle').textContent = t(tool.title); const body = document.getElementById('paneBody'); if (tool.key === 'status') { @@ -256,7 +257,7 @@ function openToolPane(tool) { body.innerHTML = `
-

The ${esc(tool.title)} tool is coming in a later stage.

+

${tHtml('rptmgmt.tool_coming', { tool: t(tool.title) })}

`; } @@ -322,7 +323,7 @@ function setStatusUpdatedLabel() { const label = document.getElementById('statusUpdated'); if (!label) { clearInterval(_statusTimer); _statusTimer = null; return; } const secs = Math.floor((Date.now() - _statusUpdatedAt) / 1000); - if (secs < 2) label.textContent = 'Updated just now'; + if (secs < 2) label.textContent = t('rptmgmt.updated_just_now'); else if (secs < 60) label.textContent = `Updated ${secs}s ago`; else label.textContent = `Updated ${fmtDuration(secs)} ago`; }; @@ -347,7 +348,7 @@ async function loadStatus() { const resp = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}/status`); data = await resp.json(); } catch (e) { - data = { success: false, error: 'Request failed' }; + data = { success: false, error: t('rptmgmt.request_failed') }; } if (btn) btn.disabled = false; @@ -355,7 +356,7 @@ async function loadStatus() { container.innerHTML = `
-

${esc((data && data.error) || 'Failed to get status')}

+

${esc((data && data.error) || t('rptmgmt.status_failed'))}

@@ -494,7 +495,7 @@ function setTelemetryUpdatedLabel() { const label = document.getElementById('telemetryUpdated'); if (!label) { clearInterval(_telemetryTimer); _telemetryTimer = null; return; } const secs = Math.floor((Date.now() - _telemetryUpdatedAt) / 1000); - if (secs < 2) label.textContent = 'Updated just now'; + if (secs < 2) label.textContent = t('rptmgmt.updated_just_now'); else if (secs < 60) label.textContent = `Updated ${secs}s ago`; else label.textContent = `Updated ${fmtDuration(secs)} ago`; }; @@ -520,7 +521,7 @@ async function loadTelemetry() { const resp = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}/telemetry`); data = await resp.json(); } catch (e) { - data = { success: false, error: 'Request failed' }; + data = { success: false, error: t('rptmgmt.request_failed') }; } if (btn) btn.disabled = false; @@ -528,7 +529,7 @@ async function loadTelemetry() { container.innerHTML = `
-

${esc((data && data.error) || 'Failed to get telemetry')}

+

${esc((data && data.error) || t('rptmgmt.telemetry_failed'))}

@@ -546,7 +547,7 @@ async function loadTelemetry() { function renderTelemetryCards(container, lpp) { if (!lpp.length) { - container.innerHTML = '
No telemetry data reported.
'; + container.innerHTML = `
${tHtml('rptmgmt.telemetry_none')}
`; return; } @@ -710,7 +711,7 @@ async function sendCliCommand(raw) { }); data = await resp.json(); } catch (e) { - data = { success: false, error: 'Request failed' }; + data = { success: false, error: t('rptmgmt.request_failed') }; } if (pendingLine) pendingLine.remove(); @@ -720,7 +721,7 @@ async function sendCliCommand(raw) { cliAppend('meta', `(${(data.elapsed_ms / 1000).toFixed(1)} s)`); } } else { - cliAppend('error', (data && data.error) || 'Command failed'); + cliAppend('error', (data && data.error) || t('rptmgmt.command_failed')); } _cliPending = false; @@ -786,7 +787,7 @@ async function loadNeighbors() { const resp = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}/neighbours`); data = await resp.json(); } catch (e) { - data = { success: false, error: 'Request failed' }; + data = { success: false, error: t('rptmgmt.request_failed') }; } if (btn) btn.disabled = false; @@ -796,7 +797,7 @@ async function loadNeighbors() { container.innerHTML = `
-

${esc((data && data.error) || 'Failed to get neighbours')}

+

${esc((data && data.error) || t('rptmgmt.neighbours_failed'))}

@@ -835,7 +836,7 @@ function renderNeighborsList() { if (toggle) toggle.style.display = mappable ? '' : 'none'; if (!entries.length) { - container.innerHTML = '
No neighbours reported yet.
Neighbours are learned from zero-hop repeater adverts.
'; + container.innerHTML = `
${tHtml('rptmgmt.neighbours_none')}
${tHtml('rptmgmt.neighbours_none_hint')}
`; return; } @@ -852,8 +853,8 @@ function renderNeighborsList() { container.innerHTML = ` - - + + ${rows} @@ -910,7 +911,7 @@ function renderNeighborsMap() { opacity: 1, fillOpacity: 0.9 }).addTo(_nbMapLayers); - centerMarker.bindPopup(`${esc(_repeater.name || 'This repeater')}
managed repeater`); + centerMarker.bindPopup(`${esc(_repeater.name || t('rptmgmt.this_repeater'))}
${tHtml('rptmgmt.managed_repeater')}`); bounds.push(center); } @@ -997,52 +998,54 @@ async function loadClock() { // Sections and fields mirror _REPEATER_SETTINGS_FIELDS in api.py. // Values travel as raw CLI strings (`get X` → `> value`, `set X value`). +// title/label/help/note hold catalog KEYS, resolved with t() at render time. +// `key` is the firmware CLI parameter and is never translated. const SETTINGS_SECTIONS = [ - { key: 'basic', title: 'Basic', icon: 'bi-tag', fields: [ - { key: 'name', label: 'Repeater name', type: 'text' }, - { key: 'password', label: 'Admin password', type: 'password', writeOnly: true, - help: 'Write-only — the current password is never shown. Changing it does not log out already active sessions.' }, - { key: 'guest.password', label: 'Guest password', type: 'text', - help: 'Password for read-only guest logins.' }, + { key: 'basic', title: 'rptmgmt.set.basic', icon: 'bi-tag', fields: [ + { key: 'name', label: 'rptmgmt.set.name', type: 'text' }, + { key: 'password', label: 'rptmgmt.set.password', type: 'password', writeOnly: true, + help: 'rptmgmt.set.password_help' }, + { key: 'guest.password', label: 'rptmgmt.set.guest_password', type: 'text', + help: 'rptmgmt.set.guest_password_help' }, ]}, - { key: 'radio', title: 'Radio', icon: 'bi-broadcast', - note: 'Frequency, bandwidth, SF and CR are applied after a reboot. TX power applies immediately.', + { key: 'radio', title: 'rptmgmt.set.radio', icon: 'bi-broadcast', + note: 'rptmgmt.set.radio_note', fields: [ - { key: 'radio', label: 'Radio parameters', type: 'radio4' }, - { key: 'tx', label: 'TX power (dBm)', type: 'number', min: 0, max: 30, step: 1 }, - { key: 'radio.rxgain', label: 'RX boosted gain', type: 'onoff' }, + { key: 'radio', label: 'rptmgmt.set.radio_params', type: 'radio4' }, + { key: 'tx', label: 'rptmgmt.set.tx', type: 'number', min: 0, max: 30, step: 1 }, + { key: 'radio.rxgain', label: 'rptmgmt.set.rxgain', type: 'onoff' }, ]}, - { key: 'location', title: 'Location', icon: 'bi-geo-alt', fields: [ - { key: 'lat', label: 'Latitude', type: 'text' }, - { key: 'lon', label: 'Longitude', type: 'text' }, + { key: 'location', title: 'rptmgmt.set.location', icon: 'bi-geo-alt', fields: [ + { key: 'lat', label: 'rptmgmt.set.lat', type: 'text' }, + { key: 'lon', label: 'rptmgmt.set.lon', type: 'text' }, ]}, - { key: 'features', title: 'Features', icon: 'bi-toggles', fields: [ - { key: 'repeat', label: 'Repeat packets', type: 'onoff' }, - { key: 'allow.read.only', label: 'Allow read-only (guest) access', type: 'onoff' }, - { key: 'multi.acks', label: 'Send multiple ACKs', type: 'zeroone' }, + { key: 'features', title: 'rptmgmt.set.features', icon: 'bi-toggles', fields: [ + { key: 'repeat', label: 'rptmgmt.set.repeat', type: 'onoff' }, + { key: 'allow.read.only', label: 'rptmgmt.set.allow_read_only', type: 'onoff' }, + { key: 'multi.acks', label: 'rptmgmt.set.multi_acks', type: 'zeroone' }, ]}, - { key: 'network', title: 'Network health', icon: 'bi-heart-pulse', fields: [ - { key: 'loop.detect', label: 'Loop detection', type: 'select', + { key: 'network', title: 'rptmgmt.set.network', icon: 'bi-heart-pulse', fields: [ + { key: 'loop.detect', label: 'rptmgmt.set.loop_detect', type: 'select', options: ['off', 'minimal', 'moderate', 'strict'] }, - { key: 'dutycycle', label: 'Duty cycle (%)', type: 'number', min: 1, max: 100, step: 1 }, + { key: 'dutycycle', label: 'rptmgmt.set.dutycycle', type: 'number', min: 1, max: 100, step: 1 }, ]}, - { key: 'advert', title: 'Advertisement', icon: 'bi-megaphone', fields: [ - { key: 'advert.interval', label: 'Local advert interval (minutes)', type: 'number', min: 0, max: 240, step: 1, - help: 'Firmware accepts 60–240 minutes, or 0 to disable.' }, - { key: 'flood.advert.interval', label: 'Flood advert interval (hours)', type: 'number', min: 0, step: 1 }, - { key: 'flood.max', label: 'Max flood hops', type: 'number', min: 0, max: 64, step: 1 }, + { key: 'advert', title: 'rptmgmt.set.advert', icon: 'bi-megaphone', fields: [ + { key: 'advert.interval', label: 'rptmgmt.set.advert_interval', type: 'number', min: 0, max: 240, step: 1, + help: 'rptmgmt.set.advert_interval_help' }, + { key: 'flood.advert.interval', label: 'rptmgmt.set.flood_advert_interval', type: 'number', min: 0, step: 1 }, + { key: 'flood.max', label: 'rptmgmt.set.flood_max', type: 'number', min: 0, max: 64, step: 1 }, ]}, - { key: 'operator', title: 'Operator info', icon: 'bi-person-vcard', fields: [ - { key: 'owner.info', label: 'Owner info', type: 'textarea', - help: 'Free-form operator / contact info shown to clients. Multiple lines allowed.' }, + { key: 'operator', title: 'rptmgmt.set.operator', icon: 'bi-person-vcard', fields: [ + { key: 'owner.info', label: 'rptmgmt.set.owner_info', type: 'textarea', + help: 'rptmgmt.set.owner_info_help' }, ]}, - { key: 'advanced', title: 'Advanced', icon: 'bi-sliders', fields: [ - { key: 'path.hash.mode', label: 'Path hash mode (0–2)', type: 'number', min: 0, max: 2, step: 1 }, - { key: 'txdelay', label: 'TX delay factor (0–2)', type: 'number', min: 0, max: 2, step: 'any' }, - { key: 'direct.txdelay', label: 'Direct TX delay factor (0–2)', type: 'number', min: 0, max: 2, step: 'any' }, - { key: 'int.thresh', label: 'Interference threshold', type: 'number', step: 1 }, - { key: 'agc.reset.interval', label: 'AGC reset interval', type: 'number', min: 0, step: 4, - help: 'Multiple of 4; 0 disables periodic AGC resets.' }, + { key: 'advanced', title: 'rptmgmt.set.advanced', icon: 'bi-sliders', fields: [ + { key: 'path.hash.mode', label: 'rptmgmt.set.path_hash_mode', type: 'number', min: 0, max: 2, step: 1 }, + { key: 'txdelay', label: 'rptmgmt.set.txdelay', type: 'number', min: 0, max: 2, step: 'any' }, + { key: 'direct.txdelay', label: 'rptmgmt.set.direct_txdelay', type: 'number', min: 0, max: 2, step: 'any' }, + { key: 'int.thresh', label: 'rptmgmt.set.int_thresh', type: 'number', step: 1 }, + { key: 'agc.reset.interval', label: 'rptmgmt.set.agc_reset', type: 'number', min: 0, step: 4, + help: 'rptmgmt.set.agc_reset_help' }, ]}, ]; @@ -1104,10 +1107,10 @@ function settingsControlHtml(f) { function settingsFieldHtml(f) { return `
- + ${settingsControlHtml(f)}
- ${f.help ? `
${esc(f.help)}
` : ''} + ${f.help ? `
${esc(t(f.help))}
` : ''}
`; } @@ -1120,22 +1123,22 @@ function renderSettingsPane(body) {

- ${sec.note ? `
${esc(sec.note)}
` : ''} + ${sec.note ? `
${esc(t(sec.note))}
` : ''}
- Some changes take effect after a reboot — use Actions → Reboot. + ${tHtml('rptmgmt.set.reboot_note')}
${sec.fields.map(settingsFieldHtml).join('')}
${sec.key === 'location' ? ` ` : ''}
`; } @@ -1609,17 +1613,16 @@ function renderActionsPane(body) {
- Danger zone + ${tHtml('rptmgmt.danger_zone')}
${actionRowHtml({ key: 'reboot', icon: 'bi-arrow-clockwise', iconClass: 'text-danger', - title: 'Reboot repeater', btn: 'Reboot', btnClass: 'btn-danger', - desc: 'The repeater drops off the mesh for a few seconds while it restarts.', + title: 'rptmgmt.act.reboot', btn: 'rptmgmt.act.reboot_btn', btnClass: 'btn-danger', + desc: 'rptmgmt.act.reboot_desc', })}
- Erase file system is not available over the mesh — - the firmware only accepts it on the USB serial console (use the MeshCore flasher instead). + ${tHtml('rptmgmt.erase_fs_note')}
@@ -1657,7 +1660,7 @@ async function runRepeaterAction(action) { }); data = await resp.json(); } catch (e) { - data = { success: false, error: 'Request failed' }; + data = { success: false, error: t('rptmgmt.request_failed') }; } _actionPending = false; @@ -1669,14 +1672,14 @@ async function runRepeaterAction(action) { if (data && data.success) { resultEl.classList.add(data.ok ? 'text-success' : 'text-danger'); const elapsed = data.elapsed_ms != null ? ` (${(data.elapsed_ms / 1000).toFixed(1)} s)` : ''; - resultEl.textContent = (data.reply || 'Done') + elapsed; + resultEl.textContent = (data.reply || t('rptmgmt.done')) + elapsed; } else { resultEl.classList.add('text-danger'); - resultEl.textContent = (data && data.error) || 'Action failed'; + resultEl.textContent = (data && data.error) || t('rptmgmt.action_failed'); } } if (!data || !data.success) { - showNotification((data && data.error) || 'Action failed', 'danger'); + showNotification((data && data.error) || t('rptmgmt.action_failed'), 'danger'); } } @@ -1688,7 +1691,7 @@ async function fetchRepeater() { const response = await fetch(`/api/repeaters/${encodeURIComponent(_pubkey)}`); const data = await response.json(); if (!data.success) { - throw new Error(data.error || 'Failed to load repeater'); + throw new Error(data.error || t('rptmgmt.load_failed')); } _repeater = data.repeater; _session = data.session; @@ -1713,7 +1716,7 @@ async function doLogin(password, save) { data = await response.json(); } catch (e) { console.error('Login request failed:', e); - data = { success: false, error: 'Login request failed' }; + data = { success: false, error: t('rptmgmt.login_request_failed') }; } if (data && data.success) { diff --git a/app/templates/repeater-manage.html b/app/templates/repeater-manage.html index be4417d..c3fa9ea 100644 --- a/app/templates/repeater-manage.html +++ b/app/templates/repeater-manage.html @@ -3,7 +3,7 @@ - Repeater Management - mc-webui + {{ t('rptmgmt.title') }} - mc-webui
RepeaterHeard${tHtml('rptmgmt.neigh.repeater')}${tHtml('rptmgmt.neigh.heard')} SNR