feat: memory-only sessions on HTTP, streamed exports, UI refresh

Session

- COOKIE_SECURE=false no longer persists the owner key for ten years.
  services/session.cookie_kwargs() drops max_age in that mode, so the
  browser holds the key in memory and the session ends with the window.
  Everything still persists server-side; only the browser link is
  temporary. base.html shows a warning banner (FR/EN) and an extra
  paragraph in the onboarding modal, and the README explains the
  trade-off and the backup-key escape hatch.
- Both cookie writers (middleware, POST /session/restore) go through
  cookie_kwargs() so the policy cannot drift between them.
- The CSRF guard on /session/restore compared request.url.scheme against
  the Origin header. Behind a TLS-terminating proxy uvicorn sees http
  while the browser sends https, so every legitimate restore was
  rejected with 403. It now compares hosts only, including
  X-Forwarded-Host.
- /static/*, /favicon.ico and /robots.txt skip the middleware. Each
  cookieless hit was inserting an Owner row no browser could ever use.

Reliability

- Malformed printer-form FK fields no longer escape as HTTP 500:
  a non-numeric client_id/driver_id raised ValueError and an unknown
  driver_id hit a FOREIGN KEY constraint. Both are now 400/404 HTMX
  fragments, and the duplicated field checks moved into
  _validate_fields().
- Package exports stream. build_intunewin() encrypts the inner ZIP in
  1 MB chunks against temp files with a streaming HMAC and SHA256, and
  both endpoints serve the result with FileResponse plus a background
  cleanup task. A 100 MB driver used to be held in memory three or four
  times over per concurrent download. The byte layout is unchanged.
- FileResponse also escapes the download filename, which was previously
  interpolated raw into Content-Disposition.
- python-multipart >= 0.0.18 (CVE-2024-53981, reachable from
  /drivers/upload) and Pillow >= 10.3 (CVE-2024-28219, reachable from
  icon upload).
- icons.py reads cfg.ICONS_DIR instead of re-deriving the path from
  DATA_DIR, matching the .intunewin export.

UI

- Sidebar/topbar shell, inline SVG icon macros (partials/icons.html),
  card and data-table components, grouped printer list, and the
  dedicated /printers/new page replacing partials/printer_form.html.

Tests

- 194 pass with a bare `pytest tests/`: tests/conftest.py now forces
  cfg.COOKIE_SECURE = False like the e2e conftest already did, so the
  Secure cookie is no longer dropped over http://testserver.
- New coverage for the malformed-FK guards, the chunk-boundary cases in
  the encrypt loop (every residue mod _CHUNK plus a multi-megabyte
  payload), temp-dir cleanup after both exports, and the whole
  COOKIE_SECURE matrix.
- test_printer_edit.py located the Edit button by its translated label,
  so it only passed on English-locale machines. It now targets the
  showModal() hook, which also cuts the e2e run from 84s to 15s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 17:58:49 +02:00
co-authored by Claude Opus 5
parent ed41f7f520
commit b397d3dc3d
38 changed files with 3739 additions and 1014 deletions
+338 -160
View File
@@ -1,9 +1,11 @@
{% 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>ImpTune</title>
<title>{% block head_title %}ImpTune{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230d6f79'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='16' font-weight='700' text-anchor='middle' fill='white'%3EiT%3C/text%3E%3C/svg%3E">
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="stylesheet" href="/static/app.css">
<script>
@@ -11,7 +13,11 @@
// Theme store: cycles Light -> Dark -> System, persists in localStorage
Alpine.store('theme', {
current: localStorage.getItem('imptune_theme') || 'auto',
icons: { light: '&#9728;', dark: '&#9790;', auto: '&#9681;' },
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);
},
@@ -23,6 +29,10 @@
}
});
// 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: (() => {
@@ -46,49 +56,78 @@
packages: 'Paquets',
add_printer: 'Ajouter une imprimante',
add_client: 'Ajouter un client',
printer_library: 'Biblioth\u00e8que d\u2019imprimantes',
printer_library: 'Bibliothèque dimprimantes',
edit: 'Modifier',
delete: 'Supprimer',
save: 'Enregistrer',
cancel: 'Annuler',
upload_driver: 'T\u00e9l\u00e9charger un pilote',
printer_name: 'Nom de l\u2019imprimante',
upload_driver: 'Importer un pilote',
printer_name: 'Nom de limprimante',
ip_address: 'Adresse IP',
port_name: 'Nom du port',
driver: 'Pilote',
duplex_mode: 'Mode recto-verso',
one_sided: 'Recto simple',
long_edge: 'Grand c\u00f4t\u00e9',
short_edge: 'Petit c\u00f4t\u00e9',
color_mode: 'Mode couleur',
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\u2019imprimante',
no_printers: 'Aucune imprimante configur\u00e9e.',
no_clients: 'Aucun client configur\u00e9.',
edit_printer: 'Modifier limprimante',
no_printers: 'Aucune imprimante configurée.',
no_clients: 'Aucun client configuré.',
client_list: 'Liste des clients',
name: 'Nom',
created: 'Cr\u00e9\u00e9 le',
created: 'Créé le',
back_to_printers: 'Retour aux imprimantes',
theme_label: 'Th\u00e8me',
theme_label: 'Thème',
lang_label: 'FR',
// Dashboard page
new_printer: 'Nouvelle imprimante',
upload_driver_btn: 'T\u00e9l\u00e9charger un pilote',
export_package: 'Exporter un paquet',
recent_activity: 'Activit\u00e9 r\u00e9cente',
recent_printers: 'Imprimantes r\u00e9centes',
recent_packages: 'Paquets r\u00e9cents',
no_packages: 'Aucun paquet export\u00e9 pour l\u2019instant.',
// Printers new page
// Shell
brand_sub: 'Paquets dimpression',
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.',
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 limprimante',
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: '\u2190 Retour \u00e0 la biblioth\u00e8que',
save_printer: 'Enregistrer l\u2019imprimante',
upload_new_driver: 'T\u00e9l\u00e9charger un nouveau pilote',
no_driver_option: '-- Aucun pilote --',
unassigned_option: '-- Non assign\u00e9 --',
// Printer list table headers
back_to_printer_library: 'Imprimantes',
save_printer: 'Enregistrer limprimante',
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 ladresse IP. Modifiable.',
hint_name: 'Le nom vu par lutilisateur sur son poste.',
hint_driver: 'Sans pilote, limprimante est enregistrée mais lexport reste bloqué.',
hint_ip: 'Adresse IPv4 de limprimante sur le réseau du client.',
// Printer list
th_name: 'Nom',
th_ip: 'Adresse IP',
th_port: 'Port',
@@ -98,37 +137,58 @@
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: '\u2014',
// Clients page
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',
// Client detail page
back_to_clients: '\u2190 Tous les 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 page
// Drivers
drivers_title: 'Pilotes',
upload_driver_section: 'T\u00e9l\u00e9charger un package de pilote',
driver_library: 'Biblioth\u00e8que de pilotes',
uploading: 'T\u00e9l\u00e9chargement...',
upload_btn: 'T\u00e9l\u00e9charger',
driver_filename: 'Nom du fichier',
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: 'T\u00e9l\u00e9charg\u00e9 le',
uploaded_at: 'Importé le',
unknown: 'Inconnu',
no_drivers: 'Aucun pilote t\u00e9l\u00e9charg\u00e9.',
// Packages page
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 pilotes assign\u00e9s \u2014 pr\u00eates pour l\u2019export.',
packages_description: 'Imprimantes avec un pilote assigné — prêtes pour lexport.',
printer_col: 'Imprimante',
client_col: 'Client',
driver_col: 'Pilote',
downloads_col: 'T\u00e9l\u00e9chargements',
no_packages_ready: 'Aucune imprimante pr\u00eate. Assignez un pilote pour activer l\u2019export.',
// Printer detail page
downloads_col: 'Téléchargements',
no_packages_ready: 'Aucune imprimante prête. Assignez un pilote pour activer lexport.',
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',
@@ -137,32 +197,41 @@
paper_size_label: 'Format papier',
collate_label: 'Assemblage',
client_label: 'Client',
unassigned: 'Non assign\u00e9',
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\u00e9',
no_driver_detail: 'Aucun pilote assigné.',
assign_driver_cta: 'Assigner un pilote',
intune_commands: 'Commandes Intune',
install_cmd_label: 'Commande d\u2019installation',
uninstall_cmd_label: 'Commande de d\u00e9sinstallation',
hint_intune_cmds: 'À coller dans les champs dinstallation et de désinstallation de lapplication Win32.',
install_cmd_label: 'Commande dinstallation',
uninstall_cmd_label: 'Commande de désinstallation',
copy: 'Copier',
copied: 'Copi\u00e9\u00a0!',
scripts_section: 'Scripts',
download_install: 'T\u00e9l\u00e9charger le script d\u2019installation',
download_uninstall: 'T\u00e9l\u00e9charger le script de d\u00e9sinstallation',
download_detect: 'T\u00e9l\u00e9charger le script de d\u00e9tection',
export_section: 'Export',
download_ninja: 'T\u00e9l\u00e9charger NinjaRMM ZIP',
download_intunewin: 'T\u00e9l\u00e9charger .intunewin',
icon_section: 'Ic\u00f4ne',
icon_uploaded: 'Ic\u00f4ne t\u00e9l\u00e9charg\u00e9e',
upload_icon: 'T\u00e9l\u00e9charger l\u2019ic\u00f4ne',
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 licône',
replace_icon: 'Remplacer licône',
no_icon: 'Aucune icône. Intune affichera licô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 lexport.',
// Edit modal
edit_printer_title: 'Modifier l\u2019imprimante',
// Driver upload section
driver_package_label: 'Package de pilote (ZIP contenant .inf + fichiers pilote)'
edit_printer_title: 'Modifier limprimante',
// 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',
@@ -170,27 +239,27 @@
printers: 'Printers',
clients: 'Clients',
packages: 'Packages',
add_printer: 'Add Printer',
add_client: 'Add Client',
add_printer: 'Add printer',
add_client: 'Add client',
printer_library: 'Printer Library',
edit: 'Edit',
delete: 'Delete',
save: 'Save',
cancel: 'Cancel',
upload_driver: 'Upload Driver',
printer_name: 'Printer Name',
ip_address: 'IP Address',
port_name: 'Port Name',
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 Mode',
paper_size: 'Paper Size',
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',
edit_printer: 'Edit printer',
no_printers: 'No printers configured yet.',
no_clients: 'No clients configured yet.',
client_list: 'Client List',
@@ -199,24 +268,53 @@
back_to_printers: 'Back to Printers',
theme_label: 'Theme',
lang_label: 'EN',
// Dashboard page
new_printer: 'New Printer',
upload_driver_btn: 'Upload Driver',
export_package: 'Export Package',
recent_activity: 'Recent Activity',
recent_printers: 'Recent Printers',
recent_packages: 'Recent Packages',
no_packages: 'No packages exported yet.',
// Printers new page
add_printer_title: 'Add Printer',
back_to_printer_library: '\u2190 Back to Printer Library',
save_printer: 'Save Printer',
upload_new_driver: 'Upload New Driver',
no_driver_option: '-- No driver --',
unassigned_option: '-- Unassigned --',
// Printer list table headers
// 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.',
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_ip: 'IP address',
th_port: 'Port',
th_driver: 'Driver',
th_duplex: 'Duplex',
@@ -224,71 +322,101 @@
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: '\u2014',
// Clients page
client_name_label: 'Client Name',
add_client_section: 'Add Client',
client_list_section: 'Client List',
// Client detail page
back_to_clients: '\u2190 All Clients',
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 page
// Drivers
drivers_title: 'Drivers',
upload_driver_section: 'Upload Driver Package',
driver_library: 'Driver Library',
uploading: 'Uploading...',
upload_driver_section: 'Add a driver package',
driver_library: 'Driver library',
uploading: 'Uploading',
upload_btn: 'Upload',
driver_filename: 'Filename',
driver_names_col: 'Driver Name(s)',
driver_filename: 'File',
driver_names_col: 'Driver name(s)',
architecture: 'Architecture',
uploaded_at: 'Uploaded',
uploaded_at: 'Added',
unknown: 'Unknown',
no_drivers: 'No drivers uploaded yet.',
// Packages page
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 drivers assigned \u2014 ready for deployment package export.',
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 package-ready printers yet. Assign a driver to a printer to enable package export.',
// Printer detail page
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',
duplex_mode_label: 'Duplex mode',
color_mode_label: 'Color mode',
color_value: 'Color',
grayscale_value: 'Grayscale',
paper_size_label: 'Paper Size',
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)',
driver_names_label: 'Driver name(s)',
architecture_label: 'Architecture',
no_driver_detail: 'No driver assigned',
intune_commands: 'Intune Commands',
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: 'Scripts',
download_install: 'Download Install Script',
download_uninstall: 'Download Uninstall Script',
download_detect: 'Download Detect Script',
export_section: 'Export',
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 uploaded',
upload_icon: 'Upload Icon',
back_to_printers_btn: 'Back to Printers',
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',
// Driver upload section
driver_package_label: 'Driver Package (ZIP containing .inf + driver files)'
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'
}
}
});
@@ -297,57 +425,107 @@
<script defer src="/static/alpine.min.js"></script>
<script src="/static/htmx.min.js"></script>
</head>
<body>
<body x-data="{ nav: false }" :class="nav && 'nav-open'">
<div class="layout">
<nav class="sidebar">
<div class="sidebar-brand">
<strong>ImpTune</strong>
<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>
<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>
<ul class="sidebar-nav">
<li><a href="/" {% if request.url.path == "/" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('dashboard')">Dashboard</a></li>
<li><a href="/drivers" {% if request.url.path == "/drivers" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('drivers')">Drivers</a></li>
<li><a href="/printers" {% if request.url.path == "/printers" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('printers')">Printers</a></li>
<li><a href="/clients" {% if request.url.path == "/clients" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('clients')">Clients</a></li>
<li><a href="/packages" {% if request.url.path == "/packages" %}class="active"{% endif %}
x-data x-text="$store.i18n.t('packages')">Packages</a></li>
</ul>
<ul class="sidebar-nav sidebar-nav-footer">
<li><a href="/session/key/download">Download backup key</a></li>
<li><a href="/session/restore" {% if request.url.path == "/session/restore" %}class="active"{% endif %}>Restore from backup key</a></li>
</ul>
</nav>
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-controls" x-data>
<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="secondary outline"
<button class="icon-btn"
x-html="$store.theme.icons[$store.theme.current]"
:aria-label="$store.theme.current"
@click="$store.theme.cycle()"
title="Toggle theme">&#9681;</button>
<!-- Language toggle button -->
<button class="secondary outline"
<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.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>
<h3>Keep your printers?</h3>
<p>Your printers, configs, and groups are private to this browser
(drivers stay shared with everyone). If you clear your cookies
without a backup key, they're gone for good.</p>
<footer>
<a role="button" href="/session/key/download">Store permanently (download backup key)</a>
<button class="secondary" onclick="this.closest('dialog').close()">Keep temporary, this browser only</button>
</footer>
<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 %}