i18n batch 3b: wire onboarding.js + admin.js (~84 keys)

- Onboarding: 5-step wizard (welcome, get player, pair, upload, done)
  with translated step titles, content, prompts, error messages
- Admin: superadmin user table, plans, system info, role/plan
  selectors, delete confirms
- 750 keys total, parity 100% across en/es/fr/de/pt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-04-29 20:04:23 -05:00
parent 04891bccee
commit 457a2e4dd4
7 changed files with 546 additions and 143 deletions

View file

@ -687,4 +687,85 @@ export default {
'playlist.add_btn': 'Hinzufügen',
'playlist.adding': 'Wird hinzugefügt...',
'playlist.added': 'Hinzugefügt',
// Onboarding
'onboarding.back': 'Zurück',
'onboarding.next': 'Weiter',
'onboarding.skip': 'Assistent überspringen',
'onboarding.go_to_dashboard': 'Zum Dashboard',
'onboarding.pair_display': 'Bildschirm koppeln',
'onboarding.step.welcome.title': 'Willkommen bei ScreenTinker!',
'onboarding.step.welcome.intro': 'Lassen Sie uns alles in unter 5 Minuten einrichten.',
'onboarding.step.welcome.guide_through': 'Dieser Assistent führt Sie durch:',
'onboarding.step.welcome.bullet_download': 'Player-App herunterladen',
'onboarding.step.welcome.bullet_pair': 'Ersten Bildschirm koppeln',
'onboarding.step.welcome.bullet_upload': 'Inhalte hochladen und zuweisen',
'onboarding.step.player.title': 'Schritt 1: Player-App holen',
'onboarding.step.player.intro': 'Installieren Sie den Player auf Ihrem Anzeigegerät.',
'onboarding.step.player.android_label': 'Android-APK',
'onboarding.step.player.android_desc': 'TV-Boxen, Tablets, Fire TV',
'onboarding.step.player.web_label': 'Web-Player',
'onboarding.step.player.web_desc': 'Beliebiger Browser, Pi, ChromeOS',
'onboarding.step.player.url_hint': 'Öffnen Sie die App auf Ihrem Bildschirm und geben Sie diese Server-URL ein:',
'onboarding.step.pair.title': 'Schritt 2: Bildschirm koppeln',
'onboarding.step.pair.intro': 'Geben Sie den 6-stelligen Code ein, der auf Ihrem Bildschirm angezeigt wird.',
'onboarding.step.pair.name_placeholder': 'Anzeigename (z. B. Lobby-TV)',
'onboarding.step.upload.title': 'Schritt 3: Inhalt hochladen',
'onboarding.step.upload.intro': 'Laden Sie ein Video oder Bild zur Anzeige hoch.',
'onboarding.step.upload.click_to_select': 'Klicken, um eine Datei auszuwählen',
'onboarding.step.upload.formats': 'MP4, WebM, JPEG, PNG, GIF',
'onboarding.step.upload.uploading': 'Wird hochgeladen...',
'onboarding.step.done.title': 'Alles bereit!',
'onboarding.step.done.intro': 'Ihr Bildschirm ist gekoppelt und der Inhalt läuft!',
'onboarding.step.done.whats_next': 'Wie geht es weiter?',
'onboarding.step.done.next_content': 'Mehr Inhalte in der <strong>Inhaltsbibliothek</strong> hinzufügen',
'onboarding.step.done.next_layouts': 'Multi-Zonen-Layouts in <strong>Layouts</strong> erstellen',
'onboarding.step.done.next_schedule': 'Zeitplan im <strong>Zeitplan</strong>-Kalender einrichten',
'onboarding.step.done.next_widgets': 'Live-Widgets (Uhr, Wetter, Ticker) in <strong>Widgets</strong> hinzufügen',
'onboarding.step.done.next_kiosk': 'Interaktive Bildschirme in <strong>Kiosk</strong> erstellen',
'onboarding.step.done.next_designer': 'Individuelle Inhalte im <strong>Designer</strong> entwerfen',
'onboarding.toast.invalid_code': 'Geben Sie einen gültigen 6-stelligen Code ein',
'onboarding.toast.pairing': 'Wird gekoppelt...',
'onboarding.toast.pair_failed': 'Kopplung fehlgeschlagen',
'onboarding.toast.pair_failed_with_error': 'Kopplung fehlgeschlagen: {error}',
'onboarding.toast.paired': 'Bildschirm gekoppelt!',
'onboarding.toast.uploaded_assigning': 'Hochgeladen! Wird dem Bildschirm zugewiesen...',
'onboarding.toast.content_assigned': 'Inhalt hochgeladen und zugewiesen!',
'onboarding.toast.upload_failed': 'Upload fehlgeschlagen',
'onboarding.toast.error_with_error': 'Fehler: {error}',
// Admin
'admin.title': 'Plattform-Admin',
'admin.subtitle': 'Superadmin-Steuerung - nur Sie sehen dies',
'admin.access_denied': 'Zugriff verweigert',
'admin.access_denied_desc': 'Plattform-Admin-Zugriff erforderlich.',
'admin.all_users': 'Alle Benutzer',
'admin.plans': 'Abonnementpläne',
'admin.system': 'System',
'admin.col.user': 'Benutzer',
'admin.col.auth': 'Auth',
'admin.col.last_login': 'Letzte Anmeldung',
'admin.col.role': 'Rolle',
'admin.col.plan': 'Plan',
'admin.col.actions': 'Aktionen',
'admin.col.devices': 'Geräte',
'admin.col.storage': 'Speicher',
'admin.col.monthly': 'Monatlich',
'admin.col.yearly': 'Jährlich',
'admin.role.user': 'Benutzer',
'admin.role.admin': 'Admin',
'admin.role.superadmin': 'Superadmin',
'admin.remove': 'Entfernen',
'admin.owner': 'Eigentümer',
'admin.confirm': 'Bestätigen?',
'admin.total_users': '{n} Benutzer insgesamt',
'admin.unlimited': 'Unbegrenzt',
'admin.free': 'Kostenlos',
'admin.version': 'Version',
'admin.frontend_hash': 'Frontend-Hash',
'admin.download_db_backup': 'DB-Backup herunterladen',
'admin.server_status': 'Serverstatus',
'admin.toast.role_updated': 'Rolle aktualisiert',
'admin.toast.plan_updated': 'Plan aktualisiert',
'admin.toast.user_removed': 'Benutzer entfernt',
};

