diff --git a/.gitignore b/.gitignore
index 5bd2d8af3276d30225062d5cddfe237c0578176d..d484f6a04277b6d27baea18c96717919acad33b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
 /src/assets/docs
 /release
 /build
+/docs/pdf_build
 
 # dependencies
 /node_modules
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ed146e000f40c2e3b3874a3c1241896787708e44..92811cedb1fad95a05865e51acd895e050f929ce 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -79,19 +79,34 @@ test:
   script:
     - npm run e2e
 
-build:
+.build:
   stage: build
-  only:
-    - pushes
-    - tags
-    - schedules
-    - web
   artifacts:
     expire_in: 10 min
     paths:
       - dist/
   script:
-    - npm run build
+    # -baseref option is used by npm to set the npm_config_basehref environment variable
+    # used in package.json
+    - npm run build-href -basehref=$BASEHREF
+
+build-dev:
+  extends: .build
+  only:
+    - tags
+    - pushes
+    - schedules
+    - web
+  variables:
+    BASEHREF: "/cassiopee/$CI_COMMIT_REF_NAME/"
+
+build-prod:
+  extends: .build
+  only:
+    - tags
+    - devel
+  variables:
+    BASEHREF: "/"
 
 clean-stale-branches:
   stage: clean-stale-branches
@@ -109,7 +124,7 @@ deploy-dev:
     - tags
     - web
   dependencies:
-    - build
+    - build-dev
   script:
     # Copie de la branche / du tag
     - ./scripts/deploy-version.sh $CI_COMMIT_REF_NAME $DEV_LOGIN $DEV_HOST $DEV_PATH
@@ -120,7 +135,7 @@ deploy-ovh-dev:
   only:
     - devel
   dependencies:
-    - build
+    - build-prod
   script:
     # Copie de la branche / du tag
     - ./scripts/deploy-version.sh prod-devel $PROD_LOGIN $PROD_HOST $PROD_DEV_PATH
@@ -131,7 +146,7 @@ deploy-prod:
     variables:
       - $CI_COMMIT_REF_NAME == "stable"
   dependencies:
-    - build
+    - build-prod
   script:
     - ./scripts/deploy-version.sh prod $PROD_LOGIN $PROD_HOST $PROD_PATH
 
diff --git a/README.md b/README.md
index 5750c79b6ba0413eaaf5bd25fba416d4790817f8..0fc8a9725a79ebaa03c2684ef07a93917538361a 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Requirements for developping Cassiopee can be achieved by manually install the r
  * npm
  * python3
  * pandoc ^2 (optional, for PDF documentation only)
- * texlive (optional, for PDF documentation only)
+ * texlive, texlive-bibtex-extra, texlive-latex-extra, latexmk (optional, for PDF documentation only)
 
 Building the HTML documentation requires MkDocs and some extensions:
 
diff --git a/package.json b/package.json
index 47e5d03490ed4a741f99a9d11dc7693f057d9ff7..b7cf7d4e8781930a18da8e1cc310cc48ecc3db55 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,8 @@
     "preprocess": "mkdir -p build; node scripts/preprocessors.js; npm run service-worker-version; bash scripts/fix-chartjs-plugin-zoom-2.0.0.sh",
     "start": "npm run preprocess && npm run mkdocs && npm run ng serve -- --host 0.0.0.0 --poll 5000",
     "build-no-pdf": "npm run preprocess && npm run mkdocs && npm run ng build -- --configuration production",
-    "build": "npm run preprocess && npm run mkdocs && npm run ng build -- --configuration production && npm run mkdocs2pdf",
+    "build": "npm run build-href -basehref=/",
+    "build-href": "npm run preprocess && npm run mkdocs && npm run ng build -- --configuration production --base-href=$npm_config_basehref && npm run mkdocs2pdf",
     "update-dist-index-mimetypes": "node scripts/update-dist-index-mimetypes.js",
     "electron": "npm run update-dist-index-mimetypes && \"node_modules/.bin/electron\" .",
     "release-linux-nocompile": "npm run update-dist-index-mimetypes && \"node_modules/.bin/electron-builder\"",
diff --git a/scripts/deploy-version.sh b/scripts/deploy-version.sh
index 4221e281bf9a84801c54b96417c6fb24955543e7..4d5bd0687b2b11ebc1bce1b92ee80aa979e0f399 100755
--- a/scripts/deploy-version.sh
+++ b/scripts/deploy-version.sh
@@ -35,10 +35,6 @@ echo "$(basename $0): deploying version $VERSION in $LOGIN@$HOST:$DIR"
 if [[ $VERSION == "prod" || $VERSION == "prod-devel" ]]; then
   display_local_href
 
-  # Modification du dossier base href -> /
-  echo "updating index.html base href to /"
-  sed -i '/<base/s/href="[^"]*"/href="\/"/' $LOCAL_DIR/index.html
-
   # Copie de la branche production
   rsync -a --delete --exclude=cassiopee-releases -e "ssh -o StrictHostKeyChecking=no" $LOCAL_DIR/ ${LOGIN}@${HOST}:${DIR}/
 
@@ -46,10 +42,6 @@ if [[ $VERSION == "prod" || $VERSION == "prod-devel" ]]; then
 else
   display_local_href
 
-  # Modification du dossier base href -> /cassiopee/version/
-  echo "updating index.html base href to /cassiopee/$VERSION/"
-  sed -i "/<base/s/href=\"[^\"]*\"/href=\"\/cassiopee\/$VERSION\/\"/" $LOCAL_DIR/index.html
-
   # Copie de la branche / du tag
   rsync -a --delete --exclude=cassiopee-releases -e "ssh -o StrictHostKeyChecking=no" $LOCAL_DIR/ "$LOGIN@$HOST:$DIR/$VERSION"
 
diff --git a/scripts/mkdocs2pdf.py b/scripts/mkdocs2pdf.py
index 6e3cf68040aa15e43debd48019a955e3e13d9b2a..37deb6a8f52ef1d99b61ccf8f1b618790e3fa4a2 100644
--- a/scripts/mkdocs2pdf.py
+++ b/scripts/mkdocs2pdf.py
@@ -20,6 +20,9 @@ import yaml
 import re
 import shutil
 
+# verbose output
+verbose = False
+
 baseDir = os.getcwd()
 buildDir = os.path.join(baseDir, 'build')
 latexSourceDir = os.path.join(baseDir, 'docs/latex')
@@ -38,6 +41,21 @@ def runCommand(cmd):
     if os.waitstatus_to_exitcode(os.system(cmd)) != 0:
         raise RuntimeError("error executing:",cmd)
 
+# Create a symbolic link
+def createLink(src):
+    # check if destination already exists
+    dest = os.path.basename(src)
+    if os.path.exists(dest):
+        if not os.path.islink(dest):
+            raise Exception('{} exists but is not a symbolic link'.format(dest))
+    else:
+        runCommand('ln -s {}'.format(src))
+
+def createEmptyDir(path):
+    if os.path.exists(path):
+        shutil.rmtree(path)
+    os.makedirs(path)
+
 # Reads an MkDocs configuration file
 def readConfig(sYAML):
     f = open(sYAML, 'r')
@@ -130,9 +148,14 @@ def convertMdToTex(filePath):
 def getLatexModel():
     # Clone Git repository
     os.chdir(pdfBuildDir)
