From bc170974b31db80bbab95e027561b9d1fee942ff Mon Sep 17 00:00:00 2001
From: blavenie <benoit.lavenier@e-is.pro>
Date: Wed, 26 Oct 2016 16:23:35 +0200
Subject: [PATCH] - Features tour: add helptip  on peers

---
 app/config.json                               | 15 +++-
 www/i18n/locale-en.json                       |  6 +-
 www/i18n/locale-fr-FR.json                    |  6 +-
 www/js/config.js                              |  4 +-
 www/js/controllers/help-controllers.js        | 84 ++++++++++++++++++-
 www/js/controllers/settings-controllers.js    |  2 +-
 www/js/services/network-services.js           |  5 +-
 www/js/services/utils-services.js             | 25 ++----
 .../es/js/controllers/market-controllers.js   |  2 +-
 .../es/js/controllers/registry-controllers.js |  2 +-
 www/plugins/es/js/services/user-services.js   |  4 +-
 www/templates/common/popover_helptip.html     |  3 +-
 www/templates/currency/tabs/view_network.html |  7 +-
 www/templates/currency/view_currency.html     |  3 +-
 14 files changed, 129 insertions(+), 39 deletions(-)

diff --git a/app/config.json b/app/config.json
index 23dee6b73..02c3f9ae6 100644
--- a/app/config.json
+++ b/app/config.json
@@ -11,7 +11,8 @@
     "useRelative": true,
     "initPhase": false,
     "helptip": {
-      "enable": true
+      "enable": true,
+      "installDocUrl": "https://github.com/duniter/duniter/blob/master/doc/install-a-node.md"
     },
     "node": {
       "host": "test-net.duniter.fr",
@@ -40,7 +41,11 @@
     "useRelative": true,
     "initPhase": false,
     "helptip": {
-      "enable": false
+      "enable": false,
+      "installDocUrl": {
+        "fr-FR": "http://www.le-sou.org/devenir-noeud/",
+        "en": "https://github.com/duniter/duniter/blob/master/doc/install-a-node.md"
+      }
     },
     "node": {
       "host": "duniter.le-sou.org",
@@ -69,7 +74,11 @@
     "useRelative": true,
     "initPhase": false,
     "helptip": {
-      "enable": false
+      "enable": true,
+      "installDocUrl": {
+        "fr-FR": "http://www.le-sou.org/devenir-noeud/",
+        "en": "https://github.com/duniter/duniter/blob/master/doc/install-a-node.md"
+      }
     },
     "node": {
       "host": "duniter.le-sou.org",
diff --git a/www/i18n/locale-en.json b/www/i18n/locale-en.json
index 4946e8087..8497747fa 100644
--- a/www/i18n/locale-en.json
+++ b/www/i18n/locale-en.json
@@ -30,7 +30,7 @@
     "BTN_CONTINUE": "Continue",
     "BTN_UNDERSTOOD": "I understood",
     "BTN_OPTIONS": "Options",
-    "BTN_HELP_TOUR": "Visite guidée",
+    "BTN_HELP_TOUR": "Features tour",
     "BTN_HELP_TOUR_SCREEN": "Discover this screen",
     "NO_ACCOUNT_QUESTION": "No a member ? Register now !",
     "SEARCH_NO_RESULT": "No result found",
@@ -372,6 +372,10 @@
       "CURRENCY_CHANGE_UNIT": "(TODO translate)<br/>Ce bouton permet de <b>changer d'unité</b>, pour visualiser les montants <b>directement en {{currency|capitalize}}</b> (plutôt qu'en &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;).",
       "CURRENCY_CHANGE_UNIT_TO_RELATIVE": "(TODO translate)<br/>Ce bouton permet de <b>changer d'unité</b>, pour visualiser les montants en &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;, c'est à dire relativement au Dividende Universel (le montant co-produit par chaque membre).",
       "CURRENCY_RULES": "(TODO translate)<br/>Les <b>règles</b> de la monnaie fixent son fonctionnement <b>exact et prévisible</b>.<br/><br/>Véritable ADN de la monnaie, elles rendent son code monétaire <b>lisible et transparent</b>.",
+      "CURRENCY_BLOCKCHAIN": "(TODO translate)<br/>Toutes les opérations de la monnaie sont enregistrées dans un grand livre de compte <b>public et infalsifiable</b>, appellé aussi <b>chaine de blocs</b> (<em>BlockChain</em> en anglais).",
+      "CURRENCY_PEERS": "(TODO translate)<br/>Les <b>noeuds</b> visibles ici correspondent aux <b>ordinateurs qui actualisent et contrôlent</b> la chaine de blocs.<br/><br/>Plus il y a de noeuds, plus la monnaie à une gestion <b>décentralisée</b> et digne de confiance.",
+      "CURRENCY_PEERS_BLOCK_NUMBER": "(TODO translate)<br/>Ce <b>numéro</b> indique le <b>dernier bloc validé</b> pour ce noeud (dernière page écrite dans le grand livre de comptes).<br/><br/>La couleur verte indique que ce bloc est également validé par <b>la plupart des autres noeuds</b>.",
+      "CURRENCY_PEERS_PARTICIPATE": "(TODO translate)<br/><b>Chaque membre</b>, équipé d'un ordinateur avec Internet, <b>peut participer en ajoutant un noeud</b>. Il suffit d'<b>installer le logiciel Duniter</b> (libre de droit et gratuit). <a target=\"_new\" href=\"{{installDocUrl}}\">Voir le manuel d'installation &gt;&gt;</a>.",
       "MENU_BTN_ACCOUNT": "(TODO translate)<br/><b>{{'ACCOUNT.TITLE'|translate}}</b> permet l'accès au solde de votre compte et à l'historique de vos transactions.",
       "MENU_BTN_ACCOUNT_MEMBER": "(TODO translate)<br/>Consultez ici l'état de votre compte, l'historique de vos transactions et vos certifications.",
       "WALLET_CERTIFICATIONS": "(TODO translate)<br/>En cliquant ici, consultez le détail de vos certifications (données et émises).",
diff --git a/www/i18n/locale-fr-FR.json b/www/i18n/locale-fr-FR.json
index 606362129..863a030e5 100644
--- a/www/i18n/locale-fr-FR.json
+++ b/www/i18n/locale-fr-FR.json
@@ -372,11 +372,15 @@
       "CURRENCY_CHANGE_UNIT": "Ce bouton permet de <b>changer d'unité</b>, pour visualiser les montants <b>directement en {{currency|capitalize}}</b> (plutôt qu'en &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;).",
       "CURRENCY_CHANGE_UNIT_TO_RELATIVE": "Ce bouton permet de <b>changer d'unité</b>, pour visualiser les montants en &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;, c'est à dire relativement au Dividende Universel (le montant co-produit par chaque membre).",
       "CURRENCY_RULES": "Les <b>règles</b> de la monnaie fixent son fonctionnement <b>exact et prévisible</b>.<br/><br/>Véritable ADN de la monnaie, elles rendent son code monétaire <b>lisible et transparent</b>.",
+      "CURRENCY_BLOCKCHAIN": "Toutes les opérations de la monnaie sont enregistrées dans un grand livre de compte <b>public et infalsifiable</b>, appellé aussi <b>chaine de blocs</b> (<em>BlockChain</em> en anglais).",
+      "CURRENCY_PEERS": "Les <b>noeuds</b> visibles ici correspondent aux <b>ordinateurs qui actualisent et contrôlent</b> la chaine de blocs.<br/><br/>Plus il y a de noeuds, plus la monnaie à une gestion <b>décentralisée</b> et digne de confiance.",
+      "CURRENCY_PEERS_BLOCK_NUMBER": "Ce <b>numéro</b> indique le <b>dernier bloc validé</b> pour ce noeud (dernière page écrite dans le grand livre de comptes).<br/><br/>La couleur verte indique que ce bloc est également validé par <b>la plupart des autres noeuds</b>.",
+      "CURRENCY_PEERS_PARTICIPATE": "<b>Chaque membre</b>, équipé d'un ordinateur avec Internet, <b>peut participer en ajoutant un noeud</b>. Il suffit d'<b>installer le logiciel Duniter</b> (libre de droit et gratuit). <a target=\"_new\" href=\"{{installDocUrl}}\">Voir le manuel d'installation &gt;&gt;</a>.",
       "MENU_BTN_ACCOUNT": "<b>{{'ACCOUNT.TITLE'|translate}}</b> permet l'accès au solde de votre compte et à l'historique de vos transactions.",
       "MENU_BTN_ACCOUNT_MEMBER": "Consultez ici l'état de votre compte, l'historique de vos transactions et vos certifications.",
       "WALLET_CERTIFICATIONS": "En cliquant ici, consultez le détail de vos certifications (reçues et émises).",
       "WALLET_BALANCE": "Le <b>solde</b> de votre compte s'affiche ici.",
-      "WALLET_BALANCE_RELATIVE": "{{'HELP.TIP.WALLET_BALANCE'|translate}}<br/><br/>L'unité utilisée (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifie que le montant en {{currency|capitalize}} a été divisé par le <b>Dividende Universel</b> (DU) co-créé par chaque membre.<br/>Actuellement 1 DU vaut {{currentUD}} {{currency|capitalize}}s.",
+      "WALLET_BALANCE_RELATIVE": "{{'HELP.TIP.WALLET_BALANCE'|translate}}<br/><br/>L'unité utilisée (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifie que le montant en {{currency|capitalize}} a été divisé par le <b>Dividende Universel</b> (DU) co-créé par chaque membre.<br/><br/>Actuellement 1 DU vaut {{currentUD|formatInteger}} {{currency|capitalize}}s.",
       "WALLET_BALANCE_CHANGE_UNIT": "Vous pourrez <b>changer l'unité</b> d'affichage des montants dans les <b><i class=\"icon ion-android-settings\"></i>&nbsp;Paramètres</b>.<br/><br/>Par exemple pour visualiser les montants <b>directement en {{currency|capitalize}}</b>, plutôt qu'en unité relative.",
       "WALLET_SEND": "Effectuer un paiement en quelques clics",
       "WALLET_SEND_NO_MONEY": "Effectuer un paiement en quelques clics.<br/>(Votre solde ne le permet pas encore)",
diff --git a/www/js/config.js b/www/js/config.js
index 0f5346538..c3e143ad8 100644
--- a/www/js/config.js
+++ b/www/js/config.js
@@ -36,8 +36,8 @@ angular.module("cesium.config", [])
 		}
 	},
 	"version": "0.4.3",
-	"build": "2016-10-25T15:15:13.492Z",
+	"build": "2016-10-26T08:32:03.211Z",
 	"newIssueUrl": "https://github.com/duniter/cesium/issues/new?labels=bug"
 })
 