View file

@ -723,4 +723,85 @@ export default {
'playlist.add_btn': 'Add',
'playlist.adding': 'Adding...',
'playlist.added': 'Added',
// Onboarding
'onboarding.back': 'Back',
'onboarding.next': 'Next',
'onboarding.skip': 'Skip Wizard',
'onboarding.go_to_dashboard': 'Go to Dashboard',
'onboarding.pair_display': 'Pair Display',
'onboarding.step.welcome.title': 'Welcome to ScreenTinker!',
'onboarding.step.welcome.intro': "Let's get you set up in under 5 minutes.",
'onboarding.step.welcome.guide_through': 'This wizard will guide you through:',
'onboarding.step.welcome.bullet_download': 'Downloading the player app',
'onboarding.step.welcome.bullet_pair': 'Pairing your first display',
'onboarding.step.welcome.bullet_upload': 'Uploading and assigning content',
'onboarding.step.player.title': 'Step 1: Get the Player App',
'onboarding.step.player.intro': 'Install the player on your display device.',
'onboarding.step.player.android_label': 'Android APK',
'onboarding.step.player.android_desc': 'TV boxes, tablets, Fire TV',
'onboarding.step.player.web_label': 'Web Player',
'onboarding.step.player.web_desc': 'Any browser, Pi, ChromeOS',
'onboarding.step.player.url_hint': 'Open the app on your display and enter this server URL:',
'onboarding.step.pair.title': 'Step 2: Pair Your Display',
'onboarding.step.pair.intro': 'Enter the 6-digit code shown on your display.',
'onboarding.step.pair.name_placeholder': 'Display name (e.g., Lobby TV)',
'onboarding.step.upload.title': 'Step 3: Upload Content',
'onboarding.step.upload.intro': 'Upload a video or image to display.',
'onboarding.step.upload.click_to_select': 'Click to select a file',
'onboarding.step.upload.formats': 'MP4, WebM, JPEG, PNG, GIF',
'onboarding.step.upload.uploading': 'Uploading...',
'onboarding.step.done.title': "You're All Set!",
'onboarding.step.done.intro': 'Your display is paired and content is playing!',
'onboarding.step.done.whats_next': "What's next?",
'onboarding.step.done.next_content': 'Add more content in the <strong>Content Library</strong>',
'onboarding.step.done.next_layouts': 'Create multi-zone layouts in <strong>Layouts</strong>',
'onboarding.step.done.next_schedule': 'Set up a schedule in the <strong>Schedule</strong> calendar',
'onboarding.step.done.next_widgets': 'Add live widgets (clock, weather, ticker) in <strong>Widgets</strong>',
'onboarding.step.done.next_kiosk': 'Create interactive screens in <strong>Kiosk</strong>',
'onboarding.step.done.next_designer': 'Design custom content in the <strong>Designer</strong>',
'onboarding.toast.invalid_code': 'Enter a valid 6-digit code',
'onboarding.toast.pairing': 'Pairing...',
'onboarding.toast.pair_failed': 'Pairing failed',
'onboarding.toast.pair_failed_with_error': 'Pairing failed: {error}',
'onboarding.toast.paired': 'Display paired!',
'onboarding.toast.uploaded_assigning': 'Uploaded! Assigning to display...',
'onboarding.toast.content_assigned': 'Content uploaded and assigned!',
'onboarding.toast.upload_failed': 'Upload failed',
'onboarding.toast.error_with_error': 'Error: {error}',
// Admin (platform admin panel)
'admin.title': 'Platform Admin',
'admin.subtitle': 'Superadmin controls - only you can see this',
'admin.access_denied': 'Access Denied',
'admin.access_denied_desc': 'Platform admin access required.',
'admin.all_users': 'All Users',
'admin.plans': 'Subscription Plans',
'admin.system': 'System',
'admin.col.user': 'User',
'admin.col.auth': 'Auth',
'admin.col.last_login': 'Last Login',
'admin.col.role': 'Role',
'admin.col.plan': 'Plan',
'admin.col.actions': 'Actions',
'admin.col.devices': 'Devices',
'admin.col.storage': 'Storage',
'admin.col.monthly': 'Monthly',
'admin.col.yearly': 'Yearly',
'admin.role.user': 'User',
'admin.role.admin': 'Admin',
'admin.role.superadmin': 'Superadmin',
'admin.remove': 'Remove',
'admin.owner': 'Owner',
'admin.confirm': 'Confirm?',
'admin.total_users': '{n} total users',
'admin.unlimited': 'Unlimited',
'admin.free': 'Free',
'admin.version': 'Version',
'admin.frontend_hash': 'Frontend Hash',
'admin.download_db_backup': 'Download DB Backup',
'admin.server_status': 'Server Status',
'admin.toast.role_updated': 'Role updated',
'admin.toast.plan_updated': 'Plan updated',
'admin.toast.user_removed': 'User removed',
};

View file