-    runCommand(
-        'git clone {} {}'.format(latexModelRepository, latexModelDir)
-    )
+    if os.path.isdir(latexModelDir):
+        # git directory exists, update it
+        os.chdir(latexModelDir)
+        runCommand('git pull')
+        # platform independent "cd .."
+        os.chdir(os.path.dirname(os.getcwd()))
+    else:
+        runCommand('git clone {} {}'.format(latexModelRepository, latexModelDir))
     # back to original working drectory
     os.chdir(baseDir)
 
@@ -146,35 +169,17 @@ def injectContentIntoModel(mergedDocFilenameTex, lang):
     # Symlink necessary resources
     os.chdir(modelDir)
     relPathToMergedTexDoc = os.path.join('..', mergedDocFilenameTex)
-    runCommand(
-        'ln -s {} .'.format(relPathToMergedTexDoc)
-    )
+    createLink(relPathToMergedTexDoc)
     latexTemplate = filenamePrefix + lang + '.tex'
     relPathToLatexTemplate = os.path.join(latexSourceDir, latexTemplate)
-    runCommand(
-        'ln -s {}'.format(relPathToLatexTemplate)
-    )
-    runCommand(
-        'ln -s {}'.format(os.path.join(latexSourceDir, 'logo_pole.png'))
-    )
-    runCommand(
-        'ln -s {}/schema_rugosite_fond.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam'))
-    )
-    runCommand(
-        'ln -s {}/bloc_cylindre.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam'))
-    )
-    runCommand(
-        'ln -s {}/bloc_face_arrondie.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam'))
-    )
-    runCommand(
-        'ln -s {}/bloc_base_carree.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam'))
-    )
-    runCommand(
-        'rm rapport_inrae/logos.tex'
-    )
-    runCommand(
-        'ln -s {} rapport_inrae/'.format(os.path.join(latexSourceDir, 'logos.tex'))
-    )
+    createLink(relPathToLatexTemplate)
+    createLink(os.path.join(latexSourceDir, 'logo_pole.png'))
+    createLink('{}/schema_rugosite_fond.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam')))
+    createLink('{}/bloc_cylindre.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam')))
+    createLink('{}/bloc_face_arrondie.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam')))
+    createLink('{}/bloc_base_carree.png'.format(os.path.join(baseDir, 'docs', lang, 'calculators', 'pam')))
+    runCommand('rm rapport_inrae/logos.tex')
+    createLink('{} rapport_inrae/'.format(os.path.join(latexSourceDir, 'logos.tex')))
     # back to original working drectory
     os.chdir(baseDir)
 
@@ -189,9 +194,11 @@ def buildPDF(lang):
     cvt = os.path.join(buildDir, 'cassiopee_version.tex')
     shutil.copy(cvt, modelDir)
 
-    os.system(
-        'latexmk -f -xelatex -pdf -interaction=nonstopmode {} > /dev/null 2>&1'.format(sourceTexFile)
-    )
+    if verbose:
+        os.system('latexmk -f -xelatex -pdf -interaction=nonstopmode {} > /dev/null'.format(sourceTexFile))
+    else:
+        os.system('latexmk -f -xelatex -pdf -interaction=nonstopmode {} > /dev/null 2>&1'.format(sourceTexFile))
+
     # copy generated PDF to release directory
     shutil.copy(outputPdfFile, outputDir)
     # back to original working drectory
@@ -201,9 +208,9 @@ def buildPDF(lang):
 def buildDocForLang(lang):
 
     # Prepare temporary build directory
-    os.makedirs(pdfBuildDir, exist_ok=True)
+    createEmptyDir(pdfBuildDir)
     # Prepare output directory
-    os.makedirs(outputDir, exist_ok=True)
+    createEmptyDir(outputDir)
 
     # Read config
     yamlPath = 'mkdocs/mkdocs-' + lang + '.yml'
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index d11194ecab2f9f8c1b5ed14ec25d6a671bc1bf23..0e1044ac651ec31ebd7d38d288ab52741aaea25c 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -22,7 +22,7 @@ import { DialogSaveSessionComponent } from "./components/dialog-save-session/dia
 import { QuicknavComponent } from "./components/quicknav/quicknav.component";
 import { NotificationsService } from "./services/notifications.service";
 
-import { decodeHtml } from "./util";
+import { decodeHtml } from "./util/util";
 
 import { HotkeysService, Hotkey } from "angular2-hotkeys";
 
@@ -33,6 +33,8 @@ import { saveAs } from "file-saver";
 import * as XLSX from "xlsx";
 
 import * as pako from "pako";
+import { DialogConfirmComponent } from "./components/dialog-confirm/dialog-confirm.component";
+import { UserConfirmationService } from "./services/user-confirmation.service";
 import { ServiceWorkerUpdateService } from "./services/service-worker-update.service";
 
 @Component({
@@ -85,13 +87,16 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
         private confirmCloseCalcDialog: MatDialog,
         private hotkeysService: HotkeysService,
         private matomoTracker: MatomoTracker,
-        private serviceWorkerUpdateService: ServiceWorkerUpdateService
+        private confirmDialog: MatDialog,
+        private serviceWorkerUpdateService: ServiceWorkerUpdateService,
+        private userConfirmationService: UserConfirmationService
     ) {
         ServiceFactory.httpService = httpService;
         ServiceFactory.applicationSetupService = appSetupService;
         ServiceFactory.i18nService = intlService;
         ServiceFactory.formulaireService = formulaireService;
         ServiceFactory.notificationsService = notificationsService;
+        ServiceFactory.serviceWorkerUpdateService = serviceWorkerUpdateService;
 
         if (!isDevMode()) {
             // évite de mettre en place un bandeau RGPD
@@ -218,10 +223,24 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
     ngOnInit() {
         this.formulaireService.addObserver(this);
         this._innerWidth = window.innerWidth;
+        this.logRevisionInfo();
+
+        // Initialise communication with UserConfirmationService.
+        // When receiving a message from it, open a dialog to ask user to confirm.
+        // Will then reply to UserConfirmationService with a message holding confirmation status.
+        this.userConfirmationService.subscribe(this);
+        this.userConfirmationService.addHandler(this, {
+            next: (data) => this.displayConfirmationDialog(data["title"], data["body"]),
+            error: () => { },
+            complete: () => { },
+        });
     }
 
     ngOnDestroy() {
         this.formulaireService.removeObserver(this);
+
+        // cancel communication link with UserConfirmationService
+        this.userConfirmationService.unsubscribe(this);
     }
 
     @HostListener("window:resize", ["$event"])
@@ -670,6 +689,12 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
         };
     }
 
+    private logRevisionInfo() {
+        const ri = this.revisionInfo;
+        console.log("JaLHyd", ri.jalhyd.date, ri.jalhyd.version);
+        console.log("ngHyd", ri.nghyd.date, ri.nghyd.version);
+    }
+
     /**
      * sauvegarde du/des formulaires
      * @param form formulaire à sélectionner par défaut dans la liste
@@ -809,4 +834,24 @@ export class AppComponent implements OnInit, OnDestroy, Observer {
             }
         }
     }
+
+    /**
+     * display a confirmation display upon request from UserConfirmationService
+     */
+    private displayConfirmationDialog(title: string, text: string) {
+        const dialogRef = this.confirmDialog.open(
+            DialogConfirmComponent,
+            {
+                data: {
+                    title: title,
+                    text: text
+                },
+                disableClose: true
+            }
+        );
+        dialogRef.afterClosed().subscribe(result => {
+            // reply to UserConfirmationService
+            this.userConfirmationService.postConfirmation(this, { "confirm": result });
+        });
+    }
 }
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 15eac95408a49c29f830397b606d7f95990029fc..5c912764c5e680f134109936abe3681c7d0de46d 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -96,6 +96,7 @@ import { JetTrajectoryChartComponent } from "./components/jet-trajectory-chart/j
 import { SessionPropertiesComponent } from "./components/session-properties/session-properties.component";
 import { VerificateurResultsComponent } from "./components/verificateur-results/verificateur-results.component";
 
