- Replace SVG favicon with PNG icon - Update base.html link ref Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
550 lines
26 KiB
HTML
550 lines
26 KiB
HTML
{% import "partials/icons.html" as ico %}
|
||
<!DOCTYPE html>
|
||
<html lang="en" data-theme="auto">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>{% block head_title %}ImpTune{% endblock %}</title>
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<link rel="stylesheet" href="/static/pico.min.css">
|
||
<link rel="stylesheet" href="/static/app.css">
|
||
<script>
|
||
document.addEventListener('alpine:init', () => {
|
||
// Theme store: cycles Light -> Dark -> System, persists in localStorage
|
||
Alpine.store('theme', {
|
||
current: localStorage.getItem('imptune_theme') || 'auto',
|
||
icons: {
|
||
light: '<svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M18.4 5.6L17 7M7 17l-1.4 1.4"/></svg>',
|
||
dark: '<svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5Z"/></svg>',
|
||
auto: '<svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="12" cy="12" r="8"/><path d="M12 4v16a8 8 0 0 0 0-16Z" fill="currentColor" stroke="none"/></svg>'
|
||
},
|
||
init() {
|
||
document.documentElement.setAttribute('data-theme', this.current);
|
||
},
|
||
cycle() {
|
||
const order = ['light', 'dark', 'auto'];
|
||
this.current = order[(order.indexOf(this.current) + 1) % order.length];
|
||
localStorage.setItem('imptune_theme', this.current);
|
||
document.documentElement.setAttribute('data-theme', this.current);
|
||
}
|
||
});
|
||
|
||
// Client-side list filter. Lives in a store so rows swapped in by HTMX
|
||
// (partials/printer_list.html) find it no matter which page included them.
|
||
Alpine.store('filter', { q: '' });
|
||
|
||
// i18n store: FR/EN toggle, persists in localStorage
|
||
Alpine.store('i18n', {
|
||
lang: (() => {
|
||
const saved = localStorage.getItem('imptune_lang');
|
||
if (saved) return saved;
|
||
return navigator.language && navigator.language.startsWith('en') ? 'en' : 'fr';
|
||
})(),
|
||
t(key) {
|
||
return (this.translations[this.lang] || {})[key] || key;
|
||
},
|
||
toggle() {
|
||
this.lang = this.lang === 'fr' ? 'en' : 'fr';
|
||
localStorage.setItem('imptune_lang', this.lang);
|
||
},
|
||
translations: {
|
||
fr: {
|
||
dashboard: 'Tableau de bord',
|
||
drivers: 'Pilotes',
|
||
printers: 'Imprimantes',
|
||
clients: 'Clients',
|
||
packages: 'Paquets',
|
||
add_printer: 'Ajouter une imprimante',
|
||
add_client: 'Ajouter un client',
|
||
printer_library: 'Bibliothèque d’imprimantes',
|
||
edit: 'Modifier',
|
||
delete: 'Supprimer',
|
||
save: 'Enregistrer',
|
||
cancel: 'Annuler',
|
||
upload_driver: 'Importer un pilote',
|
||
printer_name: 'Nom de l’imprimante',
|
||
ip_address: 'Adresse IP',
|
||
port_name: 'Nom du port',
|
||
driver: 'Pilote',
|
||
duplex_mode: 'Mode recto-verso',
|
||
one_sided: 'Recto simple',
|
||
long_edge: 'Grand côté',
|
||
short_edge: 'Petit côté',
|
||
color_mode: 'Couleur',
|
||
paper_size: 'Format papier',
|
||
collate: 'Assembler',
|
||
client: 'Client',
|
||
edit_printer: 'Modifier l’imprimante',
|
||
no_printers: 'Aucune imprimante configurée.',
|
||
no_clients: 'Aucun client configuré.',
|
||
client_list: 'Liste des clients',
|
||
name: 'Nom',
|
||
created: 'Créé le',
|
||
back_to_printers: 'Retour aux imprimantes',
|
||
theme_label: 'Thème',
|
||
lang_label: 'FR',
|
||
// Shell
|
||
brand_sub: 'Paquets d’impression',
|
||
nav_library: 'Bibliothèque',
|
||
nav_deploy: 'Déploiement',
|
||
session_menu: 'Cette session',
|
||
session_download_key: 'Télécharger ma clé de secours',
|
||
session_restore_key: 'Restaurer depuis une clé',
|
||
session_note: 'Vos imprimantes sont liées à ce navigateur. La clé de secours les récupère ailleurs.',
|
||
ephemeral_warning: 'Cette session ne dure que tant que ce navigateur reste ouvert : vos imprimantes et réglages sont bien enregistrés, mais le lien vers eux est perdu à la fermeture. Téléchargez votre clé de secours pour les conserver.',
|
||
single_user_notice: 'Mode mono-utilisateur : tout est enregistré sur le serveur sans cookie de session, et toute personne qui accède à cette application voit les mêmes imprimantes.',
|
||
open_menu: 'Ouvrir le menu',
|
||
// Dashboard
|
||
dashboard_intro: 'Trois étapes du fichier ZIP au paquet déployable.',
|
||
step_driver_title: 'Importer le pilote',
|
||
step_driver_desc: 'Un ZIP contenant le .inf et ses fichiers. ImpTune lit le .inf et détecte les modèles.',
|
||
step_printer_title: 'Configurer l’imprimante',
|
||
step_printer_desc: 'Nom, adresse IP, pilote et réglages par défaut du poste.',
|
||
step_package_title: 'Exporter le paquet',
|
||
step_package_desc: '.intunewin pour Intune, ZIP pour NinjaRMM. Scripts inclus.',
|
||
step_done: 'Fait',
|
||
go_upload_driver: 'Importer un pilote',
|
||
go_add_printer: 'Ajouter une imprimante',
|
||
go_export: 'Voir les paquets',
|
||
view_all: 'Tout voir',
|
||
recent_activity: 'Activité récente',
|
||
recent_printers: 'Imprimantes récentes',
|
||
recent_packages: 'Prêtes à exporter',
|
||
no_packages: 'Aucune imprimante prête à exporter.',
|
||
ready_to_export: 'Prête',
|
||
needs_driver: 'Pilote manquant',
|
||
// Printers new
|
||
add_printer_title: 'Ajouter une imprimante',
|
||
back_to_printer_library: 'Imprimantes',
|
||
save_printer: 'Enregistrer l’imprimante',
|
||
upload_new_driver: 'Importer un nouveau pilote',
|
||
no_driver_option: 'Aucun pilote',
|
||
unassigned_option: 'Non assigné',
|
||
section_identity: 'Identification',
|
||
section_connection: 'Connexion réseau',
|
||
section_driver: 'Pilote',
|
||
section_defaults: 'Réglages par défaut',
|
||
hint_port: 'Dérivé automatiquement de l’adresse IP. Modifiable.',
|
||
hint_name: 'Le nom vu par l’utilisateur sur son poste.',
|
||
hint_driver: 'Sans pilote, l’imprimante est enregistrée mais l’export reste bloqué.',
|
||
hint_ip: 'Adresse IPv4 de l’imprimante sur le réseau du client.',
|
||
// Printer list
|
||
th_name: 'Nom',
|
||
th_ip: 'Adresse IP',
|
||
th_port: 'Port',
|
||
th_driver: 'Pilote',
|
||
th_duplex: 'Recto-verso',
|
||
th_color: 'Couleur',
|
||
th_paper: 'Format',
|
||
th_collate: 'Assemblage',
|
||
th_actions: 'Actions',
|
||
th_status: 'État',
|
||
th_config: 'Réglages',
|
||
th_export: 'Export',
|
||
th_printers: 'Imprimantes',
|
||
th_used_by: 'Utilisé par',
|
||
th_size: 'Taille',
|
||
yes: 'Oui',
|
||
no: 'Non',
|
||
no_driver_assigned: '—',
|
||
filter_printers: 'Filtrer par nom, IP ou client',
|
||
no_match: 'Aucune imprimante ne correspond au filtre.',
|
||
printers_intro: 'Une imprimante prête a un pilote assigné : elle peut être exportée.',
|
||
// Clients
|
||
client_name_label: 'Nom du client',
|
||
add_client_section: 'Ajouter un client',
|
||
client_list_section: 'Liste des clients',
|
||
clients_intro: 'Les clients regroupent les imprimantes par site ou par organisation.',
|
||
open_client: 'Ouvrir',
|
||
// Client detail
|
||
back_to_clients: 'Clients',
|
||
printers_section: 'Imprimantes',
|
||
// Drivers
|
||
drivers_title: 'Pilotes',
|
||
upload_driver_section: 'Importer un package de pilote',
|
||
driver_library: 'Bibliothèque de pilotes',
|
||
uploading: 'Import en cours…',
|
||
upload_btn: 'Importer',
|
||
driver_filename: 'Fichier',
|
||
driver_names_col: 'Nom(s) du pilote',
|
||
architecture: 'Architecture',
|
||
uploaded_at: 'Importé le',
|
||
unknown: 'Inconnu',
|
||
no_drivers: 'Aucun pilote importé.',
|
||
drivers_intro: 'Les pilotes sont partagés : tout le monde voit la même bibliothèque.',
|
||
driver_package_label: 'Package de pilote (ZIP)',
|
||
hint_driver_zip: 'Le ZIP doit contenir le fichier .inf et tous les fichiers auxquels il renvoie.',
|
||
unused_files: 'fichier(s) du ZIP ne sont pas référencés par le .inf.',
|
||
show_unused: 'Voir ces fichiers',
|
||
// Packages
|
||
packages_title: 'Paquets',
|
||
packages_description: 'Imprimantes avec un pilote assigné — prêtes pour l’export.',
|
||
printer_col: 'Imprimante',
|
||
client_col: 'Client',
|
||
driver_col: 'Pilote',
|
||
downloads_col: 'Téléchargements',
|
||
no_packages_ready: 'Aucune imprimante prête. Assignez un pilote pour activer l’export.',
|
||
format_intune: 'Intune',
|
||
format_ninja: 'NinjaRMM',
|
||
hint_intunewin: '.intunewin — à téléverser dans Intune comme application Win32.',
|
||
hint_ninja: 'ZIP — scripts en clair pour NinjaRMM ou exécution manuelle.',
|
||
pending_printers: 'imprimante(s) attendent encore un pilote',
|
||
// Printer detail
|
||
configuration: 'Configuration',
|
||
duplex_mode_label: 'Mode recto-verso',
|
||
color_mode_label: 'Mode couleur',
|
||
color_value: 'Couleur',
|
||
grayscale_value: 'Niveaux de gris',
|
||
paper_size_label: 'Format papier',
|
||
collate_label: 'Assemblage',
|
||
client_label: 'Client',
|
||
unassigned: 'Non assigné',
|
||
driver_section: 'Pilote',
|
||
package_label: 'Package',
|
||
driver_names_label: 'Nom(s) du pilote',
|
||
architecture_label: 'Architecture',
|
||
no_driver_detail: 'Aucun pilote assigné.',
|
||
assign_driver_cta: 'Assigner un pilote',
|
||
intune_commands: 'Commandes Intune',
|
||
hint_intune_cmds: 'À coller dans les champs d’installation et de désinstallation de l’application Win32.',
|
||
install_cmd_label: 'Commande d’installation',
|
||
uninstall_cmd_label: 'Commande de désinstallation',
|
||
copy: 'Copier',
|
||
copied: 'Copié',
|
||
scripts_section: 'Scripts PowerShell',
|
||
download_install: 'install.ps1',
|
||
download_uninstall: 'uninstall.ps1',
|
||
download_detect: 'detect.ps1',
|
||
export_section: 'Déployer',
|
||
download_ninja: 'Télécharger le ZIP NinjaRMM',
|
||
download_intunewin: 'Télécharger le .intunewin',
|
||
icon_section: 'Icône',
|
||
icon_uploaded: 'Icône enregistrée',
|
||
upload_icon: 'Téléverser l’icône',
|
||
replace_icon: 'Remplacer l’icône',
|
||
no_icon: 'Aucune icône. Intune affichera l’icône par défaut.',
|
||
hint_icon: 'PNG, exactement 256 × 256, 750 Ko maximum.',
|
||
back_to_printers_btn: 'Retour aux imprimantes',
|
||
export_locked: 'Assignez un pilote à cette imprimante pour débloquer les scripts et l’export.',
|
||
// Edit modal
|
||
edit_printer_title: 'Modifier l’imprimante',
|
||
// Session restore
|
||
restore_title: 'Restaurer une clé de secours',
|
||
restore_desc: 'Collez la clé du fichier imptune-backup-key.txt pour retrouver vos imprimantes, réglages et clients dans ce navigateur.',
|
||
backup_key_label: 'Clé de secours',
|
||
restore_btn: 'Restaurer'
|
||
},
|
||
en: {
|
||
dashboard: 'Dashboard',
|
||
drivers: 'Drivers',
|
||
printers: 'Printers',
|
||
clients: 'Clients',
|
||
packages: 'Packages',
|
||
add_printer: 'Add printer',
|
||
add_client: 'Add client',
|
||
printer_library: 'Printer Library',
|
||
edit: 'Edit',
|
||
delete: 'Delete',
|
||
save: 'Save',
|
||
cancel: 'Cancel',
|
||
upload_driver: 'Add driver',
|
||
printer_name: 'Printer name',
|
||
ip_address: 'IP address',
|
||
port_name: 'Port name',
|
||
driver: 'Driver',
|
||
duplex_mode: 'Duplex mode',
|
||
one_sided: 'One-sided',
|
||
long_edge: 'Long edge',
|
||
short_edge: 'Short edge',
|
||
color_mode: 'Color',
|
||
paper_size: 'Paper size',
|
||
collate: 'Collate',
|
||
client: 'Client',
|
||
edit_printer: 'Edit printer',
|
||
no_printers: 'No printers configured yet.',
|
||
no_clients: 'No clients configured yet.',
|
||
client_list: 'Client List',
|
||
name: 'Name',
|
||
created: 'Created',
|
||
back_to_printers: 'Back to Printers',
|
||
theme_label: 'Theme',
|
||
lang_label: 'EN',
|
||
// Shell
|
||
brand_sub: 'Print packaging',
|
||
nav_library: 'Library',
|
||
nav_deploy: 'Deploy',
|
||
session_menu: 'This session',
|
||
session_download_key: 'Download my backup key',
|
||
session_restore_key: 'Restore from a key',
|
||
session_note: 'Your printers live in this browser. The backup key brings them back elsewhere.',
|
||
ephemeral_warning: 'This session lasts only while this browser stays open — your printers and configs are saved, but the link to them is lost when you close it. Download your backup key to keep them.',
|
||
single_user_notice: 'Single-user mode: everything is stored on the server without a session cookie, and anyone who can reach this app sees the same printers.',
|
||
open_menu: 'Open menu',
|
||
// Dashboard
|
||
dashboard_intro: 'Three steps from driver ZIP to deployable package.',
|
||
step_driver_title: 'Add the driver',
|
||
step_driver_desc: 'A ZIP holding the .inf and its files. ImpTune reads the .inf and finds the models.',
|
||
step_printer_title: 'Configure the printer',
|
||
step_printer_desc: 'Name, IP address, driver, and the defaults the workstation gets.',
|
||
step_package_title: 'Export the package',
|
||
step_package_desc: '.intunewin for Intune, ZIP for NinjaRMM. Scripts included.',
|
||
step_done: 'Done',
|
||
go_upload_driver: 'Add a driver',
|
||
go_add_printer: 'Add a printer',
|
||
go_export: 'View packages',
|
||
view_all: 'View all',
|
||
recent_activity: 'Recent activity',
|
||
recent_printers: 'Recent printers',
|
||
recent_packages: 'Ready to export',
|
||
no_packages: 'No printers are ready to export yet.',
|
||
ready_to_export: 'Ready',
|
||
needs_driver: 'Driver needed',
|
||
// Printers new
|
||
add_printer_title: 'Add printer',
|
||
back_to_printer_library: 'Printers',
|
||
save_printer: 'Save printer',
|
||
upload_new_driver: 'Add a new driver',
|
||
no_driver_option: 'No driver',
|
||
unassigned_option: 'Unassigned',
|
||
section_identity: 'Identity',
|
||
section_connection: 'Network connection',
|
||
section_driver: 'Driver',
|
||
section_defaults: 'Print defaults',
|
||
hint_port: 'Derived from the IP address. Edit it if your naming differs.',
|
||
hint_name: 'The name the user sees on their workstation.',
|
||
hint_driver: 'Without a driver the printer is saved, but export stays locked.',
|
||
hint_ip: 'The printer IPv4 address on the client network.',
|
||
// Printer list
|
||
th_name: 'Name',
|
||
th_ip: 'IP address',
|
||
th_port: 'Port',
|
||
th_driver: 'Driver',
|
||
th_duplex: 'Duplex',
|
||
th_color: 'Color',
|
||
th_paper: 'Paper',
|
||
th_collate: 'Collate',
|
||
th_actions: 'Actions',
|
||
th_status: 'Status',
|
||
th_config: 'Defaults',
|
||
th_export: 'Export',
|
||
th_printers: 'Printers',
|
||
th_used_by: 'Used by',
|
||
th_size: 'Size',
|
||
yes: 'Yes',
|
||
no: 'No',
|
||
no_driver_assigned: '—',
|
||
filter_printers: 'Filter by name, IP, or client',
|
||
no_match: 'No printer matches this filter.',
|
||
printers_intro: 'A printer with a driver assigned is ready to export.',
|
||
// Clients
|
||
client_name_label: 'Client name',
|
||
add_client_section: 'Add client',
|
||
client_list_section: 'Client list',
|
||
clients_intro: 'Clients group printers by site or organization.',
|
||
open_client: 'Open',
|
||
// Client detail
|
||
back_to_clients: 'Clients',
|
||
printers_section: 'Printers',
|
||
// Drivers
|
||
drivers_title: 'Drivers',
|
||
upload_driver_section: 'Add a driver package',
|
||
driver_library: 'Driver library',
|
||
uploading: 'Uploading…',
|
||
upload_btn: 'Upload',
|
||
driver_filename: 'File',
|
||
driver_names_col: 'Driver name(s)',
|
||
architecture: 'Architecture',
|
||
uploaded_at: 'Added',
|
||
unknown: 'Unknown',
|
||
no_drivers: 'No drivers uploaded yet.',
|
||
drivers_intro: 'Drivers are shared — everyone sees the same library.',
|
||
driver_package_label: 'Driver package (ZIP)',
|
||
hint_driver_zip: 'The ZIP must contain the .inf file and every file it references.',
|
||
unused_files: 'file(s) in the ZIP are unused — the .inf never references them.',
|
||
show_unused: 'Show these files',
|
||
// Packages
|
||
packages_title: 'Packages',
|
||
packages_description: 'Printers with a driver assigned — ready for export.',
|
||
printer_col: 'Printer',
|
||
client_col: 'Client',
|
||
driver_col: 'Driver',
|
||
downloads_col: 'Downloads',
|
||
no_packages_ready: 'No printer is ready yet. Assign a driver to enable export.',
|
||
format_intune: 'Intune',
|
||
format_ninja: 'NinjaRMM',
|
||
hint_intunewin: '.intunewin — upload to Intune as a Win32 app.',
|
||
hint_ninja: 'ZIP — plain scripts for NinjaRMM or a manual run.',
|
||
pending_printers: 'printer(s) still need a driver',
|
||
// Printer detail
|
||
configuration: 'Configuration',
|
||
duplex_mode_label: 'Duplex mode',
|
||
color_mode_label: 'Color mode',
|
||
color_value: 'Color',
|
||
grayscale_value: 'Grayscale',
|
||
paper_size_label: 'Paper size',
|
||
collate_label: 'Collate',
|
||
client_label: 'Client',
|
||
unassigned: 'Unassigned',
|
||
driver_section: 'Driver',
|
||
package_label: 'Package',
|
||
driver_names_label: 'Driver name(s)',
|
||
architecture_label: 'Architecture',
|
||
no_driver_detail: 'No driver assigned.',
|
||
assign_driver_cta: 'Assign a driver',
|
||
intune_commands: 'Intune commands',
|
||
hint_intune_cmds: 'Paste these into the Win32 app install and uninstall fields.',
|
||
install_cmd_label: 'Install command',
|
||
uninstall_cmd_label: 'Uninstall command',
|
||
copy: 'Copy',
|
||
copied: 'Copied',
|
||
scripts_section: 'PowerShell scripts',
|
||
download_install: 'install.ps1',
|
||
download_uninstall: 'uninstall.ps1',
|
||
download_detect: 'detect.ps1',
|
||
export_section: 'Deploy',
|
||
download_ninja: 'Download NinjaRMM ZIP',
|
||
download_intunewin: 'Download .intunewin',
|
||
icon_section: 'Icon',
|
||
icon_uploaded: 'Icon saved',
|
||
upload_icon: 'Upload icon',
|
||
replace_icon: 'Replace icon',
|
||
no_icon: 'No icon. Intune will show its default.',
|
||
hint_icon: 'PNG, exactly 256 × 256, 750 KB max.',
|
||
back_to_printers_btn: 'Back to printers',
|
||
export_locked: 'Assign a driver to this printer to unlock scripts and export.',
|
||
// Edit modal
|
||
edit_printer_title: 'Edit printer',
|
||
// Session restore
|
||
restore_title: 'Restore a backup key',
|
||
restore_desc: 'Paste the key from imptune-backup-key.txt to get your printers, settings, and clients back in this browser.',
|
||
backup_key_label: 'Backup key',
|
||
restore_btn: 'Restore'
|
||
}
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
<script defer src="/static/alpine.min.js"></script>
|
||
<script src="/static/htmx.min.js"></script>
|
||
</head>
|
||
<body x-data="{ nav: false }" :class="nav && 'nav-open'">
|
||
<div class="layout">
|
||
<div class="nav-scrim" x-show="nav" @click="nav = false" style="display:none"></div>
|
||
<nav class="sidebar" @click="nav = false">
|
||
<a class="brand" href="/">
|
||
<span class="brand-mark">iT</span>
|
||
<span>
|
||
<span class="brand-name">ImpTune</span>
|
||
<span class="brand-sub" x-data x-text="$store.i18n.t('brand_sub')">Print packaging</span>
|
||
</span>
|
||
</a>
|
||
|
||
<div class="nav-group">
|
||
<ul class="sidebar-nav">
|
||
<li><a href="/" {% if request.url.path == "/" %}class="active"{% endif %}>{{ ico.i('gauge') }}<span x-data x-text="$store.i18n.t('dashboard')">Dashboard</span></a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="nav-group">
|
||
<div class="nav-group-label" x-data x-text="$store.i18n.t('nav_library')">Library</div>
|
||
<ul class="sidebar-nav">
|
||
<li><a href="/drivers" {% if request.url.path == "/drivers" %}class="active"{% endif %}>{{ ico.i('driver') }}<span x-data x-text="$store.i18n.t('drivers')">Drivers</span></a></li>
|
||
<li><a href="/printers" {% if request.url.path.startswith("/printers") %}class="active"{% endif %}>{{ ico.i('printer') }}<span x-data x-text="$store.i18n.t('printers')">Printers</span></a></li>
|
||
<li><a href="/clients" {% if request.url.path.startswith("/clients") %}class="active"{% endif %}>{{ ico.i('group') }}<span x-data x-text="$store.i18n.t('clients')">Clients</span></a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="nav-group">
|
||
<div class="nav-group-label" x-data x-text="$store.i18n.t('nav_deploy')">Deploy</div>
|
||
<ul class="sidebar-nav">
|
||
<li><a href="/packages" {% if request.url.path == "/packages" %}class="active"{% endif %}>{{ ico.i('package') }}<span x-data x-text="$store.i18n.t('packages')">Packages</span></a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
{# Single-user mode has no cookie and no keys — the whole menu would 404. #}
|
||
{% if not request.state.single_user %}
|
||
<div class="sidebar-foot">
|
||
<details {% if request.url.path == "/session/restore" %}open{% endif %}>
|
||
<summary>{{ ico.i('key') }}<span x-data x-text="$store.i18n.t('session_menu')">This session</span></summary>
|
||
<div class="foot-links">
|
||
<a href="/session/key/download" x-data x-text="$store.i18n.t('session_download_key')">Download my backup key</a>
|
||
<a href="/session/restore" x-data x-text="$store.i18n.t('session_restore_key')">Restore from a key</a>
|
||
<p class="foot-note" x-data x-text="$store.i18n.t('session_note')">Your printers live in this browser.</p>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
{% endif %}
|
||
</nav>
|
||
|
||
<div class="main-wrapper">
|
||
<header class="topbar">
|
||
<button class="icon-btn sidebar-toggle" @click="nav = !nav"
|
||
x-data :aria-label="$store.i18n.t('open_menu')">{{ ico.i('menu', 18) }}</button>
|
||
<div class="topbar-title">
|
||
{% block crumb %}{% endblock %}
|
||
<h1>{% block page_title %}ImpTune{% endblock %}</h1>
|
||
</div>
|
||
<div class="topbar-actions" x-data>
|
||
{% block page_actions %}{% endblock %}
|
||
<span class="topbar-sep"></span>
|
||
<!-- Theme toggle button: cycles Light -> Dark -> System -->
|
||
<button class="icon-btn"
|
||
x-html="$store.theme.icons[$store.theme.current]"
|
||
:aria-label="$store.theme.current"
|
||
@click="$store.theme.cycle()"
|
||
title="Toggle theme">◑</button>
|
||
<!-- Language toggle button -->
|
||
<button class="icon-btn"
|
||
x-text="$store.i18n.t('lang_label')"
|
||
@click="$store.i18n.toggle()"
|
||
title="Toggle language">FR</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="main-content">
|
||
{% if request.state.single_user %}
|
||
{# COOKIE_SECURE=single_user: one shared store, no cookie, no isolation. #}
|
||
<div class="notice session-single-user" id="single-user-notice">
|
||
{{ ico.i('key', 16) }}
|
||
<p x-data x-text="$store.i18n.t('single_user_notice')">Single-user mode: everything is stored on the server without a session cookie, and anyone who can reach this app sees the same printers.</p>
|
||
</div>
|
||
{% endif %}
|
||
{% if request.state.ephemeral_session %}
|
||
{# COOKIE_SECURE=false: the owner key is memory-only, so the session
|
||
dies with the browser window. Say it before work is lost. #}
|
||
<div class="notice session-ephemeral" id="ephemeral-session-warning">
|
||
{{ ico.i('key', 16) }}
|
||
<p x-data x-text="$store.i18n.t('ephemeral_warning')">This session lasts only while this browser stays open — your printers and configs are saved, but the link to them is lost when you close it. Download your backup key to keep them.</p>
|
||
<a class="btn ghost" href="/session/key/download" x-data x-text="$store.i18n.t('session_download_key')">Download my backup key</a>
|
||
</div>
|
||
{% endif %}
|
||
{% if request.state.is_new_owner %}
|
||
<dialog open id="session-choice-modal">
|
||
<article>
|
||
<div class="dialog-head">
|
||
<h3>Keep your printers?</h3>
|
||
</div>
|
||
<div class="dialog-body">
|
||
<p>Your printers, configs, and clients are private to this browser —
|
||
drivers stay shared with everyone. Clear your cookies without a backup
|
||
key and they are gone for good.</p>
|
||
{% if request.state.ephemeral_session %}
|
||
<p><strong>This server runs without HTTPS</strong>, so this session is
|
||
kept in memory only: closing the browser ends it. The backup key is the
|
||
only way back in.</p>
|
||
{% endif %}
|
||
</div>
|
||
<div class="dialog-foot">
|
||
<button class="btn ghost" onclick="this.closest('dialog').close()">Keep in this browser</button>
|
||
<a role="button" class="btn" href="/session/key/download">Download backup key</a>
|
||
</div>
|
||
</article>
|
||
</dialog>
|
||
{% endif %}
|
||
{% block content %}{% endblock %}
|
||
</main>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|