@ -686,4 +686,85 @@ export default {
'playlist.add_btn': 'Agregar',
'playlist.adding': 'Agregando...',
'playlist.added': 'Agregado',
// Onboarding
'onboarding.back': 'Atrás',
'onboarding.next': 'Siguiente',
'onboarding.skip': 'Omitir asistente',
'onboarding.go_to_dashboard': 'Ir al panel',
'onboarding.pair_display': 'Vincular pantalla',
'onboarding.step.welcome.title': '¡Bienvenido a ScreenTinker!',
'onboarding.step.welcome.intro': 'Vamos a configurarlo todo en menos de 5 minutos.',
'onboarding.step.welcome.guide_through': 'Este asistente te guiará a través de:',
'onboarding.step.welcome.bullet_download': 'Descargar la app del reproductor',
'onboarding.step.welcome.bullet_pair': 'Vincular tu primera pantalla',
'onboarding.step.welcome.bullet_upload': 'Subir y asignar contenido',
'onboarding.step.player.title': 'Paso 1: Obtén la app del reproductor',
'onboarding.step.player.intro': 'Instala el reproductor en tu dispositivo de pantalla.',
'onboarding.step.player.android_label': 'APK Android',
'onboarding.step.player.android_desc': 'Cajas TV, tabletas, Fire TV',
'onboarding.step.player.web_label': 'Reproductor web',
'onboarding.step.player.web_desc': 'Cualquier navegador, Pi, ChromeOS',
'onboarding.step.player.url_hint': 'Abre la app en tu pantalla e ingresa esta URL del servidor:',
'onboarding.step.pair.title': 'Paso 2: Vincula tu pantalla',
'onboarding.step.pair.intro': 'Ingresa el código de 6 dígitos mostrado en tu pantalla.',
'onboarding.step.pair.name_placeholder': 'Nombre (p. ej., TV Vestíbulo)',
'onboarding.step.upload.title': 'Paso 3: Sube contenido',
'onboarding.step.upload.intro': 'Sube un video o imagen para mostrar.',
'onboarding.step.upload.click_to_select': 'Haz clic para seleccionar un archivo',
'onboarding.step.upload.formats': 'MP4, WebM, JPEG, PNG, GIF',
'onboarding.step.upload.uploading': 'Subiendo...',
'onboarding.step.done.title': '¡Todo listo!',
'onboarding.step.done.intro': '¡Tu pantalla está vinculada y el contenido se está reproduciendo!',
'onboarding.step.done.whats_next': '¿Qué sigue?',
'onboarding.step.done.next_content': 'Agrega más contenido en la <strong>Biblioteca de contenido</strong>',
'onboarding.step.done.next_layouts': 'Crea diseños multizona en <strong>Diseños</strong>',
'onboarding.step.done.next_schedule': 'Configura un horario en el calendario de <strong>Horario</strong>',
'onboarding.step.done.next_widgets': 'Agrega widgets en vivo (reloj, clima, ticker) en <strong>Widgets</strong>',
'onboarding.step.done.next_kiosk': 'Crea pantallas interactivas en <strong>Kiosco</strong>',
'onboarding.step.done.next_designer': 'Diseña contenido personalizado en el <strong>Diseñador</strong>',
'onboarding.toast.invalid_code': 'Ingresa un código válido de 6 dígitos',
'onboarding.toast.pairing': 'Vinculando...',
'onboarding.toast.pair_failed': 'Falló la vinculación',
'onboarding.toast.pair_failed_with_error': 'Falló la vinculación: {error}',
'onboarding.toast.paired': '¡Pantalla vinculada!',
'onboarding.toast.uploaded_assigning': '¡Subido! Asignando a la pantalla...',
'onboarding.toast.content_assigned': '¡Contenido subido y asignado!',
'onboarding.toast.upload_failed': 'Falló la subida',
'onboarding.toast.error_with_error': 'Error: {error}',
// Admin
'admin.title': 'Administración de plataforma',
'admin.subtitle': 'Controles de superadmin - solo tú puedes ver esto',
'admin.access_denied': 'Acceso denegado',
'admin.access_denied_desc': 'Se requiere acceso de administrador de plataforma.',
'admin.all_users': 'Todos los usuarios',
'admin.plans': 'Planes de suscripción',
'admin.system': 'Sistema',
'admin.col.user': 'Usuario',
'admin.col.auth': 'Auth',
'admin.col.last_login': 'Último inicio',
'admin.col.role': 'Rol',
'admin.col.plan': 'Plan',
'admin.col.actions': 'Acciones',
'admin.col.devices': 'Dispositivos',
'admin.col.storage': 'Almacenamiento',
'admin.col.monthly': 'Mensual',
'admin.col.yearly': 'Anual',
'admin.role.user': 'Usuario',
'admin.role.admin': 'Admin',
'admin.role.superadmin': 'Superadmin',
'admin.remove': 'Eliminar',
'admin.owner': 'Propietario',
'admin.confirm': '¿Confirmar?',
'admin.total_users': '{n} usuarios totales',
'admin.unlimited': 'Ilimitado',
'admin.free': 'Gratis',
'admin.version': 'Versión',
'admin.frontend_hash': 'Hash del frontend',
'admin.download_db_backup': 'Descargar respaldo de BD',
'admin.server_status': 'Estado del servidor',
'admin.toast.role_updated': 'Rol actualizado',
'admin.toast.plan_updated': 'Plan actualizado',
'admin.toast.user_removed': 'Usuario eliminado',
};

View file