+import { DialogConfirmComponent } from "./components/dialog-confirm/dialog-confirm.component";
 import { DialogConfirmEmptySessionComponent } from "./components/dialog-confirm-empty-session/dialog-confirm-empty-session.component";
 import { DialogConfirmCloseCalcComponent } from "./components/dialog-confirm-close-calc/dialog-confirm-close-calc.component";
 import { DialogEditPabComponent } from "./components/dialog-edit-pab/dialog-edit-pab.component";
@@ -127,6 +128,7 @@ import { SelectSectionDetailsComponent } from "./components/select-section-detai
 import { ServiceWorkerModule } from '@angular/service-worker';
 import { environment } from '../environments/environment';
 import { ServiceWorkerUpdateService } from "./services/service-worker-update.service";
+import { UserConfirmationService } from "./services/user-confirmation.service";
 
 const appRoutes: Routes = [
     { path: "list/search", component: CalculatorListComponent },
@@ -208,6 +210,7 @@ const appRoutes: Routes = [
         CalculatorResultsComponent,
         DialogConfirmCloseCalcComponent,
         DialogConfirmEmptySessionComponent,
+        DialogConfirmComponent,
         DialogEditPabComponent,
         DialogEditParamComputedComponent,
         DialogEditParamValuesComponent,
@@ -282,7 +285,8 @@ const appRoutes: Routes = [
             provide: ErrorStateMatcher,
             useClass: ImmediateErrorStateMatcher
         },
-        ServiceWorkerUpdateService
+        ServiceWorkerUpdateService,
+        UserConfirmationService
     ],
     schemas: [NO_ERRORS_SCHEMA],
     bootstrap: [AppComponent]
diff --git a/src/app/components/dialog-confirm/dialog-confirm.component.html b/src/app/components/dialog-confirm/dialog-confirm.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..1de1a1bae3d8e2d2cee34ed52dd36424283dc39f
--- /dev/null
+++ b/src/app/components/dialog-confirm/dialog-confirm.component.html
@@ -0,0 +1,12 @@
+<h1 mat-dialog-title [innerHTML]="uitextTitle"></h1>
+<div mat-dialog-content>
+    <p [innerHTML]="uitextBody"></p>
+</div>
+<div mat-dialog-actions [attr.align]="'end'">
+    <button id="cancel" mat-raised-button color="primary" [mat-dialog-close]="false" cdkFocusInitial>
+        {{ uitextNo }}
+    </button>
+    <button id="confirm" mat-raised-button color="warn" [mat-dialog-close]="true">
+        {{ uitextYes }}
+    </button>
+</div>
diff --git a/src/app/components/dialog-confirm/dialog-confirm.component.ts b/src/app/components/dialog-confirm/dialog-confirm.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f8cefaa2526ec20077f992157001690ec99e5c16
--- /dev/null
+++ b/src/app/components/dialog-confirm/dialog-confirm.component.ts
@@ -0,0 +1,38 @@
+import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
+import { Inject, Component } from "@angular/core";
+import { I18nService } from "../../services/internationalisation.service";
+
+@Component({
+    selector: "dialog-confirm",
+    templateUrl: "dialog-confirm.component.html",
+})
+export class DialogConfirmComponent {
+
+    private _title: string;
+    private _text: string;
+
+    constructor(
+        public dialogRef: MatDialogRef<DialogConfirmComponent>,
+        private intlService: I18nService,
+        @Inject(MAT_DIALOG_DATA) public data: any
+    ) {
+        this._title = data.title;
+        this._text = data.text;
+    }
+
+    public get uitextYes() {
+        return this.intlService.localizeText("INFO_OPTION_YES");
+    }
+
+    public get uitextNo() {
+        return this.intlService.localizeText("INFO_OPTION_NO");
+    }
+
+    public get uitextTitle() {
+        return this._title;
+    }
+
+    public get uitextBody() {
+        return this._text;
+    }
+}
diff --git a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
index 16768e9c0715efc77bbf7b50d2c4d6b45d3e64ea..47790963b1c2fd3b83d065621b01b1c2326a026a 100644
--- a/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
+++ b/src/app/components/dialog-edit-param-values/dialog-edit-param-values.component.ts
@@ -10,7 +10,7 @@ import { sprintf } from "sprintf-js";
 
 import { ParamValueMode, ExtensionStrategy } from "jalhyd";
 
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { ServiceFactory } from "app/services/service-factory";
 
 @Component({
diff --git a/src/app/components/dialog-generate-par-simulation/dialog-generate-par-simulation.component.ts b/src/app/components/dialog-generate-par-simulation/dialog-generate-par-simulation.component.ts
index b7aaf61d72def9cd99d15fec5b95d62666de2614..554bc994357a9496566c3ad921f6ec8f4b8176f1 100644
--- a/src/app/components/dialog-generate-par-simulation/dialog-generate-par-simulation.component.ts
+++ b/src/app/components/dialog-generate-par-simulation/dialog-generate-par-simulation.component.ts
@@ -3,7 +3,7 @@ import { Inject, Component } from "@angular/core";
 
 import { I18nService } from "../../services/internationalisation.service";
 import { MultiDimensionResults } from "../../results/multidimension-results";
-import { fv, longestVarParam } from "../../util";
+import { fv, longestVarParam } from "../../util/util";
 
 @Component({
     selector: "dialog-generate-par-simulation",
diff --git a/src/app/components/field-set/field-set.component.ts b/src/app/components/field-set/field-set.component.ts
index 448d40a7dc63bfcd47dbec4e10bdb29ba884e857..62b39960f24fd4f2e18e1d4c95c9e5b8dda162c1 100644
--- a/src/app/components/field-set/field-set.component.ts
+++ b/src/app/components/field-set/field-set.component.ts
@@ -16,7 +16,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { sprintf } from "sprintf-js";
 
 import { capitalize } from "jalhyd";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 
 @Component({
     selector: "field-set",
diff --git a/src/app/components/fieldset-container/fieldset-container.component.ts b/src/app/components/fieldset-container/fieldset-container.component.ts
index 042df751799ed61418e5b9a869e2277cb627110d..e07c6514d5088587c89b402db5c6cca6088bc7e7 100644
--- a/src/app/components/fieldset-container/fieldset-container.component.ts
+++ b/src/app/components/fieldset-container/fieldset-container.component.ts
@@ -6,7 +6,7 @@ import { FieldSet } from "../../formulaire/elements/fieldset";
 import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 import { ParamValueMode } from "jalhyd";
 
 @Component({
diff --git a/src/app/components/fixedvar-results/results.component.ts b/src/app/components/fixedvar-results/results.component.ts
index ae9747dbe0e9f0b4843d4ccb9292105cf295e86f..18b08975073117647fc2b4e3fbfd1e94e5e81a1c 100644
--- a/src/app/components/fixedvar-results/results.component.ts
+++ b/src/app/components/fixedvar-results/results.component.ts
@@ -2,7 +2,7 @@ import screenfull from "screenfull";
 
 import { NgParameter } from "../../formulaire/elements/ngparam";
 import { ServiceFactory } from "../../services/service-factory";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { CalculatorResults } from "../../results/calculator-results";
 
 import { Directive, HostListener } from "@angular/core";
diff --git a/src/app/components/fixedvar-results/var-results.component.ts b/src/app/components/fixedvar-results/var-results.component.ts
index 7493401d2776c1760fbe9c616232aa62ceefecea..3c9f310ce6c3d740cb8bd9303d6c41f58104848a 100644
--- a/src/app/components/fixedvar-results/var-results.component.ts
+++ b/src/app/components/fixedvar-results/var-results.component.ts
@@ -8,7 +8,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "./results.component";
 import { DialogLogEntriesDetailsComponent } from "../dialog-log-entries-details/dialog-log-entries-details.component";
 import { AppComponent } from "../../app.component";
-import { longestVarParam } from "../../../app/util";
+import { longestVarParam } from "../../../app/util/util";
 
 @Component({
     selector: "var-results",
diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts
index 49e81d946b6216bb123c23a0c64dea8c32cbe730..fe805f6581909ca8a8dfdde1779083b2412de6c0 100644
--- a/src/app/components/generic-calculator/calculator.component.ts
+++ b/src/app/components/generic-calculator/calculator.component.ts
@@ -28,7 +28,7 @@ import {
     ParallelStructure
 } from "jalhyd";
 
-import { generateValuesCombination, getUnformattedIthResult, getUnformattedIthValue } from "../../util";
+import { generateValuesCombination, getUnformattedIthResult, getUnformattedIthValue } from "../../util/util";
 
 import { AppComponent } from "../../app.component";
 import { FormulaireService } from "../../services/formulaire.service";
@@ -62,7 +62,7 @@ import { sprintf } from "sprintf-js";
 
 import * as XLSX from "xlsx";
 import { ServiceFactory } from "app/services/service-factory";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 import { FormulaireCourbeRemous } from "app/formulaire/definition/form-courbe-remous";
 import { RemousResults } from "app/results/remous-results";
 
diff --git a/src/app/components/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts
index d95168143cb67edb5fff3ffe00586e65fe60ad4f..281a11332a7c288641ab2719a9f18a1d11c9ccf2 100644
--- a/src/app/components/generic-input/generic-input.component.ts
+++ b/src/app/components/generic-input/generic-input.component.ts
@@ -5,7 +5,7 @@ import { FormulaireDefinition } from "../../formulaire/definition/form-definitio
 import { NgParameter } from "../../formulaire/elements/ngparam";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 
 /**
  * classe de gestion générique d'un champ de saisie avec titre, validation et message d'erreur
diff --git a/src/app/components/jet-trajectory-chart/jet-trajectory-chart.component.ts b/src/app/components/jet-trajectory-chart/jet-trajectory-chart.component.ts
index 5bd87e2bb12d5177c109a493bcc92517bef823ce..bf4e084e134588c955fd4da70e1a8de96b82808c 100644
--- a/src/app/components/jet-trajectory-chart/jet-trajectory-chart.component.ts
+++ b/src/app/components/jet-trajectory-chart/jet-trajectory-chart.component.ts
@@ -5,7 +5,7 @@ import { BaseChartDirective } from "ng2-charts";
 import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { IYSeries } from "../../results/y-series";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { AppComponent } from "../../app.component";
 
 import { Jet, Result } from "jalhyd";
diff --git a/src/app/components/macrorugo-compound-results/macrorugo-compound-results.component.ts b/src/app/components/macrorugo-compound-results/macrorugo-compound-results.component.ts
index f388647dafbcf7d653fcffd263b87d3dfff40718..039681e3c0195eb26e976b2d3b6ab31fd3189534 100644
--- a/src/app/components/macrorugo-compound-results/macrorugo-compound-results.component.ts
+++ b/src/app/components/macrorugo-compound-results/macrorugo-compound-results.component.ts
@@ -2,7 +2,7 @@ import { Component, Input } from "@angular/core";
 
 import { Result, cLog, Message, MessageCode, MessageSeverity, MRCInclination } from "jalhyd";
 
-import { fv } from "../../../app/util";
+import { fv } from "../../../app/util/util";
 
 import { CalculatorResults } from "../../results/calculator-results";
 import { NgParameter } from "../../formulaire/elements/ngparam";
diff --git a/src/app/components/modules-diagram/modules-diagram.component.ts b/src/app/components/modules-diagram/modules-diagram.component.ts
index 0b607e51e10e25cb2f5505c3e1bdf0993dfcffe5..0ec435a0739a6691b60d18390aab60a0f4227fdc 100644
--- a/src/app/components/modules-diagram/modules-diagram.component.ts
+++ b/src/app/components/modules-diagram/modules-diagram.component.ts
@@ -33,7 +33,7 @@ import * as SvgPanZoom from "svg-pan-zoom";
 
 import { MatomoTracker } from "@ngx-matomo/tracker";
 
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 
 @Component({
     selector: "modules-diagram",
diff --git a/src/app/components/pab-profile-chart/pab-profile-chart.component.ts b/src/app/components/pab-profile-chart/pab-profile-chart.component.ts
index ee664e43ad59b3c77bd6e000921cbb11d8c32ea6..11aca1b4046fb5c4b0119cf4a5c87b4a56f41b20 100644
--- a/src/app/components/pab-profile-chart/pab-profile-chart.component.ts
+++ b/src/app/components/pab-profile-chart/pab-profile-chart.component.ts
@@ -6,7 +6,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { PabResults } from "../../results/pab-results";
 import { IYSeries } from "../../results/y-series";
-import { fv, longestVarParam } from "../../util";
+import { fv, longestVarParam } from "../../util/util";
 import { AppComponent } from "../../app.component";
 
 import { CloisonAval, Cloisons, LoiDebit } from "jalhyd";
diff --git a/src/app/components/pab-results/pab-results-table.component.ts b/src/app/components/pab-results/pab-results-table.component.ts
index b541cdc52e332f6cde19abc74c9c9730db870fd8..b2b36a8368d34de4fa186b699152db63e9602442 100644
--- a/src/app/components/pab-results/pab-results-table.component.ts
+++ b/src/app/components/pab-results/pab-results-table.component.ts
@@ -6,7 +6,7 @@ import { PabResults } from "../../results/pab-results";
 import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { AppComponent } from "../../app.component";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 
 @Component({
     selector: "pab-results-table",
diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts
index ead6072f13b0af083cdf29b590ab1172de57f28a..dc2470aef9334fdf718ddc095c103a460279dece 100644
--- a/src/app/components/pab-table/pab-table.component.ts
+++ b/src/app/components/pab-table/pab-table.component.ts
@@ -28,7 +28,7 @@ import { PabTable } from "../../formulaire/elements/pab-table";
 import { DialogEditPabComponent } from "../dialog-edit-pab/dialog-edit-pab.component";
 import { AppComponent } from "../../app.component";
 import { NgParameter, ParamRadioConfig } from "../../formulaire/elements/ngparam";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 
 /**
  * The big editable data grid for calculator type "Pab" (component)
diff --git a/src/app/components/pb-results/pb-cloison-results.component.ts b/src/app/components/pb-results/pb-cloison-results.component.ts
index 0a59b6a8b1f0581e7ff07e706f24459e6e7df5ce..da3aae4ba32983e163a9096fdf1db300ea78aab3 100644
--- a/src/app/components/pb-results/pb-cloison-results.component.ts
+++ b/src/app/components/pb-results/pb-cloison-results.component.ts
@@ -2,7 +2,7 @@ import { Component, Input } from "@angular/core";
 
 import { FixedResultsComponent } from "../fixedvar-results/fixed-results.component";
 import { NgParameter } from "../../formulaire/elements/ngparam";
-import { getIthValue } from "../../util";
+import { getIthValue } from "../../util/util";
 import { PbCloisonResults } from "../../results/pb-cloison-results";
 
 import { Result, ResultElement } from "jalhyd";
diff --git a/src/app/components/pb-results/pb-results-table.component.ts b/src/app/components/pb-results/pb-results-table.component.ts
index 79084192dd3b7b5e0477157ea06f3e8a09b1e5c9..9e1315d86f6d1f8608f728389431494d1065d239 100644
--- a/src/app/components/pb-results/pb-results-table.component.ts
+++ b/src/app/components/pb-results/pb-results-table.component.ts
@@ -5,7 +5,7 @@ import { PreBarrage, PbBassin } from "jalhyd";
 import { I18nService } from "../../services/internationalisation.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { AppComponent } from "../../app.component";
-import { fv, getIthValue } from "../../util";
+import { fv, getIthValue } from "../../util/util";
 import { PrebarrageResults } from "../../results/prebarrage-results";
 
 @Component({
diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts
index 6de70eecd8ddf7c49d2c45a200a9ae6a6cf0a009..ef424b22fd24c3c88efd3680c3d0250f38ba1512 100644
--- a/src/app/components/pb-schema/pb-schema.component.ts
+++ b/src/app/components/pb-schema/pb-schema.component.ts
@@ -18,9 +18,9 @@ import { GenericCalculatorComponent } from "../generic-calculator/calculator.com
 import { FormulairePrebarrage } from "../../formulaire/definition/form-prebarrage";
 import { AppComponent } from "../../app.component";
 
-import { fv } from "app/util";
+import { fv } from "app/util/util";
 import { ServiceFactory } from "app/services/service-factory";
-import { DefinedBoolean } from "app/definedvalue/definedboolean";
+import { DefinedBoolean } from "../../util/definedvalue/definedboolean";
 import { PrebarrageService, PrebarrageServiceEvents } from "app/services/prebarrage.service";
 
 /**
diff --git a/src/app/components/remous-results/remous-results.component.ts b/src/app/components/remous-results/remous-results.component.ts
index 1f5ea4b720bfcb4bbf0ccbd85f42bde4dc582540..2fd73ae1e13f3ecb6b1b9444b07e0800ca6435f8 100644
--- a/src/app/components/remous-results/remous-results.component.ts
+++ b/src/app/components/remous-results/remous-results.component.ts
@@ -9,7 +9,7 @@ import { FormulaireService } from "../../services/formulaire.service";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { AppComponent } from "../../app.component";
 import { LineData, ChartData } from "./line-and-chart-data";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { VarResults } from "../../results/var-results";
 
 import { BaseChartDirective } from "ng2-charts";
diff --git a/src/app/components/results-chart/chart-type.component.ts b/src/app/components/results-chart/chart-type.component.ts
index 39839c3a96cb2feea1c220ebbf415e2855263880..123657c5173e1f09900f327f140c4cdd6eb3dc65 100644
--- a/src/app/components/results-chart/chart-type.component.ts
+++ b/src/app/components/results-chart/chart-type.component.ts
@@ -4,7 +4,7 @@ import { I18nService } from "../../services/internationalisation.service";
 import { ChartType } from "../../results/chart-type";
 import { SelectFieldChartType } from "app/formulaire/elements/select/select-field-charttype";
 import { SelectEntry } from "app/formulaire/elements/select/select-entry";
-import { decodeHtml } from "../../util";
+import { decodeHtml } from "../../util/util";
 
 @Component({
     selector: "chart-type",
diff --git a/src/app/components/results-chart/results-chart.component.ts b/src/app/components/results-chart/results-chart.component.ts
index 0b33967f3c45ae7766943a382b9db32fc3593ed7..be7eb667828b18318c0c5b92b5759e88663d7ddc 100644
--- a/src/app/components/results-chart/results-chart.component.ts
+++ b/src/app/components/results-chart/results-chart.component.ts
@@ -11,7 +11,7 @@ import { ChartType } from "../../results/chart-type";
 import { ResultsComponentDirective } from "../fixedvar-results/results.component";
 import { IYSeries } from "../../results/y-series";
 import { VarResults } from "../../results/var-results";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 import { AppComponent } from "../../app.component";
 
 import zoomPlugin from 'chartjs-plugin-zoom';
diff --git a/src/app/components/select-field-line/select-field-line.component.ts b/src/app/components/select-field-line/select-field-line.component.ts
index c6b2474a0f08e795ac58514bb58486d127e4ecdb..7c4b067efcece001ba65882f713c0e286d660f48 100644
--- a/src/app/components/select-field-line/select-field-line.component.ts
+++ b/src/app/components/select-field-line/select-field-line.component.ts
@@ -4,7 +4,7 @@ import { SelectField } from "../../formulaire/elements/select/select-field";
 import { SelectEntry } from "../../formulaire/elements/select/select-entry";
 import { I18nService } from "../../services/internationalisation.service";
 import { ApplicationSetupService } from "../../services/app-setup.service";
-import { decodeHtml } from "../../util";
+import { decodeHtml } from "../../util/util";
 
 @Component({
     selector: "select-field-line",
diff --git a/src/app/components/select-section-details/select-section-details.component.ts b/src/app/components/select-section-details/select-section-details.component.ts
index 55c21b523cead45096bfc15f39d2d733043aad92..278ce4bdca3d2ead89a5354807f4266b1f4171a0 100644
--- a/src/app/components/select-section-details/select-section-details.component.ts
+++ b/src/app/components/select-section-details/select-section-details.component.ts
@@ -3,7 +3,7 @@ import { Router } from '@angular/router';
 import { FormulaireDefinition } from 'app/formulaire/definition/form-definition';
 import { FormulaireService } from 'app/services/formulaire.service';
 import { I18nService } from 'app/services/internationalisation.service';
-import { fv } from 'app/util';
+import { fv } from 'app/util/util';
 import { formattedValue } from 'jalhyd';
 
 /**
diff --git a/src/app/components/variable-results-selector/variable-results-selector.component.ts b/src/app/components/variable-results-selector/variable-results-selector.component.ts
index 0d68b44573e4177fb943019a8516944fdbc94162..50c4a0d968df136c66c0f25f9391c0c2a5688820 100644
--- a/src/app/components/variable-results-selector/variable-results-selector.component.ts
+++ b/src/app/components/variable-results-selector/variable-results-selector.component.ts
@@ -1,7 +1,7 @@
 import { Component, Input, OnChanges } from "@angular/core";
 
 import { I18nService } from "../../services/internationalisation.service";
-import { fv, longestVarParam } from "../../util";
+import { fv, longestVarParam } from "../../util/util";
 import { MultiDimensionResults } from "../../results/multidimension-results";
 import { VariatedDetails } from "jalhyd";
 import { CalculatorResults } from "../../results/calculator-results";
diff --git a/src/app/formulaire/definition/form-pab.ts b/src/app/formulaire/definition/form-pab.ts
index 85f423501196014b5198c27d9e5a6a243524e796..d27a620927df68c68c2789250112d8889d7dbd93 100644
--- a/src/app/formulaire/definition/form-pab.ts
+++ b/src/app/formulaire/definition/form-pab.ts
@@ -3,7 +3,7 @@ import { Pab, Result, VariatedDetails } from "jalhyd";
 import { FormulaireDefinition } from "./form-definition";
 import { PabResults } from "../../results/pab-results";
 import { NgParameter } from "../elements/ngparam";
-import { longestVarParam } from "../../util";
+import { longestVarParam } from "../../util/util";
 import { PabTable } from "../elements/pab-table";
 
 /**
diff --git a/src/app/formulaire/definition/form-prebarrage.ts b/src/app/formulaire/definition/form-prebarrage.ts
index c9a0d9e0db35efb7ff665b637520798032944bf6..7aa507656ca0d4925ea3b0d9abf1f79194bf9b4a 100644
--- a/src/app/formulaire/definition/form-prebarrage.ts
+++ b/src/app/formulaire/definition/form-prebarrage.ts
@@ -9,7 +9,7 @@ import { FieldsetContainer } from "../elements/fieldset-container";
 import { CalculatorResults } from "../../results/calculator-results";
 import { PrebarrageResults } from "../../results/prebarrage-results";
 import { NgParameter } from "../elements/ngparam";
-import { longestVarParam } from "../../util";
+import { longestVarParam } from "../../util/util";
 import { FormulaireNode } from "../elements/formulaire-node";
 
 /**
diff --git a/src/app/formulaire/elements/ngparam.ts b/src/app/formulaire/elements/ngparam.ts
index e4dc902cd4034702c761634d145c30781db189b2..a579f014146807fe4e091f7a7dd6b1d0050faa18 100644
--- a/src/app/formulaire/elements/ngparam.ts
+++ b/src/app/formulaire/elements/ngparam.ts
@@ -8,7 +8,7 @@ import { sprintf } from "sprintf-js";
 import { InputField } from "./input-field";
 import { ServiceFactory } from "../../services/service-factory";
 import { FormulaireNode } from "./formulaire-node";
-import { fv } from "../../util";
+import { fv } from "../../util/util";
 
 export enum ParamRadioConfig {
     /** pas de radio, paramètre modifiable à la main uniquement */
diff --git a/src/app/formulaire/elements/select/select-field-searched-param.ts b/src/app/formulaire/elements/select/select-field-searched-param.ts
index 339acbfe5702e623c3cc8041b2860ad7ef0fe3f6..8811ef81f441628e39164f0f46404fa8cf1a1d4c 100644
--- a/src/app/formulaire/elements/select/select-field-searched-param.ts
+++ b/src/app/formulaire/elements/select/select-field-searched-param.ts
@@ -1,5 +1,5 @@
 import { ServiceFactory } from "app/services/service-factory";
-import { decodeHtml } from "app/util";
+import { decodeHtml } from "app/util/util";
 import { acSection, Nub, Solveur } from "jalhyd";
 import { SelectEntry } from "./select-entry";
 import { SelectField } from "./select-field";
diff --git a/src/app/formulaire/elements/select/select-field-solveur-target.ts b/src/app/formulaire/elements/select/select-field-solveur-target.ts
index b98546e221fc7bf13c782ca43440e24df9f34320..3d752aa4368a5c45c5b0c4a4d36375ff8cb0d8b2 100644
--- a/src/app/formulaire/elements/select/select-field-solveur-target.ts
+++ b/src/app/formulaire/elements/select/select-field-solveur-target.ts
@@ -5,7 +5,7 @@
 */
 
 import { ServiceFactory } from "app/services/service-factory";
-import { decodeHtml } from "../../../util";
+import { decodeHtml } from "../../../util/util";
 import { Session, Solveur } from "jalhyd";
 import { SelectEntry } from "./select-entry";
 import { SelectField } from "./select-field";
diff --git a/src/app/formulaire/elements/select/select-field-target-pass.ts b/src/app/formulaire/elements/select/select-field-target-pass.ts
index 6383c02ffc159a14afa7f6a11dce014769c48af9..8438e0a0fe2c50a5b5b1c8b937543249def5a423 100644
--- a/src/app/formulaire/elements/select/select-field-target-pass.ts
+++ b/src/app/formulaire/elements/select/select-field-target-pass.ts
@@ -1,5 +1,5 @@
 import { ServiceFactory } from "app/services/service-factory";
-import { decodeHtml } from "app/util";
+import { decodeHtml } from "app/util/util";
 import { CalculatorType, FishPass, Session, Verificateur } from "jalhyd";
 import { FormulaireElement } from "../formulaire-element";
 import { FormulaireNode } from "../formulaire-node";
diff --git a/src/app/formulaire/elements/select/select-field.ts b/src/app/formulaire/elements/select/select-field.ts
index 2b9b65aa97fc770e1548718791597698decb7a09..b45f59c42d2581d536dc40659295cb86ed57cc62 100644
--- a/src/app/formulaire/elements/select/select-field.ts
+++ b/src/app/formulaire/elements/select/select-field.ts
@@ -1,6 +1,6 @@
 import { Field } from "../field";
 import { SelectEntry } from "./select-entry";
-import { arraysAreEqual } from "../../../util";
+import { arraysAreEqual } from "../../../util/util";
 import { FormulaireNode } from "../formulaire-node";
 import { ServiceFactory } from "app/services/service-factory";
 import { FormulaireDefinition } from "../../definition/form-definition";
diff --git a/src/app/results/var-results.ts b/src/app/results/var-results.ts
index 37ff757b971936d696f0b040060cc6b54f2688f7..c8c814d89776af9760d03c5e524297446330f92a 100644
--- a/src/app/results/var-results.ts
+++ b/src/app/results/var-results.ts
@@ -2,7 +2,7 @@ import { CalculatedParamResults } from "./param-calc-results";
 import { ServiceFactory } from "../services/service-factory";
 import { PlottableData } from "./plottable-data";
 import { ChartType } from "./chart-type";
-import { longestVarParam } from "../util";
+import { longestVarParam } from "../util/util";
 import { FormulaireDefinition } from "../formulaire/definition/form-definition";
 
 import { sprintf } from "sprintf-js";
diff --git a/src/app/services/internationalisation.service.ts b/src/app/services/internationalisation.service.ts
index ea1bec8c3d563f2e11b1a8cfdbf45ae95c67100f..deee494fecb63836be93eeb253b8253ed708742f 100644
--- a/src/app/services/internationalisation.service.ts
+++ b/src/app/services/internationalisation.service.ts
@@ -5,7 +5,7 @@ import { Message, MessageCode, Observable, Observer, Nub, CalculatorType, PreBar
 import { StringMap } from "../stringmap";
 import { ApplicationSetupService } from "./app-setup.service";
 import { HttpService } from "./http.service";
-import { fv, decodeHtml } from "../util";
+import { fv, decodeHtml } from "../util/util";
 import { ServiceFactory } from "./service-factory";
 import { FormulaireService } from "./formulaire.service";
 
diff --git a/src/app/services/service-factory.ts b/src/app/services/service-factory.ts
index e3ea55cae3c7ce504924688dac8ae1517f3268ba..668d5aea18b12ed1295f44b2885fdec97a1eeb56 100644
--- a/src/app/services/service-factory.ts
+++ b/src/app/services/service-factory.ts
@@ -4,6 +4,7 @@ import { I18nService } from "./internationalisation.service";
 import { HttpService } from "./http.service";
 import { NotificationsService } from "./notifications.service";
 import { PrebarrageService } from "./prebarrage.service";
+import { ServiceWorkerUpdateService } from "./service-worker-update.service";
 
 /**
  * A "Singleton" the TS way, that holds pointers to all services, to be accessed
@@ -17,11 +18,13 @@ export const ServiceFactory: {
     httpService: HttpService;
     notificationsService: NotificationsService;
     prebarrageService: PrebarrageService;
+    serviceWorkerUpdateService: ServiceWorkerUpdateService;
 } = {
     applicationSetupService: undefined,
     formulaireService: undefined,
     i18nService: undefined,
     httpService: undefined,
     notificationsService: undefined,
-    prebarrageService: undefined
+    prebarrageService: undefined,
+    serviceWorkerUpdateService: undefined
 };
diff --git a/src/app/services/service-worker-update.service.ts b/src/app/services/service-worker-update.service.ts
index 96b84426ac85dc1c4666d7d6b55ecc7e4aa46f29..163fd4a12e45f941abec56a389479a73c68b291c 100644
--- a/src/app/services/service-worker-update.service.ts
+++ b/src/app/services/service-worker-update.service.ts
@@ -1,36 +1,74 @@
-import { Injectable } from "@angular/core";
+import { Injectable, NgZone } from "@angular/core";
 import { SwUpdate } from '@angular/service-worker';
 import { I18nService } from "./internationalisation.service";
 import { NotificationsService } from "./notifications.service";
+import { UserConfirmationService } from "./user-confirmation.service";
+import { interval } from "rxjs";
 
 @Injectable()
 export class ServiceWorkerUpdateService {
     constructor(
         private swUpdate: SwUpdate,
         private notificationService: NotificationsService,
-        private i18nService: I18nService
+        private i18nService: I18nService,
+        private userConfirmationService: UserConfirmationService,
+        private ngZone: NgZone
     ) {
-        swUpdate.versionUpdates.subscribe(evt => {
+        if (this.swUpdate.isEnabled) {
+            this.ngZone.runOutsideAngular(() =>
+                interval(1000 * 60 * 60).subscribe(val => {
+                    console.log('ServiceWorkerUpdateService: checking for updates...')
+                    swUpdate.checkForUpdate().then(b => {
+                        console.log("ServiceWorkerUpdateService: " + (b ? "new version found" : "no new version"));
+                    });
+                })
+            );
+        } else {
+            console.log("ServiceWorkerUpdateService: SwUpdate is disabled");
+        }
+
+        this.swUpdate.versionUpdates.subscribe(evt => {
             switch (evt.type) {
                 case 'VERSION_DETECTED':
-                    let ver = evt.version.appData["version"];
-                    let msg = i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_DETECTED", { "ver": ver });
-                    notificationService.notify(msg, 10000);
+                    let ver = (evt as any).version?.appData?.version ?? "<NA>";
+                    console.log("ServiceWorkerUpdateService: VERSION_DETECTED", ver);
+                    notificationService.notify(i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_DETECTED", { "ver": ver }), 10000);
                     break;
 
                 case 'VERSION_READY':
-                    const newVer = evt.latestVersion.appData["version"];
-                    // const currVer = evt.currentVersion.appData["version"];
-                    msg = i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_READY", { "ver": newVer });
-                    notificationService.notify(msg, 10000);
+                    const currVer = (evt as any).currentVersion?.appData?.version ?? "<NA>";
+                    const newVer = (evt as any).latestVersion?.appData?.version ?? "<NA>";
+                    console.log("ServiceWorkerUpdateService: VERSION_READY", currVer, "->", newVer);
+
+                    notificationService.notify(i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_READY", { "ver": newVer }), 10000);
+                    // PLANTE si on stocke le message dans une variable !!!!
+                    // const msg = i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_READY", { "ver": newVer });
+                    // notificationService.notify(msg, 10000);
+                    // -> ReferenceError: can't access lexical declaration 'xxx' before initialization
+                    // avec xxx qui varie d'une fois à l'autre !!!
+
+                    userConfirmationService.askUserConfirmation("Confirmation",
+                        i18nService.localizeText("INFO_SERVICE_WORKER_VERSION_READY", { "ver": newVer })).then(data => {
+                            if (data["confirm"]) {
+                                console.log("ServiceWorkerUpdateService: application update confirmed");
+                                window.location.reload();
+                            }
+                            else {
+                                console.log("ServiceWorkerUpdateService: application update canceled");
+                            }
+                        });
                     break;
 
                 case 'VERSION_INSTALLATION_FAILED':
-                    ver = evt.version.appData["version"];
-                    msg = i18nService.localizeText("ERROR_SERVICE_WORKER_INSTALL_FAILED", { "ver": ver });
-                    notificationService.notify(msg, 10000);
+                    ver = (evt as any).version?.appData?.version ?? "NA";
+                    console.log("ServiceWorkerUpdateService: VERSION_INSTALLATION_FAILED", ver);
+                    notificationService.notify(i18nService.localizeText("ERROR_SERVICE_WORKER_INSTALL_FAILED", { "ver": ver }), 10000);
                     break;
             }
         });
+        swUpdate.unrecoverable.subscribe(event => {
+            console.log("SwUpdate.unrecoverable reason", event.reason, "type", event.type);
+            notificationService.notify("SwUpdate: unrecoverable state. Reason=" + event.reason + ", type=" + event.type, 10000);
+        });
     }
 }
diff --git a/src/app/services/user-confirmation.service.ts b/src/app/services/user-confirmation.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d33beecbbcb3516620ad36170922baeeca54a8b3
--- /dev/null
+++ b/src/app/services/user-confirmation.service.ts
@@ -0,0 +1,69 @@
+import { Injectable } from "@angular/core";
+
+import { BidirectionalSubject } from "app/util/bidir_subject";
+import { Observer } from "rxjs";
+
+/**
+ * This service enables any class (even another service) to display a confirmation dialog on GUI ans get the user answer
+ */
+@Injectable()
+export class UserConfirmationService {
+
+    // used to communicate with UI component in charge of displaying confirmation dialog
+    // direction 0 : this
+    // direction 1 : UI
+    private _userConfirm: BidirectionalSubject<{}>;
+
+    public constructor() {
+        this._userConfirm = new BidirectionalSubject<{}>();
+
+        // we choose communication canal 0, UI will use 1
+        this._userConfirm.selectPostingChannel(this, 0);
+    }
+
+    /**
+     * add subscription from UI
+     * @param source 
+     */
+    public subscribe(source: any) {
+        this._userConfirm.selectPostingChannel(source, 1);
+    }
+
+    /**
+     * remove UI subscription
+     * @param source 
+     */
+    public unsubscribe(source: any) {
+        this._userConfirm.unselectPostingChannel(source);
+    }
+
+    /**
+     * add a handler (provided bu UI) to confirmation request
+     * @param source normally, UI component
+     * @param obs processing function
+     */
+    public addHandler(source: any, obs: Observer<boolean>) {
+        this._userConfirm.addHandler(source, obs)
+    }
+
+    /**
+     * forward user confirmation from UI to requesting object
+     * @param confirm user confirmation status
+     */
+    public postConfirmation(source: any, confirm: {}) {
+        this._userConfirm.post(source, confirm);
+    }
+
+    /**
+     * forward to UI a request from source to ask a user confirmation with a dialog
+     * @param source object requesting confirmation
+     * @param title confirmation dialog title
+     * @param text confirmation dialog body text
+     * @returns a Promise resolving to a boolean holding user confirmation status
+     */
+    public askUserConfirmation(title: string, text: string): Promise<{}> {
+        const ret = this._userConfirm.getReceivePromise(this);
+        this._userConfirm.post(this, { title: title, body: text }); // false or true, we don't care
+        return ret;
+    }
+}
diff --git a/src/app/util/bidir_subject.ts b/src/app/util/bidir_subject.ts
new file mode 100644
index 0000000000000000000000000000000000000000..069e004e83e7040dff2b273fad42ba9aef3ddd0c
--- /dev/null
+++ b/src/app/util/bidir_subject.ts
@@ -0,0 +1,131 @@
+import { Observer, Subject, firstValueFrom, lastValueFrom } from "rxjs";
+
+/**
+ * bi-directional subject (see RxJS Subject)
+ * Allows two objects to exchange messages in both directions. Each object has to choose a posting channel
+ * (messages will receive from the other one).
+ *
+ *    source1 ----post-----> | channel 0 | --subscribe--> source2
+ *            <--subscribe-- | channel 1 | <-----post----
+ * 
+ * EventEmitter is not used since it is reserved to properties in Angular component with @Output annotation
+ */
+export class BidirectionalSubject<T> {
+
+    // communication channels
+    private _channel0: Subject<T>;
+    private _channel1: Subject<T>;
+
+    // array of "who chose which posting channel"
+    private _channel0Posters: any[] = [];
+    private _channel1Posters: any[] = [];
+
+    constructor() {
+        this._channel0 = new Subject();
+        this._channel1 = new Subject();
+    }
+
+    /**
+     * get posting channel index
+     * @param source object that chose one of the channels
+     */
+    private getPostingChannelIndex(source: any) {
+        if (this._channel0Posters.indexOf(source) !== -1) {
+            return 0;
+        }
+        if (this._channel1Posters.indexOf(source) !== -1) {
+            return 1;
+        }
+        return -1;
+    }
+
+    /**
+     * choose a posting channel
+     * @param source object that chooses the channel
+     * @param chan channel number
+     */
+    public selectPostingChannel(source: any, chan: number) {
+        switch (chan) {
+            case 0:
+                if (this.getPostingChannelIndex(source) !== -1) {
+                    throw new Error("object already has a selected channel");
+                }
+                this._channel0Posters.push(source);
+                break;
+
+            case 1:
+                if (this.getPostingChannelIndex(source) !== -1) {
+                    throw new Error("object already has a selected channel");
+                }
+                this._channel1Posters.push(source);
+                break;
+
+            default:
+                throw new Error(`invalid channel number ${chan}`);
+        }
+    }
+
+    /**
+     * remove a source from its channel
+     */
+    public unselectPostingChannel(source: any) {
+        this._channel0Posters = this._channel0Posters.filter(o => o != source);
+        this._channel1Posters = this._channel1Posters.filter(o => o != source);
+    }
+
+    /**
+     * used by a source to post a message to communication channel
+     */
+    public post(source: any, msg: T) {
+        switch (this.getPostingChannelIndex(source)) {
+            case 0:
+                this._channel0.next(msg);
+                break;
+
+            case 1:
+                this._channel1.next(msg);
+                break;
+
+            case -1:
+                throw new Error("must select a channel first");
+        }
+    }
+
+    /**
+     * create a Promise representing a received message (when posted by another source)
+     * @param source object that will use the Promise
+     */
+    public getReceivePromise(source: any): Promise<T> {
+        switch (this.getPostingChannelIndex(source)) {
+            case 0:
+                return firstValueFrom(this._channel1);
+
+            case 1:
+                return firstValueFrom(this._channel0);
+
+            case -1:
+                throw new Error("must select a channel first");
+        }
+    }
+
+    /**
+     * Add a message handler (provided by source) to process received messages
+     * (alternative to getReceivePromise())
+     * @param source object providing handler
+     * @param handler message processing function
+     */
+    public addHandler(source: any, handler: Observer<T>) {
+        switch (this.getPostingChannelIndex(source)) {
+            case 0:
+                this._channel1.subscribe(handler);
+                break;
+
+            case 1:
+                this._channel0.subscribe(handler);
+                break;
+
+            case -1:
+                throw new Error("must select a channel first");
+        }
+    }
+}
diff --git a/src/app/definedvalue/definedboolean.ts b/src/app/util/definedvalue/definedboolean.ts
similarity index 100%
rename from src/app/definedvalue/definedboolean.ts
rename to src/app/util/definedvalue/definedboolean.ts
diff --git a/src/app/definedvalue/definedvalue.ts b/src/app/util/definedvalue/definedvalue.ts
similarity index 100%
rename from src/app/definedvalue/definedvalue.ts
rename to src/app/util/definedvalue/definedvalue.ts
diff --git a/src/app/util.ts b/src/app/util/util.ts
similarity index 97%
rename from src/app/util.ts
rename to src/app/util/util.ts
index 763a425cbebe0a5b95596c0ae707666bfea8df12..06db46788e2d96870913a5efd1faf8c49db487dd 100644
--- a/src/app/util.ts
+++ b/src/app/util/util.ts
@@ -1,5 +1,5 @@
-import { NgParameter } from "./formulaire/elements/ngparam";
-import { ServiceFactory } from "./services/service-factory";
+import { NgParameter } from "../formulaire/elements/ngparam";
+import { ServiceFactory } from "../services/service-factory";
 
 import { formattedValue, Nub, VariatedDetails, ParamDefinition, ParamValueMode, Result } from "jalhyd";
 
@@ -13,7 +13,7 @@ export function logObject(obj: {}, m?: string) {
 }
 
 export function isNumber(s: string): boolean {
-    return Number(s) !== NaN;
+    return !Number.isNaN(Number(s));
 }
 
 /**
diff --git a/src/main.ts b/src/main.ts
index 066830176864449a04f0ba173614afdd06bde81e..28da08b5bff46969e78ef8737919a9ee7b67fc3e 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -11,5 +11,9 @@ if (environment.production) {
   enableProdMode();
 }
 
-platformBrowserDynamic().bootstrapModule(AppModule)
-  .catch(err => console.log(err));
+platformBrowserDynamic().bootstrapModule(AppModule).then(() => {
+  if ('serviceWorker' in navigator && environment.production) {
+    console.log("Registering ngsw-worker.js...");
+    navigator.serviceWorker.register('ngsw-worker.js');
+  }
+}).catch(err => console.log(err));