-;
+;
\ No newline at end of file
diff --git a/www/js/controllers/help-controllers.js b/www/js/controllers/help-controllers.js
index 87bc4595e..36b0db54d 100644
--- a/www/js/controllers/help-controllers.js
+++ b/www/js/controllers/help-controllers.js
@@ -79,8 +79,8 @@ function HelpModalController($scope, $timeout, $anchorScroll, csSettings, parame
 /* ----------------------------
 *  Help Tip
 * ---------------------------- */
-function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDelegate, $timeout, $q, screenmatch,
-                            UIUtils, csSettings, csCurrency, Device, Wallet, BMA) {
+function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDelegate, $timeout, $q, $translate, $sce,
+                           UIUtils, csConfig, csSettings, csCurrency, Device, Wallet) {
 
   $scope.tour = false; // Is a tour or a helptip ?
 
@@ -134,6 +134,10 @@ function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDe
     return UIUtils.popover.helptip(id, options);
   };
 
+  $scope.showHelpModal = function(helpAnchor) {
+    Modals.showHelp({anchor: helpAnchor});
+  };
+
   $scope.startHelpTour = function() {
     $scope.tour = true;
 
@@ -227,6 +231,18 @@ function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDe
    */
   $scope.startCurrencyTour = function(startIndex, hasNext) {
 
+    var showNetworkViewIsNeed  = function() {
+      if ($state.is('app.currency_view')) {
+        // Select the second tabs
+        $timeout(function () {
+          var tabs = $window.document.querySelectorAll('ion-tabs .tabs a');
+          if (tabs && tabs.length == 2) {
+            angular.element(tabs[1]).triggerHandler('click');
+          }
+        }, 100);
+      }
+    };
+
     var contentParams;
 
     var steps = [
@@ -255,7 +271,8 @@ function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDe
                 icon: {
                   position: 'center'
                 }
-              }
+              },
+              timeout: 1200 // need for Firefox
             });
           });
       },