@ -687,4 +687,85 @@ export default {
'playlist.add_btn': 'Ajouter',
'playlist.adding': 'Ajout...',
'playlist.added': 'Ajouté',
// Onboarding
'onboarding.back': 'Retour',
'onboarding.next': 'Suivant',
'onboarding.skip': 'Ignorer l\'assistant',
'onboarding.go_to_dashboard': 'Aller au tableau de bord',
'onboarding.pair_display': 'Apparier l\'écran',
'onboarding.step.welcome.title': 'Bienvenue sur ScreenTinker !',
'onboarding.step.welcome.intro': 'Configurons tout en moins de 5 minutes.',
'onboarding.step.welcome.guide_through': 'Cet assistant vous guidera à travers :',
'onboarding.step.welcome.bullet_download': 'Télécharger l\'app du lecteur',
'onboarding.step.welcome.bullet_pair': 'Apparier votre premier écran',
'onboarding.step.welcome.bullet_upload': 'Téléverser et attribuer du contenu',
'onboarding.step.player.title': 'Étape 1 : Obtenez l\'app du lecteur',
'onboarding.step.player.intro': 'Installez le lecteur sur votre appareil d\'affichage.',
'onboarding.step.player.android_label': 'APK Android',
'onboarding.step.player.android_desc': 'Boîtiers TV, tablettes, Fire TV',
'onboarding.step.player.web_label': 'Lecteur web',
'onboarding.step.player.web_desc': 'Tout navigateur, Pi, ChromeOS',
'onboarding.step.player.url_hint': 'Ouvrez l\'app sur votre écran et saisissez cette URL du serveur :',
'onboarding.step.pair.title': 'Étape 2 : Appariez votre écran',
'onboarding.step.pair.intro': 'Saisissez le code à 6 chiffres affiché sur votre écran.',
'onboarding.step.pair.name_placeholder': 'Nom (ex. TV du hall)',
'onboarding.step.upload.title': 'Étape 3 : Téléversez du contenu',
'onboarding.step.upload.intro': 'Téléversez une vidéo ou une image à afficher.',
'onboarding.step.upload.click_to_select': 'Cliquez pour sélectionner un fichier',
'onboarding.step.upload.formats': 'MP4, WebM, JPEG, PNG, GIF',
'onboarding.step.upload.uploading': 'Téléversement...',
'onboarding.step.done.title': 'Tout est prêt !',
'onboarding.step.done.intro': 'Votre écran est apparié et le contenu est en lecture !',
'onboarding.step.done.whats_next': 'Et après ?',
'onboarding.step.done.next_content': 'Ajoutez plus de contenu dans la <strong>Bibliothèque</strong>',
'onboarding.step.done.next_layouts': 'Créez des mises en page multi-zones dans <strong>Mises en page</strong>',
'onboarding.step.done.next_schedule': 'Configurez un calendrier dans <strong>Calendrier</strong>',
'onboarding.step.done.next_widgets': 'Ajoutez des widgets en direct (horloge, météo, ticker) dans <strong>Widgets</strong>',
'onboarding.step.done.next_kiosk': 'Créez des écrans interactifs dans <strong>Kiosque</strong>',
'onboarding.step.done.next_designer': 'Concevez du contenu personnalisé dans le <strong>Concepteur</strong>',
'onboarding.toast.invalid_code': 'Saisissez un code valide à 6 chiffres',
'onboarding.toast.pairing': 'Appairage...',
'onboarding.toast.pair_failed': 'Échec de l\'appairage',
'onboarding.toast.pair_failed_with_error': 'Échec de l\'appairage : {error}',
'onboarding.toast.paired': 'Écran apparié !',
'onboarding.toast.uploaded_assigning': 'Téléversé ! Attribution à l\'écran...',
'onboarding.toast.content_assigned': 'Contenu téléversé et attribué !',
'onboarding.toast.upload_failed': 'Échec du téléversement',
'onboarding.toast.error_with_error': 'Erreur : {error}',
// Admin
'admin.title': 'Administration de la plateforme',
'admin.subtitle': 'Contrôles superadmin - vous seul pouvez voir ceci',
'admin.access_denied': 'Accès refusé',
'admin.access_denied_desc': 'Accès administrateur plateforme requis.',
'admin.all_users': 'Tous les utilisateurs',
'admin.plans': 'Plans d\'abonnement',
'admin.system': 'Système',
'admin.col.user': 'Utilisateur',
'admin.col.auth': 'Auth',
'admin.col.last_login': 'Dernière connexion',
'admin.col.role': 'Rôle',
'admin.col.plan': 'Plan',
'admin.col.actions': 'Actions',
'admin.col.devices': 'Appareils',
'admin.col.storage': 'Stockage',
'admin.col.monthly': 'Mensuel',
'admin.col.yearly': 'Annuel',
'admin.role.user': 'Utilisateur',
'admin.role.admin': 'Admin',
'admin.role.superadmin': 'Superadmin',
'admin.remove': 'Retirer',
'admin.owner': 'Propriétaire',
'admin.confirm': 'Confirmer ?',
'admin.total_users': '{n} utilisateurs au total',
'admin.unlimited': 'Illimité',
'admin.free': 'Gratuit',
'admin.version': 'Version',
'admin.frontend_hash': 'Hash du frontend',
'admin.download_db_backup': 'Télécharger la sauvegarde BDD',
'admin.server_status': 'État du serveur',
'admin.toast.role_updated': 'Rôle mis à jour',
'admin.toast.plan_updated': 'Plan mis à jour',
'admin.toast.user_removed': 'Utilisateur retiré',
};

View file

@ -687,4 +687,85 @@ export default {
'playlist.add_btn': 'Adicionar',
'playlist.adding': 'Adicionando...',
'playlist.added': 'Adicionado',
// Onboarding
'onboarding.back': 'Voltar',
'onboarding.next': 'Próximo',
'onboarding.skip': 'Pular assistente',
'onboarding.go_to_dashboard': 'Ir para o painel',
'onboarding.pair_display': 'Parear tela',
'onboarding.step.welcome.title': 'Bem-vindo ao ScreenTinker!',
'onboarding.step.welcome.intro': 'Vamos configurar tudo em menos de 5 minutos.',
'onboarding.step.welcome.guide_through': 'Este assistente irá guiá-lo através de:',
'onboarding.step.welcome.bullet_download': 'Baixar o app do player',
'onboarding.step.welcome.bullet_pair': 'Parear sua primeira tela',
'onboarding.step.welcome.bullet_upload': 'Enviar e atribuir conteúdo',
'onboarding.step.player.title': 'Passo 1: Obtenha o app do player',
'onboarding.step.player.intro': 'Instale o player no seu dispositivo de exibição.',
'onboarding.step.player.android_label': 'APK Android',
'onboarding.step.player.android_desc': 'TV boxes, tablets, Fire TV',
'onboarding.step.player.web_label': 'Player web',
'onboarding.step.player.web_desc': 'Qualquer navegador, Pi, ChromeOS',
'onboarding.step.player.url_hint': 'Abra o app na sua tela e digite esta URL do servidor:',
'onboarding.step.pair.title': 'Passo 2: Pareie sua tela',
'onboarding.step.pair.intro': 'Digite o código de 6 dígitos exibido na sua tela.',
'onboarding.step.pair.name_placeholder': 'Nome (ex. TV do lobby)',
'onboarding.step.upload.title': 'Passo 3: Envie conteúdo',
'onboarding.step.upload.intro': 'Envie um vídeo ou imagem para exibir.',
'onboarding.step.upload.click_to_select': 'Clique para selecionar um arquivo',
'onboarding.step.upload.formats': 'MP4, WebM, JPEG, PNG, GIF',
'onboarding.step.upload.uploading': 'Enviando...',
'onboarding.step.done.title': 'Tudo pronto!',
'onboarding.step.done.intro': 'Sua tela está pareada e o conteúdo está sendo exibido!',
'onboarding.step.done.whats_next': 'O que vem a seguir?',
'onboarding.step.done.next_content': 'Adicione mais conteúdo na <strong>Biblioteca de conteúdo</strong>',
'onboarding.step.done.next_layouts': 'Crie layouts multi-zona em <strong>Layouts</strong>',
'onboarding.step.done.next_schedule': 'Configure uma agenda no calendário <strong>Agenda</strong>',
'onboarding.step.done.next_widgets': 'Adicione widgets ao vivo (relógio, clima, ticker) em <strong>Widgets</strong>',
'onboarding.step.done.next_kiosk': 'Crie telas interativas em <strong>Quiosque</strong>',
'onboarding.step.done.next_designer': 'Crie conteúdo personalizado no <strong>Designer</strong>',
'onboarding.toast.invalid_code': 'Digite um código válido de 6 dígitos',
'onboarding.toast.pairing': 'Pareando...',
'onboarding.toast.pair_failed': 'Falha no pareamento',
'onboarding.toast.pair_failed_with_error': 'Falha no pareamento: {error}',
'onboarding.toast.paired': 'Tela pareada!',
'onboarding.toast.uploaded_assigning': 'Enviado! Atribuindo à tela...',
'onboarding.toast.content_assigned': 'Conteúdo enviado e atribuído!',
'onboarding.toast.upload_failed': 'Falha no envio',
'onboarding.toast.error_with_error': 'Erro: {error}',
// Admin
'admin.title': 'Administração da plataforma',
'admin.subtitle': 'Controles de superadmin - apenas você pode ver isso',
'admin.access_denied': 'Acesso negado',
'admin.access_denied_desc': 'Acesso de admin da plataforma necessário.',
'admin.all_users': 'Todos os usuários',
'admin.plans': 'Planos de assinatura',
'admin.system': 'Sistema',
'admin.col.user': 'Usuário',
'admin.col.auth': 'Auth',
'admin.col.last_login': 'Último login',
'admin.col.role': 'Função',
'admin.col.plan': 'Plano',
'admin.col.actions': 'Ações',
'admin.col.devices': 'Dispositivos',
'admin.col.storage': 'Armazenamento',
'admin.col.monthly': 'Mensal',
'admin.col.yearly': 'Anual',
'admin.role.user': 'Usuário',
'admin.role.admin': 'Admin',
'admin.role.superadmin': 'Superadmin',
'admin.remove': 'Remover',
'admin.owner': 'Proprietário',
'admin.confirm': 'Confirmar?',
'admin.total_users': '{n} usuários no total',
'admin.unlimited': 'Ilimitado',
'admin.free': 'Grátis',
'admin.version': 'Versão',
'admin.frontend_hash': 'Hash do frontend',
'admin.download_db_backup': 'Baixar backup do BD',
'admin.server_status': 'Status do servidor',
'admin.toast.role_updated': 'Função atualizada',
'admin.toast.plan_updated': 'Plano atualizado',
'admin.toast.user_removed': 'Usuário removido',
};

