From e268e929d55e3a715121444d14db2aace243c358 Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Wed, 29 Jul 2026 21:07:33 +0200 Subject: [PATCH 1/3] fix(poste-neuf): cinq defauts d'installation, et deux demandes du comptoir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La v0.5 posee sur un poste neuf le 29/07/2026 tournait de bout en bout mais aucune des etapes 4 a 7 de la notice n'etait faisable sans ligne de commande. Cinq defauts fermes ici, plus les deux demandes rapportees du comptoir. Le repli en profil neutre jetait le bloc network DU FICHIER meme quand il n'etait pas fautif. Le service se posait sur 127.0.0.1:8085 pendant que le kiosque, qui lit le meme fichier et le lit sans peine puisqu'une configuration fautive reste lisible, ouvrait l'adresse declaree : ecran client noir sur le poste meme que le repli existe pour garder vivant. Le repli garde desormais network entier -- listen et admin_on_lan ensemble -- tant qu'aucune faute ne le nomme, exactement comme il gardait deja admin. Le profil neutre remplace ce sur quoi le poste TOURNE, jamais la facon dont on l'ATTEINT pour le reparer. Le banc qui manquait separe enfin l'adresse du fichier de celle du drapeau : les deux portaient la meme valeur, donc aucun test ne pouvait dire laquelle etait servie. L'entree n. 1 du lot L8 annoncait un correctif « non applique » livre depuis le 26/07 ; le banc du 29/07 a paye une demi-heure a cette contradiction. Le retour arriere a 60 s ne restaurait pas le fichier d'avant : il ecrivait ce sur quoi le poste tourne, c'est-a-dire le profil d'usine. Mesure sur un vrai poste : cooperative videe, paliers de prix 2 -> 1, remise adherent perdue, pilote passe en apercu. Et ensuite l'ecran ne pouvait plus rien enregistrer, parce que printer.type « preview » etait servi par GET et refuse par PUT. Le pilote apercu manquait au registre depuis toujours, alors que la conception annonce trois valeurs : il est livre, et il ecrit dans un repertoire a lui. Un second enregistrement pendant qu'une confirmation est attendue est refuse. Le voile « Poste hors service » couvrait le bouton Reglages, seule entree vers l'administration : un poste sortait de l'installeur dans le seul etat ou on ne peut pas le regler. La touche passe au-dessus ; le voile garde inset: 0, donc la grille reste couverte et Reimprimer reste inactif pendant une panne. « Detecter automatiquement » ne pouvait jamais reussir : les options serie etaient construites sans Baud, Bits, Parity ni Stop, l'ouverture etait refusee avant d'essayer, et l'ecran accusait alors un port occupe pour une struct vide -- un benevole lisait « COM7 est deja utilise » sur un port libre. Les huit tests existants injectaient tous un ouvreur aveugle : ils ne pouvaient pas voir ce defaut. La completion est explicite au point d'appel, et le banc traverse desormais la vraie validation. Les volets de la page Materiel se refermaient en 2,9 s, ou vivent les deux seuls champs qui nomment un port serie. La cause n'est pas celle qui avait ete ecrite : `open` est une propriete du DOM, compilee en affectation directe non memoisee, fondue dans l'effet de gabarit que le sondage rejoue. Un banc interdit le retour du motif. « Recharger le catalogue » repondait « Le catalogue va etre relu. » -- une promesse au futur, ecrite en dur avant tout acces au support, suivie d'un silence definitif quand le fichier n'est pas la. Le poste savait deja tout dire : nom du fichier, resultat, inventaire, source surveillee, tout est en base et deja servi. Le bouton nomme maintenant ce qui est surveille, puis l'issue arrive par le sondage qui existait deja. Le seul fait que le code ne produisait jamais -- « aucun fichier la ou le poste guette » -- est rendu par un os.Stat borne. La reponse reste un 202 : la rendre synchrone rebatirait le second chemin d'import que le code interdit. Les produits vendus a l'unite n'etaient pas masques, contrairement a ce qu'on croyait : 15 produits 0499 du vrai catalogue etaient affiches, et l'ecart de 24 lignes entre 355 et 331 que SUIVI.md citait deux fois sans l'expliquer se ventile en 16 anomalies de zone reservee, 7 preemballes et 1 code interne. ui.show_by_unit_products les masque par defaut ; la grille passe a 316. Le filtre est cote client : le poser cote serveur ferait diverger en permanence deux compteurs et declencherait un rechargement du catalogue dix fois par seconde, silencieusement. Masquer est un affichage et non un refus, mais la recherche ne rattrape pas une tuile masquee. Verifie sur ce poste : 2 562 tests Go sur 31 paquets (2 352 avant), 764 tests front sur 34 fichiers (686 avant), 0 echec ; gofmt propre, vet vert, svelte-check 338 fichiers sans erreur ; boundary et deps verts ; bundle client 79 429 o gzip, 70,5 % du budget. La passe -race n'a pas pu etre jouee ici : elle exige cgo et le projet est en zero cgo, sans gcc sur ce poste -- la CI Linux la couvre. --- $(readlink | 0 cmd/openscale/capture_test.go | 19 +- cmd/openscale/catalogadmin.go | 78 +++- cmd/openscale/catalogadmin_test.go | 132 +++++- cmd/openscale/detect_test.go | 113 +++++ cmd/openscale/drivers.go | 54 ++- cmd/openscale/drivers_test.go | 83 ++++ cmd/openscale/hardware.go | 88 +++- cmd/openscale/serve.go | 157 ++++++- cmd/openscale/serve_test.go | 225 +++++++++- deploy/linux/update.sh | 11 +- deploy/windows/common.ps1 | 11 +- go | 0 internal/domain/config.go | 22 +- internal/domain/config_test.go | 56 +++ internal/domain/prepare.go | 6 + internal/domain/profiles.go | 5 + internal/domain/profiles_test.go | 18 + internal/printing/preview/driver.go | 399 ++++++++++++++++++ internal/printing/preview/driver_test.go | 221 ++++++++++ internal/printing/registry.go | 13 +- internal/scale/serial/options.go | 31 +- internal/scale/serial/options_test.go | 87 ++++ internal/station/failures_test.go | 2 +- internal/station/reload_test.go | 32 +- internal/station/revert_test.go | 100 ++++- internal/station/station.go | 82 +++- internal/station/units_test.go | 4 +- internal/web/admin_test.go | 67 ++- internal/web/catalog.go | 6 + internal/web/catalog_test.go | 61 +++ internal/web/config.go | 47 ++- internal/web/config_test.go | 61 +++ internal/web/dist/admin.html | 8 +- internal/web/dist/assets/admin-CeCqIj8w.js | 1 + internal/web/dist/assets/admin-DNV6wY58.js | 1 - .../{app-Dn-BjSsJ.js => app-Bm_67CUJ.js} | 4 +- internal/web/dist/assets/index-87_aneK-.js | 2 - internal/web/dist/assets/index-BNck1guc.js | 2 + ...{index-DURuCM7m.css => index-BZkG83Q9.css} | 2 +- internal/web/dist/assets/mount-BjqIypvA.js | 93 ---- internal/web/dist/assets/mount-C1tpIbdA.css | 1 - internal/web/dist/assets/mount-CTNNPuRa.css | 1 + internal/web/dist/assets/mount-l2eO5M4C.js | 95 +++++ internal/web/dist/index.html | 6 +- internal/web/harness_test.go | 21 +- internal/web/health.go | 38 +- internal/web/rollback_test.go | 134 ++++++ internal/web/server.go | 20 +- internal/web/session.go | 5 +- internal/web/troubleshooting.go | 60 ++- internal/web/wording_test.go | 44 ++ testdata/config-demo.json | 3 +- testdata/config-lacagette.json | 3 +- web/src/App.svelte | 16 +- web/src/admin/lib/api.ts | 20 +- web/src/admin/lib/dto.ts | 25 ++ web/src/admin/lib/fields.ts | 1 + web/src/admin/lib/inventory.ts | 110 ++++- web/src/admin/lib/reload.svelte.ts | 86 ++++ web/src/admin/lib/session.svelte.ts | 11 +- web/src/admin/mount.ts | 42 +- web/src/admin/pages/Catalog.svelte | 235 +++++++++-- web/src/admin/pages/Dashboard.svelte | 25 +- web/src/admin/pages/Hardware.svelte | 45 +- web/src/admin/pages/Troubleshooting.svelte | 101 ++++- web/src/components/StatusBar.svelte | 24 ++ web/src/lib/catalog.ts | 30 +- web/src/lib/session.svelte.ts | 3 + web/test/admin-catalog.test.ts | 146 ++++++- web/test/admin-hardware.test.ts | 169 +++++++- web/test/admin-troubleshooting.test.ts | 205 ++++++++- web/test/details-open.test.ts | 93 ++++ web/test/fixtures/odoo.ts | 6 + web/test/layers.test.ts | 162 +++++++ web/test/screen.test.ts | 185 +++++++- web/test/unit-products.test.ts | 113 +++++ 77 files changed, 4321 insertions(+), 367 deletions(-) create mode 100644 $(readlink create mode 100644 go create mode 100644 internal/printing/preview/driver.go create mode 100644 internal/printing/preview/driver_test.go create mode 100644 internal/web/dist/assets/admin-CeCqIj8w.js delete mode 100644 internal/web/dist/assets/admin-DNV6wY58.js rename internal/web/dist/assets/{app-Dn-BjSsJ.js => app-Bm_67CUJ.js} (86%) delete mode 100644 internal/web/dist/assets/index-87_aneK-.js create mode 100644 internal/web/dist/assets/index-BNck1guc.js rename internal/web/dist/assets/{index-DURuCM7m.css => index-BZkG83Q9.css} (85%) delete mode 100644 internal/web/dist/assets/mount-BjqIypvA.js delete mode 100644 internal/web/dist/assets/mount-C1tpIbdA.css create mode 100644 internal/web/dist/assets/mount-CTNNPuRa.css create mode 100644 internal/web/dist/assets/mount-l2eO5M4C.js create mode 100644 web/src/admin/lib/reload.svelte.ts create mode 100644 web/test/details-open.test.ts create mode 100644 web/test/layers.test.ts create mode 100644 web/test/unit-products.test.ts diff --git a/$(readlink b/$(readlink new file mode 100644 index 0000000..e69de29 diff --git a/cmd/openscale/capture_test.go b/cmd/openscale/capture_test.go index 6e875b2..0a241f6 100644 --- a/cmd/openscale/capture_test.go +++ b/cmd/openscale/capture_test.go @@ -58,6 +58,16 @@ type scriptedStream struct { // endErr, when set, is what the port answers instead of staying silent -- a cable // pulled in the middle of a measurement campaign. endErr error + // link is the last set of options the opener was handed, and opens counts how many + // times it was called at all. + // + // A double that IGNORED its options cannot tell a caller which built a usable link + // from one which handed over a struct with no bitrate, no parity and no stop bits -- + // and a real port refuses the second before it touches the device. Recording them is + // what makes that assertion possible; internal/scale/gramxfoc does the same with the + // port name. + link serial.Options + opens int } // newScriptedStream returns a port that answers these reads, in order, and then goes @@ -102,8 +112,15 @@ func (s *scriptedStream) Close() error { // opener is the seam capture is injected through: a serial port cannot be opened by // `go test`, so the whole command is exercised through this. +// +// It KEEPS what it was handed, so that a test can assert on the link a caller built and +// not only on the bytes it read back. func (s *scriptedStream) opener() serial.Opener { - return func(serial.Options) (io.ReadCloser, error) { return s, nil } + return func(o serial.Options) (io.ReadCloser, error) { + s.link = o + s.opens++ + return s, nil + } } // refusingOpener is a port that is not there: the commonest failure of the bench, and diff --git a/cmd/openscale/catalogadmin.go b/cmd/openscale/catalogadmin.go index 0d12013..19ce30f 100644 --- a/cmd/openscale/catalogadmin.go +++ b/cmd/openscale/catalogadmin.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "time" @@ -62,22 +63,91 @@ const maxDroppedBytes = 8 << 20 // rules it always applies — including « a file still growing is not read » (§10.5), so a // producer's export caught mid-copy is not turned into an amputated catalog by somebody // pressing a button. -func (c adminCatalog) Reload(_ context.Context) error { +// +// What it DOES add is a single look at the watched file, and the sentence that look +// yields. The watch is mute when it finds nothing — it forgets and returns — so the one +// state a volunteer most needs to hear about was the one nothing ever said. +func (c adminCatalog) Reload(ctx context.Context) (string, error) { source := c.source.current() if source == nil { // The refusal names a PAGE and not two configuration keys: the volunteer reading it // has an administration screen in front of them, not a file, and the « réglages // avancés » this sentence used to send them to were removed on 27/07/2026. - return errors.New("aucune source de catalogue n'a pu être ouverte sur ce poste : " + + return "", errors.New("aucune source de catalogue n'a pu être ouverte sur ce poste : " + "choisissez où le poste va chercher le catalogue, sur la page Catalogue") } wake, ok := source.(waker) if !ok { - return fmt.Errorf("la source %q ne sait pas relire à la demande : le prochain "+ + return "", fmt.Errorf("la source %q ne sait pas relire à la demande : le prochain "+ "balayage la relira", source.Name()) } + seen := c.lookAtWatchedFile(ctx, source) + // The watch is woken WHATEVER the look saw, and in that order: the look is an + // observation, the wake is the act the button names. A file dropped between the two is + // then read at once instead of at the next tick. wake.Wake() - return nil + return seen, nil +} + +// watchBudget bounds the ONE look this route takes at the watched directory. +// +// The drop directory is a path a human typed and may well be a network share: without a +// budget, a volunteer stands in front of a screen that answers when the share feels like +// it. It is the same reason the printer status is observed outside the HTTP handler. +const watchBudget = 2 * time.Second + +// lookAtWatchedFile says, in French, whether the file this station watches is there. +// +// It is NOT a second import path, and the difference is the whole point: nothing is +// opened, nothing is parsed, nothing is applied. The watch keeps doing all of that, with +// its stability rule, its quarantine and its acknowledgement. What this answers is the +// single fact the watch never produces, because finding nothing is how it returns +// silently — « nobody has put anything where the station is looking ». +// +// A source with no local file — a share — yields NOTHING rather than an absence: an +// answer that claimed a file was missing without having looked for one would be worse +// than the silence it replaces. +func (c adminCatalog) lookAtWatchedFile(ctx context.Context, source ports.CatalogSource) string { + watched, ok := source.(watchedFile) + if !ok { + return "" + } + path := watched.Path() + name, directory := filepath.Base(path), filepath.Dir(path) + + ctx, cancel := ports.WithBudget(ctx, c.clock, watchBudget) + defer cancel() + switch present, err := fileExists(ctx, path); { + case err != nil: + return fmt.Sprintf("Le poste n'a pas pu regarder dans %s : %s.", directory, err) + case present: + return fmt.Sprintf("%s est là, dans %s : la veille le relit maintenant.", + name, directory) + } + return fmt.Sprintf("Aucun fichier %s dans %s : il n'y a rien à relire.", name, directory) +} + +// fileExists answers « is it there? » WITHIN the budget of ctx. +// +// os.Stat takes no context and a stat on an unreachable share blocks inside the kernel, +// where no cancellation reaches it. The answer therefore travels on a buffered channel: +// the caller gives up on the deadline, and the goroutine ends with the stat — never later +// than the work it bounds — and writes into a buffer nobody has to read. +func fileExists(ctx context.Context, path string) (bool, error) { + answered := make(chan error, 1) + go func() { + _, err := os.Stat(path) + answered <- err + }() + select { + case err := <-answered: + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } + return err == nil, err + case <-ctx.Done(): + return false, ctx.Err() + } } // Import takes a CSV dropped on the screen and writes it where the ordinary watcher will diff --git a/cmd/openscale/catalogadmin_test.go b/cmd/openscale/catalogadmin_test.go index 722c086..e5b6cbf 100644 --- a/cmd/openscale/catalogadmin_test.go +++ b/cmd/openscale/catalogadmin_test.go @@ -2,10 +2,19 @@ package main import ( "context" + "os" + "path/filepath" "strings" "testing" + "time" + + "openscale/internal/fake" + "openscale/internal/station/ports" ) +// reloadEpoch is the instant the fake clock of this file starts from. +var reloadEpoch = time.Date(2026, 7, 29, 9, 0, 0, 0, time.UTC) + // TestTheReloadRefusalNamesAScreenThatExists: « réglages avancés » was removed by the // administration rework of 27/07/2026, and the settings of the catalog now live on the // Catalogue page. A refusal that sends somebody to a screen that is not there is worse @@ -15,7 +24,7 @@ import ( // station runs anyway, and this is the sentence the volunteer gets when they press the // button. func TestTheReloadRefusalNamesAScreenThatExists(t *testing.T) { - err := adminCatalog{source: &liveCatalog{}}.Reload(context.Background()) + _, err := adminCatalog{source: &liveCatalog{}}.Reload(context.Background()) if err == nil { t.Fatal("un poste sans source doit refuser de recharger") } @@ -26,3 +35,124 @@ func TestTheReloadRefusalNamesAScreenThatExists(t *testing.T) { t.Errorf("le refus ne nomme pas la page où corriger : %s", err) } } + +// TestAReloadWithNoFileWhereTheStationWatchesSaysSo. +// +// This is the dominant case and the one the station never had a word for: the watch does +// its poll, finds nothing, forgets and returns in silence. « Le catalogue va être relu. » +// was therefore followed by nothing at all, for ever, and the volunteer had no way to +// tell that from an import still running. +func TestAReloadWithNoFileWhereTheStationWatchesSaysSo(t *testing.T) { + directory := t.TempDir() + source := &watchedSource{path: filepath.Join(directory, "flv_2.csv")} + + seen, err := reloadOn(source).Reload(context.Background()) + if err != nil { + t.Fatalf("relecture = %v, attendu acceptée", err) + } + if !strings.Contains(seen, "flv_2.csv") || !strings.Contains(seen, directory) { + t.Fatalf("phrase = %q, attendu qu'elle nomme le fichier et le répertoire", seen) + } + if !strings.Contains(seen, "Aucun fichier") { + t.Fatalf("phrase = %q, attendu qu'elle dise l'absence", seen) + } + // La veille est réveillée quand même : c'est ce que le bouton annonce, et un fichier + // posé entre le regard et l'appel serait lu tout de suite plutôt qu'au tour suivant. + if !source.woken { + t.Fatal("la veille n'a pas été réveillée") + } +} + +// TestAReloadWithTheFileInPlaceSaysItIsThere. +func TestAReloadWithTheFileInPlaceSaysItIsThere(t *testing.T) { + directory := t.TempDir() + path := filepath.Join(directory, "flv_2.csv") + if err := os.WriteFile(path, []byte("id;nom\n"), 0o600); err != nil { + t.Fatalf("écriture du fichier surveillé : %v", err) + } + source := &watchedSource{path: path} + + seen, err := reloadOn(source).Reload(context.Background()) + if err != nil { + t.Fatalf("relecture = %v, attendu acceptée", err) + } + if strings.Contains(seen, "Aucun fichier") { + t.Fatalf("phrase = %q : le fichier est là", seen) + } + if !strings.Contains(seen, "flv_2.csv") { + t.Fatalf("phrase = %q, attendu qu'elle nomme le fichier", seen) + } + if !source.woken { + t.Fatal("la veille n'a pas été réveillée alors que le fichier est là") + } +} + +// TestAReloadOnASourceWithNoLocalFileClaimsNothingAboutOne. +// +// A share is watched over the network and has no file of this machine to look at. The +// answer must not assert an absence nobody checked: that is how a screen learns to say +// « il n'y a rien » about a catalog sitting on a server. +func TestAReloadOnASourceWithNoLocalFileClaimsNothingAboutOne(t *testing.T) { + source := &remoteSource{} + + seen, err := reloadOn(source).Reload(context.Background()) + if err != nil { + t.Fatalf("relecture = %v, attendu acceptée", err) + } + if seen != "" { + t.Fatalf("phrase = %q, attendu rien : aucun fichier local n'a été regardé", seen) + } + if !source.woken { + t.Fatal("la veille n'a pas été réveillée") + } +} + +// reloadOn builds the adapter over one source, with the injected clock the budget needs. +func reloadOn(source ports.CatalogSource) adminCatalog { + live := &liveCatalog{} + live.hold(source) + return adminCatalog{source: live, clock: fake.NewClock(reloadEpoch)} +} + +// watchedSource is a local drop: it can be woken, and it names a file of this machine. +type watchedSource struct { + path string + woken bool +} + +func (s *watchedSource) Name() string { return "local_drop" } +func (s *watchedSource) Path() string { return s.path } +func (s *watchedSource) Wake() { s.woken = true } + +func (s *watchedSource) Next(ctx context.Context) (*ports.Batch, error) { + <-ctx.Done() + return nil, ctx.Err() +} + +func (s *watchedSource) Acknowledge(context.Context, *ports.Batch, ports.BatchResult) error { + return nil +} + +func (s *watchedSource) Close() error { return nil } + +// remoteSource is a share: wakeable, but with no file this machine can look at. +type remoteSource struct{ woken bool } + +func (s *remoteSource) Name() string { return "webdav" } +func (s *remoteSource) Wake() { s.woken = true } + +func (s *remoteSource) Next(ctx context.Context) (*ports.Batch, error) { + <-ctx.Done() + return nil, ctx.Err() +} + +func (s *remoteSource) Acknowledge(context.Context, *ports.Batch, ports.BatchResult) error { + return nil +} + +func (s *remoteSource) Close() error { return nil } + +var ( + _ ports.CatalogSource = (*watchedSource)(nil) + _ ports.CatalogSource = (*remoteSource)(nil) +) diff --git a/cmd/openscale/detect_test.go b/cmd/openscale/detect_test.go index b70c9ec..d94ea53 100644 --- a/cmd/openscale/detect_test.go +++ b/cmd/openscale/detect_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "strings" "testing" "time" @@ -117,6 +118,118 @@ func TestAPortThatCannotBeOpenedSaysWhyItMightBeTaken(t *testing.T) { } } +// TestTheDetectionOpensThePortWithUsableLinkSettings is the assertion this bench was +// missing, and the one that would have caught the defect on the first day. +// +// Every other test of this file hands a stream back whatever it is asked for, so none of +// them can see WHAT the detection puts in the link. A link with no bitrate, no character +// size, no parity and no stop bits is refused by the real opener before it reaches the +// device — on every port, on every machine — which made « Détecter automatiquement » +// unable to succeed anywhere. +func TestTheDetectionOpensThePortWithUsableLinkSettings(t *testing.T) { + clock := fake.NewClock(time.Date(2026, 7, 26, 9, 0, 0, 0, time.UTC)) + stream := emitting(clock, 4) + hardware := adminHardware{clock: clock, registries: benchRegistries(), open: stream.opener()} + + if _, err := hardware.DetectScale(context.Background(), "COM8"); err != nil { + t.Fatalf("détection : %v", err) + } + link := stream.link + if link.Port != "COM8" { + t.Fatalf("port ouvert %q, attendu celui qui est sondé", link.Port) + } + if link.Baud != 9600 || link.Bits != 8 || link.Parity != "N" || link.Stop != 1 { + t.Fatalf("liaison ouverte en %d bauds %d%s%d : les réglages n'ont pas été complétés, "+ + "et un vrai port série refuse cette liaison avant même d'essayer", + link.Baud, link.Bits, link.Parity, link.Stop) + } + if link.Clock == nil { + t.Fatal("la liaison n'emporte pas l'horloge injectée") + } +} + +// TestTheDetectionListensWithTheSettingsThisStationDeclares: the parc defaults are a +// fallback, never the last word. +// +// A station whose scale is not at 9600 bauds would stay undetectable if the detection +// listened at the figure of the parc, and the volunteer would be told the cable is fine +// and the scale is silent. The PORT, on the contrary, is always the one being probed: +// taking it from the configuration would make a scan interrogate the same port N times. +func TestTheDetectionListensWithTheSettingsThisStationDeclares(t *testing.T) { + clock := fake.NewClock(time.Date(2026, 7, 26, 9, 0, 0, 0, time.UTC)) + stream := emitting(clock, 4) + hardware := adminHardware{ + clock: clock, registries: benchRegistries(), open: stream.opener(), + config: stationDeclaring(t, map[string]any{ + "port": "COM3", "baud": 19200, "parity": "E", + }), + } + + if _, err := hardware.DetectScale(context.Background(), "COM8"); err != nil { + t.Fatalf("détection : %v", err) + } + if stream.link.Baud != 19200 || stream.link.Parity != "E" { + t.Fatalf("liaison ouverte en %d bauds, parité %q : ce poste déclare 19200 et E", + stream.link.Baud, stream.link.Parity) + } + if stream.link.Port != "COM3" && stream.link.Port != "COM8" { + t.Fatalf("port ouvert %q, inattendu", stream.link.Port) + } + if stream.link.Port == "COM3" { + t.Fatal("la détection a écouté le port de la configuration : un balayage " + + "interrogerait alors N fois le même port au lieu des N ports de la machine") + } +} + +// TestAPortRefusedByItsSettingsIsNotReportedAsTaken is the counterpart of +// TestAPortThatCannotBeOpenedSaysWhyItMightBeTaken, and it is the sentence a volunteer +// reads on the telephone. +// +// « Le port COM7 est déjà utilisé » on a port nothing is holding sends somebody hunting +// for a process that does not exist. A refusal that comes from the settings of this +// station must name the setting, and it must not reach the port at all. +func TestAPortRefusedByItsSettingsIsNotReportedAsTaken(t *testing.T) { + clock := fake.NewClock(time.Date(2026, 7, 26, 9, 0, 0, 0, time.UTC)) + stream := emitting(clock, 4) + hardware := adminHardware{ + clock: clock, registries: benchRegistries(), open: stream.opener(), + config: stationDeclaring(t, map[string]any{"port": "COM3", "parity": "K"}), + } + + _, err := hardware.DetectScale(context.Background(), "COM8") + if err == nil { + t.Fatal("une parité inexistante a été acceptée par la détection") + } + if !strings.Contains(err.Error(), "parity") { + t.Fatalf("le refus ne nomme pas le réglage à corriger : %v", err) + } + for _, accusation := range []string{"déjà utilisé", "EXCLUSIF", "autre programme"} { + if strings.Contains(err.Error(), accusation) { + t.Fatalf("un refus de réglages accuse un port occupé (%q) : %v", accusation, err) + } + } + if stream.opens != 0 { + t.Fatalf("le port a été ouvert %d fois : des réglages inutilisables se refusent "+ + "avant de toucher le matériel", stream.opens) + } +} + +// stationDeclaring is a configuration reader answering with these scale.options, the way +// the config.json of a station carries them (§11.2). +func stationDeclaring(t *testing.T, pairs map[string]any) func() domain.Config { + t.Helper() + options := domain.DriverOptions{} + for key, value := range pairs { + encoded, err := json.Marshal(value) + if err != nil { + t.Fatalf("encodage de scale.options.%s : %v", key, err) + } + options[key] = encoded + } + cfg := domain.Config{Scale: domain.ScaleConfig{Present: true, Options: options}} + return func() domain.Config { return cfg } +} + // TestTheDetectionRefusesAnEmptyPortByName keeps the message useful: a form submitted with // an empty field gets told what to type. func TestTheDetectionRefusesAnEmptyPortByName(t *testing.T) { diff --git a/cmd/openscale/drivers.go b/cmd/openscale/drivers.go index c9e4a67..f931356 100644 --- a/cmd/openscale/drivers.go +++ b/cmd/openscale/drivers.go @@ -11,6 +11,7 @@ import ( "openscale/internal/domain" "openscale/internal/printing" + "openscale/internal/printing/preview" "openscale/internal/printing/raster" "openscale/internal/printing/transport" "openscale/internal/scale" @@ -42,20 +43,57 @@ func scaleRegistry() *scale.Registry { // printerRegistry is the set of printer drivers this binary was built with. // -// ONE entry today, `raster`, which is the production path (ADR-002): at 203 dpi no -// whole module reproduces the magnification of the label the cooperative prints, so -// the bitmap is drawn dot by dot. `sbpl` and `preview` are named by §8.1 and by -// domain.PrinterTypes, and neither has a ports.Printer implementation yet — -// internal/printing/sbpl is the shared ENCODER of the frame and -// internal/printing/preview the file writer of `openscale label`. Registering a -// driver that does not exist would put a value in a volunteer's drop-down list that -// no station can honour. +// TWO entries. `raster` is the production path (ADR-002): at 203 dpi no whole module +// reproduces the magnification of the label the cooperative prints, so the bitmap is drawn +// dot by dot. `preview` writes that same bitmap to a PNG and a life-size PDF and opens no +// device at all — and it is not a convenience, it is what the NEUTRAL PROFILE names +// (§11.3). A binary that did not carry it served, on every station whose configuration is +// unusable, a profile its own validation refused: `printer.type` came back as a fault on a +// field nobody had touched, and no save was possible from that screen at all. +// +// `sbpl` is named by §8.1 and by domain.PrinterTypes and is still not registered: +// internal/printing/sbpl is the shared ENCODER of the frame, not a ports.Printer. +// Registering a driver that does not exist would put a value in a volunteer's drop-down +// list that no station can honour. func printerRegistry() *printing.Registry { registry := printing.NewRegistry() registry.Register(rasterDriver()) + registry.Register(previewDriver()) return registry } +// previewDriver is the registry entry of the driver that prints nothing. +// +// IT DECLARES NO OPTION, and that is the requirement rather than an omission. The neutral +// profile carries no `printer.options` block — darkness, speed and the number of copies are +// settled on a real print run, and a factory profile has no business inventing three +// figures nobody measured — so the driver such a station falls back on must ask for none of +// them. An empty schema is also what tells the composition root there is no transport to +// build here. +func previewDriver() printing.Driver { + return printing.Driver{ + Descriptor: domain.PrinterDescriptor{ + ID: preview.ID, + Label: preview.Label, + Capabilities: domain.PrinterCapabilities{ + Raster: true, + Status: false, + Cutter: false, + MaxCopies: 1, + }, + }, + New: func(c printing.DriverConfig) (ports.Printer, error) { + return preview.New(preview.Options{ + Dir: c.OutputDir, + Clock: c.Clock, + Log: c.Log, + Template: c.Template, + DemoLabel: c.DemoLabel, + }) + }, + } +} + // rasterDriver is the registry entry of the production printer driver. // // The head is a WS408 — 8 dots/mm, 104 bytes of block — because that is the whole diff --git a/cmd/openscale/drivers_test.go b/cmd/openscale/drivers_test.go index f039456..ebfe1be 100644 --- a/cmd/openscale/drivers_test.go +++ b/cmd/openscale/drivers_test.go @@ -152,6 +152,89 @@ func TestPrinterSettingsRefuseAFileThatSaysNothing(t *testing.T) { } } +// TestLeProfilNeutreEstApplicableParCeBinaire. +// +// The neutral profile is what a station RUNS when its own file is unusable (§11.3), and its +// whole job is to keep the administration screen reachable so that somebody can repair the +// file. A profile this binary's own registries refuse is a station serving a configuration +// its own validation turns down: the read-modify-write cycle of the administration then +// answers 422 on `printer.type`, a field nobody touched, and no save is possible at all. +// +// No existing test could see it. internal/domain validates it against a registry that +// invents the three printers, and against an empty one where the control is skipped +// altogether — and the only test that used the real registries validated the DELIVERED file. +// +// admin.password_hash is the one fault the profile documents and means: a virgin station has +// no password, and step 1 of the first-start wizard is the answer to it. +func TestLeProfilNeutreEstApplicableParCeBinaire(t *testing.T) { + profile := domain.NeutralProfile() + for _, fault := range profile.Validate(registries()) { + if fault.Field == "admin.password_hash" { + continue + } + t.Errorf("le profil d'usine produit une faute contre les registres de ce binaire : %s", + fault.String()) + } +} + +// TestLeProfilNeutreObtientUneVraieImprimante. +// +// The other half of the same hole. A station on the neutral profile carries no +// `printer.options` at all — deliberately: darkness, speed and the number of copies are set +// on a REAL print run, and a factory profile has no business inventing them. So the printer +// it gets must come from a driver that needs none of that, and needs no device either. +// +// Until the `preview` driver was registered, that station got `unbuiltPrinter`: every button +// of the troubleshooting screen answered « l'imprimante configurée n'a pas pu être +// construite », on the one station those buttons exist for. +func TestLeProfilNeutreObtientUneVraieImprimante(t *testing.T) { + profile := domain.NeutralProfile() + templates, err := templatesFor(profile, registries()) + if err != nil { + t.Fatalf("templatesFor sur le profil d'usine : %v", err) + } + printer, err := newPrinter(profile, printerRegistry(), templates, + fake.NewClock(captureStart), nopLog{}, t.TempDir()) + if err != nil { + t.Fatalf("le profil d'usine n'obtient aucune imprimante : %v", err) + } + defer printer.Close() + if printer.Descriptor().ID == "" { + t.Fatal("l'imprimante du profil d'usine ne se nomme pas") + } +} + +// TestChaqueTypeDImprimanteDuDomaineEstConstructible keeps the drop-down list a volunteer +// reads and what this binary can actually build in step. +// +// A driver offered by the administration screen that newPrinter cannot instantiate would be +// a setting that validates and then refuses to print — the same promise +// TestEveryTransportOfTheRegistryCanBeBuilt makes one layer down. +func TestChaqueTypeDImprimanteDuDomaineEstConstructible(t *testing.T) { + for _, descriptor := range printerRegistry().Descriptors() { + cfg := shippedConfig(t) + cfg.Printer.Type = descriptor.ID + templates, err := templatesFor(cfg, registries()) + if err != nil { + t.Fatalf("templatesFor pour %q : %v", descriptor.ID, err) + } + printer, err := newPrinter(cfg, printerRegistry(), templates, + fake.NewClock(captureStart), nopLog{}, t.TempDir()) + if err != nil { + t.Fatalf("le driver %q est proposé par le registre et non constructible : %v", + descriptor.ID, err) + } + if err := printer.Close(); err != nil { + t.Fatalf("fermeture du driver %q : %v", descriptor.ID, err) + } + } +} + +// nopLog swallows what a driver reports while a test builds it. +type nopLog struct{} + +func (nopLog) Technical(_, _, _, _, _ string) {} + // registriesOfThisBinary is what a configuration is validated against: the drivers and // the transports this binary really carries. func registriesOfThisBinary() domain.Registries { diff --git a/cmd/openscale/hardware.go b/cmd/openscale/hardware.go index 11c650c..48838dd 100644 --- a/cmd/openscale/hardware.go +++ b/cmd/openscale/hardware.go @@ -58,6 +58,15 @@ type adminHardware struct { registries domain.Registries // technical is where an administrative action is recorded — a replayed frame is one. technical ports.TechnicalLog + // config reports the configuration IN FORCE, because a detection listens with the + // link settings THIS station declares and not only with those of the parc — a scale + // that is not at 9600 bauds would otherwise stay undetectable. + // + // It is a function and not h.hub.Config() on purpose: Hub.Config dereferences the + // station's own configuration, so a test building this struct without a hub would + // panic. nil therefore means « nothing declared yet », which is exactly the case of + // a station being installed — the very moment this route is used. + config func() domain.Config // open opens a serial port. nil means the real one, so the production path needs no // wiring — and a test drives the detection through a stream it hands back, exactly as @@ -130,11 +139,14 @@ func printersOf(queues []platform.PrintQueue) []web.PrinterInfo { // guess fails at the counter. Here the port is opened, the grammar of §9.2 is applied to // what comes out of it, and the answer is a count. // -// # The port in service is EXCLUSIVE +// # A refusal has to say WHICH refusal it is // // On Windows a port already held by the scale of this station cannot be opened a second -// time, and the refusal says so: a volunteer detecting on the port that already works -// must read « elle est déjà utilisée par ce poste » rather than « accès refusé ». +// time, and « accès refusé » alone would send a volunteer hunting for a permission +// problem that does not exist. That hint is only true of a refusal that came from the +// SYSTEM, though: link settings no port could accept are named — with the key of +// scale.options to correct — before anything is opened, and must never reach the screen +// as a port somebody else is holding. func (h adminHardware) DetectScale(ctx context.Context, port string) (web.ScaleDetection, error) { if strings.TrimSpace(port) == "" { return web.ScaleDetection{}, errors.New("indiquez le port à écouter : COM8, /dev/balance-serial") @@ -187,16 +199,21 @@ func (h adminHardware) CaptureFrames(ctx context.Context, port string, d time.Du // diagnosis, and a lost cable is not a failure of this function. func (h adminHardware) listen(ctx context.Context, port string, window time.Duration) ( []string, []domain.Measurement, int, error) { - link := serial.Options{Port: port, Clock: h.clock, Open: h.open} - open := link.Open - if open == nil { - open = serial.OpenSystemPort + link, err := h.linkFor(port) + if err != nil { + return nil, nil, 0, err } - stream, err := open(link) + stream, err := link.Open(link) if err != nil { - return nil, nil, 0, fmt.Errorf("le port %s ne peut pas être ouvert : %w. "+ - "S'il est celui de la balance de ce poste, il est déjà utilisé : un port série est "+ - "EXCLUSIF sous Windows", port, err) + // Every reason the LINK itself could be unusable has already been named by linkFor, + // with the key of scale.options to correct, so what is left here comes from the + // system — and that is what makes the two causes below the only two. Whoever adds a + // check upstream of this call must name its own refusal there, or a settings mistake + // will once again reach a volunteer as a port that somebody else is holding. + return nil, nil, 0, fmt.Errorf("le port %s n'a pas pu être ouvert : %w. Deux causes "+ + "possibles : un autre programme le tient — la balance de ce poste en premier, "+ + "un port série est EXCLUSIF sous Windows — ou bien ce port n'existe plus sur "+ + "cette machine", port, err) } defer stream.Close() @@ -233,6 +250,55 @@ func (h adminHardware) listen(ctx context.Context, port string, window time.Dura return frames, measurements, read, nil } +// linkFor is the link a detection listens on: the settings THIS station declares, +// completed by the defaults of the parc, on the port being probed. +// +// # The completion is written here on purpose +// +// A link assembled field by field carried no bitrate, no character size, no parity and no +// stop bits, and the real opener refuses such a link before it reaches the device: the +// detection could not succeed on any port of any machine, and every port of a scan came +// back accused of being taken. Calling serial.Options.Complete at the place where the +// port is BOUND is what makes that visible to the next reader, and what lets the caller +// tell a refusal of these settings from a refusal of the system. +// +// # The port always wins over the configuration +// +// A scan probes the ports of the machine one after the other. Taking the port from the +// configuration would interrogate the same one N times and report the other N-1 as silent. +func (h adminHardware) linkFor(port string) (serial.Options, error) { + link, err := h.declaredLink() + if err != nil { + return serial.Options{}, fmt.Errorf("les réglages série de ce poste sont refusés, "+ + "corrigez-les avant de détecter : %w", err) + } + link.Port = port + link.Clock = h.clock + if h.open != nil { + link.Open = h.open + } + return link, nil +} + +// declaredLink reads the link settings this station declares and completes them. +// +// A station that has declared nothing yet — the one being installed, which is precisely +// when this route is used — falls back on the defaults of the parc. A station that +// declares 19200 bauds is listened to at 19200: at the figure of the parc its scale would +// answer bytes that decode to nothing, and the screen would send somebody to check a +// cable that is fine. +func (h adminHardware) declaredLink() (serial.Options, error) { + var declared domain.DriverOptions + if h.config != nil { + declared = h.config().Scale.Options + } + link, err := serial.ParseOptions(declared) + if err != nil { + return serial.Options{}, err + } + return link.Complete() +} + // defaultReadBuffer is how many bytes one read may hand back. // // The same 4 KiB as the production loop, and NOT the 16 of the legacy SetupComm: a queue diff --git a/cmd/openscale/serve.go b/cmd/openscale/serve.go index 8068cd7..16589f2 100644 --- a/cmd/openscale/serve.go +++ b/cmd/openscale/serve.go @@ -185,6 +185,18 @@ Options : } o := serveOptions{configPath: *configPath, dataDir: *dataDir, listen: *listen} + // A malformed --listen is refused HERE, naming the flag, because from any later point + // the only thing left to blame is the file: the flag used to be written into the + // configuration before it was validated, so `--listen 8085` took a healthy station out + // of service and reported « configuration d'usine (ERR-CFG-01) » about config.json. + // The rule is domain's own, control 2's, and not a second one that would drift from it. + if o.listen != "" { + if err := domain.CheckListenAddress(o.listen); err != nil { + return serveOptions{}, &serviceFailure{Exit: exitFailure, Message: fmt.Sprintf( + "--listen %q n'est pas une adresse hôte:port valide (%s) ; attendu hôte:port, "+ + "par exemple 127.0.0.1:8085", o.listen, err)} + } + } if o.configPath == "" { o.configPath = platform.DefaultConfigPath() } @@ -202,14 +214,13 @@ func serve(ctx context.Context, o serveOptions, out io.Writer) error { if err != nil { return err } - if o.listen != "" { - cfg.Network.Listen = o.listen - } // The service OWNS its data directory and creates it (§15.3). Nothing here is a // mount point and nothing here is shared: an administrator who wants files to // arrive from a share mounts what they like where they like and synchronises into // it, and the unit has nothing to know about it. - for _, dir := range []string{o.dataDir, imagesRoot(o.dataDir), labelsDir(o.dataDir)} { + for _, dir := range []string{ + o.dataDir, imagesRoot(o.dataDir), labelsDir(o.dataDir), previewsDir(o.dataDir), + } { if err := os.MkdirAll(dir, 0o755); err != nil { return &serviceFailure{Exit: exitFailure, Err: err, Message: fmt.Sprintf( "le répertoire de données %s ne peut pas être créé : %v", dir, err)} @@ -262,7 +273,15 @@ func serve(ctx context.Context, o serveOptions, out io.Writer) error { outOfService := len(faults) > 0 if outOfService { reportFaults(out, o.configPath, faults) - cfg = fallbackProfile(cfg, o.listen) + cfg = fallbackProfile(cfg, faults) + } + // --listen is applied AFTER the verdict, and that is the whole point of its position. + // Written before, it was judged as if the file had carried it: a typo on the command + // line took a healthy station out of service and was reported against config.json, + // while a genuine fault in the file's own address was quietly repaired for the + // duration of the run and came back at the next restart. + if o.listen != "" { + cfg.Network.Listen = o.listen } // The technical journal belongs to the Hub, and the drivers are built BEFORE it @@ -379,16 +398,27 @@ func serve(ctx context.Context, o serveOptions, out io.Writer) error { // The rollback of §11.4 puts the FILE back as well as the running station: a // station that went back on its own and then restarted on the configuration nobody // confirmed would have cut the branch sixty seconds later than announced. - OnRevert: func(previous domain.Config) { - if err := configFile.Save(context.Background(), previous); err != nil { + // + // What arrives here is THE FILE AS IT WAS BEFORE THE SAVE, and never the + // configuration the station was running. The two differ on exactly the station this + // matters for: one whose file is unusable runs the neutral profile (§11.3) while + // its file keeps the cooperative's own settings, and writing the running + // configuration back replaced them with the factory ones — the tariffs, the + // safeguards, the categories — sixty seconds after a volunteer repaired the file. + OnRevert: func(fileBefore domain.Config) { + if err := configFile.Save(context.Background(), fileBefore); err != nil { log.Technical(domain.LevelError, "config", "", "Retour arrière appliqué au poste mais non écrit : le fichier porte "+ "encore la configuration non confirmée.", err.Error()) return } + // The detail names BOTH fingerprints. « Retour à la version précédente » + // designates two documents on an out-of-service station, and a line carrying + // one number would leave a volunteer unable to say which one came back. + running := st.Hub().Config() log.Technical(domain.LevelWarn, "config", "", "Configuration non confirmée : le fichier est revenu à la version précédente.", - previous.Fingerprint()) + fmt.Sprintf("fichier %s, poste %s", fileBefore.Fingerprint(), running.Fingerprint())) }, }) if err != nil { @@ -444,7 +474,7 @@ func serve(ctx context.Context, o serveOptions, out io.Writer) error { }, Hardware: adminHardware{ clock: clock, hub: st.Hub(), registries: registries, - technical: st.Hub().TechnicalLog(), + technical: st.Hub().TechnicalLog(), config: st.Hub().Config, }, Printer: live, Troubleshooting: adminTroubleshooting{station: st, printer: live, file: configFile}, @@ -613,21 +643,46 @@ func readConfig(path string) (domain.Config, error) { // answering « ce poste n'a pas de code de secours » — on the ONE station both exist for. // The screen was then unreachable on exactly the station §11.3 says it must serve. // -// # The listening address +// # The network block // -// --listen is what somebody types while diagnosing — « the address is taken, move this -// station off it ». The neutral profile carries 127.0.0.1:8085 like every station of the -// parc, so dropping the override answered a deliberate instruction with the very address -// the operator was trying to leave. -func fallbackProfile(broken domain.Config, listen string) domain.Config { +// Same rule, and it was learnt the same way. The neutral profile replaces what the +// station RUNS ON; it has no business replacing the way one REACHES it in order to +// repair it. Its address is 127.0.0.1:8085, which every station of the parc shares, so +// borrowing it moved a station off the address its file declares — while the kiosk, which +// reads that same file and reads it successfully because a faulty file is still a +// readable one, kept opening the declared address. A black client screen on the very +// station §11.3 exists to keep alive, and an administration screen shut back onto the +// loopback at the moment a volunteer arrives with a laptop to fix it. +// +// The address of the file is kept only while it is USABLE: when the faults name the +// network block itself, the neutral profile provides it, because a fallback that copied +// an unbindable address would turn ERR-CFG-01 — a station serving its fault list — into +// ERR-SYS-02, a station that is not there at all. +func fallbackProfile(broken domain.Config, faults []domain.Fault) domain.Config { cfg := domain.NeutralProfile() cfg.Admin = broken.Admin - if listen != "" { - cfg.Network.Listen = listen + if !faultedOn(faults, "network") { + cfg.Network = broken.Network } return cfg } +// faultedOn reports whether any fault names a field of one configuration section. +// +// It matches the section and everything beneath it — "network" answers for +// "network.listen" — so a control added to that section later is covered without this +// function having to learn its name. Half a block is what must never be borrowed: an +// address open to the network behind an administration screen closed to it is harder to +// diagnose than a fallback that is wrong in both directions at once. +func faultedOn(faults []domain.Fault, section string) bool { + for _, fault := range faults { + if fault.Field == section || strings.HasPrefix(fault.Field, section+".") { + return true + } + } + return false +} + // reportFaults writes the whole list of §11.3 where whoever started the service can // read it. // @@ -720,28 +775,44 @@ func buildPrinter(cfg domain.Config, reg *printing.Registry, templates map[strin // newPrinter builds the print service of one configuration: the driver // printer.type names, over the transport printer.options.transport names, with the // retries and the roll counter of §8.2 around it. +// +// The transport is built ONLY for a driver that declares one. Built first and +// unconditionally, as it was, it refused the empty `printer.options` of the neutral profile +// before the driver was ever consulted — so a station in factory configuration got +// `unbuiltPrinter` and answered « l'imprimante configurée n'a pas pu être construite » to +// every button of the troubleshooting screen, which is the one screen that station exists +// to serve. func newPrinter(cfg domain.Config, reg *printing.Registry, templates map[string]domain.Template, clk ports.Clock, log ports.TechnicalLog, dataDir string) (ports.Printer, error) { - byteLayer, err := newTransport(cfg.Printer.Options, clk, labelsDir(dataDir)) - if err != nil { - return nil, err + var byteLayer ports.Transport + if declaresTransport(reg, cfg.Printer.Type) { + opened, err := newTransport(cfg.Printer.Options, clk, labelsDir(dataDir)) + if err != nil { + return nil, err + } + byteLayer = opened } driver, err := reg.New(cfg.Printer.Type, printing.DriverConfig{ Options: cfg.Printer.Options, Transport: byteLayer, + OutputDir: previewsDir(dataDir), Template: templates[cfg.Printer.Template], Clock: clk, Log: log, DemoLabel: func() (domain.Label, error) { return demoLabel(cfg.Pricing) }, }) if err != nil { - _ = byteLayer.Close() + if byteLayer != nil { + _ = byteLayer.Close() + } return nil, err } capacity, _ := cfg.Printer.Options.Int(optionRollCapacity) service, err := printing.NewService(printing.ServiceOptions{ - Main: driver, - MainName: byteLayer.Describe(), + Main: driver, + // A driver with no transport names itself: printing.NewService falls back on the + // label of the descriptor, which for `preview` already says it prints nothing. + MainName: describeTransport(byteLayer), Clock: clk, // The roll counter has NO persistent store yet: internal/store carries no // AddLabels/SetLabels pair, so the count restarts with the process. What it @@ -759,6 +830,39 @@ func newPrinter(cfg domain.Config, reg *printing.Registry, templates map[string] return service, nil } +// declaresTransport reports whether the driver printer.type names carries a byte transport +// at all. +// +// The answer comes from the driver's OWN option schema — the same schema control 7 of +// §11.3 validates printer.options against and the administration screen generates its form +// from — so a driver added later says for itself whether the composition root has a device +// to open for it, with no list to keep in step here. +// +// An unknown driver answers false, and that is deliberate: reg.New refuses it one line +// later with the list of the ones that exist, and building a transport first would replace +// that refusal with a complaint about a transport key nobody typed. +func declaresTransport(reg *printing.Registry, driverID string) bool { + for _, descriptor := range reg.Descriptors() { + if descriptor.ID != driverID { + continue + } + for _, option := range descriptor.Options { + if option.Key == optionTransport { + return true + } + } + } + return false +} + +// describeTransport is the French name of the byte layer, or nothing when there is none. +func describeTransport(byteLayer ports.Transport) string { + if byteLayer == nil { + return "" + } + return byteLayer.Describe() +} + // unbuiltPrinter is what a station has when its configuration names a printer this // binary cannot build. // @@ -886,3 +990,10 @@ func imagesDir(dataDir string) fs.FS { return os.DirFS(imagesRoot(dataDir)) } // labelsDir is where the `file` transport drops one copy per label (§11.1). func labelsDir(dataDir string) string { return filepath.Join(dataDir, "labels") } + +// previewsDir is where the `preview` driver writes the PNG and the PDF of each label. +// +// A directory OF ITS OWN, and not the one above. Both answer « envoyez-moi le fichier de la +// dernière étiquette », and that sentence is how support works: mixing the raw frames of a +// transport with the images of an aperçu would make it a question with two answers. +func previewsDir(dataDir string) string { return filepath.Join(dataDir, "previews") } diff --git a/cmd/openscale/serve_test.go b/cmd/openscale/serve_test.go index d1e52a0..658f1e3 100644 --- a/cmd/openscale/serve_test.go +++ b/cmd/openscale/serve_test.go @@ -293,7 +293,7 @@ func TestTheFallbackProfileKeepsTheKEYSToTheStation(t *testing.T) { broken.Admin.RecoveryCodeHash = "$argon2id$v=19$m=65536,t=3,p=2$YXV0cmUtc2VsLTAx$Y2xlLWR1LWNvZGUtZGUtc2Vjb3Vycy1pY2k" broken.Station.Coop = "Les Amis de la Coopé" - fallback := fallbackProfile(broken, "") + fallback := fallbackProfile(broken, faultsOn("pricing.tiers[0].discount_percent")) if fallback.Admin.PasswordHash != broken.Admin.PasswordHash { t.Error("le profil de repli oublie le mot de passe d'administration du fichier") } @@ -305,16 +305,191 @@ func TestTheFallbackProfileKeepsTheKEYSToTheStation(t *testing.T) { if fallback.Station.Coop == broken.Station.Coop { t.Error("le profil de repli fait tourner le poste sur la configuration fautive") } +} + +// TestTheFallbackProfileKeepsTheDOORToTheStation is the same rule applied to the network +// block, and it is the rule the bench of 2026-07-29 discovered the hard way. +// +// The keys are useless behind a door nobody can find. §11.3 replaces what a station RUNS +// ON, never the way one REACHES it in order to repair it: the address a station answers +// on is written on the installation sheet and dialled by the kiosk from that same file, +// and admin_on_lan is what lets a volunteer arrive with a laptop rather than a keyboard. +// Borrowing the neutral 127.0.0.1:8085 moved the service off the address the file +// declares while the kiosk kept opening it — a black client screen — and shut the +// administration screen back onto the loopback at the worst possible moment. +func TestTheFallbackProfileKeepsTheDOORToTheStation(t *testing.T) { + broken := shippedConfig(t) + broken.Network = domain.NetworkConfig{Listen: "127.0.0.1:8099", AdminOnLAN: true} + + fallback := fallbackProfile(broken, faultsOn("pricing.tiers[0].discount_percent")) + if fallback.Network.Listen != broken.Network.Listen { + t.Errorf("adresse du repli = %q, attendu %q : le repli jette une adresse d'écoute qui "+ + "n'est pas fautive", fallback.Network.Listen, broken.Network.Listen) + } + if !fallback.Network.AdminOnLAN { + t.Error("le repli referme l'écran d'administration sur la boucle locale, au moment " + + "même où un bénévole vient réparer le poste depuis son portable") + } +} + +// TestTheFallbackProfileTakesTheNeutralAddressWhenTheFileAddressIsItselfFaulted is what +// keeps the test above from being an invitation to copy an unbindable address. +// +// When the faults name the network block, the file has nothing usable to lend: the +// neutral profile provides the address, exactly as before. Without this case, a fallback +// that copied « 127.0.0.1 » — no port — would turn ERR-CFG-01, a station serving its +// fault list, into ERR-SYS-02, a station that is not there at all. +func TestTheFallbackProfileTakesTheNeutralAddressWhenTheFileAddressIsItselfFaulted(t *testing.T) { + broken := shippedConfig(t) + broken.Network = domain.NetworkConfig{Listen: "127.0.0.1", AdminOnLAN: true} + + fallback := fallbackProfile(broken, faultsOn("network.listen")) if want := domain.NeutralProfile().Network.Listen; fallback.Network.Listen != want { - t.Errorf("adresse du repli = %q, attendu %q", fallback.Network.Listen, want) + t.Errorf("adresse du repli = %q, attendu %q : le repli a recopié une adresse "+ + "inliable", fallback.Network.Listen, want) + } + if fallback.Network.AdminOnLAN { + t.Error("le repli a gardé la moitié d'un bloc network fautif : l'écran " + + "d'administration s'ouvre au réseau sur une configuration que personne n'a validée") + } +} + +// faultsOn builds the verdict of a Validate that found exactly these fields wrong. +func faultsOn(fields ...string) []domain.Fault { + faults := make([]domain.Fault, 0, len(fields)) + for _, field := range fields { + faults = append(faults, domain.Fault{Field: field, Message: "faute de banc d'essai"}) + } + return faults +} + +// TestAFaultyConfigurationStillServesOnTheAddressItsFileDeclares is what the bench of +// 2026-07-29 paid for, and the assertion no test made until it did. +// +// The station shipped that day carried network.listen 8099 AND eight faults elsewhere. +// The fallback threw the address out with the rest, the service came up on the +// 127.0.0.1:8085 of the neutral profile, and the kiosk — which reads the FILE, and reads +// it successfully because a faulty file is still a readable one — opened 8099. A black +// client screen, on the very station §11.3 exists to keep alive. +// +// So: a fault ANYWHERE BUT on the address, an address in the file, no flag, and the +// station must serve on the address its file names. +func TestAFaultyConfigurationStillServesOnTheAddressItsFileDeclares(t *testing.T) { + bench := newServeBench(t, func(cfg *domain.Config) { + cfg.Pricing.Tiers[0].Discount = -10 + }).listenFlag("") + bench.start() + + if bench.address != bench.fileAddress { + t.Fatalf("le poste sert sur %q alors que son fichier déclare %q : le repli a jeté une "+ + "adresse d'écoute qui n'était pas fautive, et l'écran client ouvre une adresse que "+ + "rien ne sert", bench.address, bench.fileAddress) + } + // And it really is the fallback that is being observed, not a configuration that + // turned out to be valid after all. + if got := bench.output(); !strings.Contains(got, "ERR-CFG-01") { + t.Fatalf("la sortie ne nomme pas ERR-CFG-01 : ce banc ne traverse pas le repli\n%s", got) + } + + if err := bench.stop(); err != nil { + t.Fatalf("serve a rendu une erreur sur un arrêt demandé : %v", err) + } +} + +// TestTheListenFlagWinsOverTheAddressOfTheFile carries the assertion of +// TestTheFallbackProfileKeepsTheKEYSToTheStation all the way to the socket. +// +// --listen is what somebody types while diagnosing — « cette adresse est prise, déplace +// ce poste » — and it must win on a healthy configuration as on a broken one. Asserting +// it on fallbackProfile alone would leave the composition free to apply the flag before +// a validation that then rejects it, which is exactly what used to happen. +func TestTheListenFlagWinsOverTheAddressOfTheFile(t *testing.T) { + for _, c := range []struct { + name string + tweak func(*domain.Config) + }{ + {"configuration saine", func(*domain.Config) {}}, + {"configuration fautive", func(cfg *domain.Config) { cfg.Pricing.Tiers[0].Discount = -10 }}, + } { + t.Run(c.name, func(t *testing.T) { + asked := freeAddress(t) + bench := newServeBench(t, c.tweak).listenFlag(asked) + if asked == bench.fileAddress { + t.Fatalf("le banc a tiré deux fois la même adresse %q : il ne sépare rien", asked) + } + bench.start() + + if bench.address != asked { + t.Fatalf("le poste sert sur %q alors que --listen demandait %q : le drapeau a été "+ + "perdu", bench.address, asked) + } + if err := bench.stop(); err != nil { + t.Fatalf("serve a rendu une erreur sur un arrêt demandé : %v", err) + } + }) } +} + +// TestAMalformedListenFlagIsBlamedOnTheFlagAndNotOnTheFile is the other half of the same +// defect, and the one that punishes an innocent file. +// +// `--listen 8085` — a port with no host, which anybody types — used to be written into +// the configuration BEFORE it was validated. A perfectly healthy station then announced +// « configuration d'usine (ERR-CFG-01) » about a file that carried nothing wrong, and +// died on ERR-SYS-02 a few lines later. The refusal must name the flag, must not name +// the file, and must happen before the station takes itself out of service. +func TestAMalformedListenFlagIsBlamedOnTheFlagAndNotOnTheFile(t *testing.T) { + bench := newServeBench(t) + + ctx, cancel := context.WithTimeout(context.Background(), startBudget) + defer cancel() - // --listen is the one deliberate instruction that survives: it is what somebody - // types to move a station off an address that is already taken, and the neutral - // profile carries the address of every station of the parc. - moved := fallbackProfile(broken, "127.0.0.1:8099") - if moved.Network.Listen != "127.0.0.1:8099" { - t.Errorf("adresse du repli = %q : --listen a été perdu par le repli", moved.Network.Listen) + var out bytes.Buffer + err := runServe(ctx, []string{ + "--config", bench.configPath, "--data", bench.dataDir, "--listen", "8085"}, &out) + if err == nil { + t.Fatal("un --listen sans hôte a été accepté : le poste s'est lancé sur une adresse " + + "que personne ne peut lier") + } + + message := explain(err) + if !strings.Contains(message, "--listen") { + t.Fatalf("le refus ne nomme pas le drapeau fautif : %s", message) + } + if strings.Contains(message, bench.configPath) { + t.Fatalf("une faute de frappe en ligne de commande est imputée au fichier de "+ + "configuration : %s", message) + } + if got := out.String(); strings.Contains(got, "ERR-CFG-01") { + t.Fatalf("un poste sain est passé en configuration d'usine pour une faute de frappe "+ + "en ligne de commande :\n%s", got) + } +} + +// TestAFaultyListenAddressIsStillReportedWhenTheFlagIsGiven keeps the promise of §11.3: +// TOUTES les fautes, et pas celles que le drapeau a bien voulu laisser voir. +// +// A volunteer who runs `serve --listen ...` to get a station up must still leave with the +// list of what to repair in the file — including the address the file gets wrong. +// Applying the flag before the validation silently repaired the field for the duration of +// the run, and the fault came back at the next restart, alone in front of somebody who +// thought they had finished. +func TestAFaultyListenAddressIsStillReportedWhenTheFlagIsGiven(t *testing.T) { + asked := freeAddress(t) + bench := newServeBench(t, func(cfg *domain.Config) { + cfg.Network.Listen = "127.0.0.1" + }).listenFlag(asked) + bench.start() + + if got := bench.output(); !strings.Contains(got, "network.listen") { + t.Fatalf("le drapeau a effacé une faute du fichier : §11.3 promet toutes les fautes\n%s", got) + } + if bench.address != asked { + t.Fatalf("le poste sert sur %q alors que --listen demandait %q", bench.address, asked) + } + + if err := bench.stop(); err != nil { + t.Fatalf("serve a rendu une erreur sur un arrêt demandé : %v", err) } } @@ -327,6 +502,9 @@ type serveBench struct { configPath string dataDir string options serveOptions + // fileAddress is the address the CONFIGURATION FILE declares, kept apart from + // options.listen so that a test can say which of the two the station really bound. + fileAddress string cancel context.CancelFunc returned chan error @@ -372,18 +550,19 @@ func newServeBench(t *testing.T, tweak ...func(*domain.Config)) *serveBench { } b := &serveBench{ - t: t, - configPath: filepath.Join(dir, "config.json"), - dataDir: filepath.Join(dir, "data"), - out: &syncBuffer{}, - returned: make(chan error, 1), + t: t, + configPath: filepath.Join(dir, "config.json"), + dataDir: filepath.Join(dir, "data"), + fileAddress: cfg.Network.Listen, + out: &syncBuffer{}, + returned: make(chan error, 1), } writeConfig(t, b.configPath, cfg) - // The address travels as the FLAG as well as in the file, and that is what makes a - // bench with a deliberately invalid configuration testable: such a station falls back - // on the neutral profile, whose address is 127.0.0.1:8085 like every station of the - // parc — including the one this developer has installed on their own machine. Only - // --listen survives that fallback. + // The address travels as the FLAG as well as in the file, so that a bench never lands + // on 127.0.0.1:8085 — the address of every station of the parc, including the one this + // developer has installed on their own machine — whatever the station decides to bind. + // A test that means to prove WHICH of the two was served separates them with + // listenFlag. b.options = serveOptions{configPath: b.configPath, dataDir: b.dataDir, listen: cfg.Network.Listen} // No Timeout on the client: an SSE body is read for as long as the station keeps // it open, and a client-side deadline would end the stream itself — which is the @@ -397,6 +576,16 @@ func newServeBench(t *testing.T, tweak ...func(*domain.Config)) *serveBench { return b } +// listenFlag sets what --listen carries, the empty string meaning « no flag at all ». +// +// It is what tells the address of the FILE from the address of the FLAG: newServeBench +// puts the same one in both, so a bench left as it comes proves nothing about which of +// the two a station bound. +func (b *serveBench) listenFlag(address string) *serveBench { + b.options.listen = address + return b +} + // run executes the subcommand to completion and returns what it refused, which is what // a start-up failure test asserts on. func (b *serveBench) run(ctx context.Context) error { diff --git a/deploy/linux/update.sh b/deploy/linux/update.sh index 453abfc..40c6ead 100644 --- a/deploy/linux/update.sh +++ b/deploy/linux/update.sh @@ -45,9 +45,14 @@ ADDRESS=$(listen) healthy() { attempt=0 while [ "$attempt" -lt 60 ]; do - # L'adresse du fichier, PUIS celle du profil neutre : un poste dont la configuration - # est fautive sert sur la seconde (§11.3), et le conclure mort restaurerait la version - # précédente d'un poste parfaitement sain. + # DEUX adresses, et la seconde n'est pas une précaution vague. Un poste sert sur + # l'adresse que son fichier déclare même quand cette configuration est fautive par + # ailleurs : c'est celle qu'il faut interroger d'abord. Il ne retombe sur l'adresse du + # PROFIL NEUTRE (§11.3) que dans un seul cas, celui où network.listen est lui-même la + # faute — champ vide, ou adresse que le poste ne peut pas lier. N'interroger alors que + # l'adresse du fichier ferait conclure « le poste ne répond pas », et le retour arrière + # remettrait la version précédente d'un poste que la mise à jour n'a pas cassé, sans + # rien réparer de ce qui est en cause : le fichier. for candidate in "$ADDRESS" 'http://127.0.0.1:8085'; do if command -v curl >/dev/null 2>&1; then curl -fsS -m 2 "$candidate/healthz" >/dev/null 2>&1 && return 0 diff --git a/deploy/windows/common.ps1 b/deploy/windows/common.ps1 index 9a3f767..3b6a2c5 100644 --- a/deploy/windows/common.ps1 +++ b/deploy/windows/common.ps1 @@ -609,11 +609,12 @@ function Test-StationHealth { [CmdletBinding()] param([string]$Address = 'http://127.0.0.1:8085', [int]$TimeoutSeconds = 30) - # DEUX adresses, et la seconde n'est pas une précaution vague. Un poste dont la - # configuration est fautive démarre sur le PROFIL NEUTRE (§11.3) et sert donc sur - # l'adresse de ce profil, pas sur celle que le fichier déclare — c'est exactement l'état - # d'un poste fraîchement installé, dont il reste à régler le numéro, la balance et - # l'imprimante. Un update.ps1 qui n'interrogerait que l'adresse du fichier conclurait + # DEUX adresses, et la seconde n'est pas une précaution vague. Un poste sert sur + # l'adresse que son fichier déclare même quand cette configuration est fautive par + # ailleurs : c'est celle qu'il faut interroger d'abord. Il retombe sur l'adresse du + # PROFIL NEUTRE (§11.3) dans un seul cas, mais un cas courant — network.listen est + # lui-même la faute, ce qui est l'état d'un poste fraîchement installé dont le champ + # est vide. Un update.ps1 qui n'interrogerait que l'adresse du fichier conclurait alors # « le poste ne répond pas » et restaurerait la version précédente d'un poste sain, ce # qui est la panne que §15.5 demande d'éviter. $candidates = @($Address) diff --git a/go b/go new file mode 100644 index 0000000..e69de29 diff --git a/internal/domain/config.go b/internal/domain/config.go index c41c079..c3c8952 100644 --- a/internal/domain/config.go +++ b/internal/domain/config.go @@ -179,9 +179,15 @@ type NetworkConfig struct { AdminOnLAN bool `json:"admin_on_lan"` } -// UIConfig holds the only four screen settings an operator has a legitimate choice +// UIConfig holds the only five screen settings an operator has a legitimate choice // about. // +// The fifth one, ShowByUnitProducts, is a choice about the SHOP and not about the +// screen's looks: a tile sold by unit prints a label WITHOUT EVER READING THE SCALE, +// and a cooperative whose counter only weighs has no reason to offer that gesture. It +// is a display and never a refusal — Prepare still judges the qualification alone — so +// hiding a product closes no path, it only stops proposing it. +// // ABSENT ON PURPOSE, each for a written reason (§11.2, ADR-025): // - title: the name of the cooperative lives in station.coop; // - open_category: the idle view is the COMPLETE grid, categories are filters and @@ -203,6 +209,11 @@ type UIConfig struct { // Default 60: a trade-off between serving the customer and the fraud window. ReprintWindowSeconds int `json:"reprint_window_s"` ShowGridPrices bool `json:"show_grid_prices"` + // ShowByUnitProducts puts the by-unit tiles back into the grid. Default false: the + // key is named after the PREFIX of the barcode, which alone carries the sale mode, + // and never after the `unite` column of the CSV, which is a price label and decides + // nothing. + ShowByUnitProducts bool `json:"show_by_unit_products"` } // ScaleConfig is the weighing device of this station. @@ -1477,6 +1488,15 @@ func intOption(options DriverOptions, key string) (int, bool) { return int(value), ok } +// CheckListenAddress reports why an address cannot be listened on, and nil when it can. +// +// It is exported so that whoever accepts a listening address from OUTSIDE the file — +// `serve --listen`, and nothing else so far — judges it by the very rule control 2 +// judges network.listen by. A second implementation in the command layer would drift, +// and the station would end up refusing an address its own administration screen +// accepts, or the other way round. +func CheckListenAddress(address string) error { return checkHostPort(address) } + // checkHostPort reports why an address is not a usable host:port. func checkHostPort(address string) error { if address == "" { diff --git a/internal/domain/config_test.go b/internal/domain/config_test.go index 5a5bb98..0a4aa3e 100644 --- a/internal/domain/config_test.go +++ b/internal/domain/config_test.go @@ -1068,6 +1068,9 @@ func TestFingerprintChangesWhenASharedValueChanges(t *testing.T) { "le gabarit": func(c *Config) { c.Printer.Template = "weighing_integer_module" }, "une catégorie": func(c *Config) { c.Catalog.Categories[0].Visible = false }, "la rétention du journal": func(c *Config) { c.Journal.MaxDays = 30 }, + // Two stations that disagree here do not show the same grid: one offers fifteen + // tiles the other does not have, and the eight characters have to say so. + "les produits à l'unité montrés": func(c *Config) { c.UI.ShowByUnitProducts = true }, } { t.Run(name, func(t *testing.T) { diverging := loadDelivered(t) @@ -1079,6 +1082,59 @@ func TestFingerprintChangesWhenASharedValueChanges(t *testing.T) { } } +// TestAFileSilentAboutTheByUnitProductsHidesThem makes a silent consequence visible. +// +// UnmarshalJSON applies no default outside update.repository, so a file written before +// this key existed reads as « hide them » — a station updated in the field loses its +// by-unit tiles without a message and without a journal line. That is what was asked +// for; writing it down here is what keeps it a decision instead of a discovery made in +// front of an amputated grid. +func TestAFileSilentAboutTheByUnitProductsHidesThem(t *testing.T) { + for name, block := range map[string]struct { + ui string + wanted bool + }{ + "clé absente": {ui: `{"language":"fr"}`, wanted: false}, + "clé à faux": {ui: `{"language":"fr","show_by_unit_products":false}`, wanted: false}, + "clé à vrai": {ui: `{"language":"fr","show_by_unit_products":true}`, wanted: true}, + } { + t.Run(name, func(t *testing.T) { + var config Config + if err := json.Unmarshal([]byte(`{"version":1,"ui":`+block.ui+`}`), &config); err != nil { + t.Fatalf("décodage : %v", err) + } + if config.UI.ShowByUnitProducts != block.wanted { + t.Fatalf("show_by_unit_products relu à %v, attendu %v", + config.UI.ShowByUnitProducts, block.wanted) + } + }) + } +} + +// TestTheDeliveredFilesSayWhatTheyDoOfTheByUnitProducts: a key left out reads back with +// the meaning of the language's zero value, which is the costliest failure mode of this +// project. Both shipped files therefore spell it out, whatever they choose. +func TestTheDeliveredFilesSayWhatTheyDoOfTheByUnitProducts(t *testing.T) { + for _, name := range []string{"config-lacagette.json", "config-demo.json"} { + t.Run(name, func(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("..", "..", "testdata", name)) + if err != nil { + t.Fatalf("lecture de %s : %v", name, err) + } + var document struct { + UI map[string]json.RawMessage `json:"ui"` + } + if err := json.Unmarshal(raw, &document); err != nil { + t.Fatalf("décodage de %s : %v", name, err) + } + if _, written := document.UI["show_by_unit_products"]; !written { + t.Fatalf("%s ne porte pas show_by_unit_products : le fichier se relirait "+ + "en silence avec le sens du zéro du langage", name) + } + }) + } +} + func TestCanonicalJSONSortsKeysAndDropsWhitespace(t *testing.T) { canonical, err := CanonicalJSON(json.RawMessage(`{ "b": 1, "a": [ 2, { "d": 3, "c": 4 } ] }`)) if err != nil { diff --git a/internal/domain/prepare.go b/internal/domain/prepare.go index b79ca38..a167b81 100644 --- a/internal/domain/prepare.go +++ b/internal/domain/prepare.go @@ -184,6 +184,12 @@ func Prepare(in PrepareInput) (Preparation, error) { // rule 14 PRODUCT_WITHDRAWN, because a withdrawn product is exactly the case a // customer must be told about in French ("Ce produit n'est pas disponible."), // and because the admin screen wants to see what the weighing WOULD have cost. + // + // ui.show_by_unit_products is not read here EITHER, and that is a decision rather + // than an omission: it is a DISPLAY and never a refusal. A station that hides its + // by-unit tiles keeps printing their labels for a browser holding an older catalog, + // and the till reads them exactly as before. Closing that path would be a new + // safeguard, with its own rule number and its own French sentence. if in.Product.Qualification != Weighable { return Preparation{}, fmt.Errorf("%w: product %s is %s (%s)", ErrProductNotWeighable, in.Product.ID, in.Product.Qualification, in.Product.Reason) diff --git a/internal/domain/profiles.go b/internal/domain/profiles.go index b929924..576c5f8 100644 --- a/internal/domain/profiles.go +++ b/internal/domain/profiles.go @@ -60,6 +60,11 @@ func NeutralProfile() Config { IdleTimeoutSeconds: 45, ReprintWindowSeconds: 60, ShowGridPrices: true, + // Written out rather than left to the zero value: this profile is read as the + // documentation of what a factory station does, and a by-unit tile prints a + // label without ever reading the scale — a gesture no station offers until + // somebody decides it does. + ShowByUnitProducts: false, }, Scale: ScaleConfig{ diff --git a/internal/domain/profiles_test.go b/internal/domain/profiles_test.go index f1d6463..1dfe880 100644 --- a/internal/domain/profiles_test.go +++ b/internal/domain/profiles_test.go @@ -114,6 +114,24 @@ func TestNeutralProfileCarriesNoSiteValue(t *testing.T) { } } +// TestTheNeutralProfileHidesTheByUnitProducts, and says so in so many words. +// +// A tile sold by unit prints a label WITHOUT EVER READING THE SCALE: it is a gesture of +// its own, and a station that fell back to the factory profile must not start offering +// it because nobody wrote the key down. The profile still validates clean, which is the +// property the whole file exists to hold. +func TestTheNeutralProfileHidesTheByUnitProducts(t *testing.T) { + profile := NeutralProfile() + if profile.UI.ShowByUnitProducts { + t.Error("le profil d'usine montre les produits vendus à l'unité : " + + "un poste en configuration d'usine offrirait un geste que personne n'a décidé") + } + if faults := profile.Validate(testRegistries()); len(faults) != 0 { + t.Fatalf("le profil neutre doit passer sans faute ; obtenu :\n%s", + strings.Join(fieldsOf(faults), "\n")) + } +} + // TestNeutralProfileReadsNoClock: nothing in internal/domain reads a wall clock, so // the profile leaves modified_at to whoever writes it. func TestNeutralProfileReadsNoClock(t *testing.T) { diff --git a/internal/printing/preview/driver.go b/internal/printing/preview/driver.go new file mode 100644 index 0000000..311d388 --- /dev/null +++ b/internal/printing/preview/driver.go @@ -0,0 +1,399 @@ +package preview + +import ( + "context" + "errors" + "fmt" + "image" + "os" + "path/filepath" + "strings" + "sync" + + "openscale/internal/domain" + "openscale/internal/printing" + "openscale/internal/station/ports" +) + +// ID is the registry key of this driver and the value of printer.type (§8.1). +const ID = domain.PrinterPreview + +// Label is what a volunteer reads in the drop-down list of the administration screen. +// +// It says what the driver DOES and not only what it is called, and that sentence is the +// guard rail: `preview` is one wrong click away from `raster` in a list, and a station set +// on it goes on saying « Étiquette envoyée à l'imprimante » while nothing comes out. The +// production path is `raster` (ADR-002). +const Label = "Aperçu — écrit un fichier, n'imprime rien" + +// The three self-tests of §8.6, spelled as the troubleshooting route sends them. +const ( + // SelfTestLabel writes the demonstration label, which is exactly what this driver + // exists to produce. + SelfTestLabel = "label" + // SelfTestAlignment settles the polarity of the command and the registration of + // the media under the head. Both are facts about a machine this driver never drives. + SelfTestAlignment = "alignment" + // SelfTestRuler settles the pitch the head really prints at, which is the same case. + SelfTestRuler = "ruler" +) + +// maxCopies is what this driver honestly accepts: ONE. +// +// A preview writes a file, and n identical copies of a file are n identical files. A driver +// that produced five of them would be counting a roll that does not exist. +const maxCopies = 1 + +// The shape of the file names, and it is the one the `file` transport already writes +// (§8.4): the instant with colons replaced by hyphens, because a colon is not a legal +// character in a Windows file name and the parc is Windows. +const ( + stampLayout = "2006-01-02T15-04-05" + fileNameFormat = "%s_%03d%s" +) + +// The two extensions. They are the whole difference between the two files: same bitmap, +// one at the pitch of the head and one at exact physical scale. +const ( + imageExtension = ".png" + documentExtension = ".pdf" +) + +// maxNameAttempts bounds the search for a free pair of names inside one second. It is the +// bound the `file` transport uses, for the reason it gives: a thousand collisions in one +// second means something else is wrong, and failing with a name beats looping. +const maxNameAttempts = 1000 + +// Options is everything the preview driver is given, and nothing it could invent. +type Options struct { + // Dir is the directory the two files are written to. Required. + // + // It is handed down by the composition root and never read from printer.options, + // because it is a fact about THIS station's data directory. It is a directory of its + // own and deliberately not the one the `file` transport drops frames into: mixing + // previews and raw frames would make « envoyez-moi le fichier de la dernière + // étiquette » an ambiguous sentence, and that sentence is how support works. + Dir string + + // Clock dates the files and times a job. time.Now is out of reach here (§5.3). + Clock ports.Clock + + // Log is where the RENDER reports what a volunteer may have to act on: a name the + // automatic reduction had to truncate, a character no embedded font carries (§7.3). + // Nil is replaced by ports.NopTechnicalLog. + Log ports.TechnicalLog + + // Template is the template IN SERVICE (printer.template, §11.2). A print job carries + // its own and that one is used; this is what the self-test falls back on. + Template domain.Template + + // DemoLabel supplies the label the `label` self-test writes (§8.6). + // + // INJECTED, exactly as the raster driver takes it: a demonstration label carries a + // product and prices, which come from the catalog and the configuration. A printing + // driver that made up a price would be inventing a number nobody could check. + DemoLabel func() (domain.Label, error) +} + +// Printer is the preview driver. It satisfies ports.Printer. +// +// IT OPENS NO DEVICE, and that is what it is for. The three values of printer.type share +// the whole rendering and differ only by where the bitmap goes: `raster` hands it to the +// print queue of the system, `sbpl` writes it straight to the device, and this one writes +// the two files anybody can measure — for acceptance, for the ±1 dot adjustment, and for +// support at a distance. +// +// It is also what a station in FACTORY CONFIGURATION prints on (§11.3). The neutral profile +// carries no printer.options at all — darkness, speed and the number of copies are set on a +// real print run, and a factory profile has no business inventing them — so the driver such +// a station falls back on must require none of them. This one requires none. +type Printer struct { + dir string + clock ports.Clock + log ports.TechnicalLog + fonts *printing.Library + render *printing.Rasterizer + template domain.Template + demoLabel func() (domain.Label, error) + + // mu serialises everything that names a file: two jobs claiming the same instant must + // not claim the same sequence number. + mu sync.Mutex + sequence int + closed bool +} + +// New builds the driver from what a configuration and a composition root hand it. +// +// The messages about a missing collaborator are English: no configuration file can produce +// a nil clock, so only a developer can ever read them. +func New(o Options) (*Printer, error) { + if strings.TrimSpace(o.Dir) == "" { + return nil, errors.New("preview: New: no directory; this driver writes files and the " + + "composition root owns the data directory") + } + if o.Clock == nil { + return nil, errors.New("preview: New: no clock; a job is dated by the INJECTED clock (§5.3)") + } + + log := o.Log + if log == nil { + log = ports.NopTechnicalLog{} + } + fonts, err := printing.NewLibrary() + if err != nil { + return nil, fmt.Errorf("preview: New: %w", err) + } + render, err := printing.NewRasterizer(fonts, log) + if err != nil { + fonts.Close() + return nil, fmt.Errorf("preview: New: %w", err) + } + return &Printer{ + dir: o.Dir, clock: o.Clock, log: log, fonts: fonts, render: render, + template: o.Template, demoLabel: o.DemoLabel, + }, nil +} + +// Descriptor reports the identity of the driver and the capabilities it declares. +// +// Status is FALSE, and it is the one capability worth arguing about: there is no device to +// interrogate, so a driver claiming the capability would be claiming a return channel it +// invented. See Status for what it answers instead. +func (p *Printer) Descriptor() domain.PrinterDescriptor { + return domain.PrinterDescriptor{ + ID: ID, + Label: Label, + Capabilities: domain.PrinterCapabilities{ + Raster: true, + Status: false, + Cutter: false, + MaxCopies: maxCopies, + DotsPerMM: p.template.Media.DotsPerMM, + }, + } +} + +// Print renders one job and writes the two files. +// +// The copy count is deliberately IGNORED rather than refused: a preview writes one file, +// and the print service builds its job without a Copies field anyway (§8.2). Refusing a job +// over a number no file can honour would take a station in factory configuration out of the +// one thing it can still do. +func (p *Printer) Print(ctx context.Context, job ports.PrintJob) (ports.PrintReceipt, error) { + img, err := p.compose(job) + if err != nil { + return ports.PrintReceipt{}, err + } + return p.write(ctx, "preview.Print", job.Label.JobID, img, job.Template.Media) +} + +// compose turns a job into the bitmap the head WOULD have burnt, refusing what would show a +// wrong label rather than writing it. +// +// The barcode is checked before the render, for the reason the raster driver checks it +// there: an unusable reference is a product to fix in Odoo, and §8.5 classifies a failure by +// the action expected of a human. +func (p *Printer) compose(job ports.PrintJob) (*image.Gray, error) { + if _, err := domain.ParseEAN13(string(job.Label.Barcode)); err != nil { + return nil, &ports.PrintError{Kind: ports.KindData, Op: "preview.Print", Err: err, + Message: fmt.Sprintf("le code-barres %q de ce produit est inutilisable : %v", + string(job.Label.Barcode), err)} + } + template := job.Template + img, err := p.render.Rasterize(&template, job.Label, domain.Locale(job.Locale), printing.RenderOptions{}) + if err != nil { + return nil, &ports.PrintError{Kind: ports.KindTemplate, Op: "preview.Print", Err: err, + Message: fmt.Sprintf("l'étiquette n'a pas pu être dessinée : %v", err)} + } + return img, nil +} + +// write encodes the bitmap twice and reports how many bytes reached the disk. +// +// The PNG goes down FIRST and the PDF second, and both are written before the receipt is +// returned: the pair is what a volunteer is asked to send, and half of it is a support +// request that has to be made twice. +func (p *Printer) write(ctx context.Context, op, jobID string, img *image.Gray, media domain.Media) (ports.PrintReceipt, error) { + p.mu.Lock() + defer p.mu.Unlock() + if p.closed { + return ports.PrintReceipt{}, &ports.PrintError{Kind: ports.KindInternal, Op: op, + Message: "l'aperçu a été fermé : ce poste ne peut plus écrire d'étiquette sans redémarrer"} + } + if err := ctx.Err(); err != nil { + return ports.PrintReceipt{}, &ports.PrintError{Kind: ports.KindTransient, Op: op, Err: err, + Message: "l'aperçu a été interrompu avant d'écrire l'étiquette"} + } + + started := p.clock.Now() + base, err := p.reserve() + if err != nil { + return ports.PrintReceipt{}, &ports.PrintError{Kind: ports.KindTransient, Op: op, Err: err, + Message: fmt.Sprintf("l'étiquette n'a pas pu être écrite dans %s : %v", p.dir, err)} + } + + written := 0 + for _, target := range []struct { + path string + encode func(*os.File) error + }{ + {base + imageExtension, func(f *os.File) error { return EncodePNG(f, img) }}, + {base + documentExtension, func(f *os.File) error { return EncodePDF(f, img, media) }}, + } { + bytes, err := writeFile(target.path, target.encode) + if err != nil { + return ports.PrintReceipt{}, &ports.PrintError{Kind: ports.KindTransient, Op: op, Err: err, + Message: fmt.Sprintf("l'étiquette n'a pas pu être écrite dans %s : %v", target.path, err)} + } + written += bytes + } + return ports.PrintReceipt{ + JobID: jobID, Bytes: written, Duration: p.clock.Now().Sub(started), + }, nil +} + +// reserve claims a base name whose two files do not exist yet. +// +// EXCLUSIVE, like the `file` transport: a preview that silently replaced the one before it +// would lose the very label somebody asked for, and a support directory shared by two +// stations is exactly where that happens. +func (p *Printer) reserve() (string, error) { + if err := os.MkdirAll(p.dir, 0o755); err != nil { + return "", err + } + stamp := p.clock.Now().Format(stampLayout) + for attempt := 0; attempt < maxNameAttempts; attempt++ { + p.sequence++ + base := filepath.Join(p.dir, fmt.Sprintf(fileNameFormat, stamp, p.sequence, "")) + if free, err := bothNamesAreFree(base); err != nil { + return "", err + } else if free { + return base, nil + } + } + return "", fmt.Errorf("aucun nom libre après %d essais pour l'horodatage %s", + maxNameAttempts, stamp) +} + +// bothNamesAreFree reports that neither of the two files of one preview exists. +// +// BOTH, and not one: a directory holding the PNG of a previous run and not its PDF would +// otherwise produce a pair whose two halves describe different labels. +func bothNamesAreFree(base string) (bool, error) { + for _, extension := range []string{imageExtension, documentExtension} { + _, err := os.Stat(base + extension) + switch { + case err == nil: + return false, nil + case !errors.Is(err, os.ErrNotExist): + return false, err + } + } + return true, nil +} + +// writeFile creates one file and reports how many bytes it holds. +// +// O_EXCL, so that the freshness of a preview is a property of the file system and not of +// the sequence number this driver keeps in memory. +func writeFile(path string, encode func(*os.File) error) (int, error) { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + if err != nil { + return 0, err + } + if err := encode(file); err != nil { + file.Close() + return 0, err + } + // The size is read BEFORE the close and from the handle: a caller reporting a byte + // count it took from a second stat would be reporting on a file somebody else may have + // touched in between. + info, err := file.Stat() + if err != nil { + file.Close() + return 0, err + } + if err := file.Close(); err != nil { + return 0, err + } + return int(info.Size()), nil +} + +// Status reports what this driver honestly knows about itself. +// +// PrinterUnknown, and never PrinterReady. There is no device here, so there is nothing that +// can be offline or jammed — but there is nothing that can be declared ready either, and a +// green light over a station where no label ever comes out is the failure mode this whole +// driver has to be careful about (§14.5). The detail says, in one sentence a volunteer can +// act on, that nothing is being printed and where the files are going. +func (p *Printer) Status(context.Context) ports.PrinterStatus { + p.mu.Lock() + defer p.mu.Unlock() + if p.closed { + return ports.PrinterStatus{Health: ports.PrinterUnknown, + Detail: "l'aperçu a été fermé par le poste."} + } + return ports.PrinterStatus{Health: ports.PrinterUnknown, + Detail: fmt.Sprintf("aperçu : aucune étiquette n'est imprimée, chacune est écrite "+ + "dans %s.", p.dir)} +} + +// SelfTest writes the demonstration label, and refuses the two patterns that settle facts +// about a print head (§8.6). +// +// The refusal is not a gap to be filled later. `alignment` lifts the polarity of the +// command and the registration of the media, `ruler` the pitch the head really prints at: +// both are read off PAPER, and a file that claimed to answer them would answer about +// itself. +func (p *Printer) SelfTest(ctx context.Context, what string) error { + switch what { + case SelfTestLabel: + return p.writeDemoLabel(ctx) + case SelfTestAlignment, SelfTestRuler: + return &ports.PrintError{Kind: ports.KindConfig, Op: "preview.SelfTest", + Message: fmt.Sprintf("l'auto-test « %s » se lit sur une étiquette imprimée : il "+ + "règle la tête d'impression, et ce poste écrit un fichier sans rien imprimer. "+ + "Choisissez le pilote « %s » sur une vraie imprimante.", what, domain.PrinterRaster)} + } + return &ports.PrintError{Kind: ports.KindConfig, Op: "preview.SelfTest", + Message: fmt.Sprintf("auto-test inconnu %q : les auto-tests disponibles sont %s, %s et %s", + what, SelfTestLabel, SelfTestAlignment, SelfTestRuler)} +} + +// writeDemoLabel writes the demonstration label of the `label` self-test. +func (p *Printer) writeDemoLabel(ctx context.Context) error { + if p.demoLabel == nil { + return &ports.PrintError{Kind: ports.KindConfig, Op: "preview.SelfTest.label", + Message: "aucune étiquette de démonstration n'a été fournie à l'aperçu : " + + "l'étiquette de test porte un produit et des prix, qui viennent du catalogue et de la " + + "configuration du poste, jamais du driver"} + } + label, err := p.demoLabel() + if err != nil { + return &ports.PrintError{Kind: ports.KindData, Op: "preview.SelfTest.label", Err: err, + Message: fmt.Sprintf("l'étiquette de démonstration n'a pas pu être préparée : %v", err)} + } + _, err = p.Print(ctx, ports.PrintJob{ + Label: label, Template: p.template, Locale: string(domain.LocaleFrench), Copies: 1, + }) + return err +} + +// Close releases the font faces the renderer memoised, and nothing else. +// +// There is no handle to give up — that is the point of this driver — but the font library is +// a real resource and a reload replaces the printer (§11.4). It is idempotent, because the +// Hub closes on a reload and again on shutdown. +func (p *Printer) Close() error { + p.mu.Lock() + defer p.mu.Unlock() + if p.closed { + return nil + } + p.closed = true + p.fonts.Close() + return nil +} + +var _ ports.Printer = (*Printer)(nil) diff --git a/internal/printing/preview/driver_test.go b/internal/printing/preview/driver_test.go new file mode 100644 index 0000000..858077b --- /dev/null +++ b/internal/printing/preview/driver_test.go @@ -0,0 +1,221 @@ +package preview + +import ( + "context" + "errors" + "image/png" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "openscale/internal/domain" + "openscale/internal/fake" + "openscale/internal/station/ports" +) + +// The tests of the `preview` DRIVER — the ports.Printer of §8.1, as opposed to the +// encoders the rest of this package tests. +// +// What they establish is the contract a station operates through: one job produces the two +// files anybody can measure, the driver opens no device and claims no readiness for one, +// and closing it releases nothing because it holds nothing. + +// previewStart is the instant the fake clock is frozen at. Fixed, so that a file name is +// the same from one run to the next. +var previewStart = time.Date(2026, 7, 24, 14, 32, 5, 0, time.UTC) + +// The reference vector of §18: 0493021000003 weighed at 1 236 g. +const ( + referenceCode = "0493021000003" + referenceMass = 1236 +) + +// TestUnJobDApercuEcritUnPNGEtUnPDFDansLeRepertoireDonne. +// +// The two files are the whole point of this driver: the PNG is what the head would have +// burnt, dot for dot, and the PDF is that same bitmap at exact physical scale so that a +// ruler settles the geometry with no printer in the room (§18, L4). +func TestUnJobDApercuEcritUnPNGEtUnPDFDansLeRepertoireDonne(t *testing.T) { + dir := t.TempDir() + printer := newPreview(t, dir) + defer printer.Close() + + template := domain.NeutralSingleTemplate() + receipt, err := printer.Print(context.Background(), ports.PrintJob{ + Label: referenceLabel(t), Template: template, Locale: string(domain.LocaleFrench), + }) + if err != nil { + t.Fatalf("Print : %v", err) + } + if receipt.Bytes <= 0 { + t.Fatal("le reçu ne compte aucun octet écrit") + } + + images := filesWithSuffix(t, dir, ".png") + documents := filesWithSuffix(t, dir, ".pdf") + if len(images) != 1 || len(documents) != 1 { + t.Fatalf("%d PNG et %d PDF écrits dans %s, attendu un de chaque", + len(images), len(documents), dir) + } + + // The PNG is at the pitch of the head: one pixel per dot, and never resized. + file, err := os.Open(images[0]) + if err != nil { + t.Fatalf("ouverture du PNG : %v", err) + } + defer file.Close() + config, err := png.DecodeConfig(file) + if err != nil { + t.Fatalf("le PNG écrit n'est pas lisible : %v", err) + } + width, height := mediaDots(template.Media) + if config.Width != width || config.Height != height { + t.Fatalf("le PNG fait %d × %d dots, attendu %d × %d : il n'est pas au pas de la tête", + config.Width, config.Height, width, height) + } + + // The PDF page is the media, in points derived from the micrometres of the template. + raw, err := os.ReadFile(documents[0]) + if err != nil { + t.Fatalf("lecture du PDF : %v", err) + } + document := parsePDF(t, raw) + if !within(micrometres(document.pageWidth), float64(template.Media.WidthUM)) || + !within(micrometres(document.pageHeight), float64(template.Media.HeightUM)) { + t.Fatalf("la page fait %.0f × %.0f µm, attendu le média de %d × %d µm", + micrometres(document.pageWidth), micrometres(document.pageHeight), + template.Media.WidthUM, template.Media.HeightUM) + } +} + +// TestUnApercuNeSignaleAucunePanneMaterielle. +// +// There is no device, so there is nothing that can be offline, jammed or out of ribbon — +// and nothing that can be declared READY either. What the status says instead is what a +// volunteer standing in front of a station configured this way needs to read: no label is +// coming out, and here is where they are being written. +func TestUnApercuNeSignaleAucunePanneMaterielle(t *testing.T) { + dir := t.TempDir() + printer := newPreview(t, dir) + defer printer.Close() + + status := printer.Status(context.Background()) + if status.Health == ports.PrinterFaulted || status.Health == ports.PrinterConsumable { + t.Fatalf("un aperçu déclare une panne matérielle : %+v", status) + } + if status.Health == ports.PrinterReady { + t.Fatal("un aperçu se déclare prête à imprimer : le voyant serait au vert sur un " + + "poste dont aucune étiquette ne sort") + } + if !strings.Contains(status.Detail, dir) { + t.Fatalf("l'état ne dit pas où les étiquettes sont écrites : %q", status.Detail) + } +} + +// TestFermerUnApercuNeLibereRien. +// +// Close is called on a configuration reload and again on shutdown (§11.4, §13.4), so it is +// idempotent here as it is everywhere else — and it holds no handle to give up. +func TestFermerUnApercuNeLibereRien(t *testing.T) { + printer := newPreview(t, t.TempDir()) + if err := printer.Close(); err != nil { + t.Fatalf("première fermeture : %v", err) + } + if err := printer.Close(); err != nil { + t.Fatalf("seconde fermeture : %v", err) + } +} + +// TestUnApercuRefuseLesDeuxAutoTestsQuiPortentSurLaTete. +// +// `alignment` settles the polarity of the command and `ruler` the real pitch of the +// head — two facts about a machine this driver does not drive. Answering them with a file +// would be answering a question nobody asked, so the refusal names what they are for. +func TestUnApercuRefuseLesDeuxAutoTestsQuiPortentSurLaTete(t *testing.T) { + printer := newPreview(t, t.TempDir()) + defer printer.Close() + + for _, what := range []string{"alignment", "ruler"} { + err := printer.SelfTest(context.Background(), what) + if err == nil { + t.Fatalf("l'auto-test %q a produit un fichier : il porte sur la tête d'impression", what) + } + var refusal *ports.PrintError + if !errors.As(err, &refusal) || refusal.Kind != ports.KindConfig { + t.Fatalf("le refus de %q n'est pas un refus de configuration : %v", what, err) + } + } +} + +// TestLAutoTestEtiquetteDUnApercuEcritLesDeuxFichiers keeps the refusal above from being a +// driver that refuses everything: the demonstration label is a label, and this driver's +// whole job is to produce one that can be looked at. +func TestLAutoTestEtiquetteDUnApercuEcritLesDeuxFichiers(t *testing.T) { + dir := t.TempDir() + printer := newPreview(t, dir) + defer printer.Close() + + if err := printer.SelfTest(context.Background(), "label"); err != nil { + t.Fatalf("auto-test « label » : %v", err) + } + if got := len(filesWithSuffix(t, dir, ".pdf")); got != 1 { + t.Fatalf("%d PDF écrits par l'auto-test, attendu 1", got) + } +} + +// newPreview builds the driver the way the composition root does, minus the directory, +// which every test owns. +func newPreview(t *testing.T, dir string) *Printer { + t.Helper() + printer, err := New(Options{ + Dir: dir, + Clock: fake.NewClock(previewStart), + Template: domain.NeutralSingleTemplate(), + DemoLabel: func() (domain.Label, error) { + return referenceLabel(t), nil + }, + }) + if err != nil { + t.Fatalf("preview.New : %v", err) + } + return printer +} + +// referenceLabel builds the label of the reference vector through the ONE calculation path +// of the application, so that what this driver writes and what a real weighing prints +// cannot drift apart. +func referenceLabel(t *testing.T) domain.Label { + t.Helper() + product := domain.Product{ + Name: "AIL", Reference: referenceCode, UnitPrice: 532, + PriceSuffix: " €/kg", Mode: domain.ByWeight, Qualification: domain.Weighable, + } + label, err := domain.Price(product, + domain.Measurement{Gross: referenceMass}, domain.SingleTierRules()) + if err != nil { + t.Fatalf("calcul de l'étiquette de référence : %v", err) + } + plan, err := domain.PlanFor(product.Reference) + if err != nil { + t.Fatalf("plan de numérotation : %v", err) + } + code, err := domain.Generate(product.Reference, referenceMass, plan.PayloadWidth) + if err != nil { + t.Fatalf("génération du code-barres : %v", err) + } + label.Barcode = code + return label +} + +// filesWithSuffix lists what the driver wrote, sorted, so an assertion reads the same way +// from one run to the next. +func filesWithSuffix(t *testing.T, dir, suffix string) []string { + t.Helper() + matches, err := filepath.Glob(filepath.Join(dir, "*"+suffix)) + if err != nil { + t.Fatalf("lecture de %s : %v", dir, err) + } + return matches +} diff --git a/internal/printing/registry.go b/internal/printing/registry.go index cfdbcf0..c325cc2 100644 --- a/internal/printing/registry.go +++ b/internal/printing/registry.go @@ -32,8 +32,19 @@ type DriverConfig struct { // Transport is the byte layer that carries the frame to the head (§8.4). It is // built by the composition root from printer.options.transport, because a printer // driver never opens a device itself — that is exactly what lets one frame reach - // four destinations. The `preview` driver ignores it. + // four destinations. + // + // It is NIL for a driver that declares no `transport` option, which is what `preview` + // is: it writes files and opens nothing. The composition root builds a transport only + // for the drivers whose own schema asks for one. Transport ports.Transport + // OutputDir is where a driver that PRODUCES FILES writes them, and it is empty for a + // driver that hands its bytes to a device. + // + // It comes from the composition root for the same reason the transport does: a driver + // never picks a path of its own, because the data directory is a fact about the + // station and not about the driver. + OutputDir string // Template is the label layout in service (printer.template). Template domain.Template // Clock times a job. time.Now is out of reach of every driver (§5.3). diff --git a/internal/scale/serial/options.go b/internal/scale/serial/options.go index 9894ad9..99be6e0 100644 --- a/internal/scale/serial/options.go +++ b/internal/scale/serial/options.go @@ -211,8 +211,37 @@ func optionInt(o domain.DriverOptions, key string) (int64, bool, error) { return value, true, nil } +// Complete returns these options with every link setting a caller left out filled in +// from the defaults of the parc, and refuses — in French — a link no port could accept. +// +// # Why it is exported +// +// The godoc of Options promises that « the zero value is usable once Port, Decoder and +// Clock are set ». That promise was only ever kept by New, Loop and ParseOptions, the +// three callers of the private completion below. Anything else that built an Options +// field by field and handed it straight to OpenSystemPort got a link with no bitrate, no +// character size, no parity and no stop bits — refused on the parity before the device +// was ever touched. The detection of the Matériel screen was built that way, and could +// not succeed on any port of any machine. +// +// # Why it is not hidden inside OpenSystemPort +// +// Repairing the options inside the opener would close the same hole in one line, and it +// would also make the repair invisible at the place that matters: where a caller BINDS a +// port. A caller must be able to read, at its own call site, that the link was completed, +// and it must be able to tell a refusal of its own settings — which names the key of +// scale.options to correct — from a refusal that came from the system, which is a +// different remedy for whoever is standing in front of the station. +func (o Options) Complete() (Options, error) { + o = o.withDefaults() + if _, err := o.mode(); err != nil { + return Options{}, err + } + return o, nil +} + // withDefaults fills in every field a configuration left out. It is idempotent, so -// New, Loop and ParseOptions may all call it. +// New, Loop, ParseOptions and Complete may all call it. func (o Options) withDefaults() Options { if o.Baud == 0 { o.Baud = defaultBaud diff --git a/internal/scale/serial/options_test.go b/internal/scale/serial/options_test.go index 218cfaa..32ff3b8 100644 --- a/internal/scale/serial/options_test.go +++ b/internal/scale/serial/options_test.go @@ -266,6 +266,93 @@ func TestOpenSystemPortRefusesBeforeItTouchesTheHardware(t *testing.T) { } } +// TestCompleteMakesAnyCallerAbleToOpenThePort guards the promise the godoc of Options +// makes to everybody, and used to keep for three callers only. +// +// « The zero value is usable once Port, Decoder and Clock are set » was true through New, +// Loop and ParseOptions, the three callers of the private completion, and false for +// anything else. A caller outside this package that built an Options field by field — the +// detection of the Matériel screen did exactly that — handed OpenSystemPort a link with +// no parity and got a refusal naming the parity, on every port and on every machine. +func TestCompleteMakesAnyCallerAbleToOpenThePort(t *testing.T) { + link, err := Options{Port: "COM8"}.Complete() + if err != nil { + t.Fatalf("des réglages réduits au port sont refusés : %v", err) + } + want := linkSettings{ + Port: "COM8", Baud: 9600, Bits: 8, Parity: "N", Stop: 1, + ReadBufferSize: defaultReadBufferSize, + BackoffMin: 200 * time.Millisecond, + BackoffMax: 5 * time.Second, + } + if got := linkOf(link); got != want { + t.Errorf("liaison complétée %+v, attendu %+v", got, want) + } + if link.Open == nil { + t.Error("Open nil : sans réglage explicite c'est le vrai port série qui est visé") + } + + // What a station really declared is never overwritten, and a second pass moves nothing. + declared, err := Options{Port: "COM8", Baud: 19200, Bits: 7, Parity: "E", Stop: 2}.Complete() + if err != nil { + t.Fatalf("des réglages déclarés sont refusés : %v", err) + } + if declared.Baud != 19200 || declared.Bits != 7 || declared.Parity != "E" || declared.Stop != 2 { + t.Errorf("les valeurs déclarées ont été écrasées : %+v", linkOf(declared)) + } + twice, err := declared.Complete() + if err != nil { + t.Fatalf("second passage : %v", err) + } + if linkOf(twice) != linkOf(declared) { + t.Errorf("second passage %+v, attendu %+v", linkOf(twice), linkOf(declared)) + } + + // And a completed link really does get as far as the DEVICE: the refusal of a port no + // machine can carry names the port, never the parity. That is the whole point — a + // settings refusal and a system refusal are two different remedies for a volunteer. + nowhere, err := Options{Port: "OPENSCALE-NO-SUCH-PORT"}.Complete() + if err != nil { + t.Fatalf("la complétion refuse un port qu'elle n'a pas à juger : %v", err) + } + _, err = OpenSystemPort(nowhere) + if err == nil { + t.Fatal("un port inexistant a été ouvert") + } + if strings.Contains(err.Error(), "parité") { + t.Errorf("la liaison complétée est encore refusée sur ses réglages : %v", err) + } +} + +// TestCompleteRefusesSettingsNoPortCouldAccept: completing is not tolerating. +// +// A link that no port could ever accept must be named BEFORE the device is touched, with +// the key of scale.options to correct — a bénévole reading « accès refusé » on a parity +// typo would go hunting for a process holding the port. +func TestCompleteRefusesSettingsNoPortCouldAccept(t *testing.T) { + for _, tc := range []struct { + name string + options Options + names string // the key the message must name + }{ + {"une parité inexistante", Options{Port: "COM8", Parity: "K"}, optionParity}, + {"trois bits d'arrêt", Options{Port: "COM8", Stop: 3}, optionStop}, + {"neuf bits de données", Options{Port: "COM8", Bits: 9}, optionBits}, + {"une vitesse négative", Options{Port: "COM8", Baud: -1}, optionBaud}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.options.Complete() + if err == nil { + t.Fatalf("réglages acceptés : %+v", linkOf(tc.options)) + } + if !strings.Contains(err.Error(), tc.names) { + t.Errorf("message %q, il doit nommer %q — un bénévole doit savoir quel "+ + "champ corriger", err, tc.names) + } + }) + } +} + func TestWithDefaultsIsIdempotent(t *testing.T) { // New, Loop and ParseOptions all call it, and a second pass must not move a value a // station really declared. diff --git a/internal/station/failures_test.go b/internal/station/failures_test.go index 2b148e6..a29e254 100644 --- a/internal/station/failures_test.go +++ b/internal/station/failures_test.go @@ -90,7 +90,7 @@ func TestTheScaleComesBackInsideTwoHundredMilliseconds(t *testing.T) { next := b.hub.Config() next.Scale.Options = mustOptions(t, `{"port":"COM9"}`) - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } driver := forge.last() diff --git a/internal/station/reload_test.go b/internal/station/reload_test.go index feb2346..3e3e934 100644 --- a/internal/station/reload_test.go +++ b/internal/station/reload_test.go @@ -66,7 +66,7 @@ func TestAnInstantBlockNeverCutsAnything(t *testing.T) { next.Limits.MinWeight = 42 next.UI.ReprintWindowSeconds = 120 - outcome, err := b.station.Reload(next) + outcome, err := b.station.Reload(ReloadRequest{Next: next}) if err != nil { t.Fatalf("Reload : %v", err) } @@ -99,7 +99,7 @@ func TestSerialToManualAndBack(t *testing.T) { manual := b.hub.Config() manual.Scale.Present = false manual.Scale.Type = "" - if _, err := b.station.Reload(manual); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: manual}); err != nil { t.Fatalf("Reload vers manuel : %v", err) } if !b.scale.Closed() { @@ -114,7 +114,7 @@ func TestSerialToManualAndBack(t *testing.T) { serial := b.hub.Config() serial.Scale.Present = true serial.Scale.Type = "gram-xfoc-plus" - if _, err := b.station.Reload(serial); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: serial}); err != nil { t.Fatalf("Reload vers série : %v", err) } if forge.count() != 1 { @@ -146,7 +146,7 @@ func TestAStartThatFailsBeforeItsGoroutineStillAnswers(t *testing.T) { next.Scale.Options = mustOptions(t, `{"port":"COM9"}`) started := time.Now() - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } if elapsed := time.Since(started); elapsed > 20*time.Millisecond { @@ -178,7 +178,7 @@ func TestACloseThatNeverReturnsIsBounded(t *testing.T) { started := time.Now() done := make(chan error, 1) - go func() { _, err := b.station.Reload(next); done <- err }() + go func() { _, err := b.station.Reload(ReloadRequest{Next: next}); done <- err }() // Nothing moves until the INJECTED clock does. select { @@ -250,7 +250,7 @@ func TestAHardwareChangeArmsTheCountdownAndCanBeConfirmed(t *testing.T) { next := b.hub.Config() next.Scale.Options = mustOptions(t, `{"port":"COM9"}`) - outcome, err := b.station.Reload(next) + outcome, err := b.station.Reload(ReloadRequest{Next: next}) if err != nil { t.Fatalf("Reload : %v", err) } @@ -283,7 +283,7 @@ func TestAnUnconfirmedHardwareChangeGoesBack(t *testing.T) { before := b.hub.Config() next := before next.Scale.Options = mustOptions(t, `{"port":"COM9"}`) - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } @@ -470,7 +470,7 @@ func TestThePrinterBlockRebuildsAndReleasesTheOldOne(t *testing.T) { next := b.hub.Config() next.Printer.Options = mustOptions(t, `{"transport":"winspool","queue":"SATO WS408_3"}`) - outcome, err := b.station.Reload(next) + outcome, err := b.station.Reload(ReloadRequest{Next: next}) if err != nil { t.Fatalf("Reload : %v", err) } @@ -498,7 +498,7 @@ func TestAPrinterThatCannotBeRebuiltKeepsTheOneThatWorks(t *testing.T) { next := b.hub.Config() next.Printer.Options = mustOptions(t, `{"transport":"winspool","queue":"inconnue"}`) - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } if b.printer.Closed() { @@ -526,7 +526,7 @@ func TestTheCatalogBlockFollowsTheStationNumber(t *testing.T) { next := b.hub.Config() next.Station.Number = 3 - outcome, err := b.station.Reload(next) + outcome, err := b.station.Reload(ReloadRequest{Next: next}) if err != nil { t.Fatalf("Reload : %v", err) } @@ -597,7 +597,7 @@ func TestTheWatchLeavesTheSourceAReloadReplaced(t *testing.T) { b.station.newCatalogSource = func(domain.Config) (ports.CatalogSource, error) { return second, nil } next := b.hub.Config() next.Station.Number = 3 - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } @@ -618,7 +618,7 @@ func TestReplacingTheSourceIsNotAReadFailure(t *testing.T) { b.station.newCatalogSource = func(domain.Config) (ports.CatalogSource, error) { return second, nil } next := b.hub.Config() next.Station.Number = 3 - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } awaitEntry(t, second, "la veille est restée dans la source remplacée") @@ -633,7 +633,7 @@ func TestReplacingTheSourceIsNotAReadFailure(t *testing.T) { return nil, errors.New("partage inaccessible") } next.Station.Number = 4 - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } awaitCondition(t, func() bool { return b.technical.has("ERR-CAT-05") }, @@ -659,7 +659,7 @@ func TestTheWatchPicksUpASourceItStartedWithout(t *testing.T) { next := b.hub.Config() next.Station.Number = 3 - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } @@ -676,7 +676,7 @@ func TestACatalogSourceThatCannotBeRebuiltIsJournalled(t *testing.T) { } next := b.hub.Config() next.Station.Number = 4 - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } if b.hub.Catalog() == nil { @@ -694,7 +694,7 @@ func TestTheListenAddressArmsTheCountdownWithoutRestartingAProcess(t *testing.T) next := b.hub.Config() next.Network.Listen = "127.0.0.1:8086" - outcome, err := b.station.Reload(next) + outcome, err := b.station.Reload(ReloadRequest{Next: next}) if err != nil { t.Fatalf("Reload : %v", err) } diff --git a/internal/station/revert_test.go b/internal/station/revert_test.go index 3f753ea..fdceb4c 100644 --- a/internal/station/revert_test.go +++ b/internal/station/revert_test.go @@ -15,9 +15,10 @@ import ( // then restarted by the SCM or by a power cut would come back on the configuration nobody // confirmed, sixty seconds after the branch was supposed to be safe. // -// internal/station knows no file, so what it does is CALL BACK with the configuration it has -// just put in service. The composition root writes it, through the same atomic path as any -// other save. +// internal/station knows no file, so what it does is CALL BACK with the document that file +// carried before the save. The composition root writes it, through the same atomic path as +// any other save. This caller hands no FileBefore, so the two documents are the same one — +// which is the ordinary station, and the reason the defect stayed invisible for so long. func TestARollbackPutsTheFileBackTooIsWhatMakesTheCountdownLastPastARestart(t *testing.T) { reverted := &revertRecorder{} forge := &scaleForge{} @@ -31,7 +32,7 @@ func TestARollbackPutsTheFileBackTooIsWhatMakesTheCountdownLastPastARestart(t *t next := b.hub.Config() next.Scale.Type = "gram-xfoc-rs" - outcome, err := b.station.Reload(next) + outcome, err := b.station.Reload(ReloadRequest{Next: next}) if err != nil { t.Fatalf("Reload : %v", err) } @@ -74,7 +75,7 @@ func TestAConfirmedChangeNeverWritesTheFileBack(t *testing.T) { next := b.hub.Config() next.Scale.Type = "gram-xfoc-rs" - if _, err := b.station.Reload(next); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { t.Fatalf("Reload : %v", err) } if err := b.station.Confirm(); err != nil { @@ -92,6 +93,89 @@ func TestAConfirmedChangeNeverWritesTheFileBack(t *testing.T) { } } +// TestLaCibleDuRetourArriereEstLeFichierEtPasCeQueLePosteFaitTourner. +// +// The countdown has TWO documents to put back and they are not the same one. The station +// goes back to what it was RUNNING — on a station whose file is faulty that is the neutral +// profile, and applying anything else would hand a live station a configuration §11.3 says +// never to run. The FILE goes back to what IT carried, which is the cooperative's own. +// +// Remembering only one of the two is what wrote the factory profile over a shop's file. +func TestLaCibleDuRetourArriereEstLeFichierEtPasCeQueLePosteFaitTourner(t *testing.T) { + reverted := &revertRecorder{} + forge := &scaleForge{} + b := newBench(t, func(o *benchOptions) { + o.newScale = forge.New + o.onRevert = reverted.record + // What a station whose configuration is unusable RUNS (§11.3). + o.config = func(cfg *domain.Config) { *cfg = domain.NeutralProfile() } + }) + forge.clock = b.clock + + running := b.hub.Config() + file := loadConfig(t) + if file.Station.Coop == running.Station.Coop { + t.Fatal("le fichier et ce qui tourne portent la même coopérative : le banc ne " + + "distingue plus les deux documents") + } + + next := loadConfig(t) + next.Scale.Type = "gram-xfoc-rs" + outcome, err := b.station.Reload(ReloadRequest{Next: next, FileBefore: &file}) + if err != nil { + t.Fatalf("Reload : %v", err) + } + if outcome.ConfirmBefore.IsZero() { + t.Fatal("un changement de matériel n'arme aucun compte à rebours") + } + + b.clock.Advance(confirmationWindow + time.Second) + awaitCondition(t, func() bool { return reverted.count() == 1 }, + "le retour arrière n'a jamais été signalé à qui écrit le fichier") + + if got := reverted.last().Station.Coop; got != file.Station.Coop { + t.Errorf("le fichier serait réécrit avec la coopérative %q, attendu celle qu'il "+ + "portait, %q", got, file.Station.Coop) + } + if got := b.hub.Config().Station.Coop; got != running.Station.Coop { + t.Errorf("le poste fait tourner la coopérative %q, attendu celle qu'il faisait "+ + "tourner, %q : le fichier a été appliqué au poste vivant", got, running.Station.Coop) + } +} + +// TestSansFichierDAvantLeRetourArriereRepondCeQueLePosteFaisaitTourner. +// +// FileBefore is a POINTER and nil is a legitimate answer: it means « je n'ai pas pu lire le +// fichier », and what a caller in that state possesses is the configuration in service. The +// fallback has to stay, or a station whose file is unreadable would see its rollback write +// the zero configuration — a document nobody ever validated, and one that no station starts on. +func TestSansFichierDAvantLeRetourArriereRepondCeQueLePosteFaisaitTourner(t *testing.T) { + reverted := &revertRecorder{} + forge := &scaleForge{} + b := newBench(t, func(o *benchOptions) { + o.newScale = forge.New + o.onRevert = reverted.record + }) + forge.clock = b.clock + + before := b.hub.Config() + next := b.hub.Config() + next.Scale.Type = "gram-xfoc-rs" + if _, err := b.station.Reload(ReloadRequest{Next: next}); err != nil { + t.Fatalf("Reload : %v", err) + } + + b.clock.Advance(confirmationWindow + time.Second) + awaitCondition(t, func() bool { return reverted.count() == 1 }, + "le retour arrière n'a jamais été signalé à qui écrit le fichier") + + rewritten := reverted.last() + if got, want := rewritten.Fingerprint(), before.Fingerprint(); got != want { + t.Fatalf("le fichier serait réécrit avec l'empreinte %s, attendu %s : un appelant qui "+ + "n'a pas pu lire le fichier ne possède que ce qui tourne", got, want) + } +} + // revertRecorder keeps what the station handed back, so that « le fichier revient en // arrière » is an assertion and not a hope. type revertRecorder struct { @@ -99,11 +183,11 @@ type revertRecorder struct { configs []domain.Config } -// record is the hook the station calls. -func (r *revertRecorder) record(previous domain.Config) { +// record is the hook the station calls with the file as it was before the save. +func (r *revertRecorder) record(fileBefore domain.Config) { r.mu.Lock() defer r.mu.Unlock() - r.configs = append(r.configs, previous) + r.configs = append(r.configs, fileBefore) } // count reports how many rollbacks were signalled. diff --git a/internal/station/station.go b/internal/station/station.go index 3244512..e55071e 100644 --- a/internal/station/station.go +++ b/internal/station/station.go @@ -166,15 +166,21 @@ type Options struct { Store Closer // CatalogWait rolls an import transaction back before the database closes. CatalogWait Waiter - // OnRevert is called when the 60 s window of §11.4 closed without a confirmation and - // the station has just gone back to the configuration it had. + // OnRevert is called when the 60 s window of §11.4 closed without a confirmation, and it + // receives THE FILE AS IT WAS BEFORE THE SAVE — never the configuration the station was + // running. // // It exists because the countdown protects the RUNNING station and the file is written // before it starts: without this hook, a station that rolled back would come back, at // the next restart, on the very configuration nobody confirmed — which is exactly the // branch the countdown was cutting. What it does is the caller's business; internal/ // station knows no file. - OnRevert func(previous domain.Config) + // + // The two documents are distinct on the one station this matters for. A station whose + // configuration is unusable RUNS the neutral profile (§11.3) while its file keeps the + // cooperative's tariffs, safeguards and categories: handing the running configuration + // over here wrote the factory profile onto that file, on the very save that repaired it. + OnRevert func(fileBefore domain.Config) } // Station is the running weighing station: the Hub, its two workers, its @@ -243,7 +249,7 @@ type Station struct { confirmation *pendingConfirmation // onRevert puts the FILE back when nobody confirmed. Nil does nothing, which is what // every test that does not care about the file gets. - onRevert func(previous domain.Config) + onRevert func(fileBefore domain.Config) // started reports that Start launched the loop and the workers. Stop drains // nothing that was never launched: a process that failed before Start must @@ -259,9 +265,14 @@ type Station struct { duration time.Duration } -// pendingConfirmation is the configuration to go back to if nobody confirms. +// pendingConfirmation is what to go back to if nobody confirms — and it is TWO documents, +// because a station and its file do not always carry the same one. type pendingConfirmation struct { - previous domain.Config + // running is what the station was OPERATING ON, and it is what goes back into service. + running domain.Config + // file is what the configuration file CARRIED before the save, and it is what goes + // back on disk. On a station out of service the two differ completely (§11.3). + file domain.Config deadline time.Time } @@ -419,27 +430,68 @@ type ReloadOutcome struct { ConfirmBefore time.Time } +// ReloadRequest is one configuration change, with the document the rollback would have to +// put back on disk. +// +// The two travel together because the countdown of §11.4 has two things to undo, and a +// caller that only handed over the new configuration left the station guessing at the other. +type ReloadRequest struct { + // Next is the configuration to put in service. + Next domain.Config + // FileBefore is what the configuration FILE carried before this change was written, + // and it is the document a rollback puts back on disk. + // + // A POINTER, and nil says « je n'ai pas pu lire le fichier » — the rollback then falls + // back on the configuration in service, which is all such a caller possesses. It is not + // a domain.Config with a zero value, because the zero value of a configuration LOOKS + // like a configuration: a caller that forgot this field would arm a rollback towards a + // document nobody ever validated, and nothing would say so. + FileBefore *domain.Config +} + // Reload publishes a new configuration and restarts ONLY the subsystems whose // block actually changed. // // limits, tiers, template, UI and journal apply instantly, with no gap in service: // they are read from the atomic pointer on the next turn of the loop and by // nothing else. -func (s *Station) Reload(next domain.Config) (ReloadOutcome, error) { +func (s *Station) Reload(req ReloadRequest) (ReloadOutcome, error) { s.reloadMu.Lock() defer s.reloadMu.Unlock() - previous := *s.hub.cfg.Load() - changed := s.apply(previous, next) + running := *s.hub.cfg.Load() + changed := s.apply(running, req.Next) outcome := ReloadOutcome{Changed: changed} if needsConfirmation(changed) { outcome.ConfirmBefore = s.clock.Now().Add(confirmationWindow) - s.confirmation = &pendingConfirmation{previous: previous, deadline: outcome.ConfirmBefore} + file := running + if req.FileBefore != nil { + file = *req.FileBefore + } + s.confirmation = &pendingConfirmation{ + running: running, file: file, deadline: outcome.ConfirmBefore, + } } return outcome, nil } +// PendingConfirmation reports the end of the countdown still running, or the zero time when +// nothing is waiting to be confirmed. +// +// It exists so that the administration can REFUSE a second save inside the window, the way +// it refuses a confirmation outside it. Accepting one would replace the target of the +// rollback with a configuration nobody has confirmed either, and the version somebody really +// did validate would be the one lost. +func (s *Station) PendingConfirmation() time.Time { + s.reloadMu.Lock() + defer s.reloadMu.Unlock() + if s.confirmation == nil { + return time.Time{} + } + return s.confirmation.deadline +} + // Confirm accepts the configuration in force and stops the countdown. func (s *Station) Confirm() error { s.reloadMu.Lock() @@ -460,17 +512,21 @@ func (s *Station) revertIfUnconfirmed(now time.Time) { if s.confirmation == nil || now.Before(s.confirmation.deadline) { return } - previous := s.confirmation.previous + running, file := s.confirmation.running, s.confirmation.file s.confirmation = nil s.hub.logTechnical(domain.LevelWarn, "config", "", "Configuration non confirmée en 60 s : retour à la version précédente.", "") - s.apply(*s.hub.cfg.Load(), previous) + // The station goes back to what it was OPERATING ON, and to nothing else. Applying the + // file here would be the tempting symmetry and the wrong one: on a station out of + // service that document is the very one §11.3 refuses to run, and nothing in this + // package can put a station BACK into the out-of-service state. + s.apply(*s.hub.cfg.Load(), running) if s.onRevert != nil { // The FILE goes back too, and it has to: the countdown protects the station that // is running, and the write of §11.4 happened before the countdown started. A // station that rolled back and then restarted on the unconfirmed configuration // would have cut the branch sixty seconds later than announced. - s.onRevert(previous) + s.onRevert(file) } } diff --git a/internal/station/units_test.go b/internal/station/units_test.go index 8d45eee..6ade13d 100644 --- a/internal/station/units_test.go +++ b/internal/station/units_test.go @@ -382,7 +382,7 @@ func TestARepairedConfigurationPutsTheStationBackInServiceWITHOUTARestart(t *tes // And what is missing is ONE field. The neutral profile is valid in every other // respect — that is what makes it the profile a station falls back onto — so the // password the first access poses is literally the last fault standing (contrôle 31). - if _, err := b.station.Reload(repairedProfile()); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: repairedProfile()}); err != nil { t.Fatalf("Reload : %v", err) } // One turn of the loop, because the answer to a command is sent before the @@ -405,7 +405,7 @@ func TestAStillBrokenConfigurationLeavesTheStationOutOfService(t *testing.T) { broken := repairedProfile() broken.Station.Number = 0 // hors bornes [1, 99] : le nom du fichier surveillé en dérive - if _, err := b.station.Reload(broken); err != nil { + if _, err := b.station.Reload(ReloadRequest{Next: broken}); err != nil { t.Fatalf("Reload : %v", err) } b.flush() diff --git a/internal/web/admin_test.go b/internal/web/admin_test.go index 5212e2c..3a6c9f9 100644 --- a/internal/web/admin_test.go +++ b/internal/web/admin_test.go @@ -404,6 +404,67 @@ func TestAnUnknownSelfTestIsRefusedByName(t *testing.T) { } } +// TestTheReloadAnswerNamesWhatIsWatchedAndTheImportInForce. +// +// « Le catalogue va être relu. » was written before anything had been looked at, and the +// answer carried nothing else: no file, no directory, no instant, no result. The screen +// therefore had no way to recognise the import that followed, and on the dominant case — +// nothing where the station is looking — the promise was followed by a silence that never +// ended, because the watch returns without a word when it finds no file (§10.5). +func TestTheReloadAnswerNamesWhatIsWatchedAndTheImportInForce(t *testing.T) { + const seen = "Aucun fichier flv_2.csv dans D:\\catalog\\incoming : il n'y a rien à relire." + b := newBench(t, func(o *benchOptions) { + o.catalogAdmin = &fakeCatalogAdmin{seen: seen} + o.dashboard = stubDashboard{DashboardFacts{Source: &CatalogSourceState{ + Type: domain.CatalogSourceLocalDrop, + Label: "dépôt local, flv_2.csv dans D:\\catalog\\incoming", + }}} + }) + b.store.imports = []domain.Import{{ + ID: 7, OccurredAt: epoch, Source: domain.CatalogSourceLocalDrop, + FileName: "flv_1.csv", Result: domain.ImportApplied, + }} + + got := decodeStatus[reloadDTO](t, + b.post("/admin/api/troubleshooting/reload-catalog", `{}`), http.StatusAccepted) + if got.Message != seen { + t.Fatalf("message = %q, attendu ce que le poste a VU du fichier surveillé", got.Message) + } + if !strings.Contains(got.Watched, "flv_2.csv") { + t.Fatalf("surveillé = %q, attendu la ligne permanente du catalogue", got.Watched) + } + // L'écran reconnaît l'import SUIVANT en comparant son identifiant à celui-ci : sans + // lui, il ne peut ni annoncer l'issue ni cesser de l'attendre. + if got.LastImportID != 7 || got.LastImportAt == "" { + t.Fatalf("import en vigueur = %d à %q, attendu le dernier import du journal", + got.LastImportID, got.LastImportAt) + } +} + +// TestAStationWithNoJournalAndNoDashboardStillAnswersTheReload. +// +// Both collaborators are optional — a station whose journal is unavailable still serves +// (ADR-013), and a station wired without a Dashboard publishes no source line. The answer +// then says nothing about either, rather than panicking or inventing a sentence. +func TestAStationWithNoJournalAndNoDashboardStillAnswersTheReload(t *testing.T) { + b := newBench(t, func(o *benchOptions) { + o.catalogAdmin = &fakeCatalogAdmin{} + o.noStore = true + }) + + got := decodeStatus[reloadDTO](t, + b.post("/admin/api/troubleshooting/reload-catalog", `{}`), http.StatusAccepted) + if !got.Done { + t.Fatalf("relecture = %+v, attendu acceptée", got) + } + if got.Watched != "" || got.LastImportID != 0 { + t.Fatalf("relecture = %+v, attendu aucune affirmation sur ce que rien n'a publié", got) + } + if got.Message == "" { + t.Fatal("un poste sans journal ni tableau de bord ne dit rien du tout de sa relecture") + } +} + // TestACatalogDroppedOnTheScreenGoesThroughTheOrdinaryWatcher (A4, ADR-011). func TestACatalogDroppedOnTheScreenGoesThroughTheOrdinaryWatcher(t *testing.T) { catalog := &fakeCatalogAdmin{} @@ -650,13 +711,15 @@ type fakeCatalogAdmin struct { imported string forgot bool err error + // seen is what the source answered about the file it watches, in French. + seen string } -func (f *fakeCatalogAdmin) Reload(context.Context) error { +func (f *fakeCatalogAdmin) Reload(context.Context) (string, error) { f.mu.Lock() defer f.mu.Unlock() f.reloads++ - return f.err + return f.seen, f.err } func (f *fakeCatalogAdmin) Import(_ context.Context, name string, r io.Reader) (domain.Import, error) { diff --git a/internal/web/catalog.go b/internal/web/catalog.go index de3b531..4982285 100644 --- a/internal/web/catalog.go +++ b/internal/web/catalog.go @@ -106,6 +106,11 @@ type catalogPresentationDTO struct { IdleTimeoutSeconds int `json:"idle_timeout_s"` ReprintWindowSeconds int `json:"reprint_window_s"` Sound bool `json:"sound"` + // ShowByUnitProducts is STATED here and applied by the grid, never applied here. + // Filtering the payload would make product_count differ from the catalog_count of + // the state stream for ever, and the browser asks for the catalog again on that + // difference alone -- ten times a second, answering 304, visible to nobody. + ShowByUnitProducts bool `json:"show_by_unit_products"` } // rfc3339OrEmpty formats an instant, and the zero time as an empty string. @@ -226,6 +231,7 @@ func (s *Server) catalogOf(ctx context.Context, catalog *domain.Catalog, cfg dom IdleTimeoutSeconds: cfg.UI.IdleTimeoutSeconds, ReprintWindowSeconds: cfg.UI.ReprintWindowSeconds, Sound: cfg.UI.Sound, + ShowByUnitProducts: cfg.UI.ShowByUnitProducts, }, UpdatedAt: rfc3339OrEmpty(s.hub.CatalogUpdatedAt()), AppVersion: s.version, diff --git a/internal/web/catalog_test.go b/internal/web/catalog_test.go index e0a0e72..a75eae1 100644 --- a/internal/web/catalog_test.go +++ b/internal/web/catalog_test.go @@ -130,6 +130,67 @@ func TestTheSearchNameIsDesaccentuatedByTheServer(t *testing.T) { } } +// TestTheGridSettingAboutByUnitProductsTravelsWithTheCatalog, in both directions. +// +// The flag is a SCREEN setting and rides with the other three, exactly as +// show_grid_prices does: the station takes no decision here, it states one so that the +// grid can apply it. +func TestTheGridSettingAboutByUnitProductsTravelsWithTheCatalog(t *testing.T) { + for name, shown := range map[string]bool{"masqués": false, "montrés": true} { + t.Run(name, func(t *testing.T) { + b := newBench(t, func(o *benchOptions) { + o.config = func(c *domain.Config) { c.UI.ShowByUnitProducts = shown } + }) + + page := decodeStatus[catalogDTO](t, b.get("/api/v1/catalog"), http.StatusOK) + + if page.Options.ShowByUnitProducts != shown { + t.Fatalf("presentation.show_by_unit_products = %v, attendu %v", + page.Options.ShowByUnitProducts, shown) + } + }) + } +} + +// TestTheStationServesEveryWeighableTileWhateverTheGridShows is the guard against a +// reload ten times a second. +// +// The browser asks for the catalog again the moment `catalog_count` of the state stream +// differs from `product_count` of the payload. Filtering the by-unit products HERE would +// make those two numbers differ FOR EVER, and every SSE event — ten a second — would +// fire a GET /api/v1/catalog. The masking is a station's display choice and it belongs +// to the grid; the payload stays the inventory of what has a tile. +func TestTheStationServesEveryWeighableTileWhateverTheGridShows(t *testing.T) { + byUnit := domain.NewCatalog([]domain.Product{ + { + ID: "1", Name: "AIL", Reference: "0493021000003", + Mode: domain.ByWeight, UnitPrice: 532, CategoryCode: "vegetables", + Qualification: domain.Weighable, + }, + { + ID: "2", Name: "MELON unite SAF", Reference: "0499000064004", + Mode: domain.ByUnit, UnitPrice: 300, CategoryCode: "fruits", + Qualification: domain.Weighable, + }, + }, []domain.Category{{Code: "vegetables", Label: "Légumes", Visible: true}}) + + b := newBench(t, func(o *benchOptions) { + o.catalog = byUnit + o.config = func(c *domain.Config) { c.UI.ShowByUnitProducts = false } + }) + + page := decodeStatus[catalogDTO](t, b.get("/api/v1/catalog"), http.StatusOK) + + if page.ProductCount != 2 { + t.Fatalf("%d tuiles servies, attendu 2 : le masquage se fait à l'écran, pas ici", + page.ProductCount) + } + if got := b.state().CatalogCount; got != page.ProductCount { + t.Fatalf("catalog_count du flux = %d, product_count du catalogue = %d : "+ + "le navigateur redemanderait le catalogue à chaque événement", got, page.ProductCount) + } +} + // TestWhatHasNoTileIsNotServedToTheScreen: a prepackaged product is not an error, it // is not the scale's business, and it has no tile (§10.3, ADR-021). func TestWhatHasNoTileIsNotServedToTheScreen(t *testing.T) { diff --git a/internal/web/config.go b/internal/web/config.go index 9956920..cf83c76 100644 --- a/internal/web/config.go +++ b/internal/web/config.go @@ -8,6 +8,7 @@ import ( "time" "openscale/internal/domain" + "openscale/internal/station" ) // faultDTO is one configuration control that failed (§11.3). @@ -161,6 +162,17 @@ func (s *Server) writeConfig(w http.ResponseWriter, r *http.Request) { return } + // A second save INSIDE the window is refused, exactly as a confirmation outside it is. + // The write of step 3 happens before the countdown of step 5, so accepting one would + // move the file the rollback aims at onto a version nobody confirmed either — and the + // one somebody really did validate would be the version lost. + if deadline := s.controller.PendingConfirmation(); !deadline.IsZero() { + writeProblem(w, http.StatusConflict, "", + "Une configuration attend encore d'être confirmée. Confirmez-la, ou laissez le "+ + "poste revenir tout seul à la version précédente, puis enregistrez de nouveau.") + return + } + // The document GET serves is the DECODED Go structure, re-marshalled: a retired key // never survives that round trip, because encoding/json drops what no field claims // (§11.3, `configPayload`). A PUT of exactly what GET served can therefore never @@ -241,7 +253,19 @@ func (s *Server) writeConfig(w http.ResponseWriter, r *http.Request) { return } - outcome, err := s.controller.Reload(next) + // The rollback of §11.4 puts THE FILE AS IT WAS back, and `onDisk` is that document: it + // was read above, before the Save, and it is the same variable `served` already stands + // for. Handing it over is what keeps a station whose file is faulty — and which + // therefore RUNS the neutral profile — from having the factory settings written over + // its own file sixty seconds after a volunteer repaired it. + // + // A file that could not be read hands over nothing, and the station falls back on what + // it is running: that is all such a station has left. + reload := station.ReloadRequest{Next: next} + if onDiskErr == nil { + reload.FileBefore = &onDisk + } + outcome, err := s.controller.Reload(reload) if err != nil { writeProblem(w, http.StatusInternalServerError, "", "Configuration écrite mais non appliquée : "+err.Error()) @@ -439,6 +463,14 @@ func (s *Server) restoreConfig(w http.ResponseWriter, r *http.Request) { unavailable(w, "la configuration n'est pas versionnée ici") return } + // A restoration is a save like any other and it arms the same countdown, so it is + // refused inside the window for the same reason writeConfig is. + if deadline := s.controller.PendingConfirmation(); !deadline.IsZero() { + writeProblem(w, http.StatusConflict, "", + "Une configuration attend encore d'être confirmée. Confirmez-la, ou laissez le "+ + "poste revenir tout seul à la version précédente, puis restaurez de nouveau.") + return + } restored, err := s.configStore.Restore(r.Context(), body.Version) if err != nil { writeProblem(w, http.StatusNotFound, "", @@ -456,12 +488,23 @@ func (s *Server) restoreConfig(w http.ResponseWriter, r *http.Request) { }) return } + // READ BEFORE THE WRITE, and for the reason writeConfig reads it: restoring a version + // arms the same countdown, and a rollback that put the RUNNING configuration back would + // write the factory profile onto the file of a station that started out of service. + // This is the route that reaches it — writeConfig already refuses a station whose file + // carries a retired key, and this one does not. + fileBefore, fileErr := s.configStore.Read(r.Context()) + if err := s.configStore.Save(r.Context(), restored); err != nil { writeProblem(w, http.StatusInternalServerError, "", "Configuration non écrite : "+err.Error()) return } - outcome, err := s.controller.Reload(restored) + reload := station.ReloadRequest{Next: restored} + if fileErr == nil { + reload.FileBefore = &fileBefore + } + outcome, err := s.controller.Reload(reload) if err != nil { writeProblem(w, http.StatusInternalServerError, "", "Configuration écrite mais non appliquée : "+err.Error()) diff --git a/internal/web/config_test.go b/internal/web/config_test.go index d041817..7405bab 100644 --- a/internal/web/config_test.go +++ b/internal/web/config_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" "testing" + "time" "openscale/internal/domain" ) @@ -361,6 +362,66 @@ func localDrop(directory string) func(*benchOptions) { } } +// TestUneRestaurationDeVersionMemoriseAussiLeFichierDAvant. +// +// Restoring a backup goes through the same path as any other save (§11.4) — including the +// countdown, and therefore including the rollback. It had the same defect and no test at +// all: on a station whose file is faulty, and which therefore RUNS the neutral profile, an +// unconfirmed restoration wrote the factory settings over the cooperative's own file. +// +// It is also the one route that can reach a file control 20 refuses: writeConfig turns such +// a station away before writing anything, and this one does not. +func TestUneRestaurationDeVersionMemoriseAussiLeFichierDAvant(t *testing.T) { + shop := loadConfig(t) + store := restorableConfig{savedConfig: &savedConfig{}} + writeFileOf(t, store.savedConfig, shop) + + backup := reread(t, shop) + backup.Scale.Options["port"] = json.RawMessage(`"COM9"`) + store.backup = backup + + b := adminBench(t, func(o *benchOptions) { + o.configStore = store + // What a station in factory configuration RUNS (§11.3), which is not its file. + o.config = func(cfg *domain.Config) { *cfg = domain.NeutralProfile() } + }) + + restored := decodeStatus[configDTO](t, + b.post("/admin/api/config/restore", `{"version":1}`), http.StatusOK) + if restored.Pending == nil { + t.Fatal("restaurer une version qui change le matériel n'arme aucun compte à rebours") + } + + // Nobody confirms. + b.advance(61 * time.Second) + written := awaitRewrittenFile(t, b, store.savedConfig, "COM9") + + if written.Station.Coop != shop.Station.Coop { + t.Errorf("la coopérative du fichier est %q, attendu %q : une restauration non "+ + "confirmée a écrit le profil d'usine par-dessus le fichier du magasin", + written.Station.Coop, shop.Station.Coop) + } + if got, want := len(written.Pricing.Tiers), len(shop.Pricing.Tiers); got != want { + t.Errorf("le fichier porte %d palier(s) de tarif au lieu de %d", got, want) + } +} + +// restorableConfig is a store whose backup is a DIFFERENT document from the file in place. +// +// savedConfig hands its own file back on Restore, and a restoration that changes nothing +// arms no countdown at all — so the route under test could not be reached through it. +type restorableConfig struct { + *savedConfig + backup domain.Config +} + +// Restore hands back the backup, without applying it, exactly as the real store does. +func (r restorableConfig) Restore(context.Context, int) (domain.Config, error) { + return r.backup, nil +} + +var _ ConfigStore = restorableConfig{} + // writeFileOf makes the store hold what the station is running, so that the round trip // under test is the one the screen performs: read the FILE, edit it, save it back. func writeFileOf(t *testing.T, saved *savedConfig, cfg domain.Config) { diff --git a/internal/web/dist/admin.html b/internal/web/dist/admin.html index e254f8a..f343412 100644 --- a/internal/web/dist/admin.html +++ b/internal/web/dist/admin.html @@ -5,11 +5,11 @@ Administration - - - + + + - +
diff --git a/internal/web/dist/assets/admin-CeCqIj8w.js b/internal/web/dist/assets/admin-CeCqIj8w.js new file mode 100644 index 0000000..4a8f710 --- /dev/null +++ b/internal/web/dist/assets/admin-CeCqIj8w.js @@ -0,0 +1 @@ +import"./app-Bm_67CUJ.js";import{mountAdmin as m}from"./mount-l2eO5M4C.js";m(document.getElementById("app")); diff --git a/internal/web/dist/assets/admin-DNV6wY58.js b/internal/web/dist/assets/admin-DNV6wY58.js deleted file mode 100644 index e7f5f31..0000000 --- a/internal/web/dist/assets/admin-DNV6wY58.js +++ /dev/null @@ -1 +0,0 @@ -import"./app-Dn-BjSsJ.js";import{mountAdmin as m}from"./mount-BjqIypvA.js";m(document.getElementById("app")); diff --git a/internal/web/dist/assets/app-Dn-BjSsJ.js b/internal/web/dist/assets/app-Bm_67CUJ.js similarity index 86% rename from internal/web/dist/assets/app-Dn-BjSsJ.js rename to internal/web/dist/assets/app-Bm_67CUJ.js index 1958d5e..47515a0 100644 --- a/internal/web/dist/assets/app-Dn-BjSsJ.js +++ b/internal/web/dist/assets/app-Bm_67CUJ.js @@ -1,2 +1,2 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const f of s.addedNodes)f.tagName==="LINK"&&f.rel==="modulepreload"&&r(f)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();const Ut=!1;var Vt=Array.isArray,Dn=Array.prototype.indexOf,Je=Array.prototype.includes,rt=Array.from,Fn=Object.defineProperty,Ee=Object.getOwnPropertyDescriptor,Hn=Object.getOwnPropertyDescriptors,jn=Object.prototype,zn=Array.prototype,qt=Object.getPrototypeOf,Ot=Object.isExtensible;const Bn=()=>{};function Un(e){for(var t=0;t{e=r,t=i});return{promise:n,resolve:e,reject:t}}const O=2,Ae=4,it=8,$t=1<<24,q=16,U=32,te=64,ot=128,B=512,k=1024,A=2048,Y=4096,P=8192,H=16384,Re=32768,Ct=1<<25,Me=65536,Ze=1<<17,Vn=1<<18,xe=1<<19,qn=1<<20,K=1<<25,pe=65536,Qe=1<<21,Se=1<<22,oe=1<<23,Te=Symbol("$state"),Gn=Symbol("legacy props"),$n=Symbol(""),Yt=Symbol("attributes"),at=Symbol("class"),ut=Symbol("style"),ct=Symbol("text"),Ke=Symbol("form reset"),Ve=new class extends Error{name="StaleReactionError";message="The reaction that called `getAbortSignal()` was re-run or destroyed"},Yn=!!globalThis.document?.contentType&&globalThis.document.contentType.includes("xml");function Kn(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function Wn(e,t,n){throw new Error("https://svelte.dev/e/each_key_duplicate")}function Xn(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function Jn(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function Zn(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Qn(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function er(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function tr(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function nr(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function rr(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function ir(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const sr=1,fr=2,Kt=4,lr=8,or=16,ar=1,ur=4,cr=8,hr=16,dr=1,vr=2,M=Symbol("uninitialized"),_r="http://www.w3.org/1999/xhtml";function pr(){console.warn("https://svelte.dev/e/derived_inert")}function Ri(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function gr(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}function Wt(e){return e===this.v}function wr(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function Xt(e){return!wr(e,this.v)}let mr=!1,N=null;function ke(e){N=e}function br(e,t=!1,n){N={p:N,i:!1,c:null,e:null,s:e,x:null,r:m,l:null}}function yr(e){var t=N,n=t.e;if(n!==null){t.e=null;for(var r of n)gn(r)}return t.i=!0,N=t.p,{}}function Jt(){return!0}let ce=[];function Zt(){var e=ce;ce=[],Un(e)}function W(e){if(ce.length===0&&!He){var t=ce;queueMicrotask(()=>{t===ce&&Zt()})}ce.push(e)}function Er(){for(;ce.length>0;)Zt()}function Qt(e){var t=m;if(t===null)return w.f|=oe,e;if((t.f&Re)===0&&(t.f&Ae)===0)throw e;le(e,t)}function le(e,t){if(!(t!==null&&(t.f&H)!==0)){for(;t!==null;){if((t.f&ot)!==0){if((t.f&Re)===0)throw e;try{t.b.error(e);return}catch(n){e=n}}t=t.parent}throw e}}const Sr=-7169;function S(e,t){e.f=e.f&Sr|t}function wt(e){(e.f&B)!==0||e.deps===null?S(e,k):S(e,Y)}function en(e){if(e!==null)for(const t of e)(t.f&O)===0||(t.f&pe)===0||(t.f^=pe,en(t.deps))}function tn(e,t,n){(e.f&A)!==0?t.add(e):(e.f&Y)!==0&&n.add(e),en(e.deps),S(e,k)}let $e=!1;function Tr(e){var t=$e;try{return $e=!1,[e(),$e]}finally{$e=t}}function xi(e,t){{const n=document.body;e.autofocus=!0,W(()=>{document.activeElement===n&&e.focus()})}}let Pt=!1;function Ar(){Pt||(Pt=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(const t of e.target.elements)t[Ke]?.()})},{capture:!0}))}function Ne(e){var t=w,n=m;V(null),J(null);try{return e()}finally{V(t),J(n)}}function Ni(e,t,n,r=n){e.addEventListener(t,()=>Ne(n));const i=e[Ke];i?e[Ke]=()=>{i(),r(!0)}:e[Ke]=()=>r(!0),Ar()}function Mr(e){let t=0,n=ge(0),r;return()=>{Et()&&(F(n),Kr(()=>(t===0&&(r=Cn(()=>e(()=>je(n)))),t+=1,()=>{W(()=>{t-=1,t===0&&(r?.(),r=void 0,je(n))})})))}}var kr=Me|xe;function Or(e,t,n,r){new Cr(e,t,n,r)}class Cr{parent;is_pending=!1;transform_error;#t;#f=null;#e;#o;#r;#s=null;#n=null;#l=null;#i=null;#_=0;#a=0;#u=!1;#h=new Set;#p=new Set;#c=null;#w=Mr(()=>(this.#c=ge(this.#_),()=>{this.#c=null}));constructor(t,n,r,i){this.#t=t,this.#e=n,this.#o=s=>{var f=m;f.b=this,f.f|=ot,r(s)},this.parent=m.b,this.transform_error=i??this.parent?.transform_error??(s=>s),this.#r=St(()=>{this.#d()},kr)}#g(){try{this.#s=z(()=>this.#o(this.#t))}catch(t){this.error(t)}}#y(t){const n=this.#e.failed,{reset:r,invoke_onerror:i}=this.#m(t);W(i),n&&(this.#l=z(()=>{n(this.#t,()=>t,()=>r)}))}#m(t){var n=!1,r=!1;const i=()=>{if(n){gr();return}n=!0,r&&ir(),this.#l!==null&&ve(this.#l,()=>{this.#l=null}),this.#v(()=>{this.#d()})};return{reset:i,invoke_onerror:()=>{try{r=!0,this.#e.onerror?.(t,i),r=!1}catch(f){le(f,this.#r&&this.#r.parent)}}}}#E(){const t=this.#e.pending;t&&(this.is_pending=!0,this.#n=z(()=>t(this.#t)),W(()=>{var n=this.#i=document.createDocumentFragment(),r=ee();n.append(r),this.#s=this.#v(()=>z(()=>this.#o(r))),this.#a===0&&(this.#t.before(n),this.#i=null,ve(this.#n,()=>{this.#n=null}),this.#b(b))}))}#d(){try{if(this.is_pending=this.has_pending_snippet(),this.#a=0,this.#_=0,this.#s=z(()=>{this.#o(this.#t)}),this.#a>0){var t=this.#i=document.createDocumentFragment();At(this.#s,t);const n=this.#e.pending;this.#n=z(()=>n(this.#t))}else this.#b(b)}catch(n){this.error(n)}}#b(t){this.is_pending=!1,t.transfer_effects(this.#h,this.#p)}defer_effect(t){tn(t,this.#h,this.#p)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#e.pending}#v(t){var n=m,r=w,i=N;J(this.#r),V(this.#r),ke(this.#r.ctx);try{return ae.ensure(),t()}catch(s){return Qt(s),null}finally{J(n),V(r),ke(i)}}#S(t,n){if(!this.has_pending_snippet()){this.parent&&this.parent.#S(t,n);return}this.#a+=t,this.#a===0&&(this.#b(n),this.#n&&ve(this.#n,()=>{this.#n=null}),this.#i&&(this.#t.before(this.#i),this.#i=null))}update_pending_count(t,n){this.#S(t,n),this.#_+=t,!(!this.#c||this.#u)&&(this.#u=!0,W(()=>{this.#u=!1,this.#c&&Oe(this.#c,this.#_)}))}get_effect_pending(){return this.#w(),F(this.#c)}error(t){if(!this.#e.onerror&&!this.#e.failed)throw t;b?.is_fork?(this.#s&&b.skip_effect(this.#s),this.#n&&b.skip_effect(this.#n),this.#l&&b.skip_effect(this.#l),b.oncommit(()=>{this.#T(t)})):this.#T(t)}#T(t){this.#s&&(I(this.#s),this.#s=null),this.#n&&(I(this.#n),this.#n=null),this.#l&&(I(this.#l),this.#l=null);let n=this.#e.failed;const r=i=>{const{reset:s,invoke_onerror:f}=this.#m(i);f(),n&&(this.#l=this.#v(()=>{try{return z(()=>{var o=m;o.b=this,o.f|=ot,n(this.#t,()=>i,()=>s)})}catch(o){return le(o,this.#r.parent),null}}))};W(()=>{var i;try{i=this.transform_error(t)}catch(s){le(s,this.#r&&this.#r.parent);return}i!==null&&typeof i=="object"&&typeof i.then=="function"?i.then(r,s=>le(s,this.#r&&this.#r.parent)):r(i)})}}function Pr(e,t,n,r){const i=ze;var s=e.filter(d=>!d.settled),f=t.map(i);if(n.length===0&&s.length===0){r(f);return}var o=m,l=Rr(),a=s.length===1?s[0].promise:s.length>1?Promise.all(s.map(d=>d.promise)):null;function u(d){if((o.f&H)===0){l();try{r([...f,...d])}catch(v){le(v,o)}et()}}var h=nn();if(n.length===0){a.then(()=>u([])).finally(h);return}function c(){Promise.all(n.map(d=>xr(d))).then(u).catch(d=>le(d,o)).finally(h)}a?a.then(()=>{l(),c(),et()}):c()}function Rr(){var e=m,t=w,n=N,r=b;return function(s=!0){J(e),V(t),ke(n),s&&(e.f&H)===0&&(r?.activate(),r?.apply())}}function et(e=!0){J(null),V(null),ke(null),e&&b?.deactivate()}function nn(){var e=m,t=e.b,n=b,r=!!t?.is_rendered();return t?.update_pending_count(1,n),n.increment(r,e),()=>{t?.update_pending_count(-1,n),n.decrement(r,e)}}function ze(e){var t=O|A;return m!==null&&(m.f|=xe),{ctx:N,deps:null,effects:null,equals:Wt,f:t,fn:e,reactions:null,rv:0,v:M,wv:0,parent:m,ac:null}}const Le=Symbol("obsolete");function xr(e,t,n){let r=m;r===null&&Kn();var i=void 0,s=ge(M),f=!w,o=new Set;return Yr(()=>{var l=m,a=Gt();i=a.promise;try{Promise.resolve(e()).then(a.resolve,d=>{d!==Ve&&a.reject(d)}).finally(et)}catch(d){a.reject(d),et()}var u=b;if(f){if((l.f&Re)!==0)var h=nn();if(r.b?.is_rendered())u.async_deriveds.get(l)?.reject(Le);else for(const d of o.values())d.reject(Le);o.add(a),u.async_deriveds.set(l,a)}const c=(d,v=void 0)=>{h?.(),o.delete(a),v!==Le&&(u.activate(),v?(s.f|=oe,Oe(s,v)):((s.f&oe)!==0&&(s.f^=oe),Oe(s,d)),u.deactivate())};a.promise.then(c,d=>c(null,d||"unknown"))}),pn(()=>{for(const l of o)l.reject(Le)}),new Promise(l=>{function a(u){function h(){u===i?l(s):a(i)}u.then(h,h)}a(i)})}function Ii(e){const t=ze(e);return En(t),t}function rn(e){const t=ze(e);return t.equals=Xt,t}function Nr(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n{t.ac.abort(Ve),t.ac=null}),t.fn!==null&&(t.teardown=Bn),Be(t,0),Tt(t))}function fn(e){if(e.effects!==null)for(const t of e.effects)t.teardown&&t.fn!==null&&Pe(t)}let st=null,me=null,b=null,ht=null,G=null,dt=null,He=!1,ft=!1,be=null,We=null;var Rt=0;let Lr=1;class ae{id=Lr++;#t=!1;linked=!0;#f=null;#e=null;async_deriveds=new Map;current=new Map;previous=new Map;#o=new Set;#r=new Set;#s=0;#n=new Map;#l=null;#i=[];#_=[];#a=new Set;#u=new Set;#h=new Map;#p=new Set;is_fork=!1;#c=!1;constructor(){me===null?st=me=this:(me.#e=this,this.#f=me),me=this}#w(){if(this.is_fork)return!0;for(const r of this.#n.keys()){for(var t=r,n=!1;t.parent!==null;){if(this.#h.has(t)){n=!0;break}t=t.parent}if(!n)return!0}return!1}skip_effect(t){this.#h.has(t)||this.#h.set(t,{d:[],m:[]}),this.#p.delete(t)}unskip_effect(t,n=r=>this.schedule(r)){var r=this.#h.get(t);if(r){this.#h.delete(t);for(var i of r.d)S(i,A),n(i);for(i of r.m)S(i,Y),n(i)}this.#p.add(t)}#g(){this.#t=!0,Rt++>1e3&&(this.#v(),Fr());for(const l of this.#a)this.#u.delete(l),S(l,A),this.schedule(l);for(const l of this.#u)S(l,Y),this.schedule(l);const t=this.#i;this.#i=[],this.apply();var n=be=[],r=[],i=We=[];for(const l of t)try{this.#y(l,n,r)}catch(a){throw an(l),this.#w()||this.discard(),a}if(b=null,i.length>0){var s=ae.ensure();for(const l of i)s.schedule(l)}if(be=null,We=null,this.#w()){this.#d(r),this.#d(n);for(const[l,a]of this.#h)on(l,a);i.length>0&&b.#g();return}const f=this.#m();if(f){this.#d(r),this.#d(n),f.#E(this);return}this.#a.clear(),this.#u.clear();for(const l of this.#o)l(this);this.#o.clear(),ht=this,xt(r),xt(n),ht=null,this.#l?.resolve();var o=b;if(this.#s===0&&(this.#i.length===0||o!==null)&&this.#v(),this.#i.length>0)if(o!==null){const l=o;l.#i.push(...this.#i.filter(a=>!l.#i.includes(a)))}else o=this;o!==null&&o.#g()}#y(t,n,r){t.f^=k;for(var i=t.first;i!==null;){var s=i.f,f=(s&(U|te))!==0,o=f&&(s&k)!==0,l=o||(s&P)!==0||this.#h.has(i);if(!l&&i.fn!==null){f?i.f^=k:(s&Ae)!==0?n.push(i):Ge(i)&&((s&q)!==0&&this.#u.add(i),Pe(i));var a=i.first;if(a!==null){i=a;continue}}for(;i!==null;){var u=i.next;if(u!==null){i=u;break}i=i.parent}}}#m(){for(var t=this.#f;t!==null;){if(!t.is_fork){for(const[n,[,r]]of this.current)if(t.current.has(n)&&!r)return t}t=t.#f}return null}#E(t){for(const[r,i]of t.current)!this.previous.has(r)&&t.previous.has(r)&&this.previous.set(r,t.previous.get(r)),this.current.set(r,i);for(const[r,i]of t.async_deriveds){const s=this.async_deriveds.get(r);s&&i.promise.then(s.resolve).catch(s.reject)}t.async_deriveds.clear(),this.transfer_effects(t.#a,t.#u);const n=r=>{var i=r.reactions;if(i!==null&&!((r.f&O)!==0&&(r.f&(A|Y))===0))for(const o of i){var s=o.f;if((s&O)!==0)n(o);else{var f=o;s&(Se|q)&&!this.async_deriveds.has(f)&&(this.#u.delete(f),S(f,A),this.schedule(f))}}};for(const r of this.current.keys())n(r);this.oncommit(()=>t.discard()),t.#v(),b=this,this.#g()}#d(t){for(var n=0;n!h.current.get(c)[1]);if(!(!h.#t||i.length===0)){var s=i.filter(c=>!this.current.has(c));if(s.length===0)t&&h.discard();else if(n.length>0){if(t)for(const c of this.#p)h.unskip_effect(c,d=>{(d.f&(q|Se))!==0?h.schedule(d):h.#d([d])});h.activate();var f=new Set,o=new Map;for(var l of n)ln(l,s,f,o);o=new Map;var a=[...h.current].filter(([c,d])=>{const v=this.current.get(c);return v?v[0]!==d[0]||v[1]!==d[1]:!0}).map(([c])=>c);if(a.length>0)for(const c of this.#_)(c.f&(H|P|Ze))===0&&bt(c,a,o)&&((c.f&(Se|q))!==0?(S(c,A),h.schedule(c)):h.#a.add(c));if(h.#i.length>0&&!h.#c){h.apply();for(var u of h.#i)h.#y(u,[],[]);h.#i=[]}h.deactivate()}}}}increment(t,n){if(this.#s+=1,t){let r=this.#n.get(n)??0;this.#n.set(n,r+1)}}decrement(t,n){if(this.#s-=1,t){let r=this.#n.get(n)??0;r===1?this.#n.delete(n):this.#n.set(n,r-1)}this.#c||(this.#c=!0,W(()=>{this.#c=!1,this.linked&&this.flush()}))}transfer_effects(t,n){for(const r of t)this.#a.add(r);for(const r of n)this.#u.add(r);t.clear(),n.clear()}oncommit(t){this.#o.add(t)}ondiscard(t){this.#r.add(t)}settled(){return(this.#l??=Gt()).promise}static ensure(){if(b===null){const t=b=new ae;!ft&&!He&&W(()=>{t.#t||t.flush()})}return b}apply(){{G=null;return}}schedule(t){if(dt=t,t.b?.is_pending&&(t.f&(Ae|it|$t))!==0&&(t.f&Re)===0){t.b.defer_effect(t);return}for(var n=t;n.parent!==null;){n=n.parent;var r=n.f;if(be!==null&&n===m&&(w===null||(w.f&O)===0))return;if((r&(te|U))!==0){if((r&k)===0)return;n.f^=k}}this.#i.push(n)}#v(){if(this.linked){var t=this.#f,n=this.#e;t===null?st=n:t.#e=n,n===null?me=t:n.#f=t,this.linked=!1}}}function Dr(e){var t=He;He=!0;try{for(var n;;){if(Er(),b===null)return n;b.flush()}}finally{He=t}}function Fr(){try{Qn()}catch(e){le(e,dt)}}let Q=null;function xt(e){var t=e.length;if(t!==0){for(var n=0;n0)){de.clear();for(const i of Q){if((i.f&(H|P))!==0)continue;const s=[i];let f=i.parent;for(;f!==null;)Q.has(f)&&(Q.delete(f),s.push(f)),f=f.parent;for(let o=s.length-1;o>=0;o--){const l=s[o];(l.f&(H|P))===0&&Pe(l)}}Q.clear()}}Q=null}}function ln(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(const i of e.reactions){const s=i.f;(s&O)!==0?ln(i,t,n,r):(s&(Se|q))!==0&&(s&A)===0&&bt(i,t,r)&&(S(i,A),yt(i))}}function bt(e,t,n){const r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const i of e.deps){if(Je.call(t,i))return!0;if((i.f&O)!==0&&bt(i,t,n))return n.set(i,!0),!0}return n.set(e,!1),!1}function yt(e){b.schedule(e)}function on(e,t){if(!((e.f&U)!==0&&(e.f&k)!==0)){(e.f&A)!==0?t.d.push(e):(e.f&Y)!==0&&t.m.push(e),S(e,k);for(var n=e.first;n!==null;)on(n,t),n=n.next}}function an(e){S(e,k);for(var t=e.first;t!==null;)an(t),t=t.next}let tt=new Set;const de=new Map;let un=!1;function ge(e,t){var n={f:0,v:e,reactions:null,equals:Wt,rv:0,wv:0};return n}function ie(e,t){const n=ge(e);return En(n),n}function Hr(e,t=!1,n=!0){const r=ge(e);return t||(r.equals=Xt),r}function fe(e,t,n=!1){w!==null&&(!$||(w.f&Ze)!==0)&&Jt()&&(w.f&(O|q|Se|Ze))!==0&&(X===null||!X.has(e))&&rr();let r=n?ye(t):t;return Oe(e,r,We)}function Oe(e,t,n=null){if(!e.equals(t)){de.set(e,ne?t:e.v);var r=ae.ensure();if(r.capture(e,t),(e.f&O)!==0){const i=e;(e.f&A)!==0&&mt(i),G===null&&wt(i)}e.wv=Tn(),cn(e,A,n),m!==null&&(m.f&k)!==0&&(m.f&(U|te))===0&&(j===null?Zr([e]):j.push(e)),!r.is_fork&&tt.size>0&&!un&&jr()}return t}function jr(){un=!1;for(const e of tt){(e.f&k)!==0&&S(e,Y);let t;try{t=Ge(e)}catch{t=!0}t&&Pe(e)}tt.clear()}function je(e){fe(e,e.v+1)}function cn(e,t,n){var r=e.reactions;if(r!==null)for(var i=r.length,s=0;s{if(_e===s)return o();var l=w,a=_e;V(null),Dt(s);var u=o();return V(l),Dt(a),u};return r&&n.set("length",ie(e.length)),new Proxy(e,{defineProperty(o,l,a){(!("value"in a)||a.configurable===!1||a.enumerable===!1||a.writable===!1)&&tr();var u=n.get(l);return u===void 0?f(()=>{var h=ie(a.value);return n.set(l,h),h}):fe(u,a.value,!0),!0},deleteProperty(o,l){var a=n.get(l);if(a===void 0){if(l in o){const u=f(()=>ie(M));n.set(l,u),je(i)}}else fe(a,M),je(i);return!0},get(o,l,a){if(l===Te)return e;var u=n.get(l),h=l in o;if(u===void 0&&(!h||Ee(o,l)?.writable)&&(u=f(()=>{var d=ye(h?o[l]:M),v=ie(d);return v}),n.set(l,u)),u!==void 0){var c=F(u);return c===M?void 0:c}return Reflect.get(o,l,a)},getOwnPropertyDescriptor(o,l){var a=Reflect.getOwnPropertyDescriptor(o,l);if(a&&"value"in a){var u=n.get(l);u&&(a.value=F(u))}else if(a===void 0){var h=n.get(l),c=h?.v;if(h!==void 0&&c!==M)return{enumerable:!0,configurable:!0,value:c,writable:!0}}return a},has(o,l){if(l===Te)return!0;var a=n.get(l),u=a!==void 0&&a.v!==M||Reflect.has(o,l);if(a!==void 0||m!==null&&(!u||Ee(o,l)?.writable)){a===void 0&&(a=f(()=>{var c=u?ye(o[l]):M,d=ie(c);return d}),n.set(l,a));var h=F(a);if(h===M)return!1}return u},set(o,l,a,u){var h=n.get(l),c=l in o;if(r&&l==="length")for(var d=a;die(M)),n.set(d+"",v))}if(h===void 0)(!c||Ee(o,l)?.writable)&&(h=f(()=>ie(void 0)),fe(h,ye(a)),n.set(l,h));else{c=h.v!==M;var p=f(()=>ye(a));fe(h,p)}var _=Reflect.getOwnPropertyDescriptor(o,l);if(_?.set&&_.set.call(u,a),!c){if(r&&typeof l=="string"){var g=n.get("length"),T=Number(l);Number.isInteger(T)&&T>=g.v&&fe(g,T+1)}je(i)}return!0},ownKeys(o){F(i);var l=Reflect.ownKeys(o).filter(h=>{var c=n.get(h);return c===void 0||c.v!==M});for(var[a,u]of n)u.v!==M&&!(a in o)&&l.push(a);return l},setPrototypeOf(){nr()}})}function Nt(e){try{if(e!==null&&typeof e=="object"&&Te in e)return e[Te]}catch{}return e}function Li(e,t){return Object.is(Nt(e),Nt(t))}var It,hn,dn,vn;function zr(){if(It===void 0){It=window,hn=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;dn=Ee(t,"firstChild").get,vn=Ee(t,"nextSibling").get,Ot(e)&&(e[at]=void 0,e[Yt]=null,e[ut]=void 0,e.__e=void 0),Ot(n)&&(n[ct]=void 0)}}function ee(e=""){return document.createTextNode(e)}function Ce(e){return dn.call(e)}function qe(e){return vn.call(e)}function Br(e,t){return Ce(e)}function Di(e,t=!1){{var n=Ce(e);return n instanceof Comment&&n.data===""?qe(n):n}}function Fi(e,t=1,n=!1){let r=e;for(;t--;)r=qe(r);return r}function Ur(e){e.textContent=""}function _n(){return!1}function Vr(e,t,n){return n?document.createElement(e,{is:n}):document.createElement(e)}function qr(e){m===null&&(w===null&&Zn(),Jn()),ne&&Xn()}function Gr(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function re(e,t){var n=m;n!==null&&(n.f&P)!==0&&(e|=P);var r={ctx:N,deps:null,nodes:null,f:e|A|B,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};b?.register_created_effect(r);var i=r;if((e&Ae)!==0)be!==null?be.push(r):ae.ensure().schedule(r);else if(t!==null){try{Pe(r)}catch(f){throw I(r),f}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&(i.f&xe)===0&&(i=i.first,(e&q)!==0&&(e&Me)!==0&&i!==null&&(i.f|=Me))}if(i!==null&&(i.parent=n,n!==null&&Gr(i,n),w!==null&&(w.f&O)!==0&&(e&te)===0)){var s=w;(s.effects??=[]).push(i)}return r}function Et(){return w!==null&&!$}function pn(e){const t=re(it,null);return S(t,k),t.teardown=e,t}function Hi(e){qr();var t=m.f,n=!w&&(t&U)!==0&&N!==null&&!N.i;if(n){var r=N;(r.e??=[]).push(e)}else return gn(e)}function gn(e){return re(Ae|qn,e)}function $r(e){ae.ensure();const t=re(te|xe,e);return(n={})=>new Promise(r=>{n.outro?ve(t,()=>{I(t),r(void 0)}):(I(t),r(void 0))})}function ji(e){return re(Ae,e)}function Yr(e){return re(Se|xe,e)}function Kr(e,t=0){return re(it|t,e)}function Wr(e,t=[],n=[],r=[]){Pr(r,t,n,i=>{re(it,()=>{e(...i.map(F))})})}function St(e,t=0){var n=re(q|t,e);return n}function z(e){return re(U|xe,e)}function wn(e){var t=e.teardown;if(t!==null){const n=ne,r=w;Lt(!0),V(null);try{t.call(null)}finally{Lt(n),V(r)}}}function Tt(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const i=n.ac;i!==null&&Ne(()=>{i.abort(Ve)});var r=n.next;(n.f&te)!==0?n.parent=null:I(n,t),n=r}}function Xr(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&U)===0&&I(t),t=n}}function I(e,t=!0){var n=!1;(t||(e.f&Vn)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(Jr(e.nodes.start,e.nodes.end),n=!0),e.f|=Ct,Tt(e,t&&!n),Be(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(const s of r)s.stop();wn(e),e.f^=Ct,e.f|=H;var i=e.parent;i!==null&&i.first!==null&&mn(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function Jr(e,t){for(;e!==null;){var n=e===t?null:qe(e);e.remove(),e=n}}function mn(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function ve(e,t,n=!0){var r=[];bn(e,r,!0);var i=()=>{n&&I(e),t&&t()},s=r.length;if(s>0){var f=()=>--s||i();for(var o of r)o.out(f)}else i()}function bn(e,t,n){if((e.f&P)===0){e.f^=P;var r=e.nodes&&e.nodes.t;if(r!==null)for(const o of r)(o.is_global||n)&&t.push(o);for(var i=e.first;i!==null;){var s=i.next;if((i.f&te)===0){var f=(i.f&Me)!==0||(i.f&U)!==0&&(e.f&q)!==0;bn(i,t,f?n:!1)}i=s}}}function nt(e){yn(e,!0)}function yn(e,t){if((e.f&P)!==0){e.f^=P,(e.f&k)===0&&(S(e,A),ae.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,i=(n.f&Me)!==0||(n.f&U)!==0;yn(n,i?t:!1),n=r}var s=e.nodes&&e.nodes.t;if(s!==null)for(const f of s)(f.is_global||t)&&f.in()}}function At(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var i=n===r?null:qe(n);t.append(n),n=i}}let Xe=!1,ne=!1;function Lt(e){ne=e}let w=null,$=!1;function V(e){w=e}let m=null;function J(e){m=e}let X=null;function En(e){w!==null&&(X??=new Set).add(e)}let x=null,D=0,j=null;function Zr(e){j=e}let Sn=1,he=0,_e=he;function Dt(e){_e=e}function Tn(){return++Sn}function Ge(e){var t=e.f;if((t&A)!==0)return!0;if(t&O&&(e.f&=~pe),(t&Y)!==0){for(var n=e.deps,r=n.length,i=0;ie.wv)return!0}(t&B)!==0&&G===null&&S(e,k)}return!1}function An(e,t,n=!0){var r=e.reactions;if(r!==null&&!(X!==null&&X.has(e)))for(var i=0;i{e.ac.abort(Ve)}),e.ac=null);try{e.f|=Qe;var u=e.fn,h=u();e.f|=Re;var c=e.deps,d=b?.is_fork;if(x!==null){var v;if(d||Be(e,D),c!==null&&D>0)for(c.length=D+x.length,v=0;v{s.ac.abort(Ve),s.ac=null,S(s,A)}),Ir(s),Be(s,0)}}function Be(e,t){var n=e.deps;if(n!==null)for(var r=t;rn?.call(this,s))}return e.startsWith("pointer")||e.startsWith("touch")||e==="wheel"?W(()=>{t.addEventListener(e,i,r)}):t.addEventListener(e,i,r),i}function Bi(e,t,n,r,i){var s={capture:r,passive:i},f=ni(e,t,n,s);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&pn(()=>{t.removeEventListener(e,f,s)})}function Ui(e,t,n){(t[De]??={})[e]=n}function Vi(e){for(var t=0;t{throw p});throw c}}finally{e[De]=t,delete e.currentTarget,V(u),J(h)}}}const ri=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function ii(e){return ri?.createHTML(e)??e}function Rn(e){var t=Vr("template");return t.innerHTML=ii(e.replaceAll("","")),t.content}function Ue(e,t){var n=m;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function qi(e,t){var n=(t&dr)!==0,r=(t&vr)!==0,i,s=!e.startsWith("");return()=>{i===void 0&&(i=Rn(s?e:""+e),n||(i=Ce(i)));var f=r||hn?document.importNode(i,!0):i.cloneNode(!0);if(n){var o=Ce(f),l=f.lastChild;Ue(o,l)}else Ue(f,f);return f}}function si(e,t,n="svg"){var r=!e.startsWith(""),i=`<${n}>${r?e:""+e}`,s;return()=>{if(!s){var f=Rn(i),o=Ce(f);s=Ce(o)}var l=s.cloneNode(!0);return Ue(l,l),l}}function fi(e,t){return si(e,t,"svg")}function Gi(e=""){{var t=ee(e+"");return Ue(t,t),t}}function $i(){var e=document.createDocumentFragment(),t=document.createComment(""),n=ee();return e.append(t,n),Ue(t,n),e}function li(e,t){e!==null&&e.before(t)}function Yi(e,t){var n=t==null?"":typeof t=="object"?`${t}`:t;n!==(e[ct]??=e.nodeValue)&&(e[ct]=n,e.nodeValue=`${n}`)}function Ki(e,t){return oi(e,t)}const Ye=new Map;function oi(e,{target:t,anchor:n,props:r={},events:i,context:s,intro:f=!0,transformError:o}){zr();var l=void 0,a=$r(()=>{var u=n??t.appendChild(ee());Or(u,{pending:()=>{}},d=>{br({});var v=N;s&&(v.c=s),i&&(r.$$events=i),l=e(d,r)||{},yr()},o);var h=new Set,c=d=>{for(var v=0;v{for(var d of h)for(const _ of[t,document]){var v=Ye.get(_),p=v.get(d);--p==0?(_.removeEventListener(d,_t),v.delete(d),v.size===0&&Ye.delete(_)):v.set(d,p)}vt.delete(c),u!==n&&u.parentNode?.removeChild(u)}});return pt.set(l,a),l}let pt=new WeakMap;function Wi(e,t){const n=pt.get(e);return n?(pt.delete(e),n(t)):Promise.resolve()}class ai{anchor;#t=new Map;#f=new Map;#e=new Map;#o=new Set;#r=!0;constructor(t,n=!0){this.anchor=t,this.#r=n}#s=t=>{if(this.#t.has(t)){var n=this.#t.get(t),r=this.#f.get(n);if(r)nt(r),this.#o.delete(n);else{var i=this.#e.get(n);i&&(nt(i.effect),this.#f.set(n,i.effect),this.#e.delete(n),i.fragment.lastChild.remove(),this.anchor.before(i.fragment),r=i.effect)}for(const[s,f]of this.#t){if(this.#t.delete(s),s===t)break;const o=this.#e.get(f);o&&(I(o.effect),this.#e.delete(f))}for(const[s,f]of this.#f){if(s===n||this.#o.has(s))continue;const o=()=>{if(Array.from(this.#t.values()).includes(s)){var a=document.createDocumentFragment();At(f,a),a.append(ee()),this.#e.set(s,{effect:f,fragment:a})}else I(f);this.#o.delete(s),this.#f.delete(s)};this.#r||!r?(this.#o.add(s),ve(f,o,!1)):o()}}};#n=t=>{this.#t.delete(t);const n=Array.from(this.#t.values());for(const[r,i]of this.#e)n.includes(r)||(I(i.effect),this.#e.delete(r))};ensure(t,n){var r=b,i=_n();if(n&&!this.#f.has(t)&&!this.#e.has(t))if(i){var s=document.createDocumentFragment(),f=ee();s.append(f),this.#e.set(t,{effect:z(()=>n(f)),fragment:s})}else this.#f.set(t,z(()=>n(this.anchor)));if(this.#t.set(r,t),i){for(const[o,l]of this.#f)o===t?r.unskip_effect(l):r.skip_effect(l);for(const[o,l]of this.#e)o===t?r.unskip_effect(l.effect):r.skip_effect(l.effect);r.oncommit(this.#s),r.ondiscard(this.#n)}else this.#s(r)}}function Xi(e,t,n=!1){var r=new ai(e),i=n?Me:0;function s(f,o){r.ensure(f,o)}St(()=>{var f=!1;t((o,l=0)=>{f=!0,s(l,o)}),f||s(-1,null)},i)}function Ji(e,t){return t}function ui(e,t,n){for(var r=[],i=t.length,s,f=t.length,o=0;o{if(s){if(s.pending.delete(h),s.done.add(h),s.pending.size===0){var c=e.outrogroups;gt(e,rt(s.done)),c.delete(s),c.size===0&&(e.outrogroups=null)}}else f-=1},!1)}if(f===0){var l=r.length===0&&n!==null;if(l){var a=n,u=a.parentNode;Ur(u),u.append(a),e.items.clear()}gt(e,t,!l)}else s={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(s)}function gt(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(const f of e.pending.values())for(const o of f)r.add(e.items.get(o).e)}for(var i=0;i{var E=n();return Vt(E)?E:E==null?[]:rt(E)}),c,d=new Map,v=!0;function p(E){(T.effect.f&H)===0&&(T.pending.delete(E),T.fallback=u,ci(T,c,f,t,r),u!==null&&(c.length===0?(u.f&K)===0?nt(u):(u.f^=K,Fe(u,null,f)):ve(u,()=>{u=null})))}function _(E){T.pending.delete(E)}var g=St(()=>{c=F(h);for(var E=c.length,y=new Set,R=b,Z=_n(),L=0;Ls(f)):(u=z(()=>s(Ht??=ee())),u.f|=K)),E>y.size&&Wn(),!v)if(d.set(R,y),Z){for(const[In,Ln]of o)y.has(In)||R.skip_effect(Ln.e);R.oncommit(p),R.ondiscard(_)}else p(R);F(h)}),T={effect:g,items:o,pending:d,outrogroups:null,fallback:u};v=!1}function Ie(e){for(;e!==null&&(e.f&U)===0;)e=e.next;return e}function ci(e,t,n,r,i){var s=(r&lr)!==0,f=t.length,o=e.items,l=Ie(e.effect.first),a,u=null,h,c=[],d=[],v,p,_,g;if(s)for(g=0;g0){var we=(r&Kt)!==0&&f===0?n:null;if(s){for(g=0;g{if(h!==void 0)for(_ of h)_.nodes?.a?.apply()})}function hi(e,t,n,r,i,s,f,o){var l=(f&sr)!==0?(f&or)===0?Hr(n,!1,!1):ge(n):null,a=(f&fr)!==0?ge(i):null;return{v:l,i:a,e:z(()=>(s(t,l??n,a??i,o),()=>{e.delete(r)}))}}function Fe(e,t,n){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,s=t&&(t.f&K)===0?t.nodes.start:n;r!==null;){var f=qe(r);if(s.before(r),r===i)return;r=f}}function se(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}const jt=[...` -\r\f \v\uFEFF`];function di(e,t,n){var r=e==null?"":""+e;if(t&&(r=r?r+" "+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+" "+i:i;else if(r.length)for(var s=i.length,f=0;(f=r.indexOf(i,f))>=0;){var o=f+s;(f===0||jt.includes(r[f-1]))&&(o===r.length||jt.includes(r[o]))?r=(f===0?"":r.substring(0,f))+r.substring(o+1):f=o}}return r===""?null:r}function zt(e,t=!1){var n=t?" !important;":";",r="";for(var i of Object.keys(e)){var s=e[i];s!=null&&s!==""&&(r+=" "+i+": "+s+n)}return r}function vi(e,t){if(t){var n="",r,i;return Array.isArray(t)?(r=t[0],i=t[1]):r=t,r&&(n+=zt(r)),i&&(n+=zt(i,!0)),n=n.trim(),n===""?null:n}return String(e)}function Qi(e,t,n,r,i,s){var f=e[at];if(f!==n||f===void 0){var o=di(n,r,s);o==null?e.removeAttribute("class"):e.className=o,e[at]=n}else if(s&&i!==s)for(var l in s){var a=!!s[l];(i==null||a!==!!i[l])&&e.classList.toggle(l,a)}return s}function lt(e,t={},n,r){for(var i in n){var s=n[i];t[i]!==s&&(n[i]==null?e.style.removeProperty(i):e.style.setProperty(i,s,r))}}function _i(e,t,n,r){var i=e[ut];if(i!==t){var s=vi(t,r);s==null?e.removeAttribute("style"):e.style.cssText=s,e[ut]=t}else r&&(Array.isArray(r)?(lt(e,n?.[0],r[0]),lt(e,n?.[1],r[1],"important")):lt(e,n,r));return r}const pi=Symbol("is custom element"),gi=Symbol("is html"),wi=Yn?"progress":"PROGRESS";function es(e,t){var n=Mt(e);n.value===(n.value=t??void 0)||e.value===t&&(t!==0||e.nodeName!==wi)||(e.value=t??"")}function ts(e,t){var n=Mt(e);n.checked!==(n.checked=t??void 0)&&(e.checked=t)}function mi(e,t,n,r){var i=Mt(e);i[t]!==(i[t]=n)&&(t==="loading"&&(e[$n]=n),n==null?e.removeAttribute(t):typeof n!="string"&&bi(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function Mt(e){return e[Yt]??={[pi]:e.nodeName.includes("-"),[gi]:e.namespaceURI===_r}}var Bt=new Map;function bi(e){var t=e.getAttribute("is")||e.nodeName,n=Bt.get(t);if(n)return n;Bt.set(t,n=[]);for(var r,i=e,s=Element.prototype;s!==i;){r=Hn(i);for(var f in r)r[f].set&&f!=="innerHTML"&&f!=="textContent"&&f!=="innerText"&&n.push(f);i=qt(i)}return n}function yi(e,t,n,r){var i=!0,s=(n&cr)!==0,f=(n&hr)!==0,o=r,l=!0,a=void 0,u=()=>f&&i?(a??=ze(r),F(a)):(l&&(l=!1,o=f?Cn(r):r),o);let h;if(s){var c=Te in e||Gn in e;h=Ee(e,t)?.set??(c&&t in e?y=>e[t]=y:void 0)}var d,v=!1;s?[d,v]=Tr(()=>e[t]):d=e[t],d===void 0&&r!==void 0&&(d=u(),h&&(er(),h(d)));var p;if(p=()=>{var y=e[t];return y===void 0?u():(l=!0,y)},(n&ur)===0)return p;if(h){var _=e.$$legacy;return(function(y,R){return arguments.length>0?((!R||_||v)&&h(R?p():y),y):p()})}var g=!1,T=((n&ar)!==0?ze:rn)(()=>(g=!1,p()));s&&F(T);var E=m;return(function(y,R){if(arguments.length>0){const Z=R?F(T):s?ye(y):y;return fe(T,Z),g=!0,o!==void 0&&(o=Z),y}return ne&&g||(E.f&H)!==0?T.v:F(T)})}const Ei="5";typeof window<"u"&&((window.__svelte??={}).v??=new Set).add(Ei);var Si=fi('');function ns(e,t){const n=yi(t,"size",3,"1.5rem"),r={search:"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14M16.5 16.5 21 21",tare:"M8 4h8M9 4v3.2a3 3 0 0 1-.5 1.7l-.8 1.2a4 4 0 0 0-.7 2.2V18a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-5.7a4 4 0 0 0-.7-2.2l-.8-1.2a3 3 0 0 1-.5-1.7V4M7 14h10",printer:"M7 9V4h10v5M7 20h10v-6H7zM7 17H5a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2",alert:"M12 3.5 21.5 20h-19zM12 10v4M12 17.2v.1",backspace:"M9 5h11a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-7-7zM17 9l-6 6M11 9l6 6",check:"M4 12.5 9.5 18 20 7",close:"M6 6l12 12M18 6 6 18",settings:"M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-1.8-.3 1.6 1.6 0 0 0-1 1.5v.2a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.5 1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0 .3-1.8 1.6 1.6 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.5-1 1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.8.3H9a1.6 1.6 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.5 1.6 1.6 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8V9a1.6 1.6 0 0 0 1.5 1h.2a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1"};var i=Si();let s;var f=Br(i);Wr(()=>{s=_i(i,"",s,{width:n(),height:n()}),mi(f,"d",r[t.name])}),li(e,i)}async function rs(){const e=await fetch("/api/v1/catalog",{headers:{accept:"application/json"}});if(!e.ok)throw new Error(`GET /api/v1/catalog: ${e.status}`);return await e.json()}async function is(e){await kt("/api/v1/weigh",e)}async function ss(e,t){await kt("/api/v1/reprint",{job_id:e,key:t})}async function fs(){await kt("/api/v1/dismiss",{})}function ls(e,t){fetch("/api/v1/ui/error",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({message:e,stack:t}),keepalive:!0}).catch(()=>{})}async function kt(e,t){const n=await fetch(e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(t)});if(!n.ok)throw new Error(`POST ${e}: ${n.status}`)}const xn={Œ:"oe",œ:"oe",Æ:"ae",æ:"ae",ß:"ss",Ø:"o",ø:"o"},Ti=new RegExp(`[${Object.keys(xn).join("")}]`,"gu"),Ai=/\p{Mn}/u,Mi=/[\p{L}\p{Nd}]/u;function ki(e){const n=e.replace(Ti,s=>xn[s]).normalize("NFD");let r="",i=!1;for(const s of n)if(!Ai.test(s)){if(Mi.test(s)){i&&r.length>0&&(r+=" "),i=!1,r+=s.toLowerCase();continue}i=!0}return r}const Oi=5,Nn="";function Ci(e){const t=new Set(e.categories.filter(n=>!n.visible).map(n=>n.code));return t.size===0?e.products:e.products.filter(n=>!t.has(n.category_code))}function os(e){const t=Ci(e),n=new Map;for(const i of t)n.set(i.category_code,(n.get(i.category_code)??0)+1);const r=e.categories.filter(i=>i.visible&&(n.get(i.code)??0)>=Oi).slice().sort((i,s)=>i.rank-s.rank||i.code.localeCompare(s.code)).map(i=>({code:i.code,label:i.label,color:i.color,count:n.get(i.code)??0}));return[{code:Nn,label:"Tout",color:"var(--ink-muted)",count:t.length},...r]}function as(e,t,n){const r=ki(n),i=r.length===0?[]:r.split(" ");return e.filter(s=>t!==Nn&&s.category_code!==t?!1:i.every(f=>s.search.includes(f)))}export{zi as $,ie as A,Hi as B,Ji as C,Ct as D,es as E,ye as F,rs as G,Nn as H,ns as I,is as J,as as K,os as L,ss as M,Ci as N,fs as O,Ki as P,ls as Q,St as R,Te as S,Me as T,ai as U,Ni as V,b as W,Vt as X,Ri as Y,Li as Z,pn as _,m as a,xi as a0,ts as a1,$i as a2,Gi as a3,Bn as a4,Wi as a5,yi as b,N as c,Vi as d,ji as e,Ui as f,li as g,yr as h,Xi as i,qi as j,F as k,mi as l,_i as m,Qi as n,Yi as o,br as p,Ii as q,Kr as r,Fi as s,Wr as t,Cn as u,Br as v,Di as w,Zi as x,Bi as y,fe as z}; +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const f of s.addedNodes)f.tagName==="LINK"&&f.rel==="modulepreload"&&r(f)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();const Ut=!1;var Vt=Array.isArray,Dn=Array.prototype.indexOf,Je=Array.prototype.includes,rt=Array.from,Fn=Object.defineProperty,Ee=Object.getOwnPropertyDescriptor,Hn=Object.getOwnPropertyDescriptors,jn=Object.prototype,zn=Array.prototype,qt=Object.getPrototypeOf,Ot=Object.isExtensible;const Bn=()=>{};function Un(e){for(var t=0;t{e=r,t=i});return{promise:n,resolve:e,reject:t}}const O=2,Ae=4,it=8,Yt=1<<24,q=16,U=32,te=64,ot=128,B=512,k=1024,A=2048,$=4096,P=8192,H=16384,Re=32768,Ct=1<<25,Me=65536,Ze=1<<17,Vn=1<<18,xe=1<<19,qn=1<<20,K=1<<25,pe=65536,Qe=1<<21,Se=1<<22,oe=1<<23,Te=Symbol("$state"),Gn=Symbol("legacy props"),Yn=Symbol(""),$t=Symbol("attributes"),at=Symbol("class"),ut=Symbol("style"),ct=Symbol("text"),Ke=Symbol("form reset"),Ve=new class extends Error{name="StaleReactionError";message="The reaction that called `getAbortSignal()` was re-run or destroyed"},$n=!!globalThis.document?.contentType&&globalThis.document.contentType.includes("xml");function Kn(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function Wn(e,t,n){throw new Error("https://svelte.dev/e/each_key_duplicate")}function Xn(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function Jn(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function Zn(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Qn(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function er(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function tr(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function nr(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function rr(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function ir(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const sr=1,fr=2,Kt=4,lr=8,or=16,ar=1,ur=4,cr=8,hr=16,dr=1,vr=2,M=Symbol("uninitialized"),_r="http://www.w3.org/1999/xhtml";function pr(){console.warn("https://svelte.dev/e/derived_inert")}function xi(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function gr(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}function Wt(e){return e===this.v}function wr(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function Xt(e){return!wr(e,this.v)}let mr=!1,N=null;function ke(e){N=e}function br(e,t=!1,n){N={p:N,i:!1,c:null,e:null,s:e,x:null,r:m,l:null}}function yr(e){var t=N,n=t.e;if(n!==null){t.e=null;for(var r of n)gn(r)}return t.i=!0,N=t.p,{}}function Jt(){return!0}let ce=[];function Zt(){var e=ce;ce=[],Un(e)}function W(e){if(ce.length===0&&!He){var t=ce;queueMicrotask(()=>{t===ce&&Zt()})}ce.push(e)}function Er(){for(;ce.length>0;)Zt()}function Qt(e){var t=m;if(t===null)return w.f|=oe,e;if((t.f&Re)===0&&(t.f&Ae)===0)throw e;le(e,t)}function le(e,t){if(!(t!==null&&(t.f&H)!==0)){for(;t!==null;){if((t.f&ot)!==0){if((t.f&Re)===0)throw e;try{t.b.error(e);return}catch(n){e=n}}t=t.parent}throw e}}const Sr=-7169;function S(e,t){e.f=e.f&Sr|t}function wt(e){(e.f&B)!==0||e.deps===null?S(e,k):S(e,$)}function en(e){if(e!==null)for(const t of e)(t.f&O)===0||(t.f&pe)===0||(t.f^=pe,en(t.deps))}function tn(e,t,n){(e.f&A)!==0?t.add(e):(e.f&$)!==0&&n.add(e),en(e.deps),S(e,k)}let Ye=!1;function Tr(e){var t=Ye;try{return Ye=!1,[e(),Ye]}finally{Ye=t}}function Ni(e,t){{const n=document.body;e.autofocus=!0,W(()=>{document.activeElement===n&&e.focus()})}}let Pt=!1;function Ar(){Pt||(Pt=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(const t of e.target.elements)t[Ke]?.()})},{capture:!0}))}function Ne(e){var t=w,n=m;V(null),J(null);try{return e()}finally{V(t),J(n)}}function Ii(e,t,n,r=n){e.addEventListener(t,()=>Ne(n));const i=e[Ke];i?e[Ke]=()=>{i(),r(!0)}:e[Ke]=()=>r(!0),Ar()}function Mr(e){let t=0,n=ge(0),r;return()=>{Et()&&(F(n),Kr(()=>(t===0&&(r=Cn(()=>e(()=>je(n)))),t+=1,()=>{W(()=>{t-=1,t===0&&(r?.(),r=void 0,je(n))})})))}}var kr=Me|xe;function Or(e,t,n,r){new Cr(e,t,n,r)}class Cr{parent;is_pending=!1;transform_error;#t;#f=null;#e;#o;#r;#s=null;#n=null;#l=null;#i=null;#_=0;#a=0;#u=!1;#h=new Set;#p=new Set;#c=null;#w=Mr(()=>(this.#c=ge(this.#_),()=>{this.#c=null}));constructor(t,n,r,i){this.#t=t,this.#e=n,this.#o=s=>{var f=m;f.b=this,f.f|=ot,r(s)},this.parent=m.b,this.transform_error=i??this.parent?.transform_error??(s=>s),this.#r=St(()=>{this.#d()},kr)}#g(){try{this.#s=z(()=>this.#o(this.#t))}catch(t){this.error(t)}}#y(t){const n=this.#e.failed,{reset:r,invoke_onerror:i}=this.#m(t);W(i),n&&(this.#l=z(()=>{n(this.#t,()=>t,()=>r)}))}#m(t){var n=!1,r=!1;const i=()=>{if(n){gr();return}n=!0,r&&ir(),this.#l!==null&&ve(this.#l,()=>{this.#l=null}),this.#v(()=>{this.#d()})};return{reset:i,invoke_onerror:()=>{try{r=!0,this.#e.onerror?.(t,i),r=!1}catch(f){le(f,this.#r&&this.#r.parent)}}}}#E(){const t=this.#e.pending;t&&(this.is_pending=!0,this.#n=z(()=>t(this.#t)),W(()=>{var n=this.#i=document.createDocumentFragment(),r=ee();n.append(r),this.#s=this.#v(()=>z(()=>this.#o(r))),this.#a===0&&(this.#t.before(n),this.#i=null,ve(this.#n,()=>{this.#n=null}),this.#b(b))}))}#d(){try{if(this.is_pending=this.has_pending_snippet(),this.#a=0,this.#_=0,this.#s=z(()=>{this.#o(this.#t)}),this.#a>0){var t=this.#i=document.createDocumentFragment();At(this.#s,t);const n=this.#e.pending;this.#n=z(()=>n(this.#t))}else this.#b(b)}catch(n){this.error(n)}}#b(t){this.is_pending=!1,t.transfer_effects(this.#h,this.#p)}defer_effect(t){tn(t,this.#h,this.#p)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#e.pending}#v(t){var n=m,r=w,i=N;J(this.#r),V(this.#r),ke(this.#r.ctx);try{return ae.ensure(),t()}catch(s){return Qt(s),null}finally{J(n),V(r),ke(i)}}#S(t,n){if(!this.has_pending_snippet()){this.parent&&this.parent.#S(t,n);return}this.#a+=t,this.#a===0&&(this.#b(n),this.#n&&ve(this.#n,()=>{this.#n=null}),this.#i&&(this.#t.before(this.#i),this.#i=null))}update_pending_count(t,n){this.#S(t,n),this.#_+=t,!(!this.#c||this.#u)&&(this.#u=!0,W(()=>{this.#u=!1,this.#c&&Oe(this.#c,this.#_)}))}get_effect_pending(){return this.#w(),F(this.#c)}error(t){if(!this.#e.onerror&&!this.#e.failed)throw t;b?.is_fork?(this.#s&&b.skip_effect(this.#s),this.#n&&b.skip_effect(this.#n),this.#l&&b.skip_effect(this.#l),b.oncommit(()=>{this.#T(t)})):this.#T(t)}#T(t){this.#s&&(I(this.#s),this.#s=null),this.#n&&(I(this.#n),this.#n=null),this.#l&&(I(this.#l),this.#l=null);let n=this.#e.failed;const r=i=>{const{reset:s,invoke_onerror:f}=this.#m(i);f(),n&&(this.#l=this.#v(()=>{try{return z(()=>{var o=m;o.b=this,o.f|=ot,n(this.#t,()=>i,()=>s)})}catch(o){return le(o,this.#r.parent),null}}))};W(()=>{var i;try{i=this.transform_error(t)}catch(s){le(s,this.#r&&this.#r.parent);return}i!==null&&typeof i=="object"&&typeof i.then=="function"?i.then(r,s=>le(s,this.#r&&this.#r.parent)):r(i)})}}function Pr(e,t,n,r){const i=ze;var s=e.filter(d=>!d.settled),f=t.map(i);if(n.length===0&&s.length===0){r(f);return}var o=m,l=Rr(),a=s.length===1?s[0].promise:s.length>1?Promise.all(s.map(d=>d.promise)):null;function u(d){if((o.f&H)===0){l();try{r([...f,...d])}catch(v){le(v,o)}et()}}var h=nn();if(n.length===0){a.then(()=>u([])).finally(h);return}function c(){Promise.all(n.map(d=>xr(d))).then(u).catch(d=>le(d,o)).finally(h)}a?a.then(()=>{l(),c(),et()}):c()}function Rr(){var e=m,t=w,n=N,r=b;return function(s=!0){J(e),V(t),ke(n),s&&(e.f&H)===0&&(r?.activate(),r?.apply())}}function et(e=!0){J(null),V(null),ke(null),e&&b?.deactivate()}function nn(){var e=m,t=e.b,n=b,r=!!t?.is_rendered();return t?.update_pending_count(1,n),n.increment(r,e),()=>{t?.update_pending_count(-1,n),n.decrement(r,e)}}function ze(e){var t=O|A;return m!==null&&(m.f|=xe),{ctx:N,deps:null,effects:null,equals:Wt,f:t,fn:e,reactions:null,rv:0,v:M,wv:0,parent:m,ac:null}}const Le=Symbol("obsolete");function xr(e,t,n){let r=m;r===null&&Kn();var i=void 0,s=ge(M),f=!w,o=new Set;return $r(()=>{var l=m,a=Gt();i=a.promise;try{Promise.resolve(e()).then(a.resolve,d=>{d!==Ve&&a.reject(d)}).finally(et)}catch(d){a.reject(d),et()}var u=b;if(f){if((l.f&Re)!==0)var h=nn();if(r.b?.is_rendered())u.async_deriveds.get(l)?.reject(Le);else for(const d of o.values())d.reject(Le);o.add(a),u.async_deriveds.set(l,a)}const c=(d,v=void 0)=>{h?.(),o.delete(a),v!==Le&&(u.activate(),v?(s.f|=oe,Oe(s,v)):((s.f&oe)!==0&&(s.f^=oe),Oe(s,d)),u.deactivate())};a.promise.then(c,d=>c(null,d||"unknown"))}),pn(()=>{for(const l of o)l.reject(Le)}),new Promise(l=>{function a(u){function h(){u===i?l(s):a(i)}u.then(h,h)}a(i)})}function Li(e){const t=ze(e);return En(t),t}function rn(e){const t=ze(e);return t.equals=Xt,t}function Nr(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n{t.ac.abort(Ve),t.ac=null}),t.fn!==null&&(t.teardown=Bn),Be(t,0),Tt(t))}function fn(e){if(e.effects!==null)for(const t of e.effects)t.teardown&&t.fn!==null&&Pe(t)}let st=null,me=null,b=null,ht=null,G=null,dt=null,He=!1,ft=!1,be=null,We=null;var Rt=0;let Lr=1;class ae{id=Lr++;#t=!1;linked=!0;#f=null;#e=null;async_deriveds=new Map;current=new Map;previous=new Map;#o=new Set;#r=new Set;#s=0;#n=new Map;#l=null;#i=[];#_=[];#a=new Set;#u=new Set;#h=new Map;#p=new Set;is_fork=!1;#c=!1;constructor(){me===null?st=me=this:(me.#e=this,this.#f=me),me=this}#w(){if(this.is_fork)return!0;for(const r of this.#n.keys()){for(var t=r,n=!1;t.parent!==null;){if(this.#h.has(t)){n=!0;break}t=t.parent}if(!n)return!0}return!1}skip_effect(t){this.#h.has(t)||this.#h.set(t,{d:[],m:[]}),this.#p.delete(t)}unskip_effect(t,n=r=>this.schedule(r)){var r=this.#h.get(t);if(r){this.#h.delete(t);for(var i of r.d)S(i,A),n(i);for(i of r.m)S(i,$),n(i)}this.#p.add(t)}#g(){this.#t=!0,Rt++>1e3&&(this.#v(),Fr());for(const l of this.#a)this.#u.delete(l),S(l,A),this.schedule(l);for(const l of this.#u)S(l,$),this.schedule(l);const t=this.#i;this.#i=[],this.apply();var n=be=[],r=[],i=We=[];for(const l of t)try{this.#y(l,n,r)}catch(a){throw an(l),this.#w()||this.discard(),a}if(b=null,i.length>0){var s=ae.ensure();for(const l of i)s.schedule(l)}if(be=null,We=null,this.#w()){this.#d(r),this.#d(n);for(const[l,a]of this.#h)on(l,a);i.length>0&&b.#g();return}const f=this.#m();if(f){this.#d(r),this.#d(n),f.#E(this);return}this.#a.clear(),this.#u.clear();for(const l of this.#o)l(this);this.#o.clear(),ht=this,xt(r),xt(n),ht=null,this.#l?.resolve();var o=b;if(this.#s===0&&(this.#i.length===0||o!==null)&&this.#v(),this.#i.length>0)if(o!==null){const l=o;l.#i.push(...this.#i.filter(a=>!l.#i.includes(a)))}else o=this;o!==null&&o.#g()}#y(t,n,r){t.f^=k;for(var i=t.first;i!==null;){var s=i.f,f=(s&(U|te))!==0,o=f&&(s&k)!==0,l=o||(s&P)!==0||this.#h.has(i);if(!l&&i.fn!==null){f?i.f^=k:(s&Ae)!==0?n.push(i):Ge(i)&&((s&q)!==0&&this.#u.add(i),Pe(i));var a=i.first;if(a!==null){i=a;continue}}for(;i!==null;){var u=i.next;if(u!==null){i=u;break}i=i.parent}}}#m(){for(var t=this.#f;t!==null;){if(!t.is_fork){for(const[n,[,r]]of this.current)if(t.current.has(n)&&!r)return t}t=t.#f}return null}#E(t){for(const[r,i]of t.current)!this.previous.has(r)&&t.previous.has(r)&&this.previous.set(r,t.previous.get(r)),this.current.set(r,i);for(const[r,i]of t.async_deriveds){const s=this.async_deriveds.get(r);s&&i.promise.then(s.resolve).catch(s.reject)}t.async_deriveds.clear(),this.transfer_effects(t.#a,t.#u);const n=r=>{var i=r.reactions;if(i!==null&&!((r.f&O)!==0&&(r.f&(A|$))===0))for(const o of i){var s=o.f;if((s&O)!==0)n(o);else{var f=o;s&(Se|q)&&!this.async_deriveds.has(f)&&(this.#u.delete(f),S(f,A),this.schedule(f))}}};for(const r of this.current.keys())n(r);this.oncommit(()=>t.discard()),t.#v(),b=this,this.#g()}#d(t){for(var n=0;n!h.current.get(c)[1]);if(!(!h.#t||i.length===0)){var s=i.filter(c=>!this.current.has(c));if(s.length===0)t&&h.discard();else if(n.length>0){if(t)for(const c of this.#p)h.unskip_effect(c,d=>{(d.f&(q|Se))!==0?h.schedule(d):h.#d([d])});h.activate();var f=new Set,o=new Map;for(var l of n)ln(l,s,f,o);o=new Map;var a=[...h.current].filter(([c,d])=>{const v=this.current.get(c);return v?v[0]!==d[0]||v[1]!==d[1]:!0}).map(([c])=>c);if(a.length>0)for(const c of this.#_)(c.f&(H|P|Ze))===0&&bt(c,a,o)&&((c.f&(Se|q))!==0?(S(c,A),h.schedule(c)):h.#a.add(c));if(h.#i.length>0&&!h.#c){h.apply();for(var u of h.#i)h.#y(u,[],[]);h.#i=[]}h.deactivate()}}}}increment(t,n){if(this.#s+=1,t){let r=this.#n.get(n)??0;this.#n.set(n,r+1)}}decrement(t,n){if(this.#s-=1,t){let r=this.#n.get(n)??0;r===1?this.#n.delete(n):this.#n.set(n,r-1)}this.#c||(this.#c=!0,W(()=>{this.#c=!1,this.linked&&this.flush()}))}transfer_effects(t,n){for(const r of t)this.#a.add(r);for(const r of n)this.#u.add(r);t.clear(),n.clear()}oncommit(t){this.#o.add(t)}ondiscard(t){this.#r.add(t)}settled(){return(this.#l??=Gt()).promise}static ensure(){if(b===null){const t=b=new ae;!ft&&!He&&W(()=>{t.#t||t.flush()})}return b}apply(){{G=null;return}}schedule(t){if(dt=t,t.b?.is_pending&&(t.f&(Ae|it|Yt))!==0&&(t.f&Re)===0){t.b.defer_effect(t);return}for(var n=t;n.parent!==null;){n=n.parent;var r=n.f;if(be!==null&&n===m&&(w===null||(w.f&O)===0))return;if((r&(te|U))!==0){if((r&k)===0)return;n.f^=k}}this.#i.push(n)}#v(){if(this.linked){var t=this.#f,n=this.#e;t===null?st=n:t.#e=n,n===null?me=t:n.#f=t,this.linked=!1}}}function Dr(e){var t=He;He=!0;try{for(var n;;){if(Er(),b===null)return n;b.flush()}}finally{He=t}}function Fr(){try{Qn()}catch(e){le(e,dt)}}let Q=null;function xt(e){var t=e.length;if(t!==0){for(var n=0;n0)){de.clear();for(const i of Q){if((i.f&(H|P))!==0)continue;const s=[i];let f=i.parent;for(;f!==null;)Q.has(f)&&(Q.delete(f),s.push(f)),f=f.parent;for(let o=s.length-1;o>=0;o--){const l=s[o];(l.f&(H|P))===0&&Pe(l)}}Q.clear()}}Q=null}}function ln(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(const i of e.reactions){const s=i.f;(s&O)!==0?ln(i,t,n,r):(s&(Se|q))!==0&&(s&A)===0&&bt(i,t,r)&&(S(i,A),yt(i))}}function bt(e,t,n){const r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const i of e.deps){if(Je.call(t,i))return!0;if((i.f&O)!==0&&bt(i,t,n))return n.set(i,!0),!0}return n.set(e,!1),!1}function yt(e){b.schedule(e)}function on(e,t){if(!((e.f&U)!==0&&(e.f&k)!==0)){(e.f&A)!==0?t.d.push(e):(e.f&$)!==0&&t.m.push(e),S(e,k);for(var n=e.first;n!==null;)on(n,t),n=n.next}}function an(e){S(e,k);for(var t=e.first;t!==null;)an(t),t=t.next}let tt=new Set;const de=new Map;let un=!1;function ge(e,t){var n={f:0,v:e,reactions:null,equals:Wt,rv:0,wv:0};return n}function ie(e,t){const n=ge(e);return En(n),n}function Hr(e,t=!1,n=!0){const r=ge(e);return t||(r.equals=Xt),r}function fe(e,t,n=!1){w!==null&&(!Y||(w.f&Ze)!==0)&&Jt()&&(w.f&(O|q|Se|Ze))!==0&&(X===null||!X.has(e))&&rr();let r=n?ye(t):t;return Oe(e,r,We)}function Oe(e,t,n=null){if(!e.equals(t)){de.set(e,ne?t:e.v);var r=ae.ensure();if(r.capture(e,t),(e.f&O)!==0){const i=e;(e.f&A)!==0&&mt(i),G===null&&wt(i)}e.wv=Tn(),cn(e,A,n),m!==null&&(m.f&k)!==0&&(m.f&(U|te))===0&&(j===null?Zr([e]):j.push(e)),!r.is_fork&&tt.size>0&&!un&&jr()}return t}function jr(){un=!1;for(const e of tt){(e.f&k)!==0&&S(e,$);let t;try{t=Ge(e)}catch{t=!0}t&&Pe(e)}tt.clear()}function je(e){fe(e,e.v+1)}function cn(e,t,n){var r=e.reactions;if(r!==null)for(var i=r.length,s=0;s{if(_e===s)return o();var l=w,a=_e;V(null),Dt(s);var u=o();return V(l),Dt(a),u};return r&&n.set("length",ie(e.length)),new Proxy(e,{defineProperty(o,l,a){(!("value"in a)||a.configurable===!1||a.enumerable===!1||a.writable===!1)&&tr();var u=n.get(l);return u===void 0?f(()=>{var h=ie(a.value);return n.set(l,h),h}):fe(u,a.value,!0),!0},deleteProperty(o,l){var a=n.get(l);if(a===void 0){if(l in o){const u=f(()=>ie(M));n.set(l,u),je(i)}}else fe(a,M),je(i);return!0},get(o,l,a){if(l===Te)return e;var u=n.get(l),h=l in o;if(u===void 0&&(!h||Ee(o,l)?.writable)&&(u=f(()=>{var d=ye(h?o[l]:M),v=ie(d);return v}),n.set(l,u)),u!==void 0){var c=F(u);return c===M?void 0:c}return Reflect.get(o,l,a)},getOwnPropertyDescriptor(o,l){var a=Reflect.getOwnPropertyDescriptor(o,l);if(a&&"value"in a){var u=n.get(l);u&&(a.value=F(u))}else if(a===void 0){var h=n.get(l),c=h?.v;if(h!==void 0&&c!==M)return{enumerable:!0,configurable:!0,value:c,writable:!0}}return a},has(o,l){if(l===Te)return!0;var a=n.get(l),u=a!==void 0&&a.v!==M||Reflect.has(o,l);if(a!==void 0||m!==null&&(!u||Ee(o,l)?.writable)){a===void 0&&(a=f(()=>{var c=u?ye(o[l]):M,d=ie(c);return d}),n.set(l,a));var h=F(a);if(h===M)return!1}return u},set(o,l,a,u){var h=n.get(l),c=l in o;if(r&&l==="length")for(var d=a;die(M)),n.set(d+"",v))}if(h===void 0)(!c||Ee(o,l)?.writable)&&(h=f(()=>ie(void 0)),fe(h,ye(a)),n.set(l,h));else{c=h.v!==M;var p=f(()=>ye(a));fe(h,p)}var _=Reflect.getOwnPropertyDescriptor(o,l);if(_?.set&&_.set.call(u,a),!c){if(r&&typeof l=="string"){var g=n.get("length"),T=Number(l);Number.isInteger(T)&&T>=g.v&&fe(g,T+1)}je(i)}return!0},ownKeys(o){F(i);var l=Reflect.ownKeys(o).filter(h=>{var c=n.get(h);return c===void 0||c.v!==M});for(var[a,u]of n)u.v!==M&&!(a in o)&&l.push(a);return l},setPrototypeOf(){nr()}})}function Nt(e){try{if(e!==null&&typeof e=="object"&&Te in e)return e[Te]}catch{}return e}function Di(e,t){return Object.is(Nt(e),Nt(t))}var It,hn,dn,vn;function zr(){if(It===void 0){It=window,hn=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;dn=Ee(t,"firstChild").get,vn=Ee(t,"nextSibling").get,Ot(e)&&(e[at]=void 0,e[$t]=null,e[ut]=void 0,e.__e=void 0),Ot(n)&&(n[ct]=void 0)}}function ee(e=""){return document.createTextNode(e)}function Ce(e){return dn.call(e)}function qe(e){return vn.call(e)}function Br(e,t){return Ce(e)}function Fi(e,t=!1){{var n=Ce(e);return n instanceof Comment&&n.data===""?qe(n):n}}function Hi(e,t=1,n=!1){let r=e;for(;t--;)r=qe(r);return r}function Ur(e){e.textContent=""}function _n(){return!1}function Vr(e,t,n){return n?document.createElement(e,{is:n}):document.createElement(e)}function qr(e){m===null&&(w===null&&Zn(),Jn()),ne&&Xn()}function Gr(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function re(e,t){var n=m;n!==null&&(n.f&P)!==0&&(e|=P);var r={ctx:N,deps:null,nodes:null,f:e|A|B,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};b?.register_created_effect(r);var i=r;if((e&Ae)!==0)be!==null?be.push(r):ae.ensure().schedule(r);else if(t!==null){try{Pe(r)}catch(f){throw I(r),f}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&(i.f&xe)===0&&(i=i.first,(e&q)!==0&&(e&Me)!==0&&i!==null&&(i.f|=Me))}if(i!==null&&(i.parent=n,n!==null&&Gr(i,n),w!==null&&(w.f&O)!==0&&(e&te)===0)){var s=w;(s.effects??=[]).push(i)}return r}function Et(){return w!==null&&!Y}function pn(e){const t=re(it,null);return S(t,k),t.teardown=e,t}function ji(e){qr();var t=m.f,n=!w&&(t&U)!==0&&N!==null&&!N.i;if(n){var r=N;(r.e??=[]).push(e)}else return gn(e)}function gn(e){return re(Ae|qn,e)}function Yr(e){ae.ensure();const t=re(te|xe,e);return(n={})=>new Promise(r=>{n.outro?ve(t,()=>{I(t),r(void 0)}):(I(t),r(void 0))})}function zi(e){return re(Ae,e)}function $r(e){return re(Se|xe,e)}function Kr(e,t=0){return re(it|t,e)}function Wr(e,t=[],n=[],r=[]){Pr(r,t,n,i=>{re(it,()=>{e(...i.map(F))})})}function St(e,t=0){var n=re(q|t,e);return n}function z(e){return re(U|xe,e)}function wn(e){var t=e.teardown;if(t!==null){const n=ne,r=w;Lt(!0),V(null);try{t.call(null)}finally{Lt(n),V(r)}}}function Tt(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const i=n.ac;i!==null&&Ne(()=>{i.abort(Ve)});var r=n.next;(n.f&te)!==0?n.parent=null:I(n,t),n=r}}function Xr(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&U)===0&&I(t),t=n}}function I(e,t=!0){var n=!1;(t||(e.f&Vn)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(Jr(e.nodes.start,e.nodes.end),n=!0),e.f|=Ct,Tt(e,t&&!n),Be(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(const s of r)s.stop();wn(e),e.f^=Ct,e.f|=H;var i=e.parent;i!==null&&i.first!==null&&mn(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function Jr(e,t){for(;e!==null;){var n=e===t?null:qe(e);e.remove(),e=n}}function mn(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function ve(e,t,n=!0){var r=[];bn(e,r,!0);var i=()=>{n&&I(e),t&&t()},s=r.length;if(s>0){var f=()=>--s||i();for(var o of r)o.out(f)}else i()}function bn(e,t,n){if((e.f&P)===0){e.f^=P;var r=e.nodes&&e.nodes.t;if(r!==null)for(const o of r)(o.is_global||n)&&t.push(o);for(var i=e.first;i!==null;){var s=i.next;if((i.f&te)===0){var f=(i.f&Me)!==0||(i.f&U)!==0&&(e.f&q)!==0;bn(i,t,f?n:!1)}i=s}}}function nt(e){yn(e,!0)}function yn(e,t){if((e.f&P)!==0){e.f^=P,(e.f&k)===0&&(S(e,A),ae.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,i=(n.f&Me)!==0||(n.f&U)!==0;yn(n,i?t:!1),n=r}var s=e.nodes&&e.nodes.t;if(s!==null)for(const f of s)(f.is_global||t)&&f.in()}}function At(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var i=n===r?null:qe(n);t.append(n),n=i}}let Xe=!1,ne=!1;function Lt(e){ne=e}let w=null,Y=!1;function V(e){w=e}let m=null;function J(e){m=e}let X=null;function En(e){w!==null&&(X??=new Set).add(e)}let x=null,D=0,j=null;function Zr(e){j=e}let Sn=1,he=0,_e=he;function Dt(e){_e=e}function Tn(){return++Sn}function Ge(e){var t=e.f;if((t&A)!==0)return!0;if(t&O&&(e.f&=~pe),(t&$)!==0){for(var n=e.deps,r=n.length,i=0;ie.wv)return!0}(t&B)!==0&&G===null&&S(e,k)}return!1}function An(e,t,n=!0){var r=e.reactions;if(r!==null&&!(X!==null&&X.has(e)))for(var i=0;i{e.ac.abort(Ve)}),e.ac=null);try{e.f|=Qe;var u=e.fn,h=u();e.f|=Re;var c=e.deps,d=b?.is_fork;if(x!==null){var v;if(d||Be(e,D),c!==null&&D>0)for(c.length=D+x.length,v=0;v{s.ac.abort(Ve),s.ac=null,S(s,A)}),Ir(s),Be(s,0)}}function Be(e,t){var n=e.deps;if(n!==null)for(var r=t;rn?.call(this,s))}return e.startsWith("pointer")||e.startsWith("touch")||e==="wheel"?W(()=>{t.addEventListener(e,i,r)}):t.addEventListener(e,i,r),i}function Ui(e,t,n,r,i){var s={capture:r,passive:i},f=ni(e,t,n,s);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&pn(()=>{t.removeEventListener(e,f,s)})}function Vi(e,t,n){(t[De]??={})[e]=n}function qi(e){for(var t=0;t{throw p});throw c}}finally{e[De]=t,delete e.currentTarget,V(u),J(h)}}}const ri=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function ii(e){return ri?.createHTML(e)??e}function Rn(e){var t=Vr("template");return t.innerHTML=ii(e.replaceAll("","")),t.content}function Ue(e,t){var n=m;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function Gi(e,t){var n=(t&dr)!==0,r=(t&vr)!==0,i,s=!e.startsWith("");return()=>{i===void 0&&(i=Rn(s?e:""+e),n||(i=Ce(i)));var f=r||hn?document.importNode(i,!0):i.cloneNode(!0);if(n){var o=Ce(f),l=f.lastChild;Ue(o,l)}else Ue(f,f);return f}}function si(e,t,n="svg"){var r=!e.startsWith(""),i=`<${n}>${r?e:""+e}`,s;return()=>{if(!s){var f=Rn(i),o=Ce(f);s=Ce(o)}var l=s.cloneNode(!0);return Ue(l,l),l}}function fi(e,t){return si(e,t,"svg")}function Yi(e=""){{var t=ee(e+"");return Ue(t,t),t}}function $i(){var e=document.createDocumentFragment(),t=document.createComment(""),n=ee();return e.append(t,n),Ue(t,n),e}function li(e,t){e!==null&&e.before(t)}function Ki(e,t){var n=t==null?"":typeof t=="object"?`${t}`:t;n!==(e[ct]??=e.nodeValue)&&(e[ct]=n,e.nodeValue=`${n}`)}function Wi(e,t){return oi(e,t)}const $e=new Map;function oi(e,{target:t,anchor:n,props:r={},events:i,context:s,intro:f=!0,transformError:o}){zr();var l=void 0,a=Yr(()=>{var u=n??t.appendChild(ee());Or(u,{pending:()=>{}},d=>{br({});var v=N;s&&(v.c=s),i&&(r.$$events=i),l=e(d,r)||{},yr()},o);var h=new Set,c=d=>{for(var v=0;v{for(var d of h)for(const _ of[t,document]){var v=$e.get(_),p=v.get(d);--p==0?(_.removeEventListener(d,_t),v.delete(d),v.size===0&&$e.delete(_)):v.set(d,p)}vt.delete(c),u!==n&&u.parentNode?.removeChild(u)}});return pt.set(l,a),l}let pt=new WeakMap;function Xi(e,t){const n=pt.get(e);return n?(pt.delete(e),n(t)):Promise.resolve()}class ai{anchor;#t=new Map;#f=new Map;#e=new Map;#o=new Set;#r=!0;constructor(t,n=!0){this.anchor=t,this.#r=n}#s=t=>{if(this.#t.has(t)){var n=this.#t.get(t),r=this.#f.get(n);if(r)nt(r),this.#o.delete(n);else{var i=this.#e.get(n);i&&(nt(i.effect),this.#f.set(n,i.effect),this.#e.delete(n),i.fragment.lastChild.remove(),this.anchor.before(i.fragment),r=i.effect)}for(const[s,f]of this.#t){if(this.#t.delete(s),s===t)break;const o=this.#e.get(f);o&&(I(o.effect),this.#e.delete(f))}for(const[s,f]of this.#f){if(s===n||this.#o.has(s))continue;const o=()=>{if(Array.from(this.#t.values()).includes(s)){var a=document.createDocumentFragment();At(f,a),a.append(ee()),this.#e.set(s,{effect:f,fragment:a})}else I(f);this.#o.delete(s),this.#f.delete(s)};this.#r||!r?(this.#o.add(s),ve(f,o,!1)):o()}}};#n=t=>{this.#t.delete(t);const n=Array.from(this.#t.values());for(const[r,i]of this.#e)n.includes(r)||(I(i.effect),this.#e.delete(r))};ensure(t,n){var r=b,i=_n();if(n&&!this.#f.has(t)&&!this.#e.has(t))if(i){var s=document.createDocumentFragment(),f=ee();s.append(f),this.#e.set(t,{effect:z(()=>n(f)),fragment:s})}else this.#f.set(t,z(()=>n(this.anchor)));if(this.#t.set(r,t),i){for(const[o,l]of this.#f)o===t?r.unskip_effect(l):r.skip_effect(l);for(const[o,l]of this.#e)o===t?r.unskip_effect(l.effect):r.skip_effect(l.effect);r.oncommit(this.#s),r.ondiscard(this.#n)}else this.#s(r)}}function Ji(e,t,n=!1){var r=new ai(e),i=n?Me:0;function s(f,o){r.ensure(f,o)}St(()=>{var f=!1;t((o,l=0)=>{f=!0,s(l,o)}),f||s(-1,null)},i)}function Zi(e,t){return t}function ui(e,t,n){for(var r=[],i=t.length,s,f=t.length,o=0;o{if(s){if(s.pending.delete(h),s.done.add(h),s.pending.size===0){var c=e.outrogroups;gt(e,rt(s.done)),c.delete(s),c.size===0&&(e.outrogroups=null)}}else f-=1},!1)}if(f===0){var l=r.length===0&&n!==null;if(l){var a=n,u=a.parentNode;Ur(u),u.append(a),e.items.clear()}gt(e,t,!l)}else s={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(s)}function gt(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(const f of e.pending.values())for(const o of f)r.add(e.items.get(o).e)}for(var i=0;i{var E=n();return Vt(E)?E:E==null?[]:rt(E)}),c,d=new Map,v=!0;function p(E){(T.effect.f&H)===0&&(T.pending.delete(E),T.fallback=u,ci(T,c,f,t,r),u!==null&&(c.length===0?(u.f&K)===0?nt(u):(u.f^=K,Fe(u,null,f)):ve(u,()=>{u=null})))}function _(E){T.pending.delete(E)}var g=St(()=>{c=F(h);for(var E=c.length,y=new Set,R=b,Z=_n(),L=0;Ls(f)):(u=z(()=>s(Ht??=ee())),u.f|=K)),E>y.size&&Wn(),!v)if(d.set(R,y),Z){for(const[In,Ln]of o)y.has(In)||R.skip_effect(Ln.e);R.oncommit(p),R.ondiscard(_)}else p(R);F(h)}),T={effect:g,items:o,pending:d,outrogroups:null,fallback:u};v=!1}function Ie(e){for(;e!==null&&(e.f&U)===0;)e=e.next;return e}function ci(e,t,n,r,i){var s=(r&lr)!==0,f=t.length,o=e.items,l=Ie(e.effect.first),a,u=null,h,c=[],d=[],v,p,_,g;if(s)for(g=0;g0){var we=(r&Kt)!==0&&f===0?n:null;if(s){for(g=0;g{if(h!==void 0)for(_ of h)_.nodes?.a?.apply()})}function hi(e,t,n,r,i,s,f,o){var l=(f&sr)!==0?(f&or)===0?Hr(n,!1,!1):ge(n):null,a=(f&fr)!==0?ge(i):null;return{v:l,i:a,e:z(()=>(s(t,l??n,a??i,o),()=>{e.delete(r)}))}}function Fe(e,t,n){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,s=t&&(t.f&K)===0?t.nodes.start:n;r!==null;){var f=qe(r);if(s.before(r),r===i)return;r=f}}function se(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}const jt=[...` +\r\f \v\uFEFF`];function di(e,t,n){var r=e==null?"":""+e;if(t&&(r=r?r+" "+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+" "+i:i;else if(r.length)for(var s=i.length,f=0;(f=r.indexOf(i,f))>=0;){var o=f+s;(f===0||jt.includes(r[f-1]))&&(o===r.length||jt.includes(r[o]))?r=(f===0?"":r.substring(0,f))+r.substring(o+1):f=o}}return r===""?null:r}function zt(e,t=!1){var n=t?" !important;":";",r="";for(var i of Object.keys(e)){var s=e[i];s!=null&&s!==""&&(r+=" "+i+": "+s+n)}return r}function vi(e,t){if(t){var n="",r,i;return Array.isArray(t)?(r=t[0],i=t[1]):r=t,r&&(n+=zt(r)),i&&(n+=zt(i,!0)),n=n.trim(),n===""?null:n}return String(e)}function es(e,t,n,r,i,s){var f=e[at];if(f!==n||f===void 0){var o=di(n,r,s);o==null?e.removeAttribute("class"):e.className=o,e[at]=n}else if(s&&i!==s)for(var l in s){var a=!!s[l];(i==null||a!==!!i[l])&&e.classList.toggle(l,a)}return s}function lt(e,t={},n,r){for(var i in n){var s=n[i];t[i]!==s&&(n[i]==null?e.style.removeProperty(i):e.style.setProperty(i,s,r))}}function _i(e,t,n,r){var i=e[ut];if(i!==t){var s=vi(t,r);s==null?e.removeAttribute("style"):e.style.cssText=s,e[ut]=t}else r&&(Array.isArray(r)?(lt(e,n?.[0],r[0]),lt(e,n?.[1],r[1],"important")):lt(e,n,r));return r}const pi=Symbol("is custom element"),gi=Symbol("is html"),wi=$n?"progress":"PROGRESS";function ts(e,t){var n=Mt(e);n.value===(n.value=t??void 0)||e.value===t&&(t!==0||e.nodeName!==wi)||(e.value=t??"")}function ns(e,t){var n=Mt(e);n.checked!==(n.checked=t??void 0)&&(e.checked=t)}function mi(e,t,n,r){var i=Mt(e);i[t]!==(i[t]=n)&&(t==="loading"&&(e[Yn]=n),n==null?e.removeAttribute(t):typeof n!="string"&&bi(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function Mt(e){return e[$t]??={[pi]:e.nodeName.includes("-"),[gi]:e.namespaceURI===_r}}var Bt=new Map;function bi(e){var t=e.getAttribute("is")||e.nodeName,n=Bt.get(t);if(n)return n;Bt.set(t,n=[]);for(var r,i=e,s=Element.prototype;s!==i;){r=Hn(i);for(var f in r)r[f].set&&f!=="innerHTML"&&f!=="textContent"&&f!=="innerText"&&n.push(f);i=qt(i)}return n}function yi(e,t,n,r){var i=!0,s=(n&cr)!==0,f=(n&hr)!==0,o=r,l=!0,a=void 0,u=()=>f&&i?(a??=ze(r),F(a)):(l&&(l=!1,o=f?Cn(r):r),o);let h;if(s){var c=Te in e||Gn in e;h=Ee(e,t)?.set??(c&&t in e?y=>e[t]=y:void 0)}var d,v=!1;s?[d,v]=Tr(()=>e[t]):d=e[t],d===void 0&&r!==void 0&&(d=u(),h&&(er(),h(d)));var p;if(p=()=>{var y=e[t];return y===void 0?u():(l=!0,y)},(n&ur)===0)return p;if(h){var _=e.$$legacy;return(function(y,R){return arguments.length>0?((!R||_||v)&&h(R?p():y),y):p()})}var g=!1,T=((n&ar)!==0?ze:rn)(()=>(g=!1,p()));s&&F(T);var E=m;return(function(y,R){if(arguments.length>0){const Z=R?F(T):s?ye(y):y;return fe(T,Z),g=!0,o!==void 0&&(o=Z),y}return ne&&g||(E.f&H)!==0?T.v:F(T)})}const Ei="5";typeof window<"u"&&((window.__svelte??={}).v??=new Set).add(Ei);var Si=fi('');function rs(e,t){const n=yi(t,"size",3,"1.5rem"),r={search:"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14M16.5 16.5 21 21",tare:"M8 4h8M9 4v3.2a3 3 0 0 1-.5 1.7l-.8 1.2a4 4 0 0 0-.7 2.2V18a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-5.7a4 4 0 0 0-.7-2.2l-.8-1.2a3 3 0 0 1-.5-1.7V4M7 14h10",printer:"M7 9V4h10v5M7 20h10v-6H7zM7 17H5a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2",alert:"M12 3.5 21.5 20h-19zM12 10v4M12 17.2v.1",backspace:"M9 5h11a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-7-7zM17 9l-6 6M11 9l6 6",check:"M4 12.5 9.5 18 20 7",close:"M6 6l12 12M18 6 6 18",settings:"M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-1.8-.3 1.6 1.6 0 0 0-1 1.5v.2a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.5 1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0 .3-1.8 1.6 1.6 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.5-1 1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.8.3H9a1.6 1.6 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.5 1.6 1.6 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8V9a1.6 1.6 0 0 0 1.5 1h.2a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1"};var i=Si();let s;var f=Br(i);Wr(()=>{s=_i(i,"",s,{width:n(),height:n()}),mi(f,"d",r[t.name])}),li(e,i)}async function is(){const e=await fetch("/api/v1/catalog",{headers:{accept:"application/json"}});if(!e.ok)throw new Error(`GET /api/v1/catalog: ${e.status}`);return await e.json()}async function ss(e){await kt("/api/v1/weigh",e)}async function fs(e,t){await kt("/api/v1/reprint",{job_id:e,key:t})}async function ls(){await kt("/api/v1/dismiss",{})}function os(e,t){fetch("/api/v1/ui/error",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({message:e,stack:t}),keepalive:!0}).catch(()=>{})}async function kt(e,t){const n=await fetch(e,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(t)});if(!n.ok)throw new Error(`POST ${e}: ${n.status}`)}const xn={Œ:"oe",œ:"oe",Æ:"ae",æ:"ae",ß:"ss",Ø:"o",ø:"o"},Ti=new RegExp(`[${Object.keys(xn).join("")}]`,"gu"),Ai=/\p{Mn}/u,Mi=/[\p{L}\p{Nd}]/u;function ki(e){const n=e.replace(Ti,s=>xn[s]).normalize("NFD");let r="",i=!1;for(const s of n)if(!Ai.test(s)){if(Mi.test(s)){i&&r.length>0&&(r+=" "),i=!1,r+=s.toLowerCase();continue}i=!0}return r}const Oi=5,Nn="",Ci="by_unit";function Pi(e){const t=new Set(e.categories.filter(r=>!r.visible).map(r=>r.code)),n=!e.presentation.show_by_unit_products;return t.size===0&&!n?e.products:e.products.filter(r=>!t.has(r.category_code)&&!(n&&r.mode===Ci))}function as(e){const t=Pi(e),n=new Map;for(const i of t)n.set(i.category_code,(n.get(i.category_code)??0)+1);const r=e.categories.filter(i=>i.visible&&(n.get(i.code)??0)>=Oi).slice().sort((i,s)=>i.rank-s.rank||i.code.localeCompare(s.code)).map(i=>({code:i.code,label:i.label,color:i.color,count:n.get(i.code)??0}));return[{code:Nn,label:"Tout",color:"var(--ink-muted)",count:t.length},...r]}function us(e,t,n){const r=ki(n),i=r.length===0?[]:r.split(" ");return e.filter(s=>t!==Nn&&s.category_code!==t?!1:i.every(f=>s.search.includes(f)))}export{Bi as $,ie as A,ji as B,Zi as C,Ct as D,ts as E,ye as F,is as G,Nn as H,rs as I,ss as J,us as K,as as L,fs as M,Pi as N,ls as O,Wi as P,os as Q,St as R,Te as S,Me as T,ai as U,Ii as V,b as W,Vt as X,xi as Y,Di as Z,pn as _,m as a,Ni as a0,ns as a1,$i as a2,Bn as a3,Yi as a4,Xi as a5,yi as b,N as c,qi as d,zi as e,Vi as f,li as g,yr as h,Ji as i,Gi as j,F as k,mi as l,_i as m,es as n,Ki as o,br as p,Li as q,Kr as r,Hi as s,Wr as t,Cn as u,Br as v,Fi as w,Qi as x,Ui as y,fe as z}; diff --git a/internal/web/dist/assets/index-87_aneK-.js b/internal/web/dist/assets/index-87_aneK-.js deleted file mode 100644 index 56c4f70..0000000 --- a/internal/web/dist/assets/index-87_aneK-.js +++ /dev/null @@ -1,2 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/mount-BjqIypvA.js","assets/app-Dn-BjSsJ.js","assets/app-DWjtdCrW.css","assets/mount-C1tpIbdA.css"])))=>i.map(i=>d[i]); -import{e as Te,u as Ne,c as Re,r as Oe,a as We,D as Be,S as He,d as le,p as ne,b as T,i as B,I as ie,t as N,f as H,g as m,h as ae,j as y,k as t,s as p,l as oe,m as ce,n as te,o as z,q as h,v as u,w as ze,x as ve,y as Fe,z as x,A as D,B as ue,C as Ue,E as Ge,F as Me,G as Ve,H as Xe,J as Ke,K as Ze,L as Je,M as Ye,N as Qe,O as $e,P as et,Q as tt}from"./app-Dn-BjSsJ.js";class be{#e=new WeakMap;#t;#n;static entries=new WeakMap;constructor(e){this.#n=e}observe(e,a){var r=this.#e.get(e)||new Set;return r.add(a),this.#e.set(e,r),this.#r().observe(e,this.#n),()=>{var i=this.#e.get(e);i.delete(a),i.size===0&&(this.#e.delete(e),this.#t.unobserve(e))}}#r(){return this.#t??(this.#t=new ResizeObserver(e=>{for(var a of e){be.entries.set(a.target,a);for(var r of this.#e.get(a.target)||[])r(a)}}))}}var nt=new be({box:"border-box"});function we(n,e,a){var r=nt.observe(n,()=>a(n[e]));Te(()=>(Ne(()=>a(n[e])),r))}function he(n,e){return n===e||n?.[He]===e}function Ce(n={},e,a,r){var i=Re.r,l=We;return Te(()=>{var d,s;return Oe(()=>{d=s,s=[],Ne(()=>{he(a(...s),n)||(e(n,...s),d&&he(a(...d),n)&&e(null,...d))})}),()=>{let f=l;for(;f!==i&&f.parent!==null&&f.parent.f&Be;)f=f.parent;const v=()=>{s&&he(a(...s),n)&&e(null,...s)},c=f.teardown;f.teardown=()=>{v(),c?.()}}}),n}const at="modulepreload",rt=function(n){return"/"+n},Ee={},st=function(e,a,r){let i=Promise.resolve();if(a&&a.length>0){let f=function(v){return Promise.all(v.map(c=>Promise.resolve(c).then(E=>({status:"fulfilled",value:E}),E=>({status:"rejected",reason:E}))))};document.getElementsByTagName("link");const d=document.querySelector("meta[property=csp-nonce]"),s=d?.nonce||d?.getAttribute("nonce");i=f(a.map(v=>{if(v=rt(v),v in Ee)return;Ee[v]=!0;const c=v.endsWith(".css"),E=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${v}"]${E}`))return;const A=document.createElement("link");if(A.rel=c?"stylesheet":at,c||(A.as="script"),A.crossOrigin="",A.href=v,s&&A.setAttribute("nonce",s),document.head.appendChild(A),c)return new Promise((w,C)=>{A.addEventListener("load",w),A.addEventListener("error",()=>C(new Error(`Unable to preload CSS for ${v}`)))})}))}function l(d){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=d,window.dispatchEvent(s),!s.defaultPrevented)throw d}return i.then(d=>{for(const s of d||[])s.status==="rejected"&&l(s.reason);return e().catch(l)})};var it=y('

kg

',1),ot=y('

—kg

Poids indisponible

',1),lt=y('');function ct(n,e){ne(e,!0);const a=T(e,"taring",3,!1),r=h(()=>{if(e.linkBanner!=="")return e.linkBanner;const I=e.snapshot?.message??null;if(I!==null)return I.text;switch(e.snapshot?.state){case"product_armed":return"Posez votre produit";case"awaiting_stability":return"Pesée en cours…";case"printing":return"Étiquette en cours…";case"entering_tare":return"Tapez la tare en grammes";case"scale_lost":return"Le poids n’est plus disponible.";default:return"Touchez votre produit, l’étiquette sort"}}),i=h(()=>e.snapshot===null?"var(--waiting)":e.snapshot.state==="faulted"||e.snapshot.state==="scale_lost"?"var(--fault)":e.snapshot.state==="rejected"?"var(--warning)":e.snapshot.state==="succeeded"||e.snapshot.weight.latched?"var(--ready)":"var(--waiting)"),l=h(()=>e.showWeight&&e.snapshot!==null&&e.snapshot.weight.available&&!e.snapshot.weight.expired),d=h(()=>{const I=e.snapshot?.message;return I!=null?I.level:e.snapshot?.state==="faulted"||e.snapshot?.state==="scale_lost"?"error":e.snapshot?.state==="rejected"?"warn":"info"});var s=lt(),f=u(s);let v;var c=u(f);{var E=I=>{var W=it(),F=ze(W),K=u(F),G=p(F,2),Q=u(G);N(()=>{z(K,e.snapshot.weight.net_text),z(Q,`tare ${e.snapshot.weight.tare_g??""} g`)}),m(I,W)},A=I=>{var W=ot();m(I,W)};B(c,I=>{t(l)&&e.snapshot?I(E):I(A,-1)})}var w=p(f,2);let C;var g=u(w),k=p(w,2);let O;var q=u(k);ie(q,{name:"tare",size:"1.4em"});var X=p(k,2);let U;N(()=>{oe(s,"data-state",e.snapshot?.state??"initializing"),v=ce(f,"",v,{"border-left-color":t(i)}),C=te(w,1,"instruction svelte-4gizvm",null,C,{warn:t(d)==="warn",error:t(d)==="error"}),z(g,t(r)),O=te(k,1,"tare-key touch-target svelte-4gizvm",null,O,{active:a()}),U=ce(X,"",U,{background:t(i)})}),H("click",k,function(...I){e.ontare?.apply(this,I)}),m(n,s),ae()}le(["click"]);const ut=4.5,dt=.1,vt={r:138,g:134,b:124};function qe(n){const e=/^#([\da-f])([\da-f])([\da-f])$/iu.exec(n);if(e!==null){const[,d,s,f]=e;return{r:Number.parseInt(d+d,16),g:Number.parseInt(s+s,16),b:Number.parseInt(f+f,16)}}const a=/^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/iu.exec(n);if(a===null)return vt;const[,r,i,l]=a;return{r:Number.parseInt(r,16),g:Number.parseInt(i,16),b:Number.parseInt(l,16)}}function pe(n){const e=n/255;return e<=.03928?e/12.92:((e+.055)/1.055)**2.4}function ft({r:n,g:e,b:a}){return .2126*pe(n)+.7152*pe(e)+.0722*pe(a)}function gt(n){return 1.05/(ft(n)+.05)}function je(n){const{r:e,g:a,b:r}=qe(n),i=l=>Math.round(255-dt*(255-l));return`rgb(${i(e)}, ${i(a)}, ${i(r)})`}function De(n){let e=qe(n);for(let a=0;a<16&>(e) '),pt=y('');function _t(n,e){ne(e,!0);var a=pt(),r=u(a);ve(r,21,()=>e.chips,s=>s.code,(s,f)=>{var v=ht();let c,E;var A=p(u(v),2),w=u(A),C=p(A,2),g=u(C);N(k=>{c=te(v,1,"chip touch-target svelte-atgty1",null,c,{active:t(f).code===e.active}),oe(v,"aria-pressed",t(f).code===e.active),E=ce(v,"",E,k),z(w,t(f).label),z(g,t(f).count)},[()=>({"--chip-ink":De(t(f).color),"--chip-wash":je(t(f).color)})]),H("click",v,()=>e.onselect(t(f).code)),m(s,v)});var i=p(r,2);let l;var d=u(i);ie(d,{name:"search",size:"1.75rem"}),N(()=>{l=te(i,1,"chip touch-target search-key svelte-atgty1",null,l,{active:e.searchFieldOpen}),oe(i,"aria-pressed",e.searchFieldOpen)}),H("click",i,function(...s){e.onopensearch?.apply(this,s)}),m(n,a),ae()}le(["click"]);var bt=y(''),mt=y('

'),yt=y('

');function ke(n,e){ne(e,!0);const a=T(e,"action",3,null);var r=yt(),i=u(r),l=u(i),d=u(l);ie(d,{name:"alert",size:"3rem"});var s=p(l,2),f=u(s),v=p(s,2),c=u(v),E=p(v,2);{var A=g=>{var k=bt(),O=u(k);N(()=>z(O,a().label)),H("click",k,function(...q){a().run?.apply(this,q)}),m(g,k)};B(E,g=>{a()!==null&&g(A)})}var w=p(E,2);{var C=g=>{var k=mt(),O=u(k);N(()=>z(O,e.code)),m(g,k)};B(w,g=>{e.code!==""&&g(C)})}N(()=>{z(f,e.title),z(c,e.detail)}),m(n,r),ae()}le(["click"]);const fe=34,xe=18,wt=.5,Et=.005,kt=1.15,Le=90,me=100;function xt(n,e){return Math.max(1,Math.floor(e/(n*kt)))}function At(n,e,a,r=Le){const i=St(n,a);if(i.length===0||e<=0)return fe;const l=Pt(a),d=e*(1-Et);for(let s=fe;s>xe;s-=wt){const f=s/me;if(Tt(i,l,f,d)<=xt(s,r))return s}return xe}const It=/(?<=[-‐–—])(?!\d)/u;function St(n,e){const a=[];for(const r of n.split(/\s+/u).filter(i=>i.length>0)){const i=r.split(It).filter(l=>l.length>0);for(const[l,d]of i.entries())a.push({width:e(d),length:[...d].length,spaced:l===0})}return a}function Pt(n){return Math.max(0,n("a a")-n("aa"))}function Tt(n,e,a,r){let i=1,l=0;for(const d of n){const s=d.width*a,f=d.spaced?e*a:0;if(l>0&&l+f+s<=r){l+=f+s;continue}if(l>0&&(i++,l=0),s<=r){l=s;continue}i+=Nt(s,d.length,r)-1,l=r}return i}function Nt(n,e,a){const r=Math.max(1,Math.floor(a*e/n));return Math.ceil(e/r)}function zt(n,e){const r=document.createElement("canvas").getContext("2d");if(r===null||typeof r.measureText!="function")return null;r.font=`${e} ${me}px ${n}`;const i=new Map;return l=>{const d=i.get(l);if(d!==void 0)return d;const s=r.measureText(l).width;return i.set(l,s),s}}var Mt=y(''),Ct=y(''),qt=y(' '),jt=y(''),Dt=y('');function Lt(n,e){ne(e,!0);const a=T(e,"nameSizePx",3,fe),r=T(e,"categoryColor",3,"#8a867c"),i=T(e,"primaryCode",3,""),l=T(e,"tierAbbrev",19,()=>({})),d=T(e,"selected",3,!1),s=T(e,"rejected",3,!1),f=T(e,"busy",3,!1),v=T(e,"showPrice",3,!0);let c=D("");const E=h(()=>e.product.image_url!==""&&e.product.image_url!==t(c)),A=h(()=>g(e.product.name)),w=h(()=>je(r())),C=h(()=>De(r()));function g(S){const M=S.match(/\p{L}/u);return M===null?"·":M[0].toUpperCase()}var k=Dt();let O;var q=u(k);let X;var U=u(q);{var I=S=>{var M=Mt();N(()=>oe(M,"src",e.product.image_url)),Fe("error",M,()=>x(c,e.product.image_url,!0)),m(S,M)},W=S=>{var M=Ct();let Z;var J=u(M);N(()=>{Z=ce(M,"",Z,{color:t(C)}),z(J,t(A))}),m(S,M)};B(U,S=>{t(E)?S(I):S(W,-1)})}var F=p(q,2),K=u(F);let G;var Q=u(K),_=p(F,2);{var P=S=>{var M=jt();ve(M,21,()=>e.product.prices,Z=>Z.code,(Z,J)=>{var b=qt();let L;var j=u(b);let R;var ee=u(j),Y=p(j,2),$=u(Y),re=p(Y,2),se=u(re);N(de=>{L=te(b,1,"price svelte-1ctu6qg",null,L,{secondary:t(J).code!==i()}),R=te(j,1,"abbrev svelte-1ctu6qg",null,R,{hollow:t(J).code!==i()}),z(ee,l()[t(J).code]??""),z($,t(J).text),z(se,de)},[()=>e.product.price_suffix.trim()]),m(Z,b)}),m(S,M)};B(_,S=>{v()&&S(P)})}N(()=>{O=te(k,1,"tile touch-target svelte-1ctu6qg",null,O,{selected:d(),rejected:s()}),oe(k,"data-product-id",e.product.id),k.disabled=f(),X=ce(q,"",X,{background:t(w)}),G=ce(K,"",G,{"font-size":`${a()??""}px`}),z(Q,e.product.name)}),H("pointerdown",k,()=>e.onpick(e.product)),m(n,k),ae()}le(["pointerdown"]);var Rt=y('
'),Ot=y('

Chargement du catalogue…

',1),Wt=y('

'),Bt=y('

'),Ht=y('
'),Ft=y('
'),Ut=y('
');function Gt(n,e){ne(e,!0);const a=T(e,"colors",19,()=>({})),r=T(e,"primaryCode",3,""),i=T(e,"tierAbbrev",19,()=>({})),l=T(e,"selectedID",3,null),d=T(e,"rejectedID",3,null),s=T(e,"busyID",3,null),f=T(e,"showPrices",3,!0),v=T(e,"loading",3,!1),c=T(e,"emptyMessage",3,"Aucun produit ne correspond."),E=T(e,"emptyHint",3,""),A=230,w=8,C=26,g="'Inter Variable', system-ui, sans-serif",k="'Inter Variable'",O=16;let q=D(0),X=D(!1);ue(()=>{const b=document.fonts;b!==void 0&&b.load(`700 ${me}px ${k}`).catch(()=>[]).then(()=>{x(X,!0)})});const U=h(()=>(t(X),zt(g,700)));let I=D(null),W=D(0),F=D(0);const K=h(()=>{if(t(q)<=0)return 0;const b=Math.max(1,Math.floor((t(q)+w)/(A+w)));return(t(q)-(b-1)*w)/b-C}),G=h(()=>t(W)>0?t(W):t(K));ue(()=>{t(q),e.products.length;const b=t(I)?.querySelector(".name-box");x(W,b?.clientWidth??0,!0)});const Q=h(()=>{const b=new Map;if(t(U)===null||t(G)<=0)return b;const L=t(F)>0?t(F):Le;for(const j of e.products)b.set(j.id,At(j.name,t(G),t(U),L));return b});var _=Ut(),P=u(_),S=p(P,2);{var M=b=>{var L=Ot(),j=ze(L);ve(j,21,()=>({length:O}),Ue,(R,ee)=>{var Y=Rt();m(R,Y)}),m(b,L)},Z=b=>{var L=Bt(),j=u(L),R=u(j),ee=p(j,2);{var Y=$=>{var re=Wt(),se=u(re);N(()=>z(se,E())),m($,re)};B(ee,$=>{E()!==""&&$(Y)})}N(()=>z(R,c())),m(b,L)},J=b=>{var L=Ft();ve(L,21,()=>e.products,j=>j.id,(j,R)=>{var ee=Ht(),Y=u(ee);{let $=h(()=>t(Q).get(t(R).id)??fe),re=h(()=>t(R).id===l()),se=h(()=>t(R).id===d()),de=h(()=>t(R).id===s());Lt(Y,{get product(){return t(R)},get nameSizePx(){return t($)},get categoryColor(){return a()[t(R).category_code]},get primaryCode(){return r()},get tierAbbrev(){return i()},get selected(){return t(re)},get rejected(){return t(se)},get busy(){return t(de)},get showPrice(){return f()},get onpick(){return e.onpick}})}m(j,ee)}),N(()=>oe(L,"data-tile-count",e.products.length)),m(b,L)};B(S,b=>{v()?b(M):e.products.length===0?b(Z,1):b(J,-1)})}Ce(_,b=>x(I,b),()=>t(I)),we(P,"clientHeight",b=>x(F,b)),we(_,"clientWidth",b=>x(q,b)),m(n,_),ae()}var Vt=y(''),Xt=y('
');function Kt(n,e){ne(e,!0);let a=D(null);ue(()=>{t(a)?.focus()});var r=Xt(),i=u(r);ie(i,{name:"search",size:"1.75rem"});var l=p(i,2);Ce(l,c=>x(a,c),()=>t(a));var d=p(l,2);{var s=c=>{var E=Vt();H("click",E,function(...A){e.onclose?.apply(this,A)}),m(c,E)};B(d,c=>{e.query!==""&&c(s)})}var f=p(d,2),v=u(f);ie(v,{name:"close",size:"1.5rem"}),N(()=>Ge(l,e.query)),H("input",l,c=>e.onquery(c.currentTarget.value)),H("keydown",l,c=>{c.key==="Escape"?(c.preventDefault(),e.onclose()):c.key==="Enter"&&(c.preventDefault(),e.onenter())}),H("click",f,function(...c){e.onclose?.apply(this,c)}),m(n,r),ae()}le(["input","keydown","click"]);function Zt(n){if(n==="")return"";const e=new Date(n);if(Number.isNaN(e.getTime()))return"";const a=i=>String(i).padStart(2,"0");return`${`${a(e.getDate())}/${a(e.getMonth()+1)}/${e.getFullYear()}`} ${a(e.getHours())}:${a(e.getMinutes())}:${a(e.getSeconds())}`}function Jt(n){return n.mode==="by_unit"?`${n.product_name} ${n.quantity} ${n.quantity>1?"unités":"unité"}`:`${n.product_name} ${n.net_text} kg`}var Yt=y('Catalogue du '),Qt=y('Catalogue en attente'),$t=y(' '),en=y('aucune pour le moment'),tn=y(' '),nn=y(''),an=y('
Dernière étiquette
');function rn(n,e){ne(e,!0);const a=T(e,"catalogAt",3,""),r=T(e,"productCount",3,0),i=T(e,"appVersion",3,""),l=h(()=>[r()>0?`${r()} produits pesables`:"",i()!==""?`application ${i()}`:""].filter(_=>_!=="").join(" · "));var d=an();let s;var f=u(d),v=u(f);{var c=_=>{var P=Yt(),S=p(u(P)),M=u(S);N(()=>z(M,a())),m(_,P)},E=_=>{var P=Qt();m(_,P)};B(v,_=>{a()!==""?_(c):_(E,-1)})}var A=p(v,2);{var w=_=>{var P=$t(),S=u(P);N(()=>z(S,t(l))),m(_,P)};B(A,_=>{t(l)!==""&&_(w)})}var C=p(f,4),g=p(u(C),2);{var k=_=>{var P=en();m(_,P)},O=_=>{var P=tn(),S=u(P);N(M=>z(S,M),[()=>Jt(e.label)]),m(_,P)};B(g,_=>{e.label===null?_(k):_(O,-1)})}var q=p(C,2);{var X=_=>{var P=nn();N(()=>P.disabled=!e.available),H("click",P,function(...S){e.onreprint?.apply(this,S)}),m(_,P)};B(q,_=>{e.label!==null&&_(X)})}var U=p(q,2),I=u(U);let W;var F=p(I,2),K=u(F),G=p(U,2),Q=u(G);ie(Q,{name:"settings",size:"1.75rem"}),N(()=>{s=te(d,1,"bar svelte-161y12f",null,s,{live:e.label!==null}),W=te(I,1,"dot svelte-161y12f",null,W,{fault:!e.healthy}),oe(I,"aria-label",e.healthy?"Matériel disponible":"Matériel indisponible"),z(K,e.healthy?"Balance et imprimante disponibles":"Matériel indisponible")}),H("click",G,function(..._){e.onadmin?.apply(this,_)}),m(n,d),ae()}le(["click"]);var sn=y(''),on=y('

Tare g

');function ln(n,e){ne(e,!0);const a=["1","2","3","4","5","6","7","8","9","0"];var r=on(),i=u(r),l=p(u(i),2),d=u(l),s=p(i,2);ve(s,20,()=>a,w=>w,(w,C)=>{var g=sn(),k=u(g);N(()=>z(k,C)),H("click",g,()=>e.ondigit(C)),m(w,g)});var f=p(s,2),v=u(f),c=p(v,2),E=p(c,2),A=u(E);ie(A,{name:"check",size:"1.5rem"}),N(()=>z(d,e.grams===""?"0":e.grams)),H("click",v,function(...w){e.onclear?.apply(this,w)}),H("click",c,function(...w){e.oncancel?.apply(this,w)}),H("click",E,function(...w){e.onconfirm?.apply(this,w)}),m(n,r),ae()}le(["click"]);function cn(n){return n===null||n.state==="initializing"?!1:n.scale.connected&&n.printer.health!=="faulted"}const Ae=1500,un=2e3,Ie=5e3;function dn(n,e){return e?n>=Ie?{showWeight:!1,banner:"Poids indisponible",mustReconnect:!0}:n>=un?{showWeight:!1,banner:"Poids indisponible",mustReconnect:!1}:n>=Ae?{showWeight:!1,banner:"",mustReconnect:!1}:{showWeight:!0,banner:"",mustReconnect:!1}:{showWeight:n=Ie}}const vn=250,fn={show_grid_prices:!0,idle_timeout_s:45,reprint_window_s:60,sound:!0};class gn{#e=D(null);get state(){return t(this.#e)}set state(e){x(this.#e,e,!0)}#t=D(null);get catalog(){return t(this.#t)}set catalog(e){x(this.#t,e,!0)}#n=D(Me({showWeight:!0,banner:"",mustReconnect:!1}));get link(){return t(this.#n)}set link(e){x(this.#n,e,!0)}#r=D("");get catalogError(){return t(this.#r)}set catalogError(e){x(this.#r,e,!0)}#s=null;#i=null;#o=Date.now();#a=!1;#l=!1;get presentation(){return this.catalog?.presentation??fn}start(){this.#u(),this.#c(),this.#i=setInterval(()=>this.#v(),vn)}stop(){this.#i!==null&&clearInterval(this.#i),this.#i=null,this.#s?.close(),this.#s=null,this.#a=!1}#c(){const e=new EventSource("/api/v1/stream");this.#s=e,e.onopen=()=>{this.#a=!0,this.#o=Date.now()},e.addEventListener("state",a=>this.#d(a)),e.onerror=()=>{this.#a=!1}}#d(e){this.#o=Date.now(),this.#a=!0;const a=JSON.parse(e.data);this.state=a,this.catalog!==null&&a.catalog_count!==this.catalog.product_count&&this.#u()}#v(){const e=dn(Date.now()-this.#o,this.#a);this.link=e,e.mustReconnect&&(this.#s?.close(),this.#o=Date.now(),this.#c())}async#u(){if(!this.#l){this.#l=!0;try{this.catalog=await Ve(),this.catalogError=""}catch{this.catalog===null&&(this.catalogError="Catalogue indisponible. Prévenez un responsable.")}finally{this.#l=!1}}}}const Se="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function Pe(){let n=Date.now();const e=new Array(26);for(let r=9;r>=0;r--)e[r]=Se[n%32],n=Math.floor(n/32);const a=new Uint8Array(16);crypto.getRandomValues(a);for(let r=0;r<16;r++)e[10+r]=Se[a[r]%32];return e.join("")}var hn=y('
');function pn(n,e){ne(e,!0);const a=new gn;let r=D(Me(Xe)),i=D(""),l=D(!1);const d=h(()=>t(i)!==""||t(l));let s=D(null),f=D(-1),v=D(null);const c=h(()=>a.catalog),E=h(()=>t(c)===null?[]:Qe(t(c))),A=h(()=>t(c)===null?[]:Je(t(c))),w=h(()=>Ze(t(E),t(r),t(i))),C=h(()=>{const o={};for(const V of t(c)?.categories??[])o[V.code]=V.color;return o}),g=h(()=>a.state),k=h(()=>a.presentation),O=h(()=>cn(t(g))),q=h(()=>{if(a.catalogError!=="")return{message:a.catalogError,hint:"Le poste réessaie tout seul."};if(t(E).length>0)return{message:"Aucun produit ne correspond.",hint:"Effacez des lettres ou changez de rayon."};const o=t(g)?.station??0;return{message:o>0?`Catalogue vide. En attente du fichier flv_${o}.csv.`:"Catalogue vide. En attente du fichier du poste.",hint:"Prévenez un responsable : aucun produit ne peut être pesé."}}),X=h(()=>t(g)?.state==="rejected"?t(g).product?.id??null:null),U=h(()=>t(g)?.state==="faulted"||t(g)?.state==="out_of_service"),I=["product_armed","weight_present","weight_stable","awaiting_stability","validating","printing"],W=h(()=>t(g)!==null&&I.includes(t(g).state)?t(g).product?.id??null:null),F=h(()=>Zt(t(c)?.updated_at??""));ue(()=>(a.start(),()=>a.stop())),ue(()=>{const o=a.state?.revision??-1;t(s)!==null&&o>t(f)&&(x(s,null),x(f,-1))});async function K(o){if(t(s)===null){x(s,o.id,!0),x(f,a.state?.revision??-1,!0);try{await Ke({product_id:o.id,tare_g:a.state?.weight.tare_g??0,units:1,manual_weight_g:0,seen_weight_g:a.state?.weight.gross_g??0,measurement_seq:a.state?.weight.seq??0,key:Pe()})}catch{x(s,null)}}}async function G(){const o=a.state?.reprint.job_id;o===void 0||o===""||await Ye(o,Pe())}function Q(){x(i,""),x(l,!1)}function _(){x(l,!0)}function P(){const o=t(w).length===1?t(w)[0]:void 0;o!==void 0&&K(o)}function S(o){if(!(o.metaKey||o.ctrlKey||o.altKey)&&!(o.target instanceof HTMLElement&&o.target.tagName==="INPUT")&&!(t(v)!==null||t(U))){if(o.key==="Escape"){o.preventDefault(),Q();return}if(o.key==="Backspace"){o.preventDefault(),x(i,t(i).slice(0,-1),!0);return}if(o.key==="Enter"){P();return}o.key.length===1&&/[a-zA-Z0-9 ]/.test(o.key)&&(o.key===" "&&t(i)===""||(o.preventDefault(),x(i,t(i)+o.key)))}}ue(()=>(window.addEventListener("keydown",S),()=>window.removeEventListener("keydown",S)));async function M(){(await st(()=>import("./mount-BjqIypvA.js"),__vite__mapDeps([0,1,2,3]))).mountAdmin(document.body)}var Z=hn(),J=u(Z);{let o=h(()=>t(v)!==null);ct(J,{get snapshot(){return t(g)},get showWeight(){return a.link.showWeight},get linkBanner(){return a.link.banner},get taring(){return t(o)},ontare:()=>x(v,"")})}var b=p(J,2);{var L=o=>{ln(o,{get grams(){return t(v)},ondigit:V=>x(v,(t(v)??"")+V),onclear:()=>x(v,""),oncancel:()=>x(v,null),onconfirm:()=>x(v,null)})};B(b,o=>{t(v)!==null&&o(L)})}var j=p(b,2);{let o=h(()=>t(c)?.pricing.primary_code??""),V=h(()=>Object.fromEntries((t(c)?.pricing.tiers??[]).map(ye=>[ye.code,ye.abbrev]))),ge=h(()=>t(c)===null&&a.catalogError==="");Gt(j,{get products(){return t(w)},get colors(){return t(C)},get primaryCode(){return t(o)},get tierAbbrev(){return t(V)},get selectedID(){return t(W)},get rejectedID(){return t(X)},get busyID(){return t(s)},get showPrices(){return t(k).show_grid_prices},get loading(){return t(ge)},get emptyMessage(){return t(q).message},get emptyHint(){return t(q).hint},onpick:K})}var R=p(j,2);{var ee=o=>{Kt(o,{get query(){return t(i)},onquery:V=>x(i,V,!0),onclose:Q,onenter:P})};B(R,o=>{t(d)&&o(ee)})}var Y=p(R,2);_t(Y,{get chips(){return t(A)},get active(){return t(r)},get searchFieldOpen(){return t(d)},onselect:o=>x(r,o,!0),onopensearch:_});var $=p(Y,2);{let o=h(()=>t(g)?.last_label??null),V=h(()=>t(g)?.reprint.available??!1),ge=h(()=>t(c)?.app_version??"");rn($,{get label(){return t(o)},get available(){return t(V)},get catalogAt(){return t(F)},get productCount(){return t(E).length},get appVersion(){return t(ge)},get healthy(){return t(O)},onreprint:G,onadmin:M})}var re=p($,2);{var se=o=>{{let V=h(()=>t(g).message?.text??"Prévenez un responsable.");ke(o,{title:"Poste indisponible",get detail(){return t(V)},get code(){return t(g).fault_code},action:{label:"J’ai compris",run:()=>{$e()}}})}},de=o=>{ke(o,{title:"Poste hors service",detail:"Ce poste ne peut pas peser. Prévenez un responsable.",get code(){return t(g).fault_code}})};B(re,o=>{t(g)?.state==="faulted"?o(se):t(g)?.state==="out_of_service"&&o(de,1)})}m(n,Z),ae()}const _n=5;mn();document.addEventListener("contextmenu",n=>{bn(n.target)||n.preventDefault()});document.addEventListener("dragstart",n=>n.preventDefault());et(pn,{target:document.getElementById("app")});function bn(n){return n instanceof Element&&n.closest("[data-admin]")!==null}function mn(){let n=!1;const e=(a,r)=>{n||(n=!0,tt(a,r),yn(),setTimeout(()=>location.reload(),_n*1e3))};window.addEventListener("error",a=>e(a.message,a.error?.stack??"")),window.addEventListener("unhandledrejection",a=>e(String(a.reason),a.reason?.stack??""))}function yn(){const n=document.createElement("div");n.className="fatal",n.append(_e("h1","Une erreur est survenue"),_e("p","L’écran va se recharger tout seul."),_e("p","ERR-UI-01","code")),document.body.appendChild(n)}function _e(n,e,a=""){const r=document.createElement(n);return r.textContent=e,a!==""&&(r.className=a),r} diff --git a/internal/web/dist/assets/index-BNck1guc.js b/internal/web/dist/assets/index-BNck1guc.js new file mode 100644 index 0000000..9c4a7ca --- /dev/null +++ b/internal/web/dist/assets/index-BNck1guc.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/mount-l2eO5M4C.js","assets/app-Bm_67CUJ.js","assets/app-DWjtdCrW.css","assets/mount-CTNNPuRa.css"])))=>i.map(i=>d[i]); +import{e as Te,u as qe,c as Re,r as Oe,a as We,D as Be,S as He,d as le,p as ne,b as T,i as B,I as ie,t as q,f as H,g as m,h as ae,j as y,k as t,s as p,l as oe,m as ce,n as te,o as z,q as h,v as u,w as ze,x as fe,y as Fe,z as x,A as j,B as ue,C as Ue,E as Ge,F as Ne,G as Ve,H as Xe,J as Ke,K as Ze,L as Je,M as Ye,N as Qe,O as $e,P as et,Q as tt}from"./app-Bm_67CUJ.js";class be{#e=new WeakMap;#t;#n;static entries=new WeakMap;constructor(e){this.#n=e}observe(e,a){var r=this.#e.get(e)||new Set;return r.add(a),this.#e.set(e,r),this.#r().observe(e,this.#n),()=>{var i=this.#e.get(e);i.delete(a),i.size===0&&(this.#e.delete(e),this.#t.unobserve(e))}}#r(){return this.#t??(this.#t=new ResizeObserver(e=>{for(var a of e){be.entries.set(a.target,a);for(var r of this.#e.get(a.target)||[])r(a)}}))}}var nt=new be({box:"border-box"});function we(n,e,a){var r=nt.observe(n,()=>a(n[e]));Te(()=>(qe(()=>a(n[e])),r))}function he(n,e){return n===e||n?.[He]===e}function Me(n={},e,a,r){var i=Re.r,c=We;return Te(()=>{var d,s;return Oe(()=>{d=s,s=[],qe(()=>{he(a(...s),n)||(e(n,...s),d&&he(a(...d),n)&&e(null,...d))})}),()=>{let f=c;for(;f!==i&&f.parent!==null&&f.parent.f&Be;)f=f.parent;const v=()=>{s&&he(a(...s),n)&&e(null,...s)},l=f.teardown;f.teardown=()=>{v(),l?.()}}}),n}const at="modulepreload",rt=function(n){return"/"+n},Ee={},st=function(e,a,r){let i=Promise.resolve();if(a&&a.length>0){let f=function(v){return Promise.all(v.map(l=>Promise.resolve(l).then(E=>({status:"fulfilled",value:E}),E=>({status:"rejected",reason:E}))))};document.getElementsByTagName("link");const d=document.querySelector("meta[property=csp-nonce]"),s=d?.nonce||d?.getAttribute("nonce");i=f(a.map(v=>{if(v=rt(v),v in Ee)return;Ee[v]=!0;const l=v.endsWith(".css"),E=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${v}"]${E}`))return;const A=document.createElement("link");if(A.rel=l?"stylesheet":at,l||(A.as="script"),A.crossOrigin="",A.href=v,s&&A.setAttribute("nonce",s),document.head.appendChild(A),l)return new Promise((w,M)=>{A.addEventListener("load",w),A.addEventListener("error",()=>M(new Error(`Unable to preload CSS for ${v}`)))})}))}function c(d){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=d,window.dispatchEvent(s),!s.defaultPrevented)throw d}return i.then(d=>{for(const s of d||[])s.status==="rejected"&&c(s.reason);return e().catch(c)})};var it=y('

kg

',1),ot=y('

—kg

Poids indisponible

',1),lt=y('');function ct(n,e){ne(e,!0);const a=T(e,"taring",3,!1),r=h(()=>{if(e.linkBanner!=="")return e.linkBanner;const I=e.snapshot?.message??null;if(I!==null)return I.text;switch(e.snapshot?.state){case"product_armed":return"Posez votre produit";case"awaiting_stability":return"Pesée en cours…";case"printing":return"Étiquette en cours…";case"entering_tare":return"Tapez la tare en grammes";case"scale_lost":return"Le poids n’est plus disponible.";default:return"Touchez votre produit, l’étiquette sort"}}),i=h(()=>e.snapshot===null?"var(--waiting)":e.snapshot.state==="faulted"||e.snapshot.state==="scale_lost"?"var(--fault)":e.snapshot.state==="rejected"?"var(--warning)":e.snapshot.state==="succeeded"||e.snapshot.weight.latched?"var(--ready)":"var(--waiting)"),c=h(()=>e.showWeight&&e.snapshot!==null&&e.snapshot.weight.available&&!e.snapshot.weight.expired),d=h(()=>{const I=e.snapshot?.message;return I!=null?I.level:e.snapshot?.state==="faulted"||e.snapshot?.state==="scale_lost"?"error":e.snapshot?.state==="rejected"?"warn":"info"});var s=lt(),f=u(s);let v;var l=u(f);{var E=I=>{var W=it(),F=ze(W),K=u(F),V=p(F,2),Q=u(V);q(()=>{z(K,e.snapshot.weight.net_text),z(Q,`tare ${e.snapshot.weight.tare_g??""} g`)}),m(I,W)},A=I=>{var W=ot();m(I,W)};B(l,I=>{t(c)&&e.snapshot?I(E):I(A,-1)})}var w=p(f,2);let M;var g=u(w),k=p(w,2);let O;var C=u(k);ie(C,{name:"tare",size:"1.4em"});var X=p(k,2);let G;q(()=>{oe(s,"data-state",e.snapshot?.state??"initializing"),v=ce(f,"",v,{"border-left-color":t(i)}),M=te(w,1,"instruction svelte-4gizvm",null,M,{warn:t(d)==="warn",error:t(d)==="error"}),z(g,t(r)),O=te(k,1,"tare-key touch-target svelte-4gizvm",null,O,{active:a()}),G=ce(X,"",G,{background:t(i)})}),H("click",k,function(...I){e.ontare?.apply(this,I)}),m(n,s),ae()}le(["click"]);const ut=4.5,dt=.1,vt={r:138,g:134,b:124};function Ce(n){const e=/^#([\da-f])([\da-f])([\da-f])$/iu.exec(n);if(e!==null){const[,d,s,f]=e;return{r:Number.parseInt(d+d,16),g:Number.parseInt(s+s,16),b:Number.parseInt(f+f,16)}}const a=/^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/iu.exec(n);if(a===null)return vt;const[,r,i,c]=a;return{r:Number.parseInt(r,16),g:Number.parseInt(i,16),b:Number.parseInt(c,16)}}function pe(n){const e=n/255;return e<=.03928?e/12.92:((e+.055)/1.055)**2.4}function ft({r:n,g:e,b:a}){return .2126*pe(n)+.7152*pe(e)+.0722*pe(a)}function gt(n){return 1.05/(ft(n)+.05)}function Le(n){const{r:e,g:a,b:r}=Ce(n),i=c=>Math.round(255-dt*(255-c));return`rgb(${i(e)}, ${i(a)}, ${i(r)})`}function je(n){let e=Ce(n);for(let a=0;a<16&>(e) '),pt=y('');function _t(n,e){ne(e,!0);var a=pt(),r=u(a);fe(r,21,()=>e.chips,s=>s.code,(s,f)=>{var v=ht();let l,E;var A=p(u(v),2),w=u(A),M=p(A,2),g=u(M);q(k=>{l=te(v,1,"chip touch-target svelte-atgty1",null,l,{active:t(f).code===e.active}),oe(v,"aria-pressed",t(f).code===e.active),E=ce(v,"",E,k),z(w,t(f).label),z(g,t(f).count)},[()=>({"--chip-ink":je(t(f).color),"--chip-wash":Le(t(f).color)})]),H("click",v,()=>e.onselect(t(f).code)),m(s,v)});var i=p(r,2);let c;var d=u(i);ie(d,{name:"search",size:"1.75rem"}),q(()=>{c=te(i,1,"chip touch-target search-key svelte-atgty1",null,c,{active:e.searchFieldOpen}),oe(i,"aria-pressed",e.searchFieldOpen)}),H("click",i,function(...s){e.onopensearch?.apply(this,s)}),m(n,a),ae()}le(["click"]);var bt=y(''),mt=y('

'),yt=y('

');function ke(n,e){ne(e,!0);const a=T(e,"action",3,null);var r=yt(),i=u(r),c=u(i),d=u(c);ie(d,{name:"alert",size:"3rem"});var s=p(c,2),f=u(s),v=p(s,2),l=u(v),E=p(v,2);{var A=g=>{var k=bt(),O=u(k);q(()=>z(O,a().label)),H("click",k,function(...C){a().run?.apply(this,C)}),m(g,k)};B(E,g=>{a()!==null&&g(A)})}var w=p(E,2);{var M=g=>{var k=mt(),O=u(k);q(()=>z(O,e.code)),m(g,k)};B(w,g=>{e.code!==""&&g(M)})}q(()=>{z(f,e.title),z(l,e.detail)}),m(n,r),ae()}le(["click"]);const ge=34,xe=18,wt=.5,Et=.005,kt=1.15,De=90,me=100;function xt(n,e){return Math.max(1,Math.floor(e/(n*kt)))}function At(n,e,a,r=De){const i=St(n,a);if(i.length===0||e<=0)return ge;const c=Pt(a),d=e*(1-Et);for(let s=ge;s>xe;s-=wt){const f=s/me;if(Tt(i,c,f,d)<=xt(s,r))return s}return xe}const It=/(?<=[-‐–—])(?!\d)/u;function St(n,e){const a=[];for(const r of n.split(/\s+/u).filter(i=>i.length>0)){const i=r.split(It).filter(c=>c.length>0);for(const[c,d]of i.entries())a.push({width:e(d),length:[...d].length,spaced:c===0})}return a}function Pt(n){return Math.max(0,n("a a")-n("aa"))}function Tt(n,e,a,r){let i=1,c=0;for(const d of n){const s=d.width*a,f=d.spaced?e*a:0;if(c>0&&c+f+s<=r){c+=f+s;continue}if(c>0&&(i++,c=0),s<=r){c=s;continue}i+=qt(s,d.length,r)-1,c=r}return i}function qt(n,e,a){const r=Math.max(1,Math.floor(a*e/n));return Math.ceil(e/r)}function zt(n,e){const r=document.createElement("canvas").getContext("2d");if(r===null||typeof r.measureText!="function")return null;r.font=`${e} ${me}px ${n}`;const i=new Map;return c=>{const d=i.get(c);if(d!==void 0)return d;const s=r.measureText(c).width;return i.set(c,s),s}}var Nt=y(''),Mt=y(''),Ct=y(' '),Lt=y(''),jt=y('');function Dt(n,e){ne(e,!0);const a=T(e,"nameSizePx",3,ge),r=T(e,"categoryColor",3,"#8a867c"),i=T(e,"primaryCode",3,""),c=T(e,"tierAbbrev",19,()=>({})),d=T(e,"selected",3,!1),s=T(e,"rejected",3,!1),f=T(e,"busy",3,!1),v=T(e,"showPrice",3,!0);let l=j("");const E=h(()=>e.product.image_url!==""&&e.product.image_url!==t(l)),A=h(()=>g(e.product.name)),w=h(()=>Le(r())),M=h(()=>je(r()));function g(S){const N=S.match(/\p{L}/u);return N===null?"·":N[0].toUpperCase()}var k=jt();let O;var C=u(k);let X;var G=u(C);{var I=S=>{var N=Nt();q(()=>oe(N,"src",e.product.image_url)),Fe("error",N,()=>x(l,e.product.image_url,!0)),m(S,N)},W=S=>{var N=Mt();let Z;var J=u(N);q(()=>{Z=ce(N,"",Z,{color:t(M)}),z(J,t(A))}),m(S,N)};B(G,S=>{t(E)?S(I):S(W,-1)})}var F=p(C,2),K=u(F);let V;var Q=u(K),_=p(F,2);{var P=S=>{var N=Lt();fe(N,21,()=>e.product.prices,Z=>Z.code,(Z,J)=>{var b=Ct();let D;var L=u(b);let R;var ee=u(L),Y=p(L,2),$=u(Y),re=p(Y,2),se=u(re);q(de=>{D=te(b,1,"price svelte-1ctu6qg",null,D,{secondary:t(J).code!==i()}),R=te(L,1,"abbrev svelte-1ctu6qg",null,R,{hollow:t(J).code!==i()}),z(ee,c()[t(J).code]??""),z($,t(J).text),z(se,de)},[()=>e.product.price_suffix.trim()]),m(Z,b)}),m(S,N)};B(_,S=>{v()&&S(P)})}q(()=>{O=te(k,1,"tile touch-target svelte-1ctu6qg",null,O,{selected:d(),rejected:s()}),oe(k,"data-product-id",e.product.id),k.disabled=f(),X=ce(C,"",X,{background:t(w)}),V=ce(K,"",V,{"font-size":`${a()??""}px`}),z(Q,e.product.name)}),H("pointerdown",k,()=>e.onpick(e.product)),m(n,k),ae()}le(["pointerdown"]);var Rt=y('
'),Ot=y('

Chargement du catalogue…

',1),Wt=y('

'),Bt=y('

'),Ht=y('
'),Ft=y('
'),Ut=y('
');function Gt(n,e){ne(e,!0);const a=T(e,"colors",19,()=>({})),r=T(e,"primaryCode",3,""),i=T(e,"tierAbbrev",19,()=>({})),c=T(e,"selectedID",3,null),d=T(e,"rejectedID",3,null),s=T(e,"busyID",3,null),f=T(e,"showPrices",3,!0),v=T(e,"loading",3,!1),l=T(e,"emptyMessage",3,"Aucun produit ne correspond."),E=T(e,"emptyHint",3,""),A=230,w=8,M=26,g="'Inter Variable', system-ui, sans-serif",k="'Inter Variable'",O=16;let C=j(0),X=j(!1);ue(()=>{const b=document.fonts;b!==void 0&&b.load(`700 ${me}px ${k}`).catch(()=>[]).then(()=>{x(X,!0)})});const G=h(()=>(t(X),zt(g,700)));let I=j(null),W=j(0),F=j(0);const K=h(()=>{if(t(C)<=0)return 0;const b=Math.max(1,Math.floor((t(C)+w)/(A+w)));return(t(C)-(b-1)*w)/b-M}),V=h(()=>t(W)>0?t(W):t(K));ue(()=>{t(C),e.products.length;const b=t(I)?.querySelector(".name-box");x(W,b?.clientWidth??0,!0)});const Q=h(()=>{const b=new Map;if(t(G)===null||t(V)<=0)return b;const D=t(F)>0?t(F):De;for(const L of e.products)b.set(L.id,At(L.name,t(V),t(G),D));return b});var _=Ut(),P=u(_),S=p(P,2);{var N=b=>{var D=Ot(),L=ze(D);fe(L,21,()=>({length:O}),Ue,(R,ee)=>{var Y=Rt();m(R,Y)}),m(b,D)},Z=b=>{var D=Bt(),L=u(D),R=u(L),ee=p(L,2);{var Y=$=>{var re=Wt(),se=u(re);q(()=>z(se,E())),m($,re)};B(ee,$=>{E()!==""&&$(Y)})}q(()=>z(R,l())),m(b,D)},J=b=>{var D=Ft();fe(D,21,()=>e.products,L=>L.id,(L,R)=>{var ee=Ht(),Y=u(ee);{let $=h(()=>t(Q).get(t(R).id)??ge),re=h(()=>t(R).id===c()),se=h(()=>t(R).id===d()),de=h(()=>t(R).id===s());Dt(Y,{get product(){return t(R)},get nameSizePx(){return t($)},get categoryColor(){return a()[t(R).category_code]},get primaryCode(){return r()},get tierAbbrev(){return i()},get selected(){return t(re)},get rejected(){return t(se)},get busy(){return t(de)},get showPrice(){return f()},get onpick(){return e.onpick}})}m(L,ee)}),q(()=>oe(D,"data-tile-count",e.products.length)),m(b,D)};B(S,b=>{v()?b(N):e.products.length===0?b(Z,1):b(J,-1)})}Me(_,b=>x(I,b),()=>t(I)),we(P,"clientHeight",b=>x(F,b)),we(_,"clientWidth",b=>x(C,b)),m(n,_),ae()}var Vt=y(''),Xt=y('
');function Kt(n,e){ne(e,!0);let a=j(null);ue(()=>{t(a)?.focus()});var r=Xt(),i=u(r);ie(i,{name:"search",size:"1.75rem"});var c=p(i,2);Me(c,l=>x(a,l),()=>t(a));var d=p(c,2);{var s=l=>{var E=Vt();H("click",E,function(...A){e.onclose?.apply(this,A)}),m(l,E)};B(d,l=>{e.query!==""&&l(s)})}var f=p(d,2),v=u(f);ie(v,{name:"close",size:"1.5rem"}),q(()=>Ge(c,e.query)),H("input",c,l=>e.onquery(l.currentTarget.value)),H("keydown",c,l=>{l.key==="Escape"?(l.preventDefault(),e.onclose()):l.key==="Enter"&&(l.preventDefault(),e.onenter())}),H("click",f,function(...l){e.onclose?.apply(this,l)}),m(n,r),ae()}le(["input","keydown","click"]);function Zt(n){if(n==="")return"";const e=new Date(n);if(Number.isNaN(e.getTime()))return"";const a=i=>String(i).padStart(2,"0");return`${`${a(e.getDate())}/${a(e.getMonth()+1)}/${e.getFullYear()}`} ${a(e.getHours())}:${a(e.getMinutes())}:${a(e.getSeconds())}`}function Jt(n){return n.mode==="by_unit"?`${n.product_name} ${n.quantity} ${n.quantity>1?"unités":"unité"}`:`${n.product_name} ${n.net_text} kg`}var Yt=y('Catalogue du '),Qt=y('Catalogue en attente'),$t=y(' '),en=y('aucune pour le moment'),tn=y(' '),nn=y(''),an=y('
Dernière étiquette
');function rn(n,e){ne(e,!0);const a=T(e,"catalogAt",3,""),r=T(e,"productCount",3,0),i=T(e,"appVersion",3,""),c=h(()=>[r()>0?`${r()} produits pesables`:"",i()!==""?`application ${i()}`:""].filter(_=>_!=="").join(" · "));var d=an();let s;var f=u(d),v=u(f);{var l=_=>{var P=Yt(),S=p(u(P)),N=u(S);q(()=>z(N,a())),m(_,P)},E=_=>{var P=Qt();m(_,P)};B(v,_=>{a()!==""?_(l):_(E,-1)})}var A=p(v,2);{var w=_=>{var P=$t(),S=u(P);q(()=>z(S,t(c))),m(_,P)};B(A,_=>{t(c)!==""&&_(w)})}var M=p(f,4),g=p(u(M),2);{var k=_=>{var P=en();m(_,P)},O=_=>{var P=tn(),S=u(P);q(N=>z(S,N),[()=>Jt(e.label)]),m(_,P)};B(g,_=>{e.label===null?_(k):_(O,-1)})}var C=p(M,2);{var X=_=>{var P=nn();q(()=>P.disabled=!e.available),H("click",P,function(...S){e.onreprint?.apply(this,S)}),m(_,P)};B(C,_=>{e.label!==null&&_(X)})}var G=p(C,2),I=u(G);let W;var F=p(I,2),K=u(F),V=p(G,2),Q=u(V);ie(Q,{name:"settings",size:"1.75rem"}),q(()=>{s=te(d,1,"bar svelte-161y12f",null,s,{live:e.label!==null}),W=te(I,1,"dot svelte-161y12f",null,W,{fault:!e.healthy}),oe(I,"aria-label",e.healthy?"Matériel disponible":"Matériel indisponible"),z(K,e.healthy?"Balance et imprimante disponibles":"Matériel indisponible")}),H("click",V,function(..._){e.onadmin?.apply(this,_)}),m(n,d),ae()}le(["click"]);var sn=y(''),on=y('

Tare g

');function ln(n,e){ne(e,!0);const a=["1","2","3","4","5","6","7","8","9","0"];var r=on(),i=u(r),c=p(u(i),2),d=u(c),s=p(i,2);fe(s,20,()=>a,w=>w,(w,M)=>{var g=sn(),k=u(g);q(()=>z(k,M)),H("click",g,()=>e.ondigit(M)),m(w,g)});var f=p(s,2),v=u(f),l=p(v,2),E=p(l,2),A=u(E);ie(A,{name:"check",size:"1.5rem"}),q(()=>z(d,e.grams===""?"0":e.grams)),H("click",v,function(...w){e.onclear?.apply(this,w)}),H("click",l,function(...w){e.oncancel?.apply(this,w)}),H("click",E,function(...w){e.onconfirm?.apply(this,w)}),m(n,r),ae()}le(["click"]);function cn(n){return n===null||n.state==="initializing"?!1:n.scale.connected&&n.printer.health!=="faulted"}const Ae=1500,un=2e3,Ie=5e3;function dn(n,e){return e?n>=Ie?{showWeight:!1,banner:"Poids indisponible",mustReconnect:!0}:n>=un?{showWeight:!1,banner:"Poids indisponible",mustReconnect:!1}:n>=Ae?{showWeight:!1,banner:"",mustReconnect:!1}:{showWeight:!0,banner:"",mustReconnect:!1}:{showWeight:n=Ie}}const vn=250,fn={show_grid_prices:!0,idle_timeout_s:45,reprint_window_s:60,sound:!0,show_by_unit_products:!1};class gn{#e=j(null);get state(){return t(this.#e)}set state(e){x(this.#e,e,!0)}#t=j(null);get catalog(){return t(this.#t)}set catalog(e){x(this.#t,e,!0)}#n=j(Ne({showWeight:!0,banner:"",mustReconnect:!1}));get link(){return t(this.#n)}set link(e){x(this.#n,e,!0)}#r=j("");get catalogError(){return t(this.#r)}set catalogError(e){x(this.#r,e,!0)}#s=null;#i=null;#o=Date.now();#a=!1;#l=!1;get presentation(){return this.catalog?.presentation??fn}start(){this.#u(),this.#c(),this.#i=setInterval(()=>this.#v(),vn)}stop(){this.#i!==null&&clearInterval(this.#i),this.#i=null,this.#s?.close(),this.#s=null,this.#a=!1}#c(){const e=new EventSource("/api/v1/stream");this.#s=e,e.onopen=()=>{this.#a=!0,this.#o=Date.now()},e.addEventListener("state",a=>this.#d(a)),e.onerror=()=>{this.#a=!1}}#d(e){this.#o=Date.now(),this.#a=!0;const a=JSON.parse(e.data);this.state=a,this.catalog!==null&&a.catalog_count!==this.catalog.product_count&&this.#u()}#v(){const e=dn(Date.now()-this.#o,this.#a);this.link=e,e.mustReconnect&&(this.#s?.close(),this.#o=Date.now(),this.#c())}async#u(){if(!this.#l){this.#l=!0;try{this.catalog=await Ve(),this.catalogError=""}catch{this.catalog===null&&(this.catalogError="Catalogue indisponible. Prévenez un responsable.")}finally{this.#l=!1}}}}const Se="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function Pe(){let n=Date.now();const e=new Array(26);for(let r=9;r>=0;r--)e[r]=Se[n%32],n=Math.floor(n/32);const a=new Uint8Array(16);crypto.getRandomValues(a);for(let r=0;r<16;r++)e[10+r]=Se[a[r]%32];return e.join("")}var hn=y('
');function pn(n,e){ne(e,!0);const a=new gn;let r=j(Ne(Xe)),i=j(""),c=j(!1);const d=h(()=>t(i)!==""||t(c));let s=j(null),f=j(-1),v=j(null);const l=h(()=>a.catalog),E=h(()=>t(l)===null?[]:Qe(t(l))),A=h(()=>t(l)===null?[]:Je(t(l))),w=h(()=>Ze(t(E),t(r),t(i))),M=h(()=>{const o={};for(const U of t(l)?.categories??[])o[U.code]=U.color;return o}),g=h(()=>a.state),k=h(()=>a.presentation),O=h(()=>cn(t(g))),C=h(()=>{if(a.catalogError!=="")return{message:a.catalogError,hint:"Le poste réessaie tout seul."};if(t(E).length>0)return{message:"Aucun produit ne correspond.",hint:"Effacez des lettres ou changez de rayon."};if(t(l)!==null&&t(l).products.length>0)return{message:t(l).products.every(ve=>ve.mode==="by_unit")&&!t(k).show_by_unit_products?"Aucun produit à montrer : ce poste masque les produits vendus à l’unité, et le catalogue reçu ne contient que ceux-là.":"Aucun produit à montrer : les réglages d’affichage de ce poste masquent tout ce qu’il a reçu.",hint:"Le fichier est bien arrivé : prévenez un responsable, c’est un réglage de ce poste qui vide la grille."};const o=t(g)?.station??0;return{message:o>0?`Catalogue vide. En attente du fichier flv_${o}.csv.`:"Catalogue vide. En attente du fichier du poste.",hint:"Prévenez un responsable : aucun produit ne peut être pesé."}}),X=h(()=>t(g)?.state==="rejected"?t(g).product?.id??null:null),G=h(()=>t(g)?.state==="faulted"||t(g)?.state==="out_of_service"),I=["product_armed","weight_present","weight_stable","awaiting_stability","validating","printing"],W=h(()=>t(g)!==null&&I.includes(t(g).state)?t(g).product?.id??null:null),F=h(()=>Zt(t(l)?.updated_at??""));ue(()=>(a.start(),()=>a.stop())),ue(()=>{const o=a.state?.revision??-1;t(s)!==null&&o>t(f)&&(x(s,null),x(f,-1))});async function K(o){if(t(s)===null){x(s,o.id,!0),x(f,a.state?.revision??-1,!0);try{await Ke({product_id:o.id,tare_g:a.state?.weight.tare_g??0,units:1,manual_weight_g:0,seen_weight_g:a.state?.weight.gross_g??0,measurement_seq:a.state?.weight.seq??0,key:Pe()})}catch{x(s,null)}}}async function V(){const o=a.state?.reprint.job_id;o===void 0||o===""||await Ye(o,Pe())}function Q(){x(i,""),x(c,!1)}function _(){x(c,!0)}function P(){const o=t(w).length===1?t(w)[0]:void 0;o!==void 0&&K(o)}function S(o){if(!(o.metaKey||o.ctrlKey||o.altKey)&&!(o.target instanceof HTMLElement&&o.target.tagName==="INPUT")&&!(t(v)!==null||t(G))){if(o.key==="Escape"){o.preventDefault(),Q();return}if(o.key==="Backspace"){o.preventDefault(),x(i,t(i).slice(0,-1),!0);return}if(o.key==="Enter"){P();return}o.key.length===1&&/[a-zA-Z0-9 ]/.test(o.key)&&(o.key===" "&&t(i)===""||(o.preventDefault(),x(i,t(i)+o.key)))}}ue(()=>(window.addEventListener("keydown",S),()=>window.removeEventListener("keydown",S)));async function N(){(await st(()=>import("./mount-l2eO5M4C.js"),__vite__mapDeps([0,1,2,3]))).mountAdmin(document.body)}var Z=hn(),J=u(Z);{let o=h(()=>t(v)!==null);ct(J,{get snapshot(){return t(g)},get showWeight(){return a.link.showWeight},get linkBanner(){return a.link.banner},get taring(){return t(o)},ontare:()=>x(v,"")})}var b=p(J,2);{var D=o=>{ln(o,{get grams(){return t(v)},ondigit:U=>x(v,(t(v)??"")+U),onclear:()=>x(v,""),oncancel:()=>x(v,null),onconfirm:()=>x(v,null)})};B(b,o=>{t(v)!==null&&o(D)})}var L=p(b,2);{let o=h(()=>t(l)?.pricing.primary_code??""),U=h(()=>Object.fromEntries((t(l)?.pricing.tiers??[]).map(ye=>[ye.code,ye.abbrev]))),ve=h(()=>t(l)===null&&a.catalogError==="");Gt(L,{get products(){return t(w)},get colors(){return t(M)},get primaryCode(){return t(o)},get tierAbbrev(){return t(U)},get selectedID(){return t(W)},get rejectedID(){return t(X)},get busyID(){return t(s)},get showPrices(){return t(k).show_grid_prices},get loading(){return t(ve)},get emptyMessage(){return t(C).message},get emptyHint(){return t(C).hint},onpick:K})}var R=p(L,2);{var ee=o=>{Kt(o,{get query(){return t(i)},onquery:U=>x(i,U,!0),onclose:Q,onenter:P})};B(R,o=>{t(d)&&o(ee)})}var Y=p(R,2);_t(Y,{get chips(){return t(A)},get active(){return t(r)},get searchFieldOpen(){return t(d)},onselect:o=>x(r,o,!0),onopensearch:_});var $=p(Y,2);{let o=h(()=>t(g)?.last_label??null),U=h(()=>t(g)?.reprint.available??!1),ve=h(()=>t(l)?.app_version??"");rn($,{get label(){return t(o)},get available(){return t(U)},get catalogAt(){return t(F)},get productCount(){return t(E).length},get appVersion(){return t(ve)},get healthy(){return t(O)},onreprint:V,onadmin:N})}var re=p($,2);{var se=o=>{{let U=h(()=>t(g).message?.text??"Prévenez un responsable.");ke(o,{title:"Poste indisponible",get detail(){return t(U)},get code(){return t(g).fault_code},action:{label:"J’ai compris",run:()=>{$e()}}})}},de=o=>{ke(o,{title:"Poste hors service",detail:"Ce poste ne peut pas peser. Prévenez un responsable.",get code(){return t(g).fault_code}})};B(re,o=>{t(g)?.state==="faulted"?o(se):t(g)?.state==="out_of_service"&&o(de,1)})}m(n,Z),ae()}const _n=5;mn();document.addEventListener("contextmenu",n=>{bn(n.target)||n.preventDefault()});document.addEventListener("dragstart",n=>n.preventDefault());et(pn,{target:document.getElementById("app")});function bn(n){return n instanceof Element&&n.closest("[data-admin]")!==null}function mn(){let n=!1;const e=(a,r)=>{n||(n=!0,tt(a,r),yn(),setTimeout(()=>location.reload(),_n*1e3))};window.addEventListener("error",a=>e(a.message,a.error?.stack??"")),window.addEventListener("unhandledrejection",a=>e(String(a.reason),a.reason?.stack??""))}function yn(){const n=document.createElement("div");n.className="fatal",n.append(_e("h1","Une erreur est survenue"),_e("p","L’écran va se recharger tout seul."),_e("p","ERR-UI-01","code")),document.body.appendChild(n)}function _e(n,e,a=""){const r=document.createElement(n);return r.textContent=e,a!==""&&(r.className=a),r} diff --git a/internal/web/dist/assets/index-DURuCM7m.css b/internal/web/dist/assets/index-BZkG83Q9.css similarity index 85% rename from internal/web/dist/assets/index-DURuCM7m.css rename to internal/web/dist/assets/index-BZkG83Q9.css index f5fd7ad..ec13735 100644 --- a/internal/web/dist/assets/index-DURuCM7m.css +++ b/internal/web/dist/assets/index-BZkG83Q9.css @@ -1 +1 @@ -.banner.svelte-4gizvm{position:relative;display:flex;flex:0 0 auto;align-items:center;gap:2rem;height:var(--banner-height);padding:0 1.75rem;background:var(--surface);border-bottom:1px solid var(--border);box-shadow:var(--shadow-1);z-index:2}.weight-block.svelte-4gizvm{flex:0 0 auto;min-width:20rem;padding:.5rem 2rem .5rem 1.5rem;background:var(--bg);border-radius:var(--radius-lg);border-left:.5rem solid var(--waiting);transition:border-color var(--slide) var(--ease)}.weight.svelte-4gizvm{margin:0;font-size:clamp(6rem,5.5vw,6.75rem);font-weight:700;line-height:1;letter-spacing:-.02em}.weight.unavailable.svelte-4gizvm{color:var(--ink-muted);font-weight:400}.unit.svelte-4gizvm{font-size:2.25rem;font-weight:500;letter-spacing:0}.tare.svelte-4gizvm{margin:.5rem 0 0;color:var(--ink-muted);font-size:1.125rem;letter-spacing:.02em}.instruction.svelte-4gizvm{flex:1 1 auto;margin:0;padding-left:1.25rem;border-left:.375rem solid transparent;border-radius:.1875rem;font-size:1.75rem;line-height:1.25;text-wrap:balance;transition:border-color var(--slide) var(--ease)}.instruction.warn.svelte-4gizvm{border-left-color:var(--warning);font-weight:700}.instruction.error.svelte-4gizvm{border-left-color:var(--fault);font-weight:700}.tare-key.svelte-4gizvm{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.375rem;flex:0 0 auto;min-width:clamp(7rem,9vw,11rem);padding:0 1.5rem;border:2px solid var(--border);border-radius:var(--radius-lg);background:var(--bg);font-size:clamp(1.5rem,1.5vw,2.25rem);font-weight:700;letter-spacing:.08em;transition:border-color var(--slide) var(--ease),background-color var(--slide) var(--ease),transform var(--tap) var(--ease)}.tare-key.active.svelte-4gizvm{border-color:var(--ready);background:var(--ready-wash)}.ribbon.svelte-4gizvm{position:absolute;inset:auto 0 0;height:.375rem;transition:background-color var(--slide) var(--ease)}.filters.svelte-atgty1{display:flex;flex:0 0 auto;align-items:center;gap:.75rem;height:var(--category-height);padding:0 var(--touch-gap);background:var(--surface);border-top:1px solid var(--border);z-index:2}.chips.svelte-atgty1{display:flex;align-items:center;gap:var(--touch-gap);flex:1 1 auto;overflow-x:auto;overscroll-behavior:contain;scrollbar-width:none}.chip.svelte-atgty1{display:flex;align-items:center;gap:.625rem;padding:0 1.375rem;border:2px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);font-size:1.75rem;white-space:nowrap;transition:border-color var(--slide) var(--ease),background-color var(--slide) var(--ease),transform var(--tap) var(--ease)}.dot.svelte-atgty1{width:.75rem;height:.75rem;border-radius:50%;background:var(--chip-ink, var(--ink-muted))}.chip.active.svelte-atgty1{border-color:var(--chip-ink, var(--ink));background:var(--chip-wash, var(--bg));font-weight:700}.chip-count.svelte-atgty1{color:var(--ink-muted);font-size:1.25rem;font-weight:400}.search-key.svelte-atgty1{flex:0 0 auto;justify-content:center;padding:0;width:var(--touch-min);border-radius:var(--radius)}.search-key.active.svelte-atgty1{border-color:var(--ink);background:var(--bg)}.visually-hidden.svelte-atgty1{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.screen.svelte-2n2xuj{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;padding:3rem;background:var(--bg)}.card.svelte-2n2xuj{display:flex;flex-direction:column;align-items:center;gap:1.5rem;max-width:54rem;padding:3.5rem 4rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-2);text-align:center}.glyph.svelte-2n2xuj{display:flex;align-items:center;justify-content:center;width:6rem;height:6rem;border-radius:50%;background:var(--fault-wash);box-shadow:0 0 0 2px var(--fault) inset;color:var(--ink)}h1.svelte-2n2xuj{margin:0;font-size:3.5rem;line-height:1.05;letter-spacing:-.02em}.detail.svelte-2n2xuj{margin:0;max-width:40rem;font-size:1.75rem;line-height:1.35;text-wrap:balance}.action.svelte-2n2xuj{margin-top:.5rem;padding:0 2.5rem;border:2px solid var(--ink);border-radius:var(--radius);font-size:1.75rem;font-weight:700}.code.svelte-2n2xuj{margin:0;padding-top:.5rem;font-size:1.125rem;letter-spacing:.12em;color:var(--ink-muted)}.tile.svelte-1ctu6qg{display:flex;flex-direction:column;align-items:stretch;gap:.5rem;width:100%;height:100%;padding:var(--tile-pad);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);text-align:left;transition:box-shadow var(--slide) var(--ease),border-color var(--slide) var(--ease),background-color var(--slide) var(--ease),transform var(--tap) var(--ease)}@media(hover:hover){.tile.svelte-1ctu6qg:hover:not(:disabled){border-color:var(--ink-muted);box-shadow:var(--shadow-2);transform:translateY(-1px)}}.tile.svelte-1ctu6qg:disabled{border-color:var(--waiting);box-shadow:0 0 0 3px var(--waiting) inset,var(--shadow-1);transform:scale(.98);cursor:default}.tile.selected.svelte-1ctu6qg{border-color:var(--ready);background:var(--ready-wash);box-shadow:0 0 0 3px var(--ready) inset,var(--shadow-2)}.tile.rejected.svelte-1ctu6qg{border-color:var(--warning);background:var(--warning-wash);box-shadow:0 0 0 3px var(--warning) inset,var(--shadow-2)}.plate.svelte-1ctu6qg{display:flex;align-items:center;justify-content:center;flex:0 0 auto;width:100%;height:var(--tile-media);border-radius:var(--radius-sm);overflow:hidden}.photo.svelte-1ctu6qg{max-width:100%;height:100%;object-fit:contain;mix-blend-mode:multiply}.initial.svelte-1ctu6qg{font-size:2.5rem;font-weight:800;line-height:1}.name-box.svelte-1ctu6qg{display:flex;align-items:center;flex:1 1 auto;min-height:var(--tile-name)}.name.svelte-1ctu6qg{width:100%;min-width:0;font-weight:700;line-height:1.15;font-variant-numeric:normal;overflow-wrap:break-word}.prices.svelte-1ctu6qg{display:flex;flex-direction:column;gap:.25rem;margin-top:auto;padding-top:.5rem;border-top:1px solid var(--border)}.price.svelte-1ctu6qg{display:flex;align-items:baseline;gap:.5rem;color:var(--ink);font-size:1.75rem;font-weight:700}.price.secondary.svelte-1ctu6qg{color:var(--ink-muted);font-size:1.25rem;font-weight:400}.abbrev.svelte-1ctu6qg{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:1.5em;height:1.5em;border-radius:var(--radius-inner);background:var(--ink);color:var(--surface);font-size:.8em;font-weight:700;line-height:1}.abbrev.hollow.svelte-1ctu6qg{background:none;color:var(--ink-muted);box-shadow:inset 0 0 0 2px var(--border)}.unit.svelte-1ctu6qg{color:var(--ink-muted);font-size:.7em;font-weight:400}.grid-scroll.svelte-1cebjac{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;padding:var(--touch-gap)}.probe.svelte-1cebjac{position:absolute;width:0;height:var(--tile-name);visibility:hidden;pointer-events:none}.grid.svelte-1cebjac{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile-min),1fr));grid-auto-rows:minmax(var(--tile-height),auto);gap:var(--touch-gap);align-content:start}.state.svelte-1cebjac{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;height:100%;padding:2rem;text-align:center}.empty.svelte-1cebjac{margin:0;max-width:46rem;font-size:2rem;font-weight:600;line-height:1.3;text-wrap:balance}.hint.svelte-1cebjac{margin:0;max-width:40rem;color:var(--ink-muted);font-size:1.5rem;line-height:1.35;text-wrap:balance}.loading.svelte-1cebjac{margin:1.5rem 0 0;color:var(--ink-muted);font-size:1.5rem;text-align:center}.ghost.svelte-1cebjac{display:flex;flex-direction:column;gap:.5rem;height:var(--tile-height);padding:var(--tile-pad);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.ghost-head.svelte-1cebjac{display:flex;align-items:center;gap:.5rem;height:var(--tile-media)}.ghost-plate.svelte-1cebjac{width:var(--tile-media);height:var(--tile-media);border-radius:var(--radius-sm);background:var(--bg)}.ghost-price.svelte-1cebjac{width:6rem;height:1.25rem;margin-left:auto;border-radius:var(--radius-sm);background:var(--bg)}.ghost-line.svelte-1cebjac{height:1.75rem;border-radius:var(--radius-sm);background:var(--bg)}.ghost-line.short.svelte-1cebjac{width:55%}.search-field.svelte-13x9cek{flex:0 0 auto;display:flex;align-items:center;gap:1rem;padding:.5rem 1.0625rem;background:var(--surface);border-bottom:1px solid var(--border)}input.svelte-13x9cek{flex:1 1 auto;min-width:0;height:var(--touch-min);padding:0 1.25rem;background:var(--bg);border:2px solid var(--border);border-radius:var(--radius);font:inherit;font-size:1.75rem;font-variant-numeric:tabular-nums;color:var(--ink)}button.svelte-13x9cek{flex:0 0 auto;padding:0 1.5rem;background:var(--bg);border:2px solid var(--border);border-radius:var(--radius);font-size:1.375rem}button.close.svelte-13x9cek{display:flex;align-items:center;justify-content:center;width:var(--touch-min);padding:0}.bar.svelte-161y12f{display:flex;flex:0 0 auto;align-items:stretch;height:var(--status-height);padding:0 1.0625rem;background:var(--bg);border-top:1px solid var(--border);transition:background-color var(--slide) var(--ease)}.bar.live.svelte-161y12f{background:var(--surface)}.block.svelte-161y12f{display:flex;flex-direction:column;justify-content:center;flex:0 0 auto;min-width:0;gap:.125rem;line-height:1.25;white-space:nowrap}.block.label.svelte-161y12f,.block.health.svelte-161y12f{padding-left:1.75rem;border-left:1px solid var(--border)}.block.label.svelte-161y12f{flex:0 1 auto;padding-right:1.75rem}.block.health.svelte-161y12f{flex-direction:row;align-items:center;gap:.75rem}.spacer.svelte-161y12f{flex:1 1 auto;min-width:1.5rem}.line.svelte-161y12f{font-size:1.125rem;color:var(--ink-muted);overflow:hidden;text-overflow:ellipsis}.line.quiet.svelte-161y12f{color:var(--ink-muted)}.line.svelte-161y12f strong:where(.svelte-161y12f),.line.strong.svelte-161y12f{color:var(--ink);font-weight:600}.caption.svelte-161y12f{font-size:1rem;letter-spacing:var(--tracking-caps, .03em);text-transform:uppercase;color:var(--ink-muted)}.reprint.svelte-161y12f{flex:0 0 auto;align-self:center;margin-right:1.75rem;padding:0 2rem;border:2px solid var(--border);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow-1);font-size:1.375rem;font-weight:600;transition:border-color var(--slide) var(--ease),opacity var(--slide) var(--ease),transform var(--tap) var(--ease)}.reprint.svelte-161y12f:disabled{opacity:.35;box-shadow:none;cursor:default}.dot.svelte-161y12f{flex:0 0 auto;width:var(--dot-size-health, 1.25rem);height:var(--dot-size-health, 1.25rem);border-radius:50%;background:var(--ready);transition:background-color var(--slide) var(--ease)}.dot.fault.svelte-161y12f{background:var(--fault)}.admin.svelte-161y12f{display:flex;align-items:center;justify-content:center;flex:0 0 auto;align-self:center;width:var(--touch-min);height:var(--touch-min);margin-left:1.75rem;border:2px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--ink-muted)}.pad.svelte-wrfqsf{display:flex;flex:0 0 auto;align-items:center;gap:1.25rem;padding:.75rem 1.75rem;background:var(--surface);border-bottom:1px solid var(--border);box-shadow:var(--shadow-1);z-index:1;animation:svelte-wrfqsf-drop var(--slide) var(--ease)}@keyframes svelte-wrfqsf-drop{0%{transform:translateY(-1rem);opacity:0}}.value.svelte-wrfqsf{display:flex;flex-direction:column;flex:0 0 auto;min-width:11rem;margin:0;padding-right:1.5rem;border-right:1px solid var(--border-soft)}.label.svelte-wrfqsf{color:var(--ink-muted);font-size:1rem;letter-spacing:.08em;text-transform:uppercase}.grams.svelte-wrfqsf{font-size:2.5rem;font-weight:700;line-height:1.1}.unit.svelte-wrfqsf{font-size:1.5rem;font-weight:400;color:var(--ink-muted)}.digits.svelte-wrfqsf,.actions.svelte-wrfqsf{display:flex;gap:var(--touch-gap)}.actions.svelte-wrfqsf{margin-left:auto}.key.svelte-wrfqsf{display:flex;align-items:center;justify-content:center;gap:.5rem;width:var(--touch-min);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);box-shadow:var(--shadow-1);font-size:1.75rem;font-weight:600}.key.svelte-wrfqsf:active{background:var(--bg)}.key.action.svelte-wrfqsf{width:auto;padding:0 1.5rem;font-size:1.375rem;font-weight:500}.key.confirm.svelte-wrfqsf{border:2px solid var(--ready);background:var(--ready-wash);font-weight:700}.screen.svelte-1n46o8q{display:flex;flex-direction:column;height:100%;overflow:hidden} +.banner.svelte-4gizvm{position:relative;display:flex;flex:0 0 auto;align-items:center;gap:2rem;height:var(--banner-height);padding:0 1.75rem;background:var(--surface);border-bottom:1px solid var(--border);box-shadow:var(--shadow-1);z-index:2}.weight-block.svelte-4gizvm{flex:0 0 auto;min-width:20rem;padding:.5rem 2rem .5rem 1.5rem;background:var(--bg);border-radius:var(--radius-lg);border-left:.5rem solid var(--waiting);transition:border-color var(--slide) var(--ease)}.weight.svelte-4gizvm{margin:0;font-size:clamp(6rem,5.5vw,6.75rem);font-weight:700;line-height:1;letter-spacing:-.02em}.weight.unavailable.svelte-4gizvm{color:var(--ink-muted);font-weight:400}.unit.svelte-4gizvm{font-size:2.25rem;font-weight:500;letter-spacing:0}.tare.svelte-4gizvm{margin:.5rem 0 0;color:var(--ink-muted);font-size:1.125rem;letter-spacing:.02em}.instruction.svelte-4gizvm{flex:1 1 auto;margin:0;padding-left:1.25rem;border-left:.375rem solid transparent;border-radius:.1875rem;font-size:1.75rem;line-height:1.25;text-wrap:balance;transition:border-color var(--slide) var(--ease)}.instruction.warn.svelte-4gizvm{border-left-color:var(--warning);font-weight:700}.instruction.error.svelte-4gizvm{border-left-color:var(--fault);font-weight:700}.tare-key.svelte-4gizvm{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.375rem;flex:0 0 auto;min-width:clamp(7rem,9vw,11rem);padding:0 1.5rem;border:2px solid var(--border);border-radius:var(--radius-lg);background:var(--bg);font-size:clamp(1.5rem,1.5vw,2.25rem);font-weight:700;letter-spacing:.08em;transition:border-color var(--slide) var(--ease),background-color var(--slide) var(--ease),transform var(--tap) var(--ease)}.tare-key.active.svelte-4gizvm{border-color:var(--ready);background:var(--ready-wash)}.ribbon.svelte-4gizvm{position:absolute;inset:auto 0 0;height:.375rem;transition:background-color var(--slide) var(--ease)}.filters.svelte-atgty1{display:flex;flex:0 0 auto;align-items:center;gap:.75rem;height:var(--category-height);padding:0 var(--touch-gap);background:var(--surface);border-top:1px solid var(--border);z-index:2}.chips.svelte-atgty1{display:flex;align-items:center;gap:var(--touch-gap);flex:1 1 auto;overflow-x:auto;overscroll-behavior:contain;scrollbar-width:none}.chip.svelte-atgty1{display:flex;align-items:center;gap:.625rem;padding:0 1.375rem;border:2px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);font-size:1.75rem;white-space:nowrap;transition:border-color var(--slide) var(--ease),background-color var(--slide) var(--ease),transform var(--tap) var(--ease)}.dot.svelte-atgty1{width:.75rem;height:.75rem;border-radius:50%;background:var(--chip-ink, var(--ink-muted))}.chip.active.svelte-atgty1{border-color:var(--chip-ink, var(--ink));background:var(--chip-wash, var(--bg));font-weight:700}.chip-count.svelte-atgty1{color:var(--ink-muted);font-size:1.25rem;font-weight:400}.search-key.svelte-atgty1{flex:0 0 auto;justify-content:center;padding:0;width:var(--touch-min);border-radius:var(--radius)}.search-key.active.svelte-atgty1{border-color:var(--ink);background:var(--bg)}.visually-hidden.svelte-atgty1{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.screen.svelte-2n2xuj{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;padding:3rem;background:var(--bg)}.card.svelte-2n2xuj{display:flex;flex-direction:column;align-items:center;gap:1.5rem;max-width:54rem;padding:3.5rem 4rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-2);text-align:center}.glyph.svelte-2n2xuj{display:flex;align-items:center;justify-content:center;width:6rem;height:6rem;border-radius:50%;background:var(--fault-wash);box-shadow:0 0 0 2px var(--fault) inset;color:var(--ink)}h1.svelte-2n2xuj{margin:0;font-size:3.5rem;line-height:1.05;letter-spacing:-.02em}.detail.svelte-2n2xuj{margin:0;max-width:40rem;font-size:1.75rem;line-height:1.35;text-wrap:balance}.action.svelte-2n2xuj{margin-top:.5rem;padding:0 2.5rem;border:2px solid var(--ink);border-radius:var(--radius);font-size:1.75rem;font-weight:700}.code.svelte-2n2xuj{margin:0;padding-top:.5rem;font-size:1.125rem;letter-spacing:.12em;color:var(--ink-muted)}.tile.svelte-1ctu6qg{display:flex;flex-direction:column;align-items:stretch;gap:.5rem;width:100%;height:100%;padding:var(--tile-pad);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);text-align:left;transition:box-shadow var(--slide) var(--ease),border-color var(--slide) var(--ease),background-color var(--slide) var(--ease),transform var(--tap) var(--ease)}@media(hover:hover){.tile.svelte-1ctu6qg:hover:not(:disabled){border-color:var(--ink-muted);box-shadow:var(--shadow-2);transform:translateY(-1px)}}.tile.svelte-1ctu6qg:disabled{border-color:var(--waiting);box-shadow:0 0 0 3px var(--waiting) inset,var(--shadow-1);transform:scale(.98);cursor:default}.tile.selected.svelte-1ctu6qg{border-color:var(--ready);background:var(--ready-wash);box-shadow:0 0 0 3px var(--ready) inset,var(--shadow-2)}.tile.rejected.svelte-1ctu6qg{border-color:var(--warning);background:var(--warning-wash);box-shadow:0 0 0 3px var(--warning) inset,var(--shadow-2)}.plate.svelte-1ctu6qg{display:flex;align-items:center;justify-content:center;flex:0 0 auto;width:100%;height:var(--tile-media);border-radius:var(--radius-sm);overflow:hidden}.photo.svelte-1ctu6qg{max-width:100%;height:100%;object-fit:contain;mix-blend-mode:multiply}.initial.svelte-1ctu6qg{font-size:2.5rem;font-weight:800;line-height:1}.name-box.svelte-1ctu6qg{display:flex;align-items:center;flex:1 1 auto;min-height:var(--tile-name)}.name.svelte-1ctu6qg{width:100%;min-width:0;font-weight:700;line-height:1.15;font-variant-numeric:normal;overflow-wrap:break-word}.prices.svelte-1ctu6qg{display:flex;flex-direction:column;gap:.25rem;margin-top:auto;padding-top:.5rem;border-top:1px solid var(--border)}.price.svelte-1ctu6qg{display:flex;align-items:baseline;gap:.5rem;color:var(--ink);font-size:1.75rem;font-weight:700}.price.secondary.svelte-1ctu6qg{color:var(--ink-muted);font-size:1.25rem;font-weight:400}.abbrev.svelte-1ctu6qg{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:1.5em;height:1.5em;border-radius:var(--radius-inner);background:var(--ink);color:var(--surface);font-size:.8em;font-weight:700;line-height:1}.abbrev.hollow.svelte-1ctu6qg{background:none;color:var(--ink-muted);box-shadow:inset 0 0 0 2px var(--border)}.unit.svelte-1ctu6qg{color:var(--ink-muted);font-size:.7em;font-weight:400}.grid-scroll.svelte-1cebjac{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;padding:var(--touch-gap)}.probe.svelte-1cebjac{position:absolute;width:0;height:var(--tile-name);visibility:hidden;pointer-events:none}.grid.svelte-1cebjac{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile-min),1fr));grid-auto-rows:minmax(var(--tile-height),auto);gap:var(--touch-gap);align-content:start}.state.svelte-1cebjac{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;height:100%;padding:2rem;text-align:center}.empty.svelte-1cebjac{margin:0;max-width:46rem;font-size:2rem;font-weight:600;line-height:1.3;text-wrap:balance}.hint.svelte-1cebjac{margin:0;max-width:40rem;color:var(--ink-muted);font-size:1.5rem;line-height:1.35;text-wrap:balance}.loading.svelte-1cebjac{margin:1.5rem 0 0;color:var(--ink-muted);font-size:1.5rem;text-align:center}.ghost.svelte-1cebjac{display:flex;flex-direction:column;gap:.5rem;height:var(--tile-height);padding:var(--tile-pad);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.ghost-head.svelte-1cebjac{display:flex;align-items:center;gap:.5rem;height:var(--tile-media)}.ghost-plate.svelte-1cebjac{width:var(--tile-media);height:var(--tile-media);border-radius:var(--radius-sm);background:var(--bg)}.ghost-price.svelte-1cebjac{width:6rem;height:1.25rem;margin-left:auto;border-radius:var(--radius-sm);background:var(--bg)}.ghost-line.svelte-1cebjac{height:1.75rem;border-radius:var(--radius-sm);background:var(--bg)}.ghost-line.short.svelte-1cebjac{width:55%}.search-field.svelte-13x9cek{flex:0 0 auto;display:flex;align-items:center;gap:1rem;padding:.5rem 1.0625rem;background:var(--surface);border-bottom:1px solid var(--border)}input.svelte-13x9cek{flex:1 1 auto;min-width:0;height:var(--touch-min);padding:0 1.25rem;background:var(--bg);border:2px solid var(--border);border-radius:var(--radius);font:inherit;font-size:1.75rem;font-variant-numeric:tabular-nums;color:var(--ink)}button.svelte-13x9cek{flex:0 0 auto;padding:0 1.5rem;background:var(--bg);border:2px solid var(--border);border-radius:var(--radius);font-size:1.375rem}button.close.svelte-13x9cek{display:flex;align-items:center;justify-content:center;width:var(--touch-min);padding:0}.bar.svelte-161y12f{display:flex;flex:0 0 auto;align-items:stretch;height:var(--status-height);padding:0 1.0625rem;background:var(--bg);border-top:1px solid var(--border);transition:background-color var(--slide) var(--ease)}.bar.live.svelte-161y12f{background:var(--surface)}.block.svelte-161y12f{display:flex;flex-direction:column;justify-content:center;flex:0 0 auto;min-width:0;gap:.125rem;line-height:1.25;white-space:nowrap}.block.label.svelte-161y12f,.block.health.svelte-161y12f{padding-left:1.75rem;border-left:1px solid var(--border)}.block.label.svelte-161y12f{flex:0 1 auto;padding-right:1.75rem}.block.health.svelte-161y12f{flex-direction:row;align-items:center;gap:.75rem}.spacer.svelte-161y12f{flex:1 1 auto;min-width:1.5rem}.line.svelte-161y12f{font-size:1.125rem;color:var(--ink-muted);overflow:hidden;text-overflow:ellipsis}.line.quiet.svelte-161y12f{color:var(--ink-muted)}.line.svelte-161y12f strong:where(.svelte-161y12f),.line.strong.svelte-161y12f{color:var(--ink);font-weight:600}.caption.svelte-161y12f{font-size:1rem;letter-spacing:var(--tracking-caps, .03em);text-transform:uppercase;color:var(--ink-muted)}.reprint.svelte-161y12f{flex:0 0 auto;align-self:center;margin-right:1.75rem;padding:0 2rem;border:2px solid var(--border);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow-1);font-size:1.375rem;font-weight:600;transition:border-color var(--slide) var(--ease),opacity var(--slide) var(--ease),transform var(--tap) var(--ease)}.reprint.svelte-161y12f:disabled{opacity:.35;box-shadow:none;cursor:default}.dot.svelte-161y12f{flex:0 0 auto;width:var(--dot-size-health, 1.25rem);height:var(--dot-size-health, 1.25rem);border-radius:50%;background:var(--ready);transition:background-color var(--slide) var(--ease)}.dot.fault.svelte-161y12f{background:var(--fault)}.admin.svelte-161y12f{position:relative;z-index:20;display:flex;align-items:center;justify-content:center;flex:0 0 auto;align-self:center;width:var(--touch-min);height:var(--touch-min);margin-left:1.75rem;border:2px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--ink-muted)}.pad.svelte-wrfqsf{display:flex;flex:0 0 auto;align-items:center;gap:1.25rem;padding:.75rem 1.75rem;background:var(--surface);border-bottom:1px solid var(--border);box-shadow:var(--shadow-1);z-index:1;animation:svelte-wrfqsf-drop var(--slide) var(--ease)}@keyframes svelte-wrfqsf-drop{0%{transform:translateY(-1rem);opacity:0}}.value.svelte-wrfqsf{display:flex;flex-direction:column;flex:0 0 auto;min-width:11rem;margin:0;padding-right:1.5rem;border-right:1px solid var(--border-soft)}.label.svelte-wrfqsf{color:var(--ink-muted);font-size:1rem;letter-spacing:.08em;text-transform:uppercase}.grams.svelte-wrfqsf{font-size:2.5rem;font-weight:700;line-height:1.1}.unit.svelte-wrfqsf{font-size:1.5rem;font-weight:400;color:var(--ink-muted)}.digits.svelte-wrfqsf,.actions.svelte-wrfqsf{display:flex;gap:var(--touch-gap)}.actions.svelte-wrfqsf{margin-left:auto}.key.svelte-wrfqsf{display:flex;align-items:center;justify-content:center;gap:.5rem;width:var(--touch-min);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);box-shadow:var(--shadow-1);font-size:1.75rem;font-weight:600}.key.svelte-wrfqsf:active{background:var(--bg)}.key.action.svelte-wrfqsf{width:auto;padding:0 1.5rem;font-size:1.375rem;font-weight:500}.key.confirm.svelte-wrfqsf{border:2px solid var(--ready);background:var(--ready-wash);font-weight:700}.screen.svelte-1n46o8q{display:flex;flex-direction:column;height:100%;overflow:hidden} diff --git a/internal/web/dist/assets/mount-BjqIypvA.js b/internal/web/dist/assets/mount-BjqIypvA.js deleted file mode 100644 index 8a1e0f1..0000000 --- a/internal/web/dist/assets/mount-BjqIypvA.js +++ /dev/null @@ -1,93 +0,0 @@ -import{R as ar,T as rr,U as nr,V as xa,W as Yt,e as sr,X as lr,Y as ir,Z as or,_ as ur,u as pa,r as Fa,$ as cr,c as Ma,B as aa,A as U,k as e,z as v,F as Lt,d as Tt,b as vt,s as r,i as m,t as p,n as Ht,l as Be,o as i,f as et,g as l,v as a,j as u,p as Ct,I as dr,h as Rt,q as o,w as K,a0 as Ea,E as na,x as rt,G as Ba,a1 as ua,y as Qt,a2 as Dt,a3 as yt,a4 as sa,C as fa,K as vr,H as pr,P as fr,a5 as gr}from"./app-Dn-BjSsJ.js";function Va(n){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function mr(n,t,...s){var c=new nr(n);ar(()=>{const _=t()??null;c.ensure(_,_&&(b=>_(b,...s)))},rr)}function Wa(n,t,s=!1){if(n.multiple){if(t==null)return;if(!lr(t))return ir();for(var c of n.options)c.selected=t.includes(la(c));return}for(c of n.options){var _=la(c);if(or(_,t)){c.selected=!0;return}}(!s||t!==void 0)&&(n.selectedIndex=-1)}function hr(n){var t=new MutationObserver(()=>{"__value"in n&&Wa(n,n.__value)});t.observe(n,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),ur(()=>{t.disconnect()})}function _r(n,t,s=t){var c=new WeakSet,_=!0;xa(n,"change",b=>{var R=b?"[selected]":":checked",N;if(n.multiple)N=[].map.call(n.querySelectorAll(R),la);else{var $=n.querySelector(R)??n.querySelector("option:not([disabled])");N=$&&la($)}s(N),n.__value=N,Yt!==null&&c.add(Yt)}),sr(()=>{var b=t();if(n===document.activeElement){var R=Yt;if(c.has(R))return}if(Wa(n,b,_),_&&b===void 0){var N=n.querySelector(":checked");N!==null&&(b=la(N),s(b))}n.__value=b,_=!1}),hr(n)}function la(n){return"__value"in n?n.__value:n.value}function ia(n,t,s=t){var c=new WeakSet;xa(n,"input",async _=>{var b=_?n.defaultValue:n.value;if(b=ma(n)?ha(b):b,s(b),Yt!==null&&c.add(Yt),await cr(),b!==(b=t())){var R=n.selectionStart,N=n.selectionEnd,$=n.value.length;if(n.value=b??"",N!==null){var G=n.value.length;R===N&&N===$&&G>$?(n.selectionStart=G,n.selectionEnd=G):(n.selectionStart=R,n.selectionEnd=Math.min(N,G))}}}),pa(t)==null&&n.value&&(s(ma(n)?ha(n.value):n.value),Yt!==null&&c.add(Yt)),Fa(()=>{var _=t();if(n===document.activeElement){var b=Yt;if(c.has(b))return}ma(n)&&_===ha(n.value)||n.type==="date"&&!_&&!n.value||_!==n.value&&(n.value=_??"")})}function za(n,t,s=t){xa(n,"change",c=>{var _=c?n.defaultChecked:n.checked;s(_)}),pa(t)==null&&s(n.checked),Fa(()=>{var c=t();n.checked=!!c})}function ma(n){var t=n.type;return t==="number"||t==="range"}function ha(n){return n===""?null:+n}function ka(n){Ma===null&&Va(),aa(()=>{const t=pa(n);if(typeof t=="function")return t})}function br(n){Ma===null&&Va(),ka(()=>()=>pa(n))}class It extends Error{constructor(t,s,c="",_=[]){super(s),this.status=t,this.code=c,this.faults=_,this.name="AdminError"}status;code;faults;get needsPassword(){return this.status===401}}function wr(){return Jt("/admin/api/health")}function yr(){return ht("/admin/api/troubleshooting/reprint",{})}function xr(){return ht("/admin/api/troubleshooting/reload-catalog",{})}function kr(n){return ht("/admin/api/troubleshooting/manual-entry",{on:n})}function qr(){return ht("/admin/api/troubleshooting/roll-changed",{})}function Sr(n){return ht("/admin/api/troubleshooting/fallback-printer",{on:n})}function Lr(){return ht("/admin/api/troubleshooting/test-scale",{})}function Er(){return ht("/admin/api/troubleshooting/test-printer",{})}function zr(){return ht("/admin/api/troubleshooting/test-label",{})}async function Ga(n){const t=new FormData;t.append("file",n,n.name);const s=await fetch("/admin/api/catalog/import",{method:"POST",body:t});return Sa(s,"POST /admin/api/catalog/import")}const jr="/admin/api/diagnostic.zip";function Pr(n){return ht("/admin/api/session",{password:n})}async function Tr(){await fetch("/admin/api/session",{method:"DELETE"})}function Cr(n,t){return ht("/admin/api/session/recovery",{code:n,password:t})}function qa(){return Jt("/admin/api/config")}function Rr(n){return Ja("PUT","/admin/api/config",n)}function Or(){return ht("/admin/api/config/confirm",{})}async function Ar(){return(await Jt("/admin/api/config/versions")).versions}function Nr(n){return ht("/admin/api/config/restore",{version:n})}async function $r(){return(await Jt("/admin/api/ports")).ports}async function Dr(){return(await Jt("/admin/api/printers")).printers}async function Ir(){return(await ht("/admin/api/printers/discover",{})).printers}function Ur(n){return ht("/admin/api/scale/detect",{port:n})}async function ja(n,t){return(await ht("/admin/api/scale/capture",{port:n,seconds:t})).frames}function Ha(n){return ht("/admin/api/printer/test?what="+n,{})}function Fr(n,t,s,c){const _=new URLSearchParams({template:n,demo:t?"1":"0",dual:s?"1":"0"});return _.set("t",String(c)),"/admin/api/label/preview.png?"+_.toString()}async function Mr(n){const t=new URLSearchParams(n);return(await Jt("/admin/api/journal?"+t)).weighings}function Br(n){return"/admin/api/journal/export.csv?"+new URLSearchParams(n).toString()}async function Vr(n){const t=new URLSearchParams(n);return(await Jt("/admin/api/technical?"+t)).entries}function Wr(n){const t=n===void 0?"":"?id="+String(n);return Jt("/admin/api/imports"+t)}function Gr(){return ht("/admin/api/catalog/reload",{})}function Hr(){return ht("/admin/api/catalog/forget-quarantine",{})}function Pa(n,t){return ht("/admin/api/products/"+encodeURIComponent(n)+"/decision",t)}function Jr(n){return ht("/admin/api/replay",{frame:n})}async function Jt(n){const t=await fetch(n,{headers:{accept:"application/json"}});return Sa(t,"GET "+n)}function ht(n,t){return Ja("POST",n,t)}async function Ja(n,t,s){const c=await fetch(t,{method:n,headers:{"content-type":"application/json"},body:JSON.stringify(s)});return Sa(c,n+" "+t)}async function Sa(n,t){const s=await n.text();if(!n.ok){const c=Ka(s);throw new It(n.status,Xr(s,t,n.status)+Kr(n),c?.code??"",c?.faults??[])}if(s!=="")return JSON.parse(s)}function Kr(n){if(n.status!==429)return"";const t=Number(n.headers.get("Retry-After")??"");if(!Number.isFinite(t)||t<=0)return"";const s=Math.ceil(t/60);return s>1?` Réessayez dans ${String(s)} minutes.`:" Réessayez dans une minute."}function Xr(n,t,s){const c=Ka(n);return c?.message!==void 0&&c.message!==""?c.message:`${t} a répondu ${String(s)}.`}function Ka(n){try{return JSON.parse(n)}catch{return null}}function Yr(){return Jt("/admin/api/update")}function Qr(){return ht("/admin/api/update/check",{})}function Zr(n){return ht("/admin/api/update/apply",{version:n})}class en{constructor(t){this.admin=t}admin;#e=U(null);get config(){return e(this.#e)}set config(t){v(this.#e,t,!0)}#a=U("");get fingerprint(){return e(this.#a)}set fingerprint(t){v(this.#a,t,!0)}#r=U(Lt([]));get retired(){return e(this.#r)}set retired(t){v(this.#r,t,!0)}#n=U(null);get pending(){return e(this.#n)}set pending(t){v(this.#n,t,!0)}#s=U(Lt([]));get faults(){return e(this.#s)}set faults(t){v(this.#s,t,!0)}#l=U(!1);get dirty(){return e(this.#l)}set dirty(t){v(this.#l,t,!0)}async load(){const t=await this.admin.load(()=>qa());t!==null&&(this.config=t.config,this.fingerprint=t.config_fingerprint,this.retired=t.retired_keys??[],this.pending=t.pending_confirmation,this.faults=[],this.dirty=!1)}value(t){let s=this.config;for(const c of t.split(".")){if(s===null||typeof s!="object")return;s=s[c]}return s}text(t){const s=this.value(t);return s==null?"":String(s)}number(t){const s=this.value(t);return typeof s=="number"?s:Number(s??0)}flag(t){return this.value(t)===!0}set(t,s){if(this.config===null)return;const c=t.split("."),_=c.pop();if(_===void 0)return;let b=this.config;for(const R of c){const N=b[R];(N===null||typeof N!="object")&&(b[R]={}),b=b[R]}b[_]=s,this.dirty=!0}unset(t){const s=t.split("."),c=s.pop();if(c===void 0||this.config===null)return!1;let _=this.config;for(const b of s){if(_===null||typeof _!="object")return!1;_=_[b]}return _===null||typeof _!="object"?!1:(delete _[c],this.dirty=!0,!0)}async save(){if(this.config===null)return!1;this.faults=[];const t=this.config;let s;try{s=await Rr(t)}catch(c){if(Ta(c))throw c;return this.admin.report(c),this.faults=tn(this.admin),!1}return this.config=s.config,this.fingerprint=s.config_fingerprint,this.retired=s.retired_keys??[],this.pending=s.pending_confirmation,this.dirty=!1,!0}async confirm(){try{await Or()}catch(t){if(Ta(t))throw t;this.admin.report(t);return}this.admin.notice="La configuration est confirmée.",this.pending=null,await this.admin.refresh()}dropRetired(t){if(t.includes("[")){this.admin.actionError=`« ${t} » est logée dans un tableau : cet écran ne sait pas la retirer de là. Modifiez le fichier de configuration lui-même.`;return}this.unset(t)&&(this.retired=this.retired.filter(s=>s!==t))}}function tn(n){return n.lastFaults}function Ta(n){return n instanceof It&&(n.status===401||n.status===409)}const an={"station.number":"Numéro du poste","station.name":"Nom du poste","station.coop":"Nom de la coopérative","network.listen":"Adresse d’écoute","network.admin_on_lan":"Administration accessible depuis le réseau","ui.language":"Langue","ui.sound":"Son","ui.idle_timeout_s":"Retour à l’accueil après (secondes)","ui.reprint_window_s":"Réimpression possible pendant (secondes)","ui.show_grid_prices":"Afficher les prix sur les tuiles","scale.type":"Protocole de la balance","scale.present":"Ce poste a une balance","scale.manual_entry_allowed":"Saisie manuelle autorisée","scale.degrade_after_s":"Passer en mode dégradé après (secondes)","scale.options.port":"Port série","scale.options.baud":"Vitesse (bauds)","scale.options.bits":"Bits de données","scale.options.parity":"Parité","scale.options.stop":"Bits d’arrêt","scale.options.backoff_min_ms":"Attente minimale avant réessai (ms)","scale.options.backoff_max_ms":"Attente maximale avant réessai (ms)","printer.type":"Pilote d’impression","printer.template":"Gabarit d’étiquette","printer.options.transport":"Transport","printer.options.queue":"File d’impression","printer.options.path":"Fichier de périphérique","printer.options.address":"Adresse réseau","printer.options.darkness":"Noircissement","printer.options.speed":"Vitesse d’impression","printer.options.offset_x":"Décalage horizontal (dots)","printer.options.offset_y":"Décalage vertical (dots)","printer.options.invert_bits":"Inverser les points","printer.options.copies":"Exemplaires","printer.options.roll_capacity":"Étiquettes par rouleau","pricing.amount_rounding":"Arrondi du montant","pricing.unit_price_rounding":"Arrondi du prix au kilo","pricing.primary_code":"Tarif principal","pricing.reference_code":"Tarif de référence","barcode.verify_reference_check_digit":"Vérifier la clé de contrôle","limits.empty_max_g":"Plateau considéré vide en dessous de (g)","limits.basket_check_enabled":"Vérifier la présence du panier","limits.basket_min_g":"Poids du panier, borne basse (g)","limits.basket_max_g":"Poids du panier, borne haute (g)","limits.min_weight_g":"Poids minimum accepté","limits.max_weight_g":"Poids maximum accepté","limits.max_tare_g":"Tare maximale","limits.min_units":"Unités minimum","limits.max_units":"Unités maximum","limits.max_amount_cents":"Montant maximum (centimes)","stability.mode":"Exigence de stabilité","stability.min_duration_ms":"Durée de stabilité (ms)","stability.tolerance_g":"Tolérance de stabilité (g)","stability.timeout_ms":"Délai d’attente de stabilité (ms)","stability.on_timeout":"Au bout du délai","stability.min_latch_rate":"Taux d’accroche minimal","stability.latch_rate_window_ms":"Fenêtre de mesure du taux (ms)","stability.expiry_floor_ms":"Péremption, plancher (ms)","stability.expiry_ceiling_ms":"Péremption, plafond (ms)","stability.expiry_factor":"Péremption, facteur","catalog.type":"Où le poste va chercher le catalogue","catalog.options.directory":"Répertoire surveillé","catalog.options.url":"Adresse du serveur","catalog.options.username":"Compte","catalog.options.password":"Mot de passe","catalog.options.separator":"Séparateur du CSV","catalog.options.poll_interval_s":"Vérifier toutes les (secondes)","catalog.options.stable_polls":"Vérifications avant lecture","catalog.options.max_file_size_mb":"Taille maximale du fichier (Mo)","catalog.options.max_image_size_kb":"Taille maximale d’une image (Ko)","catalog.options.min_readable_ratio":"Part minimale de lignes lisibles","catalog.options.max_weighable_drop":"Baisse maximale des produits pesables","catalog.options.max_archives":"Archives conservées","catalog.options.archive_days":"Archives conservées (jours)","catalog.options.failures_before_reject":"Échecs avant mise en quarantaine","catalog.images.source":"Origine des photos","catalog.images.path":"Répertoire des photos","catalog.fallback_category":"Rayon par défaut","journal.max_rows":"Pesées conservées","journal.max_days":"Pesées conservées (jours)","journal.max_technical":"Événements techniques conservés","admin.session_minutes":"Durée d’une session (minutes)","admin.attempts_per_minute":"Tentatives par minute","maintenance.weekly_integrity_check":"Contrôle d’intégrité hebdomadaire","maintenance.disk_alert_mb":"Alerte disque en dessous de (Mo)"};function Ft(n){return an[n]??n}const rn={station:"l’identité du poste",network:"le réseau",ui:"l’écran client",scale:"la balance",printer:"l’imprimante",pricing:"les tarifs",barcode:"le code-barres",limits:"les garde-fous",stability:"la stabilité",catalog:"le catalogue",journal:"le journal",maintenance:"la maintenance"};function nn(n){return rn[n]??n}const sn={scale:"balance",printer:"imprimante",catalog:"catalogue",ui:"écran",config:"configuration",http:"réseau",system:"système"};function Xa(n){return sn[n]??"origine inconnue"}const ba="openscale.admin.preferences",Ya="technical";class ln{#e=U(Lt(on()));get showTechnicalNames(){return e(this.#e)}set showTechnicalNames(t){v(this.#e,t,!0)}toggleTechnicalNames(){this.showTechnicalNames=!this.showTechnicalNames,un(this.showTechnicalNames)}}function on(){try{return globalThis.localStorage?.getItem(ba)===Ya}catch{return!1}}function un(n){try{n?globalThis.localStorage?.setItem(ba,Ya):globalThis.localStorage?.removeItem(ba)}catch{}}const Gt=new ln,cn=3e3,dn=["hardware","label","rules","catalog","journal","station","update"];function Ca(n){return dn.includes(n)}function vn(n){return n instanceof It&&(n.status===401||n.status===409)}class pn{constructor(t=cn){this.periodMs=t}periodMs;#e=U(null);get health(){return e(this.#e)}set health(t){v(this.#e,t,!0)}#a=U("dashboard");get page(){return e(this.#a)}set page(t){v(this.#a,t,!0)}#r=U(!1);get expert(){return e(this.#r)}set expert(t){v(this.#r,t,!0)}#n=U("");get linkError(){return e(this.#n)}set linkError(t){v(this.#n,t,!0)}#s=U("");get actionError(){return e(this.#s)}set actionError(t){v(this.#s,t,!0)}#l=U(!1);get needsFirstPassword(){return e(this.#l)}set needsFirstPassword(t){v(this.#l,t,!0)}#d=U("");get notice(){return e(this.#d)}set notice(t){v(this.#d,t,!0)}#v=U(!1);get busy(){return e(this.#v)}set busy(t){v(this.#v,t,!0)}#p=U(Lt([]));get lastFaults(){return e(this.#p)}set lastFaults(t){v(this.#p,t,!0)}#f=U(null);get pending(){return e(this.#f)}set pending(t){v(this.#f,t,!0)}#i=null;#o=null;start(){this.refresh(),this.#i=setInterval(()=>{this.refresh()},this.periodMs)}stop(){this.#i!==null&&clearInterval(this.#i),this.#i=null}async refresh(){try{this.health=await wr(),this.linkError=""}catch(t){this.linkError=Ra(t)}}#u(){this.busy=!0,this.notice="",this.actionError="",this.needsFirstPassword=!1}#t(t){this.actionError=Ra(t),this.needsFirstPassword=t instanceof It&&t.status===409,this.#m(t)}async run(t){this.#u();try{const s=await t();this.notice=s.message}catch(s){this.#t(s)}finally{this.busy=!1}await this.refresh()}async login(t){this.#u();try{return await Pr(t),this.expert=!0,this.notice="Session d’administration ouverte.",!0}catch(s){return this.expert=!1,this.#t(s),!1}finally{this.busy=!1}}async recover(t,s){this.#u();try{const c=await Cr(t,s);return this.expert=!0,this.notice=c.warning??"Le mot de passe est remplacé et la session est ouverte.",!0}catch(c){return this.#t(c),!1}finally{this.busy=!1}}async protect(t){try{return await t()}catch(s){if(!vn(s))return this.#t(s),null;if(!await this.#g(s))return null;try{return await t()}catch(c){return this.#t(c),null}}}#g(t){return this.pending={kind:t.status===409?"first-password":"password",message:t.message},new Promise(s=>{this.#o=s})}async answerPassword(t){await this.login(t)&&this.#c(!0)}async answerRecovery(t,s){await this.recover(t,s)&&this.#c(!0)}cancelPassword(){this.#c(!1)}#c(t){this.pending=null,this.notice="";const s=this.#o;this.#o=null,s?.(t)}async logout(){await Tr(),this.expert=!1,this.page="dashboard",this.notice="Session d’administration fermée."}open(t){this.actionError="",this.notice="",this.needsFirstPassword=!1,this.page=t}async load(t){try{const s=await t();return this.actionError="",this.lastFaults=[],s}catch(s){return this.lastFaults=s instanceof It?s.faults:[],this.#t(s),null}}report(t){this.lastFaults=t instanceof It?t.faults:[],this.#t(t)}#m(t){t instanceof It&&t.needsPassword&&(this.expert=!1)}}function Ra(n){return n instanceof It?n.message:n instanceof Error?"Le poste n’a pas répondu : "+n.message:"Le poste n’a pas répondu."}var fn=u('clé'),gn=u('');function at(n,t){const s=vt(t,"kind",3,"read"),c=vt(t,"busy",3,!1),_=vt(t,"disabled",3,!1),b=vt(t,"protected",3,!1),R=vt(t,"act",3,void 0);var N=gn();let $;var G=a(N),V=r(G);{var se=be=>{var ye=fn();l(be,ye)};m(V,be=>{b()&&be(se)})}p(()=>{$=Ht(N,1,`act ${s()??""}`,"svelte-5tpq0o",$,{"touch-target":s()==="destructive",busy:c()}),Be(N,"data-kind",s()),Be(N,"data-act",R()),N.disabled=_()||c(),i(G,`${(c()?"En cours…":t.label)??""} `)}),et("click",N,function(...be){t.onrun?.apply(this,be)}),l(n,N)}Tt(["click"]);var mn=u(`

Le code de secours a été tiré à l’installation de ce poste et imprimé - sur sa fiche, rangée dans le classeur du magasin. Il n’existe nulle part ailleurs.

`,1),hn=u(`

Oublié ? Le code de secours de la fiche d’installation en repose un. - Un responsable peut aussi le faire en ligne de commande, avec openscale config password.

`,1),_n=u('

'),bn=u('');function wn(n,t){Ct(t,!0);const s=o(()=>t.admin.pending?.kind==="first-password");let c=U(""),_=U("");const b=8,R=o(()=>e(s)?e(_).trim().length>0&&e(c).length>=b:e(c).length>0);async function N(){!e(R)||t.admin.busy||(e(s)?await t.admin.answerRecovery(e(_),e(c)):await t.admin.answerPassword(e(c)),v(c,""),v(_,""))}var $=bn(),G=a($),V=a(G),se=a(V),be=a(se);dr(be,{name:"settings",size:"1.5rem"});var ye=r(se,2),ie=a(ye),j=r(V,2),oe=a(j),X=r(j,2);{var we=T=>{var y=mn(),C=r(K(y),2),D=r(a(C),2);Ea(D);var Z=r(C,2),pe=a(Z);pe.textContent="Nouveau mot de passe — 8 caractères au moins";var Oe=r(pe,2);et("keydown",D,Ae=>Ae.key==="Enter"&&void N()),ia(D,()=>e(_),Ae=>v(_,Ae)),et("keydown",Oe,Ae=>Ae.key==="Enter"&&void N()),ia(Oe,()=>e(c),Ae=>v(c,Ae)),l(T,y)},me=T=>{var y=hn(),C=K(y),D=r(a(C),2);Ea(D),et("keydown",D,Z=>Z.key==="Enter"&&void N()),ia(D,()=>e(c),Z=>v(c,Z)),l(T,y)};m(X,T=>{e(s)?T(we):T(me,-1)})}var E=r(X,2);{var H=T=>{var y=_n(),C=a(y);p(()=>i(C,t.admin.actionError)),l(T,y)};m(E,T=>{t.admin.actionError!==""&&T(H)})}var F=r(E,2),P=a(F);at(P,{label:"Annuler",onrun:()=>t.admin.cancelPassword()});var te=r(P,2);{let T=o(()=>e(s)?"Poser ce mot de passe":"Continuer"),y=o(()=>!e(R)||t.admin.busy);at(te,{kind:"write",get label(){return e(T)},get disabled(){return e(y)},onrun:()=>{N()}})}p(()=>{i(ie,e(s)?"Ce poste n’a pas encore de mot de passe":"Mot de passe d’administration"),i(oe,t.admin.pending?.message??"")}),l(n,$),Rt()}Tt(["keydown"]);var yn=u(' '),xn=u(""),kn=u(""),qn=u('

'),Sn=u(" "),Ln=u('

'),En=u('
');function _t(n,t){Ct(t,!0);const s=vt(t,"hint",3,""),c=vt(t,"kind",3,"text"),_=vt(t,"disabled",3,!1),b=vt(t,"fault",3,""),R=vt(t,"allowed",19,()=>[]),N=o(()=>`field-${t.path.replace(/\./gu,"-")}`);var $=En();let G;var V=a($),se=a(V),be=a(se),ye=r(se,2);{var ie=F=>{var P=yn(),te=a(P);p(()=>i(te,t.path)),l(F,P)};m(ye,F=>{Gt.showTechnicalNames&&F(ie)})}var j=r(V,2),oe=r(j,2);{var X=F=>{var P=kn();rt(P,20,R,te=>te,(te,T)=>{var y=xn(),C={};p(()=>{C!==(C=T)&&(y.value=(y.__value=T)??"")}),l(te,y)}),p(()=>Be(P,"id",e(N)+"-allowed")),l(F,P)};m(oe,F=>{R().length>0&&F(X)})}var we=r(oe,2);{var me=F=>{var P=qn(),te=a(P);p(()=>i(te,s())),l(F,P)};m(we,F=>{s()!==""&&F(me)})}var E=r(we,2);{var H=F=>{var P=Ln(),te=a(P),T=r(te);{var y=C=>{var D=Sn(),Z=a(D);p(pe=>i(Z,`Valeurs acceptées : ${pe??""}.`),[()=>R().join(", ")]),l(C,D)};m(T,C=>{R().length>0&&C(y)})}p(()=>i(te,`${b()??""} `)),l(F,P)};m(E,F=>{b()!==""&&F(H)})}p(()=>{G=Ht($,1,"field svelte-1oilv5s",null,G,{refused:b()!==""}),Be(V,"for",e(N)),i(be,t.label),Be(j,"id",e(N)),Be(j,"type",c()),na(j,t.value),j.disabled=_(),Be(j,"list",R().length>0?e(N)+"-allowed":void 0)}),et("input",j,F=>t.onchange(F.currentTarget.value)),l(n,$),Rt()}Tt(["input"]);const Oa=1e3;function da(n){const t=La(n);return t===null?"":[ta(t.getDate()),ta(t.getMonth()+1),String(t.getFullYear())].join("/")}function Mt(n){const t=La(n);return t===null?"":`${da(n)} à ${ta(t.getHours())}:${ta(t.getMinutes())}`}function zn(n){const t=La(n);return t===null?"":[ta(t.getHours()),ta(t.getMinutes()),ta(t.getSeconds())].join(":")}function wa(n){const t=n/1e6;return t<1e3?`${Math.round(t)} Mo`:`${Qa(t/1e3)} Go`}function ge(n){const t=String(Math.abs(Math.trunc(n))),s=[];for(let c=t.length;c>0;c-=3)s.unshift(t.slice(Math.max(0,c-3),c));return(n<0?"-":"")+s.join(" ")}function ca(n){return n0&&b.push({count:Pt(n.anomalies_count),label:"anomalies",note:"à corriger dans Odoo",link:Aa(n.anomalies_count)}),n.unit_mismatches_count>0){const R=n.unit_mismatches_count;b.push({count:"+ "+Pt(R),label:R===1?"unité divergente":"unités divergentes",note:"pesable, unité à corriger",link:Aa(R)})}return{headline:`Catalogue du ${da(n.occurred_at)} — ${Pt(s)} produits reçus`,lines:b,oneLine:Pn(n)}}function Pn(n){const t=[`${Pt(n.rows_read_count)} reçus`,`${Pt(n.weighable_count)} pesables`,`${Pt(n.not_weighable_count)} non pesables`,`${Pt(n.anomalies_count)} anomalies`],s=n.unit_mismatches_count;return s>0&&t.push(`${Pt(s)} ${s===1?"unité divergente":"unités divergentes"}`),t.join(" · ")}function Aa(n){return n===1?"voir la ligne":`voir les ${Pt(n)} lignes`}function Tn(n){return n.map(t=>`${Cn(t)} (${Pt(t.count)})`).join(", ")}function Cn(n){switch(n.code){case"PREPACKAGED_PRODUCT":return"préemballés";case"INTERNAL_CODE_NOT_WEIGHABLE":return n.value===""?"code interne":"code interne "+n.value;case"NO_BARCODE":return"sans code-barres";default:return n.code}}function Pt(n){return String(n)}var Rn=u(' '),On=u(''),An=u('
'),Nn=u('

');function Za(n,t){Ct(t,!0);const s=o(()=>jn(t.record,t.motives));var c=Nn(),_=a(c),b=a(_),R=r(_,2);rt(R,21,()=>e(s).lines,G=>G.label,(G,V)=>{var se=An(),be=a(se),ye=a(be),ie=r(be,2),j=a(ie),oe=a(j),X=r(j,2);{var we=H=>{var F=Rn(),P=a(F);p(()=>i(P,e(V).note)),l(H,F)};m(X,H=>{e(V).note!==""&&H(we)})}var me=r(X,2);{var E=H=>{var F=On(),P=a(F);p(()=>i(P,e(V).link)),et("click",F,()=>t.onshowrows(e(V).label==="anomalies"?"anomalies":"units")),l(H,F)};m(me,H=>{e(V).link!==""&&t.onshowrows!==void 0&&H(E)})}p(()=>{Be(se,"data-row",e(V).label),i(ye,e(V).count),i(oe,e(V).label)}),l(G,se)});var N=r(R,2),$=a(N);p(()=>{i(b,e(s).headline),i($,e(s).oneLine)}),l(n,c),Rt()}Tt(["click"]);var $n=u('

'),Dn=u('

');function Qe(n,t){const s=vt(t,"note",3,"");var c=Dn(),_=a(c),b=a(_),R=r(_,2);{var N=G=>{var V=$n(),se=a(V);p(()=>i(se,s())),l(G,V)};m(R,G=>{s()!==""&&G(N)})}var $=r(R,2);mr($,()=>t.children),p(()=>i(b,t.title)),l(n,c)}const _a=(n,t=sa,s=sa)=>{var c=Un(),_=a(c);rt(_,21,t,fa,(b,R)=>{var N=In(),$=a(N),G=a($),V=r($,2),se=a(V),be=r(V,2),ye=a(be),ie=r(be,2),j=a(ie);p(oe=>{i(G,`ligne ${oe??""}`),i(se,e(R).product_id),i(ye,e(R).value),i(j,e(R).message)},[()=>ge(e(R).csv_line)]),l(b,N)}),p(()=>Be(c,"data-rows",s())),l(n,c)};var In=u('
  • '),Un=u('
      '),Fn=u('

      Lecture des réglages du poste…

      '),Mn=u(`

      Le poste y cherche le fichier , et le supprime - une fois lu : c’est ce qui dit au producteur que la livraison est prise.

      `,1),Bn=u(`

      Sur un serveur WebDAV, le dépôt d’un fichier CSV depuis cet écran n’est plus - possible : le poste n’a plus de répertoire local où l’écrire. C’est le seul recours - du jour de la mise en service.

      `,1),Vn=u(`

      Ce poste ne déclare aucune source : choisissez-en une ci-dessus, sinon il n’ira - chercher aucun catalogue.

      `),Wn=u('
      ',1),Gn=u('

      Aucun import enregistré sur ce poste.

      '),Hn=u(`

      « Oublier la quarantaine » fait relire un fichier que le poste avait écarté : c’est le - seul geste de cette page qui puisse remettre en service un catalogue refusé.

      `,1),Jn=u('

      '),Kn=u(`

      Déposez ici le fichier clé

      Ce dépôt remplace toute la grille par le fichier apporté : il change ce que le poste - vend, et le mot de passe est donc demandé au moment du dépôt.

      `,1),Xn=u('

      '),Yn=u('

      Aucune anomalie sur le dernier import.

      '),Qn=u('

      ',1),Zn=u('

      '),es=u('

      Aucune unité divergente sur le dernier import.

      '),ts=u('

      ',1),as=u('

      '),rs=u('

      Aucun produit non pesable sur le dernier import.

      '),ns=u('

      ',1),ss=u('

      Aucun import enregistré : rien n’a encore pu être retiré.

      '),ls=u('

      Aucun produit retiré par le dernier import.

      '),is=u(`

      Ils restent enregistrés avec leur historique : une étiquette déjà collée reste - lisible en caisse, et un produit qui revient dans un prochain fichier retrouve sa - tuile. Ce poste n’en publie encore que le nombre — leurs noms se lisent dans Odoo, - en comparant avec l’export précédent.

      `,1),os=u('
    • '),us=u(`

        Un produit retiré de la grille ne se trouve plus ici : il se reprend dans - « Décisions en vigueur », plus bas.

        `,1),cs=u(`

        Sans motif, le poste refuse la décision. Le seul acte qui s’en passe est celui qui - EFFACE la décision en vigueur : proposer de nouveau un produit qui ne porte aucune - dérogation.

        `),ds=u(`

        Produit choisi :

        Ce produit est-il proposé dans la grille ?

        La dérogation s’enregistre seule : elle ne remet pas dans la grille un produit - qui en a été retiré, et retirer un produit n’efface pas sa dérogation.

        `),vs=u(' ',1),ps=u('

        Aucune décision locale : la grille est celle du fichier.

        '),fs=u('
      • '),gs=u('

          ',1),ms=u('

          '),hs=u('

          Aucun import dans l’historique.

          '),_s=u(' '),bs=u('

          QuandFichierSourceRésultatMotifLuesPesablesNon pesablesAnomaliesRetirés
          ',1),ws=u('
          ');function ys(n,t){Ct(t,!0);const s=vt(t,"admin",7),c=20,_=50,b=20,R=["NO_BARCODE","PREPACKAGED_PRODUCT","INTERNAL_CODE_NOT_WEIGHABLE"],N={applied:"appliqué",unchanged:"identique au précédent",rejected:"refusé",failed:"échec"},$={local_drop:"dépôt local",webdav:"WebDAV",manual:"déposé sur l’écran"},G={local_drop:["catalog.options.directory"],webdav:["catalog.options.url","catalog.options.username","catalog.options.password"]},V=o(()=>t.draft.text("catalog.type"));let se=U(Lt([])),be=U(Lt([])),ye=U(Lt([])),ie=U(""),j=U(""),oe=U(""),X=U(""),we=U(!1),me=U(""),E=U(""),H=U("loading"),F=U("loading");const P=o(()=>s().busy||e(E)!==""),te=o(()=>e(be).filter(k=>k.issue==="anomaly")),T=o(()=>e(be).filter(k=>R.includes(k.code))),y=o(()=>e(be).filter(k=>k.code==="UNIT_MISMATCH")),C=o(()=>new Map(e(ye).map(k=>[k.id,k.name]))),D=o(()=>e(ie)===""?[]:vr(e(ye),pr,e(ie))),Z=o(()=>e(D).slice(0,c)),pe=o(()=>e(te).slice(0,_)),Oe=o(()=>e(y).slice(0,_)),Ae=o(()=>e(T).slice(0,_)),S=o(()=>[...t.health.decisions].sort((k,Q)=>Q.decided_at.localeCompare(k.decided_at))),z=o(()=>e(S).slice(0,b)),g=o(()=>e(j)===""?null:e(S).find(k=>k.product_id===e(j))??null),O=o(()=>e(g)===null?!0:e(g).offered),I=o(()=>e(g)?.min_weight_g??null),J=o(()=>e(X).trim()===""?null:Number(e(X))),ce=o(()=>e(J)!==null&&Number.isFinite(e(J))&&e(J)>0),Ee=o(()=>e(oe).trim()),x=o(()=>e(Ee)!==""||!Se(!1,e(I))),M=o(()=>e(Ee)!==""||!Se(!0,e(I))),re=o(()=>e(ce)&&(e(Ee)!==""||!Se(e(O),e(J)))),he=o(()=>e(Ee)!==""||!Se(e(O),null)),de=o(()=>t.health.catalog?.products_withdrawn_count??0),He=o(()=>e(se).find(k=>t.health.catalog!==null&&k.id`${ge(e(de))} ${e(de)>1?"produits retirés":"produit retiré"}`+(e(He)===null?".":` depuis l’import du ${Mt(e(He).occurred_at)}.`)),xe=o(()=>e(D).length>e(Z).length?`${ge(e(Z).length)} produits affichés sur ${ge(e(D).length)} trouvés — précisez votre recherche.`:`${ge(e(D).length)} ${e(D).length>1?"produits trouvés":"produit trouvé"}.`),d=o(()=>`${ge(e(se).length)} ${e(se).length>1?"imports affichés":"import affiché"} : le poste n’en publie jamais plus de vingt.`),w=o(()=>e(H)==="loading"?"Lecture des signalements du dernier import…":"Les signalements du dernier import n’ont pas pu être lus : cet écran ne sait pas ce qu’ils disent."),Y=o(()=>e(H)==="loading"?"Lecture de l’historique des imports…":"L’historique des imports n’a pas pu être lu : cet écran ne sait pas ce qu’il contient.");let f;aa(()=>{const k=t.health.catalog?.id??null;k!==f&&(f=k,q())});async function q(){const k=await s().load(()=>Wr(t.health.catalog?.id));k===null?v(H,"unread"):(v(se,k.imports,!0),v(be,k.findings,!0),v(H,"read"));const Q=await s().load(()=>Ba());if(Q===null){v(F,"unread");return}v(ye,Q.products,!0),v(F,"read")}async function L(k,Q){v(E,k,!0),s().actionError="",s().notice="";try{const le=await s().protect(Q);return le===null?!1:(s().notice=le.message,await s().refresh(),!0)}finally{v(E,"")}}function _e(k){t.draft.set("catalog.type",k);for(const[Q,le]of Object.entries(G))if(Q!==k)for(const Ne of le)t.draft.unset(Ne)}function ne(k){return t.draft.faults.find(Q=>Q.field===k)?.message??""}function qe(k){v(j,k,!0),v(oe,"");const Q=e(S).find(le=>le.product_id===k)?.min_weight_g??null;v(X,Q===null?"":String(Q),!0)}function Se(k,Q){return!(k&&Q===null)}async function ue(k){if(e(j)==="")return;const Q=e(j),le=e(I),Ne=e(Ee);await L("offered",()=>Pa(Q,{offered:k,min_weight_g:le,reason:Ne}))&&v(oe,"")}async function W(k){if(e(j)==="")return;const Q=e(j),le=e(O),Ne=e(Ee);await L(k===null?"waiver-off":"waiver",()=>Pa(Q,{offered:le,min_weight_g:k,reason:Ne}))&&v(oe,"")}async function Ue(k){if(v(we,!1),k!=null){v(me,""),s().actionError="",s().notice="",v(E,"import");try{const Q=await s().protect(()=>Ga(k));if(Q===null)return;v(me,`${k.name} : ${ge(Q.rows_read_count)} lignes lues, ${ge(Q.weighable_count)} pesables. `+(Q.reason===""?"Le fichier est déposé ; son résultat s’inscrira dans l’historique des imports.":Q.reason)),await s().refresh()}finally{v(E,"")}}}function Ve(k){k.preventDefault(),v(we,!1);const Q=k.dataTransfer?.files.item(0);if(e(P)){v(me,`${Q?.name??"Ce fichier"} n’a pas été déposé : un acte est déjà en cours sur cette page. Réessayez quand il aura répondu.`);return}Ue(Q)}function Le(k){const Q=k.files?.item(0);k.value="",Ue(Q)}function Te(k){const Q=e(C).get(k);return Q!==void 0?Q:e(F)==="loading"?"Lecture du nom…":e(F)==="unread"?"Nom inconnu : le catalogue n’a pas pu être lu":"Produit absent du catalogue en service"}function Je(k,Q,le,Ne){const We=Q>1?Ne:le;return k>=Q?`${ge(Q)} ${We}.`:`${ge(k)} lignes affichées sur ${ge(Q)} ${We}.`}function ae(k){const Q=[];return k.offered||Q.push("retiré de la grille"),k.min_weight_g!==null&&Q.push(`peut peser à partir de ${ge(k.min_weight_g)} g`),Q.length===0?"aucune restriction":Q.join(" · ")}function Ie(k){const Q=N[k.result]??"résultat inconnu";return k.code===""?Q:`${Q} (${k.code})`}function ve(k){return $[k.source]??"source inconnue"}var ze=ws(),Re=a(ze);Qe(Re,{title:"Où le poste va chercher le catalogue",children:(k,Q)=>{var le=Wn(),Ne=K(le),We=a(Ne),Ye=a(We),Ke=r(We,2),B=a(Ke),ee=r(Ne,2);{var fe=Pe=>{var Fe=Fn();l(Pe,Fe)},$e=Pe=>{var Fe=Mn(),Ge=K(Fe);{let it=o(()=>Ft("catalog.options.directory")),dt=o(()=>t.draft.text("catalog.options.directory")),gt=o(()=>ne("catalog.options.directory"));_t(Ge,{get label(){return e(it)},path:"catalog.options.directory",get value(){return e(dt)},hint:"Laissez vide pour le répertoire du poste, celui que le service crée lui-même. Un répertoire nommé ici doit exister : le poste ne le crée pas.",get fault(){return e(gt)},onchange:xt=>t.draft.set("catalog.options.directory",xt)})}var ft=r(Ge,2),ot=r(a(ft)),ct=a(ot);p(()=>i(ct,`flv_${t.health.station??""}.csv`)),l(Pe,Fe)},ke=Pe=>{var Fe=Bn(),Ge=K(Fe);{let ct=o(()=>Ft("catalog.options.url")),it=o(()=>t.draft.text("catalog.options.url")),dt=o(()=>ne("catalog.options.url"));_t(Ge,{get label(){return e(ct)},path:"catalog.options.url",get value(){return e(it)},get fault(){return e(dt)},onchange:gt=>t.draft.set("catalog.options.url",gt)})}var ft=r(Ge,2);{let ct=o(()=>Ft("catalog.options.username")),it=o(()=>t.draft.text("catalog.options.username")),dt=o(()=>ne("catalog.options.username"));_t(ft,{get label(){return e(ct)},path:"catalog.options.username",get value(){return e(it)},get fault(){return e(dt)},onchange:gt=>t.draft.set("catalog.options.username",gt)})}var ot=r(ft,2);{let ct=o(()=>Ft("catalog.options.password")),it=o(()=>ne("catalog.options.password"));_t(ot,{get label(){return e(ct)},path:"catalog.options.password",kind:"password",value:"",hint:"Laissez vide : le mot de passe actuel est conservé.",get fault(){return e(it)},onchange:dt=>t.draft.set("catalog.options.password",dt)})}l(Pe,Fe)},De=Pe=>{var Fe=Vn();l(Pe,Fe)};m(ee,Pe=>{t.draft.config===null?Pe(fe):e(V)==="local_drop"?Pe($e,1):e(V)==="webdav"?Pe(ke,2):Pe(De,-1)})}p(()=>{ua(Ye,e(V)==="local_drop"),ua(B,e(V)==="webdav")}),et("change",Ye,()=>_e("local_drop")),et("change",B,()=>_e("webdav")),l(k,le)},$$slots:{default:!0}});var nt=r(Re,2);Qe(nt,{title:"Dernier import",children:(k,Q)=>{var le=Hn(),Ne=K(le);{var We=ke=>{var De=Gn();l(ke,De)},Ye=ke=>{Za(ke,{get record(){return t.health.catalog},get motives(){return t.health.catalog_motives}})};m(Ne,ke=>{t.health.catalog===null?ke(We):ke(Ye,-1)})}var Ke=r(Ne,2),B=a(Ke),ee=r(Ke,2),fe=a(ee);{let ke=o(()=>e(E)==="reload");at(fe,{act:"reload",kind:"write",label:"Recharger le catalogue",protected:!0,get busy(){return e(ke)},get disabled(){return e(P)},onrun:()=>{L("reload",Gr)}})}var $e=r(fe,2);{let ke=o(()=>e(E)==="quarantine");at($e,{act:"quarantine",kind:"destructive",label:"Oublier la quarantaine",protected:!0,get busy(){return e(ke)},get disabled(){return e(P)},onrun:()=>{L("quarantine",Hr)}})}p(()=>i(B,t.health.catalog_source===null?"Aucune source de catalogue publiée par ce poste.":"Source : "+t.health.catalog_source.label)),l(k,le)},$$slots:{default:!0}});var ut=r(nt,2);Qe(ut,{title:"Déposer un catalogue",note:"Glissez le fichier CSV ici, ou choisissez-le. Il passe par le même chemin que le fichier du producteur.",children:(k,Q)=>{var le=Kn(),Ne=K(le);{var We=Pe=>{var Fe=Jn(),Ge=a(Fe);p(()=>i(Ge,e(me))),l(Pe,Fe)};m(Ne,Pe=>{e(me)!==""&&Pe(We)})}var Ye=r(Ne,2);let Ke;var B=a(Ye),ee=r(a(B)),fe=a(ee),$e=r(B,2),ke=a($e),De=r(ke);p(()=>{Ke=Ht(Ye,1,"drop svelte-3gk7u7",null,Ke,{dropping:e(we),working:e(E)==="import"}),i(fe,`flv_${t.health.station??""}.csv`),i(ke,`${e(E)==="import"?"Import en cours…":"Choisir un fichier"} `),De.disabled=e(P)}),Qt("dragover",Ye,Pe=>{Pe.preventDefault(),!e(P)&&v(we,!0)}),Qt("dragleave",Ye,()=>v(we,!1)),Qt("drop",Ye,Ve),et("change",De,Pe=>Le(Pe.currentTarget)),l(k,le)},$$slots:{default:!0}});var pt=r(ut,2);Qe(pt,{title:"Anomalies à corriger dans Odoo",note:"Chaque ligne porte son numéro dans le CSV, son motif et la valeur fautive.",children:(k,Q)=>{var le=Dt(),Ne=K(le);{var We=B=>{var ee=Xn(),fe=a(ee);p(()=>i(fe,e(w))),l(B,ee)},Ye=B=>{var ee=Yn();l(B,ee)},Ke=B=>{var ee=Qn(),fe=K(ee),$e=a(fe),ke=r($e);{var De=Fe=>{var Ge=yt("Corrigez celles-ci dans Odoo : l’import suivant ne signalera que ce qui reste.");l(Fe,Ge)};m(ke,Fe=>{e(te).length>e(pe).length&&Fe(De)})}var Pe=r(fe,2);_a(Pe,()=>e(pe),()=>"anomalies"),p(Fe=>i($e,`${Fe??""} `),[()=>Je(e(pe).length,e(te).length,"anomalie","anomalies")]),l(B,ee)};m(Ne,B=>{e(H)!=="read"?B(We):e(te).length===0?B(Ye,1):B(Ke,-1)})}l(k,le)},$$slots:{default:!0}});var je=r(pt,2);Qe(je,{title:"Unités divergentes",note:"Le produit reste proposé : le code-barres fait foi, seul le libellé du prix est faux.",children:(k,Q)=>{var le=Dt(),Ne=K(le);{var We=B=>{var ee=Zn(),fe=a(ee);p(()=>i(fe,e(w))),l(B,ee)},Ye=B=>{var ee=es();l(B,ee)},Ke=B=>{var ee=ts(),fe=K(ee),$e=a(fe),ke=r(fe,2);_a(ke,()=>e(Oe),()=>"mismatches"),p(De=>i($e,De),[()=>Je(e(Oe).length,e(y).length,"unité divergente","unités divergentes")]),l(B,ee)};m(Ne,B=>{e(H)!=="read"?B(We):e(y).length===0?B(Ye,1):B(Ke,-1)})}l(k,le)},$$slots:{default:!0}});var st=r(je,2);Qe(st,{title:"Produits non pesables",note:"Un inventaire, pas une liste d’erreurs : ces produits portent déjà leur code-barres et n’ont aucune raison d’être pesés.",children:(k,Q)=>{var le=Dt(),Ne=K(le);{var We=B=>{var ee=as(),fe=a(ee);p(()=>i(fe,e(w))),l(B,ee)},Ye=B=>{var ee=rs();l(B,ee)},Ke=B=>{var ee=ns(),fe=K(ee),$e=a(fe),ke=r(fe,2);_a(ke,()=>e(Ae),()=>"not-weighable"),p(De=>i($e,De),[()=>Je(e(Ae).length,e(T).length,"produit non pesable","produits non pesables")]),l(B,ee)};m(Ne,B=>{e(H)!=="read"?B(We):e(T).length===0?B(Ye,1):B(Ke,-1)})}l(k,le)},$$slots:{default:!0}});var Ze=r(st,2);Qe(Ze,{title:"Produits retirés depuis l’import précédent",note:"Un produit absent du nouveau fichier est marqué retiré à sa date, jamais supprimé.",children:(k,Q)=>{var le=Dt(),Ne=K(le);{var We=B=>{var ee=ss();l(B,ee)},Ye=B=>{var ee=ls();l(B,ee)},Ke=B=>{var ee=is(),fe=K(ee),$e=a(fe);p(()=>i($e,e(Xe))),l(B,ee)};m(Ne,B=>{t.health.catalog===null?B(We):e(de)===0?B(Ye,1):B(Ke,-1)})}l(k,le)},$$slots:{default:!0}});var bt=r(Ze,2);Qe(bt,{title:"Décider d’un produit",note:"Retirer un produit et l’autoriser à peser moins sont deux décisions séparées : l’une n’efface pas l’autre.",children:(k,Q)=>{var le=vs(),Ne=r(K(le),2),We=r(Ne,2);{var Ye=ee=>{var fe=us(),$e=K(fe),ke=a($e),De=r($e,2),Pe=a(De);rt(Pe,21,()=>e(Z),Fe=>Fe.id,(Fe,Ge)=>{var ft=os(),ot=a(ft),ct=a(ot),it=r(ot,2),dt=a(it),gt=r(it,2),xt=a(gt);p(()=>{i(ct,e(Ge).name),i(dt,e(Ge).id),i(xt,`${e(Ge).unit_price_text??""}${e(Ge).price_suffix??""}`)}),et("click",ot,()=>qe(e(Ge).id)),l(Fe,ft)}),p(()=>i(ke,e(xe))),l(ee,fe)};m(We,ee=>{e(ie)!==""&&ee(Ye)})}var Ke=r(We,2);{var B=ee=>{var fe=ds(),$e=a(fe),ke=r(a($e)),De=a(ke),Pe=r(ke),Fe=r($e,2),Ge=a(Fe),ft=r(Fe,4),ot=r(ft,2);{var ct=tt=>{var qt=cs();l(tt,qt)};m(ot,tt=>{e(Ee)===""&&tt(ct)})}var it=r(ot,2),dt=r(a(it),2),gt=a(dt);{var xt=tt=>{{let qt=o(()=>e(E)==="offered"),$t=o(()=>e(P)||!e(x));at(tt,{act:"offered",kind:"destructive",label:"Ne plus proposer ce produit",protected:!0,get busy(){return e(qt)},get disabled(){return e($t)},onrun:()=>{ue(!1)}})}},Nt=tt=>{{let qt=o(()=>e(E)==="offered"),$t=o(()=>e(P)||!e(M));at(tt,{act:"offered",kind:"write",label:"Le proposer de nouveau",protected:!0,get busy(){return e(qt)},get disabled(){return e($t)},onrun:()=>{ue(!0)}})}};m(gt,tt=>{e(O)?tt(xt):tt(Nt,-1)})}var Bt=r(it,2),At=r(a(Bt),2),Vt=r(At,2),kt=a(Vt);{let tt=o(()=>e(E)==="waiver"),qt=o(()=>e(P)||!e(re));at(kt,{act:"waiver",kind:"write",label:"Enregistrer la dérogation",protected:!0,get busy(){return e(tt)},get disabled(){return e(qt)},onrun:()=>{W(e(J))}})}var Et=r(kt,2);{var Wt=tt=>{{let qt=o(()=>e(E)==="waiver-off"),$t=o(()=>e(P)||!e(he));at(tt,{act:"waiver-off",kind:"write",label:"Retirer la dérogation",protected:!0,get busy(){return e(qt)},get disabled(){return e($t)},onrun:()=>{W(null)}})}};m(Et,tt=>{e(I)!==null&&tt(Wt)})}p((tt,qt)=>{Be(fe,"data-decision",e(j)),i(De,tt),i(Pe,` (${e(j)??""})`),i(Ge,`En vigueur : ${qt??""}`),na(At,e(X))},[()=>Te(e(j)),()=>e(g)===null?"aucune décision — ce produit suit les règles générales":ae(e(g))]),ia(ft,()=>e(oe),tt=>v(oe,tt)),et("input",At,tt=>v(X,tt.currentTarget.value,!0)),l(ee,fe)};m(Ke,ee=>{e(j)!==""&&ee(B)})}ia(Ne,()=>e(ie),ee=>v(ie,ee)),l(k,le)},$$slots:{default:!0}});var Ot=r(bt,2);Qe(Ot,{title:"Décisions en vigueur",note:"Ce qu’un humain a décidé, produit par produit. C’est ici que se reprend un produit retiré de la grille.",children:(k,Q)=>{var le=Dt(),Ne=K(le);{var We=Ke=>{var B=ps();l(Ke,B)},Ye=Ke=>{var B=gs(),ee=K(B),fe=a(ee),$e=r(ee,2),ke=a($e);rt(ke,21,()=>e(z),De=>De.product_id,(De,Pe)=>{var Fe=fs(),Ge=a(Fe),ft=r(Ge,2),ot=a(ft),ct=r(ft,2),it=a(ct),dt=r(ct,2),gt=a(dt),xt=r(dt,2),Nt=a(xt),Bt=r(xt,2),At=a(Bt);p((Vt,kt,Et)=>{i(ot,Vt),i(it,e(Pe).product_id),i(gt,kt),i(Nt,e(Pe).reason),i(At,Et)},[()=>Te(e(Pe).product_id),()=>ae(e(Pe)),()=>da(e(Pe).decided_at)]),et("click",Ge,()=>qe(e(Pe).product_id)),l(De,Fe)}),p(De=>i(fe,De),[()=>Je(e(z).length,e(S).length,"décision en vigueur","décisions en vigueur")]),l(Ke,B)};m(Ne,Ke=>{e(S).length===0?Ke(We):Ke(Ye,-1)})}l(k,le)},$$slots:{default:!0}});var Ut=r(Ot,2);Qe(Ut,{title:"Vingt derniers imports",children:(k,Q)=>{var le=Dt(),Ne=K(le);{var We=B=>{var ee=ms(),fe=a(ee);p(()=>i(fe,e(Y))),l(B,ee)},Ye=B=>{var ee=hs();l(B,ee)},Ke=B=>{var ee=bs(),fe=K(ee),$e=a(fe),ke=r(fe,2),De=a(ke),Pe=r(a(De));rt(Pe,21,()=>e(se),Fe=>Fe.id,(Fe,Ge)=>{var ft=_s(),ot=a(ft),ct=a(ot),it=r(ot),dt=a(it),gt=r(it),xt=a(gt),Nt=r(gt),Bt=a(Nt),At=r(Nt),Vt=a(At),kt=r(At),Et=a(kt),Wt=r(kt),tt=a(Wt),qt=r(Wt),$t=a(qt),Zt=r(qt),ra=a(Zt),h=r(Zt),A=a(h);p((Ce,Me,lt,mt,wt,zt,jt,St)=>{i(ct,Ce),i(dt,e(Ge).file_name),i(xt,Me),i(Bt,lt),i(Vt,e(Ge).reason),i(Et,mt),i(tt,wt),i($t,zt),i(ra,jt),i(A,St)},[()=>Mt(e(Ge).occurred_at),()=>ve(e(Ge)),()=>Ie(e(Ge)),()=>ge(e(Ge).rows_read_count),()=>ge(e(Ge).weighable_count),()=>ge(e(Ge).not_weighable_count),()=>ge(e(Ge).anomalies_count),()=>ge(e(Ge).products_withdrawn_count)]),l(Fe,ft)}),p(()=>i($e,e(d))),l(B,ee)};m(Ne,B=>{e(H)!=="read"?B(We):e(se).length===0?B(Ye,1):B(Ke,-1)})}l(k,le)},$$slots:{default:!0}}),l(n,ze),Rt()}Tt(["change","click","input"]);function xs(n){switch(n){case"ok":return"OK";case"warn":return"À SURVEILLER";case"fault":return"EN PANNE";case"off":return"SANS OBJET";default:return"INCONNU"}}var ks=u('

          '),qs=u('

          ');function Ss(n,t){Ct(t,!0);var s=qs(),c=a(s),_=a(c),b=r(_,2),R=a(b),N=r(b,2),$=a(N),G=r(c,2),V=a(G),se=r(G,2);{var be=ye=>{var ie=ks(),j=a(ie);p(()=>i(j,t.light.remedy)),l(ye,ie)};m(se,ye=>{t.light.remedy!==""&&ye(be)})}p(ye=>{Be(s,"data-light",t.light.id),Be(s,"data-level",t.light.level),Be(_,"data-level",t.light.level),i(R,t.light.label),i($,ye),i(V,t.light.value)},[()=>xs(t.light.level)]),l(n,s),Rt()}function Ls(n){return[Es(n),zs(n),Ps(n),Ts(n),Rs(n),Os(n)]}function Es(n){const t=n.state.scale,s=ca(t.median_ms);return n.scale_present?t.connected?t.too_slow?{id:"scale",label:"Balance",level:"warn",value:`une mesure toutes les ${s}, plus lent que la péremption`,remedy:"À cette cadence, un poids serait déclaré périmé avant l’arrivée de la mesure suivante. Vérifiez le câble et l’adaptateur USB, puis la cadence sur la page Matériel."}:{id:"scale",label:"Balance",level:"ok",value:As(t.provisional,`une mesure toutes les ${s}`),remedy:""}:{id:"scale",label:"Balance",level:"fault",value:"elle ne répond plus",remedy:"Vérifiez le câble et l’alimentation de la balance, puis touchez « Tester la balance ». En attendant, « Basculer en saisie manuelle » permet de continuer à servir."}:{id:"scale",label:"Balance",level:"off",value:"ce poste est déclaré sans balance",remedy:""}}function zs(n){const t=n.state.printer;switch(t.health){case"faulted":return{id:"printer",label:"Imprimante",level:"fault",value:t.detail===""?"elle ne peut pas imprimer":t.detail,remedy:js(n)};case"consumable":return{id:"printer",label:"Imprimante",level:"warn",value:"elle imprime, mais le rouleau arrive en fin de vie",remedy:"Changez le rouleau quand vous passez derrière le comptoir, puis touchez « J’ai changé le rouleau ». Le poste continue de servir en attendant."};case"ready":return{id:"printer",label:"Imprimante",level:"ok",value:"elle répond et n’a rien à signaler",remedy:""};default:return{id:"printer",label:"Imprimante",level:"unknown",value:"elle prend les étiquettes et ne dit rien en retour",remedy:"C’est la réponse normale d’une file Windows en RAW ou d’un fichier de périphérique, pas une panne. Pour savoir si elle imprime, touchez « Imprimer une étiquette de test »."}}}function js(n){const t="Regardez le capot, le rouleau et le câble, puis touchez « Tester l’imprimante ».";return n.printing?.fallback_available===!0?t+" Le poste peut servir en attendant : « Imprimer sur l’imprimante du poste voisin ».":t}function Ps(n){const t=n.roll;return t===null?{id:"roll",label:"Rouleau",level:"unknown",value:"aucun compteur d’étiquettes sur ce poste",remedy:`Sans imprimante construite, il n’y a pas de rouleau à compter : vérifiez le « ${Ft("printer.type")} » et ses réglages sur la page Matériel.`}:t.known?t.level==="warn"?{id:"roll",label:"Rouleau",level:"warn",value:t.message,remedy:"Changez le rouleau, puis touchez « J’ai changé le rouleau »."}:{id:"roll",label:"Rouleau",level:"ok",value:t.message,remedy:""}:{id:"roll",label:"Rouleau",level:"unknown",value:t.message,remedy:"Touchez « J’ai changé le rouleau » en mettant un rouleau neuf : c’est le seul geste qui dise quelque chose de vrai du papier."}}function Ts(n){const t=n.catalog_source?.label??"";if(n.state.catalog_count===0)return{id:"catalog",label:"Catalogue",level:"fault",value:"aucun produit dans la grille",remedy:Cs(t)};const s=n.catalog;return s!==null&&s.result!=="applied"&&s.result!=="unchanged"?{id:"catalog",label:"Catalogue",level:"warn",value:`dernier fichier refusé : ${s.reason===""?s.result:s.reason}`,remedy:"La grille tourne toujours sur le catalogue précédent. Corrigez le fichier dans Odoo, ou touchez « Oublier la quarantaine » puis redéposez-le."}:{id:"catalog",label:"Catalogue",level:"ok",value:`${ge(n.state.catalog_count)} produits dans la grille`,remedy:""}}function Cs(n){return"Déposez le fichier du catalogue là où le poste le guette, ou utilisez « Importer un catalogue » ci-contre pour le glisser depuis une clé USB."+(n===""?"":` Ce poste surveille : ${n}.`)}function Rs(n){const t=n.disk;if(t===null)return{id:"disk",label:"Disque",level:"unknown",value:"la place libre n’a pas pu être mesurée",remedy:"Téléchargez le fichier de diagnostic : il porte ce que le poste a pu lire du volume, et c’est la pièce qu’un support demandera."};const s=wa(t.free_bytes),c=`seuil ${ge(t.alert_mb)} Mo`;return t.free_bytes===0?{id:"disk",label:"Disque",level:"fault",value:`plus un octet libre sur ${t.path}`,remedy:"Le journal ne peut plus rien écrire, alors que les étiquettes continuent de sortir. Faites de la place sur le disque, puis rechargez cette page."}:t.alert_mb>0&&t.free_bytes0?{id:"journal",label:"Journal",level:"fault",value:`${ge(t)} pesées imprimées mais non enregistrées`,remedy:"Les étiquettes sortent, les ventes sont bonnes, c’est la trace qui manque. Téléchargez le fichier de diagnostic et prévenez le support : ne redémarrez rien."}:n.counters.journal_rows_count<0?{id:"journal",label:"Journal",level:"unknown",value:"ce poste n’a pas de journal ouvert",remedy:"Le poste pèse et imprime quand même. Téléchargez le fichier de diagnostic : il dit pourquoi la base n’a pas pu être ouverte."}:{id:"journal",label:"Journal",level:"ok",value:`${ge(n.counters.journal_rows_count)} pesées enregistrées`,remedy:""}}function As(n,t){return n?t+" (cadence encore provisoire)":t}function Ns(n){return n===null||!n.known?"unknown":n.configured?"ok":"missing"}function $s(n){switch(n){case"applied":return"appliqué";case"unchanged":return"inchangé, déjà appliqué";case"rejected":return"refusé";case"failed":return"échec";default:return n}}var Ds=u("Une mesure toutes les ",1),Is=u('

          '),Us=u("Source : ",1),Fs=u(`

          Aucun import enregistré sur ce poste : le catalogue n’est jamais arrivé, ou le - journal ne le porte pas.

          `),Ms=u('

          ',1),Bs=u('

          ',1),Vs=u('

          Aucune décision locale : le catalogue est proposé tel qu’il arrive.

          '),Ws=u('

          '),Gs=u(' '),Hs=u('
        • '),Js=u('
            ',1),Ks=u(`INCONNU — la question n’a pas pu être posée à ce système. Ce - n’est pas « non configuré » : personne ne sait encore.`,1),Xs=u('OK ',1),Ys=u('NON CONFIGURÉ ',1),Qs=u('

            '),Zs=u('

            ',1),el=u('

            Rien à signaler depuis le démarrage du poste.

            '),tl=u(' '),al=u('
          • '),rl=u('
              '),nl=u(''),sl=u('

              '),ll=u('
              Numéro de poste
              Nom
              Coopérative
              Version
              Empreinte de configuration
              ',1),il=u('
              ');function ol(n,t){Ct(t,!0);const s=o(()=>t.health.new_version),c=o(()=>Ls(t.health)),_=o(()=>t.health.state.scale),b=o(()=>t.health.unattended_restart),R=o(()=>t.health.decisions),N=5,$=o(()=>e(R).slice(0,N));var G=il(),V=a(G);rt(V,21,()=>e(c),X=>X.id,(X,we)=>{Ss(X,{get light(){return e(we)}})});var se=r(V,2);Qe(se,{title:"Cadence de la balance",children:(X,we)=>{var me=Is(),E=a(me);{var H=T=>{var y=yt("Ce poste est déclaré sans balance : le poids est saisi à la main.");l(T,y)},F=T=>{var y=yt(`La balance ne répond pas : aucune cadence n’est mesurable tant qu’aucune trame - n’arrive.`);l(T,y)},P=T=>{var y=yt(`Aucun intervalle n’a encore été mesuré : la cadence apparaîtra dès les premières - trames.`);l(T,y)},te=T=>{var y=Ds(),C=r(K(y)),D=a(C),Z=r(C),pe=r(Z);{var Oe=z=>{var g=yt("C’est encore une valeur d’attente : moins de huit intervalles ont été observés.");l(z,g)};m(pe,z=>{e(_).provisional&&z(Oe)})}var Ae=r(pe,2);{var S=z=>{var g=yt("À cette cadence, un poids serait périmé avant l’arrivée de la mesure suivante.");l(z,g)};m(Ae,z=>{e(_).too_slow&&z(S)})}p((z,g)=>{i(D,z),i(Z,`, médiane - observée sur ${g??""} intervalles. `)},[()=>ca(e(_).median_ms),()=>ge(e(_).observations_count)]),l(T,y)};m(E,T=>{t.health.scale_present?e(_).connected?e(_).observations_count===0?T(P,2):T(te,-1):T(F,1):T(H)})}l(X,me)},$$slots:{default:!0}});var be=r(se,2);Qe(be,{title:"Catalogue",children:(X,we)=>{var me=Bs(),E=K(me),H=a(E);{var F=C=>{var D=yt("Aucune source de catalogue n’est publiée par ce poste.");l(C,D)},P=C=>{var D=Us(),Z=r(K(D)),pe=a(Z);p(()=>i(pe,t.health.catalog_source.label)),l(C,D)};m(H,C=>{t.health.catalog_source===null?C(F):C(P,-1)})}var te=r(E,2);{var T=C=>{var D=Fs();l(C,D)},y=C=>{var D=Ms(),Z=K(D),pe=a(Z),Oe=r(Z,2);Za(Oe,{get record(){return t.health.catalog},get motives(){return t.health.catalog_motives},get onshowrows(){return t.onshowrows}}),p((Ae,S)=>i(pe,`Dernier essai : ${Ae??""} — - ${S??""}${t.health.catalog.file_name===""?"":" ("+t.health.catalog.file_name+")"}${t.health.catalog.reason===""?"":" : "+t.health.catalog.reason}`),[()=>Mt(t.health.catalog.occurred_at),()=>$s(t.health.catalog.result)]),l(C,D)};m(te,C=>{t.health.catalog===null?C(T):C(y,-1)})}l(X,me)},$$slots:{default:!0}});var ye=r(be,2);Qe(ye,{title:"Décisions locales en vigueur",note:"Ce qu’un humain a décidé de ce catalogue, avec son motif et sa date.",children:(X,we)=>{var me=Dt(),E=K(me);{var H=P=>{var te=Vs();l(P,te)},F=P=>{var te=Js(),T=K(te);{var y=D=>{var Z=Ws(),pe=a(Z);p(Oe=>i(pe,`${Oe??""} décisions en vigueur — les - 5 plus récentes ci-dessous. La liste entière est sur la page - Catalogue.`),[()=>ge(e(R).length)]),l(D,Z)};m(T,D=>{e(R).length>N&&D(y)})}var C=r(T,2);rt(C,21,()=>e($),D=>D.product_id,(D,Z)=>{var pe=Hs(),Oe=a(pe),Ae=a(Oe),S=r(Oe,2);{var z=ce=>{var Ee=Gs(),x=a(Ee);p(M=>i(x,`peut peser à partir de ${M??""} g`),[()=>ge(e(Z).min_weight_g)]),l(ce,Ee)};m(S,ce=>{e(Z).min_weight_g!==null&&ce(z)})}var g=r(S,2),O=a(g),I=r(g,2),J=a(I);p(ce=>{i(Ae,`${e(Z).offered?"Dérogation de poids":"Produit retiré"} — ${e(Z).product_id??""}`),i(O,e(Z).reason),i(J,`${ce??""}, ${e(Z).decided_by??""}`)},[()=>da(e(Z).decided_at)]),l(D,pe)}),l(P,te)};m(E,P=>{e(R).length===0?P(H):P(F,-1)})}l(X,me)},$$slots:{default:!0}});var ie=r(ye,2);Qe(ie,{title:"Redémarrage sans intervention",children:(X,we)=>{var me=Zs(),E=K(me),H=a(E);{var F=C=>{var D=Ks();l(C,D)},P=C=>{var D=Xs(),Z=r(K(D));p(()=>i(Z,` — après une coupure de courant, ce poste revient seul sur - l’écran client. ${e(b).detail??""}`)),l(C,D)},te=C=>{var D=Ys(),Z=r(K(D));p(()=>i(Z,` — ${e(b).detail??""}`)),l(C,D)};m(H,C=>{e(b)===null||!e(b).known?C(F):e(b).configured?C(P,1):C(te,-1)})}var T=r(E,2);{var y=C=>{var D=Qs(),Z=a(D);p(()=>i(Z,e(b).remedy)),l(C,D)};m(T,C=>{e(b)!==null&&e(b).known&&!e(b).configured&&e(b).remedy!==""&&C(y)})}p(C=>Be(E,"data-verdict",C),[()=>Ns(e(b))]),l(X,me)},$$slots:{default:!0}});var j=r(ie,2);Qe(j,{title:"Dix derniers événements",children:(X,we)=>{var me=Dt(),E=K(me);{var H=P=>{var te=el();l(P,te)},F=P=>{var te=rl();rt(te,21,()=>t.health.events,T=>T.id,(T,y)=>{var C=al(),D=a(C),Z=a(D),pe=r(D,2),Oe=a(pe),Ae=r(pe,2);{var S=O=>{var I=tl(),J=a(I);p(()=>i(J,e(y).code)),l(O,I)};m(Ae,O=>{e(y).code!==""&&Gt.showTechnicalNames&&O(S)})}var z=r(Ae,2),g=a(z);p((O,I)=>{Be(C,"data-level",e(y).level),i(Z,O),i(Oe,I),i(g,e(y).message)},[()=>zn(e(y).occurred_at),()=>Xa(e(y).source)]),l(T,C)}),l(P,te)};m(E,P=>{t.health.events.length===0?P(H):P(F,-1)})}l(X,me)},$$slots:{default:!0}});var oe=r(j,2);Qe(oe,{title:"Ce poste",children:(X,we)=>{var me=ll(),E=K(me),H=r(a(E),2),F=a(H),P=r(H,4),te=a(P),T=r(P,4),y=a(T),C=r(T,4),D=a(C),Z=r(C,4),pe=a(Z),Oe=r(E,2);{var Ae=S=>{var z=sl(),g=a(z),O=r(g);{var I=J=>{var ce=nl();et("click",ce,function(...Ee){t.onshowupdate?.apply(this,Ee)}),l(J,ce)};m(O,J=>{t.onshowupdate!==void 0&&J(I)})}p(()=>{Be(z,"data-update-available",e(s)),i(g,`Version ${e(s)??""} disponible. `)}),l(S,z)};m(Oe,S=>{e(s)!==""&&S(Ae)})}p(()=>{i(F,t.health.station),i(te,t.health.station_name===""?"non renseigné":t.health.station_name),i(y,t.health.coop),i(D,t.health.version),i(pe,t.health.config_fingerprint)}),l(X,me)},$$slots:{default:!0}}),l(n,G),Rt()}Tt(["click"]);var ul=u(''),cl=u(`

              Lecture de la configuration en cours… tant qu’elle n’est pas arrivée, cette page ne - déclare rien de ce poste.

              `),Na=u('
            • '),dl=u('
                '),vl=u('

                ',1),pl=u('
              • '),fl=u('

                  ',1),gl=u('

                  '),ml=u('
                • '),hl=u('
                    '),_l=u('

                      ',1),bl=u('

                      Balance

                      L’état et la cadence viennent de ce que le poste observe vraiment, jamais d’un réglage.

                      Réglages série de la balance

                      Imprimante

                      Réglages de l’imprimante
                      ');function wl(n,t){Ct(t,!0);const s=vt(t,"admin",7),c=20,_=3,b=250,R=50,N=_*1e3+b+1e3,$=12;let G=U(Lt([])),V=U(Lt([])),se=U(Lt([])),be=U(Lt([])),ye=U(""),ie=U(!1),j=U(""),oe=U(0),X=U(0),we=U(!1),me=U(null),E=!1;const H=o(()=>t.health.state.scale),F=o(()=>t.health.state.printer),P=o(()=>t.draft.config!==null),te=o(()=>e(P)&&!t.draft.flag("scale.present")),T=o(()=>t.draft.text("scale.options.port")),y=o(()=>e(me)!==null&&e(me).port===e(T)?e(me).reason:""),C=o(()=>e(ye)===e(T)?e(be):[]),D=o(d),Z=o(Y),pe=o(L),Oe=o(qe),Ae=o(()=>Le("scale.type")!==""||Le("scale.options.port")!==""),S=o(()=>Le("printer.type")!==""||Le("printer.options.transport")!==""||Le("printer.options.queue")!=="");br(()=>{E=!0}),ka(()=>{x("ports",M)}),aa(()=>{e(we)||!z(e(T))||O(e(T))});function z(h){return!E&&e(P)&&!e(te)&&h!==""&&h===e(T)&&g(h)&&e(y)===""&&e(j)===""}function g(h){return e(G).some(A=>A.name===h)}async function O(h){v(we,!0);try{for(;z(h);){try{J(h,await ja(h,_))}catch(A){v(me,{port:h,reason:Je(A)},!0);return}await ce()}}finally{v(we,!1)}}async function I(){const h=e(T),A=await s().protect(()=>ja(h,_));A!==null&&(J(h,A),v(me,null))}function J(h,A){if(E||A.length===0)return;const Ce=h===e(ye)?e(be):[];v(ye,h,!0),v(be,[...Ce,...A].slice(-c),!0)}function ce(h=b){return new Promise(A=>setTimeout(A,h))}async function Ee(){const h=Date.now()+N;for(;e(we)&&Date.now()Ha(h));A!==null&&(s().notice=A.message,await s().refresh())}async function He(){v(oe,0),v(X,0),e(ie)||await M();const h=e(G);if(h.length===0){v(se,[],!0);return}await s().protect(()=>Xe(h))}async function Xe(h){v(se,[],!0),v(X,h.length,!0);for(const[A,Ce]of h.entries()){v(oe,A+1);try{const Me=await Ur(Ce.name);v(se,[...e(se),{port:Ce.name,message:Me.message,refused:!1}],!0)}catch(Me){if(ae(Me))throw Me;v(se,[...e(se),{port:Ce.name,message:Je(Me),refused:!0}],!0)}}}function xe(){return e(j)!=="detect"?"Détecter automatiquement":e(we)?"Détection : le port se libère…":e(X)===0?"Détection : énumération des ports…":`Détection : port ${ge(e(oe))} sur ${ge(e(X))}…`}function d(){return t.health.scale_present?e(H).connected?e(H).too_slow?{level:"warn",word:"Trop lente",detail:w()+" À cette cadence, un poids serait déclaré périmé avant l’arrivée de la mesure suivante."}:{level:"ok",word:"Connectée",detail:"Elle répond. "+w()}:{level:"fault",word:"Sans réponse",detail:"Elle ne répond plus. Vérifiez le câble et l’alimentation, puis « Tester la balance » sur la page Dépannage."}:{level:"off",word:"Sans balance",detail:"Ce poste est déclaré sans balance : le feu est éteint et le poids se saisit à la main."}}function w(){return e(H).observations_count===0?"Aucun intervalle n’a encore été mesuré : la cadence sera connue dès les premières trames.":`Une mesure toutes les ${ca(e(H).median_ms)} sur ${ge(e(H).observations_count)} intervalles`+(e(H).provisional?", cadence encore provisoire.":".")}function Y(){const h=f[e(F).health]??{level:"unknown",word:"État inconnu",detail:"Le poste a répondu un état que cet écran ne sait pas nommer."};return{...h,detail:e(F).detail===""?h.detail:e(F).detail}}const f={ready:{level:"ok",word:"Prête",detail:"Elle répond et n’a rien à signaler."},consumable:{level:"warn",word:"Rouleau en fin de vie",detail:"Elle imprime, mais le rouleau arrive en fin de vie."},faulted:{level:"fault",word:"En panne",detail:"Elle ne peut pas imprimer."},unknown:{level:"unknown",word:"Silencieuse",detail:"Elle prend les étiquettes et ne dit rien en retour : c’est la réponse normale d’une file Windows en RAW ou d’un fichier de périphérique, pas une panne."}};function q(){const h=e(F).observed_at===""?"Jamais observée depuis le démarrage":`Observée le ${Mt(e(F).observed_at)}`,A=e(F).pending_jobs_count;return`${h}, ${ge(A)} ${A>1?"travaux":"travail"} en attente.`}function L(){const h=ne();return h===""?_e():`${_e()} ${h}`}function _e(){return e(P)?e(te)?"Ce poste est déclaré sans balance : aucun port n’est écouté.":e(T)===""?"Aucun port n’est indiqué : choisissez-en un dans la liste ci-dessus pour écouter les trames.":e(ie)?g(e(T))?e(y)!==""?`L’écoute de ${e(T)} est arrêtée.`:e(j)!==""?`L’écoute de ${e(T)} est suspendue le temps de l’acte en cours.`:`Écoute de ${e(T)}.`:`${e(T)} n’est pas visible depuis ce poste : rien n’est écouté en continu.`:e(j)==="ports"?`Énumération des ports en cours : l’écoute de ${e(T)} démarre dès qu’il est vu.`:`Les ports de ce poste n’ont pas été énumérés : « Lister les ports » dira si ${e(T)} existe.`:"Lecture de la configuration en cours : le port à écouter n’est pas encore connu."}function ne(){return e(C).length===0?"Aucune trame reçue pour l’instant.":e(C).length===1?`Une seule trame reçue — ${ge(c)} au plus sont gardées.`:`Les ${ge(e(C).length)} dernières trames — ${ge(c)} au plus, la plus récente en bas.`}function qe(){return!e(P)||e(te)||e(T)===""?"":e(y)!==""?"Reprendre l’écoute":e(ie)&&!g(e(T))?"Écouter ce port une fois":""}function Se(h,A,Ce){const Me=`${ge(Ce)} ${Ce>1?A:h}`;return Ce<=$?Me+".":`${Me} — seules les ${ge($)} premières lignes sont affichées.`}function ue(h,A){const Ce=Te(h);return Ce.length>0?Ce:A.slice(0,$)}function W(h){return[...new TextEncoder().encode(h)].map(A=>A.toString(16).toUpperCase().padStart(2,"0")).join(" ")}const Ue={0:"NUL",2:"STX",3:"ETX",4:"EOT",5:"ENQ",6:"ACK",9:"TAB",10:"LF",13:"CR",21:"NAK",27:"ESC",127:"DEL"};function Ve(h){let A="";for(const Ce of h){const Me=Ce.codePointAt(0)??0;if(Me>=32&&Me!==127){A+=Ce;continue}A+=`⟨${Ue[Me]??Me.toString(16).toUpperCase().padStart(2,"0")}⟩`}return A}function Le(h){return t.draft.faults.find(A=>A.field===h)?.message??""}function Te(h){return t.draft.faults.find(A=>A.field===h)?.allowed??[]}function Je(h){return h instanceof It?h.message:h instanceof Error?"Le poste n’a pas répondu : "+h.message:"Le poste n’a pas répondu."}function ae(h){return h instanceof It&&(h.status===401||h.status===409)}const Ie=[{what:"label",name:"étiquette"},{what:"alignment",name:"alignement"},{what:"ruler",name:"réglette"}];var ve=bl(),ze=a(ve),Re=a(ze),nt=r(a(Re),2),ut=r(a(nt),1,!0),pt=r(Re,2),je=a(pt),st=r(pt,4);{var Ze=h=>{var A=ul(),Ce=a(A);p(()=>ua(Ce,e(te))),et("change",Ce,Me=>t.draft.set("scale.present",!Me.currentTarget.checked)),l(h,A)},bt=h=>{var A=cl();l(h,A)};m(st,h=>{e(P)?h(Ze):h(bt,-1)})}var Ot=r(st,2),Ut=a(Ot);{let h=o(()=>e(j)==="ports"),A=o(()=>e(j)!=="");at(Ut,{label:"Lister les ports",get busy(){return e(h)},get disabled(){return e(A)},onrun:()=>{x("ports",M)}})}var k=r(Ut,2);{let h=o(xe),A=o(()=>e(j)!=="");at(k,{act:"detect",get label(){return e(h)},protected:!0,get disabled(){return e(A)},onrun:()=>{x("detect",He)}})}var Q=r(Ot,2);{var le=h=>{var A=vl(),Ce=K(A),Me=a(Ce),lt=r(Ce,2);{var mt=wt=>{var zt=dl();rt(zt,21,()=>e(G).slice(0,$),jt=>jt.name,(jt,St)=>{var Kt=Na(),ea=a(Kt),ga=a(ea),er=r(ea,2),tr=a(er);p(()=>{i(ga,e(St).name),i(tr,`${(e(St).description===""?"aucune description USB":e(St).description)??""} - ${e(St).vid===""?"":` — VID ${e(St).vid} PID ${e(St).pid}`}`)}),et("click",ea,()=>t.draft.set("scale.options.port",e(St).name)),l(jt,Kt)}),l(wt,zt)};m(lt,wt=>{e(G).length>0&&wt(mt)})}p(wt=>i(Me,wt),[()=>e(G).length===0?"Aucun port série n’est visible depuis ce poste.":Se("port détecté","ports détectés",e(G).length)]),l(h,A)};m(Q,h=>{e(ie)&&h(le)})}var Ne=r(Q,2);{var We=h=>{var A=fl(),Ce=K(A),Me=a(Ce),lt=r(Ce,2);rt(lt,21,()=>e(se).slice(0,$),mt=>mt.port,(mt,wt)=>{var zt=pl();let jt;var St=a(zt),Kt=a(St),ea=r(St,2),ga=a(ea);p(()=>{jt=Ht(zt,1,"svelte-1p4i0xm",null,jt,{refused:e(wt).refused}),i(Kt,e(wt).port),i(ga,e(wt).message)}),l(mt,zt)}),p(mt=>i(Me,mt),[()=>Se("port interrogé","ports interrogés",e(se).length)]),l(h,A)};m(Ne,h=>{e(se).length>0&&h(We)})}var Ye=r(Ne,2),Ke=r(a(Ye),2),B=a(Ke);{let h=o(()=>t.draft.text("scale.type")),A=o(()=>Le("scale.type")),Ce=o(()=>Te("scale.type")),Me=o(()=>!e(P));_t(B,{label:"Protocole",path:"scale.type",get value(){return e(h)},hint:"Les valeurs acceptées apparaissent ici si l’enregistrement est refusé.",get fault(){return e(A)},get allowed(){return e(Ce)},get disabled(){return e(Me)},onchange:lt=>t.draft.set("scale.type",lt)})}var ee=r(B,2);{let h=o(()=>t.draft.text("scale.options.port")),A=o(()=>Le("scale.options.port")),Ce=o(()=>ue("scale.options.port",e(G).map(lt=>lt.name))),Me=o(()=>!e(P));_t(ee,{label:"Port série",path:"scale.options.port",get value(){return e(h)},hint:"Choisissez-le dans la liste détectée ci-dessus plutôt que de le taper : l’écoute permanente ne suit que des ports détectés.",get fault(){return e(A)},get allowed(){return e(Ce)},get disabled(){return e(Me)},onchange:lt=>t.draft.set("scale.options.port",lt)})}var fe=r(Ye,2),$e=a(fe),ke=a($e),De=r($e,2);{var Pe=h=>{var A=gl(),Ce=a(A),Me=r(Ce);{var lt=mt=>{{let wt=o(()=>e(j)==="listen"),zt=o(()=>e(j)!=="");at(mt,{act:"listen",get label(){return e(Oe)},protected:!0,get busy(){return e(wt)},get disabled(){return e(zt)},onrun:()=>{x("listen",I)}})}};m(Me,mt=>{e(Oe)!==""&&mt(lt)})}p(()=>i(Ce,`${e(y)??""} `)),l(h,A)};m(De,h=>{(e(y)!==""||e(Oe)!=="")&&h(Pe)})}var Fe=r(De,2);{var Ge=h=>{var A=hl();rt(A,21,()=>e(C),fa,(Ce,Me)=>{var lt=ml(),mt=a(lt),wt=a(mt),zt=r(mt,2),jt=a(zt);p((St,Kt)=>{i(wt,St),i(jt,Kt)},[()=>W(e(Me)),()=>Ve(e(Me))]),l(Ce,lt)}),l(h,A)};m(Fe,h=>{e(C).length>0&&h(Ge)})}var ft=r(ze,2),ot=a(ft),ct=r(a(ot),2),it=r(a(ct),1,!0),dt=r(ot,2),gt=a(dt),xt=r(dt,2),Nt=a(xt),Bt=r(xt,2),At=a(Bt);{let h=o(()=>e(j)==="printers"),A=o(()=>e(j)!=="");at(At,{label:"Lister les files",get busy(){return e(h)},get disabled(){return e(A)},onrun:()=>{x("printers",re)}})}var Vt=r(At,2);{let h=o(()=>e(j)==="discover"),A=o(()=>e(j)!=="");at(Vt,{label:"Rechercher l’imprimante",protected:!0,get busy(){return e(h)},get disabled(){return e(A)},onrun:()=>{x("discover",he)}})}var kt=r(Vt,2);rt(kt,17,()=>Ie,h=>h.what,(h,A)=>{{let Ce=o(()=>`Auto-test : ${e(A).name}${e(j)===e(A).what?" — en cours…":""}`),Me=o(()=>e(j)!=="");at(h,{get act(){return e(A).what},get label(){return e(Ce)},protected:!0,get disabled(){return e(Me)},onrun:()=>{x(e(A).what,()=>de(e(A).what))}})}});var Et=r(Bt,2);{var Wt=h=>{var A=_l(),Ce=K(A),Me=a(Ce),lt=r(Ce,2);rt(lt,21,()=>e(V).slice(0,$),mt=>mt.name,(mt,wt)=>{var zt=Na(),jt=a(zt),St=a(jt),Kt=r(jt,2),ea=a(Kt);p(()=>{i(St,e(wt).name),i(ea,`${e(wt).detail??""}${e(wt).default?" — file par défaut du système":""}`)}),et("click",jt,()=>t.draft.set("printer.options.queue",e(wt).name)),l(mt,zt)}),p(mt=>i(Me,mt),[()=>Se("file d’impression","files d’impression",e(V).length)]),l(h,A)};m(Et,h=>{e(V).length>0&&h(Wt)})}var tt=r(Et,2),qt=r(a(tt),2),$t=a(qt);{let h=o(()=>t.draft.text("printer.type")),A=o(()=>Le("printer.type")),Ce=o(()=>Te("printer.type")),Me=o(()=>!e(P));_t($t,{label:"Driver",path:"printer.type",get value(){return e(h)},hint:"Gardez le driver raster : c’est celui que les postes en service utilisent.",get fault(){return e(A)},get allowed(){return e(Ce)},get disabled(){return e(Me)},onchange:lt=>t.draft.set("printer.type",lt)})}var Zt=r($t,2);{let h=o(()=>t.draft.text("printer.options.transport")),A=o(()=>Le("printer.options.transport")),Ce=o(()=>Te("printer.options.transport")),Me=o(()=>!e(P));_t(Zt,{label:"Transport",path:"printer.options.transport",get value(){return e(h)},hint:"Local par défaut : une file Windows ou un fichier de périphérique de ce poste.",get fault(){return e(A)},get allowed(){return e(Ce)},get disabled(){return e(Me)},onchange:lt=>t.draft.set("printer.options.transport",lt)})}var ra=r(Zt,2);{let h=o(()=>t.draft.text("printer.options.queue")),A=o(()=>Le("printer.options.queue")),Ce=o(()=>ue("printer.options.queue",e(V).map(lt=>lt.name))),Me=o(()=>!e(P));_t(ra,{label:"File ou chemin",path:"printer.options.queue",get value(){return e(h)},hint:"Choisissez-la dans la liste ci-dessus : une file mal orthographiée ne s’imprime pas.",get fault(){return e(A)},get allowed(){return e(Ce)},get disabled(){return e(Me)},onchange:lt=>t.draft.set("printer.options.queue",lt)})}p(h=>{Be(nt,"data-level",e(D).level),i(ut,e(D).word),i(je,e(D).detail),Ye.open=e(Ae),i(ke,e(pe)),Be(ct,"data-level",e(Z).level),i(it,e(Z).word),i(gt,e(Z).detail),i(Nt,h),tt.open=e(S)},[()=>q()]),l(n,ve),Rt()}Tt(["change","click"]);var yl=u(""),xl=u('L’export sera proposé quand le journal aura répondu.'),kl=u('L’export n’est pas proposé : ce poste n’a pas répondu à la lecture du journal.'),ql=u('Exporter en CSV'),Sl=u('

                      '),Ll=u('

                      '),El=u(`

                      Aucune trame brute n’a été enregistrée pour cette pesée : il n’y a - rien à rejouer.

                      `),zl=u(`

                      La trame repart dans le décodeur du poste EN SERVICE : le poids - affiché au client change, et rien ne le remet comme il était. C’est - ce qui fait d’un refus inexpliqué un test permanent, sans - déplacement au magasin et sans balance.

                      `,1),jl=u('

                      Produit
                      Référence
                      Vente
                      Brut / tare / net
                      Stabilité
                      Origine du poids
                      Résultat
                      Trame brute
                      '),Pl=u(' ',1),Tl=u('

                      QuandProduitNetCode-barresRésultatDuréeDétail
                      ',1),Cl=u('
                      ',1),Rl=u('

                      '),Ol=u(' '),Al=u(' '),Nl=u('
                    • '),$l=u('

                        ',1),Dl=u('
                        ');function Il(n,t){Ct(t,!0);const s=vt(t,"admin",7),c=200,_=5e3,b=50,R=500,N=7,$={sent:"envoyée à l’imprimante",rejected:"refusée",failed:"en échec",reprint:"réimpression"},G=[{value:"",label:"toutes"},{value:"sent",label:"envoyées à l’imprimante"},{value:"rejected",label:"refusées"},{value:"failed",label:"en échec"},{value:"reprint",label:"réimpressions"}],V={scale:"balance",manual:"saisie manuelle",replay:"trame rejouée"},se={stable:"stable",unstable:"instable",unknown:"non déclarée par la balance",not_applicable:"sans objet — saisie manuelle"},be={by_weight:"au poids",by_unit:"à l’unité"},ye={debug:"mise au point",info:"information",warn:"avertissement",error:"erreur",critical:"critique"};let ie=U(Lt([])),j=U(Lt([])),oe=U(""),X=U(null),we=U(!1),me=U(!1),E=U("loading"),H=U("loading"),F=U(""),P=U("");const te=o(()=>({limit:String(c),...e(oe)===""?{}:{result:e(oe)}})),T=o(()=>({...e(te),limit:String(_)})),y=o(()=>e(ie).find(I=>I.id===e(X))??null),C=o(()=>G.find(I=>I.value===e(oe))?.label??""),D=o(()=>Ae(e(ie).length,c,"pesée","pesées",`L’export CSV en emporte jusqu’à ${ge(_)}.`)),Z=o(()=>Ae(e(j).length,b,"ligne","lignes",`Le fichier de diagnostic emporte les ${ge(R)} dernières.`));pe();async function pe(){v(X,null),v(me,!0),v(E,"loading"),v(H,"loading"),v(ie,[],!0),v(j,[],!0),v(F,""),v(P,"");try{const I=await s().load(()=>Mr(e(te)));v(E,I===null?"unread":"read",!0),v(F,I===null?s().actionError:"",!0),v(ie,I??[],!0);const J=await s().load(()=>Vr({limit:String(b)}));v(H,J===null?"unread":"read",!0),v(P,J===null?s().actionError:"",!0),v(j,J??[],!0)}finally{v(me,!1),s().actionError=""}}async function Oe(I){s().notice="",s().actionError="",v(we,!0);try{const J=await s().protect(()=>Jr(I));J!==null&&(s().notice=J.message)}finally{v(we,!1)}}function Ae(I,J,ce,Ee,x){const M=I>1?Ee:ce;return I{var ce=Cl(),Ee=K(ce),x=r(a(Ee),2);rt(x,21,()=>G,f=>f.value,(f,q)=>{var L=yl(),_e=a(L),ne={};p(()=>{i(_e,e(q).label),ne!==(ne=e(q).value)&&(L.value=(L.__value=e(q).value)??"")}),l(f,L)});var M=r(x,2);at(M,{label:"Rafraîchir",get busy(){return e(me)},onrun:()=>{pe()}});var re=r(M,2);{var he=f=>{var q=xl();l(f,q)},de=f=>{var q=kl();l(f,q)},He=f=>{var q=ql();p(L=>Be(q,"href",L),[()=>Br(e(T))]),l(f,q)};m(re,f=>{e(E)==="loading"?f(he):e(E)==="unread"?f(de,1):f(He,-1)})}var Xe=r(Ee,2);{var xe=f=>{var q=Sl(),L=a(q);p(_e=>i(L,`L’export emporte le même filtre que le tableau, mais pas son plafond : il descend - jusqu’à ${_e??""} pesées, en point-virgule et en UTF-8 — il s’ouvre - tel quel dans le tableur d’un Windows français. Il ne demande aucun mot de passe : la - lecture du journal n’en demande pas non plus, et le fichier de diagnostic emporte déjà - les deux cents dernières pesées.`),[()=>ge(_)]),l(f,q)};m(Xe,f=>{e(E)==="read"&&f(xe)})}var d=r(Xe,2);{var w=f=>{var q=Ll(),L=a(q);{var _e=ue=>{var W=yt("Lecture du journal…");l(ue,W)},ne=ue=>{var W=yt();p(()=>i(W,`Le journal n’a pas pu être lu : ${e(F)??""} Ce n’est pas « aucune pesée ».`)),l(ue,W)},qe=ue=>{var W=yt("Le journal ne contient aucune pesée.");l(ue,W)},Se=ue=>{var W=yt();p(()=>i(W,`Aucune pesée ne correspond au filtre « ${e(C)??""} ».`)),l(ue,W)};m(L,ue=>{e(E)==="loading"?ue(_e):e(E)==="unread"?ue(ne,1):e(oe)===""?ue(qe,2):ue(Se,-1)})}l(f,q)},Y=f=>{var q=Tl(),L=K(q),_e=a(L),ne=r(L,2),qe=a(ne),Se=r(a(qe));rt(Se,21,()=>e(ie),ue=>ue.id,(ue,W)=>{var Ue=Pl(),Ve=K(Ue);let Le;var Te=a(Ve),Je=a(Te),ae=r(Te),Ie=a(ae),ve=r(ae),ze=a(ve),Re=r(ve),nt=a(Re),ut=r(Re),pt=a(ut),je=r(ut),st=a(je),Ze=r(je),bt=a(Ze),Ot=a(bt),Ut=r(Ve,2);{var k=Q=>{var le=jl(),Ne=a(le);Be(Ne,"colspan",N);var We=a(Ne),Ye=a(We),Ke=r(We,2),B=r(a(Ke),2),ee=a(B),fe=r(B,4),$e=a(fe),ke=r(fe,4),De=a(ke),Pe=r(ke,4),Fe=a(Pe),Ge=r(Pe,4),ft=a(Ge),ot=r(Ge,4),ct=a(ot),it=r(ot,4),dt=a(it),gt=r(it,4),xt=a(gt),Nt=a(xt),Bt=r(Ke,2);{var At=kt=>{var Et=El();l(kt,Et)},Vt=kt=>{var Et=zl(),Wt=K(Et),tt=a(Wt);at(tt,{kind:"destructive",label:"Rejouer cette trame",protected:!0,get busy(){return e(we)},onrun:()=>{Oe(e(y).frame)}}),l(kt,Et)};m(Bt,kt=>{e(y).frame===""?kt(At):kt(Vt,-1)})}p((kt,Et,Wt,tt,qt,$t,Zt,ra,h)=>{Be(le,"data-detail",e(W).id),i(Ye,`Pesée ${e(y).id??""}`),i(ee,`${e(y).product_name??""} (${e(y).product_id??""})`),i($e,e(y).reference),i(De,`${kt??""} — - ${Et??""} - ${e(y).quantity>1?"unités":"unité"}`),i(Fe,`${Wt??""} g / ${tt??""} g / - ${qt??""} g`),i(ft,`${$t??""} — cadence - médiane ${Zt??""}`),i(ct,ra),i(dt,`${h??""}${e(y).detail===""?"":` — ${e(y).detail}`}`),i(Nt,e(y).frame===""?"aucune trame enregistrée":e(y).frame)},[()=>S(be,e(y).mode,"mode de vente inconnu"),()=>ge(e(y).quantity),()=>ge(e(y).gross_g),()=>ge(e(y).tare_g),()=>ge(e(y).net_g),()=>S(se,e(y).stability,"stabilité inconnue"),()=>ca(e(y).rate_ms),()=>S(V,e(y).source,"origine inconnue"),()=>S($,e(y).result,"résultat inconnu")]),l(Q,le)};m(Ut,Q=>{e(X)===e(W).id&&e(y)!==null&&Q(k)})}p((Q,le,Ne,We)=>{Le=Ht(Ve,1,"svelte-86o8oz",null,Le,{open:e(X)===e(W).id}),i(Je,Q),i(Ie,e(W).product_name),i(ze,`${le??""} g`),i(nt,e(W).barcode),i(pt,Ne),i(st,We),Be(bt,"aria-expanded",e(X)===e(W).id),i(Ot,e(X)===e(W).id?"fermer":"détail")},[()=>Mt(e(W).occurred_at),()=>ge(e(W).net_g),()=>S($,e(W).result,"résultat inconnu"),()=>ca(e(W).duration_ms)]),et("click",bt,()=>v(X,e(X)===e(W).id?null:e(W).id,!0)),l(ue,Ue)}),p(()=>i(_e,e(D))),l(f,q)};m(d,f=>{e(ie).length===0?f(w):f(Y,-1)})}et("change",x,()=>{pe()}),_r(x,()=>e(oe),f=>v(oe,f)),l(I,ce)},$$slots:{default:!0}});var O=r(g,2);Qe(O,{title:"Journal technique",children:(I,J)=>{var ce=Dt(),Ee=K(ce);{var x=re=>{var he=Rl(),de=a(he);{var He=d=>{var w=yt("Lecture du journal technique…");l(d,w)},Xe=d=>{var w=yt();p(()=>i(w,`Le journal technique n’a pas pu être lu : ${e(P)??""} Ce n’est pas « aucune - ligne ».`)),l(d,w)},xe=d=>{var w=yt("Aucune ligne technique.");l(d,w)};m(de,d=>{e(H)==="loading"?d(He):e(H)==="unread"?d(Xe,1):d(xe,-1)})}l(re,he)},M=re=>{var he=$l(),de=K(he),He=a(de),Xe=r(de,2),xe=a(Xe);rt(xe,21,()=>e(j),d=>d.id,(d,w)=>{var Y=Nl(),f=a(Y),q=a(f),L=r(f,2),_e=a(L),ne=r(L,2),qe=a(ne),Se=r(ne,2);{var ue=Te=>{var Je=Ol(),ae=a(Je);p(()=>i(ae,e(w).code)),l(Te,Je)};m(Se,Te=>{e(w).code!==""&&Gt.showTechnicalNames&&Te(ue)})}var W=r(Se,2),Ue=a(W),Ve=r(W,2);{var Le=Te=>{var Je=Al(),ae=a(Je);p(()=>i(ae,e(w).detail)),l(Te,Je)};m(Ve,Te=>{e(w).detail!==""&&Te(Le)})}p((Te,Je,ae)=>{Be(Y,"data-level",e(w).level),i(q,Te),i(_e,Je),i(qe,ae),i(Ue,e(w).message)},[()=>Mt(e(w).occurred_at),()=>S(ye,e(w).level,"niveau inconnu"),()=>Xa(e(w).source)]),l(d,Y)}),p(()=>i(He,e(Z))),l(re,he)};m(Ee,re=>{e(j).length===0?re(x):re(M,-1)})}l(I,ce)},$$slots:{default:!0}}),l(n,z),Rt()}Tt(["change","click"]);var Ul=u('

                        '),Fl=u('

                        '),Ml=u('

                        '),Bl=u('

                        '),Vl=u('

                        Lecture de la configuration en cours… les flèches attendent qu’elle soit arrivée.

                        '),Wl=u(`
                        Aperçu de l’étiquette telle qu’elle serait imprimée

                        Décalage horizontal

                        Décalage vertical

                        Le décalage ne descend pas sous zéro : le poste refuse un décalage négatif quel que - soit le gabarit. Le maximum, lui, dépend de la géométrie du gabarit ; il est annoncé - ici si l’enregistrement le dépasse.

                        Le symbole code-barres est volontairement tronqué : un symbole conforme n’entre pas - sur 40 × 25 mm avec les cinq champs texte. Ce n’est pas un défaut de rendu et il n’y - a rien à corriger.

                        Le détail chiffré du symbole — largeur de module, modules rendus, modules attendus — - n’est pas servi par ce poste. Cet écran n’affiche pas un chiffre qu’il aurait deviné.

                        `,1),Gl=u(`

                        Chaque appui sort une étiquette pour de bon : le mot de passe est demandé au moment - d’imprimer, et l’impression repart d’elle-même une fois la session ouverte.

                        `,1),Hl=u('
                        ');function Jl(n,t){Ct(t,!0);const s=vt(t,"admin",7),c="printer.template",_="printer.options.offset_x",b="printer.options.offset_y",R=0,N="L’aperçu n’a pas pu être rendu par le poste.",$="Le poste a rendu l’aperçu, mais le navigateur ne l’a pas affiché.",G=[{what:"alignment",label:"Imprimer la mire d’alignement"},{what:"ruler",label:"Imprimer la réglette"}];let V=U(1),se=U(!0),be=U(!1),ye=U(""),ie=U(null),j=U(""),oe=null,X=!1;const we=o(()=>t.draft.text(c)),me=o(()=>Fr(e(we),e(se),e(be),e(V))),E=o(()=>t.draft.config!==null),H=o(()=>t.draft.number(_)),F=o(()=>t.draft.number(b)),P=o(()=>O(_)),te=o(()=>O(b)),T=o(S),y=o(()=>s().busy||e(j)!=="");aa(()=>{if(t.draft.config===null)return;if(t.draft.dirty){oe===null&&C();return}const x={x:t.draft.number(_),y:t.draft.number(b)};if(oe!==null&&oe.x===x.x&&oe.y===x.y)return;const M=oe===null;oe=x,v(ie,x,!0),M||pe()});async function C(){if(!X){X=!0;try{const x=await qa(),M={x:D(x.config,_),y:D(x.config,b)};oe=M,v(ie,M,!0)}catch{}finally{X=!1}}}function D(x,M){let re=x;for(const he of M.split(".")){if(re===null||typeof re!="object")return 0;re=re[he]}return typeof re=="number"?re:0}function Z(x,M){const re=t.draft.number(x)+M;re=2?"s":""}`}function g(x,M){const re=Number(M);M.trim()===""||Number.isNaN(re)||t.draft.set(x,re)}function O(x){return t.draft.faults.find(M=>M.field===x)?.message??""}async function I(x){if(!e(y)){v(j,x,!0),s().actionError="",s().notice="";try{const M=await s().protect(()=>Ha(x));if(M===null)return;s().notice=M.message,await s().refresh()}finally{v(j,"")}}}var J=Hl(),ce=a(J);Qe(ce,{title:"Aperçu de l’étiquette",note:"Le même moteur que l’impression (A2) : le décalage se voit parce qu’il est cuit dans le bitmap. L’image porte le gabarit en cours d’édition, mais le décalage ENREGISTRÉ — jamais celui que les flèches sont en train de régler.",children:(x,M)=>{var re=Wl(),he=K(re);{var de=je=>{var st=Ul(),Ze=a(st);p(()=>i(Ze,e(T))),l(je,st)};m(he,je=>{e(T)!==""&&je(de)})}var He=r(he,2),Xe=a(He),xe=a(Xe),d=r(xe,2);{var w=je=>{var st=Fl(),Ze=a(st);p(()=>i(Ze,e(ye))),l(je,st)};m(d,je=>{e(ye)!==""&&je(w)})}var Y=r(Xe,2),f=a(Y),q=r(a(f),2),L=a(q),_e=r(f,2),ne=a(_e);{let je=o(()=>!e(E)||e(H)<=R);at(ne,{label:"← 1 dot",get disabled(){return e(je)},onrun:()=>Z(_,-1)})}var qe=r(ne,2);{let je=o(()=>!e(E));at(qe,{label:"1 dot →",get disabled(){return e(je)},onrun:()=>Z(_,1)})}var Se=r(_e,2);{var ue=je=>{var st=Ml(),Ze=a(st);p(()=>i(Ze,e(P))),l(je,st)};m(Se,je=>{e(P)!==""&&je(ue)})}var W=r(Se,2),Ue=r(a(W),2),Ve=a(Ue),Le=r(W,2),Te=a(Le);{let je=o(()=>!e(E)||e(F)<=R);at(Te,{label:"↑ 1 dot",get disabled(){return e(je)},onrun:()=>Z(b,-1)})}var Je=r(Te,2);{let je=o(()=>!e(E));at(Je,{label:"1 dot ↓",get disabled(){return e(je)},onrun:()=>Z(b,1)})}var ae=r(Le,2);{var Ie=je=>{var st=Bl(),Ze=a(st);p(()=>i(Ze,e(te))),l(je,st)};m(ae,je=>{e(te)!==""&&je(Ie)})}var ve=r(ae,4),ze=a(ve),Re=r(ve,2),nt=a(Re),ut=r(Re,2);{var pt=je=>{var st=Vl();l(je,st)};m(ut,je=>{e(E)||je(pt)})}p((je,st)=>{Be(xe,"src",e(me)),i(L,je),i(Ve,st)},[()=>z(e(H)),()=>z(e(F))]),Qt("load",xe,()=>v(ye,"")),Qt("error",xe,()=>{Oe(e(me))}),et("change",ze,()=>pe()),za(ze,()=>e(se),je=>v(se,je)),et("change",nt,()=>pe()),za(nt,()=>e(be),je=>v(be,je)),l(x,re)},$$slots:{default:!0}});var Ee=r(ce,2);Qe(Ee,{title:"Gabarit et impression",children:(x,M)=>{var re=Gl(),he=K(re);{let d=o(()=>O(c)),w=o(()=>!e(E));_t(he,{label:"Gabarit",path:c,get value(){return e(we)},hint:"Le gabarit reproduit à l’identique s’appelle weighing_identical (A1).",get fault(){return e(d)},get disabled(){return e(w)},onchange:Y=>{t.draft.set(c,Y),pe()}})}var de=r(he,2);{let d=o(()=>t.draft.text("printer.options.darkness")),w=o(()=>O("printer.options.darkness")),Y=o(()=>!e(E));_t(de,{label:"Noircissement",path:"printer.options.darkness",kind:"number",get value(){return e(d)},hint:"Trop bas, l’étiquette pâlit au soleil ; trop haut, elle bave et le scanner refuse.",get fault(){return e(w)},get disabled(){return e(Y)},onchange:f=>g("printer.options.darkness",f)})}var He=r(de,2);{let d=o(()=>t.draft.text("printer.options.speed")),w=o(()=>O("printer.options.speed")),Y=o(()=>!e(E));_t(He,{label:"Vitesse",path:"printer.options.speed",kind:"number",get value(){return e(d)},get fault(){return e(w)},get disabled(){return e(Y)},onchange:f=>g("printer.options.speed",f)})}var Xe=r(He,2);{let d=o(()=>t.draft.text("printer.options.copies")),w=o(()=>O("printer.options.copies")),Y=o(()=>!e(E));_t(Xe,{label:"Exemplaires",path:"printer.options.copies",kind:"number",get value(){return e(d)},hint:"Un client repart avec une étiquette : deux exemplaires se justifient, pas se devinent.",get fault(){return e(w)},get disabled(){return e(Y)},onchange:f=>g("printer.options.copies",f)})}var xe=r(Xe,2);rt(xe,21,()=>G,d=>d.what,(d,w)=>{{let Y=o(()=>e(j)===e(w).what);at(d,{get act(){return e(w).what},get label(){return e(w).label},protected:!0,get busy(){return e(Y)},get disabled(){return e(y)},onrun:()=>{I(e(w).what)}})}}),l(x,re)},$$slots:{default:!0}}),l(n,J),Rt()}Tt(["change"]);var Kl=u(" "),Xl=u(' '),Yl=u(''),Ql=u('

                        Aucun tarif déclaré dans la configuration lue.

                        '),Zl=u('Prix du catalogue Odoo — pas de remise'),$a=u(' '),ei=u(' % ',1),ti=u(' '),ai=u(`

                        CodeLibelléAbrégéRemiseOrdre

                        Un champ vidé garde la valeur du fichier : il n’écrit pas zéro, et la case la - retrouve dès qu’on quitte le champ. Une remise effacée serait le plein tarif pour - tous les adhérents.

                        `,1),ri=u(" ",1),ni=u(`

                        Les deux arrondis sont distincts parce qu’ils tombent à des endroits différents du - calcul : arrondir le prix au kilo puis le multiplier, ou multiplier puis arrondir, ne - donne pas le même centime sur une étiquette. Le poste applique l’arrondi commercial, - et l’écart se voit au centime près sur la même pesée.

                        `,1),si=u('

                        Rien ne s’affiche au client : c’est une information.

                        '),li=u('

                        '),ii=u('
                        '),oi=u('

                        '),ui=u('
                      • '),ci=u(`

                        Aucun verdict en ce moment : ces lignes portent sur la pesée EN COURS, et le poste - est au repos. Elles apparaissent le temps d’un cycle, quand un client pose son sac - et touche une tuile.

                        `),di=u(' '),vi=u(' ',1),pi=u('
                      • '),fi=u('

                          ',1),gi=u(`

                          L’ordre compte : le premier verdict bloquant décide de ce que le client lit. Les - garde-fous 1 à 7 portent sur l’état de la balance, c’est-à-dire le poids brut ; les - garde-fous 8 à 14 portent sur la vente, c’est-à-dire le net. Le code en gris est - celui du journal, celui qu’on lit au téléphone.

                            Un seuil vidé garde la valeur du fichier : il n’écrit pas zéro, et la case la retrouve - dès qu’on quitte le champ. Pour changer un seuil, on tape l’autre valeur.

                            Ce que les garde-fous disent de la pesée en cours

                            `,1),mi=u(`

                            Le plan de numérotation n’est PAS ici, et c’est tout l’intérêt : préfixes, largeur de - la référence, largeur de la charge utile, décimales et mode de vente sont une - constante du binaire, indexée par préfixe et vérifiée au démarrage. - Un champ qui change le SENS du code lu par la caisse n’est pas un réglage, c’est un - contrat externe : il change avec une version du binaire, relue et testée, jamais - depuis l’écran d’un poste.

                            `,1),hi=u('

                            Aucune dérogation : la limite générale s’applique à tous les produits.

                            '),_i=u(`

                            Les noms de produits n’ont pas pu être lus : le catalogue en service n’a pas - répondu. Les identifiants Odoo restent affichés.

                            `),bi=u(`Produit retiré : le garde-fou 14 refuse le produit avant que cette - dérogation ait un sens.`),wi=u('
                          1. '),yi=u('

                              ',1),xi=u('
                              ');function ki(n,t){Ct(t,!0);const s=(g,O=sa,I=sa,J=sa)=>{var ce=Yl(),Ee=a(ce),x=r(Ee,2),M=a(x),re=a(M),he=r(M,2);{var de=xe=>{var d=Kl(),w=a(d);p(()=>i(w,O())),l(xe,d)};m(he,xe=>{Gt.showTechnicalNames&&xe(de)})}var He=r(he,2);{var Xe=xe=>{var d=Xl(),w=a(d);p(()=>i(w,J())),l(xe,d)};m(He,xe=>{J()!==""&&xe(Xe)})}p((xe,d)=>{Be(ce,"data-flag",O()),Be(ce,"data-on",xe),ua(Ee,d),i(re,I())},[()=>String(t.draft.flag(O())),()=>t.draft.flag(O())]),et("change",Ee,xe=>t.draft.set(O(),xe.currentTarget.checked)),l(g,ce)},c=20,_=o(()=>se(t.draft)),b=o(()=>t.health.state.diagnostics),R=o(()=>t.health.decisions.filter(g=>g.min_weight_g!==null)),N=o(()=>e(R).filter(g=>!g.offered)),$=o(()=>e(R).slice(0,c));let G=U(Lt({})),V=U("loading");me();function se(g){const O=g.value("pricing.tiers");return Array.isArray(O)?O.map(I=>{const J=I??{},ce=J.discount_percent;return{code:String(J.code??""),label:String(J.label??""),abbrev:String(J.abbrev??""),written:ce===void 0?null:String(ce),discount:be(ce)?ce:null,rank:Number(J.rank??0)}}):[]}function be(g){return typeof g!="number"||!Number.isFinite(g)||g<0||g>100?!1:Math.abs(g*10-Math.round(g*10))<1e-9}const ye=o(()=>String(t.draft.value("pricing.reference_code")??""));function ie(g){return g===null?"":String(g).replace(".",",")}function j(g){const O=1e3-Math.round(g*10);return`${String(Math.trunc(O/100))},${String(O%100).padStart(2,"0")}`}function oe(g,O){const I=Number(O);O.trim()===""||Number.isNaN(I)||t.draft.set(g,I)}function X(g,O){const I=O.trim().replace(",",".");if(!/^\d{1,3}(\.\d)?$/u.test(I))return;const J=Number(I);J>100||t.draft.set(g,J)}function we(g,O){!(g instanceof HTMLInputElement)||g.value===O||(g.value=O)}async function me(){try{const g=await Ba();v(G,Object.fromEntries(g.products.map(O=>[O.id,O.name])),!0),v(V,"read")}catch{v(V,"unread")}}function E(g){const O=e(G)[g];return O!==void 0?O:e(V)==="loading"?"Lecture du nom…":e(V)==="read"?"Produit absent du catalogue en service":"Nom non lu"}function H(g){return g<=1?`${String(g)} tarif déclaré`:`${String(g)} tarifs déclarés`}const F=o(()=>`${ge(e(b).length)} ${e(b).length>1?"verdicts":"verdict"} sur les quatorze garde-fous.`),P=o(()=>`${ge(e($).length)} ${e($).length>1?"lignes affichées":"ligne affichée"} sur ${ge(e(R).length)} `+(e(N).length===0?`${e(R).length>1?"dérogations en vigueur":"dérogation en vigueur"}.`:`${e(R).length>1?"dérogations enregistrées":"dérogation enregistrée"}, dont ${ge(e(N).length)} sur un produit retiré, sans effet : le garde-fou 14 refuse le produit avant que le 8 ait un sens.`)+(e(R).length>e($).length?" Les autres se lisent produit par produit depuis l’onglet Catalogue.":""));function te(g){return`plancher ${ge(g)} g : refusé à ${ge(g)} g et en dessous`}const T=[{rank:1,code:"OVERLOAD",label:"Surcharge",when:"La balance annonce elle-même OL, ou le poids brut dépasse la capacité.",severity:"Bloquant",blocking:!0,message:"La balance est en surcharge. Retirez votre article.",thresholds:[],switchPath:"",switchLabel:"",note:`Seuil : la capacité, réglée au garde-fou 9 sous « ${Ft("limits.max_weight_g")} ».`},{rank:2,code:"MEASUREMENT_EXPIRED",label:"Poids périmé",when:"La mesure est plus vieille que la péremption, dans les deux modes de stabilité.",severity:"Bloquant",blocking:!0,message:"Poids indisponible. Patientez ou appelez un bénévole.",thresholds:[],switchPath:"",switchLabel:"",note:"Aucun seuil à régler : le poste calcule lui-même à partir du rythme de la balance."},{rank:3,code:"BASKET_MISSING",label:"Panier absent",when:"Le poids brut tombe dans la fenêtre négative du panier : il a été soulevé.",severity:"Bloquant",blocking:!0,message:"Le panier n'est pas sur la balance. Reposez-le.",thresholds:[{path:"limits.basket_min_g",label:"Bas de la fenêtre du panier",hint:"En grammes, NÉGATIF : c’est le poids du panier que la balance a perdu."},{path:"limits.basket_max_g",label:"Haut de la fenêtre du panier",hint:"Négatif lui aussi, et plus proche de zéro que le bas."}],switchPath:"limits.basket_check_enabled",switchLabel:"Ce poste travaille avec un panier taré",note:"La règle s’active ou non, en bloc : il n’y a pas de demi-mesure à régler."},{rank:4,code:"SCALE_EMPTY",label:"Plateau vide",when:"Le poids brut ne sort pas de la bande « il n’y a rien sur le plateau ».",severity:"Bloquant — un filet, hors parcours nominal",blocking:!0,message:"Posez votre produit.",thresholds:[{path:"limits.empty_max_g",label:"Plateau considéré vide",hint:"En dessous, le poste considère qu’il n’y a rien sur le plateau."}],switchPath:"",switchLabel:"",note:"Toucher une tuile sur un plateau vide ARME la sélection au lieu d’être refusé : la règle reste évaluée pour la saisie manuelle et les chemins dérivés."},{rank:5,code:"TARE_REQUIRED",label:"Remise à zéro nécessaire",when:"Le brut est sous la bande du plateau vide, et hors de la fenêtre du panier.",severity:"Bloquant",blocking:!0,message:"La balance doit être remise à zéro.",thresholds:[],switchPath:"",switchLabel:"",note:`Seuil : la valeur négative de celui du garde-fou 4, « ${Ft("limits.empty_max_g")} ».`},{rank:6,code:"WEIGHT_UNSTABLE",label:"Pesée instable",when:"La trame déclare la mesure instable.",severity:"Information par défaut (A3)",blocking:!1,message:"Pesée en cours…",thresholds:[],switchPath:"",switchLabel:"",note:"La sévérité suit l’exigence de stabilité : elle passe à Bloquant quand celle-ci est réglée sur « blocking ». L’impression n’est jamais bloquée par défaut."},{rank:7,code:"TARE_INVALID",label:"Emballage incohérent",when:"Une tare a été saisie, et elle atteint la pesée ou dépasse le maximum.",severity:"Bloquant",blocking:!0,message:"Le poids de l'emballage est supérieur ou égal à la pesée.",thresholds:[{path:"limits.max_tare_g",label:"Tare maximum",hint:"Une tare plus lourde que le maximum est une faute de frappe."}],switchPath:"",switchLabel:"",note:""},{rank:8,code:"WEIGHT_TOO_LOW",label:"Poids trop faible",when:"Vente au poids : le NET est strictement positif et ne dépasse pas le plancher.",severity:"Bloquant",blocking:!0,message:"La balance doit être retarée, ou l'emballage est trop lourd.",thresholds:[{path:"limits.min_weight_g",label:"Poids minimum",hint:"Une dérogation par produit existe, dans l’onglet Catalogue."}],switchPath:"",switchLabel:"",note:""},{rank:9,code:"WEIGHT_TOO_HIGH",label:"Poids trop élevé",when:"Le NET dépasse la capacité — strictement, pour que la capacité reste atteignable.",severity:"Bloquant",blocking:!0,message:"{{.Weight}} kg, ça paraît un peu lourd !",thresholds:[{path:"limits.max_weight_g",label:"Poids maximum",hint:"C’est la capacité du champ NNDDD du code-barres, pas un seuil de vraisemblance."}],switchPath:"",switchLabel:"",note:""},{rank:10,code:"UNITS_OUT_OF_RANGE",label:"Nombre d’unités hors plage",when:"Vente à l’unité : la quantité sort de la plage.",severity:"Bloquant",blocking:!0,message:"{{.Quantity}} unités, ça paraît un peu beaucoup !",thresholds:[{path:"limits.min_units",label:"Unités minimum",hint:""},{path:"limits.max_units",label:"Unités maximum",hint:""}],switchPath:"",switchLabel:"",note:""},{rank:11,code:"AMOUNT_OUT_OF_CAPACITY",label:"Montant hors capacité du code-barres",when:"La charge utile encode un PRIX, et il dépasse ce que le champ peut porter.",severity:"Bloquant",blocking:!0,message:"Prix trop élevé pour le code-barres.",thresholds:[{path:"limits.max_amount_cents",label:"Montant maximum",hint:"En centimes. Aucun préfixe du plan livré n’encode un prix : la règle est éprouvée sans qu’aucun produit puisse l’atteindre."}],switchPath:"",switchLabel:"",note:""},{rank:12,code:"ZERO_PRICE",label:"Prix nul",when:"Le montant du tarif imprimé en grand vaut zéro.",severity:"Bloquant",blocking:!0,message:"Prix nul. Appelez un bénévole.",thresholds:[],switchPath:"",switchLabel:"",note:"Aucun seuil : un produit à 0 € est une anomalie sans nuance."},{rank:13,code:"LIGHT_PRODUCT_ALLOWED",label:"Produit léger autorisé",when:"Le garde-fou 8 n’a pas déclenché grâce à la dérogation du produit.",severity:"Information",blocking:!1,message:"",thresholds:[],switchPath:"",switchLabel:"",note:"Aucun seuil général : c’est la dérogation par produit, listée plus bas et posée depuis l’onglet Catalogue. Rien ne s’affiche au client ; l’id du produit est journalisé."},{rank:14,code:"PRODUCT_WITHDRAWN",label:"Produit retiré",when:"Quelqu’un a décidé de ne plus proposer ce produit.",severity:"Bloquant",blocking:!0,message:"Ce produit n'est pas disponible.",thresholds:[],switchPath:"",switchLabel:"",note:"Aucun seuil : c’est une décision humaine, prise depuis l’onglet Catalogue. Aucune règle d’import ne peut la déduire."}];function y(g){return T.find(O=>O.code===g)?.label??"Garde-fou inconnu de cet écran"}const C=["{{.Weight}}","{{.Quantity}}"];function D(g){return g==="blocking"?"Bloquant":g==="info"?"Information":"Sévérité inconnue de cet écran"}var Z=xi(),pe=a(Z);Qe(pe,{title:"Grille de tarifs",note:"Un second tarif n’est pas une case à cocher : c’est une ligne de plus dans cette grille.",children:(g,O)=>{var I=ri(),J=K(I);{var ce=re=>{var he=Ql();l(re,he)},Ee=re=>{var he=ai(),de=K(he),He=a(de),Xe=r(de,2),xe=a(Xe),d=r(a(xe));rt(d,21,()=>e(_),fa,(w,Y,f)=>{var q=ti(),L=a(q),_e=a(L),ne=r(L),qe=a(ne);Be(qe,"aria-label",`Libellé du tarif ${f+1}`);var Se=r(ne),ue=a(Se);Be(ue,"aria-label",`Abrégé du tarif ${f+1}`);var W=r(Se),Ue=a(W);{var Ve=ve=>{var ze=Zl();l(ve,ze)},Le=ve=>{var ze=$a(),Re=a(ze);p(()=>i(Re,`${e(Y).written??""} — le tarif de référence est le prix du catalogue : il - ne peut pas porter de remise, et l’enregistrement la refusera.`)),l(ve,ze)},Te=ve=>{var ze=$a(),Re=a(ze);p(()=>i(Re,`${e(Y).written??""} — une remise s’écrit au dixième de point ; celle-ci se - change dans le fichier de configuration.`)),l(ve,ze)},Je=ve=>{var ze=ei(),Re=K(ze);Be(Re,"aria-label",`Remise du tarif ${f+1}`);var nt=r(Re,2),ut=a(nt);p((pt,je)=>{na(Re,pt),i(ut,`un produit à 10,00 €/kg s’affiche ${je??""} €/kg`)},[()=>ie(e(Y).discount??0),()=>j(e(Y).discount??0)]),et("input",Re,pt=>X(`pricing.tiers.${String(f)}.discount_percent`,pt.currentTarget.value)),et("focusout",Re,pt=>we(pt.currentTarget,ie(e(Y).discount??0))),l(ve,ze)};m(Ue,ve=>{e(Y).code===e(ye)&&e(Y).written===null?ve(Ve):e(Y).code===e(ye)?ve(Le,1):e(Y).written!==null&&e(Y).discount===null?ve(Te,2):ve(Je,-1)})}var ae=r(W),Ie=a(ae);p(()=>{i(_e,e(Y).code),na(qe,e(Y).label),na(ue,e(Y).abbrev),i(Ie,e(Y).rank)}),et("input",qe,ve=>t.draft.set(`pricing.tiers.${String(f)}.label`,ve.currentTarget.value)),et("input",ue,ve=>t.draft.set(`pricing.tiers.${String(f)}.abbrev`,ve.currentTarget.value)),l(w,q)}),p(w=>i(He,`${w??""}.`),[()=>H(e(_).length)]),l(re,he)};m(J,re=>{e(_).length===0?re(ce):re(Ee,-1)})}var x=r(J,2);{let re=o(()=>t.draft.text("pricing.primary_code"));_t(x,{label:"Tarif imprimé en grand",path:"pricing.primary_code",get value(){return e(re)},hint:"Le prix que le client lit sur l’étiquette (A7).",onchange:he=>t.draft.set("pricing.primary_code",he)})}var M=r(x,2);{let re=o(()=>t.draft.text("pricing.reference_code"));_t(M,{label:"Tarif qui serait encodé si le code-barres portait un prix",path:"pricing.reference_code",get value(){return e(re)},hint:"Le plan livré ne porte pas de prix, mais un poids ou un nombre d’unités : la caisse retrouve le prix par la référence, dans Odoo, et ne doit jamais sous-facturer.",onchange:he=>t.draft.set("pricing.reference_code",he)})}l(g,I)},$$slots:{default:!0}});var Oe=r(pe,2);Qe(Oe,{title:"Les deux arrondis",children:(g,O)=>{var I=ni(),J=K(I);{let Ee=o(()=>t.draft.text("pricing.unit_price_rounding"));_t(J,{label:"Arrondi du prix unitaire dérivé",path:"pricing.unit_price_rounding",get value(){return e(Ee)},hint:"Il s’applique au prix au kilo calculé par la remise.",onchange:x=>t.draft.set("pricing.unit_price_rounding",x)})}var ce=r(J,2);{let Ee=o(()=>t.draft.text("pricing.amount_rounding"));_t(ce,{label:"Arrondi du montant",path:"pricing.amount_rounding",get value(){return e(Ee)},hint:"Il s’applique au montant de l’étiquette.",onchange:x=>t.draft.set("pricing.amount_rounding",x)})}l(g,I)},$$slots:{default:!0}});var Ae=r(Oe,2);Qe(Ae,{title:"Les quatorze garde-fous, dans l’ordre d’évaluation",note:"Le seuil se modifie ici. La sévérité ne se règle pas : elle dit si le poste refuse ou avertit, et cela ne dépend pas du magasin. Le message affiché au client n’est pas encore modifiable depuis cet écran.",children:(g,O)=>{var I=gi(),J=r(K(I),2);rt(J,21,()=>T,he=>he.code,(he,de)=>{var He=ui(),Xe=a(He),xe=a(Xe),d=a(xe),w=r(xe,2),Y=a(w),f=r(w,2),q=a(f),L=r(f,2),_e=a(L),ne=r(Xe,2),qe=a(ne),Se=r(ne,2);{var ue=ae=>{var Ie=si();l(ae,Ie)},W=ae=>{var Ie=li(),ve=a(Ie);p(()=>i(ve,`« ${e(de).message??""} »`)),l(ae,Ie)};m(Se,ae=>{e(de).message===""?ae(ue):ae(W,-1)})}var Ue=r(Se,2);{var Ve=ae=>{s(ae,()=>e(de).switchPath,()=>e(de).switchLabel,()=>"")};m(Ue,ae=>{e(de).switchPath!==""&&ae(Ve)})}var Le=r(Ue,2);rt(Le,17,()=>e(de).thresholds,ae=>ae.path,(ae,Ie)=>{var ve=ii(),ze=a(ve);{let Re=o(()=>t.draft.text(e(Ie).path));_t(ze,{get label(){return e(Ie).label},get path(){return e(Ie).path},kind:"number",get value(){return e(Re)},get hint(){return e(Ie).hint},onchange:nt=>oe(e(Ie).path,nt)})}et("focusout",ve,Re=>we(Re.target,t.draft.text(e(Ie).path))),l(ae,ve)});var Te=r(Le,2);{var Je=ae=>{var Ie=oi(),ve=a(Ie);p(()=>i(ve,e(de).note)),l(ae,Ie)};m(Te,ae=>{e(de).note!==""&&ae(Je)})}p(ae=>{Be(He,"data-code",e(de).code),i(d,e(de).rank),i(Y,e(de).label),i(q,e(de).code),Be(L,"data-blocking",ae),i(_e,e(de).severity),i(qe,e(de).when)},[()=>String(e(de).blocking)]),l(he,He)});var ce=r(J,4),Ee=a(ce),x=r(ce,4);{var M=he=>{var de=ci();l(he,de)},re=he=>{var de=fi(),He=K(de),Xe=a(He),xe=r(He,2);rt(xe,21,()=>e(b),d=>d.code,(d,w)=>{var Y=pi(),f=a(Y),q=a(f),L=r(f,2),_e=a(L),ne=r(L,2),qe=a(ne),Se=r(ne,2);{var ue=Ve=>{var Le=di(),Te=a(Le);p(()=>i(Te,`« ${e(w).message??""} »`)),l(Ve,Le)};m(Se,Ve=>{e(w).message!==""&&Ve(ue)})}var W=r(Se,2);{var Ue=Ve=>{var Le=vi(),Te=K(Le),Je=a(Te),ae=r(Te,2),Ie=a(ae);p(ve=>{i(Je,ve),i(Ie,e(w).product_id)},[()=>E(e(w).product_id)]),l(Ve,Le)};m(W,Ve=>{e(w).product_id!==""&&Ve(Ue)})}p((Ve,Le,Te)=>{Be(Y,"data-blocking",Ve),i(q,Le),i(_e,e(w).code),i(qe,Te)},[()=>String(e(w).blocking),()=>y(e(w).code),()=>D(e(w).severity)]),l(d,Y)}),p(d=>{Be(He,"data-verdicts",d),i(Xe,e(F))},[()=>String(e(b).length)]),l(he,de)};m(x,he=>{e(b).length===0?he(M):he(re,-1)})}p(he=>i(Ee,`Les marqueurs ${he??""} sont remplacés par les valeurs de la pesée au - moment où le message s’affiche.`),[()=>C.join(" et ")]),l(g,I)},$$slots:{default:!0}});var S=r(Ae,2);Qe(S,{title:"Code-barres",note:"Un seul réglage, et c’est voulu : le reste du plan de numérotation n’est pas de la configuration.",children:(g,O)=>{var I=mi(),J=K(I);s(J,()=>"barcode.verify_reference_check_digit",()=>"Refuser une référence dont la clé de contrôle est fausse",()=>"Décoché, le poste recalcule une clé juste sur une référence fausse, en silence — et la caisse encaisse un autre article."),l(g,I)},$$slots:{default:!0}});var z=r(S,2);Qe(z,{title:"Dérogations de poids minimum",note:"En lecture ici ; elles se modifient depuis l’onglet Catalogue, là où se trouve le produit.",children:(g,O)=>{var I=Dt(),J=K(I);{var ce=x=>{var M=hi();l(x,M)},Ee=x=>{var M=yi(),re=K(M),he=a(re),de=r(re,2);{var He=xe=>{var d=_i();l(xe,d)};m(de,xe=>{e(V)==="unread"&&xe(He)})}var Xe=r(de,2);rt(Xe,21,()=>e($),xe=>xe.product_id,(xe,d)=>{var w=wi(),Y=a(w),f=a(Y),q=r(Y,2),L=a(q),_e=r(q,2),ne=a(_e),qe=r(_e,2),Se=a(qe),ue=r(qe,2),W=a(ue),Ue=r(ue,2);{var Ve=Le=>{var Te=bi();l(Le,Te)};m(Ue,Le=>{e(d).offered||Le(Ve)})}p((Le,Te,Je,ae)=>{Be(w,"data-withdrawn",Le),i(f,Te),i(L,e(d).product_id),i(ne,Je),i(Se,e(d).reason),i(W,`${ae??""}, ${e(d).decided_by??""}`)},[()=>String(!e(d).offered),()=>E(e(d).product_id),()=>te(e(d).min_weight_g??0),()=>da(e(d).decided_at)]),l(xe,w)}),p(()=>i(he,e(P))),l(x,M)};m(J,x=>{e(R).length===0?x(ce):x(Ee,-1)})}l(g,I)},$$slots:{default:!0}}),l(n,Z),Rt()}Tt(["change","input","focusout"]);function qi(n,t){const s=[];for(const c of[...ya(n),...ya(t)]){if(s.some(R=>R.path===c))continue;const _=Da(va(n,c)),b=Da(va(t,c));_!==b&&s.push({path:c,before:_,after:b})}return s}function va(n,t){let s=n;for(const c of t.split(".")){if(s===null||typeof s!="object")return;s=s[c]}return s}function ya(n,t=""){if(n===null||typeof n!="object"||Array.isArray(n))return t===""?[]:[t];const s=[];for(const[c,_]of Object.entries(n))s.push(...ya(_,t===""?c:`${t}.${c}`));return s}function Da(n){return n===void 0?"—":n===null?"vide":typeof n=="boolean"?n?"oui":"non":typeof n=="object"?JSON.stringify(n):String(n)}var Si=u('
                              Empreinte de la configuration en service
                              Version du binaire
                              Répertoire de données
                              Espace disque
                              ',1),Li=u('

                              '),Ia=u(" "),Ei=u(' '),zi=u("
                            • "),ji=u(`

                                Recopier reste possible : les valeurs entrent dans le brouillon, où elles se - corrigent champ par champ avant l’enregistrement.

                                `),Pi=u(`

                                Ce fichier n’a été comparé à rien : la configuration en service n’a pas pu être - lue. Ni « identique », ni « n champs changent » — ce que ce fichier changerait - sur ce poste reste inconnu.

                                `),Ti=u(`

                                Ce fichier décrit la même configuration que celle en service : il n’y a rien à - recopier. C’est ce qu’on veut lire à la fin d’un clonage. Deux champs ne sont pas - comparés : la date du dernier enregistrement, que chaque poste écrit lui-même, et - le mot de passe du catalogue, qu’aucun des deux ne porte en clair.

                                `),Ci=u(' '),Ri=u('

                                '),Oi=u(`

                                ChampEn serviceDans le fichier

                                Recopier n’applique rien : les valeurs entrent dans le brouillon, et c’est - « Enregistrer » qui les met en service.

                                `,1),Ai=u('

                                ',1),Ni=u(`

                                L’export emporte encore l’empreinte du mot de passe : c’est la seule lecture que le - poste garde derrière la clé. L’import, lui, est lu PAR LE POSTE, qui écarte les deux - secrets et le numéro de poste avant de dire ce qui changerait.

                                `,1),$i=u('

                                Lecture des versions enregistrées…

                                '),Di=u(`

                                Les versions enregistrées n’ont pas pu être lues : cette liste ne dit donc rien de - ce que ce poste garde. Ce n’est pas « aucune version ».

                                `),Ii=u('

                                Aucune version enregistrée : ce poste n’a jamais été reconfiguré.

                                '),Ui=u('
                              • '),Fi=u(`

                                  Remettre une version en service remplace la configuration du poste sur-le-champ, et - ce qui n’a pas été enregistré est perdu : c’est le seul geste de cette page qui - change le poste tout de suite, et le seul qui garde ses 72 px.

                                  `,1),Mi=u('
                                  ');function Bi(n,t){Ct(t,!0);const s=vt(t,"admin",7),c=40,_=20,b=5,R=new Set(["modified_at","catalog.options.password"]),N=[{path:"station.name",name:"le nom du poste"},{path:"network.listen",name:"l’adresse d’écoute"},{path:"scale.options.port",name:"le port de la balance"},{path:"printer.options.queue",name:"la file d’impression"},{path:"catalog.options.url",name:"l’adresse du partage"},{path:"catalog.options.username",name:"le compte du partage"},{path:"catalog.images.path",name:"le chemin des images"}];let $=U(Lt([])),G=U("reading"),V=U(null),se=U(""),be=U(null),ye=U(""),ie=U(Lt([])),j=U(""),oe="";const X=o(()=>s().busy||e(j)!==""),we=o(()=>D(e(V),e(be))),me=o(()=>e(V)!==null&&e(be)!==null),E=o(()=>Z(e(V),e(be))),H=o(()=>e(we).slice(0,c)),F=o(()=>e(ie).slice(0,_)),P=o(()=>e($).slice(0,b)),te=o(()=>Xe(e(H).length,e(we).length,"champ qui change","champs qui changent")),T=o(()=>Xe(e(F).length,e(ie).length,"contrôle refuse une clé","contrôles refusent une clé")),y=o(()=>Xe(e(P).length,e($).length,"version enregistrée","versions enregistrées")),C=o(()=>e(we).length>1?`Recopier ces ${ge(e(we).length)} champs dans le brouillon`:"Recopier ce champ dans le brouillon");Ae();function D(f,q){return f===null||q===null?[]:qi(f,q).filter(L=>!R.has(L.path))}function Z(f,q){if(f===null||q===null)return[];const L=f,_e=q;return N.filter(ne=>pe(_e,ne.path)&&!pe(L,ne.path))}function pe(f,q){const L=va(f,q);return L==null||L===""?!0:typeof L!="object"||Array.isArray(L)?!1:Object.keys(L).length===0}function Oe(f){return f.length<2?f.join(""):`${f.slice(0,-1).join(", ")} et ${f[f.length-1]??""}`}aa(()=>{const f=t.health.config_fingerprint;f!==oe&&(oe=f,S())});async function Ae(){const f=await s().load(Ar);if(f===null){v(G,"unreadable");return}v($,f,!0),v(G,"read")}async function S(){try{v(V,(await qa()).config,!0),v(se,"")}catch(f){v(V,null),v(se,f instanceof Error?f.message:"Le poste n’a pas répondu.",!0)}}async function z(f){v(j,f?"export-all":"export-clone",!0),s().notice="",s().actionError="";try{const q=await s().protect(()=>g(f));if(q===null)return;J(q.name,q.blob),s().notice=`${q.name} est remis au navigateur : c’est lui qui l’enregistre, voyez ses téléchargements.`}finally{v(j,"")}}async function g(f){const L=await fetch(`/admin/api/config/export?hardware=${f?"1":"0"}`,{headers:{accept:"application/json"}});if(!L.ok)throw new It(L.status,he(await L.text(),"L’export"));return{name:O(f),blob:await L.blob()}}function O(f){const q=f?"":"-sans-materiel";return`config-poste${String(t.health.station)}${q}-${I()}.json`}function I(){const f=new Date,q=String(f.getMonth()+1).padStart(2,"0"),L=String(f.getDate()).padStart(2,"0");return`${String(f.getFullYear())}-${q}-${L}`}function J(f,q){const L=URL.createObjectURL(q),_e=document.createElement("a");_e.href=L,_e.download=f,document.body.appendChild(_e),_e.click(),_e.remove(),setTimeout(()=>{URL.revokeObjectURL(L)},0)}async function ce(f){const q=await fetch("/admin/api/config/import",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(f)}),L=await q.text();if(!q.ok)throw new It(q.status,he(L,"L’import"));return JSON.parse(L)}async function Ee(f){if(f==null)return;s().notice="",s().actionError="";let q;try{q=JSON.parse(await f.text())}catch{s().actionError=`${f.name} n’est pas un fichier JSON lisible.`;return}v(j,"import");try{const L=await s().protect(()=>ce(q));if(L===null)return;v(be,L.config,!0),v(ye,f.name,!0),v(ie,L.faults,!0),await S(),s().notice=x(f.name)}finally{v(j,"")}}function x(f){return e(me)?e(we).length===0?`${f} décrit la même configuration que celle en service.`:`${f} est lu. Rien n’est appliqué : relisez le tableau.`:`${f} est lu, mais la configuration en service ne l’est pas : rien ne peut être comparé.`}async function M(){if(e(be)===null)return;const f=e(be),q=[...e(we)];for(const L of q)t.draft.set(L.path,va(f,L.path));s().actionError="",s().notice="Le fichier est recopié dans le brouillon. Rien n’est appliqué avant « Enregistrer ».",await S()}async function re(f){v(j,`restore-${String(f)}`),s().notice="",s().actionError="";try{if(await s().protect(()=>Nr(f))===null)return;await t.draft.load(),await Ae(),await S(),await s().refresh(),s().notice=`La version ${String(f)} est remise en service.`}finally{v(j,"")}}function he(f,q){try{const L=JSON.parse(f);if(typeof L?.message=="string"&&L.message!=="")return L.message}catch{}return`${q} a été refusé par le poste.`}function de(f){return t.draft.faults.find(q=>q.field===f)?.message??""}function He(f){return t.draft.faults.find(q=>q.field===f)?.allowed??[]}function Xe(f,q,L,_e){const ne=q>1?_e:L;return f>=q?`${ge(q)} ${ne}.`:`${ge(f)} lignes affichées sur ${ge(q)} ${ne}.`}var xe=Mi(),d=a(xe);Qe(d,{title:"Identité du poste",children:(f,q)=>{var L=Si(),_e=K(L);{let ze=o(()=>t.draft.text("station.number")),Re=o(()=>de("station.number")),nt=o(()=>He("station.number"));_t(_e,{label:"Numéro du poste",path:"station.number",kind:"number",get value(){return e(ze)},get fault(){return e(Re)},get allowed(){return e(nt)},hint:"C’est de lui que dérive le nom du fichier de catalogue attendu, flv_.csv.",onchange:ut=>t.draft.set("station.number",Number(ut))})}var ne=r(_e,2);{let ze=o(()=>t.draft.text("station.name")),Re=o(()=>de("station.name")),nt=o(()=>He("station.name"));_t(ne,{label:"Nom du poste",path:"station.name",get value(){return e(ze)},get fault(){return e(Re)},get allowed(){return e(nt)},hint:"Ce que lit un bénévole : « Poste 2 — fruits ».",onchange:ut=>t.draft.set("station.name",ut)})}var qe=r(ne,2);{let ze=o(()=>t.draft.text("station.coop")),Re=o(()=>de("station.coop")),nt=o(()=>He("station.coop"));_t(qe,{label:"Coopérative",path:"station.coop",get value(){return e(ze)},get fault(){return e(Re)},get allowed(){return e(nt)},onchange:ut=>t.draft.set("station.coop",ut)})}var Se=r(qe,2),ue=r(a(Se),2),W=a(ue),Ue=r(ue,4),Ve=a(Ue),Le=r(Ue,4),Te=a(Le),Je=r(Le,4),ae=a(Je);{var Ie=ze=>{var Re=yt("non mesuré");l(ze,Re)},ve=ze=>{var Re=yt();p((nt,ut,pt)=>i(Re,`${nt??""} libres sur - ${ut??""} — seuil d’alerte - ${pt??""} Mo`),[()=>wa(t.health.disk.free_bytes),()=>wa(t.health.disk.total_bytes),()=>ge(t.health.disk.alert_mb)]),l(ze,Re)};m(ae,ze=>{t.health.disk===null?ze(Ie):ze(ve,-1)})}p(()=>{i(W,t.health.config_fingerprint),i(Ve,t.health.version),i(Te,t.health.disk===null?"non publié par ce poste":t.health.disk.path)}),l(f,L)},$$slots:{default:!0}});var w=r(d,2);Qe(w,{title:"Exporter, importer",note:"Pour installer un autre poste : ce fichier emporte les tarifs, les garde-fous, l’étiquette, les catégories, et les réglages du matériel que les quatre postes partagent — le décalage d’étiquette, le noircissement, la vitesse, le débit de la balance. Reste ici ce qui désigne ce poste-ci ou ce magasin — le mot de passe, le code de secours, le numéro et le nom du poste, le port de la balance, la file d’impression, l’adresse du partage et son compte, le chemin des images et le réseau.",children:(f,q)=>{var L=Ni(),_e=K(L),ne=a(_e);{let ae=o(()=>e(j)==="export-all");at(ne,{label:"Exporter tout",protected:!0,get busy(){return e(ae)},get disabled(){return e(X)},onrun:()=>{z(!0)}})}var qe=r(ne,2);{let ae=o(()=>e(j)==="export-clone");at(qe,{label:"Exporter sans le matériel",protected:!0,get busy(){return e(ae)},get disabled(){return e(X)},onrun:()=>{z(!1)}})}var Se=r(qe,2);let ue;var W=a(Se),Ue=r(W,3),Ve=r(_e,4);{var Le=ae=>{var Ie=Li(),ve=a(Ie);p(()=>i(ve,`La configuration en service n’a pas pu être lue : ${e(se)??""} La colonne - « En service » ne peut donc rien affirmer.`)),l(ae,Ie)};m(Ve,ae=>{e(se)!==""&&ae(Le)})}var Te=r(Ve,2);{var Je=ae=>{var Ie=Ai(),ve=K(Ie),ze=a(ve),Re=r(ve,2);{var nt=Ze=>{var bt=ji(),Ot=a(bt),Ut=a(Ot),k=r(Ot,2);rt(k,21,()=>e(F),fa,(Q,le)=>{var Ne=zi(),We=a(Ne),Ye=a(We),Ke=r(We,2);{var B=ke=>{var De=Ia(),Pe=a(De);p(()=>i(Pe,e(le).field)),l(ke,De)};m(Ke,ke=>{Gt.showTechnicalNames&&ke(B)})}var ee=r(Ke),fe=r(ee);{var $e=ke=>{var De=Ei(),Pe=a(De);p(Fe=>i(Pe,`Valeurs acceptées : ${Fe??""}.`),[()=>e(le).allowed.join(", ")]),l(ke,De)};m(fe,ke=>{e(le).allowed!==void 0&&e(le).allowed.length>0&&ke($e)})}p(ke=>{i(Ye,ke),i(ee,` ${e(le).message??""} `)},[()=>Ft(e(le).field)]),l(Q,Ne)}),p(()=>i(Ut,`Ce fichier serait refusé en l’état : ${e(T)??""}`)),l(Ze,bt)};m(Re,Ze=>{e(ie).length>0&&Ze(nt)})}var ut=r(Re,2);{var pt=Ze=>{var bt=Pi();l(Ze,bt)},je=Ze=>{var bt=Ti();l(Ze,bt)},st=Ze=>{var bt=Oi(),Ot=K(bt),Ut=a(Ot),k=r(Ut);{var Q=fe=>{var $e=yt("Les autres sont dans le fichier, et « Recopier » les prend tous.");l(fe,$e)};m(k,fe=>{e(we).length>e(H).length&&fe(Q)})}var le=r(Ot,2),Ne=a(le),We=r(a(Ne));rt(We,21,()=>e(H),fe=>fe.path,(fe,$e)=>{const ke=o(()=>Ft(e($e).path));var De=Ci(),Pe=a(De),Fe=a(Pe),Ge=r(Fe);{var ft=gt=>{var xt=Ia(),Nt=a(xt);p(()=>i(Nt,e($e).path)),l(gt,xt)};m(Ge,gt=>{Gt.showTechnicalNames&&e(ke)!==e($e).path&>(ft)})}var ot=r(Pe),ct=a(ot),it=r(ot),dt=a(it);p(()=>{Be(De,"data-path",e($e).path),i(Fe,`${e(ke)??""} `),i(ct,e($e).before),i(dt,e($e).after)}),l(fe,De)});var Ye=r(le,2);{var Ke=fe=>{var $e=Ri(),ke=a($e);p(De=>i(ke,`Ce fichier ne porte pas ${De??""} : - l’export sans le matériel les retire, et l’import ne remet que le numéro du - poste. Les lignes correspondantes sont VIDES ci-dessus, et - « Recopier » recopie ce vide dans le brouillon.`),[()=>Oe(e(E).map(De=>De.name))]),l(fe,$e)};m(Ye,fe=>{e(E).length>0&&fe(Ke)})}var B=r(Ye,2),ee=a(B);at(ee,{kind:"write",get label(){return e(C)},get disabled(){return e(X)},onrun:()=>{M()}}),p(()=>i(Ut,`${e(te)??""} `)),l(Ze,bt)};m(ut,Ze=>{e(me)?e(we).length===0?Ze(je,1):Ze(st,-1):Ze(pt)})}p(()=>i(ze,`Fichier lu : ${e(ye)??""}`)),l(ae,Ie)};m(Te,ae=>{e(be)!==null&&ae(Je)})}p(()=>{ue=Ht(Se,1,"choose svelte-18wbxwu",null,ue,{working:e(j)==="import",off:e(X)}),i(W,`${e(j)==="import"?"Lecture du fichier…":"Importer un fichier"} `),Ue.disabled=e(X)}),et("change",Ue,ae=>{Ee(ae.currentTarget.files?.item(0))}),l(f,L)},$$slots:{default:!0}});var Y=r(w,2);Qe(Y,{title:"Cinq versions restaurables",note:"Chaque enregistrement fait tourner les versions : la plus récente est la 1.",children:(f,q)=>{var L=Dt(),_e=K(L);{var ne=W=>{var Ue=$i();l(W,Ue)},qe=W=>{var Ue=Di();l(W,Ue)},Se=W=>{var Ue=Ii();l(W,Ue)},ue=W=>{var Ue=Fi(),Ve=K(Ue),Le=a(Ve),Te=r(Ve,2),Je=a(Te);rt(Je,21,()=>e(P),ae=>ae.version,(ae,Ie)=>{var ve=Ui(),ze=a(ve),Re=a(ze),nt=r(ze,2),ut=a(nt),pt=r(nt,2),je=a(pt),st=r(pt,2);{let Ze=o(()=>e(j)===`restore-${String(e(Ie).version)}`);at(st,{kind:"destructive",label:"Remettre cette version en service",protected:!0,get busy(){return e(Ze)},get disabled(){return e(X)},onrun:()=>{re(e(Ie).version)}})}p((Ze,bt)=>{i(Re,`version ${Ze??""}`),i(ut,bt),i(je,e(Ie).config_fingerprint)},[()=>ge(e(Ie).version),()=>Mt(e(Ie).modified_at)]),l(ae,ve)}),p(()=>i(Le,`${e(y)??""} Le poste n’en garde jamais plus de cinq.`)),l(W,Ue)};m(_e,W=>{e(G)==="reading"?W(ne):e(G)==="unreadable"?W(qe,1):e($).length===0?W(Se,2):W(ue,-1)})}l(f,L)},$$slots:{default:!0}}),l(n,xe),Rt()}Tt(["change"]);var Vi=u('clé'),Wi=u(' '),Gi=u('');function Xt(n,t){const s=vt(t,"kind",3,"read"),c=vt(t,"hint",3,""),_=vt(t,"disabled",3,!1),b=vt(t,"engaged",3,!1),R=vt(t,"busy",3,!1),N=vt(t,"protected",3,!1);var $=Gi();let G;var V=a($),se=a(V),be=r(se);{var ye=oe=>{var X=Vi();l(oe,X)};m(be,oe=>{N()&&oe(ye)})}var ie=r(V,2);{var j=oe=>{var X=Wi(),we=a(X);p(()=>i(we,R()?"En cours…":c())),l(oe,X)};m(ie,oe=>{c()!==""&&oe(j)})}p(()=>{G=Ht($,1,`big touch-target ${s()??""}`,"svelte-15nlt6i",G,{engaged:b(),busy:R()}),Be($,"data-kind",s()),$.disabled=_()||R(),i(se,`${t.label??""} `)}),et("click",$,function(...oe){t.onrun?.apply(this,oe)}),l(n,$)}Tt(["click"]);const Hi="ERR-SCL-09";var Ji=u('

                                  '),Ki=u('

                                  '),Xi=u('

                                  Déposez ici le fichier clé

                                  '),Yi=u('

                                  ',1),Qi=u('');function Zi(n,t){Ct(t,!0);const s=vt(t,"admin",7);let c=U("");const _=o(()=>t.health.state.degraded?.code===Hi),b=o(()=>t.health.printing),R=o(()=>e(b)?.on_fallback===!0);let N=U(!1),$=U("");const G=o(()=>s().busy||e($)!=="");async function V(S,z){v(c,""),v($,S,!0);try{await s().run(z)}finally{v($,"")}}async function se(S,z){v(c,""),v($,S,!0);try{const g=await s().load(z);g!==null&&v(c,g.message,!0)}finally{v($,"")}}async function be(S,z){v(c,""),v($,S,!0);try{const g=await s().protect(z);g!==null&&(s().notice=g.message,await s().refresh())}finally{v($,"")}}async function ye(S){if(v(N,!1),S!=null){v(c,""),v($,"import");try{const z=await s().protect(()=>Ga(S));if(z===null)return;v(c,z.result==="rejected"||z.result==="failed"?`${S.name} : REFUSÉ${z.reason===""?"":" — "+z.reason}. Le catalogue en service n’a pas changé.`:`${S.name} : ${String(z.rows_read_count)} lignes lues, ${String(z.weighable_count)} pesables. La veille l’appliquera dans la seconde.`,!0),await s().refresh()}finally{v($,"")}}}var ie=Qi(),j=a(ie);{var oe=S=>{var z=Ji(),g=a(z);p(()=>i(g,e(c))),l(S,z)};m(j,S=>{e(c)!==""&&S(oe)})}var X=r(j,2);{var we=S=>{var z=Ki(),g=a(z);p(()=>i(g,e(b).banner)),l(S,z)};m(X,S=>{e(b)!==null&&e(b).banner!==""&&S(we)})}var me=r(X,2),E=a(me);{let S=o(()=>e($)==="scale");Xt(E,{label:"Tester la balance",hint:"Ce que le poste a déjà observé — le port n’est pas rouvert.",get busy(){return e(S)},get disabled(){return e(G)},onrun:()=>{se("scale",Lr)}})}var H=r(E,2);{let S=o(()=>e($)==="printer");Xt(H,{label:"Tester l’imprimante",hint:"Ce que le superviseur a vu il y a moins d’une seconde.",get busy(){return e(S)},get disabled(){return e(G)},onrun:()=>{se("printer",Er)}})}var F=r(H,2);{let S=o(()=>e($)==="label");Xt(F,{label:"Imprimer une étiquette de test",hint:"Une étiquette de démonstration sort de l’imprimante du poste.",get busy(){return e(S)},get disabled(){return e(G)},onrun:()=>{V("label",zr)}})}var P=r(F,2);{let S=o(()=>e($)==="reprint");Xt(P,{label:"Réimprimer la dernière",hint:"La dernière étiquette imprimée sort une seconde fois.",get busy(){return e(S)},get disabled(){return e(G)},onrun:()=>{V("reprint",yr)}})}var te=r(P,2);{let S=o(()=>e($)==="reload");Xt(te,{label:"Recharger le catalogue",kind:"write",hint:"La veille refait tout de suite le contrôle qu’elle fait toutes les cinq secondes.",get busy(){return e(S)},get disabled(){return e(G)},onrun:()=>{V("reload",xr)}})}var T=r(te,2);{let S=o(()=>e(_)?"Revenir à la balance":"Basculer en saisie manuelle"),z=o(()=>e(_)?"Le poids sera de nouveau lu sur la balance.":"Le poids se tape à la main : le poste continue de servir sans balance."),g=o(()=>e($)==="manual");Xt(T,{get label(){return e(S)},kind:"destructive",get hint(){return e(z)},get engaged(){return e(_)},protected:!0,get busy(){return e(g)},get disabled(){return e(G)},onrun:()=>{be("manual",()=>kr(!e(_)))}})}var y=r(T,2);{let S=o(()=>e($)==="roll");Xt(y,{label:"J’ai changé le rouleau",kind:"write",hint:"Le compteur d’étiquettes repart à zéro. C’est le seul geste qui dise quelque chose de vrai du papier.",get busy(){return e(S)},get disabled(){return e(G)},onrun:()=>{V("roll",qr)}})}var C=r(y,2);{var D=S=>{{let z=o(()=>e(R)?"Revenir à l’imprimante du poste":"Imprimer sur l’imprimante du poste voisin"),g=o(()=>e(R)?"Les étiquettes repartiront sur l’imprimante de ce poste.":"Les étiquettes sortiront sur l’imprimante voisine, pour cette session seulement."),O=o(()=>e($)==="fallback");Xt(S,{get label(){return e(z)},kind:"write",get hint(){return e(g)},get engaged(){return e(R)},get busy(){return e(O)},get disabled(){return e(G)},onrun:()=>{V("fallback",()=>Sr(!e(R)))}})}};m(C,S=>{e(b)!==null&&e(b).fallback_available&&S(D)})}var Z=r(C,2),pe=r(me,2);Qe(pe,{title:"Importer un catalogue",note:"Glissez le fichier CSV ici, ou choisissez-le. Il passe par le même chemin que le fichier du producteur.",children:(S,z)=>{var g=Xi();let O;var I=a(g),J=r(a(I)),ce=a(J),Ee=r(I,2),x=r(a(Ee));p(()=>{O=Ht(g,1,"drop svelte-1yd0nzg",null,O,{dropping:e(N)}),i(ce,`flv_${t.health.station??""}.csv`)}),Qt("dragover",g,M=>{M.preventDefault(),v(N,!0)}),Qt("dragleave",g,()=>v(N,!1)),Qt("drop",g,M=>{M.preventDefault(),ye(M.dataTransfer?.files.item(0))}),et("change",x,M=>{ye(M.currentTarget.files?.item(0))}),l(S,g)},$$slots:{default:!0}});var Oe=r(pe,2);{var Ae=S=>{Qe(S,{title:"Le dernier fichier a été refusé",children:(z,g)=>{var O=Yi(),I=K(O),J=a(I),ce=r(I,2),Ee=a(ce);p(x=>{i(J,`${(t.health.catalog.reason===""?"Aucun motif enregistré.":t.health.catalog.reason)??""} - Le catalogue en service n’a pas changé.`),i(Ee,`Dernier essai : ${x??""}.`)},[()=>Mt(t.health.catalog.occurred_at)]),l(z,O)},$$slots:{default:!0}})};m(Oe,S=>{t.health.catalog!==null&&t.health.catalog.result==="rejected"&&S(Ae)})}p(()=>Be(Z,"href",jr)),l(n,ie),Rt()}Tt(["change"]);const eo={succeeded:"La dernière mise à jour a réussi.","rolled-back":"La dernière mise à jour a échoué. La version précédente a été remise et le poste fonctionne.","rolled-back-unhealthy":"La dernière mise à jour a échoué et le poste n’a pas redémarré. Appelez le support.","not-started":"La dernière mise à jour n’a pas démarré : rien n’a été remplacé. Vous pouvez réessayer."},to=2e3,ao=300*1e3;var ro=u('

                                  '),Ua=u('

                                  '),no=u("
                                  Dernière vérification
                                  ",1),so=u(`

                                  La mise à jour depuis cet écran n’existe que sur les postes Windows. Sur les - autres, elle se fait à la main — voir la notice d’installation.

                                  `),lo=u('Voir les nouveautés'),io=u('

                                  Version disponible : .

                                  '),oo=u('

                                  Ce poste est à jour.

                                  '),uo=u("
                                  Version installée
                                  Dépôt suivi
                                  ",1),co=u(`

                                  Le poste va s’arrêter environ une minute. L’écran client s’éteindra puis reviendra - tout seul. Si la nouvelle version ne démarre pas, la précédente sera remise - automatiquement — mais les données enregistrées, elles, ne reviendront pas en - arrière.

                                  `),vo=u('

                                  Mise à jour en cours. Le poste redémarre, ne le débranchez pas.

                                  '),po=u('
                                  ',1),fo=u("
                                  Terminée le
                                  ",1),go=u("
                                  Raison
                                  ",1),mo=u('

                                  Depuis
                                  Vers
                                  ',1),ho=u(" ",1);function _o(n,t){Ct(t,!0);let s=U(null),c=U(""),_=U(""),b=U(!1),R=U("");const N=o(()=>e(s)?.outcome??null),$=o(()=>e(s)?.supported===!0&&e(s).available),G=o(()=>e(s)?.latest??"");aa(()=>{V()});async function V(){try{v(s,await Yr(),!0),v(c,"")}catch(E){v(c,E instanceof Error?E.message:"Lecture impossible.",!0)}}async function se(){v(_,"check");try{const E=await t.admin.protect(()=>Qr());E!==null&&(v(s,E,!0),v(c,""))}finally{v(_,"")}}async function be(E){v(_,"apply"),v(R,"");try{if(await t.admin.protect(()=>Zr(E))===null)return;v(b,!0),v(R,await ye()?"":"Le poste n’a pas répondu dans les cinq minutes. Allez le voir.",!0),await V()}finally{v(_,""),v(b,!1)}}async function ye(){const E=Date.now()+ao;for(;Date.now()setTimeout(H,to));try{if((await fetch("/healthz",{cache:"no-store"})).ok)return!0}catch{}}return!1}var ie=ho(),j=K(ie);Qe(j,{title:"Version de ce poste",children:(E,H)=>{var F=Dt(),P=K(F);{var te=y=>{var C=ro(),D=a(C);p(()=>i(D,e(c)===""?"Lecture…":e(c))),l(y,C)},T=y=>{var C=uo(),D=K(C);{var Z=x=>{var M=Ua(),re=a(M);p(()=>i(re,e(c))),l(x,M)};m(D,x=>{e(c)!==""&&x(Z)})}var pe=r(D,2),Oe=r(a(pe),2),Ae=a(Oe),S=r(Oe,4),z=a(S),g=r(S,2);{var O=x=>{var M=no(),re=r(K(M),2),he=a(re);p(de=>i(he,de),[()=>Mt(e(s).checked_at)]),l(x,M)};m(g,x=>{e(s).checked_at!==""&&x(O)})}var I=r(pe,2);{var J=x=>{var M=so();l(x,M)},ce=x=>{var M=io(),re=r(a(M)),he=a(re),de=r(re,2);{var He=d=>{var w=yt();p(Y=>i(w,`, publiée le ${Y??""}`),[()=>Mt(e(s).published_at)]),l(d,w)};m(de,d=>{e(s).published_at!==""&&d(He)})}var Xe=r(de,2);{var xe=d=>{var w=lo();p(()=>Be(w,"href",e(s).html_url)),l(d,w)};m(Xe,d=>{e(s).html_url!==""&&d(xe)})}p(()=>i(he,e(s).latest)),l(x,M)},Ee=x=>{var M=oo();l(x,M)};m(I,x=>{e(s).supported?e(s).available?x(ce,1):x(Ee,-1):x(J)})}p(()=>{i(Ae,e(s).running),i(z,e(s).repository)}),l(y,C)};m(P,y=>{e(s)===null?y(te):y(T,-1)})}l(E,F)},$$slots:{default:!0}});var oe=r(j,2);{var X=E=>{Qe(E,{title:"Installer",children:(H,F)=>{var P=po(),te=K(P),T=a(te);{let z=o(()=>e(_)==="check");at(T,{kind:"read",label:"Vérifier maintenant",protected:!0,act:"check",get busy(){return e(z)},get disabled(){return e(b)},onrun:()=>{se()}})}var y=r(T,2);{var C=z=>{{let g=o(()=>`Installer la version ${e(G)}`),O=o(()=>e(_)==="apply");at(z,{kind:"destructive",get label(){return e(g)},protected:!0,act:"apply",get busy(){return e(O)},onrun:()=>{be(e(G))}})}};m(y,z=>{e($)&&z(C)})}var D=r(te,2);{var Z=z=>{var g=co();l(z,g)};m(D,z=>{e($)&&z(Z)})}var pe=r(D,2);{var Oe=z=>{var g=vo();l(z,g)};m(pe,z=>{e(b)&&z(Oe)})}var Ae=r(pe,2);{var S=z=>{var g=Ua(),O=a(g);p(()=>i(O,e(R))),l(z,g)};m(Ae,z=>{e(R)!==""&&z(S)})}l(H,P)},$$slots:{default:!0}})};m(oe,E=>{e(s)!==null&&e(s).supported&&E(X)})}var we=r(oe,2);{var me=E=>{Qe(E,{title:"Dernière tentative",children:(H,F)=>{var P=mo(),te=K(P),T=a(te),y=r(te,2),C=r(a(y),2),D=a(C),Z=r(C,4),pe=a(Z),Oe=r(Z,2);{var Ae=g=>{var O=fo(),I=r(K(O),2),J=a(I);p(ce=>i(J,ce),[()=>Mt(e(N).finished_at)]),l(g,O)};m(Oe,g=>{e(N).finished_at!==""&&g(Ae)})}var S=r(Oe,2);{var z=g=>{var O=go(),I=r(K(O),2),J=a(I);p(()=>i(J,e(N).reason)),l(g,O)};m(S,g=>{e(N).reason!==""&&g(z)})}p(()=>{Be(te,"data-outcome",e(N).status),i(T,eo[e(N).status]),i(D,e(N).from===""?"—":e(N).from),i(pe,e(N).to===""?"—":e(N).to)}),l(H,P)},$$slots:{default:!0}})};m(we,E=>{e(N)!==null&&E(me)})}l(n,ie),Rt()}var bo=u(''),wo=u('

                                  ',1),yo=u('

                                  ',1),xo=u(''),ko=u(''),qo=u(''),So=u(''),Lo=u(' '),Eo=u(''),zo=u('

                                  Lecture de l’état du poste…

                                  '),jo=u('

                                  '),Po=u(" "),To=u("
                                • "),Co=u('
                                    '),Ro=u('
                                    '),Oo=u('

                                    ');function Ao(n,t){Ct(t,!0);const s=new pn,c=new en(s),_=[{title:"Au quotidien",pages:[{id:"dashboard",label:"Tableau de bord"},{id:"troubleshooting",label:"Dépannage"}]},{title:"Réglages",pages:[{id:"hardware",label:"Matériel"},{id:"label",label:"Étiquette"},{id:"rules",label:"Règles"},{id:"catalog",label:"Catalogue"},{id:"journal",label:"Journal"},{id:"station",label:"Poste"},{id:"update",label:"Mise à jour"}]}],b=o(()=>_.flatMap(d=>d.pages).find(d=>d.id===s.page)?.label??""),R=o(()=>s.health===null||s.health.station_name===""?`Poste ${String(s.health?.station??"")}`:s.health.station_name),N=o(()=>(c.pending?.changed_blocks??[]).map(d=>nn(d)).join(", ")),$=o(()=>(c.pending?.changed_blocks??[]).join(", "));ka(()=>(s.start(),()=>s.stop()));async function G(d){s.open(d),Ca(d)&&c.config===null&&await c.load()}async function V(){await s.protect(()=>c.save())===!0&&(s.notice="La configuration est enregistrée et appliquée.")}var se=Oo(),be=a(se),ye=r(a(be),2);rt(ye,17,()=>_,d=>d.title,(d,w)=>{var Y=wo(),f=K(Y),q=a(f),L=r(f,2);rt(L,17,()=>e(w).pages,_e=>_e.id,(_e,ne)=>{var qe=bo();let Se;var ue=a(qe);p(()=>{Se=Ht(qe,1,"entry svelte-9b2mjq",null,Se,{current:s.page===e(ne).id}),Be(qe,"aria-current",s.page===e(ne).id?"page":void 0),i(ue,e(ne).label)}),et("click",qe,()=>{G(e(ne).id)}),l(_e,qe)}),p(()=>i(q,e(w).title)),l(d,Y)});var ie=r(ye,2),j=a(ie);{var oe=d=>{var w=yo(),Y=K(w),f=a(Y),q=r(Y,2),L=a(q),_e=r(q,2),ne=a(_e);p(()=>{i(f,e(R)),i(L,`${s.health.coop??""} · version ${s.health.version??""}`),i(ne,`configuration ${s.health.config_fingerprint??""}`)}),l(d,w)};m(j,d=>{s.health!==null&&d(oe)})}var X=r(j,2),we=a(X),me=r(X,2);{var E=d=>{var w=xo();et("click",w,function(...Y){t.onclose?.apply(this,Y)}),l(d,w)};m(me,d=>{t.onclose!==void 0&&d(E)})}var H=r(be,2),F=a(H);{var P=d=>{var w=ko(),Y=a(w);p(()=>i(Y,s.notice)),l(d,w)};m(F,d=>{s.notice!==""&&d(P)})}var te=r(F,2);{var T=d=>{var w=qo(),Y=a(w);p(()=>i(Y,s.linkError)),l(d,w)};m(te,d=>{s.linkError!==""&&d(T)})}var y=r(te,2);{var C=d=>{var w=So(),Y=a(w);p(()=>i(Y,s.actionError)),l(d,w)};m(y,d=>{s.actionError!==""&&d(C)})}var D=r(y,2);{var Z=d=>{var w=Eo(),Y=a(w),f=a(Y),q=r(f);{var L=qe=>{var Se=Lo(),ue=a(Se);p(()=>i(ue,e($))),l(qe,Se)};m(q,qe=>{Gt.showTechnicalNames&&qe(L)})}var _e=r(q),ne=r(Y,2);at(ne,{kind:"write",label:"Tout fonctionne : confirmer",onrun:()=>{s.protect(()=>c.confirm())}}),p(()=>{i(f,`Configuration appliquée mais NON CONFIRMÉE. Ce qui a changé : ${e(N)??""}. `),i(_e,` Le poste reviendra tout seul à la version précédente dans - ${c.pending.seconds_left??""} secondes si personne ne confirme.`)}),l(d,w)};m(D,d=>{c.pending!==null&&d(Z)})}var pe=r(D,2),Oe=a(pe),Ae=a(Oe),S=r(Oe,2);{var z=d=>{var w=zo();l(d,w)},g=d=>{ol(d,{get health(){return s.health},onshowrows:()=>{G("catalog")},onshowupdate:()=>{G("update")}})},O=d=>{Zi(d,{get admin(){return s},get health(){return s.health}})},I=d=>{wl(d,{get admin(){return s},get draft(){return c},get health(){return s.health}})},J=d=>{Jl(d,{get admin(){return s},get draft(){return c}})},ce=d=>{ki(d,{get draft(){return c},get health(){return s.health}})},Ee=d=>{ys(d,{get admin(){return s},get draft(){return c},get health(){return s.health}})},x=d=>{Il(d,{get admin(){return s}})},M=d=>{Bi(d,{get admin(){return s},get draft(){return c},get health(){return s.health}})},re=d=>{_o(d,{get admin(){return s}})};m(S,d=>{s.health===null?d(z):s.page==="dashboard"?d(g,1):s.page==="troubleshooting"?d(O,2):s.page==="hardware"?d(I,3):s.page==="label"?d(J,4):s.page==="rules"?d(ce,5):s.page==="catalog"?d(Ee,6):s.page==="journal"?d(x,7):s.page==="station"?d(M,8):s.page==="update"&&d(re,9)})}var he=r(pe,2);{var de=d=>{var w=Ro(),Y=a(w);{var f=ne=>{var qe=jo(),Se=a(qe),ue=r(Se);rt(ue,16,()=>c.retired,W=>W,(W,Ue)=>{{let Ve=o(()=>`retirer ${Ue}`);at(W,{kind:"write",get label(){return e(Ve)},onrun:()=>c.dropRetired(Ue)})}}),p(W=>i(Se,`Ce fichier porte des réglages que cette version du poste ne connaît plus : - ${W??""}. `),[()=>c.retired.map(W=>Ft(W)).join(", ")]),l(ne,qe)};m(Y,ne=>{c.retired.length>0&&ne(f)})}var q=r(Y,2);{var L=ne=>{var qe=Co();rt(qe,21,()=>c.faults,Se=>Se.field,(Se,ue)=>{var W=To(),Ue=a(W),Ve=a(Ue),Le=r(Ue,2);{var Te=ve=>{var ze=Po(),Re=a(ze);p(()=>i(Re,e(ue).field)),l(ve,ze)};m(Le,ve=>{Gt.showTechnicalNames&&ve(Te)})}var Je=r(Le),ae=r(Je);{var Ie=ve=>{var ze=yt();p(Re=>i(ze,`— valeurs acceptées : ${Re??""}`),[()=>e(ue).allowed.join(", ")]),l(ve,ze)};m(ae,ve=>{e(ue).allowed!==void 0&&e(ue).allowed.length>0&&ve(Ie)})}p(ve=>{i(Ve,ve),i(Je,` ${e(ue).message??""} `)},[()=>Ft(e(ue).field)]),l(Se,W)}),l(ne,qe)};m(q,ne=>{c.faults.length>0&&ne(L)})}var _e=r(q,2);{let ne=o(()=>c.dirty?"Enregistrer la configuration":"Aucune modification à enregistrer"),qe=o(()=>!c.dirty||s.busy);at(_e,{kind:"write",get label(){return e(ne)},get disabled(){return e(qe)},onrun:()=>{V()}})}l(d,w)},He=o(()=>Ca(s.page)&&c.config!==null);m(he,d=>{e(He)&&d(de)})}var Xe=r(H,2);{var xe=d=>{wn(d,{get admin(){return s}})};m(Xe,d=>{s.pending!==null&&d(xe)})}p(()=>{ua(we,Gt.showTechnicalNames),i(Ae,e(b))}),et("change",we,()=>Gt.toggleTechnicalNames()),l(n,se),Rt()}Tt(["click","change"]);let oa=null;function Do(n){if(oa!==null||n.querySelector("[data-admin]")!==null)return;const t=document.createElement("div");n.appendChild(t),oa={component:fr(Ao,{target:t,props:{onclose:No}}),host:t}}function No(){if(oa===null)return;const{component:n,host:t}=oa;oa=null,gr(n),t.remove()}export{No as closeAdmin,Do as mountAdmin}; diff --git a/internal/web/dist/assets/mount-C1tpIbdA.css b/internal/web/dist/assets/mount-C1tpIbdA.css deleted file mode 100644 index a4a6c96..0000000 --- a/internal/web/dist/assets/mount-C1tpIbdA.css +++ /dev/null @@ -1 +0,0 @@ -.act.svelte-5tpq0o{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;min-height:2.75rem;padding:0 1rem;font-size:1.0625rem;font-weight:700;border-radius:var(--radius-sm);box-shadow:var(--shadow-1);transition:background-color var(--tap) var(--ease),border-color var(--tap) var(--ease),box-shadow var(--slide) var(--ease)}.read.svelte-5tpq0o{color:var(--ink);background:var(--surface);border:1px solid var(--border)}.write.svelte-5tpq0o{color:var(--surface);background:var(--action);border:1px solid var(--action)}.destructive.svelte-5tpq0o{min-height:var(--touch-min);color:var(--surface);background:var(--danger);border:1px solid var(--danger)}@media(hover:hover){.read.svelte-5tpq0o:hover:not(:disabled){border-color:var(--ink-muted);box-shadow:var(--shadow-2)}.write.svelte-5tpq0o:hover:not(:disabled){border-color:var(--action)}.destructive.svelte-5tpq0o:hover:not(:disabled){border-color:var(--danger)}.write.svelte-5tpq0o:hover:not(:disabled),.destructive.svelte-5tpq0o:hover:not(:disabled){box-shadow:var(--shadow-2);filter:brightness(.92)}}.act.svelte-5tpq0o:disabled{opacity:.5;box-shadow:none;cursor:default}.act.busy.svelte-5tpq0o:disabled{opacity:1}.key.svelte-5tpq0o{padding:.0625rem .375rem;border-radius:var(--radius-pill);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;background:var(--bg);color:var(--ink-muted)}.write.svelte-5tpq0o .key:where(.svelte-5tpq0o),.destructive.svelte-5tpq0o .key:where(.svelte-5tpq0o){background:var(--surface);color:var(--ink)}.scrim.svelte-zyka3t{position:fixed;inset:0;z-index:95;display:flex;align-items:center;justify-content:center;padding:2rem;background:#1c1b1973}.panel.svelte-zyka3t{display:flex;flex-direction:column;gap:.75rem;width:min(34rem,100%);padding:1.75rem;background:var(--surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-2)}header.svelte-zyka3t{display:flex;align-items:center;gap:.75rem}.glyph.svelte-zyka3t{display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;flex:none;border-radius:var(--radius-sm);background:var(--bg);color:var(--ink-muted)}h2.svelte-zyka3t{margin:0;font-size:1.375rem;line-height:1.2}.why.svelte-zyka3t,.how.svelte-zyka3t{margin:0;color:var(--ink-muted);font-size:1rem;line-height:1.4}.field.svelte-zyka3t{display:flex;flex-direction:column;gap:.375rem;font-size:1rem;color:var(--ink-muted)}input.svelte-zyka3t{height:2.75rem;padding:0 .75rem;font:inherit;font-size:1.125rem;color:var(--ink);background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm)}input.svelte-zyka3t:focus-visible{outline:3px solid var(--focus);outline-offset:1px}.refusal.svelte-zyka3t{margin:0;padding:.625rem .75rem;border-left:.25rem solid var(--fault);background:var(--fault-wash);font-size:1rem}footer.svelte-zyka3t{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.25rem}.field.svelte-1oilv5s{display:flex;flex-direction:column;gap:.25rem;padding:.5rem 0}label.svelte-1oilv5s{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}.name.svelte-1oilv5s{font-size:1.125rem;font-weight:700}code.svelte-1oilv5s{font-size:.9375rem;color:var(--ink-muted)}input.svelte-1oilv5s{min-height:3rem;padding:0 .75rem;font:inherit;font-variant-numeric:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.refused.svelte-1oilv5s input:where(.svelte-1oilv5s){border-left:.5rem solid var(--fault)}.hint.svelte-1oilv5s,.fault.svelte-1oilv5s{margin:0;font-size:1rem;color:var(--ink-muted)}.fault.svelte-1oilv5s{padding-left:.5rem;border-left:.25rem solid var(--fault)}.inventory.svelte-tuh4jc{display:flex;flex-direction:column;gap:.5rem}.headline.svelte-tuh4jc{margin:0;font-size:1.375rem;font-weight:700}dl.svelte-tuh4jc{margin:0;display:flex;flex-direction:column;gap:.25rem}.row.svelte-tuh4jc{display:flex;align-items:baseline;gap:.75rem}dt.svelte-tuh4jc{flex:none;width:4.5rem;text-align:right;font-size:1.375rem;font-weight:700}dd.svelte-tuh4jc{margin:0;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem;font-size:1.125rem}.label.svelte-tuh4jc{font-weight:700}.note.svelte-tuh4jc{color:var(--ink-muted)}.rows.svelte-tuh4jc{padding:0 .5rem;min-height:var(--touch-min);text-decoration:underline;color:var(--ink-muted)}.oneline.svelte-tuh4jc{margin:0;font-size:1rem;color:var(--ink-muted)}.panel.svelte-ww3z5u{padding:1rem 1.25rem 1.25rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}h2.svelte-ww3z5u{margin:0 0 .5rem;font-size:1.5rem;font-weight:700}.note.svelte-ww3z5u{margin:0 0 .75rem;font-size:1rem;color:var(--ink-muted)}.pages.svelte-3gk7u7{display:flex;flex-direction:column;gap:1rem}.fact.svelte-3gk7u7{margin:.5rem 0;font-size:1.125rem}.muted.svelte-3gk7u7{color:var(--ink-muted);font-size:1rem}.actions.svelte-3gk7u7{display:flex;flex-wrap:wrap;gap:var(--touch-gap);margin:.75rem 0 0}.key.svelte-3gk7u7{padding:.0625rem .375rem;border-radius:var(--radius-pill);background:var(--bg);color:var(--ink-muted);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase}.scroll.svelte-3gk7u7{max-height:24rem;overflow:auto;border:1px solid var(--border-soft);border-radius:var(--radius-sm);background:var(--bg)}.rows.svelte-3gk7u7{margin:0;padding:0 .75rem;list-style:none}.rows.svelte-3gk7u7 li:where(.svelte-3gk7u7){display:flex;flex-wrap:wrap;gap:.75rem;align-items:baseline;padding:.375rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.rows.svelte-3gk7u7 li:where(.svelte-3gk7u7):first-child{border-top:none}.line.svelte-3gk7u7,.id.svelte-3gk7u7,.value.svelte-3gk7u7{color:var(--ink-muted)}.line.svelte-3gk7u7{flex:none;width:7rem}.what.svelte-3gk7u7{font-weight:700}.message.svelte-3gk7u7{flex:1 1 20rem}.pick.svelte-3gk7u7{height:2.75rem;padding:0 .5rem;font-weight:700;color:var(--ink);text-decoration:underline;border-radius:var(--radius-sm);transition:background-color var(--tap) var(--ease)}@media(hover:hover){.pick.svelte-3gk7u7:hover:not(:disabled){background:var(--surface)}}.search.svelte-3gk7u7,label.svelte-3gk7u7{display:block;margin-top:.5rem;font-size:1.0625rem;font-weight:700}.choice.svelte-3gk7u7{display:flex;flex-direction:column}.choice.svelte-3gk7u7 label:where(.svelte-3gk7u7){display:flex;gap:.75rem;align-items:center;min-height:2.75rem;margin:0;font-weight:400}.choice.svelte-3gk7u7 input:where(.svelte-3gk7u7){width:1.5rem;height:1.5rem;min-height:0;flex:0 0 auto;padding:0;background:none;border:none;border-radius:0}input.svelte-3gk7u7{min-height:2.75rem;width:100%;max-width:34rem;padding:0 .75rem;font:inherit;font-variant-numeric:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm)}.decision.svelte-3gk7u7{margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--border)}.act-block.svelte-3gk7u7{margin-top:1rem;padding:.75rem 1rem 1rem;background:var(--bg);border-radius:var(--radius)}.act-block.svelte-3gk7u7 .what:where(.svelte-3gk7u7){margin:0;font-size:1.0625rem;font-weight:700}.drop.svelte-3gk7u7{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding:1rem;border:2px dashed var(--border);border-radius:var(--radius);transition:border-color var(--tap) var(--ease),background-color var(--tap) var(--ease)}.drop.dropping.svelte-3gk7u7{border-color:var(--focus);background:var(--waiting-wash)}.drop.working.svelte-3gk7u7{border-color:var(--waiting);background:var(--waiting-wash)}.drop.svelte-3gk7u7 p:where(.svelte-3gk7u7){margin:0;font-size:1.125rem}.choose.svelte-3gk7u7{display:inline-flex;align-items:center;margin:0;padding:0 1rem;font-size:1.125rem;font-weight:700;color:var(--surface);background:var(--danger);border:1px solid var(--danger);border-radius:var(--radius-sm);box-shadow:var(--shadow-1);cursor:pointer;transition:transform var(--tap) var(--ease),border-color var(--tap) var(--ease)}.choose.svelte-3gk7u7:active{transform:scale(.975)}@media(prefers-reduced-motion:reduce){.choose.svelte-3gk7u7:active{transform:none}}@media(hover:hover){.choose.svelte-3gk7u7:hover{filter:brightness(.92)}}.choose.svelte-3gk7u7 input:where(.svelte-3gk7u7){display:none}table.svelte-3gk7u7{border-collapse:collapse;width:100%;font-size:1.0625rem}th.svelte-3gk7u7,td.svelte-3gk7u7{padding:.375rem .5rem;text-align:left;white-space:nowrap;border-bottom:1px solid var(--border)}.why.svelte-3gk7u7{white-space:normal;min-width:16rem;color:var(--ink-muted)}th.svelte-3gk7u7{position:sticky;top:0;color:var(--ink-muted);font-size:1rem;background:var(--bg)}.light.svelte-1421wa8{display:flex;flex-direction:column;gap:.375rem;padding:.875rem 1rem;background:var(--surface);border:1px solid var(--border);border-left:.5rem solid var(--waiting);border-radius:var(--radius)}.light[data-level=ok].svelte-1421wa8{border-left-color:var(--ready)}.light[data-level=warn].svelte-1421wa8{border-left-color:var(--warning)}.light[data-level=fault].svelte-1421wa8{border-left-color:var(--fault)}header.svelte-1421wa8{display:flex;align-items:center;gap:.5rem}h3.svelte-1421wa8{margin:0;font-size:1.25rem;font-weight:700}.dot.svelte-1421wa8{width:1rem;height:1rem;border-radius:50%;background:var(--waiting);flex:none}.dot[data-level=ok].svelte-1421wa8{background:var(--ready)}.dot[data-level=warn].svelte-1421wa8{background:var(--warning)}.dot[data-level=fault].svelte-1421wa8{background:var(--fault)}.dot[data-level=unknown].svelte-1421wa8,.dot[data-level=off].svelte-1421wa8{background:transparent;box-shadow:inset 0 0 0 3px var(--waiting)}.verdict.svelte-1421wa8{margin-left:auto;font-size:1rem;font-weight:700;color:var(--ink-muted);letter-spacing:.03em}.value.svelte-1421wa8{margin:0;font-size:1.125rem}.remedy.svelte-1421wa8{margin:0;font-size:1rem;color:var(--ink-muted)}.dashboard.svelte-1w44m0y{display:flex;flex-direction:column;gap:1rem}.link.svelte-1w44m0y{padding:0;font:inherit;color:inherit;text-decoration:underline;background:none;border:none;cursor:pointer}.lights.svelte-1w44m0y{display:grid;grid-template-columns:repeat(auto-fit,minmax(20rem,1fr));grid-auto-rows:1fr;gap:var(--touch-gap)}.fact.svelte-1w44m0y{margin:0 0 .5rem;font-size:1.125rem;overflow-wrap:break-word}.remedy.svelte-1w44m0y,.count.svelte-1w44m0y{color:var(--ink-muted);font-size:1rem}.restart.svelte-1w44m0y strong:where(.svelte-1w44m0y){letter-spacing:.03em}.restart[data-verdict=missing].svelte-1w44m0y{margin-bottom:.25rem;padding:.5rem .75rem;border-left:.25rem solid var(--warning);background:var(--warning-wash)}.restart[data-verdict=unknown].svelte-1w44m0y{margin-bottom:.25rem;padding:.5rem .75rem;border-left:.25rem solid var(--waiting);background:var(--waiting-wash)}.decisions.svelte-1w44m0y,.events.svelte-1w44m0y{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:.375rem}.decisions.svelte-1w44m0y li:where(.svelte-1w44m0y),.events.svelte-1w44m0y li:where(.svelte-1w44m0y){display:flex;flex-wrap:wrap;gap:.75rem;align-items:baseline;padding:.375rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.what.svelte-1w44m0y,.message.svelte-1w44m0y{font-weight:700}.detail.svelte-1w44m0y,.time.svelte-1w44m0y,.source.svelte-1w44m0y,.code.svelte-1w44m0y{color:var(--ink-muted)}.events.svelte-1w44m0y li[data-level=error]:where(.svelte-1w44m0y){border-left:.25rem solid var(--fault);padding-left:.5rem}.events.svelte-1w44m0y li[data-level=warn]:where(.svelte-1w44m0y){border-left:.25rem solid var(--warning);padding-left:.5rem}.identity.svelte-1w44m0y{margin:0;display:grid;grid-template-columns:auto 1fr;gap:.25rem 1rem;font-size:1.125rem}.identity.svelte-1w44m0y dt:where(.svelte-1w44m0y){color:var(--ink-muted)}.identity.svelte-1w44m0y dd:where(.svelte-1w44m0y){margin:0;font-weight:700}.pages.svelte-1p4i0xm{display:flex;flex-direction:column;gap:1rem}.panel.svelte-1p4i0xm{padding:1rem 1.25rem 1.25rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1)}.head.svelte-1p4i0xm{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;justify-content:space-between}h2.svelte-1p4i0xm{margin:0;font-size:1.5rem;font-weight:700}.standing.svelte-1p4i0xm{display:inline-flex;align-items:center;gap:.5rem;height:2rem;padding:0 .75rem;border-radius:var(--radius-pill);background:var(--waiting-wash);font-size:1rem;font-weight:700}.standing[data-level=ok].svelte-1p4i0xm{background:var(--ready-wash)}.standing[data-level=warn].svelte-1p4i0xm{background:var(--warning-wash)}.standing[data-level=fault].svelte-1p4i0xm{background:var(--fault-wash)}.dot.svelte-1p4i0xm{width:.625rem;height:.625rem;border-radius:var(--radius-pill);background:var(--waiting)}.standing[data-level=ok].svelte-1p4i0xm .dot:where(.svelte-1p4i0xm){background:var(--ready)}.standing[data-level=warn].svelte-1p4i0xm .dot:where(.svelte-1p4i0xm){background:var(--warning)}.standing[data-level=fault].svelte-1p4i0xm .dot:where(.svelte-1p4i0xm){background:var(--fault)}.fact.svelte-1p4i0xm{margin:.75rem 0 0;font-size:1.125rem}.note.svelte-1p4i0xm,.count.svelte-1p4i0xm,.waiting.svelte-1p4i0xm{margin:.25rem 0 0;font-size:1rem;color:var(--ink-muted)}.check.svelte-1p4i0xm{display:flex;gap:.75rem;align-items:center;min-height:2.75rem;margin-top:.5rem;font-size:1.0625rem}.check.svelte-1p4i0xm input:where(.svelte-1p4i0xm){width:1.5rem;height:1.5rem;flex:0 0 auto}.check.svelte-1p4i0xm small:where(.svelte-1p4i0xm){display:block;font-size:1rem;color:var(--ink-muted)}.actions.svelte-1p4i0xm{display:flex;flex-wrap:wrap;gap:.5rem;margin:.75rem 0 0}@media(hover:hover){.pick.svelte-1p4i0xm:hover,summary.svelte-1p4i0xm:hover{background:var(--bg)}}.list.svelte-1p4i0xm{margin:.25rem 0 0;padding:0;list-style:none;display:flex;flex-direction:column}.list.svelte-1p4i0xm li:where(.svelte-1p4i0xm){display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;padding:.125rem 0;border-top:1px solid var(--border-soft);font-size:1.0625rem}.list.svelte-1p4i0xm li.refused:where(.svelte-1p4i0xm){padding-left:.5rem;border-left:.25rem solid var(--fault);background:var(--fault-wash)}.pick.svelte-1p4i0xm{height:2.75rem;padding:0 .75rem;font-weight:700;text-decoration:underline;border-radius:var(--radius-sm);transition:background-color var(--tap) var(--ease)}.what.svelte-1p4i0xm{font-weight:700}.detail.svelte-1p4i0xm{color:var(--ink-muted)}.folded.svelte-1p4i0xm{margin-top:1rem;border:1px solid var(--border);border-radius:var(--radius)}summary.svelte-1p4i0xm{display:flex;align-items:center;height:2.75rem;padding:0 .75rem;font-size:1.0625rem;font-weight:700;border-radius:var(--radius);cursor:pointer;transition:background-color var(--tap) var(--ease),transform var(--tap) var(--ease)}summary.svelte-1p4i0xm:active{transform:scale(.975)}@media(prefers-reduced-motion:reduce){summary.svelte-1p4i0xm:active{transform:none}}.frames.svelte-1p4i0xm{margin-top:1rem;max-height:18rem;overflow-y:auto;background:var(--waiting-wash);border:1px solid var(--border);border-radius:var(--radius)}.frames-head.svelte-1p4i0xm{position:sticky;top:0;margin:0;padding:.5rem .75rem;background:var(--surface);border-radius:var(--radius) var(--radius) 0 0;box-shadow:var(--shadow-2);font-size:1rem;color:var(--ink-muted)}.interrupted.svelte-1p4i0xm{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;margin:0;padding:.5rem .75rem;background:var(--warning-wash);border-left:.375rem solid var(--warning);font-size:1rem}.frame-rows.svelte-1p4i0xm{margin:0;padding:.5rem .75rem;list-style:none;display:flex;flex-direction:column;gap:.25rem}.frame-rows.svelte-1p4i0xm li:where(.svelte-1p4i0xm){display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:.75rem;font-size:.9375rem}.hex.svelte-1p4i0xm,.decoded.svelte-1p4i0xm{overflow-x:auto;white-space:pre}.decoded.svelte-1p4i0xm{color:var(--ink-muted)}div.journal.svelte-86o8oz{display:flex;flex-direction:column;gap:1rem;max-width:none;--reading-column: 68rem}.filters.svelte-86o8oz{display:flex;flex-wrap:wrap;gap:var(--touch-gap);align-items:center;margin-bottom:.75rem;font-size:1.0625rem}.filters.svelte-86o8oz label:where(.svelte-86o8oz){font-weight:700}select.svelte-86o8oz{min-height:2.75rem;padding:0 .5rem;font:inherit;font-variant-numeric:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm)}.export.svelte-86o8oz{display:inline-flex;align-items:center;gap:.5rem;height:2.75rem;padding:0 1rem;font-size:1.0625rem;font-weight:700;text-decoration:none;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-1);transition:transform var(--tap) var(--ease),border-color var(--tap) var(--ease),box-shadow var(--slide) var(--ease)}.export.svelte-86o8oz:active{transform:scale(.975)}@media(hover:hover){.export.svelte-86o8oz:hover{border-color:var(--ink-muted);box-shadow:var(--shadow-2)}}.replay.svelte-86o8oz{margin-top:.5rem}.fact.svelte-86o8oz{margin:.5rem 0;max-width:var(--reading-column);font-size:1.125rem}.muted.svelte-86o8oz{color:var(--ink-muted);font-size:1rem}.table-box.svelte-86o8oz,.lines-box.svelte-86o8oz{overflow:auto;background:var(--bg);border:1px solid var(--border-soft);border-radius:var(--radius-sm)}.table-box.svelte-86o8oz{max-height:34rem}.lines-box.svelte-86o8oz{max-height:28rem}table.svelte-86o8oz{border-collapse:collapse;min-width:100%;font-size:1.0625rem}th.svelte-86o8oz,td.svelte-86o8oz{padding:.375rem .5rem;text-align:left;white-space:nowrap;border-bottom:1px solid var(--border)}th.svelte-86o8oz{position:sticky;top:0;z-index:1;color:var(--ink-muted);font-size:1rem;background:var(--bg)}tbody.svelte-86o8oz tr:where(.svelte-86o8oz){transition:background-color var(--tap) var(--ease)}@media(hover:hover){tbody.svelte-86o8oz tr:where(.svelte-86o8oz):hover{background:var(--surface)}}tbody.svelte-86o8oz tr.open:where(.svelte-86o8oz){background:var(--surface);font-weight:700}tbody.svelte-86o8oz tr.open:where(.svelte-86o8oz)>td:where(.svelte-86o8oz):first-child{box-shadow:inset .1875rem 0 0 0 var(--focus)}.pick.svelte-86o8oz{height:2.75rem;padding:0 .5rem;color:var(--ink);text-decoration:underline;border-radius:var(--radius-sm);transition:background-color var(--tap) var(--ease)}@media(hover:hover){.pick.svelte-86o8oz:hover:not(:disabled){background:var(--surface)}}.detail-row.svelte-86o8oz{background:var(--surface)}.detail-cell.svelte-86o8oz{padding:.75rem 1rem 1rem;white-space:normal;box-shadow:inset .1875rem 0 0 0 var(--focus)}.detail-cell.svelte-86o8oz h3:where(.svelte-86o8oz){margin:0 0 .5rem;font-size:1.25rem}.detail-cell.svelte-86o8oz dl:where(.svelte-86o8oz){display:grid;grid-template-columns:auto 1fr;gap:.25rem 1rem;max-width:var(--reading-column);margin:0;font-size:1.0625rem;font-weight:400}.detail-cell.svelte-86o8oz dt:where(.svelte-86o8oz){color:var(--ink-muted)}.detail-cell.svelte-86o8oz dd:where(.svelte-86o8oz){margin:0}.detail-cell.svelte-86o8oz code:where(.svelte-86o8oz){overflow-wrap:anywhere}.lines.svelte-86o8oz{margin:0;padding:0 .75rem;list-style:none}.lines.svelte-86o8oz li:where(.svelte-86o8oz){display:flex;flex-wrap:wrap;gap:.75rem;align-items:baseline;padding:.375rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.lines.svelte-86o8oz li:where(.svelte-86o8oz):first-child{border-top:none}.lines.svelte-86o8oz li[data-level=error]:where(.svelte-86o8oz),.lines.svelte-86o8oz li[data-level=critical]:where(.svelte-86o8oz){border-left:.25rem solid var(--fault);padding-left:.5rem}.lines.svelte-86o8oz li[data-level=warn]:where(.svelte-86o8oz){border-left:.25rem solid var(--warning);padding-left:.5rem}.when.svelte-86o8oz,.level.svelte-86o8oz,.from.svelte-86o8oz,.code.svelte-86o8oz,.detail-text.svelte-86o8oz{color:var(--ink-muted)}.level.svelte-86o8oz{flex:none;width:8rem}.message.svelte-86o8oz{flex:1 1 20rem;font-weight:700}.sr-only.svelte-86o8oz{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.pages.svelte-llv1r8{display:flex;flex-direction:column;gap:1rem}.stale.svelte-llv1r8{margin:0 0 .75rem;padding:.5rem .75rem;background:var(--warning-wash);border-left:.375rem solid var(--warning);border-radius:var(--radius-sm);font-size:1.0625rem}.preview.svelte-llv1r8{display:flex;flex-wrap:wrap;gap:1.5rem;align-items:flex-start}.sheet.svelte-llv1r8{display:flex;flex-direction:column;gap:.5rem;min-width:0;padding:.75rem;background:var(--waiting-wash);border-radius:var(--radius-lg)}img.svelte-llv1r8{width:40rem;max-width:100%;image-rendering:pixelated;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-1)}.refused.svelte-llv1r8{margin:0;padding:.5rem .75rem;background:var(--fault-wash);border-left:.375rem solid var(--fault);border-radius:var(--radius-sm);font-size:1.0625rem}.nudges.svelte-llv1r8{display:flex;flex-direction:column;gap:.5rem;min-width:0}.axis.svelte-llv1r8{display:flex;flex-wrap:wrap;gap:.5rem;align-items:baseline;margin:0;font-size:1.125rem}.pair.svelte-llv1r8{display:flex;gap:var(--touch-gap)}.fault.svelte-llv1r8{margin:0;padding-left:.5rem;border-left:.25rem solid var(--fault);font-size:1rem;color:var(--ink-muted)}.check.svelte-llv1r8{display:flex;gap:.75rem;align-items:center;min-height:2.75rem;font-size:1.0625rem}.check.svelte-llv1r8 input:where(.svelte-llv1r8){width:1.5rem;height:1.5rem;flex:0 0 auto}.truncation.svelte-llv1r8,.absent.svelte-llv1r8,.waiting.svelte-llv1r8,.bound.svelte-llv1r8,.cost.svelte-llv1r8{margin:1rem 0 0;font-size:1rem;color:var(--ink-muted)}.absent.svelte-llv1r8{margin-top:.5rem}.bound.svelte-llv1r8{margin-top:.25rem}.cost.svelte-llv1r8{margin-top:.5rem}.actions.svelte-llv1r8{display:flex;flex-wrap:wrap;gap:var(--touch-gap);margin-top:.75rem}.pages.svelte-hzfkjn{display:flex;flex-direction:column;gap:1rem}.fact.svelte-hzfkjn{margin:.5rem 0;font-size:1.125rem}.muted.svelte-hzfkjn,.locked.svelte-hzfkjn{color:var(--ink-muted);font-size:1rem}h3.svelte-hzfkjn{margin:1.5rem 0 .5rem;font-size:1.25rem}.scroll.svelte-hzfkjn{overflow-x:auto}table.svelte-hzfkjn{border-collapse:collapse;width:100%;font-size:1.0625rem}th.svelte-hzfkjn,td.svelte-hzfkjn{padding:.375rem .5rem;text-align:left;border-bottom:1px solid var(--border)}th.svelte-hzfkjn{color:var(--ink-muted);font-size:1rem}input.svelte-hzfkjn{min-height:2.75rem;width:100%;min-width:6rem;padding:0 .5rem;font:inherit;font-variant-numeric:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm)}.rules.svelte-hzfkjn{margin:.75rem 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:.75rem}.rule.svelte-hzfkjn{padding:.75rem 1rem 1rem;background:var(--bg);border:1px solid var(--border-soft);border-radius:var(--radius-sm)}.rule-head.svelte-hzfkjn{display:flex;flex-wrap:wrap;gap:.5rem;align-items:baseline;margin:0}.rank.svelte-hzfkjn{flex:none;min-width:1.75rem;color:var(--ink-muted);font-variant-numeric:tabular-nums;font-weight:700}.rule-label.svelte-hzfkjn{font-size:1.125rem;font-weight:700}.token.svelte-hzfkjn{color:var(--ink-muted);font-size:.9375rem}.severity.svelte-hzfkjn{margin-left:auto;padding:.125rem .625rem;font-size:.9375rem;border-radius:var(--radius-pill);background:var(--waiting-wash)}.severity[data-blocking=true].svelte-hzfkjn{background:var(--warning-wash)}.when.svelte-hzfkjn{margin:.375rem 0 0;font-size:1rem}.quote.svelte-hzfkjn{margin:.5rem 0 0;padding:.5rem .75rem;font-size:1.0625rem;background:var(--surface);border-radius:var(--radius-sm);box-shadow:var(--shadow-1)}.quote.silent.svelte-hzfkjn{color:var(--ink-muted);font-size:1rem;box-shadow:none;background:var(--waiting-wash)}.note.svelte-hzfkjn{margin:.5rem 0 0;font-size:1rem;color:var(--ink-muted)}.toggle.svelte-hzfkjn{display:flex;align-items:center;gap:.75rem;min-height:2.75rem;margin:.5rem 0 0;padding:.375rem .75rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--waiting-wash);cursor:pointer;transition:background-color var(--tap) var(--ease),border-color var(--tap) var(--ease)}.toggle[data-on=true].svelte-hzfkjn{background:var(--ready-wash)}@media(hover:hover){.toggle.svelte-hzfkjn:hover{border-color:var(--ink-muted)}}.toggle.svelte-hzfkjn input:where(.svelte-hzfkjn){flex:none;width:1.5rem;height:1.5rem;min-height:0;min-width:0;padding:0;accent-color:var(--focus)}.toggle-text.svelte-hzfkjn{display:flex;flex-wrap:wrap;gap:.5rem;align-items:baseline}.toggle-label.svelte-hzfkjn{font-size:1.0625rem;font-weight:700}.hint.svelte-hzfkjn{flex:1 1 20rem;color:var(--ink-muted);font-size:1rem}.box.svelte-hzfkjn{display:contents}.verdicts.svelte-hzfkjn{margin:0;padding:0;list-style:none}.waivers.svelte-hzfkjn{max-height:24rem;overflow-y:auto}.verdicts.svelte-hzfkjn li:where(.svelte-hzfkjn){display:flex;flex-wrap:wrap;gap:.75rem;align-items:baseline;padding:.375rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.verdicts.svelte-hzfkjn li[data-blocking=true]:where(.svelte-hzfkjn){border-left:.25rem solid var(--warning);background:var(--warning-wash);padding-left:.5rem}.what.svelte-hzfkjn,.message.svelte-hzfkjn{font-weight:700}.detail.svelte-hzfkjn{color:var(--ink-muted)}.dead.svelte-hzfkjn{flex:1 1 20rem;padding:.125rem .625rem;font-size:1rem;background:var(--warning-wash);border-radius:var(--radius-sm)}.unread.svelte-hzfkjn{margin:.5rem 0;padding:.5rem .75rem;font-size:1rem;background:var(--fault-wash);border-left:.25rem solid var(--fault);border-radius:var(--radius-sm)}.pages.svelte-18wbxwu{display:flex;flex-direction:column;gap:1rem}.fact.svelte-18wbxwu{margin:.5rem 0;font-size:1.125rem}.muted.svelte-18wbxwu{color:var(--ink-muted);font-size:1rem}.filename.svelte-18wbxwu{margin-top:1rem;font-weight:700}.identity.svelte-18wbxwu{margin:1rem 0 0;padding:.75rem 1rem;display:grid;grid-template-columns:auto 1fr;gap:.25rem 1rem;font-size:1.0625rem;background:var(--bg);border-radius:var(--radius)}.identity.svelte-18wbxwu dt:where(.svelte-18wbxwu){color:var(--ink-muted)}.identity.svelte-18wbxwu dd:where(.svelte-18wbxwu){margin:0;font-weight:700}.actions.svelte-18wbxwu{display:flex;flex-wrap:wrap;gap:var(--touch-gap);margin:.75rem 0 0}.choose.svelte-18wbxwu{display:inline-flex;align-items:center;gap:.5rem;min-height:2.75rem;padding:0 1rem;font-size:1.0625rem;font-weight:700;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-1);cursor:pointer;transition:transform var(--tap) var(--ease),background-color var(--tap) var(--ease),border-color var(--tap) var(--ease),box-shadow var(--slide) var(--ease)}.choose.svelte-18wbxwu:active:not(.off){transform:scale(.975)}@media(hover:hover){.choose.svelte-18wbxwu:hover:not(.off){border-color:var(--ink-muted);box-shadow:var(--shadow-2)}}.choose.off.svelte-18wbxwu{opacity:.5;box-shadow:none;cursor:default}.choose.working.svelte-18wbxwu{opacity:1;border-color:var(--waiting)}.choose.svelte-18wbxwu input:where(.svelte-18wbxwu){display:none}.key.svelte-18wbxwu{padding:.0625rem .375rem;border-radius:var(--radius-pill);background:var(--bg);color:var(--ink-muted);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase}.same.svelte-18wbxwu{padding:.5rem .75rem;border-left:.375rem solid var(--ready);border-radius:var(--radius-sm);background:var(--ready-wash)}.warned.svelte-18wbxwu{padding:.5rem .75rem;border-left:.375rem solid var(--fault);border-radius:var(--radius-sm);background:var(--fault-wash)}.faults.svelte-18wbxwu{margin-top:.75rem;padding:.25rem 1rem .5rem;border-left:.375rem solid var(--warning);border-radius:var(--radius);background:var(--warning-wash)}.faults.svelte-18wbxwu ul:where(.svelte-18wbxwu){margin:0;padding-left:1.25rem;font-size:1.0625rem}.allowed.svelte-18wbxwu{display:block;color:var(--ink-muted);font-size:1rem}.scroll.svelte-18wbxwu{max-height:24rem;overflow:auto;border:1px solid var(--border-soft);border-radius:var(--radius-lg);background:var(--bg)}table.svelte-18wbxwu{border-collapse:collapse;width:100%;font-size:1.0625rem}th.svelte-18wbxwu,td.svelte-18wbxwu{padding:.375rem .75rem;text-align:left;border-bottom:1px solid var(--border)}th.svelte-18wbxwu{position:sticky;top:0;color:var(--ink-muted);font-size:1rem;background:var(--bg)}.rows.svelte-18wbxwu{margin:0;padding:0 .75rem;list-style:none}.rows.svelte-18wbxwu li:where(.svelte-18wbxwu){display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;padding:.5rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.rows.svelte-18wbxwu li:where(.svelte-18wbxwu):first-child{border-top:none}.what.svelte-18wbxwu{font-weight:700}.detail.svelte-18wbxwu{color:var(--ink-muted)}.big.svelte-15nlt6i{display:flex;flex-direction:column;justify-content:center;gap:.25rem;min-height:6rem;padding:1rem 1.25rem;text-align:left;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.write.svelte-15nlt6i{color:var(--surface);background:var(--action);border-color:var(--action)}.destructive.svelte-15nlt6i{color:var(--surface);background:var(--danger);border-color:var(--danger)}.write.svelte-15nlt6i .hint:where(.svelte-15nlt6i),.destructive.svelte-15nlt6i .hint:where(.svelte-15nlt6i){color:var(--surface);opacity:.85}.write.svelte-15nlt6i .guarded:where(.svelte-15nlt6i),.destructive.svelte-15nlt6i .guarded:where(.svelte-15nlt6i){background:var(--surface);color:var(--ink)}.big.svelte-15nlt6i:disabled{opacity:.5}.big.busy.svelte-15nlt6i{opacity:1;border-color:var(--waiting);background:var(--waiting-wash)}.big.busy.write.svelte-15nlt6i{background:var(--action)}.big.busy.destructive.svelte-15nlt6i{background:var(--danger)}.guarded.svelte-15nlt6i{margin-left:.5rem;padding:.0625rem .375rem;border-radius:var(--radius-pill);background:var(--bg);color:var(--ink-muted);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;vertical-align:middle}.big.engaged.svelte-15nlt6i{border-left:.5rem solid var(--warning)}.label.svelte-15nlt6i{font-size:1.375rem;font-weight:700}.hint.svelte-15nlt6i{font-size:1rem;color:var(--ink-muted)}.troubleshooting.svelte-1yd0nzg{display:flex;flex-direction:column;gap:1rem}.buttons.svelte-1yd0nzg{display:grid;grid-template-columns:repeat(auto-fit,minmax(22rem,1fr));gap:var(--touch-gap)}.big.svelte-1yd0nzg{display:flex;flex-direction:column;justify-content:center;gap:.25rem;min-height:6rem;padding:1rem 1.25rem;text-align:left;text-decoration:none;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.label.svelte-1yd0nzg{font-size:1.375rem;font-weight:700}.hint.svelte-1yd0nzg{font-size:1rem;color:var(--ink-muted)}.report.svelte-1yd0nzg{margin:0;padding:.75rem 1rem;font-size:1.125rem;background:var(--surface);border:1px solid var(--border);border-left:.5rem solid var(--ready);border-radius:var(--radius)}.drop.svelte-1yd0nzg{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding:1rem;border:2px dashed var(--border);border-radius:var(--radius)}.drop.dropping.svelte-1yd0nzg{border-color:var(--focus)}.drop.svelte-1yd0nzg p:where(.svelte-1yd0nzg){margin:0;font-size:1.125rem}.key.svelte-1yd0nzg{padding:.0625rem .375rem;border-radius:var(--radius-pill);background:var(--bg);color:var(--ink-muted);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase}.choose.svelte-1yd0nzg{display:inline-flex;align-items:center;padding:0 1rem;font-size:1.125rem;font-weight:700;color:var(--surface);background:var(--danger);border:1px solid var(--danger);border-radius:var(--radius);cursor:pointer}@media(hover:hover){.choose.svelte-1yd0nzg:hover{filter:brightness(.92)}}.choose.svelte-1yd0nzg input:where(.svelte-1yd0nzg){display:none}.fact.svelte-1yd0nzg{margin:0 0 .5rem;font-size:1.125rem}.muted.svelte-1yd0nzg{color:var(--ink-muted)}.row.svelte-o9s8nz{display:flex;flex-wrap:wrap;gap:.75rem;margin-bottom:.75rem}.admin.svelte-9b2mjq{position:fixed;inset:0;z-index:90;display:grid;grid-template-columns:16rem 1fr;background:var(--bg);color:var(--ink);overflow:hidden}.rail.svelte-9b2mjq{display:flex;flex-direction:column;gap:.125rem;padding:1rem .75rem;overflow-y:auto;background:var(--surface);border-right:1px solid var(--border)}h1.svelte-9b2mjq{margin:0 .5rem 1rem;font-size:1.25rem;letter-spacing:-.01em}.group.svelte-9b2mjq{margin:1rem .5rem .375rem;color:var(--ink-muted);font-size:.8125rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase}.entry.svelte-9b2mjq{padding:0 .75rem;height:2.5rem;display:flex;align-items:center;text-align:left;font-size:1.0625rem;border-radius:var(--radius-sm);color:var(--ink-muted);transition:background-color var(--tap) var(--ease),color var(--tap) var(--ease)}@media(hover:hover){.entry.svelte-9b2mjq:hover{background:var(--bg);color:var(--ink)}}.entry.current.svelte-9b2mjq{background:var(--bg);color:var(--ink);font-weight:700;box-shadow:inset .1875rem 0 0 0 var(--focus)}.foot.svelte-9b2mjq{margin-top:auto;padding:1rem .5rem 0;border-top:1px solid var(--border)}.station.svelte-9b2mjq{margin:0;font-size:1rem;font-weight:600}.build.svelte-9b2mjq{margin:.125rem 0 0;font-size:.8125rem;color:var(--ink-muted)}.technical.svelte-9b2mjq{display:flex;align-items:center;gap:.5rem;min-height:2.75rem;margin-top:.75rem;color:var(--ink-muted);font-size:.9375rem}.technical.svelte-9b2mjq input:where(.svelte-9b2mjq){width:1.5rem;height:1.5rem;flex:0 0 auto}.back.svelte-9b2mjq{margin-top:.75rem;padding:0 .5rem;height:2.25rem;font-size:.9375rem;color:var(--ink-muted);border-radius:var(--radius-sm)}@media(hover:hover){.back.svelte-9b2mjq:hover{background:var(--bg);color:var(--ink)}}.body.svelte-9b2mjq{display:flex;flex-direction:column;min-width:0;overflow:hidden}.page.svelte-9b2mjq{flex:1;min-height:0;overflow-y:auto;padding:1.5rem 2rem 3rem}.page.svelte-9b2mjq>*{max-width:68rem}.page-title.svelte-9b2mjq{margin:0 0 1.25rem;font-size:1.75rem;letter-spacing:-.01em}.waiting.svelte-9b2mjq{margin:0;font-size:1.125rem;color:var(--ink-muted)}.banner.svelte-9b2mjq{flex:none;margin:0;padding:.75rem 2rem;font-size:1.0625rem;background:var(--surface);border-bottom:1px solid var(--border)}.notice.svelte-9b2mjq{border-left:.375rem solid var(--ready);background:var(--ready-wash)}.failure.svelte-9b2mjq{border-left:.375rem solid var(--fault);background:var(--fault-wash)}.pending.svelte-9b2mjq{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;border-left:.375rem solid var(--warning);background:var(--warning-wash)}.pending.svelte-9b2mjq p:where(.svelte-9b2mjq){margin:0;flex:1 1 24rem}.save-bar.svelte-9b2mjq{flex:none;padding:.75rem 2rem;background:var(--surface);border-top:1px solid var(--border);box-shadow:var(--shadow-1)}.retired.svelte-9b2mjq{margin:0 0 .5rem;font-size:.9375rem;color:var(--ink-muted)}.faults.svelte-9b2mjq{margin:0 0 .5rem;padding-left:1.25rem;font-size:1rem} diff --git a/internal/web/dist/assets/mount-CTNNPuRa.css b/internal/web/dist/assets/mount-CTNNPuRa.css new file mode 100644 index 0000000..27b9c52 --- /dev/null +++ b/internal/web/dist/assets/mount-CTNNPuRa.css @@ -0,0 +1 @@ +.act.svelte-5tpq0o{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;min-height:2.75rem;padding:0 1rem;font-size:1.0625rem;font-weight:700;border-radius:var(--radius-sm);box-shadow:var(--shadow-1);transition:background-color var(--tap) var(--ease),border-color var(--tap) var(--ease),box-shadow var(--slide) var(--ease)}.read.svelte-5tpq0o{color:var(--ink);background:var(--surface);border:1px solid var(--border)}.write.svelte-5tpq0o{color:var(--surface);background:var(--action);border:1px solid var(--action)}.destructive.svelte-5tpq0o{min-height:var(--touch-min);color:var(--surface);background:var(--danger);border:1px solid var(--danger)}@media(hover:hover){.read.svelte-5tpq0o:hover:not(:disabled){border-color:var(--ink-muted);box-shadow:var(--shadow-2)}.write.svelte-5tpq0o:hover:not(:disabled){border-color:var(--action)}.destructive.svelte-5tpq0o:hover:not(:disabled){border-color:var(--danger)}.write.svelte-5tpq0o:hover:not(:disabled),.destructive.svelte-5tpq0o:hover:not(:disabled){box-shadow:var(--shadow-2);filter:brightness(.92)}}.act.svelte-5tpq0o:disabled{opacity:.5;box-shadow:none;cursor:default}.act.busy.svelte-5tpq0o:disabled{opacity:1}.key.svelte-5tpq0o{padding:.0625rem .375rem;border-radius:var(--radius-pill);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;background:var(--bg);color:var(--ink-muted)}.write.svelte-5tpq0o .key:where(.svelte-5tpq0o),.destructive.svelte-5tpq0o .key:where(.svelte-5tpq0o){background:var(--surface);color:var(--ink)}.scrim.svelte-zyka3t{position:fixed;inset:0;z-index:95;display:flex;align-items:center;justify-content:center;padding:2rem;background:#1c1b1973}.panel.svelte-zyka3t{display:flex;flex-direction:column;gap:.75rem;width:min(34rem,100%);padding:1.75rem;background:var(--surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-2)}header.svelte-zyka3t{display:flex;align-items:center;gap:.75rem}.glyph.svelte-zyka3t{display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;flex:none;border-radius:var(--radius-sm);background:var(--bg);color:var(--ink-muted)}h2.svelte-zyka3t{margin:0;font-size:1.375rem;line-height:1.2}.why.svelte-zyka3t,.how.svelte-zyka3t{margin:0;color:var(--ink-muted);font-size:1rem;line-height:1.4}.field.svelte-zyka3t{display:flex;flex-direction:column;gap:.375rem;font-size:1rem;color:var(--ink-muted)}input.svelte-zyka3t{height:2.75rem;padding:0 .75rem;font:inherit;font-size:1.125rem;color:var(--ink);background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm)}input.svelte-zyka3t:focus-visible{outline:3px solid var(--focus);outline-offset:1px}.refusal.svelte-zyka3t{margin:0;padding:.625rem .75rem;border-left:.25rem solid var(--fault);background:var(--fault-wash);font-size:1rem}footer.svelte-zyka3t{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.25rem}.field.svelte-1oilv5s{display:flex;flex-direction:column;gap:.25rem;padding:.5rem 0}label.svelte-1oilv5s{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}.name.svelte-1oilv5s{font-size:1.125rem;font-weight:700}code.svelte-1oilv5s{font-size:.9375rem;color:var(--ink-muted)}input.svelte-1oilv5s{min-height:3rem;padding:0 .75rem;font:inherit;font-variant-numeric:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.refused.svelte-1oilv5s input:where(.svelte-1oilv5s){border-left:.5rem solid var(--fault)}.hint.svelte-1oilv5s,.fault.svelte-1oilv5s{margin:0;font-size:1rem;color:var(--ink-muted)}.fault.svelte-1oilv5s{padding-left:.5rem;border-left:.25rem solid var(--fault)}.inventory.svelte-tuh4jc{display:flex;flex-direction:column;gap:.5rem}.headline.svelte-tuh4jc{margin:0;font-size:1.375rem;font-weight:700}dl.svelte-tuh4jc{margin:0;display:flex;flex-direction:column;gap:.25rem}.row.svelte-tuh4jc{display:flex;align-items:baseline;gap:.75rem}dt.svelte-tuh4jc{flex:none;width:4.5rem;text-align:right;font-size:1.375rem;font-weight:700}dd.svelte-tuh4jc{margin:0;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem;font-size:1.125rem}.label.svelte-tuh4jc{font-weight:700}.note.svelte-tuh4jc{color:var(--ink-muted)}.rows.svelte-tuh4jc{padding:0 .5rem;min-height:var(--touch-min);text-decoration:underline;color:var(--ink-muted)}.oneline.svelte-tuh4jc{margin:0;font-size:1rem;color:var(--ink-muted)}.panel.svelte-ww3z5u{padding:1rem 1.25rem 1.25rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}h2.svelte-ww3z5u{margin:0 0 .5rem;font-size:1.5rem;font-weight:700}.note.svelte-ww3z5u{margin:0 0 .75rem;font-size:1rem;color:var(--ink-muted)}.pages.svelte-3gk7u7{display:flex;flex-direction:column;gap:1rem}.fact.svelte-3gk7u7{margin:.5rem 0;font-size:1.125rem}.muted.svelte-3gk7u7{color:var(--ink-muted);font-size:1rem}.actions.svelte-3gk7u7{display:flex;flex-wrap:wrap;gap:var(--touch-gap);margin:.75rem 0 0}.toggle.svelte-3gk7u7{display:flex;align-items:center;gap:.75rem;min-height:2.75rem;margin:.5rem 0 0;padding:.375rem .75rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--waiting-wash);font-weight:400;cursor:pointer;transition:background-color var(--tap) var(--ease),border-color var(--tap) var(--ease)}.toggle[data-on=true].svelte-3gk7u7{background:var(--ready-wash)}@media(hover:hover){.toggle.svelte-3gk7u7:hover{border-color:var(--ink-muted)}}.toggle.svelte-3gk7u7 input:where(.svelte-3gk7u7){flex:none;width:1.5rem;height:1.5rem;min-height:0;min-width:0;padding:0;accent-color:var(--focus)}.toggle-text.svelte-3gk7u7{display:flex;flex-wrap:wrap;gap:.5rem;align-items:baseline}.toggle-label.svelte-3gk7u7{font-size:1.0625rem;font-weight:700}.hint.svelte-3gk7u7{flex:1 1 20rem;color:var(--ink-muted);font-size:1rem}.key.svelte-3gk7u7{padding:.0625rem .375rem;border-radius:var(--radius-pill);background:var(--bg);color:var(--ink-muted);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase}.scroll.svelte-3gk7u7{max-height:24rem;overflow:auto;border:1px solid var(--border-soft);border-radius:var(--radius-sm);background:var(--bg)}.rows.svelte-3gk7u7{margin:0;padding:0 .75rem;list-style:none}.rows.svelte-3gk7u7 li:where(.svelte-3gk7u7){display:flex;flex-wrap:wrap;gap:.75rem;align-items:baseline;padding:.375rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.rows.svelte-3gk7u7 li:where(.svelte-3gk7u7):first-child{border-top:none}.line.svelte-3gk7u7,.id.svelte-3gk7u7,.value.svelte-3gk7u7{color:var(--ink-muted)}.line.svelte-3gk7u7{flex:none;width:7rem}.what.svelte-3gk7u7{font-weight:700}.message.svelte-3gk7u7{flex:1 1 20rem}.pick.svelte-3gk7u7{height:2.75rem;padding:0 .5rem;font-weight:700;color:var(--ink);text-decoration:underline;border-radius:var(--radius-sm);transition:background-color var(--tap) var(--ease)}@media(hover:hover){.pick.svelte-3gk7u7:hover:not(:disabled){background:var(--surface)}}.search.svelte-3gk7u7,label.svelte-3gk7u7{display:block;margin-top:.5rem;font-size:1.0625rem;font-weight:700}.choice.svelte-3gk7u7{display:flex;flex-direction:column}.choice.svelte-3gk7u7 label:where(.svelte-3gk7u7){display:flex;gap:.75rem;align-items:center;min-height:2.75rem;margin:0;font-weight:400}.choice.svelte-3gk7u7 input:where(.svelte-3gk7u7){width:1.5rem;height:1.5rem;min-height:0;flex:0 0 auto;padding:0;background:none;border:none;border-radius:0}input.svelte-3gk7u7{min-height:2.75rem;width:100%;max-width:34rem;padding:0 .75rem;font:inherit;font-variant-numeric:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm)}.decision.svelte-3gk7u7{margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--border)}.act-block.svelte-3gk7u7{margin-top:1rem;padding:.75rem 1rem 1rem;background:var(--bg);border-radius:var(--radius)}.act-block.svelte-3gk7u7 .what:where(.svelte-3gk7u7){margin:0;font-size:1.0625rem;font-weight:700}.drop.svelte-3gk7u7{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding:1rem;border:2px dashed var(--border);border-radius:var(--radius);transition:border-color var(--tap) var(--ease),background-color var(--tap) var(--ease)}.drop.dropping.svelte-3gk7u7{border-color:var(--focus);background:var(--waiting-wash)}.drop.working.svelte-3gk7u7{border-color:var(--waiting);background:var(--waiting-wash)}.drop.svelte-3gk7u7 p:where(.svelte-3gk7u7){margin:0;font-size:1.125rem}.choose.svelte-3gk7u7{display:inline-flex;align-items:center;margin:0;padding:0 1rem;font-size:1.125rem;font-weight:700;color:var(--surface);background:var(--danger);border:1px solid var(--danger);border-radius:var(--radius-sm);box-shadow:var(--shadow-1);cursor:pointer;transition:transform var(--tap) var(--ease),border-color var(--tap) var(--ease)}.choose.svelte-3gk7u7:active{transform:scale(.975)}@media(prefers-reduced-motion:reduce){.choose.svelte-3gk7u7:active{transform:none}}@media(hover:hover){.choose.svelte-3gk7u7:hover{filter:brightness(.92)}}.choose.svelte-3gk7u7 input:where(.svelte-3gk7u7){display:none}table.svelte-3gk7u7{border-collapse:collapse;width:100%;font-size:1.0625rem}th.svelte-3gk7u7,td.svelte-3gk7u7{padding:.375rem .5rem;text-align:left;white-space:nowrap;border-bottom:1px solid var(--border)}.why.svelte-3gk7u7{white-space:normal;min-width:16rem;color:var(--ink-muted)}th.svelte-3gk7u7{position:sticky;top:0;color:var(--ink-muted);font-size:1rem;background:var(--bg)}.light.svelte-1421wa8{display:flex;flex-direction:column;gap:.375rem;padding:.875rem 1rem;background:var(--surface);border:1px solid var(--border);border-left:.5rem solid var(--waiting);border-radius:var(--radius)}.light[data-level=ok].svelte-1421wa8{border-left-color:var(--ready)}.light[data-level=warn].svelte-1421wa8{border-left-color:var(--warning)}.light[data-level=fault].svelte-1421wa8{border-left-color:var(--fault)}header.svelte-1421wa8{display:flex;align-items:center;gap:.5rem}h3.svelte-1421wa8{margin:0;font-size:1.25rem;font-weight:700}.dot.svelte-1421wa8{width:1rem;height:1rem;border-radius:50%;background:var(--waiting);flex:none}.dot[data-level=ok].svelte-1421wa8{background:var(--ready)}.dot[data-level=warn].svelte-1421wa8{background:var(--warning)}.dot[data-level=fault].svelte-1421wa8{background:var(--fault)}.dot[data-level=unknown].svelte-1421wa8,.dot[data-level=off].svelte-1421wa8{background:transparent;box-shadow:inset 0 0 0 3px var(--waiting)}.verdict.svelte-1421wa8{margin-left:auto;font-size:1rem;font-weight:700;color:var(--ink-muted);letter-spacing:.03em}.value.svelte-1421wa8{margin:0;font-size:1.125rem}.remedy.svelte-1421wa8{margin:0;font-size:1rem;color:var(--ink-muted)}.dashboard.svelte-1w44m0y{display:flex;flex-direction:column;gap:1rem}.link.svelte-1w44m0y{padding:0;font:inherit;color:inherit;text-decoration:underline;background:none;border:none;cursor:pointer}.lights.svelte-1w44m0y{display:grid;grid-template-columns:repeat(auto-fit,minmax(20rem,1fr));grid-auto-rows:1fr;gap:var(--touch-gap)}.fact.svelte-1w44m0y{margin:0 0 .5rem;font-size:1.125rem;overflow-wrap:break-word}.remedy.svelte-1w44m0y,.count.svelte-1w44m0y{color:var(--ink-muted);font-size:1rem}.restart.svelte-1w44m0y strong:where(.svelte-1w44m0y){letter-spacing:.03em}.restart[data-verdict=missing].svelte-1w44m0y{margin-bottom:.25rem;padding:.5rem .75rem;border-left:.25rem solid var(--warning);background:var(--warning-wash)}.restart[data-verdict=unknown].svelte-1w44m0y{margin-bottom:.25rem;padding:.5rem .75rem;border-left:.25rem solid var(--waiting);background:var(--waiting-wash)}.decisions.svelte-1w44m0y,.events.svelte-1w44m0y{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:.375rem}.decisions.svelte-1w44m0y li:where(.svelte-1w44m0y),.events.svelte-1w44m0y li:where(.svelte-1w44m0y){display:flex;flex-wrap:wrap;gap:.75rem;align-items:baseline;padding:.375rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.what.svelte-1w44m0y,.message.svelte-1w44m0y{font-weight:700}.detail.svelte-1w44m0y,.time.svelte-1w44m0y,.source.svelte-1w44m0y,.code.svelte-1w44m0y{color:var(--ink-muted)}.events.svelte-1w44m0y li[data-level=error]:where(.svelte-1w44m0y){border-left:.25rem solid var(--fault);padding-left:.5rem}.events.svelte-1w44m0y li[data-level=warn]:where(.svelte-1w44m0y){border-left:.25rem solid var(--warning);padding-left:.5rem}.identity.svelte-1w44m0y{margin:0;display:grid;grid-template-columns:auto 1fr;gap:.25rem 1rem;font-size:1.125rem}.identity.svelte-1w44m0y dt:where(.svelte-1w44m0y){color:var(--ink-muted)}.identity.svelte-1w44m0y dd:where(.svelte-1w44m0y){margin:0;font-weight:700}.pages.svelte-1p4i0xm{display:flex;flex-direction:column;gap:1rem}.panel.svelte-1p4i0xm{padding:1rem 1.25rem 1.25rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1)}.head.svelte-1p4i0xm{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;justify-content:space-between}h2.svelte-1p4i0xm{margin:0;font-size:1.5rem;font-weight:700}.standing.svelte-1p4i0xm{display:inline-flex;align-items:center;gap:.5rem;height:2rem;padding:0 .75rem;border-radius:var(--radius-pill);background:var(--waiting-wash);font-size:1rem;font-weight:700}.standing[data-level=ok].svelte-1p4i0xm{background:var(--ready-wash)}.standing[data-level=warn].svelte-1p4i0xm{background:var(--warning-wash)}.standing[data-level=fault].svelte-1p4i0xm{background:var(--fault-wash)}.dot.svelte-1p4i0xm{width:.625rem;height:.625rem;border-radius:var(--radius-pill);background:var(--waiting)}.standing[data-level=ok].svelte-1p4i0xm .dot:where(.svelte-1p4i0xm){background:var(--ready)}.standing[data-level=warn].svelte-1p4i0xm .dot:where(.svelte-1p4i0xm){background:var(--warning)}.standing[data-level=fault].svelte-1p4i0xm .dot:where(.svelte-1p4i0xm){background:var(--fault)}.fact.svelte-1p4i0xm{margin:.75rem 0 0;font-size:1.125rem}.note.svelte-1p4i0xm,.count.svelte-1p4i0xm,.waiting.svelte-1p4i0xm{margin:.25rem 0 0;font-size:1rem;color:var(--ink-muted)}.check.svelte-1p4i0xm{display:flex;gap:.75rem;align-items:center;min-height:2.75rem;margin-top:.5rem;font-size:1.0625rem}.check.svelte-1p4i0xm input:where(.svelte-1p4i0xm){width:1.5rem;height:1.5rem;flex:0 0 auto}.check.svelte-1p4i0xm small:where(.svelte-1p4i0xm){display:block;font-size:1rem;color:var(--ink-muted)}.actions.svelte-1p4i0xm{display:flex;flex-wrap:wrap;gap:.5rem;margin:.75rem 0 0}@media(hover:hover){.pick.svelte-1p4i0xm:hover,summary.svelte-1p4i0xm:hover{background:var(--bg)}}.list.svelte-1p4i0xm{margin:.25rem 0 0;padding:0;list-style:none;display:flex;flex-direction:column}.list.svelte-1p4i0xm li:where(.svelte-1p4i0xm){display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;padding:.125rem 0;border-top:1px solid var(--border-soft);font-size:1.0625rem}.list.svelte-1p4i0xm li.refused:where(.svelte-1p4i0xm){padding-left:.5rem;border-left:.25rem solid var(--fault);background:var(--fault-wash)}.pick.svelte-1p4i0xm{height:2.75rem;padding:0 .75rem;font-weight:700;text-decoration:underline;border-radius:var(--radius-sm);transition:background-color var(--tap) var(--ease)}.what.svelte-1p4i0xm{font-weight:700}.detail.svelte-1p4i0xm{color:var(--ink-muted)}.folded.svelte-1p4i0xm{margin-top:1rem;border:1px solid var(--border);border-radius:var(--radius)}summary.svelte-1p4i0xm{display:flex;align-items:center;height:2.75rem;padding:0 .75rem;font-size:1.0625rem;font-weight:700;border-radius:var(--radius);cursor:pointer;transition:background-color var(--tap) var(--ease),transform var(--tap) var(--ease)}summary.svelte-1p4i0xm:active{transform:scale(.975)}@media(prefers-reduced-motion:reduce){summary.svelte-1p4i0xm:active{transform:none}}.frames.svelte-1p4i0xm{margin-top:1rem;max-height:18rem;overflow-y:auto;background:var(--waiting-wash);border:1px solid var(--border);border-radius:var(--radius)}.frames-head.svelte-1p4i0xm{position:sticky;top:0;margin:0;padding:.5rem .75rem;background:var(--surface);border-radius:var(--radius) var(--radius) 0 0;box-shadow:var(--shadow-2);font-size:1rem;color:var(--ink-muted)}.interrupted.svelte-1p4i0xm{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;margin:0;padding:.5rem .75rem;background:var(--warning-wash);border-left:.375rem solid var(--warning);font-size:1rem}.frame-rows.svelte-1p4i0xm{margin:0;padding:.5rem .75rem;list-style:none;display:flex;flex-direction:column;gap:.25rem}.frame-rows.svelte-1p4i0xm li:where(.svelte-1p4i0xm){display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:.75rem;font-size:.9375rem}.hex.svelte-1p4i0xm,.decoded.svelte-1p4i0xm{overflow-x:auto;white-space:pre}.decoded.svelte-1p4i0xm{color:var(--ink-muted)}div.journal.svelte-86o8oz{display:flex;flex-direction:column;gap:1rem;max-width:none;--reading-column: 68rem}.filters.svelte-86o8oz{display:flex;flex-wrap:wrap;gap:var(--touch-gap);align-items:center;margin-bottom:.75rem;font-size:1.0625rem}.filters.svelte-86o8oz label:where(.svelte-86o8oz){font-weight:700}select.svelte-86o8oz{min-height:2.75rem;padding:0 .5rem;font:inherit;font-variant-numeric:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm)}.export.svelte-86o8oz{display:inline-flex;align-items:center;gap:.5rem;height:2.75rem;padding:0 1rem;font-size:1.0625rem;font-weight:700;text-decoration:none;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-1);transition:transform var(--tap) var(--ease),border-color var(--tap) var(--ease),box-shadow var(--slide) var(--ease)}.export.svelte-86o8oz:active{transform:scale(.975)}@media(hover:hover){.export.svelte-86o8oz:hover{border-color:var(--ink-muted);box-shadow:var(--shadow-2)}}.replay.svelte-86o8oz{margin-top:.5rem}.fact.svelte-86o8oz{margin:.5rem 0;max-width:var(--reading-column);font-size:1.125rem}.muted.svelte-86o8oz{color:var(--ink-muted);font-size:1rem}.table-box.svelte-86o8oz,.lines-box.svelte-86o8oz{overflow:auto;background:var(--bg);border:1px solid var(--border-soft);border-radius:var(--radius-sm)}.table-box.svelte-86o8oz{max-height:34rem}.lines-box.svelte-86o8oz{max-height:28rem}table.svelte-86o8oz{border-collapse:collapse;min-width:100%;font-size:1.0625rem}th.svelte-86o8oz,td.svelte-86o8oz{padding:.375rem .5rem;text-align:left;white-space:nowrap;border-bottom:1px solid var(--border)}th.svelte-86o8oz{position:sticky;top:0;z-index:1;color:var(--ink-muted);font-size:1rem;background:var(--bg)}tbody.svelte-86o8oz tr:where(.svelte-86o8oz){transition:background-color var(--tap) var(--ease)}@media(hover:hover){tbody.svelte-86o8oz tr:where(.svelte-86o8oz):hover{background:var(--surface)}}tbody.svelte-86o8oz tr.open:where(.svelte-86o8oz){background:var(--surface);font-weight:700}tbody.svelte-86o8oz tr.open:where(.svelte-86o8oz)>td:where(.svelte-86o8oz):first-child{box-shadow:inset .1875rem 0 0 0 var(--focus)}.pick.svelte-86o8oz{height:2.75rem;padding:0 .5rem;color:var(--ink);text-decoration:underline;border-radius:var(--radius-sm);transition:background-color var(--tap) var(--ease)}@media(hover:hover){.pick.svelte-86o8oz:hover:not(:disabled){background:var(--surface)}}.detail-row.svelte-86o8oz{background:var(--surface)}.detail-cell.svelte-86o8oz{padding:.75rem 1rem 1rem;white-space:normal;box-shadow:inset .1875rem 0 0 0 var(--focus)}.detail-cell.svelte-86o8oz h3:where(.svelte-86o8oz){margin:0 0 .5rem;font-size:1.25rem}.detail-cell.svelte-86o8oz dl:where(.svelte-86o8oz){display:grid;grid-template-columns:auto 1fr;gap:.25rem 1rem;max-width:var(--reading-column);margin:0;font-size:1.0625rem;font-weight:400}.detail-cell.svelte-86o8oz dt:where(.svelte-86o8oz){color:var(--ink-muted)}.detail-cell.svelte-86o8oz dd:where(.svelte-86o8oz){margin:0}.detail-cell.svelte-86o8oz code:where(.svelte-86o8oz){overflow-wrap:anywhere}.lines.svelte-86o8oz{margin:0;padding:0 .75rem;list-style:none}.lines.svelte-86o8oz li:where(.svelte-86o8oz){display:flex;flex-wrap:wrap;gap:.75rem;align-items:baseline;padding:.375rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.lines.svelte-86o8oz li:where(.svelte-86o8oz):first-child{border-top:none}.lines.svelte-86o8oz li[data-level=error]:where(.svelte-86o8oz),.lines.svelte-86o8oz li[data-level=critical]:where(.svelte-86o8oz){border-left:.25rem solid var(--fault);padding-left:.5rem}.lines.svelte-86o8oz li[data-level=warn]:where(.svelte-86o8oz){border-left:.25rem solid var(--warning);padding-left:.5rem}.when.svelte-86o8oz,.level.svelte-86o8oz,.from.svelte-86o8oz,.code.svelte-86o8oz,.detail-text.svelte-86o8oz{color:var(--ink-muted)}.level.svelte-86o8oz{flex:none;width:8rem}.message.svelte-86o8oz{flex:1 1 20rem;font-weight:700}.sr-only.svelte-86o8oz{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.pages.svelte-llv1r8{display:flex;flex-direction:column;gap:1rem}.stale.svelte-llv1r8{margin:0 0 .75rem;padding:.5rem .75rem;background:var(--warning-wash);border-left:.375rem solid var(--warning);border-radius:var(--radius-sm);font-size:1.0625rem}.preview.svelte-llv1r8{display:flex;flex-wrap:wrap;gap:1.5rem;align-items:flex-start}.sheet.svelte-llv1r8{display:flex;flex-direction:column;gap:.5rem;min-width:0;padding:.75rem;background:var(--waiting-wash);border-radius:var(--radius-lg)}img.svelte-llv1r8{width:40rem;max-width:100%;image-rendering:pixelated;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-1)}.refused.svelte-llv1r8{margin:0;padding:.5rem .75rem;background:var(--fault-wash);border-left:.375rem solid var(--fault);border-radius:var(--radius-sm);font-size:1.0625rem}.nudges.svelte-llv1r8{display:flex;flex-direction:column;gap:.5rem;min-width:0}.axis.svelte-llv1r8{display:flex;flex-wrap:wrap;gap:.5rem;align-items:baseline;margin:0;font-size:1.125rem}.pair.svelte-llv1r8{display:flex;gap:var(--touch-gap)}.fault.svelte-llv1r8{margin:0;padding-left:.5rem;border-left:.25rem solid var(--fault);font-size:1rem;color:var(--ink-muted)}.check.svelte-llv1r8{display:flex;gap:.75rem;align-items:center;min-height:2.75rem;font-size:1.0625rem}.check.svelte-llv1r8 input:where(.svelte-llv1r8){width:1.5rem;height:1.5rem;flex:0 0 auto}.truncation.svelte-llv1r8,.absent.svelte-llv1r8,.waiting.svelte-llv1r8,.bound.svelte-llv1r8,.cost.svelte-llv1r8{margin:1rem 0 0;font-size:1rem;color:var(--ink-muted)}.absent.svelte-llv1r8{margin-top:.5rem}.bound.svelte-llv1r8{margin-top:.25rem}.cost.svelte-llv1r8{margin-top:.5rem}.actions.svelte-llv1r8{display:flex;flex-wrap:wrap;gap:var(--touch-gap);margin-top:.75rem}.pages.svelte-hzfkjn{display:flex;flex-direction:column;gap:1rem}.fact.svelte-hzfkjn{margin:.5rem 0;font-size:1.125rem}.muted.svelte-hzfkjn,.locked.svelte-hzfkjn{color:var(--ink-muted);font-size:1rem}h3.svelte-hzfkjn{margin:1.5rem 0 .5rem;font-size:1.25rem}.scroll.svelte-hzfkjn{overflow-x:auto}table.svelte-hzfkjn{border-collapse:collapse;width:100%;font-size:1.0625rem}th.svelte-hzfkjn,td.svelte-hzfkjn{padding:.375rem .5rem;text-align:left;border-bottom:1px solid var(--border)}th.svelte-hzfkjn{color:var(--ink-muted);font-size:1rem}input.svelte-hzfkjn{min-height:2.75rem;width:100%;min-width:6rem;padding:0 .5rem;font:inherit;font-variant-numeric:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm)}.rules.svelte-hzfkjn{margin:.75rem 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:.75rem}.rule.svelte-hzfkjn{padding:.75rem 1rem 1rem;background:var(--bg);border:1px solid var(--border-soft);border-radius:var(--radius-sm)}.rule-head.svelte-hzfkjn{display:flex;flex-wrap:wrap;gap:.5rem;align-items:baseline;margin:0}.rank.svelte-hzfkjn{flex:none;min-width:1.75rem;color:var(--ink-muted);font-variant-numeric:tabular-nums;font-weight:700}.rule-label.svelte-hzfkjn{font-size:1.125rem;font-weight:700}.token.svelte-hzfkjn{color:var(--ink-muted);font-size:.9375rem}.severity.svelte-hzfkjn{margin-left:auto;padding:.125rem .625rem;font-size:.9375rem;border-radius:var(--radius-pill);background:var(--waiting-wash)}.severity[data-blocking=true].svelte-hzfkjn{background:var(--warning-wash)}.when.svelte-hzfkjn{margin:.375rem 0 0;font-size:1rem}.quote.svelte-hzfkjn{margin:.5rem 0 0;padding:.5rem .75rem;font-size:1.0625rem;background:var(--surface);border-radius:var(--radius-sm);box-shadow:var(--shadow-1)}.quote.silent.svelte-hzfkjn{color:var(--ink-muted);font-size:1rem;box-shadow:none;background:var(--waiting-wash)}.note.svelte-hzfkjn{margin:.5rem 0 0;font-size:1rem;color:var(--ink-muted)}.toggle.svelte-hzfkjn{display:flex;align-items:center;gap:.75rem;min-height:2.75rem;margin:.5rem 0 0;padding:.375rem .75rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--waiting-wash);cursor:pointer;transition:background-color var(--tap) var(--ease),border-color var(--tap) var(--ease)}.toggle[data-on=true].svelte-hzfkjn{background:var(--ready-wash)}@media(hover:hover){.toggle.svelte-hzfkjn:hover{border-color:var(--ink-muted)}}.toggle.svelte-hzfkjn input:where(.svelte-hzfkjn){flex:none;width:1.5rem;height:1.5rem;min-height:0;min-width:0;padding:0;accent-color:var(--focus)}.toggle-text.svelte-hzfkjn{display:flex;flex-wrap:wrap;gap:.5rem;align-items:baseline}.toggle-label.svelte-hzfkjn{font-size:1.0625rem;font-weight:700}.hint.svelte-hzfkjn{flex:1 1 20rem;color:var(--ink-muted);font-size:1rem}.box.svelte-hzfkjn{display:contents}.verdicts.svelte-hzfkjn{margin:0;padding:0;list-style:none}.waivers.svelte-hzfkjn{max-height:24rem;overflow-y:auto}.verdicts.svelte-hzfkjn li:where(.svelte-hzfkjn){display:flex;flex-wrap:wrap;gap:.75rem;align-items:baseline;padding:.375rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.verdicts.svelte-hzfkjn li[data-blocking=true]:where(.svelte-hzfkjn){border-left:.25rem solid var(--warning);background:var(--warning-wash);padding-left:.5rem}.what.svelte-hzfkjn,.message.svelte-hzfkjn{font-weight:700}.detail.svelte-hzfkjn{color:var(--ink-muted)}.dead.svelte-hzfkjn{flex:1 1 20rem;padding:.125rem .625rem;font-size:1rem;background:var(--warning-wash);border-radius:var(--radius-sm)}.unread.svelte-hzfkjn{margin:.5rem 0;padding:.5rem .75rem;font-size:1rem;background:var(--fault-wash);border-left:.25rem solid var(--fault);border-radius:var(--radius-sm)}.pages.svelte-18wbxwu{display:flex;flex-direction:column;gap:1rem}.fact.svelte-18wbxwu{margin:.5rem 0;font-size:1.125rem}.muted.svelte-18wbxwu{color:var(--ink-muted);font-size:1rem}.filename.svelte-18wbxwu{margin-top:1rem;font-weight:700}.identity.svelte-18wbxwu{margin:1rem 0 0;padding:.75rem 1rem;display:grid;grid-template-columns:auto 1fr;gap:.25rem 1rem;font-size:1.0625rem;background:var(--bg);border-radius:var(--radius)}.identity.svelte-18wbxwu dt:where(.svelte-18wbxwu){color:var(--ink-muted)}.identity.svelte-18wbxwu dd:where(.svelte-18wbxwu){margin:0;font-weight:700}.actions.svelte-18wbxwu{display:flex;flex-wrap:wrap;gap:var(--touch-gap);margin:.75rem 0 0}.choose.svelte-18wbxwu{display:inline-flex;align-items:center;gap:.5rem;min-height:2.75rem;padding:0 1rem;font-size:1.0625rem;font-weight:700;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-1);cursor:pointer;transition:transform var(--tap) var(--ease),background-color var(--tap) var(--ease),border-color var(--tap) var(--ease),box-shadow var(--slide) var(--ease)}.choose.svelte-18wbxwu:active:not(.off){transform:scale(.975)}@media(hover:hover){.choose.svelte-18wbxwu:hover:not(.off){border-color:var(--ink-muted);box-shadow:var(--shadow-2)}}.choose.off.svelte-18wbxwu{opacity:.5;box-shadow:none;cursor:default}.choose.working.svelte-18wbxwu{opacity:1;border-color:var(--waiting)}.choose.svelte-18wbxwu input:where(.svelte-18wbxwu){display:none}.key.svelte-18wbxwu{padding:.0625rem .375rem;border-radius:var(--radius-pill);background:var(--bg);color:var(--ink-muted);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase}.same.svelte-18wbxwu{padding:.5rem .75rem;border-left:.375rem solid var(--ready);border-radius:var(--radius-sm);background:var(--ready-wash)}.warned.svelte-18wbxwu{padding:.5rem .75rem;border-left:.375rem solid var(--fault);border-radius:var(--radius-sm);background:var(--fault-wash)}.faults.svelte-18wbxwu{margin-top:.75rem;padding:.25rem 1rem .5rem;border-left:.375rem solid var(--warning);border-radius:var(--radius);background:var(--warning-wash)}.faults.svelte-18wbxwu ul:where(.svelte-18wbxwu){margin:0;padding-left:1.25rem;font-size:1.0625rem}.allowed.svelte-18wbxwu{display:block;color:var(--ink-muted);font-size:1rem}.scroll.svelte-18wbxwu{max-height:24rem;overflow:auto;border:1px solid var(--border-soft);border-radius:var(--radius-lg);background:var(--bg)}table.svelte-18wbxwu{border-collapse:collapse;width:100%;font-size:1.0625rem}th.svelte-18wbxwu,td.svelte-18wbxwu{padding:.375rem .75rem;text-align:left;border-bottom:1px solid var(--border)}th.svelte-18wbxwu{position:sticky;top:0;color:var(--ink-muted);font-size:1rem;background:var(--bg)}.rows.svelte-18wbxwu{margin:0;padding:0 .75rem;list-style:none}.rows.svelte-18wbxwu li:where(.svelte-18wbxwu){display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;padding:.5rem 0;border-top:1px solid var(--border);font-size:1.0625rem}.rows.svelte-18wbxwu li:where(.svelte-18wbxwu):first-child{border-top:none}.what.svelte-18wbxwu{font-weight:700}.detail.svelte-18wbxwu{color:var(--ink-muted)}.big.svelte-15nlt6i{display:flex;flex-direction:column;justify-content:center;gap:.25rem;min-height:6rem;padding:1rem 1.25rem;text-align:left;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.write.svelte-15nlt6i{color:var(--surface);background:var(--action);border-color:var(--action)}.destructive.svelte-15nlt6i{color:var(--surface);background:var(--danger);border-color:var(--danger)}.write.svelte-15nlt6i .hint:where(.svelte-15nlt6i),.destructive.svelte-15nlt6i .hint:where(.svelte-15nlt6i){color:var(--surface);opacity:.85}.write.svelte-15nlt6i .guarded:where(.svelte-15nlt6i),.destructive.svelte-15nlt6i .guarded:where(.svelte-15nlt6i){background:var(--surface);color:var(--ink)}.big.svelte-15nlt6i:disabled{opacity:.5}.big.busy.svelte-15nlt6i{opacity:1;border-color:var(--waiting);background:var(--waiting-wash)}.big.busy.write.svelte-15nlt6i{background:var(--action)}.big.busy.destructive.svelte-15nlt6i{background:var(--danger)}.guarded.svelte-15nlt6i{margin-left:.5rem;padding:.0625rem .375rem;border-radius:var(--radius-pill);background:var(--bg);color:var(--ink-muted);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;vertical-align:middle}.big.engaged.svelte-15nlt6i{border-left:.5rem solid var(--warning)}.label.svelte-15nlt6i{font-size:1.375rem;font-weight:700}.hint.svelte-15nlt6i{font-size:1rem;color:var(--ink-muted)}.troubleshooting.svelte-1yd0nzg{display:flex;flex-direction:column;gap:1rem}.buttons.svelte-1yd0nzg{display:grid;grid-template-columns:repeat(auto-fit,minmax(22rem,1fr));gap:var(--touch-gap)}.big.svelte-1yd0nzg{display:flex;flex-direction:column;justify-content:center;gap:.25rem;min-height:6rem;padding:1rem 1.25rem;text-align:left;text-decoration:none;color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.label.svelte-1yd0nzg{font-size:1.375rem;font-weight:700}.hint.svelte-1yd0nzg{font-size:1rem;color:var(--ink-muted)}.report.svelte-1yd0nzg{margin:0;padding:.75rem 1rem;font-size:1.125rem;background:var(--surface);border:1px solid var(--border);border-left:.5rem solid var(--ready);border-radius:var(--radius)}.drop.svelte-1yd0nzg{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding:1rem;border:2px dashed var(--border);border-radius:var(--radius)}.drop.dropping.svelte-1yd0nzg{border-color:var(--focus)}.drop.svelte-1yd0nzg p:where(.svelte-1yd0nzg){margin:0;font-size:1.125rem}.key.svelte-1yd0nzg{padding:.0625rem .375rem;border-radius:var(--radius-pill);background:var(--bg);color:var(--ink-muted);font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase}.choose.svelte-1yd0nzg{display:inline-flex;align-items:center;padding:0 1rem;font-size:1.125rem;font-weight:700;color:var(--surface);background:var(--danger);border:1px solid var(--danger);border-radius:var(--radius);cursor:pointer}@media(hover:hover){.choose.svelte-1yd0nzg:hover{filter:brightness(.92)}}.choose.svelte-1yd0nzg input:where(.svelte-1yd0nzg){display:none}.fact.svelte-1yd0nzg{margin:0 0 .5rem;font-size:1.125rem}.muted.svelte-1yd0nzg{color:var(--ink-muted)}.row.svelte-o9s8nz{display:flex;flex-wrap:wrap;gap:.75rem;margin-bottom:.75rem}.admin.svelte-9b2mjq{position:fixed;inset:0;z-index:90;display:grid;grid-template-columns:16rem 1fr;background:var(--bg);color:var(--ink);overflow:hidden}.rail.svelte-9b2mjq{display:flex;flex-direction:column;gap:.125rem;padding:1rem .75rem;overflow-y:auto;background:var(--surface);border-right:1px solid var(--border)}h1.svelte-9b2mjq{margin:0 .5rem 1rem;font-size:1.25rem;letter-spacing:-.01em}.group.svelte-9b2mjq{margin:1rem .5rem .375rem;color:var(--ink-muted);font-size:.8125rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase}.entry.svelte-9b2mjq{padding:0 .75rem;height:2.5rem;display:flex;align-items:center;text-align:left;font-size:1.0625rem;border-radius:var(--radius-sm);color:var(--ink-muted);transition:background-color var(--tap) var(--ease),color var(--tap) var(--ease)}@media(hover:hover){.entry.svelte-9b2mjq:hover{background:var(--bg);color:var(--ink)}}.entry.current.svelte-9b2mjq{background:var(--bg);color:var(--ink);font-weight:700;box-shadow:inset .1875rem 0 0 0 var(--focus)}.foot.svelte-9b2mjq{margin-top:auto;padding:1rem .5rem 0;border-top:1px solid var(--border)}.station.svelte-9b2mjq{margin:0;font-size:1rem;font-weight:600}.build.svelte-9b2mjq{margin:.125rem 0 0;font-size:.8125rem;color:var(--ink-muted)}.technical.svelte-9b2mjq{display:flex;align-items:center;gap:.5rem;min-height:2.75rem;margin-top:.75rem;color:var(--ink-muted);font-size:.9375rem}.technical.svelte-9b2mjq input:where(.svelte-9b2mjq){width:1.5rem;height:1.5rem;flex:0 0 auto}.back.svelte-9b2mjq{margin-top:.75rem;padding:0 .5rem;height:2.25rem;font-size:.9375rem;color:var(--ink-muted);border-radius:var(--radius-sm)}@media(hover:hover){.back.svelte-9b2mjq:hover{background:var(--bg);color:var(--ink)}}.body.svelte-9b2mjq{display:flex;flex-direction:column;min-width:0;overflow:hidden}.page.svelte-9b2mjq{flex:1;min-height:0;overflow-y:auto;padding:1.5rem 2rem 3rem}.page.svelte-9b2mjq>*{max-width:68rem}.page-title.svelte-9b2mjq{margin:0 0 1.25rem;font-size:1.75rem;letter-spacing:-.01em}.waiting.svelte-9b2mjq{margin:0;font-size:1.125rem;color:var(--ink-muted)}.banner.svelte-9b2mjq{flex:none;margin:0;padding:.75rem 2rem;font-size:1.0625rem;background:var(--surface);border-bottom:1px solid var(--border)}.notice.svelte-9b2mjq{border-left:.375rem solid var(--ready);background:var(--ready-wash)}.failure.svelte-9b2mjq{border-left:.375rem solid var(--fault);background:var(--fault-wash)}.pending.svelte-9b2mjq{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;border-left:.375rem solid var(--warning);background:var(--warning-wash)}.pending.svelte-9b2mjq p:where(.svelte-9b2mjq){margin:0;flex:1 1 24rem}.save-bar.svelte-9b2mjq{flex:none;padding:.75rem 2rem;background:var(--surface);border-top:1px solid var(--border);box-shadow:var(--shadow-1)}.retired.svelte-9b2mjq{margin:0 0 .5rem;font-size:.9375rem;color:var(--ink-muted)}.faults.svelte-9b2mjq{margin:0 0 .5rem;padding-left:1.25rem;font-size:1rem} diff --git a/internal/web/dist/assets/mount-l2eO5M4C.js b/internal/web/dist/assets/mount-l2eO5M4C.js new file mode 100644 index 0000000..9cc82c6 --- /dev/null +++ b/internal/web/dist/assets/mount-l2eO5M4C.js @@ -0,0 +1,95 @@ +import{R as cr,T as dr,U as vr,V as Sa,W as ra,e as pr,X as fr,Y as gr,Z as mr,_ as Ga,u as ga,r as La,$ as hr,c as Ha,B as Xt,A as F,k as e,z as p,F as Lt,d as At,b as vt,s as r,i as g,t as v,n as Zt,l as Ue,o as i,f as Ze,g as l,v as a,j as o,p as Nt,I as _r,h as $t,q as u,w as Y,a0 as Ta,E as oa,x as at,G as Ja,a1 as la,y as na,a2 as Ut,a3 as ia,a4 as xt,C as ma,K as br,H as wr,P as yr,a5 as xr}from"./app-Bm_67CUJ.js";function Ka(n){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function kr(n,t,...s){var c=new vr(n);cr(()=>{const _=t()??null;c.ensure(_,_&&(w=>_(w,...s)))},dr)}function Xa(n,t,s=!1){if(n.multiple){if(t==null)return;if(!fr(t))return gr();for(var c of n.options)c.selected=t.includes(ua(c));return}for(c of n.options){var _=ua(c);if(mr(_,t)){c.selected=!0;return}}(!s||t!==void 0)&&(n.selectedIndex=-1)}function qr(n){var t=new MutationObserver(()=>{"__value"in n&&Xa(n,n.__value)});t.observe(n,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),Ga(()=>{t.disconnect()})}function Sr(n,t,s=t){var c=new WeakSet,_=!0;Sa(n,"change",w=>{var T=w?"[selected]":":checked",A;if(n.multiple)A=[].map.call(n.querySelectorAll(T),ua);else{var M=n.querySelector(T)??n.querySelector("option:not([disabled])");A=M&&ua(M)}s(A),n.__value=A,ra!==null&&c.add(ra)}),pr(()=>{var w=t();if(n===document.activeElement){var T=ra;if(c.has(T))return}if(Xa(n,w,_),_&&w===void 0){var A=n.querySelector(":checked");A!==null&&(w=ua(A),s(w))}n.__value=w,_=!1}),qr(n)}function ua(n){return"__value"in n?n.__value:n.value}function ca(n,t,s=t){var c=new WeakSet;Sa(n,"input",async _=>{var w=_?n.defaultValue:n.value;if(w=ba(n)?wa(w):w,s(w),ra!==null&&c.add(ra),await hr(),w!==(w=t())){var T=n.selectionStart,A=n.selectionEnd,M=n.value.length;if(n.value=w??"",A!==null){var H=n.value.length;T===A&&A===M&&H>M?(n.selectionStart=H,n.selectionEnd=H):(n.selectionStart=T,n.selectionEnd=Math.min(A,H))}}}),ga(t)==null&&n.value&&(s(ba(n)?wa(n.value):n.value),ra!==null&&c.add(ra)),La(()=>{var _=t();if(n===document.activeElement){var w=ra;if(c.has(w))return}ba(n)&&_===wa(n.value)||n.type==="date"&&!_&&!n.value||_!==n.value&&(n.value=_??"")})}function Ca(n,t,s=t){Sa(n,"change",c=>{var _=c?n.defaultChecked:n.checked;s(_)}),ga(t)==null&&s(n.checked),La(()=>{var c=t();n.checked=!!c})}function ba(n){var t=n.type;return t==="number"||t==="range"}function wa(n){return n===""?null:+n}function Oa(n,t,s,c,_){var w=()=>{c(s[n])};s.addEventListener(t,w),_?La(()=>{s[n]=_()}):w(),(s===document.body||s===window||s===document)&&Ga(()=>{s.removeEventListener(t,w)})}function Ea(n){Ha===null&&Ka(),Xt(()=>{const t=ga(n);if(typeof t=="function")return t})}function Lr(n){Ha===null&&Ka(),Ea(()=>()=>ga(n))}class Ft extends Error{constructor(t,s,c="",_=[]){super(s),this.status=t,this.code=c,this.faults=_,this.name="AdminError"}status;code;faults;get needsPassword(){return this.status===401}}function Er(){return ea("/admin/api/health")}function zr(){return gt("/admin/api/troubleshooting/reprint",{})}function jr(){return gt("/admin/api/troubleshooting/reload-catalog",{})}function Pr(n){return gt("/admin/api/troubleshooting/manual-entry",{on:n})}function Tr(){return gt("/admin/api/troubleshooting/roll-changed",{})}function Cr(n){return gt("/admin/api/troubleshooting/fallback-printer",{on:n})}function Or(){return gt("/admin/api/troubleshooting/test-scale",{})}function Rr(){return gt("/admin/api/troubleshooting/test-printer",{})}function Ar(){return gt("/admin/api/troubleshooting/test-label",{})}async function Ya(n){const t=new FormData;t.append("file",n,n.name);const s=await fetch("/admin/api/catalog/import",{method:"POST",body:t});return ja(s,"POST /admin/api/catalog/import")}const Nr="/admin/api/diagnostic.zip";function $r(n){return gt("/admin/api/session",{password:n})}async function Dr(){await fetch("/admin/api/session",{method:"DELETE"})}function Ir(n,t){return gt("/admin/api/session/recovery",{code:n,password:t})}function za(){return ea("/admin/api/config")}function Ur(n){return Za("PUT","/admin/api/config",n)}function Fr(){return gt("/admin/api/config/confirm",{})}async function Mr(){return(await ea("/admin/api/config/versions")).versions}function Br(n){return gt("/admin/api/config/restore",{version:n})}async function Vr(){return(await ea("/admin/api/ports")).ports}async function Wr(){return(await ea("/admin/api/printers")).printers}async function Gr(){return(await gt("/admin/api/printers/discover",{})).printers}function Hr(n){return gt("/admin/api/scale/detect",{port:n})}async function Ra(n,t){return(await gt("/admin/api/scale/capture",{port:n,seconds:t})).frames}function Qa(n){return gt("/admin/api/printer/test?what="+n,{})}function Jr(n,t,s,c){const _=new URLSearchParams({template:n,demo:t?"1":"0",dual:s?"1":"0"});return _.set("t",String(c)),"/admin/api/label/preview.png?"+_.toString()}async function Kr(n){const t=new URLSearchParams(n);return(await ea("/admin/api/journal?"+t)).weighings}function Xr(n){return"/admin/api/journal/export.csv?"+new URLSearchParams(n).toString()}async function Yr(n){const t=new URLSearchParams(n);return(await ea("/admin/api/technical?"+t)).entries}function Qr(n){const t=n===void 0?"":"?id="+String(n);return ea("/admin/api/imports"+t)}function Zr(){return gt("/admin/api/catalog/reload",{})}function en(){return gt("/admin/api/catalog/forget-quarantine",{})}function Aa(n,t){return gt("/admin/api/products/"+encodeURIComponent(n)+"/decision",t)}function tn(n){return gt("/admin/api/replay",{frame:n})}async function ea(n){const t=await fetch(n,{headers:{accept:"application/json"}});return ja(t,"GET "+n)}function gt(n,t){return Za("POST",n,t)}async function Za(n,t,s){const c=await fetch(t,{method:n,headers:{"content-type":"application/json"},body:JSON.stringify(s)});return ja(c,n+" "+t)}async function ja(n,t){const s=await n.text();if(!n.ok){const c=er(s);throw new Ft(n.status,rn(s,t,n.status)+an(n),c?.code??"",c?.faults??[])}if(s!=="")return JSON.parse(s)}function an(n){if(n.status!==429)return"";const t=Number(n.headers.get("Retry-After")??"");if(!Number.isFinite(t)||t<=0)return"";const s=Math.ceil(t/60);return s>1?` Réessayez dans ${String(s)} minutes.`:" Réessayez dans une minute."}function rn(n,t,s){const c=er(n);return c?.message!==void 0&&c.message!==""?c.message:`${t} a répondu ${String(s)}.`}function er(n){try{return JSON.parse(n)}catch{return null}}function nn(){return ea("/admin/api/update")}function sn(){return gt("/admin/api/update/check",{})}function ln(n){return gt("/admin/api/update/apply",{version:n})}class on{constructor(t){this.admin=t}admin;#t=F(null);get config(){return e(this.#t)}set config(t){p(this.#t,t,!0)}#r=F("");get fingerprint(){return e(this.#r)}set fingerprint(t){p(this.#r,t,!0)}#a=F(Lt([]));get retired(){return e(this.#a)}set retired(t){p(this.#a,t,!0)}#e=F(null);get pending(){return e(this.#e)}set pending(t){p(this.#e,t,!0)}#n=F(Lt([]));get faults(){return e(this.#n)}set faults(t){p(this.#n,t,!0)}#l=F(!1);get dirty(){return e(this.#l)}set dirty(t){p(this.#l,t,!0)}async load(){const t=await this.admin.load(()=>za());t!==null&&(this.config=t.config,this.fingerprint=t.config_fingerprint,this.retired=t.retired_keys??[],this.pending=t.pending_confirmation,this.faults=[],this.dirty=!1)}value(t){let s=this.config;for(const c of t.split(".")){if(s===null||typeof s!="object")return;s=s[c]}return s}text(t){const s=this.value(t);return s==null?"":String(s)}number(t){const s=this.value(t);return typeof s=="number"?s:Number(s??0)}flag(t){return this.value(t)===!0}set(t,s){if(this.config===null)return;const c=t.split("."),_=c.pop();if(_===void 0)return;let w=this.config;for(const T of c){const A=w[T];(A===null||typeof A!="object")&&(w[T]={}),w=w[T]}w[_]=s,this.dirty=!0}unset(t){const s=t.split("."),c=s.pop();if(c===void 0||this.config===null)return!1;let _=this.config;for(const w of s){if(_===null||typeof _!="object")return!1;_=_[w]}return _===null||typeof _!="object"?!1:(delete _[c],this.dirty=!0,!0)}async save(){if(this.config===null)return!1;this.faults=[];const t=this.config;let s;try{s=await Ur(t)}catch(c){if(Na(c))throw c;return this.admin.report(c),this.faults=un(this.admin),!1}return this.config=s.config,this.fingerprint=s.config_fingerprint,this.retired=s.retired_keys??[],this.pending=s.pending_confirmation,this.dirty=!1,!0}async confirm(){try{await Fr()}catch(t){if(Na(t))throw t;this.admin.report(t);return}this.admin.notice="La configuration est confirmée.",this.pending=null,await this.admin.refresh()}dropRetired(t){if(t.includes("[")){this.admin.actionError=`« ${t} » est logée dans un tableau : cet écran ne sait pas la retirer de là. Modifiez le fichier de configuration lui-même.`;return}this.unset(t)&&(this.retired=this.retired.filter(s=>s!==t))}}function un(n){return n.lastFaults}function Na(n){return n instanceof Ft&&(n.status===401||n.status===409)}const cn={"station.number":"Numéro du poste","station.name":"Nom du poste","station.coop":"Nom de la coopérative","network.listen":"Adresse d’écoute","network.admin_on_lan":"Administration accessible depuis le réseau","ui.language":"Langue","ui.sound":"Son","ui.idle_timeout_s":"Retour à l’accueil après (secondes)","ui.reprint_window_s":"Réimpression possible pendant (secondes)","ui.show_grid_prices":"Afficher les prix sur les tuiles","ui.show_by_unit_products":"Afficher les produits vendus à l’unité","scale.type":"Protocole de la balance","scale.present":"Ce poste a une balance","scale.manual_entry_allowed":"Saisie manuelle autorisée","scale.degrade_after_s":"Passer en mode dégradé après (secondes)","scale.options.port":"Port série","scale.options.baud":"Vitesse (bauds)","scale.options.bits":"Bits de données","scale.options.parity":"Parité","scale.options.stop":"Bits d’arrêt","scale.options.backoff_min_ms":"Attente minimale avant réessai (ms)","scale.options.backoff_max_ms":"Attente maximale avant réessai (ms)","printer.type":"Pilote d’impression","printer.template":"Gabarit d’étiquette","printer.options.transport":"Transport","printer.options.queue":"File d’impression","printer.options.path":"Fichier de périphérique","printer.options.address":"Adresse réseau","printer.options.darkness":"Noircissement","printer.options.speed":"Vitesse d’impression","printer.options.offset_x":"Décalage horizontal (dots)","printer.options.offset_y":"Décalage vertical (dots)","printer.options.invert_bits":"Inverser les points","printer.options.copies":"Exemplaires","printer.options.roll_capacity":"Étiquettes par rouleau","pricing.amount_rounding":"Arrondi du montant","pricing.unit_price_rounding":"Arrondi du prix au kilo","pricing.primary_code":"Tarif principal","pricing.reference_code":"Tarif de référence","barcode.verify_reference_check_digit":"Vérifier la clé de contrôle","limits.empty_max_g":"Plateau considéré vide en dessous de (g)","limits.basket_check_enabled":"Vérifier la présence du panier","limits.basket_min_g":"Poids du panier, borne basse (g)","limits.basket_max_g":"Poids du panier, borne haute (g)","limits.min_weight_g":"Poids minimum accepté","limits.max_weight_g":"Poids maximum accepté","limits.max_tare_g":"Tare maximale","limits.min_units":"Unités minimum","limits.max_units":"Unités maximum","limits.max_amount_cents":"Montant maximum (centimes)","stability.mode":"Exigence de stabilité","stability.min_duration_ms":"Durée de stabilité (ms)","stability.tolerance_g":"Tolérance de stabilité (g)","stability.timeout_ms":"Délai d’attente de stabilité (ms)","stability.on_timeout":"Au bout du délai","stability.min_latch_rate":"Taux d’accroche minimal","stability.latch_rate_window_ms":"Fenêtre de mesure du taux (ms)","stability.expiry_floor_ms":"Péremption, plancher (ms)","stability.expiry_ceiling_ms":"Péremption, plafond (ms)","stability.expiry_factor":"Péremption, facteur","catalog.type":"Où le poste va chercher le catalogue","catalog.options.directory":"Répertoire surveillé","catalog.options.url":"Adresse du serveur","catalog.options.username":"Compte","catalog.options.password":"Mot de passe","catalog.options.separator":"Séparateur du CSV","catalog.options.poll_interval_s":"Vérifier toutes les (secondes)","catalog.options.stable_polls":"Vérifications avant lecture","catalog.options.max_file_size_mb":"Taille maximale du fichier (Mo)","catalog.options.max_image_size_kb":"Taille maximale d’une image (Ko)","catalog.options.min_readable_ratio":"Part minimale de lignes lisibles","catalog.options.max_weighable_drop":"Baisse maximale des produits pesables","catalog.options.max_archives":"Archives conservées","catalog.options.archive_days":"Archives conservées (jours)","catalog.options.failures_before_reject":"Échecs avant mise en quarantaine","catalog.images.source":"Origine des photos","catalog.images.path":"Répertoire des photos","catalog.fallback_category":"Rayon par défaut","journal.max_rows":"Pesées conservées","journal.max_days":"Pesées conservées (jours)","journal.max_technical":"Événements techniques conservés","admin.session_minutes":"Durée d’une session (minutes)","admin.attempts_per_minute":"Tentatives par minute","maintenance.weekly_integrity_check":"Contrôle d’intégrité hebdomadaire","maintenance.disk_alert_mb":"Alerte disque en dessous de (Mo)"};function Gt(n){return cn[n]??n}const dn={station:"l’identité du poste",network:"le réseau",ui:"l’écran client",scale:"la balance",printer:"l’imprimante",pricing:"les tarifs",barcode:"le code-barres",limits:"les garde-fous",stability:"la stabilité",catalog:"le catalogue",journal:"le journal",maintenance:"la maintenance"};function vn(n){return dn[n]??n}const pn={scale:"balance",printer:"imprimante",catalog:"catalogue",ui:"écran",config:"configuration",http:"réseau",system:"système"};function tr(n){return pn[n]??"origine inconnue"}const xa="openscale.admin.preferences",ar="technical";class fn{#t=F(Lt(gn()));get showTechnicalNames(){return e(this.#t)}set showTechnicalNames(t){p(this.#t,t,!0)}toggleTechnicalNames(){this.showTechnicalNames=!this.showTechnicalNames,mn(this.showTechnicalNames)}}function gn(){try{return globalThis.localStorage?.getItem(xa)===ar}catch{return!1}}function mn(n){try{n?globalThis.localStorage?.setItem(xa,ar):globalThis.localStorage?.removeItem(xa)}catch{}}const Ht=new fn,hn=3e3,_n=["hardware","label","rules","catalog","journal","station","update"];function $a(n){return _n.includes(n)}function bn(n){return n instanceof Ft&&(n.status===401||n.status===409)}class wn{constructor(t=hn){this.periodMs=t}periodMs;#t=F(null);get health(){return e(this.#t)}set health(t){p(this.#t,t,!0)}#r=F("dashboard");get page(){return e(this.#r)}set page(t){p(this.#r,t,!0)}#a=F(!1);get expert(){return e(this.#a)}set expert(t){p(this.#a,t,!0)}#e=F("");get linkError(){return e(this.#e)}set linkError(t){p(this.#e,t,!0)}#n=F("");get actionError(){return e(this.#n)}set actionError(t){p(this.#n,t,!0)}#l=F(!1);get needsFirstPassword(){return e(this.#l)}set needsFirstPassword(t){p(this.#l,t,!0)}#d=F("");get notice(){return e(this.#d)}set notice(t){p(this.#d,t,!0)}#v=F(!1);get busy(){return e(this.#v)}set busy(t){p(this.#v,t,!0)}#p=F(Lt([]));get lastFaults(){return e(this.#p)}set lastFaults(t){p(this.#p,t,!0)}#f=F(null);get pending(){return e(this.#f)}set pending(t){p(this.#f,t,!0)}#i=null;#o=null;start(){this.refresh(),this.#i=setInterval(()=>{this.refresh()},this.periodMs)}stop(){this.#i!==null&&clearInterval(this.#i),this.#i=null}async refresh(){try{this.health=await Er(),this.linkError=""}catch(t){this.linkError=Da(t)}}#u(){this.busy=!0,this.notice="",this.actionError="",this.needsFirstPassword=!1}#s(t){this.actionError=Da(t),this.needsFirstPassword=t instanceof Ft&&t.status===409,this.#m(t)}async run(t){this.#u();let s=null;try{s=await t(),this.notice=s.message}catch(c){this.#s(c)}finally{this.busy=!1}return await this.refresh(),s}async login(t){this.#u();try{return await $r(t),this.expert=!0,this.notice="Session d’administration ouverte.",!0}catch(s){return this.expert=!1,this.#s(s),!1}finally{this.busy=!1}}async recover(t,s){this.#u();try{const c=await Ir(t,s);return this.expert=!0,this.notice=c.warning??"Le mot de passe est remplacé et la session est ouverte.",!0}catch(c){return this.#s(c),!1}finally{this.busy=!1}}async protect(t){try{return await t()}catch(s){if(!bn(s))return this.#s(s),null;if(!await this.#g(s))return null;try{return await t()}catch(c){return this.#s(c),null}}}#g(t){return this.pending={kind:t.status===409?"first-password":"password",message:t.message},new Promise(s=>{this.#o=s})}async answerPassword(t){await this.login(t)&&this.#c(!0)}async answerRecovery(t,s){await this.recover(t,s)&&this.#c(!0)}cancelPassword(){this.#c(!1)}#c(t){this.pending=null,this.notice="";const s=this.#o;this.#o=null,s?.(t)}async logout(){await Dr(),this.expert=!1,this.page="dashboard",this.notice="Session d’administration fermée."}open(t){this.actionError="",this.notice="",this.needsFirstPassword=!1,this.page=t}async load(t){try{const s=await t();return this.actionError="",this.lastFaults=[],s}catch(s){return this.lastFaults=s instanceof Ft?s.faults:[],this.#s(s),null}}report(t){this.lastFaults=t instanceof Ft?t.faults:[],this.#s(t)}#m(t){t instanceof Ft&&t.needsPassword&&(this.expert=!1)}}function Da(n){return n instanceof Ft?n.message:n instanceof Error?"Le poste n’a pas répondu : "+n.message:"Le poste n’a pas répondu."}var yn=o('clé'),xn=o('');function tt(n,t){const s=vt(t,"kind",3,"read"),c=vt(t,"busy",3,!1),_=vt(t,"disabled",3,!1),w=vt(t,"protected",3,!1),T=vt(t,"act",3,void 0);var A=xn();let M;var H=a(A),J=r(H);{var pe=fe=>{var _e=yn();l(fe,_e)};g(J,fe=>{w()&&fe(pe)})}v(()=>{M=Zt(A,1,`act ${s()??""}`,"svelte-5tpq0o",M,{"touch-target":s()==="destructive",busy:c()}),Ue(A,"data-kind",s()),Ue(A,"data-act",T()),A.disabled=_()||c(),i(H,`${(c()?"En cours…":t.label)??""} `)}),Ze("click",A,function(...fe){t.onrun?.apply(this,fe)}),l(n,A)}At(["click"]);var kn=o(`

                                    Le code de secours a été tiré à l’installation de ce poste et imprimé + sur sa fiche, rangée dans le classeur du magasin. Il n’existe nulle part ailleurs.

                                    `,1),qn=o(`

                                    Oublié ? Le code de secours de la fiche d’installation en repose un. + Un responsable peut aussi le faire en ligne de commande, avec openscale config password.

                                    `,1),Sn=o('

                                    '),Ln=o('');function En(n,t){Nt(t,!0);const s=u(()=>t.admin.pending?.kind==="first-password");let c=F(""),_=F("");const w=8,T=u(()=>e(s)?e(_).trim().length>0&&e(c).length>=w:e(c).length>0);async function A(){!e(T)||t.admin.busy||(e(s)?await t.admin.answerRecovery(e(_),e(c)):await t.admin.answerPassword(e(c)),p(c,""),p(_,""))}var M=Ln(),H=a(M),J=a(H),pe=a(J),fe=a(pe);_r(fe,{name:"settings",size:"1.5rem"});var _e=r(pe,2),ae=a(_e),C=r(J,2),ge=a(C),Q=r(C,2);{var be=N=>{var L=kn(),q=r(Y(L),2),D=r(a(q),2);Ta(D);var X=r(q,2),me=a(X);me.textContent="Nouveau mot de passe — 8 caractères au moins";var ze=r(me,2);Ze("keydown",D,Ce=>Ce.key==="Enter"&&void A()),ca(D,()=>e(_),Ce=>p(_,Ce)),Ze("keydown",ze,Ce=>Ce.key==="Enter"&&void A()),ca(ze,()=>e(c),Ce=>p(c,Ce)),l(N,L)},se=N=>{var L=qn(),q=Y(L),D=r(a(q),2);Ta(D),Ze("keydown",D,X=>X.key==="Enter"&&void A()),ca(D,()=>e(c),X=>p(c,X)),l(N,L)};g(Q,N=>{e(s)?N(be):N(se,-1)})}var P=r(Q,2);{var ne=N=>{var L=Sn(),q=a(L);v(()=>i(q,t.admin.actionError)),l(N,L)};g(P,N=>{t.admin.actionError!==""&&N(ne)})}var V=r(P,2),B=a(V);tt(B,{label:"Annuler",onrun:()=>t.admin.cancelPassword()});var re=r(B,2);{let N=u(()=>e(s)?"Poser ce mot de passe":"Continuer"),L=u(()=>!e(T)||t.admin.busy);tt(re,{kind:"write",get label(){return e(N)},get disabled(){return e(L)},onrun:()=>{A()}})}v(()=>{i(ae,e(s)?"Ce poste n’a pas encore de mot de passe":"Mot de passe d’administration"),i(ge,t.admin.pending?.message??"")}),l(n,M),$t()}At(["keydown"]);var zn=o(' '),jn=o(""),Pn=o(""),Tn=o('

                                    '),Cn=o(" "),On=o('

                                    '),Rn=o('
                                    ');function _t(n,t){Nt(t,!0);const s=vt(t,"hint",3,""),c=vt(t,"kind",3,"text"),_=vt(t,"disabled",3,!1),w=vt(t,"fault",3,""),T=vt(t,"allowed",19,()=>[]),A=u(()=>`field-${t.path.replace(/\./gu,"-")}`);var M=Rn();let H;var J=a(M),pe=a(J),fe=a(pe),_e=r(pe,2);{var ae=V=>{var B=zn(),re=a(B);v(()=>i(re,t.path)),l(V,B)};g(_e,V=>{Ht.showTechnicalNames&&V(ae)})}var C=r(J,2),ge=r(C,2);{var Q=V=>{var B=Pn();at(B,20,T,re=>re,(re,N)=>{var L=jn(),q={};v(()=>{q!==(q=N)&&(L.value=(L.__value=N)??"")}),l(re,L)}),v(()=>Ue(B,"id",e(A)+"-allowed")),l(V,B)};g(ge,V=>{T().length>0&&V(Q)})}var be=r(ge,2);{var se=V=>{var B=Tn(),re=a(B);v(()=>i(re,s())),l(V,B)};g(be,V=>{s()!==""&&V(se)})}var P=r(be,2);{var ne=V=>{var B=On(),re=a(B),N=r(re);{var L=q=>{var D=Cn(),X=a(D);v(me=>i(X,`Valeurs acceptées : ${me??""}.`),[()=>T().join(", ")]),l(q,D)};g(N,q=>{T().length>0&&q(L)})}v(()=>i(re,`${w()??""} `)),l(V,B)};g(P,V=>{w()!==""&&V(ne)})}v(()=>{H=Zt(M,1,"field svelte-1oilv5s",null,H,{refused:w()!==""}),Ue(J,"for",e(A)),i(fe,t.label),Ue(C,"id",e(A)),Ue(C,"type",c()),oa(C,t.value),C.disabled=_(),Ue(C,"list",T().length>0?e(A)+"-allowed":void 0)}),Ze("input",C,V=>t.onchange(V.currentTarget.value)),l(n,M),$t()}At(["input"]);const Ia=1e3;function pa(n){const t=Pa(n);return t===null?"":[sa(t.getDate()),sa(t.getMonth()+1),String(t.getFullYear())].join("/")}function Mt(n){const t=Pa(n);return t===null?"":`${pa(n)} à ${sa(t.getHours())}:${sa(t.getMinutes())}`}function An(n){const t=Pa(n);return t===null?"":[sa(t.getHours()),sa(t.getMinutes()),sa(t.getSeconds())].join(":")}function ka(n){const t=n/1e6;return t<1e3?`${Math.round(t)} Mo`:`${rr(t/1e3)} Go`}function he(n){const t=String(Math.abs(Math.trunc(n))),s=[];for(let c=t.length;c>0;c-=3)s.unshift(t.slice(Math.max(0,c-3),c));return(n<0?"-":"")+s.join(" ")}function va(n){return n0&&w.push({count:Rt(n.anomalies_count),label:"anomalies",note:"à corriger dans Odoo",link:Ua(n.anomalies_count)}),n.unit_mismatches_count>0){const T=n.unit_mismatches_count;w.push({count:"+ "+Rt(T),label:T===1?"unité divergente":"unités divergentes",note:"pesable, unité à corriger",link:Ua(T)})}return{headline:`Catalogue du ${pa(n.occurred_at)} — ${Rt(s)} produits reçus`,lines:w,oneLine:Nn(n)}}function Nn(n){const t=[`${Rt(n.rows_read_count)} reçus`,`${Rt(n.weighable_count)} pesables`,`${Rt(n.not_weighable_count)} non pesables`,`${Rt(n.anomalies_count)} anomalies`],s=n.unit_mismatches_count;return s>0&&t.push(`${Rt(s)} ${s===1?"unité divergente":"unités divergentes"}`),t.join(" · ")}const $n={applied:"appliqué",unchanged:"identique au précédent",rejected:"refusé",failed:"échec"},Dn={local_drop:"dépôt local",webdav:"WebDAV",manual:"déposé sur l’écran"};function ha(n){return $n[n]??"résultat inconnu"}function sr(n){return Dn[n]??"source inconnue"}function In(n,t){const s=[n.file_name===""?"Le dernier fichier":n.file_name,ha(n.result)],c=Mt(n.occurred_at);c!==""&&s.push("le "+c),s.push("via "+sr(n.source));const _=n.reason===""?"":" "+n.reason;return`${s.join(" ")} — ${nr(n,t).oneLine}.${_}`}function Un(n){return n===""?"Aucun nouvel import enregistré à cet instant, et ce poste ne publie pas ce qu’il surveille.":"Aucun nouvel import enregistré à cet instant. Le poste surveille "+n+" : le fichier n’y était pas, ou il n’a pas encore fini d’arriver."}function Fn(n){return"Ce poste n’a pas de journal : il ne pourra rien dire de l’issue de cette relecture."+(n===""?"":" Le poste surveille "+n+".")}function Ua(n){return n===1?"voir la ligne":`voir les ${Rt(n)} lignes`}function Mn(n){return n.map(t=>`${Bn(t)} (${Rt(t.count)})`).join(", ")}function Bn(n){switch(n.code){case"PREPACKAGED_PRODUCT":return"préemballés";case"INTERNAL_CODE_NOT_WEIGHABLE":return n.value===""?"code interne":"code interne "+n.value;case"NO_BARCODE":return"sans code-barres";default:return n.code}}function Rt(n){return String(n)}var Vn=o(' '),Wn=o(''),Gn=o('
                                    '),Hn=o('

                                    ');function lr(n,t){Nt(t,!0);const s=u(()=>nr(t.record,t.motives));var c=Hn(),_=a(c),w=a(_),T=r(_,2);at(T,21,()=>e(s).lines,H=>H.label,(H,J)=>{var pe=Gn(),fe=a(pe),_e=a(fe),ae=r(fe,2),C=a(ae),ge=a(C),Q=r(C,2);{var be=ne=>{var V=Vn(),B=a(V);v(()=>i(B,e(J).note)),l(ne,V)};g(Q,ne=>{e(J).note!==""&&ne(be)})}var se=r(Q,2);{var P=ne=>{var V=Wn(),B=a(V);v(()=>i(B,e(J).link)),Ze("click",V,()=>t.onshowrows(e(J).label==="anomalies"?"anomalies":"units")),l(ne,V)};g(se,ne=>{e(J).link!==""&&t.onshowrows!==void 0&&ne(P)})}v(()=>{Ue(pe,"data-row",e(J).label),i(_e,e(J).count),i(ge,e(J).label)}),l(H,pe)});var A=r(T,2),M=a(A);v(()=>{i(w,e(s).headline),i(M,e(s).oneLine)}),l(n,c),$t()}At(["click"]);var Jn=o('

                                    '),Kn=o('

                                    ');function Qe(n,t){const s=vt(t,"note",3,"");var c=Kn(),_=a(c),w=a(_),T=r(_,2);{var A=H=>{var J=Jn(),pe=a(J);v(()=>i(pe,s())),l(H,J)};g(T,H=>{s()!==""&&H(A)})}var M=r(T,2);kr(M,()=>t.children),v(()=>i(w,t.title)),l(n,c)}const Xn=-1,Yn=10;class ir{#t=F("");get sentence(){return e(this.#t)}set sentence(t){p(this.#t,t,!0)}#r=0;#a="";#e=0;begin(t){this.sentence="",this.#r=t.last_import_id,this.#a=t.watched,this.#e=Yn}forget(){this.sentence="",this.#e=0}observe(t){if(this.#e===0)return;if(t.counters.journal_rows_count===Xn){this.#n(Fn(this.#a));return}const s=t.catalog;if(s!==null&&s.id!==this.#r){this.#n(In(s,t.catalog_motives));return}this.#e-=1,this.#e===0&&(this.sentence=Un(this.#a))}#n(t){this.sentence=t,this.#e=0}}const ya=(n,t=ia,s=ia)=>{var c=Zn(),_=a(c);at(_,21,t,ma,(w,T)=>{var A=Qn(),M=a(A),H=a(M),J=r(M,2),pe=a(J),fe=r(J,2),_e=a(fe),ae=r(fe,2),C=a(ae);v(ge=>{i(H,`ligne ${ge??""}`),i(pe,e(T).product_id),i(_e,e(T).value),i(C,e(T).message)},[()=>he(e(T).csv_line)]),l(w,A)}),v(()=>Ue(c,"data-rows",s())),l(n,c)};var Qn=o('
                                  • '),Zn=o('
                                      '),es=o(" "),ts=o(''),as=o('

                                      Lecture des réglages du poste…

                                      '),rs=o(`

                                      Le poste y cherche le fichier , et le supprime + une fois lu : c’est ce qui dit au producteur que la livraison est prise.

                                      `,1),ns=o(`

                                      Sur un serveur WebDAV, le dépôt d’un fichier CSV depuis cet écran n’est plus + possible : le poste n’a plus de répertoire local où l’écrire. C’est le seul recours + du jour de la mise en service.

                                      `,1),ss=o(`

                                      Ce poste ne déclare aucune source : choisissez-en une ci-dessus, sinon il n’ira + chercher aucun catalogue.

                                      `),ls=o('
                                      ',1),is=o('

                                      Aucun import enregistré sur ce poste.

                                      '),os=o('

                                      '),us=o(`

                                      « Oublier la quarantaine » fait relire un fichier que le poste avait écarté : c’est le + seul geste de cette page qui puisse remettre en service un catalogue refusé.

                                      `,1),cs=o(`

                                      Un produit masqué reste vendable : la caisse lit toujours son code-barres, et une + étiquette déjà imprimée reste valable. Ce réglage ne fait que retirer sa tuile.

                                      `,1),ds=o('

                                      '),vs=o(`

                                      Déposez ici le fichier clé

                                      Ce dépôt remplace toute la grille par le fichier apporté : il change ce que le poste + vend, et le mot de passe est donc demandé au moment du dépôt.

                                      `,1),ps=o('

                                      '),fs=o('

                                      Aucune anomalie sur le dernier import.

                                      '),gs=o('

                                      ',1),ms=o('

                                      '),hs=o('

                                      Aucune unité divergente sur le dernier import.

                                      '),_s=o('

                                      ',1),bs=o('

                                      '),ws=o('

                                      Aucun produit non pesable sur le dernier import.

                                      '),ys=o('

                                      ',1),xs=o('

                                      Aucun import enregistré : rien n’a encore pu être retiré.

                                      '),ks=o('

                                      Aucun produit retiré par le dernier import.

                                      '),qs=o(`

                                      Ils restent enregistrés avec leur historique : une étiquette déjà collée reste + lisible en caisse, et un produit qui revient dans un prochain fichier retrouve sa + tuile. Ce poste n’en publie encore que le nombre — leurs noms se lisent dans Odoo, + en comparant avec l’export précédent.

                                      `,1),Ss=o('
                                    • '),Ls=o(`

                                        Un produit retiré de la grille ne se trouve plus ici : il se reprend dans + « Décisions en vigueur », plus bas.

                                        `,1),Es=o(`

                                        Sans motif, le poste refuse la décision. Le seul acte qui s’en passe est celui qui + EFFACE la décision en vigueur : proposer de nouveau un produit qui ne porte aucune + dérogation.

                                        `),zs=o(`

                                        Produit choisi :

                                        Ce produit est-il proposé dans la grille ?

                                        La dérogation s’enregistre seule : elle ne remet pas dans la grille un produit + qui en a été retiré, et retirer un produit n’efface pas sa dérogation.

                                        `),js=o(' ',1),Ps=o('

                                        Aucune décision locale : la grille est celle du fichier.

                                        '),Ts=o('
                                      • '),Cs=o('

                                          ',1),Os=o('

                                          '),Rs=o('

                                          Aucun import dans l’historique.

                                          '),As=o(' '),Ns=o('

                                          QuandFichierSourceRésultatMotifLuesPesablesNon pesablesAnomaliesRetirés
                                          ',1),$s=o('
                                          ');function Ds(n,t){Nt(t,!0);const s=(b,G=ia)=>{var oe=ts(),qe=a(oe),Je=r(qe,2),Se=a(Je),Pe=a(Se),j=r(Se,2);{var U=Ke=>{var Ye=es(),ue=a(Ye);v(()=>i(ue,G().path)),l(Ke,Ye)};g(j,Ke=>{Ht.showTechnicalNames&&Ke(U)})}var xe=r(j,2),Xe=a(xe);v((Ke,Ye)=>{Ue(oe,"data-flag",G().path),Ue(oe,"data-on",Ke),la(qe,Ye),i(Pe,G().label),i(Xe,G().hint)},[()=>String(t.draft.flag(G().path)),()=>t.draft.flag(G().path)]),Ze("change",qe,Ke=>t.draft.set(G().path,Ke.currentTarget.checked)),l(b,oe)},c=vt(t,"admin",7),_=20,w=50,T=20,A=["NO_BARCODE","PREPACKAGED_PRODUCT","INTERNAL_CODE_NOT_WEIGHABLE"],M={local_drop:["catalog.options.directory"],webdav:["catalog.options.url","catalog.options.username","catalog.options.password"]},H=u(()=>t.draft.text("catalog.type"));let J=F(Lt([])),pe=F(Lt([])),fe=F(Lt([])),_e=F(""),ae=F(""),C=F(""),ge=F(""),Q=F(!1),be=F(""),se=F(""),P=F("loading"),ne=F("loading");const V=u(()=>c().busy||e(se)!==""),B=u(()=>e(pe).filter(b=>b.issue==="anomaly")),re=u(()=>e(pe).filter(b=>A.includes(b.code))),N=u(()=>e(pe).filter(b=>b.code==="UNIT_MISMATCH")),L=u(()=>new Map(e(fe).map(b=>[b.id,b.name]))),q=u(()=>e(fe).filter(b=>b.mode==="by_unit").length),D=u(()=>{if(e(ne)==="loading")return"Lecture du catalogue en service…";if(e(ne)==="unread")return"Le catalogue en service n’a pas pu être lu : cet écran ne sait pas combien de produits se vendent à l’unité.";if(e(q)===0)return"Aucun produit vendu à l’unité dans le catalogue en service.";const b=e(q)>1,G=b?"produits vendus à l’unité sont":"produit vendu à l’unité est",oe=t.draft.flag("ui.show_by_unit_products")?`${b?"montrés":"montré"} dans la grille de ce poste`:`${b?"masqués":"masqué"} sur ce poste`;return`${he(e(q))} ${G} ${oe}.`}),X=u(()=>e(_e)===""?[]:br(e(fe),wr,e(_e))),me=u(()=>e(X).slice(0,_)),ze=u(()=>e(B).slice(0,w)),Ce=u(()=>e(N).slice(0,w)),Le=u(()=>e(re).slice(0,w)),Z=u(()=>[...t.health.decisions].sort((b,G)=>G.decided_at.localeCompare(b.decided_at))),y=u(()=>e(Z).slice(0,T)),I=u(()=>e(ae)===""?null:e(Z).find(b=>b.product_id===e(ae))??null),W=u(()=>e(I)===null?!0:e(I).offered),ee=u(()=>e(I)?.min_weight_g??null),ce=u(()=>e(ge).trim()===""?null:Number(e(ge))),Ve=u(()=>e(ce)!==null&&Number.isFinite(e(ce))&&e(ce)>0),S=u(()=>e(C).trim()),h=u(()=>e(S)!==""||!Ae(!1,e(ee))),k=u(()=>e(S)!==""||!Ae(!0,e(ee))),O=u(()=>e(Ve)&&(e(S)!==""||!Ae(e(W),e(ce)))),le=u(()=>e(S)!==""||!Ae(e(W),null)),Re=u(()=>t.health.catalog?.products_withdrawn_count??0),We=u(()=>e(J).find(b=>t.health.catalog!==null&&b.id`${he(e(Re))} ${e(Re)>1?"produits retirés":"produit retiré"}`+(e(We)===null?".":` depuis l’import du ${Mt(e(We).occurred_at)}.`)),d=u(()=>e(X).length>e(me).length?`${he(e(me).length)} produits affichés sur ${he(e(X).length)} trouvés — précisez votre recherche.`:`${he(e(X).length)} ${e(X).length>1?"produits trouvés":"produit trouvé"}.`),x=u(()=>`${he(e(J).length)} ${e(J).length>1?"imports affichés":"import affiché"} : le poste n’en publie jamais plus de vingt.`),$=u(()=>e(P)==="loading"?"Lecture des signalements du dernier import…":"Les signalements du dernier import n’ont pas pu être lus : cet écran ne sait pas ce qu’ils disent."),f=u(()=>e(P)==="loading"?"Lecture de l’historique des imports…":"L’historique des imports n’a pas pu être lu : cet écran ne sait pas ce qu’il contient.");let E;Xt(()=>{const b=t.health.catalog?.id??null;b!==E&&(E=b,ye())});const z=new ir;Xt(()=>{z.observe(t.health)});async function ye(){const b=await c().load(()=>Qr(t.health.catalog?.id));b===null?p(P,"unread"):(p(J,b.imports,!0),p(pe,b.findings,!0),p(P,"read"));const G=await c().load(()=>Ja());if(G===null){p(ne,"unread");return}p(fe,G.products,!0),p(ne,"read")}async function ie(b,G){p(se,b,!0),c().actionError="",c().notice="",z.forget();try{const oe=await c().protect(G);return oe===null?null:(c().notice=oe.message,await c().refresh(),oe)}finally{p(se,"")}}async function ke(){const b=await ie("reload",Zr);b!==null&&z.begin(b)}function Oe(b){t.draft.set("catalog.type",b);for(const[G,oe]of Object.entries(M))if(G!==b)for(const qe of oe)t.draft.unset(qe)}function de(b){return t.draft.faults.find(G=>G.field===b)?.message??""}function K(b){p(ae,b,!0),p(C,"");const G=e(Z).find(oe=>oe.product_id===b)?.min_weight_g??null;p(ge,G===null?"":String(G),!0)}function Ae(b,G){return!(b&&G===null)}async function Ge(b){if(e(ae)==="")return;const G=e(ae),oe=e(ee),qe=e(S);await ie("offered",()=>Aa(G,{offered:b,min_weight_g:oe,reason:qe}))!==null&&p(C,"")}async function Fe(b){if(e(ae)==="")return;const G=e(ae),oe=e(W),qe=e(S);await ie(b===null?"waiver-off":"waiver",()=>Aa(G,{offered:oe,min_weight_g:b,reason:qe}))!==null&&p(C,"")}async function De(b){if(p(Q,!1),b!=null){p(be,""),c().actionError="",c().notice="",p(se,"import");try{const G=await c().protect(()=>Ya(b));if(G===null)return;p(be,`${b.name} : ${he(G.rows_read_count)} lignes lues, ${he(G.weighable_count)} pesables. `+(G.reason===""?"Le fichier est déposé ; son résultat s’inscrira dans l’historique des imports.":G.reason)),await c().refresh()}finally{p(se,"")}}}function Me(b){b.preventDefault(),p(Q,!1);const G=b.dataTransfer?.files.item(0);if(e(V)){p(be,`${G?.name??"Ce fichier"} n’a pas été déposé : un acte est déjà en cours sur cette page. Réessayez quand il aura répondu.`);return}De(G)}function te(b){const G=b.files?.item(0);b.value="",De(G)}function Ne(b){const G=e(L).get(b);return G!==void 0?G:e(ne)==="loading"?"Lecture du nom…":e(ne)==="unread"?"Nom inconnu : le catalogue n’a pas pu être lu":"Produit absent du catalogue en service"}function ve(b,G,oe,qe){const Je=G>1?qe:oe;return b>=G?`${he(G)} ${Je}.`:`${he(b)} lignes affichées sur ${he(G)} ${Je}.`}function je(b){const G=[];return b.offered||G.push("retiré de la grille"),b.min_weight_g!==null&&G.push(`peut peser à partir de ${he(b.min_weight_g)} g`),G.length===0?"aucune restriction":G.join(" · ")}function $e(b){const G=ha(b.result);return b.code===""?G:`${G} (${b.code})`}function st(b){return sr(b.source)}var it=$s(),pt=a(it);Qe(pt,{title:"Où le poste va chercher le catalogue",children:(b,G)=>{var oe=ls(),qe=Y(oe),Je=a(qe),Se=a(Je),Pe=r(Je,2),j=a(Pe),U=r(qe,2);{var xe=ue=>{var Ie=as();l(ue,Ie)},Xe=ue=>{var Ie=rs(),Be=Y(Ie);{let ut=u(()=>Gt("catalog.options.directory")),ht=u(()=>t.draft.text("catalog.options.directory")),kt=u(()=>de("catalog.options.directory"));_t(Be,{get label(){return e(ut)},path:"catalog.options.directory",get value(){return e(ht)},hint:"Laissez vide pour le répertoire du poste, celui que le service crée lui-même. Un répertoire nommé ici doit exister : le poste ne le crée pas.",get fault(){return e(kt)},onchange:zt=>t.draft.set("catalog.options.directory",zt)})}var ct=r(Be,2),dt=r(a(ct)),ot=a(dt);v(()=>i(ot,`flv_${t.health.station??""}.csv`)),l(ue,Ie)},Ke=ue=>{var Ie=ns(),Be=Y(Ie);{let ot=u(()=>Gt("catalog.options.url")),ut=u(()=>t.draft.text("catalog.options.url")),ht=u(()=>de("catalog.options.url"));_t(Be,{get label(){return e(ot)},path:"catalog.options.url",get value(){return e(ut)},get fault(){return e(ht)},onchange:kt=>t.draft.set("catalog.options.url",kt)})}var ct=r(Be,2);{let ot=u(()=>Gt("catalog.options.username")),ut=u(()=>t.draft.text("catalog.options.username")),ht=u(()=>de("catalog.options.username"));_t(ct,{get label(){return e(ot)},path:"catalog.options.username",get value(){return e(ut)},get fault(){return e(ht)},onchange:kt=>t.draft.set("catalog.options.username",kt)})}var dt=r(ct,2);{let ot=u(()=>Gt("catalog.options.password")),ut=u(()=>de("catalog.options.password"));_t(dt,{get label(){return e(ot)},path:"catalog.options.password",kind:"password",value:"",hint:"Laissez vide : le mot de passe actuel est conservé.",get fault(){return e(ut)},onchange:ht=>t.draft.set("catalog.options.password",ht)})}l(ue,Ie)},Ye=ue=>{var Ie=ss();l(ue,Ie)};g(U,ue=>{t.draft.config===null?ue(xe):e(H)==="local_drop"?ue(Xe,1):e(H)==="webdav"?ue(Ke,2):ue(Ye,-1)})}v(()=>{la(Se,e(H)==="local_drop"),la(j,e(H)==="webdav")}),Ze("change",Se,()=>Oe("local_drop")),Ze("change",j,()=>Oe("webdav")),l(b,oe)},$$slots:{default:!0}});var Ee=r(pt,2);Qe(Ee,{title:"Dernier import",children:(b,G)=>{var oe=us(),qe=Y(oe);{var Je=ue=>{var Ie=is();l(ue,Ie)},Se=ue=>{lr(ue,{get record(){return t.health.catalog},get motives(){return t.health.catalog_motives}})};g(qe,ue=>{t.health.catalog===null?ue(Je):ue(Se,-1)})}var Pe=r(qe,2),j=a(Pe),U=r(Pe,2);{var xe=ue=>{var Ie=os(),Be=a(Ie);v(()=>i(Be,z.sentence)),l(ue,Ie)};g(U,ue=>{z.sentence!==""&&ue(xe)})}var Xe=r(U,2),Ke=a(Xe);{let ue=u(()=>e(se)==="reload");tt(Ke,{act:"reload",kind:"write",label:"Recharger le catalogue",protected:!0,get busy(){return e(ue)},get disabled(){return e(V)},onrun:()=>{ke()}})}var Ye=r(Ke,2);{let ue=u(()=>e(se)==="quarantine");tt(Ye,{act:"quarantine",kind:"destructive",label:"Oublier la quarantaine",protected:!0,get busy(){return e(ue)},get disabled(){return e(V)},onrun:()=>{ie("quarantine",en)}})}v(()=>i(j,t.health.catalog_source===null?"Aucune source de catalogue publiée par ce poste.":"Source : "+t.health.catalog_source.label)),l(b,oe)},$$slots:{default:!0}});var nt=r(Ee,2);Qe(nt,{title:"Ce que la grille montre",note:"Un réglage d’affichage : il ne change ni le fichier reçu, ni ce que le poste sait peser.",children:(b,G)=>{var oe=cs(),qe=Y(oe);s(qe,()=>({path:"ui.show_by_unit_products",label:"Afficher les produits vendus à l’unité",hint:"Décoché, leurs tuiles quittent la grille et la recherche ne les retrouve plus. Ce que le poste perd : une tuile vendue à l’unité imprime une étiquette sans jamais lire la balance, et c’est le seul geste que ce réglage retire."}));var Je=r(qe,2),Se=a(Je);v(()=>i(Se,e(D))),l(b,oe)},$$slots:{default:!0}});var et=r(nt,2);Qe(et,{title:"Déposer un catalogue",note:"Glissez le fichier CSV ici, ou choisissez-le. Il passe par le même chemin que le fichier du producteur.",children:(b,G)=>{var oe=vs(),qe=Y(oe);{var Je=ue=>{var Ie=ds(),Be=a(Ie);v(()=>i(Be,e(be))),l(ue,Ie)};g(qe,ue=>{e(be)!==""&&ue(Je)})}var Se=r(qe,2);let Pe;var j=a(Se),U=r(a(j)),xe=a(U),Xe=r(j,2),Ke=a(Xe),Ye=r(Ke);v(()=>{Pe=Zt(Se,1,"drop svelte-3gk7u7",null,Pe,{dropping:e(Q),working:e(se)==="import"}),i(xe,`flv_${t.health.station??""}.csv`),i(Ke,`${e(se)==="import"?"Import en cours…":"Choisir un fichier"} `),Ye.disabled=e(V)}),na("dragover",Se,ue=>{ue.preventDefault(),!e(V)&&p(Q,!0)}),na("dragleave",Se,()=>p(Q,!1)),na("drop",Se,Me),Ze("change",Ye,ue=>te(ue.currentTarget)),l(b,oe)},$$slots:{default:!0}});var mt=r(et,2);Qe(mt,{title:"Anomalies à corriger dans Odoo",note:"Chaque ligne porte son numéro dans le CSV, son motif et la valeur fautive.",children:(b,G)=>{var oe=Ut(),qe=Y(oe);{var Je=j=>{var U=ps(),xe=a(U);v(()=>i(xe,e($))),l(j,U)},Se=j=>{var U=fs();l(j,U)},Pe=j=>{var U=gs(),xe=Y(U),Xe=a(xe),Ke=r(Xe);{var Ye=Ie=>{var Be=xt("Corrigez celles-ci dans Odoo : l’import suivant ne signalera que ce qui reste.");l(Ie,Be)};g(Ke,Ie=>{e(B).length>e(ze).length&&Ie(Ye)})}var ue=r(xe,2);ya(ue,()=>e(ze),()=>"anomalies"),v(Ie=>i(Xe,`${Ie??""} `),[()=>ve(e(ze).length,e(B).length,"anomalie","anomalies")]),l(j,U)};g(qe,j=>{e(P)!=="read"?j(Je):e(B).length===0?j(Se,1):j(Pe,-1)})}l(b,oe)},$$slots:{default:!0}});var Dt=r(mt,2);Qe(Dt,{title:"Unités divergentes",note:"Le produit reste proposé : le code-barres fait foi, seul le libellé du prix est faux.",children:(b,G)=>{var oe=Ut(),qe=Y(oe);{var Je=j=>{var U=ms(),xe=a(U);v(()=>i(xe,e($))),l(j,U)},Se=j=>{var U=hs();l(j,U)},Pe=j=>{var U=_s(),xe=Y(U),Xe=a(xe),Ke=r(xe,2);ya(Ke,()=>e(Ce),()=>"mismatches"),v(Ye=>i(Xe,Ye),[()=>ve(e(Ce).length,e(N).length,"unité divergente","unités divergentes")]),l(j,U)};g(qe,j=>{e(P)!=="read"?j(Je):e(N).length===0?j(Se,1):j(Pe,-1)})}l(b,oe)},$$slots:{default:!0}});var Bt=r(Dt,2);Qe(Bt,{title:"Produits non pesables",note:"Un inventaire, pas une liste d’erreurs : ces produits portent déjà leur code-barres et n’ont aucune raison d’être pesés.",children:(b,G)=>{var oe=Ut(),qe=Y(oe);{var Je=j=>{var U=bs(),xe=a(U);v(()=>i(xe,e($))),l(j,U)},Se=j=>{var U=ws();l(j,U)},Pe=j=>{var U=ys(),xe=Y(U),Xe=a(xe),Ke=r(xe,2);ya(Ke,()=>e(Le),()=>"not-weighable"),v(Ye=>i(Xe,Ye),[()=>ve(e(Le).length,e(re).length,"produit non pesable","produits non pesables")]),l(j,U)};g(qe,j=>{e(P)!=="read"?j(Je):e(re).length===0?j(Se,1):j(Pe,-1)})}l(b,oe)},$$slots:{default:!0}});var Vt=r(Bt,2);Qe(Vt,{title:"Produits retirés depuis l’import précédent",note:"Un produit absent du nouveau fichier est marqué retiré à sa date, jamais supprimé.",children:(b,G)=>{var oe=Ut(),qe=Y(oe);{var Je=j=>{var U=xs();l(j,U)},Se=j=>{var U=ks();l(j,U)},Pe=j=>{var U=qs(),xe=Y(U),Xe=a(xe);v(()=>i(Xe,e(we))),l(j,U)};g(qe,j=>{t.health.catalog===null?j(Je):e(Re)===0?j(Se,1):j(Pe,-1)})}l(b,oe)},$$slots:{default:!0}});var Et=r(Vt,2);Qe(Et,{title:"Décider d’un produit",note:"Retirer un produit et l’autoriser à peser moins sont deux décisions séparées : l’une n’efface pas l’autre.",children:(b,G)=>{var oe=js(),qe=r(Y(oe),2),Je=r(qe,2);{var Se=U=>{var xe=Ls(),Xe=Y(xe),Ke=a(Xe),Ye=r(Xe,2),ue=a(Ye);at(ue,21,()=>e(me),Ie=>Ie.id,(Ie,Be)=>{var ct=Ss(),dt=a(ct),ot=a(dt),ut=r(dt,2),ht=a(ut),kt=r(ut,2),zt=a(kt);v(()=>{i(ot,e(Be).name),i(ht,e(Be).id),i(zt,`${e(Be).unit_price_text??""}${e(Be).price_suffix??""}`)}),Ze("click",dt,()=>K(e(Be).id)),l(Ie,ct)}),v(()=>i(Ke,e(d))),l(U,xe)};g(Je,U=>{e(_e)!==""&&U(Se)})}var Pe=r(Je,2);{var j=U=>{var xe=zs(),Xe=a(xe),Ke=r(a(Xe)),Ye=a(Ke),ue=r(Ke),Ie=r(Xe,2),Be=a(Ie),ct=r(Ie,4),dt=r(ct,2);{var ot=rt=>{var yt=Es();l(rt,yt)};g(dt,rt=>{e(S)===""&&rt(ot)})}var ut=r(dt,2),ht=r(a(ut),2),kt=a(ht);{var zt=rt=>{{let yt=u(()=>e(se)==="offered"),Yt=u(()=>e(V)||!e(h));tt(rt,{act:"offered",kind:"destructive",label:"Ne plus proposer ce produit",protected:!0,get busy(){return e(yt)},get disabled(){return e(Yt)},onrun:()=>{Ge(!1)}})}},qt=rt=>{{let yt=u(()=>e(se)==="offered"),Yt=u(()=>e(V)||!e(k));tt(rt,{act:"offered",kind:"write",label:"Le proposer de nouveau",protected:!0,get busy(){return e(yt)},get disabled(){return e(Yt)},onrun:()=>{Ge(!0)}})}};g(kt,rt=>{e(W)?rt(zt):rt(qt,-1)})}var jt=r(ut,2),Ct=r(a(jt),2),It=r(Ct,2),Wt=a(It);{let rt=u(()=>e(se)==="waiver"),yt=u(()=>e(V)||!e(O));tt(Wt,{act:"waiver",kind:"write",label:"Enregistrer la dérogation",protected:!0,get busy(){return e(rt)},get disabled(){return e(yt)},onrun:()=>{Fe(e(ce))}})}var Jt=r(Wt,2);{var ta=rt=>{{let yt=u(()=>e(se)==="waiver-off"),Yt=u(()=>e(V)||!e(le));tt(rt,{act:"waiver-off",kind:"write",label:"Retirer la dérogation",protected:!0,get busy(){return e(yt)},get disabled(){return e(Yt)},onrun:()=>{Fe(null)}})}};g(Jt,rt=>{e(ee)!==null&&rt(ta)})}v((rt,yt)=>{Ue(xe,"data-decision",e(ae)),i(Ye,rt),i(ue,` (${e(ae)??""})`),i(Be,`En vigueur : ${yt??""}`),oa(Ct,e(ge))},[()=>Ne(e(ae)),()=>e(I)===null?"aucune décision — ce produit suit les règles générales":je(e(I))]),ca(ct,()=>e(C),rt=>p(C,rt)),Ze("input",Ct,rt=>p(ge,rt.currentTarget.value,!0)),l(U,xe)};g(Pe,U=>{e(ae)!==""&&U(j)})}ca(qe,()=>e(_e),U=>p(_e,U)),l(b,oe)},$$slots:{default:!0}});var bt=r(Et,2);Qe(bt,{title:"Décisions en vigueur",note:"Ce qu’un humain a décidé, produit par produit. C’est ici que se reprend un produit retiré de la grille.",children:(b,G)=>{var oe=Ut(),qe=Y(oe);{var Je=Pe=>{var j=Ps();l(Pe,j)},Se=Pe=>{var j=Cs(),U=Y(j),xe=a(U),Xe=r(U,2),Ke=a(Xe);at(Ke,21,()=>e(y),Ye=>Ye.product_id,(Ye,ue)=>{var Ie=Ts(),Be=a(Ie),ct=r(Be,2),dt=a(ct),ot=r(ct,2),ut=a(ot),ht=r(ot,2),kt=a(ht),zt=r(ht,2),qt=a(zt),jt=r(zt,2),Ct=a(jt);v((It,Wt,Jt)=>{i(dt,It),i(ut,e(ue).product_id),i(kt,Wt),i(qt,e(ue).reason),i(Ct,Jt)},[()=>Ne(e(ue).product_id),()=>je(e(ue)),()=>pa(e(ue).decided_at)]),Ze("click",Be,()=>K(e(ue).product_id)),l(Ye,Ie)}),v(Ye=>i(xe,Ye),[()=>ve(e(y).length,e(Z).length,"décision en vigueur","décisions en vigueur")]),l(Pe,j)};g(qe,Pe=>{e(Z).length===0?Pe(Je):Pe(Se,-1)})}l(b,oe)},$$slots:{default:!0}});var Tt=r(bt,2);Qe(Tt,{title:"Vingt derniers imports",children:(b,G)=>{var oe=Ut(),qe=Y(oe);{var Je=j=>{var U=Os(),xe=a(U);v(()=>i(xe,e(f))),l(j,U)},Se=j=>{var U=Rs();l(j,U)},Pe=j=>{var U=Ns(),xe=Y(U),Xe=a(xe),Ke=r(xe,2),Ye=a(Ke),ue=r(a(Ye));at(ue,21,()=>e(J),Ie=>Ie.id,(Ie,Be)=>{var ct=As(),dt=a(ct),ot=a(dt),ut=r(dt),ht=a(ut),kt=r(ut),zt=a(kt),qt=r(kt),jt=a(qt),Ct=r(qt),It=a(Ct),Wt=r(Ct),Jt=a(Wt),ta=r(Wt),rt=a(ta),yt=r(ta),Yt=a(yt),m=r(yt),R=a(m),Te=r(m),He=a(Te);v((lt,ft,wt,Pt,Ot,St,Kt,Qt)=>{i(ot,lt),i(ht,e(Be).file_name),i(zt,ft),i(jt,wt),i(It,e(Be).reason),i(Jt,Pt),i(rt,Ot),i(Yt,St),i(R,Kt),i(He,Qt)},[()=>Mt(e(Be).occurred_at),()=>st(e(Be)),()=>$e(e(Be)),()=>he(e(Be).rows_read_count),()=>he(e(Be).weighable_count),()=>he(e(Be).not_weighable_count),()=>he(e(Be).anomalies_count),()=>he(e(Be).products_withdrawn_count)]),l(Ie,ct)}),v(()=>i(Xe,e(x))),l(j,U)};g(qe,j=>{e(P)!=="read"?j(Je):e(J).length===0?j(Se,1):j(Pe,-1)})}l(b,oe)},$$slots:{default:!0}}),l(n,it),$t()}At(["change","click","input"]);function Is(n){switch(n){case"ok":return"OK";case"warn":return"À SURVEILLER";case"fault":return"EN PANNE";case"off":return"SANS OBJET";default:return"INCONNU"}}var Us=o('

                                          '),Fs=o('

                                          ');function Ms(n,t){Nt(t,!0);var s=Fs(),c=a(s),_=a(c),w=r(_,2),T=a(w),A=r(w,2),M=a(A),H=r(c,2),J=a(H),pe=r(H,2);{var fe=_e=>{var ae=Us(),C=a(ae);v(()=>i(C,t.light.remedy)),l(_e,ae)};g(pe,_e=>{t.light.remedy!==""&&_e(fe)})}v(_e=>{Ue(s,"data-light",t.light.id),Ue(s,"data-level",t.light.level),Ue(_,"data-level",t.light.level),i(T,t.light.label),i(M,_e),i(J,t.light.value)},[()=>Is(t.light.level)]),l(n,s),$t()}function Bs(n){return[Vs(n),Ws(n),Hs(n),Js(n),Xs(n),Ys(n)]}function Vs(n){const t=n.state.scale,s=va(t.median_ms);return n.scale_present?t.connected?t.too_slow?{id:"scale",label:"Balance",level:"warn",value:`une mesure toutes les ${s}, plus lent que la péremption`,remedy:"À cette cadence, un poids serait déclaré périmé avant l’arrivée de la mesure suivante. Vérifiez le câble et l’adaptateur USB, puis la cadence sur la page Matériel."}:{id:"scale",label:"Balance",level:"ok",value:Qs(t.provisional,`une mesure toutes les ${s}`),remedy:""}:{id:"scale",label:"Balance",level:"fault",value:"elle ne répond plus",remedy:"Vérifiez le câble et l’alimentation de la balance, puis touchez « Tester la balance ». En attendant, « Basculer en saisie manuelle » permet de continuer à servir."}:{id:"scale",label:"Balance",level:"off",value:"ce poste est déclaré sans balance",remedy:""}}function Ws(n){const t=n.state.printer;switch(t.health){case"faulted":return{id:"printer",label:"Imprimante",level:"fault",value:t.detail===""?"elle ne peut pas imprimer":t.detail,remedy:Gs(n)};case"consumable":return{id:"printer",label:"Imprimante",level:"warn",value:"elle imprime, mais le rouleau arrive en fin de vie",remedy:"Changez le rouleau quand vous passez derrière le comptoir, puis touchez « J’ai changé le rouleau ». Le poste continue de servir en attendant."};case"ready":return{id:"printer",label:"Imprimante",level:"ok",value:"elle répond et n’a rien à signaler",remedy:""};default:return{id:"printer",label:"Imprimante",level:"unknown",value:"elle prend les étiquettes et ne dit rien en retour",remedy:"C’est la réponse normale d’une file Windows en RAW ou d’un fichier de périphérique, pas une panne. Pour savoir si elle imprime, touchez « Imprimer une étiquette de test »."}}}function Gs(n){const t="Regardez le capot, le rouleau et le câble, puis touchez « Tester l’imprimante ».";return n.printing?.fallback_available===!0?t+" Le poste peut servir en attendant : « Imprimer sur l’imprimante du poste voisin ».":t}function Hs(n){const t=n.roll;return t===null?{id:"roll",label:"Rouleau",level:"unknown",value:"aucun compteur d’étiquettes sur ce poste",remedy:`Sans imprimante construite, il n’y a pas de rouleau à compter : vérifiez le « ${Gt("printer.type")} » et ses réglages sur la page Matériel.`}:t.known?t.level==="warn"?{id:"roll",label:"Rouleau",level:"warn",value:t.message,remedy:"Changez le rouleau, puis touchez « J’ai changé le rouleau »."}:{id:"roll",label:"Rouleau",level:"ok",value:t.message,remedy:""}:{id:"roll",label:"Rouleau",level:"unknown",value:t.message,remedy:"Touchez « J’ai changé le rouleau » en mettant un rouleau neuf : c’est le seul geste qui dise quelque chose de vrai du papier."}}function Js(n){const t=n.catalog_source?.label??"";if(n.state.catalog_count===0)return{id:"catalog",label:"Catalogue",level:"fault",value:"aucun produit dans la grille",remedy:Ks(t)};const s=n.catalog;return s!==null&&s.result!=="applied"&&s.result!=="unchanged"?{id:"catalog",label:"Catalogue",level:"warn",value:`dernier fichier refusé : ${s.reason===""?s.result:s.reason}`,remedy:"La grille tourne toujours sur le catalogue précédent. Corrigez le fichier dans Odoo, ou touchez « Oublier la quarantaine » puis redéposez-le."}:{id:"catalog",label:"Catalogue",level:"ok",value:`${he(n.state.catalog_count)} produits dans la grille`,remedy:""}}function Ks(n){return"Déposez le fichier du catalogue là où le poste le guette, ou utilisez « Importer un catalogue » ci-contre pour le glisser depuis une clé USB."+(n===""?"":` Ce poste surveille : ${n}.`)}function Xs(n){const t=n.disk;if(t===null)return{id:"disk",label:"Disque",level:"unknown",value:"la place libre n’a pas pu être mesurée",remedy:"Téléchargez le fichier de diagnostic : il porte ce que le poste a pu lire du volume, et c’est la pièce qu’un support demandera."};const s=ka(t.free_bytes),c=`seuil ${he(t.alert_mb)} Mo`;return t.free_bytes===0?{id:"disk",label:"Disque",level:"fault",value:`plus un octet libre sur ${t.path}`,remedy:"Le journal ne peut plus rien écrire, alors que les étiquettes continuent de sortir. Faites de la place sur le disque, puis rechargez cette page."}:t.alert_mb>0&&t.free_bytes0?{id:"journal",label:"Journal",level:"fault",value:`${he(t)} pesées imprimées mais non enregistrées`,remedy:"Les étiquettes sortent, les ventes sont bonnes, c’est la trace qui manque. Téléchargez le fichier de diagnostic et prévenez le support : ne redémarrez rien."}:n.counters.journal_rows_count<0?{id:"journal",label:"Journal",level:"unknown",value:"ce poste n’a pas de journal ouvert",remedy:"Le poste pèse et imprime quand même. Téléchargez le fichier de diagnostic : il dit pourquoi la base n’a pas pu être ouverte."}:{id:"journal",label:"Journal",level:"ok",value:`${he(n.counters.journal_rows_count)} pesées enregistrées`,remedy:""}}function Qs(n,t){return n?t+" (cadence encore provisoire)":t}function Zs(n){return n===null||!n.known?"unknown":n.configured?"ok":"missing"}var el=o("Une mesure toutes les ",1),tl=o('

                                          '),al=o("Source : ",1),rl=o(`

                                          Aucun import enregistré sur ce poste : le catalogue n’est jamais arrivé, ou le + journal ne le porte pas.

                                          `),nl=o('

                                          ',1),sl=o('

                                          ',1),ll=o('

                                          Aucune décision locale : le catalogue est proposé tel qu’il arrive.

                                          '),il=o('

                                          '),ol=o(' '),ul=o('
                                        • '),cl=o('
                                            ',1),dl=o(`INCONNU — la question n’a pas pu être posée à ce système. Ce + n’est pas « non configuré » : personne ne sait encore.`,1),vl=o('OK ',1),pl=o('NON CONFIGURÉ ',1),fl=o('

                                            '),gl=o('

                                            ',1),ml=o('

                                            Rien à signaler depuis le démarrage du poste.

                                            '),hl=o(' '),_l=o('
                                          • '),bl=o('
                                              '),wl=o(''),yl=o('

                                              '),xl=o('
                                              Numéro de poste
                                              Nom
                                              Coopérative
                                              Version
                                              Empreinte de configuration
                                              ',1),kl=o('
                                              ');function ql(n,t){Nt(t,!0);const s=u(()=>t.health.new_version),c=u(()=>Bs(t.health)),_=u(()=>t.health.state.scale),w=u(()=>t.health.unattended_restart),T=u(()=>t.health.decisions),A=5,M=u(()=>e(T).slice(0,A));var H=kl(),J=a(H);at(J,21,()=>e(c),Q=>Q.id,(Q,be)=>{Ms(Q,{get light(){return e(be)}})});var pe=r(J,2);Qe(pe,{title:"Cadence de la balance",children:(Q,be)=>{var se=tl(),P=a(se);{var ne=N=>{var L=xt("Ce poste est déclaré sans balance : le poids est saisi à la main.");l(N,L)},V=N=>{var L=xt(`La balance ne répond pas : aucune cadence n’est mesurable tant qu’aucune trame\r + n’arrive.`);l(N,L)},B=N=>{var L=xt(`Aucun intervalle n’a encore été mesuré : la cadence apparaîtra dès les premières\r + trames.`);l(N,L)},re=N=>{var L=el(),q=r(Y(L)),D=a(q),X=r(q),me=r(X);{var ze=Z=>{var y=xt("C’est encore une valeur d’attente : moins de huit intervalles ont été observés.");l(Z,y)};g(me,Z=>{e(_).provisional&&Z(ze)})}var Ce=r(me,2);{var Le=Z=>{var y=xt("À cette cadence, un poids serait périmé avant l’arrivée de la mesure suivante.");l(Z,y)};g(Ce,Z=>{e(_).too_slow&&Z(Le)})}v((Z,y)=>{i(D,Z),i(X,`, médiane + observée sur ${y??""} intervalles. `)},[()=>va(e(_).median_ms),()=>he(e(_).observations_count)]),l(N,L)};g(P,N=>{t.health.scale_present?e(_).connected?e(_).observations_count===0?N(B,2):N(re,-1):N(V,1):N(ne)})}l(Q,se)},$$slots:{default:!0}});var fe=r(pe,2);Qe(fe,{title:"Catalogue",children:(Q,be)=>{var se=sl(),P=Y(se),ne=a(P);{var V=q=>{var D=xt("Aucune source de catalogue n’est publiée par ce poste.");l(q,D)},B=q=>{var D=al(),X=r(Y(D)),me=a(X);v(()=>i(me,t.health.catalog_source.label)),l(q,D)};g(ne,q=>{t.health.catalog_source===null?q(V):q(B,-1)})}var re=r(P,2);{var N=q=>{var D=rl();l(q,D)},L=q=>{var D=nl(),X=Y(D),me=a(X),ze=r(X,2);lr(ze,{get record(){return t.health.catalog},get motives(){return t.health.catalog_motives},get onshowrows(){return t.onshowrows}}),v((Ce,Le)=>i(me,`Dernier essai : ${Ce??""} — + ${Le??""}${t.health.catalog.file_name===""?"":" ("+t.health.catalog.file_name+")"}${t.health.catalog.reason===""?"":" : "+t.health.catalog.reason}`),[()=>Mt(t.health.catalog.occurred_at),()=>ha(t.health.catalog.result)]),l(q,D)};g(re,q=>{t.health.catalog===null?q(N):q(L,-1)})}l(Q,se)},$$slots:{default:!0}});var _e=r(fe,2);Qe(_e,{title:"Décisions locales en vigueur",note:"Ce qu’un humain a décidé de ce catalogue, avec son motif et sa date.",children:(Q,be)=>{var se=Ut(),P=Y(se);{var ne=B=>{var re=ll();l(B,re)},V=B=>{var re=cl(),N=Y(re);{var L=D=>{var X=il(),me=a(X);v(ze=>i(me,`${ze??""} décisions en vigueur — les + 5 plus récentes ci-dessous. La liste entière est sur la page + Catalogue.`),[()=>he(e(T).length)]),l(D,X)};g(N,D=>{e(T).length>A&&D(L)})}var q=r(N,2);at(q,21,()=>e(M),D=>D.product_id,(D,X)=>{var me=ul(),ze=a(me),Ce=a(ze),Le=r(ze,2);{var Z=ce=>{var Ve=ol(),S=a(Ve);v(h=>i(S,`peut peser à partir de ${h??""} g`),[()=>he(e(X).min_weight_g)]),l(ce,Ve)};g(Le,ce=>{e(X).min_weight_g!==null&&ce(Z)})}var y=r(Le,2),I=a(y),W=r(y,2),ee=a(W);v(ce=>{i(Ce,`${e(X).offered?"Dérogation de poids":"Produit retiré"} — ${e(X).product_id??""}`),i(I,e(X).reason),i(ee,`${ce??""}, ${e(X).decided_by??""}`)},[()=>pa(e(X).decided_at)]),l(D,me)}),l(B,re)};g(P,B=>{e(T).length===0?B(ne):B(V,-1)})}l(Q,se)},$$slots:{default:!0}});var ae=r(_e,2);Qe(ae,{title:"Redémarrage sans intervention",children:(Q,be)=>{var se=gl(),P=Y(se),ne=a(P);{var V=q=>{var D=dl();l(q,D)},B=q=>{var D=vl(),X=r(Y(D));v(()=>i(X,` — après une coupure de courant, ce poste revient seul sur + l’écran client. ${e(w).detail??""}`)),l(q,D)},re=q=>{var D=pl(),X=r(Y(D));v(()=>i(X,` — ${e(w).detail??""}`)),l(q,D)};g(ne,q=>{e(w)===null||!e(w).known?q(V):e(w).configured?q(B,1):q(re,-1)})}var N=r(P,2);{var L=q=>{var D=fl(),X=a(D);v(()=>i(X,e(w).remedy)),l(q,D)};g(N,q=>{e(w)!==null&&e(w).known&&!e(w).configured&&e(w).remedy!==""&&q(L)})}v(q=>Ue(P,"data-verdict",q),[()=>Zs(e(w))]),l(Q,se)},$$slots:{default:!0}});var C=r(ae,2);Qe(C,{title:"Dix derniers événements",children:(Q,be)=>{var se=Ut(),P=Y(se);{var ne=B=>{var re=ml();l(B,re)},V=B=>{var re=bl();at(re,21,()=>t.health.events,N=>N.id,(N,L)=>{var q=_l(),D=a(q),X=a(D),me=r(D,2),ze=a(me),Ce=r(me,2);{var Le=I=>{var W=hl(),ee=a(W);v(()=>i(ee,e(L).code)),l(I,W)};g(Ce,I=>{e(L).code!==""&&Ht.showTechnicalNames&&I(Le)})}var Z=r(Ce,2),y=a(Z);v((I,W)=>{Ue(q,"data-level",e(L).level),i(X,I),i(ze,W),i(y,e(L).message)},[()=>An(e(L).occurred_at),()=>tr(e(L).source)]),l(N,q)}),l(B,re)};g(P,B=>{t.health.events.length===0?B(ne):B(V,-1)})}l(Q,se)},$$slots:{default:!0}});var ge=r(C,2);Qe(ge,{title:"Ce poste",children:(Q,be)=>{var se=xl(),P=Y(se),ne=r(a(P),2),V=a(ne),B=r(ne,4),re=a(B),N=r(B,4),L=a(N),q=r(N,4),D=a(q),X=r(q,4),me=a(X),ze=r(P,2);{var Ce=Le=>{var Z=yl(),y=a(Z),I=r(y);{var W=ee=>{var ce=wl();Ze("click",ce,function(...Ve){t.onshowupdate?.apply(this,Ve)}),l(ee,ce)};g(I,ee=>{t.onshowupdate!==void 0&&ee(W)})}v(()=>{Ue(Z,"data-update-available",e(s)),i(y,`Version ${e(s)??""} disponible. `)}),l(Le,Z)};g(ze,Le=>{e(s)!==""&&Le(Ce)})}v(()=>{i(V,t.health.station),i(re,t.health.station_name===""?"non renseigné":t.health.station_name),i(L,t.health.coop),i(D,t.health.version),i(me,t.health.config_fingerprint)}),l(Q,se)},$$slots:{default:!0}}),l(n,H),$t()}At(["click"]);var Sl=o(''),Ll=o(`

                                              Lecture de la configuration en cours… tant qu’elle n’est pas arrivée, cette page ne + déclare rien de ce poste.

                                              `),Fa=o('
                                            • '),El=o('
                                                '),zl=o('

                                                ',1),jl=o('
                                              • '),Pl=o('

                                                  ',1),Tl=o('

                                                  '),Cl=o('
                                                • '),Ol=o('
                                                    '),Rl=o('

                                                      ',1),Al=o('

                                                      Balance

                                                      L’état et la cadence viennent de ce que le poste observe vraiment, jamais d’un réglage.

                                                      Réglages série de la balance

                                                      Imprimante

                                                      Réglages de l’imprimante
                                                      ');function Nl(n,t){Nt(t,!0);const s=vt(t,"admin",7),c=20,_=3,w=250,T=50,A=_*1e3+w+1e3,M=12;let H=F(Lt([])),J=F(Lt([])),pe=F(Lt([])),fe=F(Lt([])),_e=F(""),ae=F(!1),C=F(""),ge=F(0),Q=F(0),be=F(!1),se=F(null),P=F(!1),ne=F(!1),V=!1;const B=u(()=>t.health.state.scale),re=u(()=>t.health.state.printer),N=u(()=>t.draft.config!==null),L=u(()=>e(N)&&!t.draft.flag("scale.present")),q=u(()=>t.draft.text("scale.options.port")),D=u(()=>e(se)!==null&&e(se).port===e(q)?e(se).reason:""),X=u(()=>e(_e)===e(q)?e(fe):[]),me=u($),ze=u(E),Ce=u(ie),Le=u(de),Z=u(()=>Me("scale.type")!==""||Me("scale.options.port")!==""),y=u(()=>Me("printer.type")!==""||Me("printer.options.transport")!==""||Me("printer.options.queue")!=="");Xt(()=>{e(Z)&&p(P,!0)}),Xt(()=>{e(y)&&p(ne,!0)}),Lr(()=>{V=!0}),Ea(()=>{k("ports",O)}),Xt(()=>{e(be)||!I(e(q))||ee(e(q))});function I(m){return!V&&e(N)&&!e(L)&&m!==""&&m===e(q)&&W(m)&&e(D)===""&&e(C)===""}function W(m){return e(H).some(R=>R.name===m)}async function ee(m){p(be,!0);try{for(;I(m);){try{Ve(m,await Ra(m,_))}catch(R){p(se,{port:m,reason:Ne(R)},!0);return}await S()}}finally{p(be,!1)}}async function ce(){const m=e(q),R=await s().protect(()=>Ra(m,_));R!==null&&(Ve(m,R),p(se,null))}function Ve(m,R){if(V||R.length===0)return;const Te=m===e(_e)?e(fe):[];p(_e,m,!0),p(fe,[...Te,...R].slice(-c),!0)}function S(m=w){return new Promise(R=>setTimeout(R,m))}async function h(){const m=Date.now()+A;for(;e(be)&&Date.now()Qa(m));R!==null&&(s().notice=R.message,await s().refresh())}async function we(){p(ge,0),p(Q,0),e(ae)||await O();const m=e(H);if(m.length===0){p(pe,[],!0);return}await s().protect(()=>d(m))}async function d(m){p(pe,[],!0),p(Q,m.length,!0);for(const[R,Te]of m.entries()){p(ge,R+1);try{const He=await Hr(Te.name);p(pe,[...e(pe),{port:Te.name,message:He.message,refused:!1}],!0)}catch(He){if(ve(He))throw He;p(pe,[...e(pe),{port:Te.name,message:Ne(He),refused:!0}],!0)}}}function x(){return e(C)!=="detect"?"Détecter automatiquement":e(be)?"Détection : le port se libère…":e(Q)===0?"Détection : énumération des ports…":`Détection : port ${he(e(ge))} sur ${he(e(Q))}…`}function $(){return t.health.scale_present?e(B).connected?e(B).too_slow?{level:"warn",word:"Trop lente",detail:f()+" À cette cadence, un poids serait déclaré périmé avant l’arrivée de la mesure suivante."}:{level:"ok",word:"Connectée",detail:"Elle répond. "+f()}:{level:"fault",word:"Sans réponse",detail:"Elle ne répond plus. Vérifiez le câble et l’alimentation, puis « Tester la balance » sur la page Dépannage."}:{level:"off",word:"Sans balance",detail:"Ce poste est déclaré sans balance : le feu est éteint et le poids se saisit à la main."}}function f(){return e(B).observations_count===0?"Aucun intervalle n’a encore été mesuré : la cadence sera connue dès les premières trames.":`Une mesure toutes les ${va(e(B).median_ms)} sur ${he(e(B).observations_count)} intervalles`+(e(B).provisional?", cadence encore provisoire.":".")}function E(){const m=z[e(re).health]??{level:"unknown",word:"État inconnu",detail:"Le poste a répondu un état que cet écran ne sait pas nommer."};return{...m,detail:e(re).detail===""?m.detail:e(re).detail}}const z={ready:{level:"ok",word:"Prête",detail:"Elle répond et n’a rien à signaler."},consumable:{level:"warn",word:"Rouleau en fin de vie",detail:"Elle imprime, mais le rouleau arrive en fin de vie."},faulted:{level:"fault",word:"En panne",detail:"Elle ne peut pas imprimer."},unknown:{level:"unknown",word:"Silencieuse",detail:"Elle prend les étiquettes et ne dit rien en retour : c’est la réponse normale d’une file Windows en RAW ou d’un fichier de périphérique, pas une panne."}};function ye(){const m=e(re).observed_at===""?"Jamais observée depuis le démarrage":`Observée le ${Mt(e(re).observed_at)}`,R=e(re).pending_jobs_count;return`${m}, ${he(R)} ${R>1?"travaux":"travail"} en attente.`}function ie(){const m=Oe();return m===""?ke():`${ke()} ${m}`}function ke(){return e(N)?e(L)?"Ce poste est déclaré sans balance : aucun port n’est écouté.":e(q)===""?"Aucun port n’est indiqué : choisissez-en un dans la liste ci-dessus pour écouter les trames.":e(ae)?W(e(q))?e(D)!==""?`L’écoute de ${e(q)} est arrêtée.`:e(C)!==""?`L’écoute de ${e(q)} est suspendue le temps de l’acte en cours.`:`Écoute de ${e(q)}.`:`${e(q)} n’est pas visible depuis ce poste : rien n’est écouté en continu.`:e(C)==="ports"?`Énumération des ports en cours : l’écoute de ${e(q)} démarre dès qu’il est vu.`:`Les ports de ce poste n’ont pas été énumérés : « Lister les ports » dira si ${e(q)} existe.`:"Lecture de la configuration en cours : le port à écouter n’est pas encore connu."}function Oe(){return e(X).length===0?"Aucune trame reçue pour l’instant.":e(X).length===1?`Une seule trame reçue — ${he(c)} au plus sont gardées.`:`Les ${he(e(X).length)} dernières trames — ${he(c)} au plus, la plus récente en bas.`}function de(){return!e(N)||e(L)||e(q)===""?"":e(D)!==""?"Reprendre l’écoute":e(ae)&&!W(e(q))?"Écouter ce port une fois":""}function K(m,R,Te){const He=`${he(Te)} ${Te>1?R:m}`;return Te<=M?He+".":`${He} — seules les ${he(M)} premières lignes sont affichées.`}function Ae(m,R){const Te=te(m);return Te.length>0?Te:R.slice(0,M)}function Ge(m){return[...new TextEncoder().encode(m)].map(R=>R.toString(16).toUpperCase().padStart(2,"0")).join(" ")}const Fe={0:"NUL",2:"STX",3:"ETX",4:"EOT",5:"ENQ",6:"ACK",9:"TAB",10:"LF",13:"CR",21:"NAK",27:"ESC",127:"DEL"};function De(m){let R="";for(const Te of m){const He=Te.codePointAt(0)??0;if(He>=32&&He!==127){R+=Te;continue}R+=`⟨${Fe[He]??He.toString(16).toUpperCase().padStart(2,"0")}⟩`}return R}function Me(m){return t.draft.faults.find(R=>R.field===m)?.message??""}function te(m){return t.draft.faults.find(R=>R.field===m)?.allowed??[]}function Ne(m){return m instanceof Ft?m.message:m instanceof Error?"Le poste n’a pas répondu : "+m.message:"Le poste n’a pas répondu."}function ve(m){return m instanceof Ft&&(m.status===401||m.status===409)}const je=[{what:"label",name:"étiquette"},{what:"alignment",name:"alignement"},{what:"ruler",name:"réglette"}];var $e=Al(),st=a($e),it=a(st),pt=r(a(it),2),Ee=r(a(pt),1,!0),nt=r(it,2),et=a(nt),mt=r(nt,4);{var Dt=m=>{var R=Sl(),Te=a(R);v(()=>la(Te,e(L))),Ze("change",Te,He=>t.draft.set("scale.present",!He.currentTarget.checked)),l(m,R)},Bt=m=>{var R=Ll();l(m,R)};g(mt,m=>{e(N)?m(Dt):m(Bt,-1)})}var Vt=r(mt,2),Et=a(Vt);{let m=u(()=>e(C)==="ports"),R=u(()=>e(C)!=="");tt(Et,{label:"Lister les ports",get busy(){return e(m)},get disabled(){return e(R)},onrun:()=>{k("ports",O)}})}var bt=r(Et,2);{let m=u(x),R=u(()=>e(C)!=="");tt(bt,{act:"detect",get label(){return e(m)},protected:!0,get disabled(){return e(R)},onrun:()=>{k("detect",we)}})}var Tt=r(Vt,2);{var b=m=>{var R=zl(),Te=Y(R),He=a(Te),lt=r(Te,2);{var ft=wt=>{var Pt=El();at(Pt,21,()=>e(H).slice(0,M),Ot=>Ot.name,(Ot,St)=>{var Kt=Fa(),Qt=a(Kt),_a=a(Qt),or=r(Qt,2),ur=a(or);v(()=>{i(_a,e(St).name),i(ur,`${(e(St).description===""?"aucune description USB":e(St).description)??""} + ${e(St).vid===""?"":` — VID ${e(St).vid} PID ${e(St).pid}`}`)}),Ze("click",Qt,()=>t.draft.set("scale.options.port",e(St).name)),l(Ot,Kt)}),l(wt,Pt)};g(lt,wt=>{e(H).length>0&&wt(ft)})}v(wt=>i(He,wt),[()=>e(H).length===0?"Aucun port série n’est visible depuis ce poste.":K("port détecté","ports détectés",e(H).length)]),l(m,R)};g(Tt,m=>{e(ae)&&m(b)})}var G=r(Tt,2);{var oe=m=>{var R=Pl(),Te=Y(R),He=a(Te),lt=r(Te,2);at(lt,21,()=>e(pe).slice(0,M),ft=>ft.port,(ft,wt)=>{var Pt=jl();let Ot;var St=a(Pt),Kt=a(St),Qt=r(St,2),_a=a(Qt);v(()=>{Ot=Zt(Pt,1,"svelte-1p4i0xm",null,Ot,{refused:e(wt).refused}),i(Kt,e(wt).port),i(_a,e(wt).message)}),l(ft,Pt)}),v(ft=>i(He,ft),[()=>K("port interrogé","ports interrogés",e(pe).length)]),l(m,R)};g(G,m=>{e(pe).length>0&&m(oe)})}var qe=r(G,2),Je=r(a(qe),2),Se=a(Je);{let m=u(()=>t.draft.text("scale.type")),R=u(()=>Me("scale.type")),Te=u(()=>te("scale.type")),He=u(()=>!e(N));_t(Se,{label:"Protocole",path:"scale.type",get value(){return e(m)},hint:"Les valeurs acceptées apparaissent ici si l’enregistrement est refusé.",get fault(){return e(R)},get allowed(){return e(Te)},get disabled(){return e(He)},onchange:lt=>t.draft.set("scale.type",lt)})}var Pe=r(Se,2);{let m=u(()=>t.draft.text("scale.options.port")),R=u(()=>Me("scale.options.port")),Te=u(()=>Ae("scale.options.port",e(H).map(lt=>lt.name))),He=u(()=>!e(N));_t(Pe,{label:"Port série",path:"scale.options.port",get value(){return e(m)},hint:"Choisissez-le dans la liste détectée ci-dessus plutôt que de le taper : l’écoute permanente ne suit que des ports détectés.",get fault(){return e(R)},get allowed(){return e(Te)},get disabled(){return e(He)},onchange:lt=>t.draft.set("scale.options.port",lt)})}var j=r(qe,2),U=a(j),xe=a(U),Xe=r(U,2);{var Ke=m=>{var R=Tl(),Te=a(R),He=r(Te);{var lt=ft=>{{let wt=u(()=>e(C)==="listen"),Pt=u(()=>e(C)!=="");tt(ft,{act:"listen",get label(){return e(Le)},protected:!0,get busy(){return e(wt)},get disabled(){return e(Pt)},onrun:()=>{k("listen",ce)}})}};g(He,ft=>{e(Le)!==""&&ft(lt)})}v(()=>i(Te,`${e(D)??""} `)),l(m,R)};g(Xe,m=>{(e(D)!==""||e(Le)!=="")&&m(Ke)})}var Ye=r(Xe,2);{var ue=m=>{var R=Ol();at(R,21,()=>e(X),ma,(Te,He)=>{var lt=Cl(),ft=a(lt),wt=a(ft),Pt=r(ft,2),Ot=a(Pt);v((St,Kt)=>{i(wt,St),i(Ot,Kt)},[()=>Ge(e(He)),()=>De(e(He))]),l(Te,lt)}),l(m,R)};g(Ye,m=>{e(X).length>0&&m(ue)})}var Ie=r(st,2),Be=a(Ie),ct=r(a(Be),2),dt=r(a(ct),1,!0),ot=r(Be,2),ut=a(ot),ht=r(ot,2),kt=a(ht),zt=r(ht,2),qt=a(zt);{let m=u(()=>e(C)==="printers"),R=u(()=>e(C)!=="");tt(qt,{label:"Lister les files",get busy(){return e(m)},get disabled(){return e(R)},onrun:()=>{k("printers",le)}})}var jt=r(qt,2);{let m=u(()=>e(C)==="discover"),R=u(()=>e(C)!=="");tt(jt,{label:"Rechercher l’imprimante",protected:!0,get busy(){return e(m)},get disabled(){return e(R)},onrun:()=>{k("discover",Re)}})}var Ct=r(jt,2);at(Ct,17,()=>je,m=>m.what,(m,R)=>{{let Te=u(()=>`Auto-test : ${e(R).name}${e(C)===e(R).what?" — en cours…":""}`),He=u(()=>e(C)!=="");tt(m,{get act(){return e(R).what},get label(){return e(Te)},protected:!0,get disabled(){return e(He)},onrun:()=>{k(e(R).what,()=>We(e(R).what))}})}});var It=r(zt,2);{var Wt=m=>{var R=Rl(),Te=Y(R),He=a(Te),lt=r(Te,2);at(lt,21,()=>e(J).slice(0,M),ft=>ft.name,(ft,wt)=>{var Pt=Fa(),Ot=a(Pt),St=a(Ot),Kt=r(Ot,2),Qt=a(Kt);v(()=>{i(St,e(wt).name),i(Qt,`${e(wt).detail??""}${e(wt).default?" — file par défaut du système":""}`)}),Ze("click",Ot,()=>t.draft.set("printer.options.queue",e(wt).name)),l(ft,Pt)}),v(ft=>i(He,ft),[()=>K("file d’impression","files d’impression",e(J).length)]),l(m,R)};g(It,m=>{e(J).length>0&&m(Wt)})}var Jt=r(It,2),ta=r(a(Jt),2),rt=a(ta);{let m=u(()=>t.draft.text("printer.type")),R=u(()=>Me("printer.type")),Te=u(()=>te("printer.type")),He=u(()=>!e(N));_t(rt,{label:"Driver",path:"printer.type",get value(){return e(m)},hint:"Gardez le driver raster : c’est celui que les postes en service utilisent.",get fault(){return e(R)},get allowed(){return e(Te)},get disabled(){return e(He)},onchange:lt=>t.draft.set("printer.type",lt)})}var yt=r(rt,2);{let m=u(()=>t.draft.text("printer.options.transport")),R=u(()=>Me("printer.options.transport")),Te=u(()=>te("printer.options.transport")),He=u(()=>!e(N));_t(yt,{label:"Transport",path:"printer.options.transport",get value(){return e(m)},hint:"Local par défaut : une file Windows ou un fichier de périphérique de ce poste.",get fault(){return e(R)},get allowed(){return e(Te)},get disabled(){return e(He)},onchange:lt=>t.draft.set("printer.options.transport",lt)})}var Yt=r(yt,2);{let m=u(()=>t.draft.text("printer.options.queue")),R=u(()=>Me("printer.options.queue")),Te=u(()=>Ae("printer.options.queue",e(J).map(lt=>lt.name))),He=u(()=>!e(N));_t(Yt,{label:"File ou chemin",path:"printer.options.queue",get value(){return e(m)},hint:"Choisissez-la dans la liste ci-dessus : une file mal orthographiée ne s’imprime pas.",get fault(){return e(R)},get allowed(){return e(Te)},get disabled(){return e(He)},onchange:lt=>t.draft.set("printer.options.queue",lt)})}v(m=>{Ue(pt,"data-level",e(me).level),i(Ee,e(me).word),i(et,e(me).detail),i(xe,e(Ce)),Ue(ct,"data-level",e(ze).level),i(dt,e(ze).word),i(ut,e(ze).detail),i(kt,m)},[()=>ye()]),Oa("open","toggle",qe,m=>p(P,m),()=>e(P)),Oa("open","toggle",Jt,m=>p(ne,m),()=>e(ne)),l(n,$e),$t()}At(["change","click"]);var $l=o(""),Dl=o('L’export sera proposé quand le journal aura répondu.'),Il=o('L’export n’est pas proposé : ce poste n’a pas répondu à la lecture du journal.'),Ul=o('Exporter en CSV'),Fl=o('

                                                      '),Ml=o('

                                                      '),Bl=o(`

                                                      Aucune trame brute n’a été enregistrée pour cette pesée : il n’y a + rien à rejouer.

                                                      `),Vl=o(`

                                                      La trame repart dans le décodeur du poste EN SERVICE : le poids + affiché au client change, et rien ne le remet comme il était. C’est + ce qui fait d’un refus inexpliqué un test permanent, sans + déplacement au magasin et sans balance.

                                                      `,1),Wl=o('

                                                      Produit
                                                      Référence
                                                      Vente
                                                      Brut / tare / net
                                                      Stabilité
                                                      Origine du poids
                                                      Résultat
                                                      Trame brute
                                                      '),Gl=o(' ',1),Hl=o('

                                                      QuandProduitNetCode-barresRésultatDuréeDétail
                                                      ',1),Jl=o('
                                                      ',1),Kl=o('

                                                      '),Xl=o(' '),Yl=o(' '),Ql=o('
                                                    • '),Zl=o('

                                                        ',1),ei=o('
                                                        ');function ti(n,t){Nt(t,!0);const s=vt(t,"admin",7),c=200,_=5e3,w=50,T=500,A=7,M={sent:"envoyée à l’imprimante",rejected:"refusée",failed:"en échec",reprint:"réimpression"},H=[{value:"",label:"toutes"},{value:"sent",label:"envoyées à l’imprimante"},{value:"rejected",label:"refusées"},{value:"failed",label:"en échec"},{value:"reprint",label:"réimpressions"}],J={scale:"balance",manual:"saisie manuelle",replay:"trame rejouée"},pe={stable:"stable",unstable:"instable",unknown:"non déclarée par la balance",not_applicable:"sans objet — saisie manuelle"},fe={by_weight:"au poids",by_unit:"à l’unité"},_e={debug:"mise au point",info:"information",warn:"avertissement",error:"erreur",critical:"critique"};let ae=F(Lt([])),C=F(Lt([])),ge=F(""),Q=F(null),be=F(!1),se=F(!1),P=F("loading"),ne=F("loading"),V=F(""),B=F("");const re=u(()=>({limit:String(c),...e(ge)===""?{}:{result:e(ge)}})),N=u(()=>({...e(re),limit:String(_)})),L=u(()=>e(ae).find(W=>W.id===e(Q))??null),q=u(()=>H.find(W=>W.value===e(ge))?.label??""),D=u(()=>Ce(e(ae).length,c,"pesée","pesées",`L’export CSV en emporte jusqu’à ${he(_)}.`)),X=u(()=>Ce(e(C).length,w,"ligne","lignes",`Le fichier de diagnostic emporte les ${he(T)} dernières.`));me();async function me(){p(Q,null),p(se,!0),p(P,"loading"),p(ne,"loading"),p(ae,[],!0),p(C,[],!0),p(V,""),p(B,"");try{const W=await s().load(()=>Kr(e(re)));p(P,W===null?"unread":"read",!0),p(V,W===null?s().actionError:"",!0),p(ae,W??[],!0);const ee=await s().load(()=>Yr({limit:String(w)}));p(ne,ee===null?"unread":"read",!0),p(B,ee===null?s().actionError:"",!0),p(C,ee??[],!0)}finally{p(se,!1),s().actionError=""}}async function ze(W){s().notice="",s().actionError="",p(be,!0);try{const ee=await s().protect(()=>tn(W));ee!==null&&(s().notice=ee.message)}finally{p(be,!1)}}function Ce(W,ee,ce,Ve,S){const h=W>1?Ve:ce;return W{var ce=Jl(),Ve=Y(ce),S=r(a(Ve),2);at(S,21,()=>H,f=>f.value,(f,E)=>{var z=$l(),ye=a(z),ie={};v(()=>{i(ye,e(E).label),ie!==(ie=e(E).value)&&(z.value=(z.__value=e(E).value)??"")}),l(f,z)});var h=r(S,2);tt(h,{label:"Rafraîchir",get busy(){return e(se)},onrun:()=>{me()}});var k=r(h,2);{var O=f=>{var E=Dl();l(f,E)},le=f=>{var E=Il();l(f,E)},Re=f=>{var E=Ul();v(z=>Ue(E,"href",z),[()=>Xr(e(N))]),l(f,E)};g(k,f=>{e(P)==="loading"?f(O):e(P)==="unread"?f(le,1):f(Re,-1)})}var We=r(Ve,2);{var we=f=>{var E=Fl(),z=a(E);v(ye=>i(z,`L’export emporte le même filtre que le tableau, mais pas son plafond : il descend + jusqu’à ${ye??""} pesées, en point-virgule et en UTF-8 — il s’ouvre + tel quel dans le tableur d’un Windows français. Il ne demande aucun mot de passe : la + lecture du journal n’en demande pas non plus, et le fichier de diagnostic emporte déjà + les deux cents dernières pesées.`),[()=>he(_)]),l(f,E)};g(We,f=>{e(P)==="read"&&f(we)})}var d=r(We,2);{var x=f=>{var E=Ml(),z=a(E);{var ye=de=>{var K=xt("Lecture du journal…");l(de,K)},ie=de=>{var K=xt();v(()=>i(K,`Le journal n’a pas pu être lu : ${e(V)??""} Ce n’est pas « aucune pesée ».`)),l(de,K)},ke=de=>{var K=xt("Le journal ne contient aucune pesée.");l(de,K)},Oe=de=>{var K=xt();v(()=>i(K,`Aucune pesée ne correspond au filtre « ${e(q)??""} ».`)),l(de,K)};g(z,de=>{e(P)==="loading"?de(ye):e(P)==="unread"?de(ie,1):e(ge)===""?de(ke,2):de(Oe,-1)})}l(f,E)},$=f=>{var E=Hl(),z=Y(E),ye=a(z),ie=r(z,2),ke=a(ie),Oe=r(a(ke));at(Oe,21,()=>e(ae),de=>de.id,(de,K)=>{var Ae=Gl(),Ge=Y(Ae);let Fe;var De=a(Ge),Me=a(De),te=r(De),Ne=a(te),ve=r(te),je=a(ve),$e=r(ve),st=a($e),it=r($e),pt=a(it),Ee=r(it),nt=a(Ee),et=r(Ee),mt=a(et),Dt=a(mt),Bt=r(Ge,2);{var Vt=Et=>{var bt=Wl(),Tt=a(bt);Ue(Tt,"colspan",A);var b=a(Tt),G=a(b),oe=r(b,2),qe=r(a(oe),2),Je=a(qe),Se=r(qe,4),Pe=a(Se),j=r(Se,4),U=a(j),xe=r(j,4),Xe=a(xe),Ke=r(xe,4),Ye=a(Ke),ue=r(Ke,4),Ie=a(ue),Be=r(ue,4),ct=a(Be),dt=r(Be,4),ot=a(dt),ut=a(ot),ht=r(oe,2);{var kt=qt=>{var jt=Bl();l(qt,jt)},zt=qt=>{var jt=Vl(),Ct=Y(jt),It=a(Ct);tt(It,{kind:"destructive",label:"Rejouer cette trame",protected:!0,get busy(){return e(be)},onrun:()=>{ze(e(L).frame)}}),l(qt,jt)};g(ht,qt=>{e(L).frame===""?qt(kt):qt(zt,-1)})}v((qt,jt,Ct,It,Wt,Jt,ta,rt,yt)=>{Ue(bt,"data-detail",e(K).id),i(G,`Pesée ${e(L).id??""}`),i(Je,`${e(L).product_name??""} (${e(L).product_id??""})`),i(Pe,e(L).reference),i(U,`${qt??""} — + ${jt??""} + ${e(L).quantity>1?"unités":"unité"}`),i(Xe,`${Ct??""} g / ${It??""} g / + ${Wt??""} g`),i(Ye,`${Jt??""} — cadence + médiane ${ta??""}`),i(Ie,rt),i(ct,`${yt??""}${e(L).detail===""?"":` — ${e(L).detail}`}`),i(ut,e(L).frame===""?"aucune trame enregistrée":e(L).frame)},[()=>Le(fe,e(L).mode,"mode de vente inconnu"),()=>he(e(L).quantity),()=>he(e(L).gross_g),()=>he(e(L).tare_g),()=>he(e(L).net_g),()=>Le(pe,e(L).stability,"stabilité inconnue"),()=>va(e(L).rate_ms),()=>Le(J,e(L).source,"origine inconnue"),()=>Le(M,e(L).result,"résultat inconnu")]),l(Et,bt)};g(Bt,Et=>{e(Q)===e(K).id&&e(L)!==null&&Et(Vt)})}v((Et,bt,Tt,b)=>{Fe=Zt(Ge,1,"svelte-86o8oz",null,Fe,{open:e(Q)===e(K).id}),i(Me,Et),i(Ne,e(K).product_name),i(je,`${bt??""} g`),i(st,e(K).barcode),i(pt,Tt),i(nt,b),Ue(mt,"aria-expanded",e(Q)===e(K).id),i(Dt,e(Q)===e(K).id?"fermer":"détail")},[()=>Mt(e(K).occurred_at),()=>he(e(K).net_g),()=>Le(M,e(K).result,"résultat inconnu"),()=>va(e(K).duration_ms)]),Ze("click",mt,()=>p(Q,e(Q)===e(K).id?null:e(K).id,!0)),l(de,Ae)}),v(()=>i(ye,e(D))),l(f,E)};g(d,f=>{e(ae).length===0?f(x):f($,-1)})}Ze("change",S,()=>{me()}),Sr(S,()=>e(ge),f=>p(ge,f)),l(W,ce)},$$slots:{default:!0}});var I=r(y,2);Qe(I,{title:"Journal technique",children:(W,ee)=>{var ce=Ut(),Ve=Y(ce);{var S=k=>{var O=Kl(),le=a(O);{var Re=d=>{var x=xt("Lecture du journal technique…");l(d,x)},We=d=>{var x=xt();v(()=>i(x,`Le journal technique n’a pas pu être lu : ${e(B)??""} Ce n’est pas « aucune + ligne ».`)),l(d,x)},we=d=>{var x=xt("Aucune ligne technique.");l(d,x)};g(le,d=>{e(ne)==="loading"?d(Re):e(ne)==="unread"?d(We,1):d(we,-1)})}l(k,O)},h=k=>{var O=Zl(),le=Y(O),Re=a(le),We=r(le,2),we=a(We);at(we,21,()=>e(C),d=>d.id,(d,x)=>{var $=Ql(),f=a($),E=a(f),z=r(f,2),ye=a(z),ie=r(z,2),ke=a(ie),Oe=r(ie,2);{var de=De=>{var Me=Xl(),te=a(Me);v(()=>i(te,e(x).code)),l(De,Me)};g(Oe,De=>{e(x).code!==""&&Ht.showTechnicalNames&&De(de)})}var K=r(Oe,2),Ae=a(K),Ge=r(K,2);{var Fe=De=>{var Me=Yl(),te=a(Me);v(()=>i(te,e(x).detail)),l(De,Me)};g(Ge,De=>{e(x).detail!==""&&De(Fe)})}v((De,Me,te)=>{Ue($,"data-level",e(x).level),i(E,De),i(ye,Me),i(ke,te),i(Ae,e(x).message)},[()=>Mt(e(x).occurred_at),()=>Le(_e,e(x).level,"niveau inconnu"),()=>tr(e(x).source)]),l(d,$)}),v(()=>i(Re,e(X))),l(k,O)};g(Ve,k=>{e(C).length===0?k(S):k(h,-1)})}l(W,ce)},$$slots:{default:!0}}),l(n,Z),$t()}At(["change","click"]);var ai=o('

                                                        '),ri=o('

                                                        '),ni=o('

                                                        '),si=o('

                                                        '),li=o('

                                                        Lecture de la configuration en cours… les flèches attendent qu’elle soit arrivée.

                                                        '),ii=o(`
                                                        Aperçu de l’étiquette telle qu’elle serait imprimée

                                                        Décalage horizontal

                                                        Décalage vertical

                                                        Le décalage ne descend pas sous zéro : le poste refuse un décalage négatif quel que + soit le gabarit. Le maximum, lui, dépend de la géométrie du gabarit ; il est annoncé + ici si l’enregistrement le dépasse.

                                                        Le symbole code-barres est volontairement tronqué : un symbole conforme n’entre pas + sur 40 × 25 mm avec les cinq champs texte. Ce n’est pas un défaut de rendu et il n’y + a rien à corriger.

                                                        Le détail chiffré du symbole — largeur de module, modules rendus, modules attendus — + n’est pas servi par ce poste. Cet écran n’affiche pas un chiffre qu’il aurait deviné.

                                                        `,1),oi=o(`

                                                        Chaque appui sort une étiquette pour de bon : le mot de passe est demandé au moment + d’imprimer, et l’impression repart d’elle-même une fois la session ouverte.

                                                        `,1),ui=o('
                                                        ');function ci(n,t){Nt(t,!0);const s=vt(t,"admin",7),c="printer.template",_="printer.options.offset_x",w="printer.options.offset_y",T=0,A="L’aperçu n’a pas pu être rendu par le poste.",M="Le poste a rendu l’aperçu, mais le navigateur ne l’a pas affiché.",H=[{what:"alignment",label:"Imprimer la mire d’alignement"},{what:"ruler",label:"Imprimer la réglette"}];let J=F(1),pe=F(!0),fe=F(!1),_e=F(""),ae=F(null),C=F(""),ge=null,Q=!1;const be=u(()=>t.draft.text(c)),se=u(()=>Jr(e(be),e(pe),e(fe),e(J))),P=u(()=>t.draft.config!==null),ne=u(()=>t.draft.number(_)),V=u(()=>t.draft.number(w)),B=u(()=>I(_)),re=u(()=>I(w)),N=u(Le),L=u(()=>s().busy||e(C)!=="");Xt(()=>{if(t.draft.config===null)return;if(t.draft.dirty){ge===null&&q();return}const S={x:t.draft.number(_),y:t.draft.number(w)};if(ge!==null&&ge.x===S.x&&ge.y===S.y)return;const h=ge===null;ge=S,p(ae,S,!0),h||me()});async function q(){if(!Q){Q=!0;try{const S=await za(),h={x:D(S.config,_),y:D(S.config,w)};ge=h,p(ae,h,!0)}catch{}finally{Q=!1}}}function D(S,h){let k=S;for(const O of h.split(".")){if(k===null||typeof k!="object")return 0;k=k[O]}return typeof k=="number"?k:0}function X(S,h){const k=t.draft.number(S)+h;k=2?"s":""}`}function y(S,h){const k=Number(h);h.trim()===""||Number.isNaN(k)||t.draft.set(S,k)}function I(S){return t.draft.faults.find(h=>h.field===S)?.message??""}async function W(S){if(!e(L)){p(C,S,!0),s().actionError="",s().notice="";try{const h=await s().protect(()=>Qa(S));if(h===null)return;s().notice=h.message,await s().refresh()}finally{p(C,"")}}}var ee=ui(),ce=a(ee);Qe(ce,{title:"Aperçu de l’étiquette",note:"Le même moteur que l’impression (A2) : le décalage se voit parce qu’il est cuit dans le bitmap. L’image porte le gabarit en cours d’édition, mais le décalage ENREGISTRÉ — jamais celui que les flèches sont en train de régler.",children:(S,h)=>{var k=ii(),O=Y(k);{var le=Ee=>{var nt=ai(),et=a(nt);v(()=>i(et,e(N))),l(Ee,nt)};g(O,Ee=>{e(N)!==""&&Ee(le)})}var Re=r(O,2),We=a(Re),we=a(We),d=r(we,2);{var x=Ee=>{var nt=ri(),et=a(nt);v(()=>i(et,e(_e))),l(Ee,nt)};g(d,Ee=>{e(_e)!==""&&Ee(x)})}var $=r(We,2),f=a($),E=r(a(f),2),z=a(E),ye=r(f,2),ie=a(ye);{let Ee=u(()=>!e(P)||e(ne)<=T);tt(ie,{label:"← 1 dot",get disabled(){return e(Ee)},onrun:()=>X(_,-1)})}var ke=r(ie,2);{let Ee=u(()=>!e(P));tt(ke,{label:"1 dot →",get disabled(){return e(Ee)},onrun:()=>X(_,1)})}var Oe=r(ye,2);{var de=Ee=>{var nt=ni(),et=a(nt);v(()=>i(et,e(B))),l(Ee,nt)};g(Oe,Ee=>{e(B)!==""&&Ee(de)})}var K=r(Oe,2),Ae=r(a(K),2),Ge=a(Ae),Fe=r(K,2),De=a(Fe);{let Ee=u(()=>!e(P)||e(V)<=T);tt(De,{label:"↑ 1 dot",get disabled(){return e(Ee)},onrun:()=>X(w,-1)})}var Me=r(De,2);{let Ee=u(()=>!e(P));tt(Me,{label:"1 dot ↓",get disabled(){return e(Ee)},onrun:()=>X(w,1)})}var te=r(Fe,2);{var Ne=Ee=>{var nt=si(),et=a(nt);v(()=>i(et,e(re))),l(Ee,nt)};g(te,Ee=>{e(re)!==""&&Ee(Ne)})}var ve=r(te,4),je=a(ve),$e=r(ve,2),st=a($e),it=r($e,2);{var pt=Ee=>{var nt=li();l(Ee,nt)};g(it,Ee=>{e(P)||Ee(pt)})}v((Ee,nt)=>{Ue(we,"src",e(se)),i(z,Ee),i(Ge,nt)},[()=>Z(e(ne)),()=>Z(e(V))]),na("load",we,()=>p(_e,"")),na("error",we,()=>{ze(e(se))}),Ze("change",je,()=>me()),Ca(je,()=>e(pe),Ee=>p(pe,Ee)),Ze("change",st,()=>me()),Ca(st,()=>e(fe),Ee=>p(fe,Ee)),l(S,k)},$$slots:{default:!0}});var Ve=r(ce,2);Qe(Ve,{title:"Gabarit et impression",children:(S,h)=>{var k=oi(),O=Y(k);{let d=u(()=>I(c)),x=u(()=>!e(P));_t(O,{label:"Gabarit",path:c,get value(){return e(be)},hint:"Le gabarit reproduit à l’identique s’appelle weighing_identical (A1).",get fault(){return e(d)},get disabled(){return e(x)},onchange:$=>{t.draft.set(c,$),me()}})}var le=r(O,2);{let d=u(()=>t.draft.text("printer.options.darkness")),x=u(()=>I("printer.options.darkness")),$=u(()=>!e(P));_t(le,{label:"Noircissement",path:"printer.options.darkness",kind:"number",get value(){return e(d)},hint:"Trop bas, l’étiquette pâlit au soleil ; trop haut, elle bave et le scanner refuse.",get fault(){return e(x)},get disabled(){return e($)},onchange:f=>y("printer.options.darkness",f)})}var Re=r(le,2);{let d=u(()=>t.draft.text("printer.options.speed")),x=u(()=>I("printer.options.speed")),$=u(()=>!e(P));_t(Re,{label:"Vitesse",path:"printer.options.speed",kind:"number",get value(){return e(d)},get fault(){return e(x)},get disabled(){return e($)},onchange:f=>y("printer.options.speed",f)})}var We=r(Re,2);{let d=u(()=>t.draft.text("printer.options.copies")),x=u(()=>I("printer.options.copies")),$=u(()=>!e(P));_t(We,{label:"Exemplaires",path:"printer.options.copies",kind:"number",get value(){return e(d)},hint:"Un client repart avec une étiquette : deux exemplaires se justifient, pas se devinent.",get fault(){return e(x)},get disabled(){return e($)},onchange:f=>y("printer.options.copies",f)})}var we=r(We,2);at(we,21,()=>H,d=>d.what,(d,x)=>{{let $=u(()=>e(C)===e(x).what);tt(d,{get act(){return e(x).what},get label(){return e(x).label},protected:!0,get busy(){return e($)},get disabled(){return e(L)},onrun:()=>{W(e(x).what)}})}}),l(S,k)},$$slots:{default:!0}}),l(n,ee),$t()}At(["change"]);var di=o(" "),vi=o(' '),pi=o(''),fi=o('

                                                        Aucun tarif déclaré dans la configuration lue.

                                                        '),gi=o('Prix du catalogue Odoo — pas de remise'),Ma=o(' '),mi=o(' % ',1),hi=o(' '),_i=o(`

                                                        CodeLibelléAbrégéRemiseOrdre

                                                        Un champ vidé garde la valeur du fichier : il n’écrit pas zéro, et la case la + retrouve dès qu’on quitte le champ. Une remise effacée serait le plein tarif pour + tous les adhérents.

                                                        `,1),bi=o(" ",1),wi=o(`

                                                        Les deux arrondis sont distincts parce qu’ils tombent à des endroits différents du + calcul : arrondir le prix au kilo puis le multiplier, ou multiplier puis arrondir, ne + donne pas le même centime sur une étiquette. Le poste applique l’arrondi commercial, + et l’écart se voit au centime près sur la même pesée.

                                                        `,1),yi=o('

                                                        Rien ne s’affiche au client : c’est une information.

                                                        '),xi=o('

                                                        '),ki=o('
                                                        '),qi=o('

                                                        '),Si=o('
                                                      • '),Li=o(`

                                                        Aucun verdict en ce moment : ces lignes portent sur la pesée EN COURS, et le poste + est au repos. Elles apparaissent le temps d’un cycle, quand un client pose son sac + et touche une tuile.

                                                        `),Ei=o(' '),zi=o(' ',1),ji=o('
                                                      • '),Pi=o('

                                                          ',1),Ti=o(`

                                                          L’ordre compte : le premier verdict bloquant décide de ce que le client lit. Les + garde-fous 1 à 7 portent sur l’état de la balance, c’est-à-dire le poids brut ; les + garde-fous 8 à 14 portent sur la vente, c’est-à-dire le net. Le code en gris est + celui du journal, celui qu’on lit au téléphone.

                                                            Un seuil vidé garde la valeur du fichier : il n’écrit pas zéro, et la case la retrouve + dès qu’on quitte le champ. Pour changer un seuil, on tape l’autre valeur.

                                                            Ce que les garde-fous disent de la pesée en cours

                                                            `,1),Ci=o(`

                                                            Le plan de numérotation n’est PAS ici, et c’est tout l’intérêt : préfixes, largeur de + la référence, largeur de la charge utile, décimales et mode de vente sont une + constante du binaire, indexée par préfixe et vérifiée au démarrage. + Un champ qui change le SENS du code lu par la caisse n’est pas un réglage, c’est un + contrat externe : il change avec une version du binaire, relue et testée, jamais + depuis l’écran d’un poste.

                                                            `,1),Oi=o('

                                                            Aucune dérogation : la limite générale s’applique à tous les produits.

                                                            '),Ri=o(`

                                                            Les noms de produits n’ont pas pu être lus : le catalogue en service n’a pas + répondu. Les identifiants Odoo restent affichés.

                                                            `),Ai=o(`Produit retiré : le garde-fou 14 refuse le produit avant que cette + dérogation ait un sens.`),Ni=o('
                                                          1. '),$i=o('

                                                              ',1),Di=o('
                                                              ');function Ii(n,t){Nt(t,!0);const s=(y,I=ia,W=ia,ee=ia)=>{var ce=pi(),Ve=a(ce),S=r(Ve,2),h=a(S),k=a(h),O=r(h,2);{var le=we=>{var d=di(),x=a(d);v(()=>i(x,I())),l(we,d)};g(O,we=>{Ht.showTechnicalNames&&we(le)})}var Re=r(O,2);{var We=we=>{var d=vi(),x=a(d);v(()=>i(x,ee())),l(we,d)};g(Re,we=>{ee()!==""&&we(We)})}v((we,d)=>{Ue(ce,"data-flag",I()),Ue(ce,"data-on",we),la(Ve,d),i(k,W())},[()=>String(t.draft.flag(I())),()=>t.draft.flag(I())]),Ze("change",Ve,we=>t.draft.set(I(),we.currentTarget.checked)),l(y,ce)},c=20,_=u(()=>pe(t.draft)),w=u(()=>t.health.state.diagnostics),T=u(()=>t.health.decisions.filter(y=>y.min_weight_g!==null)),A=u(()=>e(T).filter(y=>!y.offered)),M=u(()=>e(T).slice(0,c));let H=F(Lt({})),J=F("loading");se();function pe(y){const I=y.value("pricing.tiers");return Array.isArray(I)?I.map(W=>{const ee=W??{},ce=ee.discount_percent;return{code:String(ee.code??""),label:String(ee.label??""),abbrev:String(ee.abbrev??""),written:ce===void 0?null:String(ce),discount:fe(ce)?ce:null,rank:Number(ee.rank??0)}}):[]}function fe(y){return typeof y!="number"||!Number.isFinite(y)||y<0||y>100?!1:Math.abs(y*10-Math.round(y*10))<1e-9}const _e=u(()=>String(t.draft.value("pricing.reference_code")??""));function ae(y){return y===null?"":String(y).replace(".",",")}function C(y){const I=1e3-Math.round(y*10);return`${String(Math.trunc(I/100))},${String(I%100).padStart(2,"0")}`}function ge(y,I){const W=Number(I);I.trim()===""||Number.isNaN(W)||t.draft.set(y,W)}function Q(y,I){const W=I.trim().replace(",",".");if(!/^\d{1,3}(\.\d)?$/u.test(W))return;const ee=Number(W);ee>100||t.draft.set(y,ee)}function be(y,I){!(y instanceof HTMLInputElement)||y.value===I||(y.value=I)}async function se(){try{const y=await Ja();p(H,Object.fromEntries(y.products.map(I=>[I.id,I.name])),!0),p(J,"read")}catch{p(J,"unread")}}function P(y){const I=e(H)[y];return I!==void 0?I:e(J)==="loading"?"Lecture du nom…":e(J)==="read"?"Produit absent du catalogue en service":"Nom non lu"}function ne(y){return y<=1?`${String(y)} tarif déclaré`:`${String(y)} tarifs déclarés`}const V=u(()=>`${he(e(w).length)} ${e(w).length>1?"verdicts":"verdict"} sur les quatorze garde-fous.`),B=u(()=>`${he(e(M).length)} ${e(M).length>1?"lignes affichées":"ligne affichée"} sur ${he(e(T).length)} `+(e(A).length===0?`${e(T).length>1?"dérogations en vigueur":"dérogation en vigueur"}.`:`${e(T).length>1?"dérogations enregistrées":"dérogation enregistrée"}, dont ${he(e(A).length)} sur un produit retiré, sans effet : le garde-fou 14 refuse le produit avant que le 8 ait un sens.`)+(e(T).length>e(M).length?" Les autres se lisent produit par produit depuis l’onglet Catalogue.":""));function re(y){return`plancher ${he(y)} g : refusé à ${he(y)} g et en dessous`}const N=[{rank:1,code:"OVERLOAD",label:"Surcharge",when:"La balance annonce elle-même OL, ou le poids brut dépasse la capacité.",severity:"Bloquant",blocking:!0,message:"La balance est en surcharge. Retirez votre article.",thresholds:[],switchPath:"",switchLabel:"",note:`Seuil : la capacité, réglée au garde-fou 9 sous « ${Gt("limits.max_weight_g")} ».`},{rank:2,code:"MEASUREMENT_EXPIRED",label:"Poids périmé",when:"La mesure est plus vieille que la péremption, dans les deux modes de stabilité.",severity:"Bloquant",blocking:!0,message:"Poids indisponible. Patientez ou appelez un bénévole.",thresholds:[],switchPath:"",switchLabel:"",note:"Aucun seuil à régler : le poste calcule lui-même à partir du rythme de la balance."},{rank:3,code:"BASKET_MISSING",label:"Panier absent",when:"Le poids brut tombe dans la fenêtre négative du panier : il a été soulevé.",severity:"Bloquant",blocking:!0,message:"Le panier n'est pas sur la balance. Reposez-le.",thresholds:[{path:"limits.basket_min_g",label:"Bas de la fenêtre du panier",hint:"En grammes, NÉGATIF : c’est le poids du panier que la balance a perdu."},{path:"limits.basket_max_g",label:"Haut de la fenêtre du panier",hint:"Négatif lui aussi, et plus proche de zéro que le bas."}],switchPath:"limits.basket_check_enabled",switchLabel:"Ce poste travaille avec un panier taré",note:"La règle s’active ou non, en bloc : il n’y a pas de demi-mesure à régler."},{rank:4,code:"SCALE_EMPTY",label:"Plateau vide",when:"Le poids brut ne sort pas de la bande « il n’y a rien sur le plateau ».",severity:"Bloquant — un filet, hors parcours nominal",blocking:!0,message:"Posez votre produit.",thresholds:[{path:"limits.empty_max_g",label:"Plateau considéré vide",hint:"En dessous, le poste considère qu’il n’y a rien sur le plateau."}],switchPath:"",switchLabel:"",note:"Toucher une tuile sur un plateau vide ARME la sélection au lieu d’être refusé : la règle reste évaluée pour la saisie manuelle et les chemins dérivés."},{rank:5,code:"TARE_REQUIRED",label:"Remise à zéro nécessaire",when:"Le brut est sous la bande du plateau vide, et hors de la fenêtre du panier.",severity:"Bloquant",blocking:!0,message:"La balance doit être remise à zéro.",thresholds:[],switchPath:"",switchLabel:"",note:`Seuil : la valeur négative de celui du garde-fou 4, « ${Gt("limits.empty_max_g")} ».`},{rank:6,code:"WEIGHT_UNSTABLE",label:"Pesée instable",when:"La trame déclare la mesure instable.",severity:"Information par défaut (A3)",blocking:!1,message:"Pesée en cours…",thresholds:[],switchPath:"",switchLabel:"",note:"La sévérité suit l’exigence de stabilité : elle passe à Bloquant quand celle-ci est réglée sur « blocking ». L’impression n’est jamais bloquée par défaut."},{rank:7,code:"TARE_INVALID",label:"Emballage incohérent",when:"Une tare a été saisie, et elle atteint la pesée ou dépasse le maximum.",severity:"Bloquant",blocking:!0,message:"Le poids de l'emballage est supérieur ou égal à la pesée.",thresholds:[{path:"limits.max_tare_g",label:"Tare maximum",hint:"Une tare plus lourde que le maximum est une faute de frappe."}],switchPath:"",switchLabel:"",note:""},{rank:8,code:"WEIGHT_TOO_LOW",label:"Poids trop faible",when:"Vente au poids : le NET est strictement positif et ne dépasse pas le plancher.",severity:"Bloquant",blocking:!0,message:"La balance doit être retarée, ou l'emballage est trop lourd.",thresholds:[{path:"limits.min_weight_g",label:"Poids minimum",hint:"Une dérogation par produit existe, dans l’onglet Catalogue."}],switchPath:"",switchLabel:"",note:""},{rank:9,code:"WEIGHT_TOO_HIGH",label:"Poids trop élevé",when:"Le NET dépasse la capacité — strictement, pour que la capacité reste atteignable.",severity:"Bloquant",blocking:!0,message:"{{.Weight}} kg, ça paraît un peu lourd !",thresholds:[{path:"limits.max_weight_g",label:"Poids maximum",hint:"C’est la capacité du champ NNDDD du code-barres, pas un seuil de vraisemblance."}],switchPath:"",switchLabel:"",note:""},{rank:10,code:"UNITS_OUT_OF_RANGE",label:"Nombre d’unités hors plage",when:"Vente à l’unité : la quantité sort de la plage.",severity:"Bloquant",blocking:!0,message:"{{.Quantity}} unités, ça paraît un peu beaucoup !",thresholds:[{path:"limits.min_units",label:"Unités minimum",hint:""},{path:"limits.max_units",label:"Unités maximum",hint:""}],switchPath:"",switchLabel:"",note:""},{rank:11,code:"AMOUNT_OUT_OF_CAPACITY",label:"Montant hors capacité du code-barres",when:"La charge utile encode un PRIX, et il dépasse ce que le champ peut porter.",severity:"Bloquant",blocking:!0,message:"Prix trop élevé pour le code-barres.",thresholds:[{path:"limits.max_amount_cents",label:"Montant maximum",hint:"En centimes. Aucun préfixe du plan livré n’encode un prix : la règle est éprouvée sans qu’aucun produit puisse l’atteindre."}],switchPath:"",switchLabel:"",note:""},{rank:12,code:"ZERO_PRICE",label:"Prix nul",when:"Le montant du tarif imprimé en grand vaut zéro.",severity:"Bloquant",blocking:!0,message:"Prix nul. Appelez un bénévole.",thresholds:[],switchPath:"",switchLabel:"",note:"Aucun seuil : un produit à 0 € est une anomalie sans nuance."},{rank:13,code:"LIGHT_PRODUCT_ALLOWED",label:"Produit léger autorisé",when:"Le garde-fou 8 n’a pas déclenché grâce à la dérogation du produit.",severity:"Information",blocking:!1,message:"",thresholds:[],switchPath:"",switchLabel:"",note:"Aucun seuil général : c’est la dérogation par produit, listée plus bas et posée depuis l’onglet Catalogue. Rien ne s’affiche au client ; l’id du produit est journalisé."},{rank:14,code:"PRODUCT_WITHDRAWN",label:"Produit retiré",when:"Quelqu’un a décidé de ne plus proposer ce produit.",severity:"Bloquant",blocking:!0,message:"Ce produit n'est pas disponible.",thresholds:[],switchPath:"",switchLabel:"",note:"Aucun seuil : c’est une décision humaine, prise depuis l’onglet Catalogue. Aucune règle d’import ne peut la déduire."}];function L(y){return N.find(I=>I.code===y)?.label??"Garde-fou inconnu de cet écran"}const q=["{{.Weight}}","{{.Quantity}}"];function D(y){return y==="blocking"?"Bloquant":y==="info"?"Information":"Sévérité inconnue de cet écran"}var X=Di(),me=a(X);Qe(me,{title:"Grille de tarifs",note:"Un second tarif n’est pas une case à cocher : c’est une ligne de plus dans cette grille.",children:(y,I)=>{var W=bi(),ee=Y(W);{var ce=k=>{var O=fi();l(k,O)},Ve=k=>{var O=_i(),le=Y(O),Re=a(le),We=r(le,2),we=a(We),d=r(a(we));at(d,21,()=>e(_),ma,(x,$,f)=>{var E=hi(),z=a(E),ye=a(z),ie=r(z),ke=a(ie);Ue(ke,"aria-label",`Libellé du tarif ${f+1}`);var Oe=r(ie),de=a(Oe);Ue(de,"aria-label",`Abrégé du tarif ${f+1}`);var K=r(Oe),Ae=a(K);{var Ge=ve=>{var je=gi();l(ve,je)},Fe=ve=>{var je=Ma(),$e=a(je);v(()=>i($e,`${e($).written??""} — le tarif de référence est le prix du catalogue : il + ne peut pas porter de remise, et l’enregistrement la refusera.`)),l(ve,je)},De=ve=>{var je=Ma(),$e=a(je);v(()=>i($e,`${e($).written??""} — une remise s’écrit au dixième de point ; celle-ci se + change dans le fichier de configuration.`)),l(ve,je)},Me=ve=>{var je=mi(),$e=Y(je);Ue($e,"aria-label",`Remise du tarif ${f+1}`);var st=r($e,2),it=a(st);v((pt,Ee)=>{oa($e,pt),i(it,`un produit à 10,00 €/kg s’affiche ${Ee??""} €/kg`)},[()=>ae(e($).discount??0),()=>C(e($).discount??0)]),Ze("input",$e,pt=>Q(`pricing.tiers.${String(f)}.discount_percent`,pt.currentTarget.value)),Ze("focusout",$e,pt=>be(pt.currentTarget,ae(e($).discount??0))),l(ve,je)};g(Ae,ve=>{e($).code===e(_e)&&e($).written===null?ve(Ge):e($).code===e(_e)?ve(Fe,1):e($).written!==null&&e($).discount===null?ve(De,2):ve(Me,-1)})}var te=r(K),Ne=a(te);v(()=>{i(ye,e($).code),oa(ke,e($).label),oa(de,e($).abbrev),i(Ne,e($).rank)}),Ze("input",ke,ve=>t.draft.set(`pricing.tiers.${String(f)}.label`,ve.currentTarget.value)),Ze("input",de,ve=>t.draft.set(`pricing.tiers.${String(f)}.abbrev`,ve.currentTarget.value)),l(x,E)}),v(x=>i(Re,`${x??""}.`),[()=>ne(e(_).length)]),l(k,O)};g(ee,k=>{e(_).length===0?k(ce):k(Ve,-1)})}var S=r(ee,2);{let k=u(()=>t.draft.text("pricing.primary_code"));_t(S,{label:"Tarif imprimé en grand",path:"pricing.primary_code",get value(){return e(k)},hint:"Le prix que le client lit sur l’étiquette (A7).",onchange:O=>t.draft.set("pricing.primary_code",O)})}var h=r(S,2);{let k=u(()=>t.draft.text("pricing.reference_code"));_t(h,{label:"Tarif qui serait encodé si le code-barres portait un prix",path:"pricing.reference_code",get value(){return e(k)},hint:"Le plan livré ne porte pas de prix, mais un poids ou un nombre d’unités : la caisse retrouve le prix par la référence, dans Odoo, et ne doit jamais sous-facturer.",onchange:O=>t.draft.set("pricing.reference_code",O)})}l(y,W)},$$slots:{default:!0}});var ze=r(me,2);Qe(ze,{title:"Les deux arrondis",children:(y,I)=>{var W=wi(),ee=Y(W);{let Ve=u(()=>t.draft.text("pricing.unit_price_rounding"));_t(ee,{label:"Arrondi du prix unitaire dérivé",path:"pricing.unit_price_rounding",get value(){return e(Ve)},hint:"Il s’applique au prix au kilo calculé par la remise.",onchange:S=>t.draft.set("pricing.unit_price_rounding",S)})}var ce=r(ee,2);{let Ve=u(()=>t.draft.text("pricing.amount_rounding"));_t(ce,{label:"Arrondi du montant",path:"pricing.amount_rounding",get value(){return e(Ve)},hint:"Il s’applique au montant de l’étiquette.",onchange:S=>t.draft.set("pricing.amount_rounding",S)})}l(y,W)},$$slots:{default:!0}});var Ce=r(ze,2);Qe(Ce,{title:"Les quatorze garde-fous, dans l’ordre d’évaluation",note:"Le seuil se modifie ici. La sévérité ne se règle pas : elle dit si le poste refuse ou avertit, et cela ne dépend pas du magasin. Le message affiché au client n’est pas encore modifiable depuis cet écran.",children:(y,I)=>{var W=Ti(),ee=r(Y(W),2);at(ee,21,()=>N,O=>O.code,(O,le)=>{var Re=Si(),We=a(Re),we=a(We),d=a(we),x=r(we,2),$=a(x),f=r(x,2),E=a(f),z=r(f,2),ye=a(z),ie=r(We,2),ke=a(ie),Oe=r(ie,2);{var de=te=>{var Ne=yi();l(te,Ne)},K=te=>{var Ne=xi(),ve=a(Ne);v(()=>i(ve,`« ${e(le).message??""} »`)),l(te,Ne)};g(Oe,te=>{e(le).message===""?te(de):te(K,-1)})}var Ae=r(Oe,2);{var Ge=te=>{s(te,()=>e(le).switchPath,()=>e(le).switchLabel,()=>"")};g(Ae,te=>{e(le).switchPath!==""&&te(Ge)})}var Fe=r(Ae,2);at(Fe,17,()=>e(le).thresholds,te=>te.path,(te,Ne)=>{var ve=ki(),je=a(ve);{let $e=u(()=>t.draft.text(e(Ne).path));_t(je,{get label(){return e(Ne).label},get path(){return e(Ne).path},kind:"number",get value(){return e($e)},get hint(){return e(Ne).hint},onchange:st=>ge(e(Ne).path,st)})}Ze("focusout",ve,$e=>be($e.target,t.draft.text(e(Ne).path))),l(te,ve)});var De=r(Fe,2);{var Me=te=>{var Ne=qi(),ve=a(Ne);v(()=>i(ve,e(le).note)),l(te,Ne)};g(De,te=>{e(le).note!==""&&te(Me)})}v(te=>{Ue(Re,"data-code",e(le).code),i(d,e(le).rank),i($,e(le).label),i(E,e(le).code),Ue(z,"data-blocking",te),i(ye,e(le).severity),i(ke,e(le).when)},[()=>String(e(le).blocking)]),l(O,Re)});var ce=r(ee,4),Ve=a(ce),S=r(ce,4);{var h=O=>{var le=Li();l(O,le)},k=O=>{var le=Pi(),Re=Y(le),We=a(Re),we=r(Re,2);at(we,21,()=>e(w),d=>d.code,(d,x)=>{var $=ji(),f=a($),E=a(f),z=r(f,2),ye=a(z),ie=r(z,2),ke=a(ie),Oe=r(ie,2);{var de=Ge=>{var Fe=Ei(),De=a(Fe);v(()=>i(De,`« ${e(x).message??""} »`)),l(Ge,Fe)};g(Oe,Ge=>{e(x).message!==""&&Ge(de)})}var K=r(Oe,2);{var Ae=Ge=>{var Fe=zi(),De=Y(Fe),Me=a(De),te=r(De,2),Ne=a(te);v(ve=>{i(Me,ve),i(Ne,e(x).product_id)},[()=>P(e(x).product_id)]),l(Ge,Fe)};g(K,Ge=>{e(x).product_id!==""&&Ge(Ae)})}v((Ge,Fe,De)=>{Ue($,"data-blocking",Ge),i(E,Fe),i(ye,e(x).code),i(ke,De)},[()=>String(e(x).blocking),()=>L(e(x).code),()=>D(e(x).severity)]),l(d,$)}),v(d=>{Ue(Re,"data-verdicts",d),i(We,e(V))},[()=>String(e(w).length)]),l(O,le)};g(S,O=>{e(w).length===0?O(h):O(k,-1)})}v(O=>i(Ve,`Les marqueurs ${O??""} sont remplacés par les valeurs de la pesée au + moment où le message s’affiche.`),[()=>q.join(" et ")]),l(y,W)},$$slots:{default:!0}});var Le=r(Ce,2);Qe(Le,{title:"Code-barres",note:"Un seul réglage, et c’est voulu : le reste du plan de numérotation n’est pas de la configuration.",children:(y,I)=>{var W=Ci(),ee=Y(W);s(ee,()=>"barcode.verify_reference_check_digit",()=>"Refuser une référence dont la clé de contrôle est fausse",()=>"Décoché, le poste recalcule une clé juste sur une référence fausse, en silence — et la caisse encaisse un autre article."),l(y,W)},$$slots:{default:!0}});var Z=r(Le,2);Qe(Z,{title:"Dérogations de poids minimum",note:"En lecture ici ; elles se modifient depuis l’onglet Catalogue, là où se trouve le produit.",children:(y,I)=>{var W=Ut(),ee=Y(W);{var ce=S=>{var h=Oi();l(S,h)},Ve=S=>{var h=$i(),k=Y(h),O=a(k),le=r(k,2);{var Re=we=>{var d=Ri();l(we,d)};g(le,we=>{e(J)==="unread"&&we(Re)})}var We=r(le,2);at(We,21,()=>e(M),we=>we.product_id,(we,d)=>{var x=Ni(),$=a(x),f=a($),E=r($,2),z=a(E),ye=r(E,2),ie=a(ye),ke=r(ye,2),Oe=a(ke),de=r(ke,2),K=a(de),Ae=r(de,2);{var Ge=Fe=>{var De=Ai();l(Fe,De)};g(Ae,Fe=>{e(d).offered||Fe(Ge)})}v((Fe,De,Me,te)=>{Ue(x,"data-withdrawn",Fe),i(f,De),i(z,e(d).product_id),i(ie,Me),i(Oe,e(d).reason),i(K,`${te??""}, ${e(d).decided_by??""}`)},[()=>String(!e(d).offered),()=>P(e(d).product_id),()=>re(e(d).min_weight_g??0),()=>pa(e(d).decided_at)]),l(we,x)}),v(()=>i(O,e(B))),l(S,h)};g(ee,S=>{e(T).length===0?S(ce):S(Ve,-1)})}l(y,W)},$$slots:{default:!0}}),l(n,X),$t()}At(["change","input","focusout"]);function Ui(n,t){const s=[];for(const c of[...qa(n),...qa(t)]){if(s.some(T=>T.path===c))continue;const _=Ba(fa(n,c)),w=Ba(fa(t,c));_!==w&&s.push({path:c,before:_,after:w})}return s}function fa(n,t){let s=n;for(const c of t.split(".")){if(s===null||typeof s!="object")return;s=s[c]}return s}function qa(n,t=""){if(n===null||typeof n!="object"||Array.isArray(n))return t===""?[]:[t];const s=[];for(const[c,_]of Object.entries(n))s.push(...qa(_,t===""?c:`${t}.${c}`));return s}function Ba(n){return n===void 0?"—":n===null?"vide":typeof n=="boolean"?n?"oui":"non":typeof n=="object"?JSON.stringify(n):String(n)}var Fi=o('
                                                              Empreinte de la configuration en service
                                                              Version du binaire
                                                              Répertoire de données
                                                              Espace disque
                                                              ',1),Mi=o('

                                                              '),Va=o(" "),Bi=o(' '),Vi=o("
                                                            • "),Wi=o(`

                                                                Recopier reste possible : les valeurs entrent dans le brouillon, où elles se + corrigent champ par champ avant l’enregistrement.

                                                                `),Gi=o(`

                                                                Ce fichier n’a été comparé à rien : la configuration en service n’a pas pu être + lue. Ni « identique », ni « n champs changent » — ce que ce fichier changerait + sur ce poste reste inconnu.

                                                                `),Hi=o(`

                                                                Ce fichier décrit la même configuration que celle en service : il n’y a rien à + recopier. C’est ce qu’on veut lire à la fin d’un clonage. Deux champs ne sont pas + comparés : la date du dernier enregistrement, que chaque poste écrit lui-même, et + le mot de passe du catalogue, qu’aucun des deux ne porte en clair.

                                                                `),Ji=o(' '),Ki=o('

                                                                '),Xi=o(`

                                                                ChampEn serviceDans le fichier

                                                                Recopier n’applique rien : les valeurs entrent dans le brouillon, et c’est + « Enregistrer » qui les met en service.

                                                                `,1),Yi=o('

                                                                ',1),Qi=o(`

                                                                L’export emporte encore l’empreinte du mot de passe : c’est la seule lecture que le + poste garde derrière la clé. L’import, lui, est lu PAR LE POSTE, qui écarte les deux + secrets et le numéro de poste avant de dire ce qui changerait.

                                                                `,1),Zi=o('

                                                                Lecture des versions enregistrées…

                                                                '),eo=o(`

                                                                Les versions enregistrées n’ont pas pu être lues : cette liste ne dit donc rien de + ce que ce poste garde. Ce n’est pas « aucune version ».

                                                                `),to=o('

                                                                Aucune version enregistrée : ce poste n’a jamais été reconfiguré.

                                                                '),ao=o('
                                                              • '),ro=o(`

                                                                  Remettre une version en service remplace la configuration du poste sur-le-champ, et + ce qui n’a pas été enregistré est perdu : c’est le seul geste de cette page qui + change le poste tout de suite, et le seul qui garde ses 72 px.

                                                                  `,1),no=o('
                                                                  ');function so(n,t){Nt(t,!0);const s=vt(t,"admin",7),c=40,_=20,w=5,T=new Set(["modified_at","catalog.options.password"]),A=[{path:"station.name",name:"le nom du poste"},{path:"network.listen",name:"l’adresse d’écoute"},{path:"scale.options.port",name:"le port de la balance"},{path:"printer.options.queue",name:"la file d’impression"},{path:"catalog.options.url",name:"l’adresse du partage"},{path:"catalog.options.username",name:"le compte du partage"},{path:"catalog.images.path",name:"le chemin des images"}];let M=F(Lt([])),H=F("reading"),J=F(null),pe=F(""),fe=F(null),_e=F(""),ae=F(Lt([])),C=F(""),ge="";const Q=u(()=>s().busy||e(C)!==""),be=u(()=>D(e(J),e(fe))),se=u(()=>e(J)!==null&&e(fe)!==null),P=u(()=>X(e(J),e(fe))),ne=u(()=>e(be).slice(0,c)),V=u(()=>e(ae).slice(0,_)),B=u(()=>e(M).slice(0,w)),re=u(()=>We(e(ne).length,e(be).length,"champ qui change","champs qui changent")),N=u(()=>We(e(V).length,e(ae).length,"contrôle refuse une clé","contrôles refusent une clé")),L=u(()=>We(e(B).length,e(M).length,"version enregistrée","versions enregistrées")),q=u(()=>e(be).length>1?`Recopier ces ${he(e(be).length)} champs dans le brouillon`:"Recopier ce champ dans le brouillon");Ce();function D(f,E){return f===null||E===null?[]:Ui(f,E).filter(z=>!T.has(z.path))}function X(f,E){if(f===null||E===null)return[];const z=f,ye=E;return A.filter(ie=>me(ye,ie.path)&&!me(z,ie.path))}function me(f,E){const z=fa(f,E);return z==null||z===""?!0:typeof z!="object"||Array.isArray(z)?!1:Object.keys(z).length===0}function ze(f){return f.length<2?f.join(""):`${f.slice(0,-1).join(", ")} et ${f[f.length-1]??""}`}Xt(()=>{const f=t.health.config_fingerprint;f!==ge&&(ge=f,Le())});async function Ce(){const f=await s().load(Mr);if(f===null){p(H,"unreadable");return}p(M,f,!0),p(H,"read")}async function Le(){try{p(J,(await za()).config,!0),p(pe,"")}catch(f){p(J,null),p(pe,f instanceof Error?f.message:"Le poste n’a pas répondu.",!0)}}async function Z(f){p(C,f?"export-all":"export-clone",!0),s().notice="",s().actionError="";try{const E=await s().protect(()=>y(f));if(E===null)return;ee(E.name,E.blob),s().notice=`${E.name} est remis au navigateur : c’est lui qui l’enregistre, voyez ses téléchargements.`}finally{p(C,"")}}async function y(f){const z=await fetch(`/admin/api/config/export?hardware=${f?"1":"0"}`,{headers:{accept:"application/json"}});if(!z.ok)throw new Ft(z.status,O(await z.text(),"L’export"));return{name:I(f),blob:await z.blob()}}function I(f){const E=f?"":"-sans-materiel";return`config-poste${String(t.health.station)}${E}-${W()}.json`}function W(){const f=new Date,E=String(f.getMonth()+1).padStart(2,"0"),z=String(f.getDate()).padStart(2,"0");return`${String(f.getFullYear())}-${E}-${z}`}function ee(f,E){const z=URL.createObjectURL(E),ye=document.createElement("a");ye.href=z,ye.download=f,document.body.appendChild(ye),ye.click(),ye.remove(),setTimeout(()=>{URL.revokeObjectURL(z)},0)}async function ce(f){const E=await fetch("/admin/api/config/import",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(f)}),z=await E.text();if(!E.ok)throw new Ft(E.status,O(z,"L’import"));return JSON.parse(z)}async function Ve(f){if(f==null)return;s().notice="",s().actionError="";let E;try{E=JSON.parse(await f.text())}catch{s().actionError=`${f.name} n’est pas un fichier JSON lisible.`;return}p(C,"import");try{const z=await s().protect(()=>ce(E));if(z===null)return;p(fe,z.config,!0),p(_e,f.name,!0),p(ae,z.faults,!0),await Le(),s().notice=S(f.name)}finally{p(C,"")}}function S(f){return e(se)?e(be).length===0?`${f} décrit la même configuration que celle en service.`:`${f} est lu. Rien n’est appliqué : relisez le tableau.`:`${f} est lu, mais la configuration en service ne l’est pas : rien ne peut être comparé.`}async function h(){if(e(fe)===null)return;const f=e(fe),E=[...e(be)];for(const z of E)t.draft.set(z.path,fa(f,z.path));s().actionError="",s().notice="Le fichier est recopié dans le brouillon. Rien n’est appliqué avant « Enregistrer ».",await Le()}async function k(f){p(C,`restore-${String(f)}`),s().notice="",s().actionError="";try{if(await s().protect(()=>Br(f))===null)return;await t.draft.load(),await Ce(),await Le(),await s().refresh(),s().notice=`La version ${String(f)} est remise en service.`}finally{p(C,"")}}function O(f,E){try{const z=JSON.parse(f);if(typeof z?.message=="string"&&z.message!=="")return z.message}catch{}return`${E} a été refusé par le poste.`}function le(f){return t.draft.faults.find(E=>E.field===f)?.message??""}function Re(f){return t.draft.faults.find(E=>E.field===f)?.allowed??[]}function We(f,E,z,ye){const ie=E>1?ye:z;return f>=E?`${he(E)} ${ie}.`:`${he(f)} lignes affichées sur ${he(E)} ${ie}.`}var we=no(),d=a(we);Qe(d,{title:"Identité du poste",children:(f,E)=>{var z=Fi(),ye=Y(z);{let je=u(()=>t.draft.text("station.number")),$e=u(()=>le("station.number")),st=u(()=>Re("station.number"));_t(ye,{label:"Numéro du poste",path:"station.number",kind:"number",get value(){return e(je)},get fault(){return e($e)},get allowed(){return e(st)},hint:"C’est de lui que dérive le nom du fichier de catalogue attendu, flv_.csv.",onchange:it=>t.draft.set("station.number",Number(it))})}var ie=r(ye,2);{let je=u(()=>t.draft.text("station.name")),$e=u(()=>le("station.name")),st=u(()=>Re("station.name"));_t(ie,{label:"Nom du poste",path:"station.name",get value(){return e(je)},get fault(){return e($e)},get allowed(){return e(st)},hint:"Ce que lit un bénévole : « Poste 2 — fruits ».",onchange:it=>t.draft.set("station.name",it)})}var ke=r(ie,2);{let je=u(()=>t.draft.text("station.coop")),$e=u(()=>le("station.coop")),st=u(()=>Re("station.coop"));_t(ke,{label:"Coopérative",path:"station.coop",get value(){return e(je)},get fault(){return e($e)},get allowed(){return e(st)},onchange:it=>t.draft.set("station.coop",it)})}var Oe=r(ke,2),de=r(a(Oe),2),K=a(de),Ae=r(de,4),Ge=a(Ae),Fe=r(Ae,4),De=a(Fe),Me=r(Fe,4),te=a(Me);{var Ne=je=>{var $e=xt("non mesuré");l(je,$e)},ve=je=>{var $e=xt();v((st,it,pt)=>i($e,`${st??""} libres sur + ${it??""} — seuil d’alerte + ${pt??""} Mo`),[()=>ka(t.health.disk.free_bytes),()=>ka(t.health.disk.total_bytes),()=>he(t.health.disk.alert_mb)]),l(je,$e)};g(te,je=>{t.health.disk===null?je(Ne):je(ve,-1)})}v(()=>{i(K,t.health.config_fingerprint),i(Ge,t.health.version),i(De,t.health.disk===null?"non publié par ce poste":t.health.disk.path)}),l(f,z)},$$slots:{default:!0}});var x=r(d,2);Qe(x,{title:"Exporter, importer",note:"Pour installer un autre poste : ce fichier emporte les tarifs, les garde-fous, l’étiquette, les catégories, et les réglages du matériel que les quatre postes partagent — le décalage d’étiquette, le noircissement, la vitesse, le débit de la balance. Reste ici ce qui désigne ce poste-ci ou ce magasin — le mot de passe, le code de secours, le numéro et le nom du poste, le port de la balance, la file d’impression, l’adresse du partage et son compte, le chemin des images et le réseau.",children:(f,E)=>{var z=Qi(),ye=Y(z),ie=a(ye);{let te=u(()=>e(C)==="export-all");tt(ie,{label:"Exporter tout",protected:!0,get busy(){return e(te)},get disabled(){return e(Q)},onrun:()=>{Z(!0)}})}var ke=r(ie,2);{let te=u(()=>e(C)==="export-clone");tt(ke,{label:"Exporter sans le matériel",protected:!0,get busy(){return e(te)},get disabled(){return e(Q)},onrun:()=>{Z(!1)}})}var Oe=r(ke,2);let de;var K=a(Oe),Ae=r(K,3),Ge=r(ye,4);{var Fe=te=>{var Ne=Mi(),ve=a(Ne);v(()=>i(ve,`La configuration en service n’a pas pu être lue : ${e(pe)??""} La colonne + « En service » ne peut donc rien affirmer.`)),l(te,Ne)};g(Ge,te=>{e(pe)!==""&&te(Fe)})}var De=r(Ge,2);{var Me=te=>{var Ne=Yi(),ve=Y(Ne),je=a(ve),$e=r(ve,2);{var st=et=>{var mt=Wi(),Dt=a(mt),Bt=a(Dt),Vt=r(Dt,2);at(Vt,21,()=>e(V),ma,(Et,bt)=>{var Tt=Vi(),b=a(Tt),G=a(b),oe=r(b,2);{var qe=j=>{var U=Va(),xe=a(U);v(()=>i(xe,e(bt).field)),l(j,U)};g(oe,j=>{Ht.showTechnicalNames&&j(qe)})}var Je=r(oe),Se=r(Je);{var Pe=j=>{var U=Bi(),xe=a(U);v(Xe=>i(xe,`Valeurs acceptées : ${Xe??""}.`),[()=>e(bt).allowed.join(", ")]),l(j,U)};g(Se,j=>{e(bt).allowed!==void 0&&e(bt).allowed.length>0&&j(Pe)})}v(j=>{i(G,j),i(Je,` ${e(bt).message??""} `)},[()=>Gt(e(bt).field)]),l(Et,Tt)}),v(()=>i(Bt,`Ce fichier serait refusé en l’état : ${e(N)??""}`)),l(et,mt)};g($e,et=>{e(ae).length>0&&et(st)})}var it=r($e,2);{var pt=et=>{var mt=Gi();l(et,mt)},Ee=et=>{var mt=Hi();l(et,mt)},nt=et=>{var mt=Xi(),Dt=Y(mt),Bt=a(Dt),Vt=r(Bt);{var Et=Se=>{var Pe=xt("Les autres sont dans le fichier, et « Recopier » les prend tous.");l(Se,Pe)};g(Vt,Se=>{e(be).length>e(ne).length&&Se(Et)})}var bt=r(Dt,2),Tt=a(bt),b=r(a(Tt));at(b,21,()=>e(ne),Se=>Se.path,(Se,Pe)=>{const j=u(()=>Gt(e(Pe).path));var U=Ji(),xe=a(U),Xe=a(xe),Ke=r(Xe);{var Ye=dt=>{var ot=Va(),ut=a(ot);v(()=>i(ut,e(Pe).path)),l(dt,ot)};g(Ke,dt=>{Ht.showTechnicalNames&&e(j)!==e(Pe).path&&dt(Ye)})}var ue=r(xe),Ie=a(ue),Be=r(ue),ct=a(Be);v(()=>{Ue(U,"data-path",e(Pe).path),i(Xe,`${e(j)??""} `),i(Ie,e(Pe).before),i(ct,e(Pe).after)}),l(Se,U)});var G=r(bt,2);{var oe=Se=>{var Pe=Ki(),j=a(Pe);v(U=>i(j,`Ce fichier ne porte pas ${U??""} : + l’export sans le matériel les retire, et l’import ne remet que le numéro du + poste. Les lignes correspondantes sont VIDES ci-dessus, et + « Recopier » recopie ce vide dans le brouillon.`),[()=>ze(e(P).map(U=>U.name))]),l(Se,Pe)};g(G,Se=>{e(P).length>0&&Se(oe)})}var qe=r(G,2),Je=a(qe);tt(Je,{kind:"write",get label(){return e(q)},get disabled(){return e(Q)},onrun:()=>{h()}}),v(()=>i(Bt,`${e(re)??""} `)),l(et,mt)};g(it,et=>{e(se)?e(be).length===0?et(Ee,1):et(nt,-1):et(pt)})}v(()=>i(je,`Fichier lu : ${e(_e)??""}`)),l(te,Ne)};g(De,te=>{e(fe)!==null&&te(Me)})}v(()=>{de=Zt(Oe,1,"choose svelte-18wbxwu",null,de,{working:e(C)==="import",off:e(Q)}),i(K,`${e(C)==="import"?"Lecture du fichier…":"Importer un fichier"} `),Ae.disabled=e(Q)}),Ze("change",Ae,te=>{Ve(te.currentTarget.files?.item(0))}),l(f,z)},$$slots:{default:!0}});var $=r(x,2);Qe($,{title:"Cinq versions restaurables",note:"Chaque enregistrement fait tourner les versions : la plus récente est la 1.",children:(f,E)=>{var z=Ut(),ye=Y(z);{var ie=K=>{var Ae=Zi();l(K,Ae)},ke=K=>{var Ae=eo();l(K,Ae)},Oe=K=>{var Ae=to();l(K,Ae)},de=K=>{var Ae=ro(),Ge=Y(Ae),Fe=a(Ge),De=r(Ge,2),Me=a(De);at(Me,21,()=>e(B),te=>te.version,(te,Ne)=>{var ve=ao(),je=a(ve),$e=a(je),st=r(je,2),it=a(st),pt=r(st,2),Ee=a(pt),nt=r(pt,2);{let et=u(()=>e(C)===`restore-${String(e(Ne).version)}`);tt(nt,{kind:"destructive",label:"Remettre cette version en service",protected:!0,get busy(){return e(et)},get disabled(){return e(Q)},onrun:()=>{k(e(Ne).version)}})}v((et,mt)=>{i($e,`version ${et??""}`),i(it,mt),i(Ee,e(Ne).config_fingerprint)},[()=>he(e(Ne).version),()=>Mt(e(Ne).modified_at)]),l(te,ve)}),v(()=>i(Fe,`${e(L)??""} Le poste n’en garde jamais plus de cinq.`)),l(K,Ae)};g(ye,K=>{e(H)==="reading"?K(ie):e(H)==="unreadable"?K(ke,1):e(M).length===0?K(Oe,2):K(de,-1)})}l(f,z)},$$slots:{default:!0}}),l(n,we),$t()}At(["change"]);var lo=o('clé'),io=o(' '),oo=o('');function aa(n,t){const s=vt(t,"kind",3,"read"),c=vt(t,"hint",3,""),_=vt(t,"disabled",3,!1),w=vt(t,"engaged",3,!1),T=vt(t,"busy",3,!1),A=vt(t,"protected",3,!1);var M=oo();let H;var J=a(M),pe=a(J),fe=r(pe);{var _e=ge=>{var Q=lo();l(ge,Q)};g(fe,ge=>{A()&&ge(_e)})}var ae=r(J,2);{var C=ge=>{var Q=io(),be=a(Q);v(()=>i(be,T()?"En cours…":c())),l(ge,Q)};g(ae,ge=>{c()!==""&&ge(C)})}v(()=>{H=Zt(M,1,`big touch-target ${s()??""}`,"svelte-15nlt6i",H,{engaged:w(),busy:T()}),Ue(M,"data-kind",s()),M.disabled=_()||T(),i(pe,`${t.label??""} `)}),Ze("click",M,function(...ge){t.onrun?.apply(this,ge)}),l(n,M)}At(["click"]);const uo="ERR-SCL-09";var co=o('

                                                                  '),vo=o('

                                                                  '),po=o('

                                                                  '),fo=o('

                                                                  Déposez ici le fichier clé

                                                                  '),go=o('

                                                                  ',1),mo=o('');function ho(n,t){Nt(t,!0);const s=vt(t,"admin",7);let c=F("");const _=u(()=>t.health.state.degraded?.code===uo),w=u(()=>t.health.printing),T=u(()=>e(w)?.on_fallback===!0);let A=F(!1),M=F("");const H=u(()=>s().busy||e(M)!==""),J=new ir;Xt(()=>{J.observe(t.health)});const pe=u(()=>t.health.catalog_source===null?"Ce poste ne publie pas la source de son catalogue.":"Catalogue surveillé : "+t.health.catalog_source.label),fe=u(()=>t.health.catalog!==null&&(t.health.catalog.result==="rejected"||t.health.catalog.result==="failed")?t.health.catalog:null);function _e(h){p(c,""),J.forget(),p(M,h,!0)}async function ae(h,k){_e(h);try{await s().run(k)}finally{p(M,"")}}async function C(){_e("reload");try{const h=await s().run(jr);h!==null&&J.begin(h)}finally{p(M,"")}}async function ge(h,k){_e(h);try{const O=await s().load(k);O!==null&&p(c,O.message,!0)}finally{p(M,"")}}async function Q(h,k){_e(h);try{const O=await s().protect(k);O!==null&&(s().notice=O.message,await s().refresh())}finally{p(M,"")}}async function be(h){if(p(A,!1),h!=null){_e("import");try{const k=await s().protect(()=>Ya(h));if(k===null)return;p(c,k.result==="rejected"||k.result==="failed"?`${h.name} : REFUSÉ${k.reason===""?"":" — "+k.reason}. Le catalogue en service n’a pas changé.`:`${h.name} : ${String(k.rows_read_count)} lignes lues, ${String(k.weighable_count)} pesables. La veille l’appliquera dans la seconde.`,!0),await s().refresh()}finally{p(M,"")}}}var se=mo(),P=a(se);{var ne=h=>{var k=co(),O=a(k);v(()=>i(O,e(c))),l(h,k)};g(P,h=>{e(c)!==""&&h(ne)})}var V=r(P,2);{var B=h=>{var k=vo(),O=a(k);v(()=>i(O,J.sentence)),l(h,k)};g(V,h=>{J.sentence!==""&&h(B)})}var re=r(V,2);{var N=h=>{var k=po(),O=a(k);v(()=>i(O,e(w).banner)),l(h,k)};g(re,h=>{e(w)!==null&&e(w).banner!==""&&h(N)})}var L=r(re,2),q=a(L),D=r(L,2),X=a(D);{let h=u(()=>e(M)==="scale");aa(X,{label:"Tester la balance",hint:"Ce que le poste a déjà observé — le port n’est pas rouvert.",get busy(){return e(h)},get disabled(){return e(H)},onrun:()=>{ge("scale",Or)}})}var me=r(X,2);{let h=u(()=>e(M)==="printer");aa(me,{label:"Tester l’imprimante",hint:"Ce que le superviseur a vu il y a moins d’une seconde.",get busy(){return e(h)},get disabled(){return e(H)},onrun:()=>{ge("printer",Rr)}})}var ze=r(me,2);{let h=u(()=>e(M)==="label");aa(ze,{label:"Imprimer une étiquette de test",hint:"Une étiquette de démonstration sort de l’imprimante du poste.",get busy(){return e(h)},get disabled(){return e(H)},onrun:()=>{ae("label",Ar)}})}var Ce=r(ze,2);{let h=u(()=>e(M)==="reprint");aa(Ce,{label:"Réimprimer la dernière",hint:"La dernière étiquette imprimée sort une seconde fois.",get busy(){return e(h)},get disabled(){return e(H)},onrun:()=>{ae("reprint",zr)}})}var Le=r(Ce,2);{let h=u(()=>e(M)==="reload");aa(Le,{label:"Recharger le catalogue",kind:"write",hint:"La veille refait tout de suite le contrôle qu’elle fait toutes les cinq secondes.",get busy(){return e(h)},get disabled(){return e(H)},onrun:()=>{C()}})}var Z=r(Le,2);{let h=u(()=>e(_)?"Revenir à la balance":"Basculer en saisie manuelle"),k=u(()=>e(_)?"Le poids sera de nouveau lu sur la balance.":"Le poids se tape à la main : le poste continue de servir sans balance."),O=u(()=>e(M)==="manual");aa(Z,{get label(){return e(h)},kind:"destructive",get hint(){return e(k)},get engaged(){return e(_)},protected:!0,get busy(){return e(O)},get disabled(){return e(H)},onrun:()=>{Q("manual",()=>Pr(!e(_)))}})}var y=r(Z,2);{let h=u(()=>e(M)==="roll");aa(y,{label:"J’ai changé le rouleau",kind:"write",hint:"Le compteur d’étiquettes repart à zéro. C’est le seul geste qui dise quelque chose de vrai du papier.",get busy(){return e(h)},get disabled(){return e(H)},onrun:()=>{ae("roll",Tr)}})}var I=r(y,2);{var W=h=>{{let k=u(()=>e(T)?"Revenir à l’imprimante du poste":"Imprimer sur l’imprimante du poste voisin"),O=u(()=>e(T)?"Les étiquettes repartiront sur l’imprimante de ce poste.":"Les étiquettes sortiront sur l’imprimante voisine, pour cette session seulement."),le=u(()=>e(M)==="fallback");aa(h,{get label(){return e(k)},kind:"write",get hint(){return e(O)},get engaged(){return e(T)},get busy(){return e(le)},get disabled(){return e(H)},onrun:()=>{ae("fallback",()=>Cr(!e(T)))}})}};g(I,h=>{e(w)!==null&&e(w).fallback_available&&h(W)})}var ee=r(I,2),ce=r(D,2);Qe(ce,{title:"Importer un catalogue",note:"Glissez le fichier CSV ici, ou choisissez-le. Il passe par le même chemin que le fichier du producteur.",children:(h,k)=>{var O=fo();let le;var Re=a(O),We=r(a(Re)),we=a(We),d=r(Re,2),x=r(a(d));v(()=>{le=Zt(O,1,"drop svelte-1yd0nzg",null,le,{dropping:e(A)}),i(we,`flv_${t.health.station??""}.csv`)}),na("dragover",O,$=>{$.preventDefault(),p(A,!0)}),na("dragleave",O,()=>p(A,!1)),na("drop",O,$=>{$.preventDefault(),be($.dataTransfer?.files.item(0))}),Ze("change",x,$=>{be($.currentTarget.files?.item(0))}),l(h,O)},$$slots:{default:!0}});var Ve=r(ce,2);{var S=h=>{Qe(h,{title:"Le dernier fichier n’a pas pris service",children:(k,O)=>{var le=go(),Re=Y(le),We=a(Re),we=r(Re,2),d=a(we);v((x,$)=>{i(We,`${x??""} — + ${(e(fe).reason===""?"aucun motif enregistré.":e(fe).reason)??""} + Le catalogue en service n’a pas changé.`),i(d,`Dernier essai : ${$??""}.`)},[()=>ha(e(fe).result),()=>Mt(e(fe).occurred_at)]),l(k,le)},$$slots:{default:!0}})};g(Ve,h=>{e(fe)!==null&&h(S)})}v(()=>{i(q,e(pe)),Ue(ee,"href",Nr)}),l(n,se),$t()}At(["change"]);const _o={succeeded:"La dernière mise à jour a réussi.","rolled-back":"La dernière mise à jour a échoué. La version précédente a été remise et le poste fonctionne.","rolled-back-unhealthy":"La dernière mise à jour a échoué et le poste n’a pas redémarré. Appelez le support.","not-started":"La dernière mise à jour n’a pas démarré : rien n’a été remplacé. Vous pouvez réessayer."},bo=2e3,wo=300*1e3;var yo=o('

                                                                  '),Wa=o('

                                                                  '),xo=o("
                                                                  Dernière vérification
                                                                  ",1),ko=o(`

                                                                  La mise à jour depuis cet écran n’existe que sur les postes Windows. Sur les + autres, elle se fait à la main — voir la notice d’installation.

                                                                  `),qo=o('Voir les nouveautés'),So=o('

                                                                  Version disponible : .

                                                                  '),Lo=o('

                                                                  Ce poste est à jour.

                                                                  '),Eo=o("
                                                                  Version installée
                                                                  Dépôt suivi
                                                                  ",1),zo=o(`

                                                                  Le poste va s’arrêter environ une minute. L’écran client s’éteindra puis reviendra + tout seul. Si la nouvelle version ne démarre pas, la précédente sera remise + automatiquement — mais les données enregistrées, elles, ne reviendront pas en + arrière.

                                                                  `),jo=o('

                                                                  Mise à jour en cours. Le poste redémarre, ne le débranchez pas.

                                                                  '),Po=o('
                                                                  ',1),To=o("
                                                                  Terminée le
                                                                  ",1),Co=o("
                                                                  Raison
                                                                  ",1),Oo=o('

                                                                  Depuis
                                                                  Vers
                                                                  ',1),Ro=o(" ",1);function Ao(n,t){Nt(t,!0);let s=F(null),c=F(""),_=F(""),w=F(!1),T=F("");const A=u(()=>e(s)?.outcome??null),M=u(()=>e(s)?.supported===!0&&e(s).available),H=u(()=>e(s)?.latest??"");Xt(()=>{J()});async function J(){try{p(s,await nn(),!0),p(c,"")}catch(P){p(c,P instanceof Error?P.message:"Lecture impossible.",!0)}}async function pe(){p(_,"check");try{const P=await t.admin.protect(()=>sn());P!==null&&(p(s,P,!0),p(c,""))}finally{p(_,"")}}async function fe(P){p(_,"apply"),p(T,"");try{if(await t.admin.protect(()=>ln(P))===null)return;p(w,!0),p(T,await _e()?"":"Le poste n’a pas répondu dans les cinq minutes. Allez le voir.",!0),await J()}finally{p(_,""),p(w,!1)}}async function _e(){const P=Date.now()+wo;for(;Date.now()setTimeout(ne,bo));try{if((await fetch("/healthz",{cache:"no-store"})).ok)return!0}catch{}}return!1}var ae=Ro(),C=Y(ae);Qe(C,{title:"Version de ce poste",children:(P,ne)=>{var V=Ut(),B=Y(V);{var re=L=>{var q=yo(),D=a(q);v(()=>i(D,e(c)===""?"Lecture…":e(c))),l(L,q)},N=L=>{var q=Eo(),D=Y(q);{var X=S=>{var h=Wa(),k=a(h);v(()=>i(k,e(c))),l(S,h)};g(D,S=>{e(c)!==""&&S(X)})}var me=r(D,2),ze=r(a(me),2),Ce=a(ze),Le=r(ze,4),Z=a(Le),y=r(Le,2);{var I=S=>{var h=xo(),k=r(Y(h),2),O=a(k);v(le=>i(O,le),[()=>Mt(e(s).checked_at)]),l(S,h)};g(y,S=>{e(s).checked_at!==""&&S(I)})}var W=r(me,2);{var ee=S=>{var h=ko();l(S,h)},ce=S=>{var h=So(),k=r(a(h)),O=a(k),le=r(k,2);{var Re=d=>{var x=xt();v($=>i(x,`, publiée le ${$??""}`),[()=>Mt(e(s).published_at)]),l(d,x)};g(le,d=>{e(s).published_at!==""&&d(Re)})}var We=r(le,2);{var we=d=>{var x=qo();v(()=>Ue(x,"href",e(s).html_url)),l(d,x)};g(We,d=>{e(s).html_url!==""&&d(we)})}v(()=>i(O,e(s).latest)),l(S,h)},Ve=S=>{var h=Lo();l(S,h)};g(W,S=>{e(s).supported?e(s).available?S(ce,1):S(Ve,-1):S(ee)})}v(()=>{i(Ce,e(s).running),i(Z,e(s).repository)}),l(L,q)};g(B,L=>{e(s)===null?L(re):L(N,-1)})}l(P,V)},$$slots:{default:!0}});var ge=r(C,2);{var Q=P=>{Qe(P,{title:"Installer",children:(ne,V)=>{var B=Po(),re=Y(B),N=a(re);{let Z=u(()=>e(_)==="check");tt(N,{kind:"read",label:"Vérifier maintenant",protected:!0,act:"check",get busy(){return e(Z)},get disabled(){return e(w)},onrun:()=>{pe()}})}var L=r(N,2);{var q=Z=>{{let y=u(()=>`Installer la version ${e(H)}`),I=u(()=>e(_)==="apply");tt(Z,{kind:"destructive",get label(){return e(y)},protected:!0,act:"apply",get busy(){return e(I)},onrun:()=>{fe(e(H))}})}};g(L,Z=>{e(M)&&Z(q)})}var D=r(re,2);{var X=Z=>{var y=zo();l(Z,y)};g(D,Z=>{e(M)&&Z(X)})}var me=r(D,2);{var ze=Z=>{var y=jo();l(Z,y)};g(me,Z=>{e(w)&&Z(ze)})}var Ce=r(me,2);{var Le=Z=>{var y=Wa(),I=a(y);v(()=>i(I,e(T))),l(Z,y)};g(Ce,Z=>{e(T)!==""&&Z(Le)})}l(ne,B)},$$slots:{default:!0}})};g(ge,P=>{e(s)!==null&&e(s).supported&&P(Q)})}var be=r(ge,2);{var se=P=>{Qe(P,{title:"Dernière tentative",children:(ne,V)=>{var B=Oo(),re=Y(B),N=a(re),L=r(re,2),q=r(a(L),2),D=a(q),X=r(q,4),me=a(X),ze=r(X,2);{var Ce=y=>{var I=To(),W=r(Y(I),2),ee=a(W);v(ce=>i(ee,ce),[()=>Mt(e(A).finished_at)]),l(y,I)};g(ze,y=>{e(A).finished_at!==""&&y(Ce)})}var Le=r(ze,2);{var Z=y=>{var I=Co(),W=r(Y(I),2),ee=a(W);v(()=>i(ee,e(A).reason)),l(y,I)};g(Le,y=>{e(A).reason!==""&&y(Z)})}v(()=>{Ue(re,"data-outcome",e(A).status),i(N,_o[e(A).status]),i(D,e(A).from===""?"—":e(A).from),i(me,e(A).to===""?"—":e(A).to)}),l(ne,B)},$$slots:{default:!0}})};g(be,P=>{e(A)!==null&&P(se)})}l(n,ae),$t()}var No=o(''),$o=o('

                                                                  ',1),Do=o('

                                                                  ',1),Io=o(''),Uo=o(''),Fo=o(''),Mo=o(''),Bo=o(' '),Vo=o(''),Wo=o('

                                                                  Lecture de l’état du poste…

                                                                  '),Go=o('

                                                                  '),Ho=o(" "),Jo=o("
                                                                • "),Ko=o('
                                                                    '),Xo=o('
                                                                    '),Yo=o('

                                                                    ');function Qo(n,t){Nt(t,!0);const s=new wn,c=new on(s),_=[{title:"Au quotidien",pages:[{id:"dashboard",label:"Tableau de bord"},{id:"troubleshooting",label:"Dépannage"}]},{title:"Réglages",pages:[{id:"hardware",label:"Matériel"},{id:"label",label:"Étiquette"},{id:"rules",label:"Règles"},{id:"catalog",label:"Catalogue"},{id:"journal",label:"Journal"},{id:"station",label:"Poste"},{id:"update",label:"Mise à jour"}]}],w=u(()=>_.flatMap(d=>d.pages).find(d=>d.id===s.page)?.label??""),T=u(()=>s.health===null||s.health.station_name===""?`Poste ${String(s.health?.station??"")}`:s.health.station_name),A=u(()=>(c.pending?.changed_blocks??[]).map(d=>vn(d)).join(", ")),M=u(()=>(c.pending?.changed_blocks??[]).join(", "));Ea(()=>(s.start(),()=>s.stop()));async function H(d){s.open(d),$a(d)&&c.config===null&&await c.load()}async function J(){await s.protect(()=>c.save())===!0&&(s.notice="La configuration est enregistrée et appliquée.")}var pe=Yo(),fe=a(pe),_e=r(a(fe),2);at(_e,17,()=>_,d=>d.title,(d,x)=>{var $=$o(),f=Y($),E=a(f),z=r(f,2);at(z,17,()=>e(x).pages,ye=>ye.id,(ye,ie)=>{var ke=No();let Oe;var de=a(ke);v(()=>{Oe=Zt(ke,1,"entry svelte-9b2mjq",null,Oe,{current:s.page===e(ie).id}),Ue(ke,"aria-current",s.page===e(ie).id?"page":void 0),i(de,e(ie).label)}),Ze("click",ke,()=>{H(e(ie).id)}),l(ye,ke)}),v(()=>i(E,e(x).title)),l(d,$)});var ae=r(_e,2),C=a(ae);{var ge=d=>{var x=Do(),$=Y(x),f=a($),E=r($,2),z=a(E),ye=r(E,2),ie=a(ye);v(()=>{i(f,e(T)),i(z,`${s.health.coop??""} · version ${s.health.version??""}`),i(ie,`configuration ${s.health.config_fingerprint??""}`)}),l(d,x)};g(C,d=>{s.health!==null&&d(ge)})}var Q=r(C,2),be=a(Q),se=r(Q,2);{var P=d=>{var x=Io();Ze("click",x,function(...$){t.onclose?.apply(this,$)}),l(d,x)};g(se,d=>{t.onclose!==void 0&&d(P)})}var ne=r(fe,2),V=a(ne);{var B=d=>{var x=Uo(),$=a(x);v(()=>i($,s.notice)),l(d,x)};g(V,d=>{s.notice!==""&&d(B)})}var re=r(V,2);{var N=d=>{var x=Fo(),$=a(x);v(()=>i($,s.linkError)),l(d,x)};g(re,d=>{s.linkError!==""&&d(N)})}var L=r(re,2);{var q=d=>{var x=Mo(),$=a(x);v(()=>i($,s.actionError)),l(d,x)};g(L,d=>{s.actionError!==""&&d(q)})}var D=r(L,2);{var X=d=>{var x=Vo(),$=a(x),f=a($),E=r(f);{var z=ke=>{var Oe=Bo(),de=a(Oe);v(()=>i(de,e(M))),l(ke,Oe)};g(E,ke=>{Ht.showTechnicalNames&&ke(z)})}var ye=r(E),ie=r($,2);tt(ie,{kind:"write",label:"Tout fonctionne : confirmer",onrun:()=>{s.protect(()=>c.confirm())}}),v(()=>{i(f,`Configuration appliquée mais NON CONFIRMÉE. Ce qui a changé : ${e(A)??""}. `),i(ye,` Le poste reviendra tout seul à la version précédente dans + ${c.pending.seconds_left??""} secondes si personne ne confirme.`)}),l(d,x)};g(D,d=>{c.pending!==null&&d(X)})}var me=r(D,2),ze=a(me),Ce=a(ze),Le=r(ze,2);{var Z=d=>{var x=Wo();l(d,x)},y=d=>{ql(d,{get health(){return s.health},onshowrows:()=>{H("catalog")},onshowupdate:()=>{H("update")}})},I=d=>{ho(d,{get admin(){return s},get health(){return s.health}})},W=d=>{Nl(d,{get admin(){return s},get draft(){return c},get health(){return s.health}})},ee=d=>{ci(d,{get admin(){return s},get draft(){return c}})},ce=d=>{Ii(d,{get draft(){return c},get health(){return s.health}})},Ve=d=>{Ds(d,{get admin(){return s},get draft(){return c},get health(){return s.health}})},S=d=>{ti(d,{get admin(){return s}})},h=d=>{so(d,{get admin(){return s},get draft(){return c},get health(){return s.health}})},k=d=>{Ao(d,{get admin(){return s}})};g(Le,d=>{s.health===null?d(Z):s.page==="dashboard"?d(y,1):s.page==="troubleshooting"?d(I,2):s.page==="hardware"?d(W,3):s.page==="label"?d(ee,4):s.page==="rules"?d(ce,5):s.page==="catalog"?d(Ve,6):s.page==="journal"?d(S,7):s.page==="station"?d(h,8):s.page==="update"&&d(k,9)})}var O=r(me,2);{var le=d=>{var x=Xo(),$=a(x);{var f=ie=>{var ke=Go(),Oe=a(ke),de=r(Oe);at(de,16,()=>c.retired,K=>K,(K,Ae)=>{{let Ge=u(()=>`retirer ${Ae}`);tt(K,{kind:"write",get label(){return e(Ge)},onrun:()=>c.dropRetired(Ae)})}}),v(K=>i(Oe,`Ce fichier porte des réglages que cette version du poste ne connaît plus : + ${K??""}. `),[()=>c.retired.map(K=>Gt(K)).join(", ")]),l(ie,ke)};g($,ie=>{c.retired.length>0&&ie(f)})}var E=r($,2);{var z=ie=>{var ke=Ko();at(ke,21,()=>c.faults,Oe=>Oe.field,(Oe,de)=>{var K=Jo(),Ae=a(K),Ge=a(Ae),Fe=r(Ae,2);{var De=ve=>{var je=Ho(),$e=a(je);v(()=>i($e,e(de).field)),l(ve,je)};g(Fe,ve=>{Ht.showTechnicalNames&&ve(De)})}var Me=r(Fe),te=r(Me);{var Ne=ve=>{var je=xt();v($e=>i(je,`— valeurs acceptées : ${$e??""}`),[()=>e(de).allowed.join(", ")]),l(ve,je)};g(te,ve=>{e(de).allowed!==void 0&&e(de).allowed.length>0&&ve(Ne)})}v(ve=>{i(Ge,ve),i(Me,` ${e(de).message??""} `)},[()=>Gt(e(de).field)]),l(Oe,K)}),l(ie,ke)};g(E,ie=>{c.faults.length>0&&ie(z)})}var ye=r(E,2);{let ie=u(()=>c.dirty?"Enregistrer la configuration":"Aucune modification à enregistrer"),ke=u(()=>!c.dirty||s.busy);tt(ye,{kind:"write",get label(){return e(ie)},get disabled(){return e(ke)},onrun:()=>{J()}})}l(d,x)},Re=u(()=>$a(s.page)&&c.config!==null);g(O,d=>{e(Re)&&d(le)})}var We=r(ne,2);{var we=d=>{En(d,{get admin(){return s}})};g(We,d=>{s.pending!==null&&d(we)})}v(()=>{la(be,Ht.showTechnicalNames),i(Ce,e(w))}),Ze("change",be,()=>Ht.toggleTechnicalNames()),l(n,pe),$t()}At(["click","change"]);let da=null;function tu(n){if(da!==null||n.querySelector("[data-admin]")!==null)return;const t=document.createElement("div");n.appendChild(t),da={component:yr(Qo,{target:t,props:{onclose:Zo}}),host:t}}function Zo(){if(da===null)return;const{component:n,host:t}=da;da=null,xr(n),t.remove()}export{Zo as closeAdmin,tu as mountAdmin}; diff --git a/internal/web/dist/index.html b/internal/web/dist/index.html index 38fc3ed..6e970a3 100644 --- a/internal/web/dist/index.html +++ b/internal/web/dist/index.html @@ -6,10 +6,10 @@ Pesée - - + + - +
                                                                    diff --git a/internal/web/harness_test.go b/internal/web/harness_test.go index f6fc4f2..08d9678 100644 --- a/internal/web/harness_test.go +++ b/internal/web/harness_test.go @@ -12,6 +12,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "sync" "testing" "testing/fstest" @@ -115,9 +116,9 @@ func newBench(t *testing.T, tweak ...func(*benchOptions)) *bench { // composition root that does it (§11.4, serve.go). Without it here, the store the // routes read would keep the configuration nobody confirmed, and the screen would // show a port the station stopped using sixty seconds ago. - OnRevert: func(previous domain.Config) { + OnRevert: func(fileBefore domain.Config) { if o.configStore != nil { - _ = o.configStore.Save(context.Background(), previous) + _ = o.configStore.Save(context.Background(), fileBefore) } }, }) @@ -360,7 +361,9 @@ func (b *bench) setPassword(password, recovery string) { } cfg := b.hub.Config() cfg.Admin.PasswordHash, cfg.Admin.RecoveryCodeHash = hash, recoveryHash - if _, err := b.station.Reload(cfg); err != nil { + // No FileBefore: the admin block alone moves, so no countdown is armed and no rollback + // has a file to aim at. + if _, err := b.station.Reload(station.ReloadRequest{Next: cfg}); err != nil { b.t.Fatalf("Reload : %v", err) } } @@ -417,6 +420,18 @@ func (l *recordingLog) has(code string) bool { return false } +// saying returns the first line whose message carries that fragment. +func (l *recordingLog) saying(fragment string) (TechnicalLine, bool) { + l.mu.Lock() + defer l.mu.Unlock() + for _, line := range l.lines { + if strings.Contains(line.Message, fragment) { + return line, true + } + } + return TechnicalLine{}, false +} + // memoryStore is the persistence, in a map. It satisfies web.Store, station.Journal // and station.TechnicalSink at once, which is what the real database does too. type memoryStore struct { diff --git a/internal/web/health.go b/internal/web/health.go index 9b08cba..0bed007 100644 --- a/internal/web/health.go +++ b/internal/web/health.go @@ -370,9 +370,8 @@ func (s *Server) fillHealthFromStore(ctx context.Context, body *adminHealthDTO) if lines, err := s.store.TechnicalEntries(ctx, TechnicalQuery{Limit: 10}); err == nil { body.Events = technicalLinesOf(lines) } - if list, err := s.store.Imports(ctx, 1, 0); err == nil && len(list) > 0 { - last := importOf(list[0]) - body.Catalog = &last + if last := s.lastImport(ctx); last != nil { + body.Catalog = last if findings, err := s.store.Findings(ctx, last.ID); err == nil { body.CatalogMotives = motivesOf(findings) } @@ -382,6 +381,39 @@ func (s *Server) fillHealthFromStore(ctx context.Context, body *adminHealthDTO) } } +// lastImport is the import in force, or nil when there is none to read. +// +// Nil covers three states on purpose, because the screen owes the same prudence to all +// three: no journal at all (ADR-013), a journal that refused the read, and a station +// installed this morning that has never received a catalog. +func (s *Server) lastImport(ctx context.Context) *importDTO { + if s.store == nil { + return nil + } + list, err := s.store.Imports(ctx, 1, 0) + if err != nil || len(list) == 0 { + return nil + } + last := importOf(list[0]) + return &last +} + +// watchedCatalog is the permanent catalog line of §14.4, or an empty string. +// +// The wording comes from the SOURCE itself — « dépôt local, flv_2.csv dans … » — which is +// why nothing here composes it: only the source knows whether it has an account and which +// file name a station number derives. +func (s *Server) watchedCatalog(ctx context.Context) string { + if s.dashboard == nil { + return "" + } + source := s.dashboard.Dashboard(ctx).Source + if source == nil { + return "" + } + return source.Label +} + // notWeighableMotives are the three reasons a row has no tile, and the only findings this // breakdown counts (§10.3). // diff --git a/internal/web/rollback_test.go b/internal/web/rollback_test.go index eb747e4..89f22e3 100644 --- a/internal/web/rollback_test.go +++ b/internal/web/rollback_test.go @@ -148,6 +148,140 @@ func awaitFile(t *testing.T, saved *savedConfig, want string) { "remis en place, et le prochain démarrage repartirait sur la version non confirmée", want) } +// TestUnRetourArriereNEcritPasLeProfilDUsineParDessusLeFichierDuMagasin. +// +// The station this is about is the one §11.3 exists for: its file is faulty, so it RUNS the +// neutral profile while the file keeps the cooperative's tariffs, safeguards and categories. +// A volunteer repairs the file from the screen, the save moves the hardware blocks, and +// nobody presses « Confirmer » — the roll had run out, the browser was closed, anything. +// +// Sixty seconds later the countdown fires, and what it must put back is THE FILE AS IT WAS +// BEFORE THE SAVE. Putting back what the station is RUNNING writes the factory profile over +// the shop's own file: the cooperative disappears, the two tariff tiers become one and the +// member discount with them, the basket check goes off and the printer becomes `preview`. +// Measured on a real station, in that order, from a single gesture that was meant to repair it. +// +// It is the same reasoning as TestARescueDoesNotReplaceTheShopsConfigurationWithTheFactoryOne, +// which the recovery code already applies: the file and what is running are two documents. +func TestUnRetourArriereNEcritPasLeProfilDUsineParDessusLeFichierDuMagasin(t *testing.T) { + shop := loadConfig(t) + saved := &savedConfig{} + writeFileOf(t, saved, shop) + + b := adminBench(t, func(o *benchOptions) { + o.configStore = saved + // What a station in factory configuration RUNS (§11.3), which is not its file. + o.config = func(cfg *domain.Config) { *cfg = domain.NeutralProfile() } + }) + + repaired := reread(t, shop) + repaired.Scale.Options["port"] = json.RawMessage(`"COM9"`) + response := b.do(http.MethodPut, "/admin/api/config", marshal(t, repaired), nil) + armed := decodeStatus[configDTO](t, response, http.StatusOK) + if armed.Pending == nil { + t.Fatal("réparer un poste en configuration d'usine n'arme aucun compte à rebours : " + + "le banc ne construit pas la situation à l'étude") + } + if port, _ := saved.saved().Scale.Options.Text("port"); port != "COM9" { + t.Fatalf("le fichier porte le port %q juste après l'enregistrement, attendu COM9 : "+ + "l'écriture précède le compte à rebours (§11.4, étapes 3 puis 5)", port) + } + + // Nobody confirms. + b.advance(61 * time.Second) + written := awaitRewrittenFile(t, b, saved, "COM9") + + if got, want := len(written.Pricing.Tiers), len(shop.Pricing.Tiers); got != want { + t.Errorf("le fichier porte %d palier(s) de tarif au lieu de %d : la remise adhérent "+ + "a disparu du fichier du magasin", got, want) + } + if !written.Limits.BasketCheckEnabled { + t.Error("le contrôle du panier est désactivé dans le fichier : c'est le réglage " + + "d'usine, pas celui du magasin") + } + if written.Station.Coop != shop.Station.Coop { + t.Errorf("la coopérative du fichier est %q, attendu %q : le fichier du magasin a été "+ + "remplacé par le profil d'usine", written.Station.Coop, shop.Station.Coop) + } + if written.Printer.Type == domain.PrinterPreview { + t.Error("le fichier déclare le pilote d'aperçu : le poste n'imprimerait plus rien " + + "au prochain démarrage") + } + // And the station keeps running what it was running: nothing makes a poste ENTER the + // out-of-service state, so applying the shop's file to a live station would be worse + // than the defect this test closes. + if b.hub.Config().Station.Coop == shop.Station.Coop { + t.Error("le poste s'est mis à faire tourner le fichier au lieu de son profil neutre") + } +} + +// TestUnSecondEnregistrementPendantUneConfirmationEstRefuse. +// +// The file is written at step 3 and the countdown starts at step 5, so a second save inside +// the window writes the file again — and the document the rollback aims at becomes a version +// nobody confirmed either. The one somebody DID validate is then the version lost, which is +// the exact opposite of what the countdown is for. +// +// So it is refused, with the 409 a confirmation outside the window already answers. +func TestUnSecondEnregistrementPendantUneConfirmationEstRefuse(t *testing.T) { + saved := &savedConfig{} + b := adminBench(t, func(o *benchOptions) { o.configStore = saved }) + writeFileOf(t, saved, b.hub.Config()) + + first := reread(t, b.hub.Config()) + first.Scale.Options["port"] = json.RawMessage(`"COM9"`) + armed := decodeStatus[configDTO](t, + b.do(http.MethodPut, "/admin/api/config", marshal(t, first), nil), http.StatusOK) + if armed.Pending == nil { + t.Fatal("un changement de matériel n'arme aucun compte à rebours") + } + + second := reread(t, b.hub.Config()) + second.Scale.Options["port"] = json.RawMessage(`"COM7"`) + response := b.do(http.MethodPut, "/admin/api/config", marshal(t, second), nil) + refusal := body(t, response) + if response.StatusCode != http.StatusConflict { + t.Fatalf("second enregistrement dans la fenêtre = %d, attendu 409 : %s", + response.StatusCode, refusal) + } + if port, _ := saved.saved().Scale.Options.Text("port"); port != "COM9" { + t.Fatalf("le fichier porte %q : un enregistrement refusé a quand même écrit", port) + } + + // And the rollback still aims at the version that was in force before the FIRST save. + b.advance(61 * time.Second) + written := awaitRewrittenFile(t, b, saved, "COM9") + if port, _ := written.Scale.Options.Text("port"); port != "COM8" { + t.Fatalf("le retour arrière a remis le port %q, attendu COM8 : la cible du compte à "+ + "rebours a bougé", port) + } +} + +// awaitRewrittenFile waits for the rollback to have written the file a second time, and +// returns what it wrote. +// +// The station comes back first and the file a moment later, on the same goroutine, so a +// wait on the running configuration alone would read the store while the write is still in +// flight. The sentinel is the PORT the save wrote — and not the fingerprint of the +// document, which is computed on an export WITHOUT the hardware and is therefore blind to +// the one block this save moved. The clock keeps moving because the supervisor is the only +// goroutine that watches deadlines. +func awaitRewrittenFile(t *testing.T, b *bench, saved *savedConfig, savedPort string) domain.Config { + t.Helper() + deadline := time.Now().Add(hang) + for time.Now().Before(deadline) { + written := saved.saved() + if port, declared := written.Scale.Options.Text("port"); !declared || port != savedPort { + return written + } + b.clock.Advance(time.Second) + time.Sleep(time.Millisecond) + } + t.Fatal("le fichier porte encore la configuration non confirmée : le retour arrière ne " + + "l'a jamais réécrit, et le prochain démarrage repartirait dessus") + return domain.Config{} +} + // Compile-time proof that the station is what the routes drive here, and not a double: the // rollback under test is the real one of §11.4. var _ Controller = (*station.Station)(nil) diff --git a/internal/web/server.go b/internal/web/server.go index 2b48ff1..bbfa96d 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -63,9 +63,17 @@ type Controller interface { // Alive reports that the Hub loop is publishing. Alive() bool // Reload publishes a new configuration and restarts what actually changed. - Reload(next domain.Config) (station.ReloadOutcome, error) + // + // The request carries the FILE as it was before the change, and not only the new + // configuration, because a rollback has two documents to put back: the station goes + // back to what it was running, the file to what it carried. On the one station §11.3 + // serves — a faulty file, the neutral profile in memory — those are not the same + // document, and handing over only one wrote the factory profile onto the shop's file. + Reload(req station.ReloadRequest) (station.ReloadOutcome, error) // Confirm accepts the configuration in force and stops the 60 s countdown. Confirm() error + // PendingConfirmation reports the end of the countdown still running, or the zero time. + PendingConfirmation() time.Time } // Store is the persistence as the administration screens read it. @@ -117,8 +125,14 @@ type ConfigStore interface { // CatalogAdmin is the catalog source as the administration screen acts on it. type CatalogAdmin interface { - // Reload asks the source for a fresh batch now. - Reload(ctx context.Context) error + // Reload asks the source for a fresh batch now, and reports IN FRENCH what it saw of + // the file it watches. + // + // That sentence is the ONE fact the watch never produces: it polls, finds nothing and + // returns in silence, so « Recharger le catalogue » used to be followed by nothing at + // all. It is EMPTY when the source watches no file of this machine — a share is + // watched over the network — because an absence nobody checked must not be asserted. + Reload(ctx context.Context) (string, error) // Import takes a CSV dropped on the screen and writes it where the ordinary // watcher will find it — same parser, same qualification (A4, ADR-011). Import(ctx context.Context, name string, r io.Reader) (domain.Import, error) diff --git a/internal/web/session.go b/internal/web/session.go index 1c4a856..8f41e8e 100644 --- a/internal/web/session.go +++ b/internal/web/session.go @@ -17,6 +17,7 @@ import ( "golang.org/x/crypto/argon2" "openscale/internal/domain" + "openscale/internal/station" "openscale/internal/station/ports" ) @@ -501,9 +502,11 @@ func (s *Server) recoverSession(w http.ResponseWriter, r *http.Request) { } // And the station keeps running what it was running, with the new password in force: // a recovery is not a moment to hand a station a configuration nobody has validated. + // No FileBefore: this changes the admin block alone, so no hardware block moves, no + // countdown is armed, and there is no rollback for a file to be the target of. next := cfg next.Admin.PasswordHash = hash - if _, err := s.controller.Reload(next); err != nil { + if _, err := s.controller.Reload(station.ReloadRequest{Next: next}); err != nil { writeProblem(w, http.StatusInternalServerError, "", "Configuration non rechargée : "+err.Error()) return diff --git a/internal/web/troubleshooting.go b/internal/web/troubleshooting.go index 0d5a59f..61f8702 100644 --- a/internal/web/troubleshooting.go +++ b/internal/web/troubleshooting.go @@ -40,6 +40,27 @@ type actionDTO struct { Message string `json:"message"` } +// reloadDTO is the answer of « Recharger le catalogue », and it is its OWN type. +// +// NOT a widened actionDTO: that one serves seven handlers and its two fields are frozen +// by tests, so growing it would change seven answers for the sake of one. And this answer +// has more to say than « done », because it cannot say the one thing anybody wants: the +// import is asynchronous BY DESIGN — there is no second import path — so what comes back +// is not the outcome but the two things that let a screen wait for it honestly. +type reloadDTO struct { + Done bool `json:"done"` + // Message is what the station SAW, and never a promise about what it will do. + Message string `json:"message"` + // Watched is the permanent catalog line of §14.4 — « dépôt local, flv_2.csv dans … ». + // Empty when nothing published it, and the screen then asserts nothing about it. + Watched string `json:"watched"` + // LastImportID is the import in force AT THE INSTANT OF THE PRESS, and 0 when this + // station has no journal (ADR-013). It is what the screen compares the dashboard + // against: a different id, three seconds later, is the outcome arriving. + LastImportID int64 `json:"last_import_id"` + LastImportAt string `json:"last_import_at"` +} + // troubleshootingReprint is POST /admin/api/troubleshooting/reprint. // // It carries NO idempotency key, on purpose: a volunteer pressing the button twice @@ -57,15 +78,46 @@ func (s *Server) reloadCatalog(w http.ResponseWriter, r *http.Request) { unavailable(w, "aucune source de catalogue n'est configurée") return } - if err := s.catalog.Reload(r.Context()); err != nil { + seen, err := s.catalog.Reload(r.Context()) + if err != nil { writeProblem(w, http.StatusBadGateway, "ERR-CAT-03", "Le catalogue n'a pas pu être relu : "+err.Error()) return } + // The line names the screen this handler can VOUCH for. It used to say « depuis + // l'écran de dépannage » under a press that had come from the Catalogue page, because + // both doors land here — and a journal that names the wrong screen is worse than one + // that names none. s.technical.Technical(domain.LevelInfo, "catalog", "", - "Relecture du catalogue demandée depuis l'écran de dépannage.", "") - writeJSON(w, http.StatusAccepted, actionDTO{ - Done: true, Message: "Le catalogue va être relu."}) + "Relecture du catalogue demandée depuis l'écran d'administration.", "") + + watched := s.watchedCatalog(r.Context()) + body := reloadDTO{Done: true, Message: reloadMessage(seen, watched), Watched: watched} + if last := s.lastImport(r.Context()); last != nil { + body.LastImportID, body.LastImportAt = last.ID, last.OccurredAt + } + // A 202, and it stays one. Making the reload synchronous would rebuild the second + // import path the composition root forbids, with its own guards and its own bugs — + // and the stability rule requires waiting anyway: a file still being written must not + // be read because somebody pressed a button. + writeJSON(w, http.StatusAccepted, body) +} + +// reloadMessage states a FACT, in decreasing order of what the station could establish. +// +// « Le catalogue va être relu. » was a promise written before any support had been +// touched: it named no file, no directory and no instant, and on the case that matters — +// nothing where the station is looking — it was followed by a silence that never ended. +func reloadMessage(seen, watched string) string { + switch { + case seen != "": + return seen + case watched != "": + return "Le poste surveille " + watched + "." + } + // Neither the source nor the dashboard said anything. What is left is the only thing + // still true: the watch has been asked to look now. + return "La veille du catalogue a été priée de regarder tout de suite." } // manualEntry is POST /admin/api/troubleshooting/manual-entry. diff --git a/internal/web/wording_test.go b/internal/web/wording_test.go index 1ae9476..5b30e9a 100644 --- a/internal/web/wording_test.go +++ b/internal/web/wording_test.go @@ -241,6 +241,50 @@ func TestACallerThatGaveUpIsNotAnswered(t *testing.T) { } } +// TestTheReloadStatesAFactAndNotAPromise. +// +// « Le catalogue va être relu. » named nothing that could be checked, and the volunteer +// who pressed the button on a station where the file was not there read a promise that +// nothing ever kept. What replaces it says what is watched and where — the two things +// somebody can go and look at. +func TestTheReloadStatesAFactAndNotAPromise(t *testing.T) { + b := newBench(t, func(o *benchOptions) { + o.catalogAdmin = &fakeCatalogAdmin{} + o.dashboard = stubDashboard{DashboardFacts{Source: &CatalogSourceState{ + Type: domain.CatalogSourceLocalDrop, + Label: "dépôt local, flv_2.csv dans D:\\catalog\\incoming", + }}} + }) + + got := decodeStatus[reloadDTO](t, + b.post("/admin/api/troubleshooting/reload-catalog", `{}`), http.StatusAccepted) + if strings.Contains(got.Message, "va être relu") { + t.Fatalf("message = %q : c'est encore la promesse au futur", got.Message) + } + if !strings.Contains(got.Message, "flv_2.csv") || + !strings.Contains(got.Message, "D:\\catalog\\incoming") { + t.Fatalf("message = %q, attendu qu'il nomme ce qui est surveillé et où", got.Message) + } +} + +// TestTheReloadLineNamesTheScreenItCanVouchFor. +// +// One handler serves both doors — the troubleshooting page and the Catalogue page — so +// « depuis l'écran de dépannage » was written under a press that had come from somewhere +// else. A journal that names the wrong screen is worse than one that names none. +func TestTheReloadLineNamesTheScreenItCanVouchFor(t *testing.T) { + b := newBench(t, func(o *benchOptions) { o.catalogAdmin = &fakeCatalogAdmin{} }) + b.post("/admin/api/troubleshooting/reload-catalog", `{}`).Body.Close() + + line, ok := b.technical.saying("Relecture du catalogue") + if !ok { + t.Fatal("aucune ligne technique n'a été écrite pour la relecture") + } + if strings.Contains(line.Message, "de dépannage") { + t.Fatalf("ligne technique = %q : la même route sert la page Catalogue", line.Message) + } +} + // TestACatalogSourceThatRefusesIsReportedAndNotSwallowed. func TestACatalogSourceThatRefusesIsReportedAndNotSwallowed(t *testing.T) { catalog := &fakeCatalogAdmin{err: errors.New("répertoire en lecture seule")} diff --git a/testdata/config-demo.json b/testdata/config-demo.json index 977c64f..8030579 100644 --- a/testdata/config-demo.json +++ b/testdata/config-demo.json @@ -16,7 +16,8 @@ "sound": true, "idle_timeout_s": 45, "reprint_window_s": 60, - "show_grid_prices": true + "show_grid_prices": true, + "show_by_unit_products": true }, "scale": { "type": "gram-xfoc-plus", diff --git a/testdata/config-lacagette.json b/testdata/config-lacagette.json index 939f7c2..387c5e0 100644 --- a/testdata/config-lacagette.json +++ b/testdata/config-lacagette.json @@ -11,7 +11,8 @@ "sound": true, "idle_timeout_s": 45, "reprint_window_s": 60, - "show_grid_prices": true + "show_grid_prices": true, + "show_by_unit_products": false }, "scale": { diff --git a/web/src/App.svelte b/web/src/App.svelte index 2061d85..7bf7e4e 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -43,7 +43,7 @@ const settings = $derived(session.presentation) const healthy = $derived(hardwareIsHealthy(snapshot)) /** - * What the grid says when it has nothing to draw — and the three cases differ. + * What the grid says when it has nothing to draw — and the four cases differ. * * « Aucun produit ne correspond » is true of a filter that matched nothing and * FALSE of a station whose catalog has not arrived: there is nothing to @@ -51,6 +51,10 @@ * looking for a search box. §14.4 fixes the wording of that second case, and * the name of the awaited file is DERIVED from the station number — never * written in a message. + * + * The third case is the one this station creates for itself: the file arrived and + * everything it carried is hidden here. Announcing an awaited file would send a + * volunteer looking for one that has been on the disk since the morning. */ const empty = $derived.by(() => { if (session.catalogError !== '') { @@ -59,6 +63,16 @@ if (products.length > 0) { return { message: 'Aucun produit ne correspond.', hint: 'Effacez des lettres ou changez de rayon.' } } + if (catalog !== null && catalog.products.length > 0) { + const byUnitOnly = catalog.products.every((p) => p.mode === 'by_unit') + return { + message: + byUnitOnly && !settings.show_by_unit_products + ? 'Aucun produit à montrer : ce poste masque les produits vendus à l’unité, et le catalogue reçu ne contient que ceux-là.' + : 'Aucun produit à montrer : les réglages d’affichage de ce poste masquent tout ce qu’il a reçu.', + hint: 'Le fichier est bien arrivé : prévenez un responsable, c’est un réglage de ce poste qui vide la grille.', + } + } const station = snapshot?.station ?? 0 return { message: diff --git a/web/src/admin/lib/api.ts b/web/src/admin/lib/api.ts index 3410c32..3d4939b 100644 --- a/web/src/admin/lib/api.ts +++ b/web/src/admin/lib/api.ts @@ -10,6 +10,7 @@ import type { PortDTO, PrinterDeviceDTO, ProblemDTO, + ReloadDTO, SessionDTO, TechnicalLineDTO, UpdateDTO, @@ -59,9 +60,16 @@ export function reprintLast(): Promise { return postJSON('/admin/api/troubleshooting/reprint', {}) } -/** « Recharger le catalogue » : la veille refait le MÊME poll (§14.4). */ -export function reloadCatalog(): Promise { - return postJSON('/admin/api/troubleshooting/reload-catalog', {}) +/** + * « Recharger le catalogue » : la veille refait le MÊME poll (§14.4). + * + * La réponse est un 202 et elle ne porte PAS l'issue — la veille lit, qualifie et + * applique sur son propre fil. Elle porte ce que le poste a vu du fichier surveillé, et + * l'import en service à l'instant de l'appui, qui est ce qui permet de reconnaître le + * suivant. + */ +export function reloadCatalog(): Promise { + return postJSON('/admin/api/troubleshooting/reload-catalog', {}) } /** « Basculer en saisie manuelle ». Un état du poste, jamais une configuration écrite. */ @@ -263,9 +271,9 @@ export function fetchImports( return getJSON<{ imports: ImportDTO[]; findings: FindingDTO[] }>('/admin/api/imports' + query) } -/** « Recharger le catalogue », porte expert de la même action. */ -export function reloadCatalogAsExpert(): Promise { - return postJSON('/admin/api/catalog/reload', {}) +/** « Recharger le catalogue », porte expert de la même action et de la même réponse. */ +export function reloadCatalogAsExpert(): Promise { + return postJSON('/admin/api/catalog/reload', {}) } /** « Oublier la quarantaine » : le prochain fichier sera relu (§10.5). */ diff --git a/web/src/admin/lib/dto.ts b/web/src/admin/lib/dto.ts index 87b6b98..d0445ac 100644 --- a/web/src/admin/lib/dto.ts +++ b/web/src/admin/lib/dto.ts @@ -19,6 +19,31 @@ export interface ActionDTO { message: string } +/** + * Ce que « Recharger le catalogue » répond, sur les deux routes qui y mènent. + * + * Un type à lui, et non un `ActionDTO` élargi : celui-là sert sept routes et son format + * est figé. Et cette réponse-ci ne peut pas porter ce que tout le monde veut — l'import + * est asynchrone par conception, la veille fait le travail sur son propre fil, et la + * réponse est un 202. Ce qu'elle porte est donc de quoi RECONNAÎTRE l'issue quand elle + * arrivera, par le sondage de trois secondes. + */ +export interface ReloadDTO { + done: boolean + /** Ce que le poste a VU, jamais une promesse de ce qu'il va faire. */ + message: string + /** La ligne permanente du catalogue. Vide quand ce poste ne la publie pas. */ + watched: string + /** + * L'import en service à l'instant de l'appui, et 0 quand ce poste n'a pas de journal. + * + * C'est ce que l'écran compare au tableau de bord : un identifiant différent, trois + * secondes plus tard, est l'issue qui arrive. + */ + last_import_id: number + last_import_at: string +} + /** Un refus de la couche HTTP. `code` est vide quand aucun ERR-xxx-nn n'est alloué. */ export interface ProblemDTO { code: string diff --git a/web/src/admin/lib/fields.ts b/web/src/admin/lib/fields.ts index c5f2b50..fce6f75 100644 --- a/web/src/admin/lib/fields.ts +++ b/web/src/admin/lib/fields.ts @@ -27,6 +27,7 @@ export const FIELD_LABELS: Readonly> = { 'ui.idle_timeout_s': 'Retour à l’accueil après (secondes)', 'ui.reprint_window_s': 'Réimpression possible pendant (secondes)', 'ui.show_grid_prices': 'Afficher les prix sur les tuiles', + 'ui.show_by_unit_products': 'Afficher les produits vendus à l’unité', 'scale.type': 'Protocole de la balance', 'scale.present': 'Ce poste a une balance', diff --git a/web/src/admin/lib/inventory.ts b/web/src/admin/lib/inventory.ts index 1443fe6..9690ec8 100644 --- a/web/src/admin/lib/inventory.ts +++ b/web/src/admin/lib/inventory.ts @@ -1,5 +1,5 @@ import type { ImportDTO, MotiveDTO } from './dto' -import { frenchDate } from './format' +import { frenchDate, frenchDateTime } from './format' /** * L'inventaire du catalogue, écrit MOT POUR MOT comme §14.4 le donne. @@ -121,6 +121,114 @@ function oneLineOf(record: ImportDTO): string { return parts.join(' · ') } +/** + * Comment un import s'est terminé, en français. + * + * Les quatre jetons du noyau, et le tableau vit ici parce que trois écrans les lisent : le + * tableau de bord, l'historique de la page Catalogue et la phrase que « Recharger le + * catalogue » laisse derrière lui. Trois copies auraient fini par diverger, et un bénévole + * aurait lu deux mots pour un même sort selon l'écran d'où il regarde. + * + * « identique au précédent » et jamais « inchangé, déjà appliqué » : un fichier identique + * à l'octet est un cas NOMINAL — le producteur peut déposer le même export chaque nuit — + * mais celui qui vient d'appuyer sur « Recharger » doit comprendre qu'aucun nouveau + * catalogue n'est en service. + */ +const IMPORT_RESULTS: Record = { + applied: 'appliqué', + unchanged: 'identique au précédent', + rejected: 'refusé', + failed: 'échec', +} + +/** D'où un catalogue est arrivé, en français. Les trois jetons du noyau. */ +const IMPORT_SOURCES: Record = { + local_drop: 'dépôt local', + webdav: 'WebDAV', + manual: 'déposé sur l’écran', +} + +/** + * Le mot français d'un résultat d'import. + * + * @param result - le jeton que le service a écrit. + */ +export function importResultWord(result: string): string { + return IMPORT_RESULTS[result] ?? 'résultat inconnu' +} + +/** + * Le mot français d'une source de catalogue. + * + * @param source - le jeton que le service a écrit. + */ +export function importSourceWord(source: string): string { + return IMPORT_SOURCES[source] ?? 'source inconnue' +} + +/** + * L'issue d'un import, en UNE phrase : « flv_1.csv appliqué le 24/07/2026 à 14:00 via + * dépôt local — 153 reçus · 107 pesables · 39 non pesables · 7 anomalies. » + * + * Elle est ici, et non dans une page, parce que les deux portes de « Recharger le + * catalogue » — le gros bouton du Dépannage et la porte experte de la page Catalogue — + * doivent en rendre exactement la même : un acte ne peut pas s'annoncer différemment selon + * l'écran d'où on l'atteint. + * + * @param record - l'import, tel que le tableau de bord le donne. + * @param motives - la répartition des non-pesables par motif, ou une liste vide. + */ +export function importOutcomeSentence(record: ImportDTO, motives: MotiveDTO[]): string { + const said = [ + record.file_name === '' ? 'Le dernier fichier' : record.file_name, + importResultWord(record.result), + ] + const when = frenchDateTime(record.occurred_at) + if (when !== '') said.push('le ' + when) + said.push('via ' + importSourceWord(record.source)) + + // Le motif du service passe TEL QUEL : c'est lui qui sait pourquoi il a écarté un + // fichier, et un écran qui le résumerait enlèverait la seule chose qui dise quoi faire. + const why = record.reason === '' ? '' : ' ' + record.reason + return `${said.join(' ')} — ${inventoryOf(record, motives).oneLine}.${why}` +} + +/** + * Ce que l'écran dit quand l'attente d'une issue a atteint son plafond. + * + * Elle ne conclut RIEN : la veille qui ne trouve rien revient sans écrire une ligne, donc + * « le fichier n'était pas là » est une hypothèse et pas un constat. Ce qui est affirmé + * tient en deux faits — aucun import enregistré, et voici ce qui est surveillé. + * + * @param watched - ce que le poste surveille, ou une chaîne vide. + */ +export function noImportSentence(watched: string): string { + if (watched === '') { + return 'Aucun nouvel import enregistré à cet instant, et ce poste ne publie pas ce qu’il surveille.' + } + return ( + 'Aucun nouvel import enregistré à cet instant. Le poste surveille ' + + watched + + ' : le fichier n’y était pas, ou il n’a pas encore fini d’arriver.' + ) +} + +/** + * Ce que l'écran dit à un poste dont le journal est indisponible. + * + * Il n'écrira jamais la ligne d'import que l'attente guette : lui promettre une issue, + * c'est l'attendre pour rien puis accuser un fichier qui est peut-être en service. + * + * @param watched - ce que le poste surveille, ou une chaîne vide. + */ +export function noJournalSentence(watched: string): string { + const where = watched === '' ? '' : ' Le poste surveille ' + watched + '.' + return ( + 'Ce poste n’a pas de journal : il ne pourra rien dire de l’issue de cette relecture.' + + where + ) +} + /** Le libellé du lien vers les lignes concernées, au singulier comme au pluriel. */ function rowLink(count: number): string { return count === 1 ? 'voir la ligne' : `voir les ${integer(count)} lignes` diff --git a/web/src/admin/lib/reload.svelte.ts b/web/src/admin/lib/reload.svelte.ts new file mode 100644 index 0000000..a623d58 --- /dev/null +++ b/web/src/admin/lib/reload.svelte.ts @@ -0,0 +1,86 @@ +import type { HealthDTO, ReloadDTO } from './dto' +import { importOutcomeSentence, noImportSentence, noJournalSentence } from './inventory' + +/** + * L'attente de l'issue d'un « Recharger le catalogue ». + * + * L'import est asynchrone par conception : la route répond un 202 et c'est la veille, sur + * son propre fil, qui lit le fichier, le qualifie, l'applique et l'acquitte. La réponse + * HTTP ne peut donc pas porter l'issue, et rendre la route synchrone reconstruirait le + * second chemin d'import que le poste s'interdit. + * + * Le seul canal dont l'administration dispose est le sondage du tableau de bord, toutes + * les trois secondes, et l'identifiant de l'import en service est ce qui dit que l'attente + * est finie. Ce mécanisme existait déjà sur la page Catalogue ; il vit ici pour que les + * DEUX portes de cet acte en rendent exactement la même phrase. + */ + +/** Ce que le service écrit dans `journal_rows_count` quand ce poste n'a pas de journal. */ +const NO_JOURNAL = -1 + +/** + * Combien de sondages l'écran attend avant de dire ce qu'il SAIT. + * + * Dix tours de trois secondes font trente secondes, plus du double du pire cas de la + * veille elle-même : cinq secondes de balayage, deux observations identiques avant de + * lire, puis la lecture. Un plafond plus court dirait « rien ne s'est passé » pendant que + * l'import tourne, ce qui est exactement le mensonge que ce travail supprime. + */ +const WAITED_POLLS = 10 + +/** L'attente d'une relecture, du moment de l'appui à la phrase qui la conclut. */ +export class ReloadWatch { + /** La phrase à l'écran, vide tant qu'il n'y a rien à dire. */ + sentence = $state('') + + /** L'import en service au moment de l'appui : l'écran attend qu'il change. */ + #baseline = 0 + /** Ce que le poste surveille, tel que la réponse l'a nommé. */ + #watched = '' + /** Combien de sondages il reste à attendre. Zéro veut dire « on n'attend plus ». */ + #polls = 0 + + /** + * Arme l'attente sur ce que la route vient de répondre. + * + * @param answer - la réponse de « Recharger le catalogue ». + */ + begin(answer: ReloadDTO): void { + this.sentence = '' + this.#baseline = answer.last_import_id + this.#watched = answer.watched + this.#polls = WAITED_POLLS + } + + /** Oublie l'attente : un autre acte a pris l'écran, et sa phrase avec. */ + forget(): void { + this.sentence = '' + this.#polls = 0 + } + + /** + * Confronte l'attente au tableau de bord que le sondage vient de relire. + * + * @param health - le tableau de bord, tel qu'il vient d'être lu. + */ + observe(health: HealthDTO): void { + if (this.#polls === 0) return + if (health.counters.journal_rows_count === NO_JOURNAL) { + this.#settle(noJournalSentence(this.#watched)) + return + } + const record = health.catalog + if (record !== null && record.id !== this.#baseline) { + this.#settle(importOutcomeSentence(record, health.catalog_motives)) + return + } + this.#polls -= 1 + if (this.#polls === 0) this.sentence = noImportSentence(this.#watched) + } + + /** Pose la phrase qui conclut, et cesse d'attendre. */ + #settle(sentence: string): void { + this.sentence = sentence + this.#polls = 0 + } +} diff --git a/web/src/admin/lib/session.svelte.ts b/web/src/admin/lib/session.svelte.ts index d43af61..e4078ea 100644 --- a/web/src/admin/lib/session.svelte.ts +++ b/web/src/admin/lib/session.svelte.ts @@ -168,12 +168,18 @@ export class Admin { * bord est relu derrière, parce que la moitié de l'intérêt d'un bouton de dépannage est * de voir le feu changer de couleur. * + * Elle REND ce que le poste a répondu, parce qu'une des neuf actions a plus à dire que + * sa phrase : « Recharger le catalogue » rapporte l'import en service à l'instant de + * l'appui, sans quoi l'écran n'a rien à quoi reconnaître celui qui va suivre. + * * @param action - l'appel à passer. + * @returns ce que l'action a répondu, ou null si elle a échoué. */ - async run(action: () => Promise<{ message: string }>): Promise { + async run(action: () => Promise): Promise { this.#beginAction() + let done: T | null = null try { - const done = await action() + done = await action() this.notice = done.message } catch (failure) { this.#failAction(failure) @@ -181,6 +187,7 @@ export class Admin { this.busy = false } await this.refresh() + return done } /** diff --git a/web/src/admin/mount.ts b/web/src/admin/mount.ts index 24d6415..7a714ed 100644 --- a/web/src/admin/mount.ts +++ b/web/src/admin/mount.ts @@ -2,24 +2,37 @@ import { mount, unmount } from 'svelte' import App from './App.svelte' /** - * Le bundle d'administration, chargé PARESSEUSEMENT. + * The administration bundle, loaded LAZILY. * - * Chargé sur un appui de trois secondes dans le coin bas-droit neutre, DANS LA MÊME - * FENÊTRE : une fois chargé il partage le contexte JS de l'écran client — même `window`, - * même `window.onerror`, même tas. Ce que la séparation garantit réellement, et que la CI - * mesure, c'est (a) le poids — le budget « < 110 ko gzip » de l'écran client ne contient - * aucun octet d'administration — et (b) le chargement : sur un poste qui ne l'ouvre jamais, - * ce qui est le cas nominal toute la journée, ce fichier n'est ni téléchargé, ni analysé, - * ni exécuté (§14.1). + * The way in is the settings key at the right of the permanent bottom bar, which ADR-032 + * makes the ONE entry from the station itself: no URL to type, no way out of the kiosk. + * `App.svelte` imports this module on that press and mounts it IN THE SAME WINDOW, so + * from that point on it shares the JS context of the client screen — same `window`, same + * error net installed by `main.ts`, same heap. What the split into two entries really + * guarantees, and what CI measures, is (a) weight — the « < 110 ko gzip » budget of the + * client screen carries no administration byte — and (b) loading: on a station that never + * opens it, which is the nominal case all day long, this file is neither downloaded, nor + * parsed, nor executed (§14.1). + * + * That key is the only thing painted ABOVE the out-of-service overlay (`StatusBar.svelte`), + * and that is what keeps this module reachable at all: a station comes out of the installer + * in `OutOfService`, where the overlay would otherwise bury the one entry on the very + * station that has to be configured. + * + * `web/src/admin.ts` mounts the same screen as a document of its own, served on `/admin`. + * That path is out of reach AT THE STATION — the browser is started with `--kiosk `, + * so there is no address bar in front of the volunteer — and exists for repair from + * another machine on the network. */ -/** Ce qui est monté en ce moment : un deuxième appui long ne monte pas un doublon. */ +/** What is mounted right now: a second press of the settings key does not mount a duplicate. */ let opened: { component: unknown; host: HTMLElement } | null = null /** - * Monte l'écran d'administration dans un élément hôte. + * Mounts the administration screen under a host element. * - * @param host - où l'accrocher, normalement `document.body`. + * @param host - what to hang it under: `document.body` from the client screen, + * the `#app` of `admin.html` when the administration is opened as its own page. */ export function mountAdmin(host: HTMLElement): void { if (opened !== null || host.querySelector('[data-admin]') !== null) return @@ -30,10 +43,11 @@ export function mountAdmin(host: HTMLElement): void { } /** - * Retire l'écran d'administration et rend la grille. + * Removes the administration screen and gives the grid back. * - * C'est la seule sortie sur un poste en kiosque : il n'y a derrière cet écran ni bureau, ni - * barre des tâches, ni `Alt+F4` (§15.2). + * It is the only exit the screen offers, and the station offers no other in front of the + * volunteer: the browser is started with `--kiosk`, so there is neither browser chrome nor + * an address bar to leave by (§15.2). */ export function closeAdmin(): void { if (opened === null) return diff --git a/web/src/admin/pages/Catalog.svelte b/web/src/admin/pages/Catalog.svelte index 5485946..1d76d3b 100644 --- a/web/src/admin/pages/Catalog.svelte +++ b/web/src/admin/pages/Catalog.svelte @@ -7,9 +7,12 @@ import Panel from '../components/Panel.svelte' import * as api from '../lib/api' import type { Draft } from '../lib/draft.svelte' - import type { DecisionDTO, FindingDTO, HealthDTO, ImportDTO } from '../lib/dto' + import type { DecisionDTO, FindingDTO, HealthDTO, ImportDTO, ReloadDTO } from '../lib/dto' import { labelOf } from '../lib/fields' import { frenchDate, frenchDateTime, frenchInteger } from '../lib/format' + import { importResultWord, importSourceWord } from '../lib/inventory' + import { preferences } from '../lib/preferences.svelte' + import { ReloadWatch } from '../lib/reload.svelte' import type { Admin } from '../lib/session.svelte' /** @@ -44,7 +47,16 @@ */ interface Props { admin: Admin - /** The configuration document: this page edits the `catalog` block. */ + /** + * The configuration document: this page edits the `catalog` block, and the one + * `ui` key that decides WHAT THE GRID SHOWS. + * + * That key sits in `ui` and not in `catalog` on purpose: a change to the catalog + * block runs the disk probe and restarts the catalog source, which is a heavy price + * for a display choice — and it would fail outright while a WebDAV share is + * momentarily unreachable. The draft is shared by every page and the save is global, + * so hosting the switch here couples no blocks together. + */ draft: Draft health: HealthDTO } @@ -67,29 +79,6 @@ 'INTERNAL_CODE_NOT_WEIGHABLE', ] - /** - * How an import ended, in French. - * - * `applied`, `unchanged`, `rejected` and `failed` are the four tokens of - * `internal/domain/journal.go`, and the history table used to print them as they came: - * a volunteer read « unchanged » in a column headed « Résultat » and had no way to - * know whether that was good news. `unchanged` is a NOMINAL outcome — a producer may - * drop a byte-identical export every night — and it has to read like one. - */ - const IMPORT_RESULTS: Record = { - applied: 'appliqué', - unchanged: 'identique au précédent', - rejected: 'refusé', - failed: 'échec', - } - - /** Where a catalog came from, in French. The three tokens of §10.1. */ - const IMPORT_SOURCES: Record = { - local_drop: 'dépôt local', - webdav: 'WebDAV', - manual: 'déposé sur l’écran', - } - /** * The settings each source owns OUTRIGHT. * @@ -153,6 +142,43 @@ new Map(products.map((product): [string, string] => [product.id, product.name])), ) + /** + * How many products of the catalog IN SERVICE are sold by unit. + * + * Derived, never written down. Without this figure, the gap between the « 331 + * pesables » of the last import above and the « 316 produits pesables » the client + * screen states at the bottom of its grid is explainable by nobody over the phone — + * and a hard-coded 15 would pass a rendering test while lying at the next import. + * + * The client route serves EVERY weighable tile whatever this station shows, so the + * count is right in both positions of the switch. + */ + const byUnitCount = $derived(products.filter((product) => product.mode === 'by_unit').length) + + /** + * What this station does with them, in one French sentence. + * + * It follows the SWITCH and not the saved file, so that the trade-off is legible + * before the save rather than after it. And it states nothing this page has not read: + * a station whose client catalog could not be opened does not know this number. + */ + const byUnitSentence = $derived.by(() => { + if (namesState === 'loading') return 'Lecture du catalogue en service…' + if (namesState === 'unread') { + return 'Le catalogue en service n’a pas pu être lu : cet écran ne sait pas combien de ' + + 'produits se vendent à l’unité.' + } + if (byUnitCount === 0) { + return 'Aucun produit vendu à l’unité dans le catalogue en service.' + } + const many = byUnitCount > 1 + const subject = many ? 'produits vendus à l’unité sont' : 'produit vendu à l’unité est' + const said = draft.flag('ui.show_by_unit_products') + ? `${many ? 'montrés' : 'montré'} dans la grille de ce poste` + : `${many ? 'masqués' : 'masqué'} sur ce poste` + return `${frenchInteger(byUnitCount)} ${subject} ${said}.` + }) + /** * The products the search retains, and how many there are BEFORE the cap. * @@ -301,6 +327,20 @@ void load() }) + /** + * What « Recharger le catalogue » left pending, and the sentence that will conclude it. + * + * The very mechanism above, put to a second use: the reload answers a 202 that carries + * no outcome, so the id of the import in force is again what says the wait is over. The + * wait itself lives in a shared module because the SAME act is reachable from the + * troubleshooting page, and it may not read differently there. + */ + const reloadWatch = new ReloadWatch() + + $effect(() => { + reloadWatch.observe(health) + }) + /** Reads the import history, the findings of the last one, and the catalog in service. */ async function load(): Promise { const history = await admin.load(() => api.fetchImports(health.catalog?.id)) @@ -336,27 +376,42 @@ * @param action - the protected call, which may be passed TWICE. * @returns true when the station accepted it. */ - async function guarded( + async function guarded( label: string, - action: () => Promise<{ message: string }>, - ): Promise { + action: () => Promise, + ): Promise { working = label // What an act leaves on screen lives until ANOTHER act replaces it (§5.1), and // starting this one is that replacement: `protect` does not clear it by itself, so a // refusal from a minute ago would sit above the sentence this act is about to write. admin.actionError = '' admin.notice = '' + reloadWatch.forget() try { const done = await admin.protect(action) - if (done === null) return false + if (done === null) return null admin.notice = done.message await admin.refresh() - return true + return done } finally { working = '' } } + /** + * Reloads the catalog through the EXPERT door, and waits for the same outcome the + * troubleshooting page waits for. + * + * The answer is a 202 and carries no outcome — the watch reads, qualifies and applies on + * its own thread — so what the screen shows a few seconds later comes from the polling, + * word for word the sentence the other door writes. An act cannot announce itself + * differently depending on the screen it is reached from. + */ + async function reloadCatalog(): Promise { + const answer: ReloadDTO | null = await guarded('reload', api.reloadCatalogAsExpert) + if (answer !== null) reloadWatch.begin(answer) + } + /** * Chooses the catalog source, and wipes the settings of the other one. * @@ -421,7 +476,7 @@ const written = await guarded('offered', () => api.saveDecision(id, { offered, min_weight_g: grams, reason: text }), ) - if (written) reason = '' + if (written !== null) reason = '' } /** @@ -443,7 +498,7 @@ const written = await guarded(grams === null ? 'waiver-off' : 'waiver', () => api.saveDecision(id, { offered, min_weight_g: grams, reason: text }), ) - if (written) reason = '' + if (written !== null) reason = '' } /** @@ -576,13 +631,13 @@ /** How an import ended, in French — never the token the service wrote. */ function frenchResult(record: ImportDTO): string { - const said = IMPORT_RESULTS[record.result] ?? 'résultat inconnu' + const said = importResultWord(record.result) return record.code === '' ? said : `${said} (${record.code})` } /** Where an import came from, in French — never the token the service wrote. */ function frenchSource(record: ImportDTO): string { - return IMPORT_SOURCES[record.source] ?? 'source inconnue' + return importSourceWord(record.source) } @@ -611,6 +666,29 @@ {/snippet} + +{#snippet toggle(field: { path: string; label: string; hint: string })} + +{/snippet} +
                                                                    + + {@render toggle({ + path: 'ui.show_by_unit_products', + label: 'Afficher les produits vendus à l’unité', + hint: + 'Décoché, leurs tuiles quittent la grille et la recherche ne les retrouve plus. ' + + 'Ce que le poste perd : une tuile vendue à l’unité imprime une étiquette sans ' + + 'jamais lire la balance, et c’est le seul geste que ce réglage retire.', + })} +

                                                                    {byUnitSentence}

                                                                    +

                                                                    + Un produit masqué reste vendable : la caisse lit toujours son code-barres, et une + étiquette déjà imprimée reste valable. Ce réglage ne fait que retirer sa tuile. +

                                                                    +
                                                                    + Dernier essai : {frenchDateTime(health.catalog.occurred_at)} — - {resultWord(health.catalog.result)}{health.catalog.file_name === '' + {importResultWord(health.catalog.result)}{health.catalog.file_name === '' ? '' : ' (' + health.catalog.file_name + ')'}{health.catalog.reason === '' ? '' @@ -245,13 +246,6 @@