@@ -316,6 +333,65 @@ function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDe
             content: 'HELP.TIP.CURRENCY_RULES',
             icon: {
               position: 'bottom-left'
+            }
+          }
+        });
+      },
+
+      function() {
+        showNetworkViewIsNeed();
+        return $scope.showHelpTip('helptip-currency-peers', {
+          bindings: {
+            content: 'HELP.TIP.CURRENCY_BLOCKCHAIN',
+            icon: {
+              position: 'center',
+              glyph: 'ion-information-circled'
+            }
+          }
+        });
+      },
+
+      function() {
+        showNetworkViewIsNeed();
+        return $scope.showHelpTip('helptip-currency-peer-0', {
+          bindings: {
+            content: 'HELP.TIP.CURRENCY_PEERS',
+            icon: {
+              position: 'center'
+            }
+          },
+          timeout: 1000,
+          retry: 20
+        });
+      },
+
+
+      function() {
+        showNetworkViewIsNeed();
+        return $scope.showHelpTip('helptip-currency-peer-0-block', {
+          bindings: {
+            content: 'HELP.TIP.CURRENCY_PEERS_BLOCK_NUMBER',
+            icon: {
+              position: 'right'
+            }
+          }
+        });
+      },
+
+      function() {
+        showNetworkViewIsNeed();
+        var locale = csSettings.data.locale.id;
+        return $scope.showHelpTip('helptip-currency-peers', {
+          bindings: {
+            content: 'HELP.TIP.CURRENCY_PEERS_PARTICIPATE',
+            contentParams: {
+              installDocUrl: (csConfig.helptip && csConfig.helptip.installDocUrl) ?
+                (csConfig.helptip.installDocUrl[locale] ? csConfig.helptip.installDocUrl[locale] : csConfig.helptip.installDocUrl) :
+                'http://duniter.org'
+            },
+            icon: {
+              position: 'center',
+              glyph: 'ion-information-circled'
             },
             hasNext: hasNext
           }
@@ -622,7 +698,7 @@ function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDe
       },
 
       function() {
-        if (UIUtils.screen.isSmall()) {
+        if ($state.is('app.wallet_view_cert')) {
           // Select the second tabs
           $timeout(function() {
             var tabs = $window.document.querySelectorAll('ion-tabs .tabs a');
diff --git a/www/js/controllers/settings-controllers.js b/www/js/controllers/settings-controllers.js
index 5d26a8ba1..15fbfc08c 100644
--- a/www/js/controllers/settings-controllers.js
+++ b/www/js/controllers/settings-controllers.js
@@ -206,7 +206,7 @@ function SettingsController($scope, $q, $ionicPopup, $timeout, $translate, csHtt
   // Show help tip (show only not already shown)
   $scope.showHelpTip = function(index, tour) {
     if (!$scope.isLogin() && !tour) return;
-    var index = angular.isDefined(index) ? index : csSettings.data.helptip.settings;
+    index = angular.isDefined(index) ? index : csSettings.data.helptip.settings;
     if (index < 0) return;
     if (index === 0) index = 1; // skip first step
 
diff --git a/www/js/services/network-services.js b/www/js/services/network-services.js
index f923e8550..f1f118565 100644
--- a/www/js/services/network-services.js
+++ b/www/js/services/network-services.js
@@ -215,7 +215,6 @@ angular.module('cesium.network.services', ['ngResource', 'ngApi', 'cesium.bma.se
         });
         // Listen for new peer
         data.bma.websocket.peer().on('peer', function(peer) {
-          // Waiting block propagation
           $timeout(function() {
             var promise = processPeer(peer, true);
             if (promise) promise.then(function() {
@@ -225,8 +224,8 @@ angular.module('cesium.network.services', ['ngResource', 'ngApi', 'cesium.bma.se
               }
               sortPeers();
               api.data.raise.changed(data); // send event to plugins
-            }, 500);
-          })
+            }, 1000); // Waiting block propagation
+          });
         });
       },
 
diff --git a/www/js/services/utils-services.js b/www/js/services/utils-services.js
index ad445747a..0ea2ddc78 100644
--- a/www/js/services/utils-services.js
+++ b/www/js/services/utils-services.js
@@ -320,32 +320,25 @@ angular.module('cesium.utils.services', ['ngResource'])
       $timeout(function() { // This is need for Firefox
         popover.show(event)
         .then(function() {
+          var element;
           // Auto select text
           if (options.autoselect) {
-            var inputs = document.querySelectorAll(options.autoselect);
-            for (var i=0; i<inputs.length; i++) {
-              var input = inputs[i];
+            element = document.querySelectorAll(options.autoselect)[0];
+            if (element) {
               if ($window.getSelection && !$window.getSelection().toString()) {
-                input.setSelectionRange(0, input.value.length);
-                input.focus();
+                element.setSelectionRange(0, element.value.length);
+                element.focus();
               }
               else {
-                input.focus();
+                element.focus();
               }
-              break;
             }
           }
           else {
             // Auto focus on a element
             if (options.autofocus) {
-              var elements = document.querySelectorAll(options.autofocus);
-              for (var i=0; i<elements.length; i++) {
-                var element = elements[i];
-                if (element !== null) {
-                  element.focus();
-                  break;
-                }
-              }
+              element = document.querySelectorAll(options.autofocus)[0];
+              if (element) element.focus();
             }
           }
 
@@ -393,7 +386,7 @@ angular.module('cesium.utils.services', ['ngResource'])
 
           // Execute action on hidden popover
           options.scope.$on('popover.hidden', function() {
-            if (popover.options.autoremove) {
+            if (popover.options && popover.options.autoremove) {
               _cleanup(popover);
             }
           });
diff --git a/www/plugins/es/js/controllers/market-controllers.js b/www/plugins/es/js/controllers/market-controllers.js
index 3f7289b28..50af465c5 100644
--- a/www/plugins/es/js/controllers/market-controllers.js
+++ b/www/plugins/es/js/controllers/market-controllers.js
@@ -434,7 +434,7 @@ function ESMarketRecordViewController($scope, $anchorScroll, $ionicPopover, $sta
       return esUser.profile.fillAvatars([{pubkey: $scope.formData.issuer}])
         .then(function(idties) {
           return idties[0];
-        })
+        });
     })
     .then(function (member) {
       $scope.issuer = member;
diff --git a/www/plugins/es/js/controllers/registry-controllers.js b/www/plugins/es/js/controllers/registry-controllers.js
index f19323e11..87d1f6e55 100644
--- a/www/plugins/es/js/controllers/registry-controllers.js
+++ b/www/plugins/es/js/controllers/registry-controllers.js
@@ -422,7 +422,7 @@ function ESRegistryRecordViewController($scope, $state, $q, $timeout, $ionicPopo
           return esUser.profile.fillAvatars([{pubkey: $scope.formData.issuer}])
             .then(function(idties) {
               return idties[0];
-            })
+            });
         })
         .then(function(member){
           $scope.issuer = member;
diff --git a/www/plugins/es/js/services/user-services.js b/www/plugins/es/js/services/user-services.js
index eacf9ee83..33ab592e9 100644
--- a/www/plugins/es/js/services/user-services.js
+++ b/www/plugins/es/js/services/user-services.js
@@ -394,8 +394,8 @@ angular.module('cesium.es.user.services', ['cesium.services', 'cesium.es.http.se
     Device.ready().then(function() {
 
       if (csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.askEnable && // if config ask enable
-        csSettings.data.plugins.es && !csSettings.data.plugins.es.enable // AND user settings has disable plugin
-        && csSettings.data.plugins.es.askEnable // AND user has not yet answer 'NO'
+        csSettings.data.plugins.es && !csSettings.data.plugins.es.enable && // AND user settings has disable plugin
+        csSettings.data.plugins.es.askEnable // AND user has not yet answer 'NO'
       ) {
         UIUtils.alert.confirm('ES_SETTINGS.CONFIRM.ASK_ENABLE', 'ES_SETTINGS.CONFIRM.ASK_ENABLE_TITLE', {
           cancelText: 'COMMON.BTN_NO',
diff --git a/www/templates/common/popover_helptip.html b/www/templates/common/popover_helptip.html
index 45a6ff8fe..1e7014c67 100644
--- a/www/templates/common/popover_helptip.html
+++ b/www/templates/common/popover_helptip.html
@@ -16,7 +16,8 @@
     </p>
 
     <p class="padding light">
-      <span ng-bind-html="content | translate:contentParams"></span>
+      <ng-bind-html ng-bind-html="content | translate:contentParams"></ng-bind-html>
+      <ng-bind-html ng-bind-html="trustContent"></ng-bind-html>
     </p>
 
     <!-- buttons (if helptip) -->
diff --git a/www/templates/currency/tabs/view_network.html b/www/templates/currency/tabs/view_network.html
index 6e877bf94..c40276ece 100644
--- a/www/templates/currency/tabs/view_network.html
+++ b/www/templates/currency/tabs/view_network.html
@@ -1,7 +1,8 @@
 
     <div class="list">
 
-        <div class="item item-divider item-icon-right">
+        <div id="helptip-currency-peers"
+             class="item item-divider item-icon-right">
             {{'PEER.PEERS'|translate}}
             <ion-spinner class="icon" icon="android" ng-if="loadingPeers"></ion-spinner>
             <a class="icon ion-loop gray hidden-xs hidden-sm" ng-if="!loadingPeers" ng-click="refresh()">
@@ -10,6 +11,7 @@
 
         <a class="peer-item item item-icon-left"
            ng-repeat="peer in peers track by peer.server"
+           id="helptip-currency-peer-{{$index}}"
            ng-class="{ assertive: !peer.online, balanced: (peer.online && peer.hasMainConsensusBlock), energized: (peer.online && !peer.hasMainConsensusBlock)}"
            ui-sref="app.view_peer({server: peer.server})">
             <i class="icon ion-android-globe"></i>
@@ -26,7 +28,8 @@
                 <h4 class="hidden-sm hidden-xs hidden-md gray">v{{peer.version}}</h4>
               </div>
               <div class="col col-20 no-padding">
-                <span class="badge" ng-class="{ 'badge-balanced': peer.hasMainConsensusBlock, 'badge-energized': peer.hasConsensusBlock }">{{peer.currentNumber}}</span>
+                <span id="helptip-currency-peer-{{$index}}-block"
+                  class="badge" ng-class="{ 'badge-balanced': peer.hasMainConsensusBlock, 'badge-energized': peer.hasConsensusBlock }">{{peer.currentNumber}}</span>
               </div>
             </div>
         </a>
diff --git a/www/templates/currency/view_currency.html b/www/templates/currency/view_currency.html
index 7591115be..b03a22a23 100644
--- a/www/templates/currency/view_currency.html
+++ b/www/templates/currency/view_currency.html
@@ -22,7 +22,8 @@
                 </ion-nav-view>
             </ion-tab>
 
-            <ion-tab title="{{'CURRENCY.VIEW.TAB_NETWORK'|translate}}" icon="ion-network" >
+            <ion-tab id="helptip-currency-tab-peers"
+                     title="{{'CURRENCY.VIEW.TAB_NETWORK'|translate}}" icon="ion-network" >
                 <ion-nav-view name="network-tab">
                   <ion-content>
                       <ng-include src="'templates/currency/tabs/view_network.html'"/>
-- 
GitLab