View file

@ -1,6 +1,7 @@
import { api } from '../api.js';
import { showToast } from '../components/toast.js';
import { esc } from '../utils.js';
import { t } from '../i18n.js';
const headers = () => ({ Authorization: `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json' });
const API = (url, opts = {}) => fetch('/api' + url, { headers: headers(), ...opts }).then(r => r.json());
@ -8,31 +9,28 @@ const API = (url, opts = {}) => fetch('/api' + url, { headers: headers(), ...opt
export async function render(container) {
const user = JSON.parse(localStorage.getItem('user') || '{}');
if (user.role !== 'superadmin') {
container.innerHTML = '<div class="empty-state"><h3>Access Denied</h3><p>Platform admin access required.</p></div>';
container.innerHTML = `<div class="empty-state"><h3>${t('admin.access_denied')}</h3><p>${t('admin.access_denied_desc')}</p></div>`;
return;
}
container.innerHTML = `
<div class="page-header">
<div><h1>Platform Admin</h1><div class="subtitle">Superadmin controls - only you can see this</div></div>
<div><h1>${t('admin.title')}</h1><div class="subtitle">${t('admin.subtitle')}</div></div>
</div>
<!-- All Users -->
<div class="settings-section">
<h3>All Users</h3>
<div id="allUsersTable"><p style="color:var(--text-muted)">Loading...</p></div>
<h3>${t('admin.all_users')}</h3>
<div id="allUsersTable"><p style="color:var(--text-muted)">${t('common.loading')}</p></div>
</div>
<!-- Plan Management -->
<div class="settings-section">
<h3>Subscription Plans</h3>
<div id="plansTable"><p style="color:var(--text-muted)">Loading...</p></div>
<h3>${t('admin.plans')}</h3>
<div id="plansTable"><p style="color:var(--text-muted)">${t('common.loading')}</p></div>
</div>
<!-- System Info -->
<div class="settings-section">
<h3>System</h3>
<div id="systemInfo"><p style="color:var(--text-muted)">Loading...</p></div>
<h3>${t('admin.system')}</h3>
<div id="systemInfo"><p style="color:var(--text-muted)">${t('common.loading')}</p></div>
</div>
`;
@ -51,24 +49,24 @@ async function loadUsers() {
<div class="table-wrap">
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:720px">
<thead><tr style="border-bottom:1px solid var(--border)">
<th style="padding:8px;text-align:left;color:var(--text-muted)">User</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">Auth</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">Last Login</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">Role</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">Plan</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">Actions</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">${t('admin.col.user')}</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">${t('admin.col.auth')}</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">${t('admin.col.last_login')}</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">${t('admin.col.role')}</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">${t('admin.col.plan')}</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">${t('admin.col.actions')}</th>
</tr></thead>
<tbody>
${users.map(u => `
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:8px"><div style="font-weight:500">${u.name || u.email}</div><div style="font-size:11px;color:var(--text-muted)">${u.email}</div></td>
<td style="padding:8px"><span style="background:var(--bg-primary);padding:2px 8px;border-radius:10px;font-size:11px">${u.auth_provider}</span></td>
<td style="padding:8px;font-size:11px;color:var(--text-muted)">${u.last_login ? new Date(u.last_login * 1000).toLocaleString() : 'Never'}</td>
<td style="padding:8px;font-size:11px;color:var(--text-muted)">${u.last_login ? new Date(u.last_login * 1000).toLocaleString() : t('common.never')}</td>
<td style="padding:8px">
<select class="input" style="max-width:120px;width:100%;background:var(--bg-input);font-size:12px;padding:4px" data-role-user="${u.id}">
<option value="user" ${u.role === 'user' ? 'selected' : ''}>User</option>
<option value="admin" ${u.role === 'admin' ? 'selected' : ''}>Admin</option>
<option value="superadmin" ${u.role === 'superadmin' ? 'selected' : ''}>Superadmin</option>
<option value="user" ${u.role === 'user' ? 'selected' : ''}>${t('admin.role.user')}</option>
<option value="admin" ${u.role === 'admin' ? 'selected' : ''}>${t('admin.role.admin')}</option>
<option value="superadmin" ${u.role === 'superadmin' ? 'selected' : ''}>${t('admin.role.superadmin')}</option>
</select>
</td>
<td style="padding:8px">
@ -77,47 +75,44 @@ async function loadUsers() {
</select>
</td>
<td style="padding:8px">
${u.role !== 'superadmin' ? `<button class="btn btn-danger btn-sm" data-delete-user="${u.id}">Remove</button>` : '<span style="color:var(--text-muted);font-size:11px">Owner</span>'}
${u.role !== 'superadmin' ? `<button class="btn btn-danger btn-sm" data-delete-user="${u.id}">${t('admin.remove')}</button>` : `<span style="color:var(--text-muted);font-size:11px">${t('admin.owner')}</span>`}
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<p style="color:var(--text-muted);font-size:11px;margin-top:8px">${users.length} total users</p>
<p style="color:var(--text-muted);font-size:11px;margin-top:8px">${t('admin.total_users', { n: users.length })}</p>
`;
// Role change
el.querySelectorAll('[data-role-user]').forEach(select => {
select.onchange = async () => {
try {
await API(`/auth/users/${select.dataset.roleUser}/role`, { method: 'PUT', body: JSON.stringify({ role: select.value }) });
showToast('Role updated', 'success');
showToast(t('admin.toast.role_updated'), 'success');
} catch (err) { showToast(err.message, 'error'); loadUsers(); }
};
});
// Plan change
el.querySelectorAll('[data-plan-user]').forEach(select => {
select.onchange = async () => {
try {
await API('/subscription/assign', { method: 'POST', body: JSON.stringify({ user_id: select.dataset.planUser, plan_id: select.value }) });
showToast('Plan updated', 'success');
showToast(t('admin.toast.plan_updated'), 'success');
} catch (err) { showToast(err.message, 'error'); loadUsers(); }
};
});
// Delete user
el.querySelectorAll('[data-delete-user]').forEach(btn => {
let confirming = false;
btn.onclick = async () => {
if (confirming) {
try { await api.deleteUser(btn.dataset.deleteUser); showToast('User removed', 'success'); loadUsers(); }
try { await api.deleteUser(btn.dataset.deleteUser); showToast(t('admin.toast.user_removed'), 'success'); loadUsers(); }
catch (err) { showToast(err.message, 'error'); }
return;
}
confirming = true; btn.textContent = 'Confirm?'; btn.style.background = 'var(--danger)'; btn.style.color = 'white';
setTimeout(() => { confirming = false; btn.textContent = 'Remove'; btn.style.background = ''; btn.style.color = ''; }, 3000);
confirming = true; btn.textContent = t('admin.confirm'); btn.style.background = 'var(--danger)'; btn.style.color = 'white';
setTimeout(() => { confirming = false; btn.textContent = t('admin.remove'); btn.style.background = ''; btn.style.color = ''; }, 3000);
};
});
} catch (err) { el.innerHTML = `<p style="color:var(--danger)">${esc(err.message)}</p>`; }
@ -131,19 +126,19 @@ async function loadPlans() {
<div class="table-wrap">
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:500px">
<thead><tr style="border-bottom:1px solid var(--border)">
<th style="padding:8px;text-align:left;color:var(--text-muted)">Plan</th>
<th style="padding:8px;text-align:right;color:var(--text-muted)">Devices</th>
<th style="padding:8px;text-align:right;color:var(--text-muted)">Storage</th>
<th style="padding:8px;text-align:right;color:var(--text-muted)">Monthly</th>
<th style="padding:8px;text-align:right;color:var(--text-muted)">Yearly</th>
<th style="padding:8px;text-align:left;color:var(--text-muted)">${t('admin.col.plan')}</th>
<th style="padding:8px;text-align:right;color:var(--text-muted)">${t('admin.col.devices')}</th>
<th style="padding:8px;text-align:right;color:var(--text-muted)">${t('admin.col.storage')}</th>
<th style="padding:8px;text-align:right;color:var(--text-muted)">${t('admin.col.monthly')}</th>
<th style="padding:8px;text-align:right;color:var(--text-muted)">${t('admin.col.yearly')}</th>
</tr></thead>
<tbody>
${plans.map(p => `
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:8px;font-weight:500">${p.display_name}</td>
<td style="padding:8px;text-align:right">${p.max_devices === -1 ? 'Unlimited' : p.max_devices}</td>
<td style="padding:8px;text-align:right">${p.max_storage_mb === -1 ? 'Unlimited' : p.max_storage_mb >= 1024 ? (p.max_storage_mb/1024)+'GB' : p.max_storage_mb+'MB'}</td>
<td style="padding:8px;text-align:right">${p.price_monthly > 0 ? '$'+p.price_monthly : 'Free'}</td>
<td style="padding:8px;text-align:right">${p.max_devices === -1 ? t('admin.unlimited') : p.max_devices}</td>
<td style="padding:8px;text-align:right">${p.max_storage_mb === -1 ? t('admin.unlimited') : p.max_storage_mb >= 1024 ? (p.max_storage_mb/1024)+'GB' : p.max_storage_mb+'MB'}</td>
<td style="padding:8px;text-align:right">${p.price_monthly > 0 ? '$'+p.price_monthly : t('admin.free')}</td>
<td style="padding:8px;text-align:right">${p.price_yearly > 0 ? '$'+p.price_yearly : '-'}</td>
</tr>
`).join('')}
@ -161,12 +156,12 @@ async function loadSystem() {
const token = localStorage.getItem('token');
el.innerHTML = `
<div class="info-grid">
<div class="info-card"><div class="info-card-label">Version</div><div class="info-card-value small">${version.version}</div></div>
<div class="info-card"><div class="info-card-label">Frontend Hash</div><div class="info-card-value small">${version.hash}</div></div>
<div class="info-card"><div class="info-card-label">${t('admin.version')}</div><div class="info-card-value small">${version.version}</div></div>
<div class="info-card"><div class="info-card-label">${t('admin.frontend_hash')}</div><div class="info-card-value small">${version.hash}</div></div>
</div>
<div style="display:flex;gap:8px;margin-top:16px">
<a href="/api/status/backup?token=${token}" class="btn btn-secondary btn-sm" style="text-decoration:none">Download DB Backup</a>
<a href="/api/status" target="_blank" class="btn btn-secondary btn-sm" style="text-decoration:none">Server Status</a>
<a href="/api/status/backup?token=${token}" class="btn btn-secondary btn-sm" style="text-decoration:none">${t('admin.download_db_backup')}</a>
<a href="/api/status" target="_blank" class="btn btn-secondary btn-sm" style="text-decoration:none">${t('admin.server_status')}</a>
</div>
`;
} catch (err) { el.innerHTML = `<p style="color:var(--danger)">${esc(err.message)}</p>`; }

View file

@ -1,96 +1,101 @@
import { showToast } from '../components/toast.js';
import { t } from '../i18n.js';
const STEPS = [
// Steps are computed lazily so translated strings refresh on language change.
function getSteps() {
return [
{
title: 'Welcome to ScreenTinker!',
title: t('onboarding.step.welcome.title'),
icon: '&#128075;',
content: `<p style="font-size:16px;color:var(--text-secondary);margin-bottom:16px">Let's get you set up in under 5 minutes.</p>
<p style="color:var(--text-muted);font-size:14px">This wizard will guide you through:</p>
content: `<p style="font-size:16px;color:var(--text-secondary);margin-bottom:16px">${t('onboarding.step.welcome.intro')}</p>
<p style="color:var(--text-muted);font-size:14px">${t('onboarding.step.welcome.guide_through')}</p>
<ul style="color:var(--text-muted);font-size:14px;padding-left:20px;margin-top:8px;line-height:2">
<li>Downloading the player app</li>
<li>Pairing your first display</li>
<li>Uploading and assigning content</li>
<li>${t('onboarding.step.welcome.bullet_download')}</li>
<li>${t('onboarding.step.welcome.bullet_pair')}</li>
<li>${t('onboarding.step.welcome.bullet_upload')}</li>
</ul>`,
action: null
},
{
title: 'Step 1: Get the Player App',
title: t('onboarding.step.player.title'),
icon: '&#128229;',
content: `<p style="color:var(--text-secondary);margin-bottom:16px">Install the player on your display device.</p>
content: `<p style="color:var(--text-secondary);margin-bottom:16px">${t('onboarding.step.player.intro')}</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<a href="/download/apk" style="background:var(--bg-input);border:1px solid var(--border);border-radius:8px;padding:16px;text-align:center;text-decoration:none;color:var(--text-primary)">
<div style="font-size:32px;margin-bottom:8px">&#129302;</div>
<div style="font-weight:600;font-size:14px">Android APK</div>
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">TV boxes, tablets, Fire TV</div>
<div style="font-weight:600;font-size:14px">${t('onboarding.step.player.android_label')}</div>
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">${t('onboarding.step.player.android_desc')}</div>
</a>
<a href="/player" target="_blank" style="background:var(--bg-input);border:1px solid var(--border);border-radius:8px;padding:16px;text-align:center;text-decoration:none;color:var(--text-primary)">
<div style="font-size:32px;margin-bottom:8px">&#127760;</div>
<div style="font-weight:600;font-size:14px">Web Player</div>
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">Any browser, Pi, ChromeOS</div>
<div style="font-weight:600;font-size:14px">${t('onboarding.step.player.web_label')}</div>
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">${t('onboarding.step.player.web_desc')}</div>
</a>
</div>
<p style="color:var(--text-muted);font-size:12px;margin-top:12px">Open the app on your display and enter this server URL:</p>
<p style="color:var(--text-muted);font-size:12px;margin-top:12px">${t('onboarding.step.player.url_hint')}</p>
<code style="display:block;background:var(--bg-input);padding:10px;border-radius:6px;margin-top:6px;font-size:14px;user-select:all">${window.location.origin}</code>`,
action: null
},
{
title: 'Step 2: Pair Your Display',
title: t('onboarding.step.pair.title'),
icon: '&#128279;',
content: `<p style="color:var(--text-secondary);margin-bottom:16px">Enter the 6-digit code shown on your display.</p>
content: `<p style="color:var(--text-secondary);margin-bottom:16px">${t('onboarding.step.pair.intro')}</p>
<div style="text-align:center;margin:20px 0">
<input type="text" id="onboardPairingCode" maxlength="6" pattern="[0-9]{6}" placeholder="000000"
style="max-width:240px;width:100%;padding:16px;background:var(--bg-input);border:1px solid var(--border);border-radius:8px;
color:var(--text-primary);font-size:32px;font-weight:700;text-align:center;letter-spacing:8px;font-family:monospace">
</div>
<div style="text-align:center">
<input type="text" id="onboardDeviceName" placeholder="Display name (e.g., Lobby TV)"
<input type="text" id="onboardDeviceName" placeholder="${t('onboarding.step.pair.name_placeholder')}"
style="max-width:240px;width:100%;padding:10px;background:var(--bg-input);border:1px solid var(--border);border-radius:8px;color:var(--text-primary);font-size:14px;text-align:center">
</div>
<p id="onboardPairStatus" style="color:var(--text-muted);font-size:13px;text-align:center;margin-top:12px"></p>`,
action: 'pair'
},
{
title: 'Step 3: Upload Content',
title: t('onboarding.step.upload.title'),
icon: '&#128228;',
content: `<p style="color:var(--text-secondary);margin-bottom:16px">Upload a video or image to display.</p>
content: `<p style="color:var(--text-secondary);margin-bottom:16px">${t('onboarding.step.upload.intro')}</p>
<div style="border:2px dashed var(--border);border-radius:12px;padding:32px;text-align:center;cursor:pointer" id="onboardUploadArea">
<div style="font-size:32px;margin-bottom:8px">&#128193;</div>
<p style="color:var(--text-secondary)">Click to select a file</p>
<p style="color:var(--text-muted);font-size:12px;margin-top:4px">MP4, WebM, JPEG, PNG, GIF</p>
<p style="color:var(--text-secondary)">${t('onboarding.step.upload.click_to_select')}</p>
<p style="color:var(--text-muted);font-size:12px;margin-top:4px">${t('onboarding.step.upload.formats')}</p>
<input type="file" id="onboardFileInput" style="display:none" accept="video/*,image/*">
</div>
<div id="onboardUploadProgress" style="display:none;margin-top:12px">
<div style="height:4px;background:var(--bg-primary);border-radius:2px;overflow:hidden">
<div id="onboardProgressBar" style="height:100%;background:var(--accent);width:0%;transition:width 0.3s"></div>
</div>
<p id="onboardUploadText" style="font-size:12px;color:var(--text-muted);margin-top:6px">Uploading...</p>
<p id="onboardUploadText" style="font-size:12px;color:var(--text-muted);margin-top:6px">${t('onboarding.step.upload.uploading')}</p>
</div>`,
action: 'upload'
},
{
title: "You're All Set!",
title: t('onboarding.step.done.title'),
icon: '&#127881;',
content: `<p style="font-size:16px;color:var(--text-secondary);margin-bottom:20px">Your display is paired and content is playing!</p>
content: `<p style="font-size:16px;color:var(--text-secondary);margin-bottom:20px">${t('onboarding.step.done.intro')}</p>
<div style="background:var(--bg-input);border-radius:8px;padding:16px;margin-bottom:16px">
<p style="font-size:14px;color:var(--text-primary);font-weight:600;margin-bottom:8px">What's next?</p>
<p style="font-size:14px;color:var(--text-primary);font-weight:600;margin-bottom:8px">${t('onboarding.step.done.whats_next')}</p>
<ul style="color:var(--text-muted);font-size:13px;padding-left:20px;line-height:2">
<li>Add more content in the <strong>Content Library</strong></li>
<li>Create multi-zone layouts in <strong>Layouts</strong></li>
<li>Set up a schedule in the <strong>Schedule</strong> calendar</li>
<li>Add live widgets (clock, weather, ticker) in <strong>Widgets</strong></li>
<li>Create interactive screens in <strong>Kiosk</strong></li>
<li>Design custom content in the <strong>Designer</strong></li>
<li>${t('onboarding.step.done.next_content')}</li>
<li>${t('onboarding.step.done.next_layouts')}</li>
<li>${t('onboarding.step.done.next_schedule')}</li>
<li>${t('onboarding.step.done.next_widgets')}</li>
<li>${t('onboarding.step.done.next_kiosk')}</li>
<li>${t('onboarding.step.done.next_designer')}</li>
</ul>
</div>`,
action: null
}
];
}
export function render(container) {
let currentStep = 0;
let pairedDeviceId = null;
function renderStep() {
const STEPS = getSteps();
const step = STEPS[currentStep];
const isFirst = currentStep === 0;
const isLast = currentStep === STEPS.length - 1;
@ -113,17 +118,16 @@ export function render(container) {
</div>
<div style="display:flex;justify-content:space-between">
${isFirst ? '<div></div>' : `<button class="btn btn-secondary" id="prevBtn">Back</button>`}
${isFirst ? '<div></div>' : `<button class="btn btn-secondary" id="prevBtn">${t('onboarding.back')}</button>`}
<div style="display:flex;gap:8px">
${!isLast ? `<button class="btn btn-secondary" id="skipBtn" style="color:var(--text-muted)">Skip Wizard</button>` : ''}
<button class="btn btn-primary" id="nextBtn">${isLast ? 'Go to Dashboard' : step.action ? (step.action === 'pair' ? 'Pair Display' : 'Next') : 'Next'}</button>
${!isLast ? `<button class="btn btn-secondary" id="skipBtn" style="color:var(--text-muted)">${t('onboarding.skip')}</button>` : ''}
<button class="btn btn-primary" id="nextBtn">${isLast ? t('onboarding.go_to_dashboard') : step.action === 'pair' ? t('onboarding.pair_display') : t('onboarding.next')}</button>
</div>
</div>
</div>
</div>
`;
// Bind buttons
document.getElementById('prevBtn')?.addEventListener('click', () => { currentStep--; renderStep(); });
document.getElementById('skipBtn')?.addEventListener('click', () => {
localStorage.setItem('rd_onboarded', 'true');
@ -132,7 +136,6 @@ export function render(container) {
});
document.getElementById('nextBtn')?.addEventListener('click', handleNext);
// Step-specific setup
if (step.action === 'upload') {
const area = document.getElementById('onboardUploadArea');
const input = document.getElementById('onboardFileInput');
@ -142,6 +145,7 @@ export function render(container) {
}
async function handleNext() {
const STEPS = getSteps();
const step = STEPS[currentStep];
if (step.action === 'pair') {
@ -150,12 +154,12 @@ export function render(container) {
const status = document.getElementById('onboardPairStatus');
if (!code || code.length !== 6) {
if (status) status.textContent = 'Enter a valid 6-digit code';
if (status) status.textContent = t('onboarding.toast.invalid_code');
return;
}
try {
if (status) status.textContent = 'Pairing...';
if (status) status.textContent = t('onboarding.toast.pairing');
const token = localStorage.getItem('token');
const res = await fetch('/api/provision/pair', {
method: 'POST',
@ -163,13 +167,13 @@ export function render(container) {
body: JSON.stringify({ pairing_code: code, name: name || undefined })
});
const data = await res.json();
if (!res.ok) { if (status) status.textContent = data.error || 'Pairing failed'; return; }
if (!res.ok) { if (status) status.textContent = data.error || t('onboarding.toast.pair_failed'); return; }
pairedDeviceId = data.id;
showToast('Display paired!', 'success');
showToast(t('onboarding.toast.paired'), 'success');
currentStep++;
renderStep();
} catch (err) {
if (status) status.textContent = 'Pairing failed: ' + err.message;
if (status) status.textContent = t('onboarding.toast.pair_failed_with_error', { error: err.message });
}
return;
}
@ -208,9 +212,8 @@ export function render(container) {
xhr.onload = async () => {
if (xhr.status >= 200 && xhr.status < 300) {
const content = JSON.parse(xhr.responseText);
if (text) text.textContent = 'Uploaded! Assigning to display...';
if (text) text.textContent = t('onboarding.toast.uploaded_assigning');
// Auto-assign to paired device
if (pairedDeviceId) {
try {
await fetch(`/api/assignments/device/${pairedDeviceId}`, {
@ -221,17 +224,17 @@ export function render(container) {
} catch {}
}
showToast('Content uploaded and assigned!', 'success');
showToast(t('onboarding.toast.content_assigned'), 'success');
currentStep++;
renderStep();
} else {
if (text) text.textContent = 'Upload failed';
if (text) text.textContent = t('onboarding.toast.upload_failed');
}
};
xhr.onerror = () => { if (text) text.textContent = 'Upload failed'; };
xhr.onerror = () => { if (text) text.textContent = t('onboarding.toast.upload_failed'); };
xhr.send(formData);
} catch (err) {
if (text) text.textContent = 'Error: ' + err.message;
if (text) text.textContent = t('onboarding.toast.error_with_error', { error: err.message });
}
}