feat(dashboard): surface per-device settings PIN + backfill existing fleet (#152)
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run

The server provisions a unique settings-menu PIN per device, but nothing surfaced
it — leaving the on-device hidden settings menu effectively unopenable. Show the
PIN on the device Info tab (native players only), with i18n across 6 locales.

Also backfill a unique 6-digit PIN for already-paired devices that predate the
settings_pin column, so the existing fleet isn't locked out (delivered on their
next reconnect via the existing device:paired re-send). Idempotent UPDATE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-07-09 19:55:28 -05:00
parent d474122334
commit b72e964433
8 changed files with 23 additions and 0 deletions

View file

@ -254,6 +254,8 @@ export default {
'device.info.uptime': 'Betriebszeit',
'device.info.android_version': 'Android-Version',
'device.info.app_version': 'App-Version',
'device.info.settings_pin': 'Einstellungs-PIN',
'device.info.settings_pin_hint': 'Geräte-Einstellungsmenü (2× Zurück)',
'device.info.screen_resolution': 'Bildschirmauflösung',
'device.info.ram': 'RAM',
'device.info.cpu_usage': 'CPU-Auslastung',

View file

@ -296,6 +296,8 @@ export default {
'device.info.uptime': 'Uptime',
'device.info.android_version': 'Android Version',
'device.info.app_version': 'App Version',
'device.info.settings_pin': 'Settings PIN',
'device.info.settings_pin_hint': 'On-device settings menu (2× Back)',
'device.info.screen_resolution': 'Screen Resolution',
'device.info.ram': 'RAM',
'device.info.cpu_usage': 'CPU Usage',

View file

@ -253,6 +253,8 @@ export default {
'device.info.uptime': 'Tiempo activo',
'device.info.android_version': 'Versión de Android',
'device.info.app_version': 'Versión de la app',
'device.info.settings_pin': 'PIN de ajustes',
'device.info.settings_pin_hint': 'Menú de ajustes del dispositivo (2× Atrás)',
'device.info.screen_resolution': 'Resolución de pantalla',
'device.info.ram': 'RAM',
'device.info.cpu_usage': 'Uso de CPU',

View file

@ -254,6 +254,8 @@ export default {
'device.info.uptime': 'Disponibilité',
'device.info.android_version': 'Version d\'Android',
'device.info.app_version': 'Version de l\'app',
'device.info.settings_pin': 'Code PIN des réglages',
'device.info.settings_pin_hint': 'Menu des réglages sur l\'appareil (2× Retour)',
'device.info.screen_resolution': 'Résolution',
'device.info.ram': 'RAM',
'device.info.cpu_usage': 'Utilisation CPU',

View file

@ -270,6 +270,8 @@ export default {
'device.info.uptime': 'Tempo di attività',
'device.info.android_version': 'Versione Android',
'device.info.app_version': 'Versione App',
'device.info.settings_pin': 'PIN impostazioni',
'device.info.settings_pin_hint': 'Menu impostazioni sul dispositivo (2× Indietro)',
'device.info.screen_resolution': 'Risoluzione Schermo',
'device.info.ram': 'RAM',
'device.info.cpu_usage': 'Utilizzo CPU',

View file

@ -254,6 +254,8 @@ export default {
'device.info.uptime': 'Tempo ativo',
'device.info.android_version': 'Versão do Android',
'device.info.app_version': 'Versão do app',
'device.info.settings_pin': 'PIN de configurações',
'device.info.settings_pin_hint': 'Menu de configurações no dispositivo (2× Voltar)',
'device.info.screen_resolution': 'Resolução',
'device.info.ram': 'RAM',
'device.info.cpu_usage': 'Uso de CPU',

View file

@ -305,6 +305,11 @@ async function loadDevice(deviceId, activeTab = null) {
<div class="info-card-label">${t('device.info.app_version')}</div>
<div class="info-card-value small">${device.app_version || '--'}</div>
</div>
<div class="info-card">
<div class="info-card-label">${t('device.info.settings_pin')}</div>
<div class="info-card-value small" style="font-family:monospace;letter-spacing:1px">${device.settings_pin || '--'}</div>
<div style="font-size:11px;color:var(--text-muted);margin-top:2px">${t('device.info.settings_pin_hint')}</div>
</div>
` : ''}
<div class="info-card">
<div class="info-card-label">${t('device.info.screen_resolution')}</div>

View file

@ -267,6 +267,12 @@ const migrations = [
// settings_pin: 6-digit PIN for the in-app hidden settings menu, provisioned by
// the server during pairing so each device gets a unique PIN (never a hardcoded default).
"ALTER TABLE devices ADD COLUMN settings_pin TEXT",
// Backfill a unique 6-digit PIN for already-paired devices that predate the
// settings_pin column (their next reconnect re-sends device:paired with it, so
// the existing fleet isn't locked out of the on-device menu). Idempotent: the
// IS NULL guard means it only ever touches un-provisioned rows. Unpaired rows
// (user_id IS NULL) are skipped — they get a PIN when they pair.
"UPDATE devices SET settings_pin = CAST(abs(random()) % 900000 + 100000 AS TEXT) WHERE settings_pin IS NULL AND user_id IS NOT NULL",
// #150: fingerprint-keyed device settings that SURVIVE device-row deletion, so a
// delete + re-pair (MDM churn) restores orientation/name/playlist/etc for the SAME
// physical device instead of silently resetting to defaults. NO FK to devices -> it