diff --git a/assets/config/.eslintrc b/assets/config/.eslintrc
index 408fea27..3e358608 100644
--- a/assets/config/.eslintrc
+++ b/assets/config/.eslintrc
@@ -1,309 +1,313 @@
 {
     "parser": "babel-eslint",
 
     "parserOptions": {
         "ecmaVersion": 2017,
         "ecmaFeatures": {
             "experimentalObjectRestSpread": true,
             "jsx": true
         },
         "sourceType": "module",
         "allowImportExportEverywhere": true
     },
 
     "env": {
         "es6": true,
         "node": true,
         "cypress/globals": true
     },
 
     "plugins": [
         "import",
         "node",
         "promise",
         "standard",
         "cypress",
         "chai-friendly"
     ],
 
     "globals": {
         "document": false,
         "navigator": false,
         "window": false,
         "$": false,
         "jQuery": false,
         "history": false,
         "localStorage": false,
         "sessionStorage": false,
         "Urls": false,
         "hljs": false,
         "Waypoint": false,
         "swh": false,
         "fetch": false,
         "__STATIC__": false,
         "Image": false,
         "Cookies": false,
         "nb": false,
         "MathJax": false
     },
 
     "rules": {
         "accessor-pairs": "error",
         "arrow-spacing": ["error", {
             "before": true,
             "after": true
         }],
         "block-spacing": ["error", "always"],
         "brace-style": ["error", "1tbs", {
             "allowSingleLine": true
         }],
         "camelcase": ["error", {
             "properties": "never"
         }],
         "comma-dangle": ["error", {
             "arrays": "never",
             "objects": "never",
             "imports": "never",
             "exports": "never",
             "functions": "never"
         }],
         "comma-spacing": ["error", {
             "before": false,
             "after": true
         }],
         "comma-style": ["error", "last"],
         "constructor-super": "error",
         "curly": ["error", "multi-line"],
         "dot-location": ["error", "property"],
         "eol-last": "error",
         "eqeqeq": ["error", "always", {
             "null": "ignore"
         }],
         "func-call-spacing": ["error", "never"],
         "generator-star-spacing": ["error", {
             "before": true,
             "after": true
         }],
         "handle-callback-err": ["error", "^(err|error)$"],
         "indent": ["error", 2, {
             "SwitchCase": 1,
             "VariableDeclarator": 1,
             "outerIIFEBody": 1,
             "MemberExpression": "off",
             "FunctionDeclaration": {
                 "parameters": "first",
                 "body": 1
             },
             "FunctionExpression": {
                 "parameters": "first",
                 "body": 1
             },
             "CallExpression": {
                 "arguments": "first"
             },
             "ArrayExpression": "first",
             "ObjectExpression": "first",
             "ImportDeclaration": "first",
             "flatTernaryExpressions": false,
             "ignoreComments": false
         }],
         "key-spacing": ["error", {
             "beforeColon": false,
             "afterColon": true
         }],
         "keyword-spacing": ["error", {
             "before": true,
             "after": true
         }],
         "new-cap": ["error", {
             "newIsCap": true,
             "capIsNew": false
         }],
         "new-parens": "error",
         "no-array-constructor": "error",
         "no-caller": "error",
         "no-class-assign": "error",
         "no-compare-neg-zero": "error",
         "no-cond-assign": "error",
         "no-const-assign": "error",
         "no-constant-condition": ["error", {
             "checkLoops": false
         }],
         "no-control-regex": "error",
         "no-debugger": "error",
         "no-delete-var": "error",
         "no-dupe-args": "error",
         "no-dupe-class-members": "error",
         "no-dupe-keys": "error",
         "no-duplicate-case": "error",
         "no-empty-character-class": "error",
         "no-empty-pattern": "error",
         "no-eval": "error",
         "no-ex-assign": "error",
         "no-extend-native": "error",
         "no-extra-bind": "error",
         "no-extra-boolean-cast": "error",
         "no-extra-parens": ["error", "functions"],
         "no-fallthrough": "error",
         "no-floating-decimal": "error",
         "no-func-assign": "error",
         "no-global-assign": "error",
         "no-implied-eval": "error",
         "no-inner-declarations": ["error", "functions"],
         "no-invalid-regexp": "error",
         "no-irregular-whitespace": "error",
         "no-iterator": "error",
         "no-label-var": "error",
         "no-labels": ["error", {
             "allowLoop": false,
             "allowSwitch": false
         }],
         "no-lone-blocks": "error",
         "no-mixed-operators": ["error", {
             "groups": [
                 ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
                 ["&&", "||"],
                 ["in", "instanceof"]
             ],
             "allowSamePrecedence": true
         }],
         "no-mixed-spaces-and-tabs": "error",
         "no-multi-spaces": "error",
         "no-multi-str": "error",
         "no-multiple-empty-lines": ["error", {
             "max": 1,
             "maxEOF": 0
         }],
         "no-negated-in-lhs": "error",
         "no-new": 0,
         "no-new-func": "error",
         "no-new-object": "error",
         "no-new-require": "error",
         "no-new-symbol": "error",
         "no-new-wrappers": "error",
         "no-obj-calls": "error",
         "no-octal": "error",
         "no-octal-escape": "error",
         "no-path-concat": "error",
         "no-proto": "error",
         "no-redeclare": "error",
         "no-regex-spaces": "error",
         "no-return-assign": ["error", "except-parens"],
         "no-return-await": "error",
         "no-self-assign": "error",
         "no-self-compare": "error",
         "no-sequences": "error",
         "no-shadow-restricted-names": "error",
         "no-sparse-arrays": "error",
         "no-tabs": "error",
         "no-template-curly-in-string": "error",
         "no-this-before-super": "error",
         "no-throw-literal": "error",
         "no-trailing-spaces": "error",
         "no-undef": "error",
         "no-undef-init": "error",
         "no-unexpected-multiline": "error",
         "no-unmodified-loop-condition": "error",
         "no-unneeded-ternary": ["error", {
             "defaultAssignment": false
         }],
         "no-unreachable": "error",
         "no-unsafe-finally": "error",
         "no-unsafe-negation": "error",
         "no-unused-expressions": 0,
         "no-unused-vars": ["error", {
             "vars": "all",
             "args": "none",
             "ignoreRestSiblings": true
         }],
         "no-use-before-define": ["error", {
             "functions": false,
             "classes": false,
             "variables": false
         }],
         "no-useless-call": "error",
         "no-useless-computed-key": "error",
         "no-useless-constructor": "error",
         "no-useless-escape": "error",
         "no-useless-rename": "error",
         "no-useless-return": "error",
         "no-whitespace-before-property": "error",
         "no-with": "error",
         "object-property-newline": ["error", {
             "allowMultiplePropertiesPerLine": true
         }],
         "one-var": ["error", {
             "initialized": "never"
         }],
         "operator-linebreak": ["error", "after", {
             "overrides": {
                 "?": "before",
                 ":": "before"
             }
         }],
         "padded-blocks": ["off", {
             "blocks": "never",
             "switches": "never",
             "classes": "never"
         }],
         "prefer-promise-reject-errors": "error",
+        "prefer-const": ["error", {
+            "destructuring": "any",
+            "ignoreReadBeforeAssign": false
+        }],
         "quotes": ["error", "single", {
             "avoidEscape": true,
             "allowTemplateLiterals": true
         }],
         "rest-spread-spacing": ["error", "never"],
         "semi": ["error", "always"],
         "semi-spacing": ["error", {
             "before": false,
             "after": true
         }],
         "space-before-blocks": ["error", "always"],
         "space-before-function-paren": ["error", "never"],
         "space-in-parens": ["error", "never"],
         "space-infix-ops": "error",
         "space-unary-ops": ["error", {
             "words": true,
             "nonwords": false
         }],
         "spaced-comment": ["error", "always", {
             "line": {
                 "markers": ["*package", "!", "/", ",", "="]
             },
             "block": {
                 "balanced": true,
                 "markers": ["*package", "!", ",", ":", "::", "flow-include"],
                 "exceptions": ["*"]
             }
         }],
         "symbol-description": "error",
         "template-curly-spacing": ["error", "never"],
         "template-tag-spacing": ["error", "never"],
         "unicode-bom": ["error", "never"],
         "use-isnan": "error",
         "valid-typeof": ["error", {
             "requireStringLiterals": true
         }],
         "wrap-iife": ["error", "any", {
             "functionPrototypeMethods": true
         }],
         "yield-star-spacing": ["error", "both"],
         "yoda": ["error", "never"],
 
         "import/export": "off",
         "import/first": "error",
         "import/no-duplicates": "error",
         "import/no-webpack-loader-syntax": "off",
 
         "node/no-deprecated-api": "error",
         "node/process-exit-as-throw": "error",
 
         "promise/param-names": "error",
 
         "standard/array-bracket-even-spacing": ["error", "either"],
         "standard/computed-property-even-spacing": ["error", "even"],
         "standard/no-callback-literal": "error",
         "standard/object-curly-even-spacing": ["error", "either"],
         "chai-friendly/no-unused-expressions": 2,
         "object-curly-spacing": ["error", "never"]
     }
 }
\ No newline at end of file
diff --git a/assets/config/webpack-plugins/dump-highlightjs-languages-data-plugin.js b/assets/config/webpack-plugins/dump-highlightjs-languages-data-plugin.js
index 13da23cb..df7f60f4 100644
--- a/assets/config/webpack-plugins/dump-highlightjs-languages-data-plugin.js
+++ b/assets/config/webpack-plugins/dump-highlightjs-languages-data-plugin.js
@@ -1,46 +1,46 @@
 /**
  * Copyright (C) 2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 'use strict';
 
 const fs = require('fs');
 const path = require('path');
 const hljs = require('highlight.js');
 var stringify = require('json-stable-stringify');
 
 // Simple webpack plugin to dump JSON data related to the set of
 // programming languages supported by the highlightjs library.
 // The JSON file is saved into swh-web static folder and will
 // be consumed by the backend django application.
 
 class DumpHighlightjsLanguagesDataPlugin {
 
   apply(compiler) {
     compiler.hooks.done.tap('DumpHighlightjsLanguagesDataPlugin', statsObj => {
       const outputPath = statsObj.compilation.compiler.outputPath;
       const hljsDataFile = path.join(outputPath, 'json/highlightjs-languages.json');
       const languages = hljs.listLanguages();
       const hljsLanguagesData = {'languages': languages};
       const languageAliases = {};
-      for (let language of languages) {
+      for (const language of languages) {
         const languageData = hljs.getLanguage(language);
         if (!languageData.hasOwnProperty('aliases')) {
           continue;
         }
-        for (let alias of languageData.aliases) {
+        for (const alias of languageData.aliases) {
           languageAliases[alias] = language;
           languageAliases[alias.toLowerCase()] = language;
         }
       }
       hljsLanguagesData['languages_aliases'] = languageAliases;
       fs.writeFileSync(hljsDataFile, stringify(hljsLanguagesData, {space: 4}));
     });
   }
 
 };
 
 module.exports = DumpHighlightjsLanguagesDataPlugin;
diff --git a/assets/config/webpack-plugins/fix-swh-source-maps-webpack-plugin.js b/assets/config/webpack-plugins/fix-swh-source-maps-webpack-plugin.js
index 62b68c87..2f6d8733 100644
--- a/assets/config/webpack-plugins/fix-swh-source-maps-webpack-plugin.js
+++ b/assets/config/webpack-plugins/fix-swh-source-maps-webpack-plugin.js
@@ -1,48 +1,48 @@
 /**
  * Copyright (C) 2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 'use strict';
 
 // This plugin modifies the generated asset sourcemaps on the fly
 // in order to be successfully loaded by Firefox.
 // The following error is reported without that:
 // Source map error: TypeError: Invalid URL: webpack://swh.[name]/...
 
 class FixSwhSourceMapsPlugin {
 
   constructor() {
     this.sourceMapRegexp = /\.map($|\?)/i;
   }
 
   apply(compiler) {
     compiler.hooks.compilation.tap('FixSwhSourceMapsPlugin', compilation => {
       const {Compilation, sources} = require('webpack');
       compilation.hooks.processAssets.tap(
         {
           name: 'FixSwhSourceMapsPlugin',
           stage: Compilation.PROCESS_ASSETS_STAGE_ANALYSE
         },
         () => {
           Object.keys(compilation.assets).filter(key => {
             return this.sourceMapRegexp.test(key);
           }).forEach(key => {
             let bundleName = key.replace(/^js\//, '');
             bundleName = bundleName.replace(/^css\//, '');
-            let pos = bundleName.indexOf('.');
+            const pos = bundleName.indexOf('.');
             bundleName = bundleName.slice(0, pos);
-            let asset = compilation.assets[key];
-            let source = asset.source().replace(/swh.\[name\]/g, 'swh.' + bundleName);
+            const asset = compilation.assets[key];
+            const source = asset.source().replace(/swh.\[name\]/g, 'swh.' + bundleName);
             compilation.updateAsset(key, new sources.RawSource(source));
           });
         }
       );
     });
   }
 
 };
 
 module.exports = FixSwhSourceMapsPlugin;
diff --git a/assets/config/webpack-plugins/generate-weblabels-webpack-plugin/index.js b/assets/config/webpack-plugins/generate-weblabels-webpack-plugin/index.js
index 4230497f..3e2482a0 100644
--- a/assets/config/webpack-plugins/generate-weblabels-webpack-plugin/index.js
+++ b/assets/config/webpack-plugins/generate-weblabels-webpack-plugin/index.js
@@ -1,409 +1,409 @@
 /**
  * Copyright (C) 2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 // This is a plugin for webpack >= 4 enabling to generate a Web Labels page intended
 // to be consume by the LibreJS Firefox plugin (https://www.gnu.org/software/librejs/).
 // See README.md for its complete documentation.
 
 const ejs = require('ejs');
 const fs = require('fs');
 const log = require('webpack-log');
 const path = require('path');
 const schema = require('./plugin-options-schema.json');
 const spdxParse = require('spdx-expression-parse');
 const spdxLicensesMapping = require('./spdx-licenses-mapping');
 const {validate} = require('schema-utils');
 
 const pluginName = 'GenerateWebLabelsPlugin';
 
 class GenerateWebLabelsPlugin {
 
   constructor(opts) {
     // check that provided options match JSON schema
     validate(schema, opts, pluginName);
     this.options = opts || {};
     this.weblabelsDirName = this.options['outputDir'] || 'jssources';
     this.outputType = this.options['outputType'] || 'html';
     // source file extension handled by webpack and compiled to js
     this.srcExts = ['js', 'ts', 'coffee', 'lua'];
     this.srcExtsRegexp = new RegExp('^.*.(' + this.srcExts.join('|') + ')$');
     this.chunkIdToName = {};
     this.chunkNameToJsAsset = {};
     this.chunkJsAssetToSrcFiles = {};
     this.srcIdsInChunkJsAsset = {};
     this.packageJsonCache = {};
     this.packageLicenseFile = {};
     this.exclude = [];
     this.copiedFiles = new Set();
     this.logger = log({name: pluginName});
     // populate module prefix patterns to exclude
     if (Array.isArray(this.options['exclude'])) {
       this.options['exclude'].forEach(toExclude => {
         if (!toExclude.startsWith('.')) {
           this.exclude.push('./' + path.join('node_modules', toExclude));
         } else {
           this.exclude.push(toExclude);
         }
       });
     }
   }
 
   apply(compiler) {
     compiler.hooks.done.tap(pluginName, statsObj => {
 
       // get the stats object in JSON format
-      let stats = statsObj.toJson();
+      const stats = statsObj.toJson();
       this.stats = stats;
 
       // set output folder
       this.weblabelsOutputDir = path.join(stats.outputPath, this.weblabelsDirName);
       this.recursiveMkdir(this.weblabelsOutputDir);
 
       stats.assets.forEach(asset => {
         for (let i = 0; i < asset.chunks.length; ++i) {
           this.chunkIdToName[asset.chunks[i]] = asset.chunkNames[i];
         }
       });
 
       // map each generated webpack chunk to its js asset
       Object.keys(stats.assetsByChunkName).forEach((chunkName, i) => {
         if (Array.isArray(stats.assetsByChunkName[chunkName])) {
-          for (let asset of stats.assetsByChunkName[chunkName]) {
+          for (const asset of stats.assetsByChunkName[chunkName]) {
             if (asset.endsWith('.js')) {
               this.chunkNameToJsAsset[chunkName] = asset;
               this.chunkNameToJsAsset[i] = asset;
               break;
             }
           }
         } else if (stats.assetsByChunkName[chunkName].endsWith('.js')) {
           this.chunkNameToJsAsset[chunkName] = stats.assetsByChunkName[chunkName];
           this.chunkNameToJsAsset[i] = stats.assetsByChunkName[chunkName];
         }
       });
 
       // iterate on all bundled webpack modules
       stats.modules.forEach(mod => {
 
         let srcFilePath = mod.name;
 
         // do not process non js related modules
         if (!this.srcExtsRegexp.test(srcFilePath)) {
           return;
         }
 
         // do not process modules unrelated to a source file
         if (!srcFilePath.startsWith('./')) {
           return;
         }
 
         // do not process modules in the exclusion list
-        for (let toExclude of this.exclude) {
+        for (const toExclude of this.exclude) {
           if (srcFilePath.startsWith(toExclude)) {
             return;
           }
         }
 
         // remove webpack loader call if any
-        let loaderEndPos = srcFilePath.indexOf('!');
+        const loaderEndPos = srcFilePath.indexOf('!');
         if (loaderEndPos !== -1) {
           srcFilePath = srcFilePath.slice(loaderEndPos + 1);
         }
 
         // iterate on all chunks containing the module
         mod.chunks.forEach(chunk => {
-          let chunkName = this.chunkIdToName[chunk];
-          let chunkJsAsset = stats.publicPath + this.chunkNameToJsAsset[chunkName];
+          const chunkName = this.chunkIdToName[chunk];
+          const chunkJsAsset = stats.publicPath + this.chunkNameToJsAsset[chunkName];
 
           // init the chunk to source files mapping if needed
           if (!this.chunkJsAssetToSrcFiles.hasOwnProperty(chunkJsAsset)) {
             this.chunkJsAssetToSrcFiles[chunkJsAsset] = [];
             this.srcIdsInChunkJsAsset[chunkJsAsset] = new Set();
           }
           // check if the source file needs to be replaces
           if (this.options['srcReplace'] && this.options['srcReplace'].hasOwnProperty(srcFilePath)) {
             srcFilePath = this.options['srcReplace'][srcFilePath];
           }
 
           // init source file metadata
-          let srcFileData = {'id': this.cleanupPath(srcFilePath)};
+          const srcFileData = {'id': this.cleanupPath(srcFilePath)};
 
           // find and parse the corresponding package.json file
           let packageJsonPath;
-          let nodeModule = srcFilePath.startsWith('./node_modules/');
+          const nodeModule = srcFilePath.startsWith('./node_modules/');
           if (nodeModule) {
             packageJsonPath = this.findPackageJsonPath(srcFilePath);
           } else {
             packageJsonPath = './package.json';
           }
-          let packageJson = this.parsePackageJson(packageJsonPath);
+          const packageJson = this.parsePackageJson(packageJsonPath);
 
           // extract license information, overriding it if needed
           let licenseOverridden = false;
           let licenseFilePath;
           if (this.options['licenseOverride']) {
-            for (let srcFilePrefixKey of Object.keys(this.options['licenseOverride'])) {
+            for (const srcFilePrefixKey of Object.keys(this.options['licenseOverride'])) {
               let srcFilePrefix = srcFilePrefixKey;
               if (!srcFilePrefixKey.startsWith('.')) {
                 srcFilePrefix = './' + path.join('node_modules', srcFilePrefixKey);
               }
               if (srcFilePath.startsWith(srcFilePrefix)) {
-                let spdxLicenseExpression = this.options['licenseOverride'][srcFilePrefixKey]['spdxLicenseExpression'];
+                const spdxLicenseExpression = this.options['licenseOverride'][srcFilePrefixKey]['spdxLicenseExpression'];
                 licenseFilePath = this.options['licenseOverride'][srcFilePrefixKey]['licenseFilePath'];
-                let parsedSpdxLicenses = this.parseSpdxLicenseExpression(spdxLicenseExpression, `file ${srcFilePath}`);
+                const parsedSpdxLicenses = this.parseSpdxLicenseExpression(spdxLicenseExpression, `file ${srcFilePath}`);
                 srcFileData['licenses'] = this.spdxToWebLabelsLicenses(parsedSpdxLicenses);
                 licenseOverridden = true;
                 break;
               }
             }
           }
 
           if (!licenseOverridden) {
             srcFileData['licenses'] = this.extractLicenseInformation(packageJson);
-            let licenseDir = path.join(...packageJsonPath.split('/').slice(0, -1));
+            const licenseDir = path.join(...packageJsonPath.split('/').slice(0, -1));
             licenseFilePath = this.findLicenseFile(licenseDir);
           }
 
           // copy original license file and get its url
-          let licenseCopyUrl = this.copyLicenseFile(licenseFilePath);
+          const licenseCopyUrl = this.copyLicenseFile(licenseFilePath);
           srcFileData['licenses'].forEach(license => {
             license['copy_url'] = licenseCopyUrl;
           });
 
           // generate url for downloading non-minified source code
           srcFileData['src_url'] = stats.publicPath + path.join(this.weblabelsDirName, srcFileData['id']);
 
           // add source file metadata to the webpack chunk
           this.addSrcFileDataToJsChunkAsset(chunkJsAsset, srcFileData);
           // copy non-minified source to output folder
           this.copyFileToOutputPath(srcFilePath);
         });
       });
 
       // process additional scripts if needed
       if (this.options['additionalScripts']) {
         for (let script of Object.keys(this.options['additionalScripts'])) {
-          let scriptFilesData = this.options['additionalScripts'][script];
+          const scriptFilesData = this.options['additionalScripts'][script];
           if (script.indexOf('://') === -1 && !script.startsWith('/')) {
             script = stats.publicPath + script;
           }
           this.chunkJsAssetToSrcFiles[script] = [];
           this.srcIdsInChunkJsAsset[script] = new Set();
-          for (let scriptSrc of scriptFilesData) {
-            let scriptSrcData = {'id': scriptSrc['id']};
-            let licenceFilePath = scriptSrc['licenseFilePath'];
-            let parsedSpdxLicenses = this.parseSpdxLicenseExpression(scriptSrc['spdxLicenseExpression'],
-                                                                     `file ${scriptSrc['path']}`);
+          for (const scriptSrc of scriptFilesData) {
+            const scriptSrcData = {'id': scriptSrc['id']};
+            const licenceFilePath = scriptSrc['licenseFilePath'];
+            const parsedSpdxLicenses = this.parseSpdxLicenseExpression(scriptSrc['spdxLicenseExpression'],
+                                                                       `file ${scriptSrc['path']}`);
             scriptSrcData['licenses'] = this.spdxToWebLabelsLicenses(parsedSpdxLicenses);
             if (licenceFilePath.indexOf('://') === -1 && !licenceFilePath.startsWith('/')) {
-              let licenseCopyUrl = this.copyLicenseFile(licenceFilePath);
+              const licenseCopyUrl = this.copyLicenseFile(licenceFilePath);
               scriptSrcData['licenses'].forEach(license => {
                 license['copy_url'] = licenseCopyUrl;
               });
             } else {
               scriptSrcData['licenses'].forEach(license => {
                 license['copy_url'] = licenceFilePath;
               });
             }
             if (scriptSrc['path'].indexOf('://') === -1 && !scriptSrc['path'].startsWith('/')) {
               scriptSrcData['src_url'] = stats.publicPath + path.join(this.weblabelsDirName, scriptSrc['id']);
             } else {
               scriptSrcData['src_url'] = scriptSrc['path'];
             }
             this.addSrcFileDataToJsChunkAsset(script, scriptSrcData);
             this.copyFileToOutputPath(scriptSrc['path']);
           }
         }
       }
 
-      for (let srcFiles of Object.values(this.chunkJsAssetToSrcFiles)) {
+      for (const srcFiles of Object.values(this.chunkJsAssetToSrcFiles)) {
         srcFiles.sort((a, b) => a.id.localeCompare(b.id));
       }
 
       if (this.outputType === 'json') {
         // generate the jslicenses.json file
-        let weblabelsData = JSON.stringify(this.chunkJsAssetToSrcFiles);
-        let weblabelsJsonFile = path.join(this.weblabelsOutputDir, 'jslicenses.json');
+        const weblabelsData = JSON.stringify(this.chunkJsAssetToSrcFiles);
+        const weblabelsJsonFile = path.join(this.weblabelsOutputDir, 'jslicenses.json');
         fs.writeFileSync(weblabelsJsonFile, weblabelsData);
       } else {
         // generate the jslicenses.html file
-        let weblabelsPageFile = path.join(this.weblabelsOutputDir, 'jslicenses.html');
+        const weblabelsPageFile = path.join(this.weblabelsOutputDir, 'jslicenses.html');
         ejs.renderFile(path.join(__dirname, 'jslicenses.ejs'),
                        {'jslicenses_data': this.chunkJsAssetToSrcFiles},
                        {'rmWhitespace': true},
                        (e, str) => {
                          fs.writeFileSync(weblabelsPageFile, str);
                        });
       }
     });
   }
 
   addSrcFileDataToJsChunkAsset(chunkJsAsset, srcFileData) {
     if (!this.srcIdsInChunkJsAsset[chunkJsAsset].has(srcFileData['id'])) {
       this.chunkJsAssetToSrcFiles[chunkJsAsset].push(srcFileData);
       this.srcIdsInChunkJsAsset[chunkJsAsset].add(srcFileData['id']);
     }
   }
 
   cleanupPath(moduleFilePath) {
     return moduleFilePath.replace(/^[./]*node_modules\//, '').replace(/^.\//, '');
   }
 
   findPackageJsonPath(srcFilePath) {
-    let pathSplit = srcFilePath.split('/');
+    const pathSplit = srcFilePath.split('/');
     let packageJsonPath;
     for (let i = 3; i < pathSplit.length; ++i) {
       packageJsonPath = path.join(...pathSplit.slice(0, i), 'package.json');
       if (fs.existsSync(packageJsonPath)) {
         break;
       }
     }
     return packageJsonPath;
   }
 
   findLicenseFile(packageJsonDir) {
     if (!this.packageLicenseFile.hasOwnProperty(packageJsonDir)) {
       let foundLicenseFile;
       fs.readdirSync(packageJsonDir).forEach(file => {
         if (foundLicenseFile) {
           return;
         }
         if (file.toLowerCase().startsWith('license')) {
           foundLicenseFile = path.join(packageJsonDir, file);
         }
       });
       this.packageLicenseFile[packageJsonDir] = foundLicenseFile;
     }
     return this.packageLicenseFile[packageJsonDir];
   }
 
   copyLicenseFile(licenseFilePath) {
     let licenseCopyPath = '';
     if (licenseFilePath && fs.existsSync(licenseFilePath)) {
       let ext = '';
       // add a .txt extension in order to serve license file with text/plain
       // content type to client browsers
       if (licenseFilePath.toLowerCase().indexOf('license.') === -1) {
         ext = '.txt';
       }
       this.copyFileToOutputPath(licenseFilePath, ext);
       licenseFilePath = this.cleanupPath(licenseFilePath);
       licenseCopyPath = this.stats.publicPath + path.join(this.weblabelsDirName, licenseFilePath + ext);
     }
     return licenseCopyPath;
   }
 
   parsePackageJson(packageJsonPath) {
     if (!this.packageJsonCache.hasOwnProperty(packageJsonPath)) {
-      let packageJsonStr = fs.readFileSync(packageJsonPath).toString('utf8');
+      const packageJsonStr = fs.readFileSync(packageJsonPath).toString('utf8');
       this.packageJsonCache[packageJsonPath] = JSON.parse(packageJsonStr);
     }
     return this.packageJsonCache[packageJsonPath];
   }
 
   parseSpdxLicenseExpression(spdxLicenseExpression, context) {
     let parsedLicense;
     try {
       parsedLicense = spdxParse(spdxLicenseExpression);
       if (spdxLicenseExpression.indexOf('AND') !== -1) {
         this.logger.warn(`The SPDX license expression '${spdxLicenseExpression}' associated to ${context} ` +
                          'contains an AND operator, this is currently not properly handled and erroneous ' +
                          'licenses information may be provided to LibreJS');
       }
     } catch (e) {
       this.logger.warn(`Unable to parse the SPDX license expression '${spdxLicenseExpression}' associated to ${context}.`);
       this.logger.warn('Some generated JavaScript assets may be blocked by LibreJS due to missing license information.');
       parsedLicense = {'license': spdxLicenseExpression};
     }
     return parsedLicense;
   }
 
   spdxToWebLabelsLicense(spdxLicenceId) {
     for (let i = 0; i < spdxLicensesMapping.length; ++i) {
       if (spdxLicensesMapping[i]['spdx_ids'].indexOf(spdxLicenceId) !== -1) {
-        let licenseData = Object.assign({}, spdxLicensesMapping[i]);
+        const licenseData = Object.assign({}, spdxLicensesMapping[i]);
         delete licenseData['spdx_ids'];
         delete licenseData['magnet_link'];
         licenseData['copy_url'] = '';
         return licenseData;
       }
     }
     this.logger.warn(`Unable to associate the SPDX license identifier '${spdxLicenceId}' to a LibreJS supported license.`);
     this.logger.warn('Some generated JavaScript assets may be blocked by LibreJS due to missing license information.');
     return {
       'name': spdxLicenceId,
       'url': '',
       'copy_url': ''
     };
   }
 
   spdxToWebLabelsLicenses(spdxLicenses) {
     // This method simply extracts all referenced licenses in the SPDX expression
     // regardless of their combinations.
     // TODO: Handle licenses combination properly once LibreJS has a spec for it.
     let ret = [];
     if (spdxLicenses.hasOwnProperty('license')) {
       ret.push(this.spdxToWebLabelsLicense(spdxLicenses['license']));
     } else if (spdxLicenses.hasOwnProperty('left')) {
       if (spdxLicenses['left'].hasOwnProperty('license')) {
-        let licenseData = this.spdxToWebLabelsLicense(spdxLicenses['left']['license']);
+        const licenseData = this.spdxToWebLabelsLicense(spdxLicenses['left']['license']);
         ret.push(licenseData);
       } else {
         ret = ret.concat(this.spdxToWebLabelsLicenses(spdxLicenses['left']));
       }
       ret = ret.concat(this.spdxToWebLabelsLicenses(spdxLicenses['right']));
     }
     return ret;
   }
 
   extractLicenseInformation(packageJson) {
     let spdxLicenseExpression;
     if (packageJson.hasOwnProperty('license')) {
       spdxLicenseExpression = packageJson['license'];
     } else if (packageJson.hasOwnProperty('licenses')) {
       // for node packages using deprecated licenses property
-      let licenses = packageJson['licenses'];
+      const licenses = packageJson['licenses'];
       if (Array.isArray(licenses)) {
-        let l = [];
+        const l = [];
         licenses.forEach(license => {
           l.push(license['type']);
         });
         spdxLicenseExpression = l.join(' OR ');
       } else {
         spdxLicenseExpression = licenses['type'];
       }
     }
-    let parsedSpdxLicenses = this.parseSpdxLicenseExpression(spdxLicenseExpression,
-                                                             `module ${packageJson['name']}`);
+    const parsedSpdxLicenses = this.parseSpdxLicenseExpression(spdxLicenseExpression,
+                                                               `module ${packageJson['name']}`);
     return this.spdxToWebLabelsLicenses(parsedSpdxLicenses);
   }
 
   copyFileToOutputPath(srcFilePath, ext = '') {
     if (this.copiedFiles.has(srcFilePath) || srcFilePath.indexOf('://') !== -1 ||
         !fs.existsSync(srcFilePath)) {
       return;
     }
     let destPath = this.cleanupPath(srcFilePath);
-    let destDir = path.join(this.weblabelsOutputDir, ...destPath.split('/').slice(0, -1));
+    const destDir = path.join(this.weblabelsOutputDir, ...destPath.split('/').slice(0, -1));
     this.recursiveMkdir(destDir);
     destPath = path.join(this.weblabelsOutputDir, destPath + ext);
     fs.copyFileSync(srcFilePath, destPath);
     this.copiedFiles.add(srcFilePath);
   }
 
   recursiveMkdir(destPath) {
-    let destPathSplit = destPath.split('/');
+    const destPathSplit = destPath.split('/');
     for (let i = 1; i < destPathSplit.length; ++i) {
-      let currentPath = path.join('/', ...destPathSplit.slice(0, i + 1));
+      const currentPath = path.join('/', ...destPathSplit.slice(0, i + 1));
       if (!fs.existsSync(currentPath)) {
         fs.mkdirSync(currentPath);
       }
     }
   }
 
 };
 
 module.exports = GenerateWebLabelsPlugin;
diff --git a/assets/config/webpack.config.development.js b/assets/config/webpack.config.development.js
index 7eb0b585..8660afd5 100644
--- a/assets/config/webpack.config.development.js
+++ b/assets/config/webpack.config.development.js
@@ -1,482 +1,482 @@
 /**
  * Copyright (C) 2018-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 // webpack configuration for compiling static assets in development mode
 
 // import required node modules and webpack plugins
 const chalk = require('chalk');
 const fs = require('fs');
 const path = require('path');
 const webpack = require('webpack');
 const BundleTracker = require('webpack-bundle-tracker');
 const RobotstxtPlugin = require('robotstxt-webpack-plugin');
 const CleanWebpackPlugin = require('clean-webpack-plugin').CleanWebpackPlugin;
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const FixSwhSourceMapsPlugin = require('./webpack-plugins/fix-swh-source-maps-webpack-plugin');
 const CopyWebpackPlugin = require('copy-webpack-plugin');
 const GenerateWebLabelsPlugin = require('./webpack-plugins/generate-weblabels-webpack-plugin');
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
 const DumpHighlightjsLanguagesDataPlugin = require('./webpack-plugins/dump-highlightjs-languages-data-plugin');
 
 // are we running webpack-dev-server ?
 const isDevServer = process.argv.find(v => v.includes('serve')) !== undefined;
 // webpack-dev-server configuration
 const devServerPort = 3000;
 const devServerPublicPath = 'http://localhost:' + devServerPort + '/static/';
 // set publicPath according if we are using webpack-dev-server to serve
 // our assets or not
 const publicPath = isDevServer ? devServerPublicPath : '/static/';
 
 const nodeModules = path.resolve(__dirname, '../../node_modules/');
 
 // collect all bundles we want to produce with webpack
 var bundles = {};
 const bundlesDir = path.join(__dirname, '../src/bundles');
 fs.readdirSync(bundlesDir).forEach(file => {
   bundles[file] = ['bundles/' + file + '/index.js'];
   // workaround for https://github.com/webpack/webpack-dev-server/issues/2692
   if (isDevServer) {
     bundles[file].unshift(`webpack-dev-server/client/index.js?http://localhost:${devServerPort}`);
   }
 });
 
 // common loaders for css related assets (css, sass)
-let cssLoaders = [
+const cssLoaders = [
   MiniCssExtractPlugin.loader,
   {
     loader: 'cache-loader'
   },
   {
     loader: 'css-loader',
     options: {
       sourceMap: !isDevServer
     }
   },
   {
     loader: 'postcss-loader',
     options: {
       sourceMap: !isDevServer,
       postcssOptions: {
         plugins: [
           // lint swh-web stylesheets
           ['stylelint', {
             'config': {
               'extends': 'stylelint-config-standard',
               'rules': {
                 'indentation': 4,
                 'font-family-no-missing-generic-family-keyword': null,
                 'no-descending-specificity': null
               },
               'ignoreFiles': ['node_modules/**/*.css',
                               'assets/src/thirdparty/**/*.css']
             }
           }],
           // automatically add vendor prefixes to css rules
           'autoprefixer',
           'postcss-normalize',
           ['postcss-reporter', {
             clearReportedMessages: true
           }]
         ]
       }
     }
   }
 ];
 
 // webpack development configuration
 module.exports = {
   // use caching to speedup incremental builds
   cache: {
     type: 'memory'
   },
   // set mode to development
   mode: 'development',
   // workaround for https://github.com/webpack/webpack-dev-server/issues/2758
   target: process.env.NODE_ENV === 'development' ? 'web' : 'browserslist',
   // use eval source maps when using webpack-dev-server for quick debugging,
   // otherwise generate source map files (more expensive)
   devtool: isDevServer ? 'eval' : 'source-map',
   // webpack-dev-server configuration
   devServer: {
     clientLogLevel: 'warning',
     host: '0.0.0.0',
     port: devServerPort,
     publicPath: devServerPublicPath,
     // enable to serve static assets not managed by webpack
     contentBase: path.resolve('./'),
     // we do not use hot reloading here (as a framework like React needs to be used in order to fully benefit from that feature)
     // and prefer to fully reload the frontend application in the browser instead
     hot: false,
     inline: true,
     historyApiFallback: true,
     headers: {
       'Access-Control-Allow-Origin': '*'
     },
     compress: true,
     stats: 'errors-only',
     overlay: {
       warnings: true,
       errors: true
     },
     // workaround for https://github.com/webpack/webpack-dev-server/issues/2692
     injectClient: false,
     transportMode: 'ws'
   },
   // set entries to the bundles we want to produce
   entry: bundles,
   // assets output configuration
   output: {
     path: path.resolve('./static/'),
     filename: 'js/[name].[contenthash].js',
     chunkFilename: 'js/[name].[contenthash].js',
     publicPath: publicPath,
     // each bundle will be compiled as a umd module with its own namespace
     // in order to easily use them in django templates
     library: ['swh', '[name]'],
     libraryTarget: 'umd'
   },
   // module resolving configuration
   resolve: {
     // alias pdfjs to its minified version
     alias: {
       'pdfjs-dist': 'pdfjs-dist/build/pdf.min.js'
     },
     // configure base paths for resolving modules with webpack
     modules: [
       'node_modules',
       path.resolve(__dirname, '../src')
     ]
   },
   stats: 'errors-warnings',
   // module import configuration
   module: {
     rules: [
       {
       // Preprocess all js files with eslint for consistent code style
       // and avoid bad js development practices.
         enforce: 'pre',
         test: /\.js$/,
         exclude: /node_modules/,
         use: [{
           loader: 'eslint-loader',
           options: {
             configFile: path.join(__dirname, '.eslintrc'),
             ignorePath: path.join(__dirname, '.eslintignore'),
             cache: true,
             emitWarning: true
           }
         }]
       },
       {
       // Use babel-loader in order to use es6 syntax in js files
       // but also advanced js features like async/await syntax.
       // All code get transpiled to es5 in order to be executed
       // in a large majority of browsers.
         test: /\.js$/,
         exclude: /node_modules/,
         use: [
           {
             loader: 'cache-loader'
           },
           {
             loader: 'babel-loader',
             options: {
               presets: [
                 // use env babel presets to benefit from es6 syntax
                 ['@babel/preset-env', {
                   // Do not transform es6 module syntax to another module type
                   // in order to benefit from dead code elimination (aka tree shaking)
                   // when running webpack in production mode
                   'loose': true,
                   'modules': false
                 }]
               ],
               plugins: [
               // use babel transform-runtime plugin in order to use aync/await syntax
                 ['@babel/plugin-transform-runtime', {
                   'regenerator': true
                 }],
                 // use other babel plugins to benefit from advanced js features (es2017)
                 '@babel/plugin-syntax-dynamic-import'
               ],
               env: {
                 test: {
                   plugins: ['istanbul']
                 }
               }
             }
           }]
       },
       {
         test: /\.ejs$/,
         use: [{
           loader: 'ejs-compiled-loader',
           options: {
             htmlmin: true,
             htmlminOptions: {
               removeComments: true
             }
           }
         }]
       },
       // expose jquery to the global context as $ and jQuery when importing it
       {
         test: require.resolve('jquery'),
         use: [{
           loader: 'expose-loader',
           options: {
             exposes: [
               {
                 globalName: '$',
                 override: true
               },
               {
                 globalName: 'jQuery',
                 override: true
               }
             ]
           }
         }]
       },
       // expose highlightjs to the global context as hljs when importing it
       {
         test: require.resolve('highlight.js'),
         use: [{
           loader: 'expose-loader',
           options: {
             exposes: {
               globalName: 'hljs',
               override: true
             }
           }
         }]
       },
       {
         test: require.resolve('js-cookie'),
         use: [{
           loader: 'expose-loader',
           options: {
             exposes: {
               globalName: 'Cookies',
               override: true
             }
           }
         }]
       },
       // css import configuration:
       //  - first process it with postcss
       //  - then extract it to a dedicated file associated to each bundle
       {
         test: /\.css$/,
         use: cssLoaders
       },
       // sass import configuration:
       //  - generate css with sass-loader
       //  - process it with postcss
       //  - then extract it to a dedicated file associated to each bundle
       {
         test: /\.scss$/,
         use: cssLoaders.concat([
           {
             loader: 'sass-loader',
             options: {
               sourceMap: !isDevServer
             }
           }
         ])
       },
       // web fonts import configuration
       {
         test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
         use: [{
           loader: 'file-loader',
           options: {
             name: '[name].[ext]',
             outputPath: 'fonts/'
           }
         }]
       }, {
         test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
         use: [{
           loader: 'file-loader',
           options: {
             name: '[name].[ext]',
             outputPath: 'fonts/'
           }
         }]
       }, {
         test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
         use: [{
           loader: 'file-loader',
           options: {
             name: '[name].[ext]',
             outputPath: 'fonts/'
           }
         }]
       }, {
         test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
         use: [{
           loader: 'file-loader',
           options: {
             name: '[name].[ext]',
             outputPath: 'fonts/'
           }
         }]
       }, {
         test: /\.otf(\?v=\d+\.\d+\.\d+)?$/,
         use: [{
           loader: 'file-loader',
           options: {
             name: '[name].[ext]',
             outputPath: 'fonts/'
           }
         }]
       }, {
         test: /\.png$/,
         use: [{
           loader: 'file-loader',
           options: {
             name: '[name].[ext]',
             outputPath: 'img/thirdParty/'
           }
         }]
       }
     ],
     // tell webpack to not parse already minified files to speedup build process
     noParse: [path.resolve(nodeModules, 'pdfjs-dist/build/pdf.min.js'),
               path.resolve(nodeModules, 'mathjax/es5/tex-mml-chtml.js')]
   },
   // webpack plugins
   plugins: [
     // cleanup previously generated assets
     new CleanWebpackPlugin({
       cleanOnceBeforeBuildPatterns: ['**/*', '!xml', '!xml/*', '!img', '!img/*',
                                      '!img/logos', '!img/logos/*', '!img/icons',
                                      '!img/icons/*', '!json', '!json/*']
     }),
     // needed in order to use django_webpack_loader
     new BundleTracker({
       filename: './static/webpack-stats.json'
     }),
     // for generating the robots.txt file
     new RobotstxtPlugin({
       policy: [{
         userAgent: '*',
         disallow: '/api/'
       }]
     }),
     // for extracting all stylesheets in separate css files
     new MiniCssExtractPlugin({
       filename: 'css/[name].[contenthash].css',
       chunkFilename: 'css/[name].[contenthash].css'
     }),
     // fix generated asset sourcemaps to workaround a Firefox issue
     new FixSwhSourceMapsPlugin(),
     // define some global variables accessible from js code
     new webpack.DefinePlugin({
       __STATIC__: JSON.stringify(publicPath)
     }),
     // needed in order to use bootstrap 4.x
     new webpack.ProvidePlugin({
       Popper: ['popper.js', 'default'],
       Alert: 'exports-loader?Alert!bootstrap/js/dist/alert',
       Button: 'exports-loader?Button!bootstrap/js/dist/button',
       Carousel: 'exports-loader?Carousel!bootstrap/js/dist/carousel',
       Collapse: 'exports-loader?Collapse!bootstrap/js/dist/collapse',
       Dropdown: 'exports-loader?Dropdown!bootstrap/js/dist/dropdown',
       Modal: 'exports-loader?Modal!bootstrap/js/dist/modal',
       Popover: 'exports-loader?Popover!bootstrap/js/dist/popover',
       Scrollspy: 'exports-loader?Scrollspy!bootstrap/js/dist/scrollspy',
       Tab: 'exports-loader?Tab!bootstrap/js/dist/tab',
       Tooltip: 'exports-loader?Tooltip!bootstrap/js/dist/tooltip',
       Util: 'exports-loader?Util!bootstrap/js/dist/util'
     }),
     // needed in order to use pdf.js
     new webpack.IgnorePlugin(/^\.\/pdf.worker.js$/),
     new CopyWebpackPlugin({
       patterns: [
         {
           from: path.resolve(nodeModules, 'pdfjs-dist/build/pdf.worker.min.js'),
           to: path.resolve(__dirname, '../../static/js/')
         },
         {
           from: path.resolve(nodeModules, 'mathjax/es5/output/chtml/fonts/woff-v2/**'),
           to: path.resolve(__dirname, '../../static/fonts/[name].[ext]')
         }
       ]
     }),
     new GenerateWebLabelsPlugin({
       outputType: 'json',
       exclude: ['mini-css-extract-plugin',
                 'bootstrap-loader'],
       srcReplace: {
         './node_modules/pdfjs-dist/build/pdf.min.js':
         './node_modules/pdfjs-dist/build/pdf.js',
         './node_modules/admin-lte/dist/js/adminlte.min.js':
         './node_modules/admin-lte/dist/js/adminlte.js'
       },
       licenseOverride: {
         './assets/src/thirdparty/jquery.tabSlideOut/jquery.tabSlideOut.js': {
           'spdxLicenseExpression': 'GPL-3.0',
           'licenseFilePath': './assets/src/thirdparty/jquery.tabSlideOut/LICENSE'
         }
       },
       additionalScripts: Object.assign(
         {
           'js/pdf.worker.min.js': [
             {
               'id': 'pdfjs-dist/build/pdf.worker.js',
               'path': './node_modules/pdfjs-dist/build/pdf.worker.js',
               'spdxLicenseExpression': 'Apache-2.0',
               'licenseFilePath': './node_modules/pdfjs-dist/LICENSE'
 
             }
           ],
           '/jsreverse/': [
             {
               'id': 'jsreverse',
               'path': '/jsreverse/',
               'spdxLicenseExpression': 'AGPL-3.0-or-later',
               'licenseFilePath': './LICENSE'
             }
           ],
           'https://piwik.inria.fr/matomo.js': [
             {
               'id': 'matomo.js',
               'path': 'https://github.com/matomo-org/matomo/blob/master/js/piwik.js',
               'spdxLicenseExpression': 'BSD-3-Clause',
               'licenseFilePath': 'https://github.com/matomo-org/matomo/blob/master/js/LICENSE.txt'
             }
           ]
         }
       )
     }),
     new ProgressBarPlugin({
       format: chalk.cyan.bold('webpack build of swh-web assets') + ' [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)',
       width: 50
     }),
     new DumpHighlightjsLanguagesDataPlugin()
   ],
   // webpack optimizations
   optimization: {
     // ensure the vendors bundle gets emitted in a single chunk
     splitChunks: {
       cacheGroups: {
         defaultVendors: {
           test: 'vendors',
           chunks: 'all',
           name: 'vendors',
           enforce: true
         }
       }
     }
   },
   // disable webpack warnings about bundle sizes
   performance: {
     hints: false
   }
 };
diff --git a/assets/src/bundles/admin/deposit.js b/assets/src/bundles/admin/deposit.js
index b9f162fc..acb1208a 100644
--- a/assets/src/bundles/admin/deposit.js
+++ b/assets/src/bundles/admin/deposit.js
@@ -1,163 +1,163 @@
 /**
  * Copyright (C) 2018-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 function genSwhLink(data, type) {
   if (type === 'display') {
     if (data && data.startsWith('swh')) {
       const browseUrl = Urls.browse_swhid(data);
       const formattedSWHID = data.replace(/;/g, ';<br/>');
       return `<a href="${browseUrl}">${formattedSWHID}</a>`;
     }
   }
   return data;
 }
 
 export function initDepositAdmin() {
   let depositsTable;
   $(document).ready(() => {
     $.fn.dataTable.ext.errMode = 'none';
     depositsTable = $('#swh-admin-deposit-list')
       .on('error.dt', (e, settings, techNote, message) => {
         $('#swh-admin-deposit-list-error').text(message);
       })
       .DataTable({
         serverSide: true,
         processing: true,
         // let's define the order of table options display
         // f: (f)ilter
         // l: (l)ength changing
         // r: p(r)ocessing
         // t: (t)able
         // i: (i)nfo
         // p: (p)agination
         // see https://datatables.net/examples/basic_init/dom.html
         dom: '<<"d-flex justify-content-between align-items-center"f' +
              '<"#list-exclude">l>rt<"bottom"ip>>',
         // div#list-exclude is a custom filter added next to dataTable
         // initialization below through js dom manipulation, see
         // https://datatables.net/examples/advanced_init/dom_toolbar.html
         ajax: {
           url: Urls.admin_deposit_list(),
           data: d => {
             d.excludePattern = $('#swh-admin-deposit-list-exclude-filter').val();
           }
         },
         columns: [
           {
             data: 'id',
             name: 'id'
           },
           {
             data: 'swhid_context',
             name: 'swhid_context',
             render: (data, type, row) => {
               if (data && type === 'display') {
-                let originPattern = ';origin=';
-                let originPatternIdx = data.indexOf(originPattern);
+                const originPattern = ';origin=';
+                const originPatternIdx = data.indexOf(originPattern);
                 if (originPatternIdx !== -1) {
                   let originUrl = data.slice(originPatternIdx + originPattern.length);
-                  let nextSepPattern = ';';
-                  let nextSepPatternIdx = originUrl.indexOf(nextSepPattern);
+                  const nextSepPattern = ';';
+                  const nextSepPatternIdx = originUrl.indexOf(nextSepPattern);
                   if (nextSepPatternIdx !== -1) { /* Remove extra context */
                     originUrl = originUrl.slice(0, nextSepPatternIdx);
                   }
                   return `<a href="${originUrl}">${originUrl}</a>`;
                 }
               }
               return data;
             }
           },
           {
             data: 'reception_date',
             name: 'reception_date',
             render: (data, type, row) => {
               if (type === 'display') {
-                let date = new Date(data);
+                const date = new Date(data);
                 return date.toLocaleString();
               }
               return data;
             }
           },
           {
             data: 'status',
             name: 'status'
           },
           {
             data: 'status_detail',
             name: 'status_detail',
             render: (data, type, row) => {
               if (type === 'display' && data) {
                 let text = data;
                 if (typeof data === 'object') {
                   text = JSON.stringify(data, null, 4);
                 }
                 return `<div style="width: 200px; white-space: pre; overflow-x: auto;">${text}</div>`;
               }
               return data;
             },
             orderable: false,
             visible: false
           },
           {
             data: 'swhid',
             name: 'swhid',
             render: (data, type, row) => {
               return genSwhLink(data, type);
             },
             orderable: false,
             visible: false
           },
           {
             data: 'swhid_context',
             name: 'swhid_context',
             render: (data, type, row) => {
               return genSwhLink(data, type);
             },
             orderable: false,
             visible: false
           }
         ],
         scrollX: true,
         scrollY: '50vh',
         scrollCollapse: true,
         order: [[0, 'desc']]
       });
 
     // Some more customization is needed on the table
     $('div#list-exclude').html(`<div id="swh-admin-deposit-list-exclude-wrapper">
     <div id="swh-admin-deposit-list-exclude-div-wrapper" class="dataTables_filter">
       <label>
         Exclude:<input id="swh-admin-deposit-list-exclude-filter"
                        type="search"
                        value="check-deposit"
                        class="form-control form-control-sm"
                        placeholder="exclude-pattern" aria-controls="swh-admin-deposit-list">
           </input>
       </label>
     </div>
   </div>
 `);
     // Adding exclusion pattern update behavior, when typing, update search
     $('#swh-admin-deposit-list-exclude-filter').keyup(function() {
       depositsTable.draw();
     });
     // at last draw the table
     depositsTable.draw();
   });
 
   $('a.toggle-col').on('click', function(e) {
     e.preventDefault();
     var column = depositsTable.column($(this).attr('data-column'));
     column.visible(!column.visible());
     if (column.visible()) {
       $(this).removeClass('col-hidden');
     } else {
       $(this).addClass('col-hidden');
     }
   });
 
 }
diff --git a/assets/src/bundles/admin/origin-save.js b/assets/src/bundles/admin/origin-save.js
index 87c1dff5..aabc4aa2 100644
--- a/assets/src/bundles/admin/origin-save.js
+++ b/assets/src/bundles/admin/origin-save.js
@@ -1,353 +1,353 @@
 /**
  * Copyright (C) 2018-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {handleFetchError, csrfPost, htmlAlert} from 'utils/functions';
 import {swhSpinnerSrc} from 'utils/constants';
 
 let authorizedOriginTable;
 let unauthorizedOriginTable;
 let pendingSaveRequestsTable;
 let acceptedSaveRequestsTable;
 let rejectedSaveRequestsTable;
 
 function enableRowSelection(tableSel) {
   $(`${tableSel} tbody`).on('click', 'tr', function() {
     if ($(this).hasClass('selected')) {
       $(this).removeClass('selected');
       $(tableSel).closest('.tab-pane').find('.swh-action-need-selection').prop('disabled', true);
     } else {
       $(`${tableSel} tr.selected`).removeClass('selected');
       $(this).addClass('selected');
       $(tableSel).closest('.tab-pane').find('.swh-action-need-selection').prop('disabled', false);
     }
   });
 }
 
 export function initOriginSaveAdmin() {
   $(document).ready(() => {
 
     $.fn.dataTable.ext.errMode = 'throw';
 
     authorizedOriginTable = $('#swh-authorized-origin-urls').DataTable({
       serverSide: true,
       ajax: Urls.admin_origin_save_authorized_urls_list(),
       columns: [{data: 'url', name: 'url'}],
       scrollY: '50vh',
       scrollCollapse: true,
       info: false
     });
     enableRowSelection('#swh-authorized-origin-urls');
     swh.webapp.addJumpToPagePopoverToDataTable(authorizedOriginTable);
 
     unauthorizedOriginTable = $('#swh-unauthorized-origin-urls').DataTable({
       serverSide: true,
       ajax: Urls.admin_origin_save_unauthorized_urls_list(),
       columns: [{data: 'url', name: 'url'}],
       scrollY: '50vh',
       scrollCollapse: true,
       info: false
     });
     enableRowSelection('#swh-unauthorized-origin-urls');
     swh.webapp.addJumpToPagePopoverToDataTable(unauthorizedOriginTable);
 
-    let columnsData = [
+    const columnsData = [
       {
         data: 'id',
         name: 'id',
         visible: false,
         searchable: false
       },
       {
         data: 'save_request_date',
         name: 'request_date',
         render: (data, type, row) => {
           if (type === 'display') {
-            let date = new Date(data);
+            const date = new Date(data);
             return date.toLocaleString();
           }
           return data;
         }
       },
       {
         data: 'visit_type',
         name: 'visit_type'
       },
       {
         data: 'origin_url',
         name: 'origin_url',
         render: (data, type, row) => {
           if (type === 'display') {
             let html = '';
             const sanitizedURL = $.fn.dataTable.render.text().display(data);
             if (row.save_task_status === 'succeeded') {
               let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(sanitizedURL)}`;
               if (row.visit_date) {
                 browseOriginUrl += `&amp;timestamp=${encodeURIComponent(row.visit_date)}`;
               }
               html += `<a href="${browseOriginUrl}">${sanitizedURL}</a>`;
             } else {
               html += sanitizedURL;
             }
             html += `&nbsp;<a href="${sanitizedURL}"><i class="mdi mdi-open-in-new" aria-hidden="true"></i></a>`;
             return html;
           }
           return data;
         }
       }
     ];
 
     pendingSaveRequestsTable = $('#swh-origin-save-pending-requests').DataTable({
       serverSide: true,
       processing: true,
       language: {
         processing: `<img src="${swhSpinnerSrc}"></img>`
       },
       ajax: Urls.origin_save_requests_list('pending'),
       searchDelay: 1000,
       columns: columnsData,
       scrollY: '50vh',
       scrollCollapse: true,
       order: [[0, 'desc']],
       responsive: {
         details: {
           type: 'none'
         }
       }
     });
     enableRowSelection('#swh-origin-save-pending-requests');
     swh.webapp.addJumpToPagePopoverToDataTable(pendingSaveRequestsTable);
 
     rejectedSaveRequestsTable = $('#swh-origin-save-rejected-requests').DataTable({
       serverSide: true,
       processing: true,
       language: {
         processing: `<img src="${swhSpinnerSrc}"></img>`
       },
       ajax: Urls.origin_save_requests_list('rejected'),
       searchDelay: 1000,
       columns: columnsData,
       scrollY: '50vh',
       scrollCollapse: true,
       order: [[0, 'desc']],
       responsive: {
         details: {
           type: 'none'
         }
       }
     });
     enableRowSelection('#swh-origin-save-rejected-requests');
     swh.webapp.addJumpToPagePopoverToDataTable(rejectedSaveRequestsTable);
 
     columnsData.push({
       data: 'save_task_status',
       name: 'save_task_status'
     });
 
     columnsData.push({
       name: 'info',
       render: (data, type, row) => {
         if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed') {
           return '<i class="mdi mdi-information-outline swh-save-request-info" aria-hidden="true" style="cursor: pointer"' +
                   `onclick="swh.save.displaySaveRequestInfo(event, ${row.id})"></i>`;
         } else {
           return '';
         }
       }
     });
 
     acceptedSaveRequestsTable = $('#swh-origin-save-accepted-requests').DataTable({
       serverSide: true,
       processing: true,
       language: {
         processing: `<img src="${swhSpinnerSrc}"></img>`
       },
       ajax: Urls.origin_save_requests_list('accepted'),
       searchDelay: 1000,
       columns: columnsData,
       scrollY: '50vh',
       scrollCollapse: true,
       order: [[0, 'desc']],
       responsive: {
         details: {
           type: 'none'
         }
       }
     });
     enableRowSelection('#swh-origin-save-accepted-requests');
     swh.webapp.addJumpToPagePopoverToDataTable(acceptedSaveRequestsTable);
 
     $('#swh-origin-save-requests-nav-item').on('shown.bs.tab', () => {
       pendingSaveRequestsTable.draw();
     });
 
     $('#swh-origin-save-url-filters-nav-item').on('shown.bs.tab', () => {
       authorizedOriginTable.draw();
     });
 
     $('#swh-authorized-origins-tab').on('shown.bs.tab', () => {
       authorizedOriginTable.draw();
     });
 
     $('#swh-unauthorized-origins-tab').on('shown.bs.tab', () => {
       unauthorizedOriginTable.draw();
     });
 
     $('#swh-save-requests-pending-tab').on('shown.bs.tab', () => {
       pendingSaveRequestsTable.draw();
     });
 
     $('#swh-save-requests-accepted-tab').on('shown.bs.tab', () => {
       acceptedSaveRequestsTable.draw();
     });
 
     $('#swh-save-requests-rejected-tab').on('shown.bs.tab', () => {
       rejectedSaveRequestsTable.draw();
     });
 
     $('#swh-save-requests-pending-tab').click(() => {
       pendingSaveRequestsTable.ajax.reload(null, false);
     });
 
     $('#swh-save-requests-accepted-tab').click(() => {
       acceptedSaveRequestsTable.ajax.reload(null, false);
     });
 
     $('#swh-save-requests-rejected-tab').click(() => {
       rejectedSaveRequestsTable.ajax.reload(null, false);
     });
 
     $('body').on('click', e => {
       if ($(e.target).parents('.popover').length > 0) {
         e.stopPropagation();
       } else if ($(e.target).parents('.swh-save-request-info').length === 0) {
         $('.swh-save-request-info').popover('dispose');
       }
     });
 
   });
 }
 
 export async function addAuthorizedOriginUrl() {
   const originUrl = $('#swh-authorized-url-prefix').val();
   const addOriginUrl = Urls.admin_origin_save_add_authorized_url(originUrl);
   try {
     const response = await csrfPost(addOriginUrl);
     handleFetchError(response);
     authorizedOriginTable.row.add({'url': originUrl}).draw();
     $('.swh-add-authorized-origin-status').html(
       htmlAlert('success', 'The origin url prefix has been successfully added in the authorized list.', true)
     );
   } catch (_) {
     $('.swh-add-authorized-origin-status').html(
       htmlAlert('warning', 'The provided origin url prefix is already registered in the authorized list.', true)
     );
   }
 }
 
 export async function removeAuthorizedOriginUrl() {
   const originUrl = $('#swh-authorized-origin-urls tr.selected').text();
   if (originUrl) {
     const removeOriginUrl = Urls.admin_origin_save_remove_authorized_url(originUrl);
     try {
       const response = await csrfPost(removeOriginUrl);
       handleFetchError(response);
       authorizedOriginTable.row('.selected').remove().draw();
     } catch (_) {}
   }
 }
 
 export async function addUnauthorizedOriginUrl() {
   const originUrl = $('#swh-unauthorized-url-prefix').val();
   const addOriginUrl = Urls.admin_origin_save_add_unauthorized_url(originUrl);
   try {
     const response = await csrfPost(addOriginUrl);
     handleFetchError(response);
     unauthorizedOriginTable.row.add({'url': originUrl}).draw();
     $('.swh-add-unauthorized-origin-status').html(
       htmlAlert('success', 'The origin url prefix has been successfully added in the unauthorized list.', true)
     );
   } catch (_) {
     $('.swh-add-unauthorized-origin-status').html(
       htmlAlert('warning', 'The provided origin url prefix is already registered in the unauthorized list.', true)
     );
   }
 }
 
 export async function removeUnauthorizedOriginUrl() {
   const originUrl = $('#swh-unauthorized-origin-urls tr.selected').text();
   if (originUrl) {
     const removeOriginUrl = Urls.admin_origin_save_remove_unauthorized_url(originUrl);
     try {
       const response = await csrfPost(removeOriginUrl);
       handleFetchError(response);
       unauthorizedOriginTable.row('.selected').remove().draw();
     } catch (_) {};
   }
 }
 
 export function acceptOriginSaveRequest() {
   const selectedRow = pendingSaveRequestsTable.row('.selected');
   if (selectedRow.length) {
     const acceptOriginSaveRequestCallback = async() => {
       const rowData = selectedRow.data();
       const acceptSaveRequestUrl = Urls.admin_origin_save_request_accept(rowData['visit_type'], rowData['origin_url']);
       await csrfPost(acceptSaveRequestUrl);
       pendingSaveRequestsTable.ajax.reload(null, false);
     };
 
     swh.webapp.showModalConfirm(
       'Accept origin save request ?',
       'Are you sure to accept this origin save request ?',
       acceptOriginSaveRequestCallback);
   }
 }
 
 export function rejectOriginSaveRequest() {
   const selectedRow = pendingSaveRequestsTable.row('.selected');
   if (selectedRow.length) {
-    let rejectOriginSaveRequestCallback = async() => {
+    const rejectOriginSaveRequestCallback = async() => {
       const rowData = selectedRow.data();
       const rejectSaveRequestUrl = Urls.admin_origin_save_request_reject(rowData['visit_type'], rowData['origin_url']);
       await csrfPost(rejectSaveRequestUrl);
       pendingSaveRequestsTable.ajax.reload(null, false);
     };
 
     swh.webapp.showModalConfirm(
       'Reject origin save request ?',
       'Are you sure to reject this origin save request ?',
       rejectOriginSaveRequestCallback);
   }
 }
 
 function removeOriginSaveRequest(requestTable) {
-  let selectedRow = requestTable.row('.selected');
+  const selectedRow = requestTable.row('.selected');
   if (selectedRow.length) {
-    let requestId = selectedRow.data()['id'];
-    let removeOriginSaveRequestCallback = async() => {
+    const requestId = selectedRow.data()['id'];
+    const removeOriginSaveRequestCallback = async() => {
       const removeSaveRequestUrl = Urls.admin_origin_save_request_remove(requestId);
       await csrfPost(removeSaveRequestUrl);
       requestTable.ajax.reload(null, false);
     };
 
     swh.webapp.showModalConfirm(
       'Remove origin save request ?',
       'Are you sure to remove this origin save request ?',
       removeOriginSaveRequestCallback);
   }
 }
 
 export function removePendingOriginSaveRequest() {
   removeOriginSaveRequest(pendingSaveRequestsTable);
 }
 
 export function removeAcceptedOriginSaveRequest() {
   removeOriginSaveRequest(acceptedSaveRequestsTable);
 }
 
 export function removeRejectedOriginSaveRequest() {
   removeOriginSaveRequest(rejectedSaveRequestsTable);
 }
diff --git a/assets/src/bundles/auth/index.js b/assets/src/bundles/auth/index.js
index a560efb4..4eecdf43 100644
--- a/assets/src/bundles/auth/index.js
+++ b/assets/src/bundles/auth/index.js
@@ -1,190 +1,190 @@
 /**
  * Copyright (C) 2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {handleFetchError, csrfPost, removeUrlFragment} from 'utils/functions';
 import './auth.css';
 
 let apiTokensTable;
 
 function tokenForm(infoText, buttonText) {
   const form =
     `<form id="swh-token-form" class="text-center">
       <p id="swh-token-form-text">${infoText}</p>
       <input id="swh-token-form-submit" type="submit" value="${buttonText}">
       <div id="swh-token-form-message"></div>
     </form>`;
   return form;
 }
 
 function errorMessage(message) {
   return `<p id="swh-token-error-message" class="mt-3 swh-token-form-message">${message}</p>`;
 }
 
 function successMessage(message) {
   return `<p id="swh-token-success-message" class="mt-3 swh-token-form-message">${message}</p>`;
 }
 
 function disableSubmitButton() {
   $('#swh-token-form-submit').prop('disabled', true);
 }
 
 function generateToken() {
   window.location = Urls.oidc_generate_bearer_token();
 }
 
 async function displayToken(tokenId) {
   const postData = {
     token_id: tokenId
   };
   try {
     const response = await csrfPost(Urls.oidc_get_bearer_token(), {}, JSON.stringify(postData));
     handleFetchError(response);
     const token = await response.text();
     const tokenHtml =
       `<p>Below is your token.</p>
       <pre id="swh-bearer-token" class="mt-3">${token}</pre>`;
     swh.webapp.showModalHtml('Display bearer token', tokenHtml);
   } catch (response) {
     const responseText = await response.text();
     let errorMsg = 'Internal server error.';
     if (response.status === 400) {
       errorMsg = responseText;
     }
     swh.webapp.showModalHtml('Display bearer token', errorMessage(errorMsg));
   }
 }
 
 async function revokeTokens(tokenIds) {
   const postData = {
     token_ids: tokenIds
   };
   try {
     const response = await csrfPost(Urls.oidc_revoke_bearer_tokens(), {}, JSON.stringify(postData));
     handleFetchError(response);
     disableSubmitButton();
     $('#swh-token-form-message').html(
       successMessage(`Bearer token${tokenIds.length > 1 ? 's' : ''} successfully revoked.`));
     apiTokensTable.draw();
   } catch (_) {
     $('#swh-token-form-message').html(errorMessage('Internal server error.'));
   }
 }
 
 function revokeToken(tokenId) {
   revokeTokens([tokenId]);
 }
 
 function revokeAllTokens() {
   const tokenIds = [];
   const rowsData = apiTokensTable.rows().data();
   for (let i = 0; i < rowsData.length; ++i) {
     tokenIds.push(rowsData[i].id);
   }
   revokeTokens(tokenIds);
 }
 
 export function applyTokenAction(action, tokenId) {
   const actionData = {
     display: {
       submitCallback: displayToken
     },
     generate: {
       modalTitle: 'Bearer token generation',
       infoText: 'Click on the button to generate the token. You will be redirected to ' +
                 'Software Heritage Authentication Service and might be asked to enter ' +
                 'your password again.',
       buttonText: 'Generate token',
       submitCallback: generateToken
     },
     revoke: {
       modalTitle: 'Revoke bearer token',
       infoText: 'Click on the button to revoke the token.',
       buttonText: 'Revoke token',
       submitCallback: revokeToken
     },
     revokeAll: {
       modalTitle: 'Revoke all bearer tokens',
       infoText: 'Click on the button to revoke all tokens.',
       buttonText: 'Revoke tokens',
       submitCallback: revokeAllTokens
     }
   };
 
   if (!actionData[action]) {
     return;
   }
 
   if (action !== 'display') {
     const tokenFormHtml = tokenForm(
       actionData[action].infoText, actionData[action].buttonText);
 
     swh.webapp.showModalHtml(actionData[action].modalTitle, tokenFormHtml);
     $(`#swh-token-form`).submit(event => {
       event.preventDefault();
       event.stopPropagation();
       actionData[action].submitCallback(tokenId);
     });
   } else {
     actionData[action].submitCallback(tokenId);
   }
 }
 
 export function initProfilePage() {
   $(document).ready(() => {
     apiTokensTable = $('#swh-bearer-tokens-table')
       .on('error.dt', (e, settings, techNote, message) => {
         $('#swh-origin-save-request-list-error').text(
           'An error occurred while retrieving the tokens list');
         console.log(message);
       })
       .DataTable({
         serverSide: true,
         ajax: Urls.oidc_list_bearer_tokens(),
         columns: [
           {
             data: 'creation_date',
             name: 'creation_date',
             render: (data, type, row) => {
               if (type === 'display') {
-                let date = new Date(data);
+                const date = new Date(data);
                 return date.toLocaleString();
               }
               return data;
             }
           },
           {
             render: (data, type, row) => {
               const html =
                 `<button class="btn btn-default"
                          onclick="swh.auth.applyTokenAction('display', ${row.id})">
                   Display token
                 </button>
                 <button class="btn btn-default"
                         onclick="swh.auth.applyTokenAction('revoke', ${row.id})">
                   Revoke token
                 </button>`;
               return html;
             }
           }
         ],
         ordering: false,
         searching: false,
         scrollY: '50vh',
         scrollCollapse: true
       });
     $('#swh-oidc-profile-tokens-tab').on('shown.bs.tab', () => {
       apiTokensTable.draw();
       window.location.hash = '#tokens';
     });
     $('#swh-oidc-profile-account-tab').on('shown.bs.tab', () => {
       removeUrlFragment();
     });
     if (window.location.hash === '#tokens') {
       $('.nav-tabs a[href="#swh-oidc-profile-tokens"]').tab('show');
     }
   });
 }
diff --git a/assets/src/bundles/browse/origin-search.js b/assets/src/bundles/browse/origin-search.js
index 226d722b..3f094136 100644
--- a/assets/src/bundles/browse/origin-search.js
+++ b/assets/src/bundles/browse/origin-search.js
@@ -1,260 +1,260 @@
 /**
  * Copyright (C) 2018-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {handleFetchError, isArchivedOrigin} from 'utils/functions';
 
 const limit = 100;
-let linksPrev = [];
+const linksPrev = [];
 let linkNext = null;
 let linkCurrent = null;
 let inSearch = false;
 
 function parseLinkHeader(s) {
-  let re = /<(.+)>; rel="next"/;
+  const re = /<(.+)>; rel="next"/;
   return s.match(re)[1];
 }
 
 function fixTableRowsStyle() {
   setTimeout(() => {
     $('#origin-search-results tbody tr').removeAttr('style');
   });
 }
 
 function clearOriginSearchResultsTable() {
   $('#origin-search-results tbody tr').remove();
 }
 
 async function populateOriginSearchResultsTable(origins) {
   if (origins.length > 0) {
     $('#swh-origin-search-results').show();
     $('#swh-no-result').hide();
     clearOriginSearchResultsTable();
-    let table = $('#origin-search-results tbody');
-    let promises = [];
-    for (let [i, origin] of origins.entries()) {
-      let browseUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(origin.url)}`;
+    const table = $('#origin-search-results tbody');
+    const promises = [];
+    for (const [i, origin] of origins.entries()) {
+      const browseUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(origin.url)}`;
       let tableRow =
         `<tr id="origin-${i}" class="swh-search-result-entry swh-tr-hover-highlight">`;
       tableRow +=
         `<td id="visit-type-origin-${i}" class="swh-origin-visit-type" style="width: 120px;">` +
         '<i title="Checking software origin type" class="mdi mdi-sync mdi-spin mdi-fw"></i>' +
         'Checking</td>';
       tableRow +=
         '<td style="white-space: nowrap;">' +
         `<a href="${browseUrl}">${origin.url}</a></td>`;
       tableRow +=
         `<td class="swh-visit-status" id="visit-status-origin-${i}">` +
         '<i title="Checking archiving status" class="mdi mdi-sync mdi-spin mdi-fw"></i>' +
         'Checking</td>';
       tableRow += '</tr>';
       table.append(tableRow);
       // get async latest visit snapshot and update visit status icon
       let latestSnapshotUrl = Urls.api_1_origin_visit_latest(origin.url);
       latestSnapshotUrl += '?require_snapshot=true';
       promises.push(fetch(latestSnapshotUrl));
     }
     const responses = await Promise.all(promises);
     const responsesData = await Promise.all(responses.map(r => r.json()));
     for (let i = 0; i < responses.length; ++i) {
       const response = responses[i];
       const data = responsesData[i];
       if (response.status !== 404 && data.type) {
         $(`#visit-type-origin-${i}`).html(data.type);
         $(`#visit-status-origin-${i}`).html(
           '<i title="Software origin has been archived by Software Heritage" ' +
           'class="mdi mdi-check-bold mdi-fw"></i>Archived');
       } else {
         $(`#visit-type-origin-${i}`).html('unknown');
         $(`#visit-status-origin-${i}`).html(
           '<i title="Software origin archival by Software Heritage is pending" ' +
           'class="mdi mdi-close-thick mdi-fw"></i>Pending archival');
         if ($('#swh-filter-empty-visits').prop('checked')) {
           $(`#origin-${i}`).remove();
         }
       }
     }
     fixTableRowsStyle();
   } else {
     $('#swh-origin-search-results').hide();
     $('#swh-no-result').text('No origins matching the search criteria were found.');
     $('#swh-no-result').show();
   }
 
   if (linkNext === null) {
     $('#origins-next-results-button').addClass('disabled');
   } else {
     $('#origins-next-results-button').removeClass('disabled');
   }
 
   if (linksPrev.length === 0) {
     $('#origins-prev-results-button').addClass('disabled');
   } else {
     $('#origins-prev-results-button').removeClass('disabled');
   }
 
   inSearch = false;
   setTimeout(() => {
     window.scrollTo(0, 0);
   });
 }
 
 function searchOriginsFirst(searchQueryText, limit) {
   let baseSearchUrl;
-  let searchMetadata = $('#swh-search-origin-metadata').prop('checked');
+  const searchMetadata = $('#swh-search-origin-metadata').prop('checked');
   if (searchMetadata) {
     baseSearchUrl = new URL(Urls.api_1_origin_metadata_search(), window.location);
     baseSearchUrl.searchParams.append('fulltext', searchQueryText);
   } else {
     baseSearchUrl = new URL(Urls.api_1_origin_search(searchQueryText), window.location);
   }
 
-  let withVisit = $('#swh-search-origins-with-visit').prop('checked');
+  const withVisit = $('#swh-search-origins-with-visit').prop('checked');
   baseSearchUrl.searchParams.append('limit', limit);
   baseSearchUrl.searchParams.append('with_visit', withVisit);
   const visitType = $('#swh-search-visit-type').val();
   if (visitType !== 'any') {
     baseSearchUrl.searchParams.append('visit_type', visitType);
   }
-  let searchUrl = baseSearchUrl.toString();
+  const searchUrl = baseSearchUrl.toString();
   searchOrigins(searchUrl);
 }
 
 async function searchOrigins(searchUrl) {
   clearOriginSearchResultsTable();
   $('.swh-loading').addClass('show');
   try {
     const response = await fetch(searchUrl);
     handleFetchError(response);
     const data = await response.json();
     // Save link to the current results page
     linkCurrent = searchUrl;
     // Save link to the next results page.
     linkNext = null;
     if (response.headers.has('Link')) {
-      let parsedLink = parseLinkHeader(response.headers.get('Link'));
+      const parsedLink = parseLinkHeader(response.headers.get('Link'));
       if (parsedLink !== undefined) {
         linkNext = parsedLink;
       }
     }
     // prevLinks is updated by the caller, which is the one to know if
     // we're going forward or backward in the pages.
 
     $('.swh-loading').removeClass('show');
     populateOriginSearchResultsTable(data);
   } catch (response) {
     $('.swh-loading').removeClass('show');
     inSearch = false;
     $('#swh-origin-search-results').hide();
     $('#swh-no-result').text(`Error ${response.status}: ${response.statusText}`);
     $('#swh-no-result').show();
   }
 }
 
 async function doSearch() {
   $('#swh-no-result').hide();
   const searchQueryText = $('#swh-origins-url-patterns').val();
   inSearch = true;
   if (searchQueryText.startsWith('swh:')) {
     try {
       // searchQueryText may be a PID so sending search queries to PID resolve endpoint
       const resolveSWHIDUrl = Urls.api_1_resolve_swhid(searchQueryText);
       const response = await fetch(resolveSWHIDUrl);
       handleFetchError(response);
       const data = await response.json();
       // SWHID has been successfully resolved,
       // so redirect to browse page
       window.location = data.browse_url;
     } catch (response) {
       // display a useful error message if the input
       // looks like a SWHID
       const data = await response.json();
       $('#swh-origin-search-results').hide();
       $('.swh-search-pagination').hide();
       $('#swh-no-result').text(data.reason);
       $('#swh-no-result').show();
     }
   } else if (await isArchivedOrigin(searchQueryText)) {
     // redirect to the browse origin
     window.location.href =
       `${Urls.browse_origin()}?origin_url=${encodeURIComponent(searchQueryText)}`;
   } else {
     // otherwise, proceed with origins search irrespective of the error
     $('#swh-origin-search-results').show();
     $('.swh-search-pagination').show();
     searchOriginsFirst(searchQueryText, limit);
   }
 }
 
 export function initOriginSearch() {
   $(document).ready(() => {
     $('#swh-search-origins').submit(event => {
       event.preventDefault();
       if (event.target.checkValidity()) {
         $(event.target).removeClass('was-validated');
-        let searchQueryText = $('#swh-origins-url-patterns').val().trim();
-        let withVisit = $('#swh-search-origins-with-visit').prop('checked');
-        let withContent = $('#swh-filter-empty-visits').prop('checked');
-        let searchMetadata = $('#swh-search-origin-metadata').prop('checked');
+        const searchQueryText = $('#swh-origins-url-patterns').val().trim();
+        const withVisit = $('#swh-search-origins-with-visit').prop('checked');
+        const withContent = $('#swh-filter-empty-visits').prop('checked');
+        const searchMetadata = $('#swh-search-origin-metadata').prop('checked');
         const visitType = $('#swh-search-visit-type').val();
-        let queryParameters = new URLSearchParams();
+        const queryParameters = new URLSearchParams();
         queryParameters.append('q', searchQueryText);
         if (withVisit) {
           queryParameters.append('with_visit', withVisit);
         }
         if (withContent) {
           queryParameters.append('with_content', withContent);
         }
         if (searchMetadata) {
           queryParameters.append('search_metadata', searchMetadata);
         }
         if (visitType !== 'any') {
           queryParameters.append('visit_type', visitType);
         }
         // Update the url, triggering page reload and effective search
         window.location = `${Urls.browse_search()}?${queryParameters.toString()}`;
       } else {
         $(event.target).addClass('was-validated');
       }
     });
 
     $('#origins-next-results-button').click(event => {
       if ($('#origins-next-results-button').hasClass('disabled') || inSearch) {
         return;
       }
       inSearch = true;
       linksPrev.push(linkCurrent);
       searchOrigins(linkNext);
       event.preventDefault();
     });
 
     $('#origins-prev-results-button').click(event => {
       if ($('#origins-prev-results-button').hasClass('disabled') || inSearch) {
         return;
       }
       inSearch = true;
       searchOrigins(linksPrev.pop());
       event.preventDefault();
     });
 
-    let urlParams = new URLSearchParams(window.location.search);
-    let query = urlParams.get('q');
-    let withVisit = urlParams.has('with_visit');
-    let withContent = urlParams.has('with_content');
-    let searchMetadata = urlParams.has('search_metadata');
-    let visitType = urlParams.get('visit_type');
+    const urlParams = new URLSearchParams(window.location.search);
+    const query = urlParams.get('q');
+    const withVisit = urlParams.has('with_visit');
+    const withContent = urlParams.has('with_content');
+    const searchMetadata = urlParams.has('search_metadata');
+    const visitType = urlParams.get('visit_type');
     if (query) {
       $('#swh-origins-url-patterns').val(query);
       $('#swh-search-origins-with-visit').prop('checked', withVisit);
       $('#swh-filter-empty-visits').prop('checked', withContent);
       $('#swh-search-origin-metadata').prop('checked', searchMetadata);
       if (visitType) {
         $('#swh-search-visit-type').val(visitType);
       }
       doSearch();
     }
   });
 }
diff --git a/assets/src/bundles/browse/snapshot-navigation.js b/assets/src/bundles/browse/snapshot-navigation.js
index 8ade39fb..e85496d8 100644
--- a/assets/src/bundles/browse/snapshot-navigation.js
+++ b/assets/src/bundles/browse/snapshot-navigation.js
@@ -1,56 +1,56 @@
 /**
  * Copyright (C) 2018  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 export function initSnapshotNavigation(snapshotContext, branch) {
 
   function setBranchesTabActive() {
     $('.swh-releases-switch').removeClass('active');
     $('.swh-branches-switch').addClass('active');
     $('#swh-tab-releases').removeClass('active');
     $('#swh-tab-branches').addClass('active');
   }
 
   function setReleasesTabActive() {
     $('.swh-branches-switch').removeClass('active');
     $('.swh-releases-switch').addClass('active');
     $('#swh-tab-branches').removeClass('active');
     $('#swh-tab-releases').addClass('active');
   }
 
   $(document).ready(() => {
     $('.dropdown-menu a.swh-branches-switch').click(e => {
       setBranchesTabActive();
       e.stopPropagation();
     });
 
     $('.dropdown-menu a.swh-releases-switch').click(e => {
       setReleasesTabActive();
       e.stopPropagation();
     });
 
     let dropdownResized = false;
 
     // hack to resize the branches/releases dropdown content,
     // taking icons into account, in order to make the whole names readable
     $('#swh-branches-releases-dd').on('show.bs.dropdown', () => {
       if (dropdownResized) return;
-      let dropdownWidth = $('.swh-branches-releases').width();
+      const dropdownWidth = $('.swh-branches-releases').width();
       $('.swh-branches-releases').width(dropdownWidth + 25);
       dropdownResized = true;
     });
 
     if (snapshotContext) {
       if (branch) {
         setBranchesTabActive();
       } else {
         setReleasesTabActive();
       }
     }
 
   });
 
 }
diff --git a/assets/src/bundles/browse/swhid-utils.js b/assets/src/bundles/browse/swhid-utils.js
index 92b139e5..e0583216 100644
--- a/assets/src/bundles/browse/swhid-utils.js
+++ b/assets/src/bundles/browse/swhid-utils.js
@@ -1,122 +1,122 @@
 /**
  * Copyright (C) 2018-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import ClipboardJS from 'clipboard';
 import 'thirdparty/jquery.tabSlideOut/jquery.tabSlideOut';
 import 'thirdparty/jquery.tabSlideOut/jquery.tabSlideOut.css';
 
 import {BREAKPOINT_SM} from 'utils/constants';
 
 export function swhIdObjectTypeToggled(event) {
   event.preventDefault();
   $(event.target).tab('show');
 }
 
 export function swhIdContextOptionToggled(event) {
   event.stopPropagation();
-  let swhIdElt = $(event.target).closest('.swhid-ui').find('.swhid');
-  let swhIdWithContext = $(event.target).data('swhid-with-context');
-  let swhIdWithContextUrl = $(event.target).data('swhid-with-context-url');
+  const swhIdElt = $(event.target).closest('.swhid-ui').find('.swhid');
+  const swhIdWithContext = $(event.target).data('swhid-with-context');
+  const swhIdWithContextUrl = $(event.target).data('swhid-with-context-url');
   let currentSwhId = swhIdElt.text();
   if ($(event.target).prop('checked')) {
     swhIdElt.attr('href', swhIdWithContextUrl);
     currentSwhId = swhIdWithContext.replace(/;/g, ';\n');
   } else {
     const pos = currentSwhId.indexOf(';');
     if (pos !== -1) {
       currentSwhId = currentSwhId.slice(0, pos);
     }
     swhIdElt.attr('href', '/' + currentSwhId);
   }
   swhIdElt.text(currentSwhId);
 
   addLinesInfo();
 }
 
 function addLinesInfo() {
-  let swhIdElt = $('#swhid-tab-content').find('.swhid');
+  const swhIdElt = $('#swhid-tab-content').find('.swhid');
   let currentSwhId = swhIdElt.text().replace(/;\n/g, ';');
-  let lines = [];
+  const lines = [];
   let linesPart = ';lines=';
-  let linesRegexp = new RegExp(/L(\d+)/g);
+  const linesRegexp = new RegExp(/L(\d+)/g);
   let line = linesRegexp.exec(window.location.hash);
   while (line) {
     lines.push(parseInt(line[1]));
     line = linesRegexp.exec(window.location.hash);
   }
   if (lines.length > 0) {
     linesPart += lines[0];
   }
   if (lines.length > 1) {
     linesPart += '-' + lines[1];
   }
 
   if ($('#swhid-context-option-content').prop('checked')) {
     currentSwhId = currentSwhId.replace(/;lines=\d+-*\d*/g, '');
     if (lines.length > 0) {
       currentSwhId += linesPart;
     }
 
     swhIdElt.text(currentSwhId.replace(/;/g, ';\n'));
     swhIdElt.attr('href', '/' + currentSwhId);
   }
 }
 
 $(document).ready(() => {
   new ClipboardJS('.btn-swhid-copy', {
     text: trigger => {
-      let swhId = $(trigger).closest('.swhid-ui').find('.swhid').text();
+      const swhId = $(trigger).closest('.swhid-ui').find('.swhid').text();
       return swhId.replace(/;\n/g, ';');
     }
   });
 
   new ClipboardJS('.btn-swhid-url-copy', {
     text: trigger => {
-      let swhIdUrl = $(trigger).closest('.swhid-ui').find('.swhid').attr('href');
+      const swhIdUrl = $(trigger).closest('.swhid-ui').find('.swhid').attr('href');
       return window.location.origin + swhIdUrl;
     }
   });
 
   if (window.innerWidth * 0.7 > 1000) {
     $('#swh-identifiers').css('width', '1000px');
   }
 
-  let tabSlideOptions = {
+  const tabSlideOptions = {
     tabLocation: 'right',
     clickScreenToCloseFilters: ['.ui-slideouttab-panel', '.modal'],
     offset: function() {
       const width = $(window).width();
       if (width < BREAKPOINT_SM) {
         return '250px';
       } else {
         return '200px';
       }
     }
   };
   // ensure tab scrolling on small screens
   if (window.innerHeight < 600 || window.innerWidth < 500) {
     tabSlideOptions['otherOffset'] = '20px';
   }
 
   // initiate the sliding identifiers tab
   $('#swh-identifiers').tabSlideOut(tabSlideOptions);
 
   // set the tab visible once the close animation is terminated
   $('#swh-identifiers').css('display', 'block');
   $('.swhid-context-option').trigger('click');
 
   // highlighted code lines changed
   $(window).on('hashchange', () => {
     addLinesInfo();
   });
 
   // highlighted code lines removed
   $('body').click(() => {
     addLinesInfo();
   });
 
 });
diff --git a/assets/src/bundles/origin/visits-calendar.js b/assets/src/bundles/origin/visits-calendar.js
index 90b7bb26..6c2aa630 100644
--- a/assets/src/bundles/origin/visits-calendar.js
+++ b/assets/src/bundles/origin/visits-calendar.js
@@ -1,147 +1,147 @@
 /**
  * Copyright (C) 2018-2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import Calendar from 'js-year-calendar';
 import 'js-year-calendar/dist/js-year-calendar.css';
 
-let minSize = 15;
-let maxSize = 28;
+const minSize = 15;
+const maxSize = 28;
 let currentPopover = null;
 let visitsByDate = {};
 
 function closePopover() {
   if (currentPopover) {
     $(currentPopover).popover('dispose');
     currentPopover = null;
   }
 }
 
 // function to update the visits calendar view based on the selected year
 export function updateCalendar(year, filteredVisits, yearClickedCallback) {
   visitsByDate = {};
   let maxNbVisitsByDate = 0;
   let minDate, maxDate;
   for (let i = 0; i < filteredVisits.length; ++i) {
     filteredVisits[i]['startDate'] = filteredVisits[i]['date'];
     filteredVisits[i]['endDate'] = filteredVisits[i]['startDate'];
-    let date = new Date(filteredVisits[i]['date']);
+    const date = new Date(filteredVisits[i]['date']);
     date.setHours(0, 0, 0, 0);
-    let dateStr = date.toDateString();
+    const dateStr = date.toDateString();
     if (!visitsByDate.hasOwnProperty(dateStr)) {
       visitsByDate[dateStr] = [filteredVisits[i]];
     } else {
       visitsByDate[dateStr].push(filteredVisits[i]);
     }
     maxNbVisitsByDate = Math.max(maxNbVisitsByDate, visitsByDate[dateStr].length);
     if (i === 0) {
       minDate = maxDate = date;
     } else {
       if (date.getTime() < minDate.getTime()) {
         minDate = date;
       }
       if (date.getTime() > maxDate.getTime()) {
         maxDate = date;
       }
     }
   }
 
   closePopover();
 
   new Calendar('#swh-visits-calendar', {
     dataSource: filteredVisits,
     style: 'custom',
     minDate: minDate,
     maxDate: maxDate,
     startYear: year,
     renderEnd: e => yearClickedCallback(e.currentYear),
     customDataSourceRenderer: (element, date, events) => {
-      let dateStr = date.toDateString();
-      let nbVisits = visitsByDate[dateStr].length;
+      const dateStr = date.toDateString();
+      const nbVisits = visitsByDate[dateStr].length;
       let t = nbVisits / maxNbVisitsByDate;
       if (maxNbVisitsByDate === 1) {
         t = 0;
       }
-      let size = minSize + t * (maxSize - minSize);
-      let offsetX = (maxSize - size) / 2 - parseInt($(element).css('padding-left'));
-      let offsetY = (maxSize - size) / 2 - parseInt($(element).css('padding-top')) + 1;
-      let cellWrapper = $('<div></div>');
+      const size = minSize + t * (maxSize - minSize);
+      const offsetX = (maxSize - size) / 2 - parseInt($(element).css('padding-left'));
+      const offsetY = (maxSize - size) / 2 - parseInt($(element).css('padding-top')) + 1;
+      const cellWrapper = $('<div></div>');
       cellWrapper.css('position', 'relative');
-      let dayNumber = $('<div></div>');
+      const dayNumber = $('<div></div>');
       dayNumber.text($(element).text());
-      let circle = $('<div></div>');
+      const circle = $('<div></div>');
       let r = 0;
       let g = 0;
       for (let i = 0; i < nbVisits; ++i) {
-        let visit = visitsByDate[dateStr][i];
+        const visit = visitsByDate[dateStr][i];
         if (visit.status === 'full') {
           g += 255;
         } else if (visit.status === 'partial') {
           r += 255;
           g += 255;
         } else {
           r += 255;
         }
       }
       r /= nbVisits;
       g /= nbVisits;
       circle.css('background-color', 'rgba(' + r + ', ' + g + ', 0, 0.3)');
       circle.css('width', size + 'px');
       circle.css('height', size + 'px');
       circle.css('border-radius', size + 'px');
       circle.css('position', 'absolute');
       circle.css('top', offsetY + 'px');
       circle.css('left', offsetX + 'px');
       cellWrapper.append(dayNumber);
       cellWrapper.append(circle);
       $(element)[0].innerHTML = $(cellWrapper)[0].outerHTML;
     },
     mouseOnDay: e => {
       if (currentPopover !== e.element) {
         closePopover();
       }
-      let dateStr = e.date.toDateString();
+      const dateStr = e.date.toDateString();
       if (visitsByDate.hasOwnProperty(dateStr)) {
 
-        let visits = visitsByDate[dateStr];
+        const visits = visitsByDate[dateStr];
         let content = '<div><h6>' + e.date.toDateString() + '</h6></div>';
         content += '<ul class="swh-list-unstyled">';
         for (let i = 0; i < visits.length; ++i) {
-          let visitTime = visits[i].formatted_date.substr(visits[i].formatted_date.indexOf(',') + 2);
+          const visitTime = visits[i].formatted_date.substr(visits[i].formatted_date.indexOf(',') + 2);
           content += '<li><a class="swh-visit-icon swh-visit-' + visits[i].status + '" title="' + visits[i].status +
                      ' visit" href="' + visits[i].url + '">' + visitTime + '</a></li>';
         }
         content += '</ul>';
 
         $(e.element).popover({
           trigger: 'manual',
           container: 'body',
           html: true,
           content: content
         }).on('mouseleave', () => {
           if (!$('.popover:hover').length) {
             // close popover when leaving day in calendar
             // except if the pointer is hovering it
             closePopover();
           }
         });
 
         $(e.element).on('shown.bs.popover', () => {
           $('.popover').mouseleave(() => {
             // close popover when pointer leaves it
             closePopover();
           });
         });
 
         $(e.element).popover('show');
         currentPopover = e.element;
       }
     }
   });
   $('#swh-visits-calendar.calendar table td').css('width', maxSize + 'px');
   $('#swh-visits-calendar.calendar table td').css('height', maxSize + 'px');
   $('#swh-visits-calendar.calendar table td').css('padding', '0px');
 }
diff --git a/assets/src/bundles/origin/visits-histogram.js b/assets/src/bundles/origin/visits-histogram.js
index 6198c918..d349ff35 100644
--- a/assets/src/bundles/origin/visits-histogram.js
+++ b/assets/src/bundles/origin/visits-histogram.js
@@ -1,336 +1,336 @@
 /**
  * Copyright (C) 2018-2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 // Creation of a stacked histogram with D3.js for software origin visits history
 // Parameters description:
 //  - container: selector for the div that will contain the histogram
 //  - visitsData: raw swh origin visits data
 //  - currentYear: the visits year to display by default
 //  - yearClickCallback: callback when the user selects a year through the histogram
 
 export async function createVisitsHistogram(container, visitsData, currentYear, yearClickCallback) {
 
   const d3 = await import(/* webpackChunkName: "d3" */ 'utils/d3');
 
   // remove previously created histogram and tooltip if any
   d3.select(container).select('svg').remove();
   d3.select('div.d3-tooltip').remove();
 
   // histogram size and margins
   let width = 1000;
   let height = 200;
-  let margin = {top: 20, right: 80, bottom: 30, left: 50};
+  const margin = {top: 20, right: 80, bottom: 30, left: 50};
 
   // create responsive svg
-  let svg = d3.select(container)
+  const svg = d3.select(container)
     .attr('style',
           'padding-bottom: ' + Math.ceil(height * 100 / width) + '%')
     .append('svg')
     .attr('viewBox', '0 0 ' + width + ' ' + height);
 
   // create tooltip div
-  let tooltip = d3.select('body')
+  const tooltip = d3.select('body')
     .append('div')
     .attr('class', 'd3-tooltip')
     .style('opacity', 0);
 
   // update width and height without margins
   width = width - margin.left - margin.right;
   height = height - margin.top - margin.bottom;
 
   // create main svg group element
-  let g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+  const g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
 
   // create x scale
-  let x = d3.scaleTime().rangeRound([0, width]);
+  const x = d3.scaleTime().rangeRound([0, width]);
 
   // create y scale
-  let y = d3.scaleLinear().range([height, 0]);
+  const y = d3.scaleLinear().range([height, 0]);
 
   // create ordinal colorscale mapping visit status
-  let colors = d3.scaleOrdinal()
+  const colors = d3.scaleOrdinal()
     .domain(['full', 'partial', 'failed', 'ongoing'])
     .range(['#008000', '#edc344', '#ff0000', '#0000ff']);
 
   // first swh crawls were made in 2015
-  let startYear = 2015;
+  const startYear = 2015;
   // set latest display year as the current one
-  let now = new Date();
-  let endYear = now.getUTCFullYear() + 1;
-  let monthExtent = [new Date(Date.UTC(startYear, 0, 1)), new Date(Date.UTC(endYear, 0, 1))];
+  const now = new Date();
+  const endYear = now.getUTCFullYear() + 1;
+  const monthExtent = [new Date(Date.UTC(startYear, 0, 1)), new Date(Date.UTC(endYear, 0, 1))];
 
   // create months bins based on setup extent
-  let monthBins = d3.timeMonths(d3.timeMonth.offset(monthExtent[0], -1), monthExtent[1]);
+  const monthBins = d3.timeMonths(d3.timeMonth.offset(monthExtent[0], -1), monthExtent[1]);
   // create years bins based on setup extent
-  let yearBins = d3.timeYears(monthExtent[0], monthExtent[1]);
+  const yearBins = d3.timeYears(monthExtent[0], monthExtent[1]);
 
   // set x scale domain
   x.domain(d3.extent(monthBins));
 
   // use D3 histogram layout to create a function that will bin the visits by month
-  let binByMonth = d3.histogram()
+  const binByMonth = d3.histogram()
     .value(d => d.date)
     .domain(x.domain())
     .thresholds(monthBins);
 
   // use D3 nest function to group the visits by status
-  let visitsByStatus = d3.groups(visitsData, d => d['status'])
+  const visitsByStatus = d3.groups(visitsData, d => d['status'])
     .sort((a, b) => d3.ascending(a[0], b[0]));
 
   // prepare data in order to be able to stack visit statuses by month
-  let statuses = [];
-  let histData = [];
+  const statuses = [];
+  const histData = [];
   for (let i = 0; i < monthBins.length; ++i) {
     histData[i] = {};
   }
   visitsByStatus.forEach(entry => {
     statuses.push(entry[0]);
-    let monthsData = binByMonth(entry[1]);
+    const monthsData = binByMonth(entry[1]);
     for (let i = 0; i < monthsData.length; ++i) {
       histData[i]['x0'] = monthsData[i]['x0'];
       histData[i]['x1'] = monthsData[i]['x1'];
       histData[i][entry[0]] = monthsData[i];
     }
   });
 
   // create function to stack visits statuses by month
-  let stacked = d3.stack()
+  const stacked = d3.stack()
     .keys(statuses)
     .value((d, key) => d[key].length);
 
   // compute the maximum amount of visits by month
-  let yMax = d3.max(histData, d => {
+  const yMax = d3.max(histData, d => {
     let total = 0;
     for (let i = 0; i < statuses.length; ++i) {
       total += d[statuses[i]].length;
     }
     return total;
   });
 
   // set y scale domain
   y.domain([0, yMax]);
 
   // compute ticks values for the y axis
-  let step = 5;
-  let yTickValues = [];
+  const step = 5;
+  const yTickValues = [];
   for (let i = 0; i <= yMax / step; ++i) {
     yTickValues.push(i * step);
   }
   if (yTickValues.length === 0) {
     for (let i = 0; i <= yMax; ++i) {
       yTickValues.push(i);
     }
   } else if (yMax % step !== 0) {
     yTickValues.push(yMax);
   }
 
   // add histogram background grid
   g.append('g')
     .attr('class', 'grid')
     .call(d3.axisLeft(y)
       .tickValues(yTickValues)
       .tickSize(-width)
       .tickFormat(''));
 
   // create one fill only rectangle by displayed year
   // each rectangle will be made visible when hovering the mouse over a year range
   // user will then be able to select a year by clicking in the rectangle
 
   g.append('g')
     .selectAll('rect')
     .data(yearBins)
     .enter().append('rect')
     .attr('class', d => 'year' + d.getUTCFullYear())
     .attr('fill', 'red')
     .attr('fill-opacity', d => d.getUTCFullYear() === currentYear ? 0.3 : 0)
     .attr('stroke', 'none')
     .attr('x', d => {
-      let date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+      const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
       return x(date);
     })
     .attr('y', 0)
     .attr('height', height)
     .attr('width', d => {
-      let date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
-      let yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
+      const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+      const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
       return yearWidth;
     })
     // mouse event callbacks used to show rectangle years
     // when hovering the mouse over the histograms
     .on('mouseover', (event, d) => {
       svg.selectAll('rect.year' + d.getUTCFullYear())
         .attr('fill-opacity', 0.5);
     })
     .on('mouseout', (event, d) => {
       svg.selectAll('rect.year' + d.getUTCFullYear())
         .attr('fill-opacity', 0);
       svg.selectAll('rect.year' + currentYear)
         .attr('fill-opacity', 0.3);
     })
     // callback to select a year after a mouse click
     // in a rectangle year
     .on('click', (event, d) => {
       svg.selectAll('rect.year' + currentYear)
         .attr('fill-opacity', 0);
       svg.selectAll('rect.yearoutline' + currentYear)
         .attr('stroke', 'none');
       currentYear = d.getUTCFullYear();
       svg.selectAll('rect.year' + currentYear)
         .attr('fill-opacity', 0.5);
       svg.selectAll('rect.yearoutline' + currentYear)
         .attr('stroke', 'black');
       yearClickCallback(currentYear);
     });
 
   // create the stacked histogram of visits
   g.append('g')
     .selectAll('g')
     .data(stacked(histData))
     .enter().append('g')
     .attr('fill', d => colors(d.key))
     .selectAll('rect')
     .data(d => d)
     .enter().append('rect')
     .attr('class', d => 'month' + d.data.x1.getMonth())
     .attr('x', d => x(d.data.x0))
     .attr('y', d => y(d[1]))
     .attr('height', d => y(d[0]) - y(d[1]))
     .attr('width', d => x(d.data.x1) - x(d.data.x0) - 1)
     // mouse event callbacks used to show rectangle years
     // but also to show tooltip when hovering the mouse
     // over the histogram bars
     .on('mouseover', (event, d) => {
       svg.selectAll('rect.year' + d.data.x1.getUTCFullYear())
         .attr('fill-opacity', 0.5);
       tooltip.transition()
         .duration(200)
         .style('opacity', 1);
-      let ds = d.data.x1.toISOString().substr(0, 7).split('-');
+      const ds = d.data.x1.toISOString().substr(0, 7).split('-');
       let tooltipText = '<b>' + ds[1] + ' / ' + ds[0] + ':</b><br/>';
       for (let i = 0; i < statuses.length; ++i) {
-        let visitStatus = statuses[i];
-        let nbVisits = d.data[visitStatus].length;
+        const visitStatus = statuses[i];
+        const nbVisits = d.data[visitStatus].length;
         if (nbVisits === 0) continue;
         tooltipText += nbVisits + ' ' + visitStatus + ' visits';
         if (i !== statuses.length - 1) tooltipText += '<br/>';
       }
       tooltip.html(tooltipText)
         .style('left', event.pageX + 15 + 'px')
         .style('top', event.pageY + 'px');
     })
     .on('mouseout', (event, d) => {
       svg.selectAll('rect.year' + d.data.x1.getUTCFullYear())
         .attr('fill-opacity', 0);
       svg.selectAll('rect.year' + currentYear)
         .attr('fill-opacity', 0.3);
       tooltip.transition()
         .duration(500)
         .style('opacity', 0);
     })
     .on('mousemove', (event) => {
       tooltip.style('left', event.pageX + 15 + 'px')
         .style('top', event.pageY + 'px');
     })
     // callback to select a year after a mouse click
     // inside a histogram bar
     .on('click', (event, d) => {
       svg.selectAll('rect.year' + currentYear)
         .attr('fill-opacity', 0);
       svg.selectAll('rect.yearoutline' + currentYear)
         .attr('stroke', 'none');
       currentYear = d.data.x1.getUTCFullYear();
       svg.selectAll('rect.year' + currentYear)
         .attr('fill-opacity', 0.5);
       svg.selectAll('rect.yearoutline' + currentYear)
         .attr('stroke', 'black');
       yearClickCallback(currentYear);
     });
 
   // create one stroke only rectangle by displayed year
   // that will be displayed on top of the histogram when the user has selected a year
   g.append('g')
     .selectAll('rect')
     .data(yearBins)
     .enter().append('rect')
     .attr('class', d => 'yearoutline' + d.getUTCFullYear())
     .attr('fill', 'none')
     .attr('stroke', d => d.getUTCFullYear() === currentYear ? 'black' : 'none')
     .attr('x', d => {
-      let date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+      const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
       return x(date);
     })
     .attr('y', 0)
     .attr('height', height)
     .attr('width', d => {
-      let date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
-      let yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
+      const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+      const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
       return yearWidth;
     });
 
   // add x axis with a tick for every 1st day of each year
-  let xAxis = g.append('g')
+  const xAxis = g.append('g')
     .attr('class', 'axis')
     .attr('transform', 'translate(0,' + height + ')')
     .call(
       d3.axisBottom(x)
         .ticks(d3.timeYear.every(1))
         .tickFormat(d => d.getUTCFullYear())
     );
 
   // shift tick labels in order to display them at the middle
   // of each year range
   xAxis.selectAll('text')
     .attr('transform', d => {
-      let year = d.getUTCFullYear();
-      let date = new Date(Date.UTC(year, 0, 1));
-      let yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
+      const year = d.getUTCFullYear();
+      const date = new Date(Date.UTC(year, 0, 1));
+      const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
       return 'translate(' + -yearWidth / 2 + ', 0)';
     });
 
   // add y axis for the number of visits
   g.append('g')
     .attr('class', 'axis')
     .call(d3.axisLeft(y).tickValues(yTickValues));
 
   // add legend for visit statuses
-  let legendGroup = g.append('g')
+  const legendGroup = g.append('g')
     .attr('font-family', 'sans-serif')
     .attr('font-size', 10)
     .attr('text-anchor', 'end');
 
   legendGroup.append('text')
     .attr('x', width + margin.right - 5)
     .attr('y', 9.5)
     .attr('dy', '0.32em')
     .text('visit status:');
 
-  let legend = legendGroup.selectAll('g')
+  const legend = legendGroup.selectAll('g')
     .data(statuses.slice().reverse())
     .enter().append('g')
     .attr('transform', (d, i) => 'translate(0,' + (i + 1) * 20 + ')');
 
   legend.append('rect')
     .attr('x', width + 2 * margin.right / 3)
     .attr('width', 19)
     .attr('height', 19)
     .attr('fill', colors);
 
   legend.append('text')
     .attr('x', width + 2 * margin.right / 3 - 5)
     .attr('y', 9.5)
     .attr('dy', '0.32em')
     .text(d => d);
 
   // add text label for the y axis
   g.append('text')
     .attr('transform', 'rotate(-90)')
     .attr('y', -margin.left)
     .attr('x', -(height / 2))
     .attr('dy', '1em')
     .style('text-anchor', 'middle')
     .text('Number of visits');
 }
diff --git a/assets/src/bundles/origin/visits-reporting.js b/assets/src/bundles/origin/visits-reporting.js
index 9f541062..776f9179 100644
--- a/assets/src/bundles/origin/visits-reporting.js
+++ b/assets/src/bundles/origin/visits-reporting.js
@@ -1,138 +1,138 @@
 /**
  * Copyright (C) 2018  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {createVisitsHistogram} from './visits-histogram';
 import {updateCalendar} from './visits-calendar';
 import './visits-reporting.css';
 
 // will hold all visits
 let allVisits;
 // will hold filtered visits to display
 let filteredVisits;
 // will hold currently displayed year
 let currentYear;
 
 // function to gather full visits
 function filterFullVisits(differentSnapshots) {
-  let filteredVisits = [];
+  const filteredVisits = [];
   for (let i = 0; i < allVisits.length; ++i) {
     if (allVisits[i].status !== 'full') continue;
     if (!differentSnapshots) {
       filteredVisits.push(allVisits[i]);
     } else if (filteredVisits.length === 0) {
       filteredVisits.push(allVisits[i]);
     } else {
-      let lastVisit = filteredVisits[filteredVisits.length - 1];
+      const lastVisit = filteredVisits[filteredVisits.length - 1];
       if (allVisits[i].snapshot !== lastVisit.snapshot) {
         filteredVisits.push(allVisits[i]);
       }
     }
   }
   return filteredVisits;
 }
 
 // function to update the visits list view based on the selected year
 function updateVisitsList(year) {
   $('#swh-visits-list').children().remove();
-  let visitsByYear = [];
+  const visitsByYear = [];
   for (let i = 0; i < filteredVisits.length; ++i) {
     if (filteredVisits[i].date.getUTCFullYear() === year) {
       visitsByYear.push(filteredVisits[i]);
     }
   }
   let visitsCpt = 0;
-  let nbVisitsByRow = 4;
+  const nbVisitsByRow = 4;
   let visitsListHtml = '<div class="swh-visits-list-row">';
   for (let i = 0; i < visitsByYear.length; ++i) {
     if (visitsCpt > 0 && visitsCpt % nbVisitsByRow === 0) {
       visitsListHtml += '</div><div class="swh-visits-list-row">';
     }
     visitsListHtml += '<div class="swh-visits-list-column" style="width: ' + 100 / nbVisitsByRow + '%;">';
     visitsListHtml += '<a class="swh-visit-icon swh-visit-' + visitsByYear[i].status + '" title="' + visitsByYear[i].status +
                         ' visit" href="' + visitsByYear[i].url + '">' + visitsByYear[i].formatted_date + '</a>';
     visitsListHtml += '</div>';
     ++visitsCpt;
   }
   visitsListHtml += '</div>';
   $('#swh-visits-list').append($(visitsListHtml));
 }
 
 function yearChangedCalendar(year) {
   currentYear = year;
   updateVisitsList(year);
   createVisitsHistogram('.d3-wrapper', filteredVisits, currentYear, yearClickedTimeline);
 }
 
 // callback when the user selects a year through the visits histogram
 function yearClickedTimeline(year) {
   currentYear = year;
   updateCalendar(year, filteredVisits, yearChangedCalendar);
   updateVisitsList(year);
 }
 
 // function to update the visits views (histogram, calendar, list)
 function updateDisplayedVisits() {
   if (filteredVisits.length === 0) {
     return;
   }
   if (!currentYear) {
     currentYear = filteredVisits[filteredVisits.length - 1].date.getUTCFullYear();
   }
   createVisitsHistogram('.d3-wrapper', filteredVisits, currentYear, yearClickedTimeline);
   updateCalendar(currentYear, filteredVisits, yearChangedCalendar);
   updateVisitsList(currentYear);
 }
 
 // callback when the user only wants to see full visits pointing
 // to different snapshots (default)
 export function showFullVisitsDifferentSnapshots(event) {
   filteredVisits = filterFullVisits(true);
   updateDisplayedVisits();
 }
 
 // callback when the user only wants to see full visits
 export function showFullVisits(event) {
   filteredVisits = filterFullVisits(false);
   updateDisplayedVisits();
 }
 
 // callback when the user wants to see all visits (including partial, ongoing and failed ones)
 export function showAllVisits(event) {
   filteredVisits = allVisits;
   updateDisplayedVisits();
 }
 
 export function initVisitsReporting(visits) {
   $(document).ready(() => {
     allVisits = visits;
     // process input visits
     let firstFullVisit;
     allVisits.forEach((v, i) => {
       // Turn Unix epoch into Javascript Date object
       v.date = new Date(Math.floor(v.date * 1000));
-      let visitLink = '<a class="swh-visit-icon swh-visit-' + v.status + '" href="' + v.url + '">' + v.formatted_date + '</a>';
+      const visitLink = '<a class="swh-visit-icon swh-visit-' + v.status + '" href="' + v.url + '">' + v.formatted_date + '</a>';
       if (v.status === 'full') {
         if (!firstFullVisit) {
           firstFullVisit = v;
           $('#swh-first-full-visit').append($(visitLink));
           if (allVisits.length === 1) {
             $('#swh-last-full-visit')[0].innerHTML = visitLink;
           }
         } else {
           $('#swh-last-full-visit')[0].innerHTML = visitLink;
         }
       }
       if (i === allVisits.length - 1) {
         $('#swh-last-visit').append($(visitLink));
       }
     });
 
     // display full visits pointing to different snapshots by default
     showFullVisitsDifferentSnapshots();
   });
 
 }
diff --git a/assets/src/bundles/revision/diff-utils.js b/assets/src/bundles/revision/diff-utils.js
index 8c462d48..1312f8d6 100644
--- a/assets/src/bundles/revision/diff-utils.js
+++ b/assets/src/bundles/revision/diff-utils.js
@@ -1,793 +1,793 @@
 /**
  * Copyright (C) 2018-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import 'waypoints/lib/jquery.waypoints';
 
 import {swhSpinnerSrc} from 'utils/constants';
 import {removeUrlFragment} from 'utils/functions';
 
 import diffPanelTemplate from './diff-panel.ejs';
 
 // number of changed files in the revision
 let changes = null;
 let nbChangedFiles = 0;
 // to track the number of already computed files diffs
 let nbDiffsComputed = 0;
 
 // the no newline at end of file marker from Github
-let noNewLineMarker = '<span class="no-nl-marker" title="No newline at end of file">' +
+const noNewLineMarker = '<span class="no-nl-marker" title="No newline at end of file">' +
                         '<svg aria-hidden="true" height="16" version="1.1" viewBox="0 0 16 16" width="16">' +
                           '<path fill-rule="evenodd" d="M16 5v3c0 .55-.45 1-1 1h-3v2L9 8l3-3v2h2V5h2zM8 8c0 2.2-1.8 4-4 4s-4-1.8-4-4 1.8-4 4-4 4 1.8 4 4zM1.5 9.66L5.66 5.5C5.18 5.19 4.61 5 4 5 2.34 5 1 6.34 1 8c0 .61.19 1.17.5 1.66zM7 8c0-.61-.19-1.17-.5-1.66L2.34 10.5c.48.31 1.05.5 1.66.5 1.66 0 3-1.34 3-3z"></path>' +
                         '</svg>' +
                       '</span>';
 
 // to track the total number of added lines in files diffs
 let nbAdditions = 0;
 // to track the total number of deleted lines in files diffs
 let nbDeletions = 0;
 // to track the already computed diffs by id
-let computedDiffs = {};
+const computedDiffs = {};
 // map a diff id to its computation url
-let diffsUrls = {};
+const diffsUrls = {};
 // to keep track of diff lines to highlight
 let startLines = null;
 let endLines = null;
 // map max line numbers characters to diff
 const diffMaxNumberChars = {};
 // focused diff for highlighting
 let focusedDiff = null;
 // highlighting color
 const lineHighlightColor = '#fdf3da';
 // might contain diff lines to highlight parsed from URL fragment
 let selectedDiffLinesInfo;
 // URL fragment to append when switching to 'Changes' tab
 const changesUrlFragment = '#swh-revision-changes';
 // current displayed tab name
 let currentTabName = 'Files';
 
 // to check if a DOM element is in the viewport
 function isInViewport(elt) {
-  let elementTop = $(elt).offset().top;
-  let elementBottom = elementTop + $(elt).outerHeight();
+  const elementTop = $(elt).offset().top;
+  const elementBottom = elementTop + $(elt).outerHeight();
 
-  let viewportTop = $(window).scrollTop();
-  let viewportBottom = viewportTop + $(window).height();
+  const viewportTop = $(window).scrollTop();
+  const viewportBottom = viewportTop + $(window).height();
 
   return elementBottom > viewportTop && elementTop < viewportBottom;
 }
 
 // to format the diffs line numbers
 export function formatDiffLineNumbers(diffId, fromLine, toLine) {
   const maxNumberChars = diffMaxNumberChars[diffId];
   const fromLineStr = toLnStr(fromLine);
   const toLineStr = toLnStr(toLine);
   let ret = '';
   for (let i = 0; i < (maxNumberChars - fromLineStr.length); ++i) {
     ret += ' ';
   }
   ret += fromLineStr;
   ret += '  ';
   for (let i = 0; i < (maxNumberChars - toLineStr.length); ++i) {
     ret += ' ';
   }
   ret += toLineStr;
   return ret;
 }
 
 function parseDiffHunkRangeIfAny(lineText) {
   let baseFromLine, baseToLine;
   if (lineText.startsWith('@@')) {
-    let linesInfoRegExp = new RegExp(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@$/gm);
-    let linesInfoRegExp2 = new RegExp(/^@@ -(\d+) \+(\d+),(\d+) @@$/gm);
-    let linesInfoRegExp3 = new RegExp(/^@@ -(\d+),(\d+) \+(\d+) @@$/gm);
-    let linesInfoRegExp4 = new RegExp(/^@@ -(\d+) \+(\d+) @@$/gm);
-    let linesInfo = linesInfoRegExp.exec(lineText);
-    let linesInfo2 = linesInfoRegExp2.exec(lineText);
-    let linesInfo3 = linesInfoRegExp3.exec(lineText);
-    let linesInfo4 = linesInfoRegExp4.exec(lineText);
+    const linesInfoRegExp = new RegExp(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@$/gm);
+    const linesInfoRegExp2 = new RegExp(/^@@ -(\d+) \+(\d+),(\d+) @@$/gm);
+    const linesInfoRegExp3 = new RegExp(/^@@ -(\d+),(\d+) \+(\d+) @@$/gm);
+    const linesInfoRegExp4 = new RegExp(/^@@ -(\d+) \+(\d+) @@$/gm);
+    const linesInfo = linesInfoRegExp.exec(lineText);
+    const linesInfo2 = linesInfoRegExp2.exec(lineText);
+    const linesInfo3 = linesInfoRegExp3.exec(lineText);
+    const linesInfo4 = linesInfoRegExp4.exec(lineText);
     if (linesInfo) {
       baseFromLine = parseInt(linesInfo[1]) - 1;
       baseToLine = parseInt(linesInfo[3]) - 1;
     } else if (linesInfo2) {
       baseFromLine = parseInt(linesInfo2[1]) - 1;
       baseToLine = parseInt(linesInfo2[2]) - 1;
     } else if (linesInfo3) {
       baseFromLine = parseInt(linesInfo3[1]) - 1;
       baseToLine = parseInt(linesInfo3[3]) - 1;
     } else if (linesInfo4) {
       baseFromLine = parseInt(linesInfo4[1]) - 1;
       baseToLine = parseInt(linesInfo4[2]) - 1;
     }
   }
   if (baseFromLine !== undefined) {
     return [baseFromLine, baseToLine];
   } else {
     return null;
   }
 }
 
 function toLnInt(lnStr) {
   return lnStr ? parseInt(lnStr) : 0;
 };
 
 function toLnStr(lnInt) {
   return lnInt ? lnInt.toString() : '';
 };
 
 // parse diff line numbers to an int array [from, to]
 export function parseDiffLineNumbers(lineNumbersStr, from, to) {
   let lines;
   if (!from && !to) {
     lines = lineNumbersStr.replace(/[ ]+/g, ' ').split(' ');
     if (lines.length > 2) {
       lines.shift();
     }
     lines = lines.map(x => toLnInt(x));
   } else {
-    let lineNumber = toLnInt(lineNumbersStr.trim());
+    const lineNumber = toLnInt(lineNumbersStr.trim());
     if (from) {
       lines = [lineNumber, 0];
     } else if (to) {
       lines = [0, lineNumber];
     }
   }
   return lines;
 }
 
 // serialize selected line numbers range to string for URL fragment
 export function selectedDiffLinesToFragment(startLines, endLines, unified) {
   let selectedLinesFragment = '';
   selectedLinesFragment += `F${startLines[0] || 0}`;
   selectedLinesFragment += `T${startLines[1] || 0}`;
   selectedLinesFragment += `-F${endLines[0] || 0}`;
   selectedLinesFragment += `T${endLines[1] || 0}`;
   if (unified) {
     selectedLinesFragment += '-unified';
   } else {
     selectedLinesFragment += '-split';
   }
   return selectedLinesFragment;
 }
 
 // parse selected lines from URL fragment
 export function fragmentToSelectedDiffLines(fragment) {
   const RE_LINES = /F([0-9]+)T([0-9]+)-F([0-9]+)T([0-9]+)-([a-z]+)/;
   const matchObj = RE_LINES.exec(fragment);
   if (matchObj.length === 6) {
     return {
       startLines: [parseInt(matchObj[1]), parseInt(matchObj[2])],
       endLines: [parseInt(matchObj[3]), parseInt(matchObj[4])],
       unified: matchObj[5] === 'unified'
     };
   } else {
     return null;
   }
 }
 
 // function to highlight a single diff line
 function highlightDiffLine(diffId, i) {
-  let line = $(`#${diffId} .hljs-ln-line[data-line-number="${i}"]`);
-  let lineNumbers = $(`#${diffId} .hljs-ln-numbers[data-line-number="${i}"]`);
+  const line = $(`#${diffId} .hljs-ln-line[data-line-number="${i}"]`);
+  const lineNumbers = $(`#${diffId} .hljs-ln-numbers[data-line-number="${i}"]`);
   lineNumbers.css('color', 'black');
   lineNumbers.css('font-weight', 'bold');
   line.css('background-color', lineHighlightColor);
   line.css('mix-blend-mode', 'multiply');
   return line;
 }
 
 // function to reset highlighting
 function resetHighlightedDiffLines(resetVars = true) {
   if (resetVars) {
     focusedDiff = null;
     startLines = null;
     endLines = null;
   }
   $('.hljs-ln-line[data-line-number]').css('background-color', 'initial');
   $('.hljs-ln-line[data-line-number]').css('mix-blend-mode', 'initial');
   $('.hljs-ln-numbers[data-line-number]').css('color', '#aaa');
   $('.hljs-ln-numbers[data-line-number]').css('font-weight', 'initial');
   if (currentTabName === 'Changes' && window.location.hash !== changesUrlFragment) {
     window.history.replaceState('', document.title,
                                 window.location.pathname + window.location.search + changesUrlFragment);
   }
 }
 
 // highlight lines in a diff, return first highlighted line numbers element
 function highlightDiffLines(diffId, startLines, endLines, unified) {
   let firstHighlightedLine;
   // unified diff case
   if (unified) {
     let start = formatDiffLineNumbers(diffId, startLines[0], startLines[1]);
     let end = formatDiffLineNumbers(diffId, endLines[0], endLines[1]);
 
     const startLine = $(`#${diffId} .hljs-ln-line[data-line-number="${start}"]`);
     const endLine = $(`#${diffId} .hljs-ln-line[data-line-number="${end}"]`);
     if ($(endLine).position().top < $(startLine).position().top) {
       [start, end] = [end, start];
       firstHighlightedLine = endLine;
     } else {
       firstHighlightedLine = startLine;
     }
     const lineTd = highlightDiffLine(diffId, start);
     let tr = $(lineTd).closest('tr');
     let lineNumbers = $(tr).children('.hljs-ln-line').data('line-number').toString();
     while (lineNumbers !== end) {
       if (lineNumbers.trim()) {
         highlightDiffLine(diffId, lineNumbers);
       }
       tr = $(tr).next();
       lineNumbers = $(tr).children('.hljs-ln-line').data('line-number').toString();
     }
     highlightDiffLine(diffId, end);
 
   // split diff case
   } else {
     // highlight only from part of the diff
     if (startLines[0] && endLines[0]) {
       const start = Math.min(startLines[0], endLines[0]);
       const end = Math.max(startLines[0], endLines[0]);
       for (let i = start; i <= end; ++i) {
         highlightDiffLine(`${diffId}-from`, i);
       }
       firstHighlightedLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${start}"]`);
     // highlight only to part of the diff
     } else if (startLines[1] && endLines[1]) {
       const start = Math.min(startLines[1], endLines[1]);
       const end = Math.max(startLines[1], endLines[1]);
       for (let i = start; i <= end; ++i) {
         highlightDiffLine(`${diffId}-to`, i);
       }
       firstHighlightedLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${start}"]`);
     // highlight both part of the diff
     } else {
       let left, right;
       if (startLines[0] && endLines[1]) {
         left = startLines[0];
         right = endLines[1];
       } else {
         left = endLines[0];
         right = startLines[1];
       }
 
       const leftLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${left}"]`);
       const rightLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${right}"]`);
       const leftLineAbove = $(leftLine).position().top < $(rightLine).position().top;
 
       if (leftLineAbove) {
         firstHighlightedLine = leftLine;
       } else {
         firstHighlightedLine = rightLine;
       }
 
       let fromTr = $(`#${diffId}-from tr`).first();
       let fromLn = $(fromTr).children('.hljs-ln-line').data('line-number');
       let toTr = $(`#${diffId}-to tr`).first();
       let toLn = $(toTr).children('.hljs-ln-line').data('line-number');
       let canHighlight = false;
 
       while (true) {
         if (leftLineAbove && fromLn === left) {
           canHighlight = true;
         } else if (!leftLineAbove && toLn === right) {
           canHighlight = true;
         }
 
         if (canHighlight && fromLn) {
           highlightDiffLine(`${diffId}-from`, fromLn);
         }
 
         if (canHighlight && toLn) {
           highlightDiffLine(`${diffId}-to`, toLn);
         }
 
         if ((leftLineAbove && toLn === right) || (!leftLineAbove && fromLn === left)) {
           break;
         }
 
         fromTr = $(fromTr).next();
         fromLn = $(fromTr).children('.hljs-ln-line').data('line-number');
         toTr = $(toTr).next();
         toLn = $(toTr).children('.hljs-ln-line').data('line-number');
       }
 
     }
   }
 
-  let selectedLinesFragment = selectedDiffLinesToFragment(startLines, endLines, unified);
+  const selectedLinesFragment = selectedDiffLinesToFragment(startLines, endLines, unified);
   window.location.hash = `diff_${diffId}+${selectedLinesFragment}`;
   return firstHighlightedLine;
 }
 
 // callback to switch from side-by-side diff to unified one
 export function showUnifiedDiff(diffId) {
   $(`#${diffId}-split-diff`).css('display', 'none');
   $(`#${diffId}-unified-diff`).css('display', 'block');
 }
 
 // callback to switch from unified diff to side-by-side one
 export function showSplitDiff(diffId) {
   $(`#${diffId}-unified-diff`).css('display', 'none');
   $(`#${diffId}-split-diff`).css('display', 'block');
 }
 
 // to compute diff and process it for display
 export async function computeDiff(diffUrl, diffId) {
 
   // force diff computation ?
-  let force = diffUrl.indexOf('force=true') !== -1;
+  const force = diffUrl.indexOf('force=true') !== -1;
 
   // it no forced computation and diff already computed, do nothing
   if (!force && computedDiffs.hasOwnProperty(diffId)) {
     return;
   }
 
   function setLineNumbers(lnElt, lineNumbers) {
     $(lnElt).attr('data-line-number', lineNumbers || '');
     $(lnElt).children().attr('data-line-number', lineNumbers || '');
     $(lnElt).siblings().attr('data-line-number', lineNumbers || '');
   }
 
   // mark diff computation as already requested
   computedDiffs[diffId] = true;
 
   $(`#${diffId}-loading`).css('visibility', 'visible');
 
   // set spinner visible while requesting diff
   $(`#${diffId}-loading`).css('display', 'block');
   $(`#${diffId}-highlightjs`).css('display', 'none');
 
   // request diff computation and process it
   const response = await fetch(diffUrl);
   const data = await response.json();
 
   // increment number of computed diffs
   ++nbDiffsComputed;
   // toggle the 'Compute all diffs' button if all diffs have been computed
   if (nbDiffsComputed === changes.length) {
     $('#swh-compute-all-diffs').addClass('active');
   }
 
   // Large diff (> threshold) are not automatically computed,
   // add a button to force its computation
   if (data.diff_str.indexOf('Large diff') === 0) {
     $(`#${diffId}`)[0].innerHTML = data.diff_str +
           `<br/><button class="btn btn-default btn-sm" type="button"
            onclick="swh.revision.computeDiff('${diffUrl}&force=true', '${diffId}')">` +
            'Request diff</button>';
     setDiffVisible(diffId);
   } else if (data.diff_str.indexOf('@@') !== 0) {
     $(`#${diffId}`).text(data.diff_str);
     setDiffVisible(diffId);
   } else {
 
     // prepare code highlighting
     $(`.${diffId}`).removeClass('nohighlight');
     $(`.${diffId}`).addClass(data.language);
 
     // set unified diff text
     $(`#${diffId}`).text(data.diff_str);
 
     // code highlighting for unified diff
     $(`#${diffId}`).each((i, elt) => {
       hljs.highlightElement(elt);
       hljs.lineNumbersElementSync(elt);
     });
 
     // process unified diff lines in order to generate side-by-side diffs text
     // but also compute line numbers for unified and side-by-side diffs
     let baseFromLine = '';
     let baseToLine = '';
-    let fromToLines = [];
-    let fromLines = [];
-    let toLines = [];
+    const fromToLines = [];
+    const fromLines = [];
+    const toLines = [];
     let maxNumberChars = 0;
     let diffFromStr = '';
     let diffToStr = '';
     let linesOffset = 0;
 
     $(`#${diffId} .hljs-ln-numbers`).each((i, lnElt) => {
-      let lnText = lnElt.nextSibling.innerText;
-      let linesInfo = parseDiffHunkRangeIfAny(lnText);
+      const lnText = lnElt.nextSibling.innerText;
+      const linesInfo = parseDiffHunkRangeIfAny(lnText);
       let fromLine = '';
       let toLine = '';
       // parsed lines info from the diff output
       if (linesInfo) {
         baseFromLine = linesInfo[0];
         baseToLine = linesInfo[1];
         linesOffset = 0;
         diffFromStr += (lnText + '\n');
         diffToStr += (lnText + '\n');
         fromLines.push('');
         toLines.push('');
         // line removed in the from file
       } else if (lnText.length > 0 && lnText[0] === '-') {
         baseFromLine = baseFromLine + 1;
         fromLine = baseFromLine.toString();
         fromLines.push(fromLine);
         ++nbDeletions;
         diffFromStr += (lnText + '\n');
         ++linesOffset;
         // line added in the to file
       } else if (lnText.length > 0 && lnText[0] === '+') {
         baseToLine = baseToLine + 1;
         toLine = baseToLine.toString();
         toLines.push(toLine);
         ++nbAdditions;
         diffToStr += (lnText + '\n');
         --linesOffset;
         // line present in both files
       } else {
         baseFromLine = baseFromLine + 1;
         baseToLine = baseToLine + 1;
         fromLine = baseFromLine.toString();
         toLine = baseToLine.toString();
         for (let j = 0; j < Math.abs(linesOffset); ++j) {
           if (linesOffset > 0) {
             diffToStr += '\n';
             toLines.push('');
           } else {
             diffFromStr += '\n';
             fromLines.push('');
           }
         }
         linesOffset = 0;
         diffFromStr += (lnText + '\n');
         diffToStr += (lnText + '\n');
         toLines.push(toLine);
         fromLines.push(fromLine);
       }
       if (!baseFromLine) {
         fromLine = '';
       }
       if (!baseToLine) {
         toLine = '';
       }
       fromToLines[i] = [fromLine, toLine];
       maxNumberChars = Math.max(maxNumberChars, fromLine.length);
       maxNumberChars = Math.max(maxNumberChars, toLine.length);
     });
 
     diffMaxNumberChars[diffId] = maxNumberChars;
 
     // set side-by-side diffs text
     $(`#${diffId}-from`).text(diffFromStr);
     $(`#${diffId}-to`).text(diffToStr);
 
     // code highlighting for side-by-side diffs
     $(`#${diffId}-from, #${diffId}-to`).each((i, elt) => {
       hljs.highlightElement(elt);
       hljs.lineNumbersElementSync(elt);
     });
 
     // diff highlighting for added/removed lines on top of code highlighting
     $(`.${diffId} .hljs-ln-numbers`).each((i, lnElt) => {
-      let lnText = lnElt.nextSibling.innerText;
+      const lnText = lnElt.nextSibling.innerText;
       if (lnText.startsWith('@@')) {
         $(lnElt).parent().addClass('swh-diff-lines-info');
-        let linesInfoText = $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text();
+        const linesInfoText = $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text();
         $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').children().remove();
         $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text('');
         $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').append(`<span class="hljs-meta">${linesInfoText}</span>`);
       } else if (lnText.length > 0 && lnText[0] === '-') {
         $(lnElt).parent().addClass('swh-diff-removed-line');
       } else if (lnText.length > 0 && lnText[0] === '+') {
         $(lnElt).parent().addClass('swh-diff-added-line');
       }
     });
 
     // set line numbers for unified diff
     $(`#${diffId} .hljs-ln-numbers`).each((i, lnElt) => {
       const lineNumbers = formatDiffLineNumbers(diffId, fromToLines[i][0], fromToLines[i][1]);
       setLineNumbers(lnElt, lineNumbers);
     });
 
     // set line numbers for the from side-by-side diff
     $(`#${diffId}-from .hljs-ln-numbers`).each((i, lnElt) => {
       setLineNumbers(lnElt, fromLines[i]);
     });
 
     // set line numbers for the to side-by-side diff
     $(`#${diffId}-to .hljs-ln-numbers`).each((i, lnElt) => {
       setLineNumbers(lnElt, toLines[i]);
     });
 
     // last processing:
     //  - remove the '+' and '-' at the beginning of the diff lines
     //    from code highlighting
     //  - add the "no new line at end of file marker" if needed
     $(`.${diffId} .hljs-ln-code`).each((i, lnElt) => {
       if (lnElt.firstChild) {
         if (lnElt.firstChild.nodeName !== '#text') {
-          let lineText = lnElt.firstChild.innerHTML;
+          const lineText = lnElt.firstChild.innerHTML;
           if (lineText[0] === '-' || lineText[0] === '+') {
             lnElt.firstChild.innerHTML = lineText.substr(1);
-            let newTextNode = document.createTextNode(lineText[0]);
+            const newTextNode = document.createTextNode(lineText[0]);
             $(lnElt).prepend(newTextNode);
           }
         }
         $(lnElt).contents().filter((i, elt) => {
           return elt.nodeType === 3; // Node.TEXT_NODE
         }).each((i, textNode) => {
-          let swhNoNewLineMarker = '[swh-no-nl-marker]';
+          const swhNoNewLineMarker = '[swh-no-nl-marker]';
           if (textNode.textContent.indexOf(swhNoNewLineMarker) !== -1) {
             textNode.textContent = textNode.textContent.replace(swhNoNewLineMarker, '');
             $(lnElt).append($(noNewLineMarker));
           }
         });
       }
     });
 
     // hide the diff mode switch button in case of not generated diffs
     if (data.diff_str.indexOf('Diffs are not generated for non textual content') !== 0) {
       $(`#diff_${diffId} .diff-styles`).css('visibility', 'visible');
     }
 
     setDiffVisible(diffId);
 
     // highlight diff lines if provided in URL fragment
     if (selectedDiffLinesInfo &&
               selectedDiffLinesInfo.diffPanelId.indexOf(diffId) !== -1) {
       if (!selectedDiffLinesInfo.unified) {
         showSplitDiff(diffId);
       }
       const firstHighlightedLine = highlightDiffLines(
         diffId, selectedDiffLinesInfo.startLines,
         selectedDiffLinesInfo.endLines, selectedDiffLinesInfo.unified);
 
       $('html, body').animate(
         {
           scrollTop: firstHighlightedLine.offset().top - 50
         },
         {
           duration: 500
         }
       );
     }
   }
 
 }
 
 function setDiffVisible(diffId) {
   // set the unified diff visible by default
   $(`#${diffId}-loading`).css('display', 'none');
   $(`#${diffId}-highlightjs`).css('display', 'block');
 
   // update displayed counters
   $('#swh-revision-lines-added').text(`${nbAdditions} additions`);
   $('#swh-revision-lines-deleted').text(`${nbDeletions} deletions`);
   $('#swh-nb-diffs-computed').text(nbDiffsComputed);
 
   // refresh the waypoints triggering diffs computation as
   // the DOM layout has been updated
   Waypoint.refreshAll();
 }
 
 // to compute all visible diffs in the viewport
 function computeVisibleDiffs() {
   $('.swh-file-diff-panel').each((i, elt) => {
     if (isInViewport(elt)) {
-      let diffId = elt.id.replace('diff_', '');
+      const diffId = elt.id.replace('diff_', '');
       computeDiff(diffsUrls[diffId], diffId);
     }
   });
 }
 
 function genDiffPanel(diffData) {
   let diffPanelTitle = diffData.path;
   if (diffData.type === 'rename') {
     diffPanelTitle = `${diffData.from_path} &rarr; ${diffData.to_path}`;
   }
   return diffPanelTemplate({
     diffData: diffData,
     diffPanelTitle: diffPanelTitle,
     swhSpinnerSrc: swhSpinnerSrc
   });
 }
 
 // setup waypoints to request diffs computation on the fly while scrolling
 function setupWaypoints() {
   for (let i = 0; i < changes.length; ++i) {
-    let diffData = changes[i];
+    const diffData = changes[i];
 
     // create a waypoint that will trigger diff computation when
     // the top of the diff panel hits the bottom of the viewport
     $(`#diff_${diffData.id}`).waypoint({
       handler: function() {
         if (isInViewport(this.element)) {
-          let diffId = this.element.id.replace('diff_', '');
+          const diffId = this.element.id.replace('diff_', '');
           computeDiff(diffsUrls[diffId], diffId);
           this.destroy();
         }
       },
       offset: '100%'
     });
 
     // create a waypoint that will trigger diff computation when
     // the bottom of the diff panel hits the top of the viewport
     $(`#diff_${diffData.id}`).waypoint({
       handler: function() {
         if (isInViewport(this.element)) {
-          let diffId = this.element.id.replace('diff_', '');
+          const diffId = this.element.id.replace('diff_', '');
           computeDiff(diffsUrls[diffId], diffId);
           this.destroy();
         }
       },
       offset: function() {
         return -$(this.element).height();
       }
     });
   }
   Waypoint.refreshAll();
 }
 
 function scrollToDiffPanel(diffPanelId, setHash = true) {
   // disable waypoints while scrolling as we do not want to
   // launch computation of diffs the user is not interested in
   // (file changes list can be large)
   Waypoint.disableAll();
 
   $('html, body').animate(
     {
       scrollTop: $(diffPanelId).offset().top
     },
     {
       duration: 500,
       complete: () => {
         if (setHash) {
           window.location.hash = diffPanelId;
         }
         // enable waypoints back after scrolling
         Waypoint.enableAll();
         // compute diffs visible in the viewport
         computeVisibleDiffs();
       }
     });
 }
 
 // callback when the user clicks on the 'Compute all diffs' button
 export function computeAllDiffs(event) {
   $(event.currentTarget).addClass('active');
-  for (let diffId in diffsUrls) {
+  for (const diffId in diffsUrls) {
     if (diffsUrls.hasOwnProperty(diffId)) {
       computeDiff(diffsUrls[diffId], diffId);
     }
   }
   event.stopPropagation();
 }
 
 export async function initRevisionDiff(revisionMessageBody, diffRevisionUrl) {
 
   await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
 
   // callback when the 'Changes' tab is activated
   $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', async e => {
     currentTabName = e.currentTarget.text.trim();
     if (currentTabName === 'Changes') {
       window.location.hash = changesUrlFragment;
       $('#readme-panel').css('display', 'none');
 
       if (changes) {
         return;
       }
 
       // request computation of revision file changes list
       // when navigating to the 'Changes' tab and add diff panels
       // to the DOM when receiving the result
       const response = await fetch(diffRevisionUrl);
       const data = await response.json();
 
       changes = data.changes;
       nbChangedFiles = data.total_nb_changes;
       let changedFilesText = `${nbChangedFiles} changed file`;
       if (nbChangedFiles !== 1) {
         changedFilesText += 's';
       }
       $('#swh-revision-changed-files').text(changedFilesText);
       $('#swh-total-nb-diffs').text(changes.length);
       $('#swh-revision-changes-list pre')[0].innerHTML = data.changes_msg;
 
       $('#swh-revision-changes-loading').css('display', 'none');
       $('#swh-revision-changes-list pre').css('display', 'block');
       $('#swh-compute-all-diffs').css('visibility', 'visible');
       $('#swh-revision-changes-list').removeClass('in');
 
       if (nbChangedFiles > changes.length) {
         $('#swh-too-large-revision-diff').css('display', 'block');
         $('#swh-nb-loaded-diffs').text(changes.length);
       }
 
       for (let i = 0; i < changes.length; ++i) {
-        let diffData = changes[i];
+        const diffData = changes[i];
         diffsUrls[diffData.id] = diffData.diff_url;
         $('#swh-revision-diffs').append(genDiffPanel(diffData));
       }
 
       setupWaypoints();
       computeVisibleDiffs();
 
       if (selectedDiffLinesInfo) {
         scrollToDiffPanel(selectedDiffLinesInfo.diffPanelId, false);
       }
 
     } else if (currentTabName === 'Files') {
       removeUrlFragment();
       $('#readme-panel').css('display', 'block');
     }
   });
 
   $(document).ready(() => {
 
     if (revisionMessageBody.length > 0) {
       $('#swh-revision-message').addClass('in');
     } else {
       $('#swh-collapse-revision-message').attr('data-toggle', '');
     }
 
     // callback when the user requests to scroll on a specific diff or back to top
     $('#swh-revision-changes-list a[href^="#"], #back-to-top a[href^="#"]').click(e => {
-      let href = $.attr(e.currentTarget, 'href');
+      const href = $.attr(e.currentTarget, 'href');
       scrollToDiffPanel(href);
       return false;
     });
 
     // click callback for highlighting diff lines
     $('body').click(evt => {
 
       if (currentTabName !== 'Changes') {
         return;
       }
 
       if (evt.target.classList.contains('hljs-ln-n')) {
 
         const diffId = $(evt.target).closest('code').prop('id');
 
         const from = diffId.indexOf('-from') !== -1;
         const to = diffId.indexOf('-to') !== -1;
 
         const lineNumbers = $(evt.target).data('line-number').toString();
 
         const currentDiff = diffId.replace('-from', '').replace('-to', '');
         if (!evt.shiftKey || currentDiff !== focusedDiff || !lineNumbers.trim()) {
           resetHighlightedDiffLines();
           focusedDiff = currentDiff;
         }
         if (currentDiff === focusedDiff && lineNumbers.trim()) {
           if (!evt.shiftKey) {
             startLines = parseDiffLineNumbers(lineNumbers, from, to);
             highlightDiffLines(currentDiff, startLines, startLines, !from && !to);
           } else if (startLines) {
             resetHighlightedDiffLines(false);
             endLines = parseDiffLineNumbers(lineNumbers, from, to);
             highlightDiffLines(currentDiff, startLines, endLines, !from && !to);
           }
         }
 
       } else {
         resetHighlightedDiffLines();
       }
     });
 
     // if an URL fragment for highlighting a diff is present
     // parse highlighting info and initiate diff loading
     const fragment = window.location.hash;
     if (fragment) {
       const split = fragment.split('+');
       if (split.length === 2) {
         selectedDiffLinesInfo = fragmentToSelectedDiffLines(split[1]);
         if (selectedDiffLinesInfo) {
           selectedDiffLinesInfo.diffPanelId = split[0];
           $(`.nav-tabs a[href="${changesUrlFragment}"]`).tab('show');
         }
       }
       if (fragment === changesUrlFragment) {
         $(`.nav-tabs a[href="${changesUrlFragment}"]`).tab('show');
       }
     }
 
   });
 
 }
diff --git a/assets/src/bundles/revision/log-utils.js b/assets/src/bundles/revision/log-utils.js
index a2b06b95..3ec8566e 100644
--- a/assets/src/bundles/revision/log-utils.js
+++ b/assets/src/bundles/revision/log-utils.js
@@ -1,27 +1,27 @@
 /**
  * Copyright (C) 2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 export function revsOrderingTypeClicked(event) {
-  let urlParams = new URLSearchParams(window.location.search);
-  let orderingType = $(event.target).val();
+  const urlParams = new URLSearchParams(window.location.search);
+  const orderingType = $(event.target).val();
   if (orderingType) {
     urlParams.set('revs_ordering', $(event.target).val());
   } else if (urlParams.has('revs_ordering')) {
     urlParams.delete('revs_ordering');
   }
   window.location.search = urlParams.toString();
 }
 
 export function initRevisionsLog() {
   $(document).ready(() => {
-    let urlParams = new URLSearchParams(window.location.search);
-    let revsOrderingType = urlParams.get('revs_ordering');
+    const urlParams = new URLSearchParams(window.location.search);
+    const revsOrderingType = urlParams.get('revs_ordering');
     if (revsOrderingType) {
       $(`:input[value="${revsOrderingType}"]`).prop('checked', true);
     }
   });
 }
diff --git a/assets/src/bundles/save/index.js b/assets/src/bundles/save/index.js
index e9c9afc8..0a34d78f 100644
--- a/assets/src/bundles/save/index.js
+++ b/assets/src/bundles/save/index.js
@@ -1,566 +1,566 @@
 /**
  * Copyright (C) 2018-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {csrfPost, handleFetchError, isGitRepoUrl, htmlAlert, removeUrlFragment,
         getCanonicalOriginURL} from 'utils/functions';
 import {swhSpinnerSrc} from 'utils/constants';
 import artifactFormRowTemplate from './artifact-form-row.ejs';
 
 let saveRequestsTable;
 
 async function originSaveRequest(
   originType, originUrl, extraData,
   acceptedCallback, pendingCallback, errorCallback
 ) {
   // Actually trigger the origin save request
-  let addSaveOriginRequestUrl = Urls.api_1_save_origin(originType, originUrl);
+  const addSaveOriginRequestUrl = Urls.api_1_save_origin(originType, originUrl);
   $('.swh-processing-save-request').css('display', 'block');
   let headers = {};
   let body = null;
   if (extraData !== {}) {
     body = JSON.stringify(extraData);
     headers = {
       'Content-Type': 'application/json'
     };
   };
 
   try {
     const response = await csrfPost(addSaveOriginRequestUrl, headers, body);
     handleFetchError(response);
     const data = await response.json();
     $('.swh-processing-save-request').css('display', 'none');
     if (data.save_request_status === 'accepted') {
       acceptedCallback();
     } else {
       pendingCallback();
     }
   } catch (response) {
     $('.swh-processing-save-request').css('display', 'none');
     const errorData = await response.json();
     errorCallback(response.status, errorData);
   };
 }
 
 function addArtifactVersionAutofillHandler(formId) {
   // autofill artifact version input with the filename from
   // the artifact url without extensions
   $(`#swh-input-artifact-url-${formId}`).on('input', function(event) {
     const artifactUrl = $(this).val().trim();
     let filename = artifactUrl.split('/').slice(-1)[0];
     if (filename !== artifactUrl) {
       filename = filename.replace(/tar.*$/, 'tar');
       const filenameNoExt = filename.split('.').slice(0, -1).join('.');
       const artifactVersion = $(`#swh-input-artifact-version-${formId}`);
       if (filenameNoExt !== filename) {
         artifactVersion.val(filenameNoExt);
       }
     }
   });
 }
 
 export function maybeRequireExtraInputs() {
   // Read the actual selected value and depending on the origin type, display some extra
   // inputs or hide them. This makes the extra inputs disabled when not displayed.
   const originType = $('#swh-input-visit-type').val();
   let display = 'none';
   let disabled = true;
 
   if (originType === 'archives') {
     display = 'flex';
     disabled = false;
   }
   $('.swh-save-origin-archives-form').css('display', display);
   if (!disabled) {
     // help paragraph must have block display for proper rendering
     $('#swh-save-origin-archives-help').css('display', 'block');
   }
   $('.swh-save-origin-archives-form .form-control').prop('disabled', disabled);
 
   if (originType === 'archives' && $('.swh-save-origin-archives-form').length === 1) {
     // insert first artifact row when the archives visit type is selected for the first time
     $('.swh-save-origin-archives-form').last().after(
       artifactFormRowTemplate({deletableRow: false, formId: 0}));
     addArtifactVersionAutofillHandler(0);
   }
 }
 
 export function addArtifactFormRow() {
   const formId = $('.swh-save-origin-artifact-form').length;
   $('.swh-save-origin-artifact-form').last().after(
     artifactFormRowTemplate({
       deletableRow: true,
       formId: formId
     })
   );
   addArtifactVersionAutofillHandler(formId);
 }
 
 export function deleteArtifactFormRow(event) {
   $(event.target).closest('.swh-save-origin-artifact-form').remove();
 }
 
 const userRequestsFilterCheckbox = `
 <div class="custom-control custom-checkbox swhid-option">
   <input class="custom-control-input" value="option-user-requests-filter" type="checkbox"
          id="swh-save-requests-user-filter">
   <label class="custom-control-label font-weight-normal" for="swh-save-requests-user-filter">
     show only your own requests
   </label>
 </div>
 `;
 
 export function initOriginSave() {
 
   $(document).ready(async() => {
 
     $.fn.dataTable.ext.errMode = 'none';
 
     const response = await fetch(Urls.origin_save_types_list());
     const data = await response.json();
 
-    for (let originType of data) {
+    for (const originType of data) {
       $('#swh-input-visit-type').append(`<option value="${originType}">${originType}</option>`);
     }
     // set git as the default value as before
     $('#swh-input-visit-type').val('git');
 
     saveRequestsTable = $('#swh-origin-save-requests')
       .on('error.dt', (e, settings, techNote, message) => {
         $('#swh-origin-save-request-list-error').text('An error occurred while retrieving the save requests list');
         console.log(message);
       })
       .DataTable({
         serverSide: true,
         processing: true,
         language: {
           processing: `<img src="${swhSpinnerSrc}"></img>`
         },
         ajax: {
           url: Urls.origin_save_requests_list('all'),
           data: (d) => {
             if (swh.webapp.isUserLoggedIn() && $('#swh-save-requests-user-filter').prop('checked')) {
               d.user_requests_only = '1';
             }
           }
         },
         searchDelay: 1000,
         // see https://datatables.net/examples/advanced_init/dom_toolbar.html and the comments section
         // this option customizes datatables UI components by adding an extra checkbox above the table
         // while keeping bootstrap layout
         dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' +
              '<"row"<"col-sm-12"tr>>' +
              '<"row"<"col-sm-5"i><"col-sm-7"p>>',
         fnInitComplete: function() {
           if (swh.webapp.isUserLoggedIn()) {
             $('div.user-requests-filter').html(userRequestsFilterCheckbox);
             $('#swh-save-requests-user-filter').on('change', () => {
               saveRequestsTable.draw();
             });
           }
         },
         columns: [
           {
             data: 'save_request_date',
             name: 'request_date',
             render: (data, type, row) => {
               if (type === 'display') {
-                let date = new Date(data);
+                const date = new Date(data);
                 return date.toLocaleString();
               }
               return data;
             }
           },
           {
             data: 'visit_type',
             name: 'visit_type'
           },
           {
             data: 'origin_url',
             name: 'origin_url',
             render: (data, type, row) => {
               if (type === 'display') {
                 let html = '';
                 const sanitizedURL = $.fn.dataTable.render.text().display(data);
                 if (row.save_task_status === 'succeeded') {
                   let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(sanitizedURL)}`;
                   if (row.visit_date) {
                     browseOriginUrl += `&amp;timestamp=${encodeURIComponent(row.visit_date)}`;
                   }
                   html += `<a href="${browseOriginUrl}">${sanitizedURL}</a>`;
                 } else {
                   html += sanitizedURL;
                 }
                 html += `&nbsp;<a href="${sanitizedURL}"><i class="mdi mdi-open-in-new" aria-hidden="true"></i></a>`;
                 return html;
               }
               return data;
             }
           },
           {
             data: 'save_request_status',
             name: 'status'
           },
           {
             data: 'save_task_status',
             name: 'loading_task_status'
           },
           {
             name: 'info',
             render: (data, type, row) => {
               if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed') {
                 return `<i class="mdi mdi-information-outline swh-save-request-info" ` +
                        'aria-hidden="true" style="cursor: pointer"' +
                        `onclick="swh.save.displaySaveRequestInfo(event, ${row.id})"></i>`;
               } else {
                 return '';
               }
             }
           },
           {
             render: (data, type, row) => {
               if (row.save_request_status === 'accepted') {
                 const saveAgainButton =
                   '<button class="btn btn-default btn-sm swh-save-origin-again" type="button" ' +
                   `onclick="swh.save.fillSaveRequestFormAndScroll(` +
                   `'${row.visit_type}', '${row.origin_url}');">` +
                   '<i class="mdi mdi-camera mdi-fw" aria-hidden="true"></i>' +
                   'Save again</button>';
                 return saveAgainButton;
               } else {
                 return '';
               }
             }
           }
         ],
         scrollY: '50vh',
         scrollCollapse: true,
         order: [[0, 'desc']],
         responsive: {
           details: {
             type: 'none'
           }
         }
       });
 
     swh.webapp.addJumpToPagePopoverToDataTable(saveRequestsTable);
 
     $('#swh-origin-save-requests-list-tab').on('shown.bs.tab', () => {
       saveRequestsTable.draw();
       window.location.hash = '#requests';
     });
 
     $('#swh-origin-save-request-help-tab').on('shown.bs.tab', () => {
       removeUrlFragment();
       $('.swh-save-request-info').popover('dispose');
     });
 
-    let saveRequestAcceptedAlert = htmlAlert(
+    const saveRequestAcceptedAlert = htmlAlert(
       'success',
       'The "save code now" request has been accepted and will be processed as soon as possible.',
       true
     );
 
-    let saveRequestPendingAlert = htmlAlert(
+    const saveRequestPendingAlert = htmlAlert(
       'warning',
       'The "save code now" request has been put in pending state and may be accepted for processing after manual review.',
       true
     );
 
-    let saveRequestRateLimitedAlert = htmlAlert(
+    const saveRequestRateLimitedAlert = htmlAlert(
       'danger',
       'The rate limit for "save code now" requests has been reached. Please try again later.',
       true
     );
 
-    let saveRequestUnknownErrorAlert = htmlAlert(
+    const saveRequestUnknownErrorAlert = htmlAlert(
       'danger',
       'An unexpected error happened when submitting the "save code now request".',
       true
     );
 
     $('#swh-save-origin-form').submit(async event => {
       event.preventDefault();
       event.stopPropagation();
       $('.alert').alert('close');
       if (event.target.checkValidity()) {
         $(event.target).removeClass('was-validated');
-        let originType = $('#swh-input-visit-type').val();
+        const originType = $('#swh-input-visit-type').val();
         let originUrl = $('#swh-input-origin-url').val();
 
         originUrl = await getCanonicalOriginURL(originUrl);
 
         // read the extra inputs for the 'archives' type
-        let extraData = {};
+        const extraData = {};
         if (originType === 'archives') {
           extraData['archives_data'] = [];
           for (let i = 0; i < $('.swh-save-origin-artifact-form').length; ++i) {
             extraData['archives_data'].push({
               'artifact_url': $(`#swh-input-artifact-url-${i}`).val(),
               'artifact_version': $(`#swh-input-artifact-version-${i}`).val()
             });
           }
         }
 
         originSaveRequest(originType, originUrl, extraData,
                           () => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert),
                           () => $('#swh-origin-save-request-status').html(saveRequestPendingAlert),
                           (statusCode, errorData) => {
                             $('#swh-origin-save-request-status').css('color', 'red');
                             if (statusCode === 403) {
                               const errorAlert = htmlAlert('danger', `Error: ${errorData['reason']}`);
                               $('#swh-origin-save-request-status').html(errorAlert);
                             } else if (statusCode === 429) {
                               $('#swh-origin-save-request-status').html(saveRequestRateLimitedAlert);
                             } else if (statusCode === 400) {
                               const errorAlert = htmlAlert('danger', errorData['reason']);
                               $('#swh-origin-save-request-status').html(errorAlert);
                             } else {
                               $('#swh-origin-save-request-status').html(saveRequestUnknownErrorAlert);
                             }
                           });
       } else {
         $(event.target).addClass('was-validated');
       }
     });
 
     $('#swh-show-origin-save-requests-list').on('click', (event) => {
       event.preventDefault();
       $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
     });
 
     $('#swh-input-origin-url').on('input', function(event) {
-      let originUrl = $(this).val().trim();
+      const originUrl = $(this).val().trim();
       $(this).val(originUrl);
       $('#swh-input-visit-type option').each(function() {
-        let val = $(this).val();
+        const val = $(this).val();
         if (val && originUrl.includes(val)) {
           $(this).prop('selected', true);
         }
       });
     });
 
     if (window.location.hash === '#requests') {
       $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
     }
 
   });
 
 }
 
 export function validateSaveOriginUrl(input) {
-  let originType = $('#swh-input-visit-type').val();
+  const originType = $('#swh-input-visit-type').val();
   let originUrl = null;
   let validUrl = true;
 
   try {
     originUrl = new URL(input.value.trim());
   } catch (TypeError) {
     validUrl = false;
   }
 
   if (validUrl) {
-    let allowedProtocols = ['http:', 'https:', 'svn:', 'git:'];
+    const allowedProtocols = ['http:', 'https:', 'svn:', 'git:'];
     validUrl = (
       allowedProtocols.find(protocol => protocol === originUrl.protocol) !== undefined
     );
   }
 
   if (validUrl && originType === 'git') {
     // additional checks for well known code hosting providers
     switch (originUrl.hostname) {
       case 'github.com':
         validUrl = isGitRepoUrl(originUrl);
         break;
 
       case 'git.code.sf.net':
         validUrl = isGitRepoUrl(originUrl, '/p/');
         break;
 
       case 'bitbucket.org':
         validUrl = isGitRepoUrl(originUrl);
         break;
 
       default:
         if (originUrl.hostname.startsWith('gitlab.')) {
           validUrl = isGitRepoUrl(originUrl);
         }
         break;
     }
   }
 
   if (validUrl) {
     input.setCustomValidity('');
   } else {
     input.setCustomValidity('The origin url is not valid or does not reference a code repository');
   }
 }
 
 export function initTakeNewSnapshot() {
 
-  let newSnapshotRequestAcceptedAlert = htmlAlert(
+  const newSnapshotRequestAcceptedAlert = htmlAlert(
     'success',
     'The "take new snapshot" request has been accepted and will be processed as soon as possible.',
     true
   );
 
-  let newSnapshotRequestPendingAlert = htmlAlert(
+  const newSnapshotRequestPendingAlert = htmlAlert(
     'warning',
     'The "take new snapshot" request has been put in pending state and may be accepted for processing after manual review.',
     true
   );
 
-  let newSnapshotRequestRateLimitAlert = htmlAlert(
+  const newSnapshotRequestRateLimitAlert = htmlAlert(
     'danger',
     'The rate limit for "take new snapshot" requests has been reached. Please try again later.',
     true
   );
 
-  let newSnapshotRequestUnknownErrorAlert = htmlAlert(
+  const newSnapshotRequestUnknownErrorAlert = htmlAlert(
     'danger',
     'An unexpected error happened when submitting the "save code now request".',
     true
   );
 
   $(document).ready(() => {
     $('#swh-take-new-snapshot-form').submit(event => {
       event.preventDefault();
       event.stopPropagation();
 
-      let originType = $('#swh-input-visit-type').val();
-      let originUrl = $('#swh-input-origin-url').val();
-      let extraData = {};
+      const originType = $('#swh-input-visit-type').val();
+      const originUrl = $('#swh-input-origin-url').val();
+      const extraData = {};
 
       originSaveRequest(originType, originUrl, extraData,
                         () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestAcceptedAlert),
                         () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestPendingAlert),
                         (statusCode, errorData) => {
                           $('#swh-take-new-snapshot-request-status').css('color', 'red');
                           if (statusCode === 403) {
                             const errorAlert = htmlAlert('danger', `Error: ${errorData['detail']}`, true);
                             $('#swh-take-new-snapshot-request-status').html(errorAlert);
                           } else if (statusCode === 429) {
                             $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRateLimitAlert);
                           } else {
                             $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestUnknownErrorAlert);
                           }
                         });
     });
   });
 }
 
 export function formatValuePerType(type, value) {
   // Given some typed value, format and return accordingly formatted value
   const mapFormatPerTypeFn = {
     'json': (v) => JSON.stringify(v, null, 2),
     'date': (v) => new Date(v).toLocaleString(),
     'raw': (v) => v,
     'duration': (v) => v + ' seconds'
   };
 
   return value === null ? null : mapFormatPerTypeFn[type](value);
 }
 
 export async function displaySaveRequestInfo(event, saveRequestId) {
   event.stopPropagation();
   const saveRequestTaskInfoUrl = Urls.origin_save_task_info(saveRequestId);
   // close popover when clicking again on the info icon
   if ($(event.target).data('bs.popover')) {
     $(event.target).popover('dispose');
     return;
   }
   $('.swh-save-request-info').popover('dispose');
   $(event.target).popover({
     animation: false,
     boundary: 'viewport',
     container: 'body',
     title: 'Save request task information ' +
              '<i style="cursor: pointer; position: absolute; right: 1rem;" ' +
              `class="mdi mdi-close swh-save-request-info-close"></i>`,
     content: `<div class="swh-popover swh-save-request-info-popover">
                   <div class="text-center">
                     <img src=${swhSpinnerSrc}></img>
                     <p>Fetching task information ...</p>
                   </div>
                 </div>`,
     html: true,
     placement: 'left',
     sanitizeFn: swh.webapp.filterXSS
   });
 
   $(event.target).on('shown.bs.popover', function() {
     const popoverId = $(this).attr('aria-describedby');
     $(`#${popoverId} .mdi-close`).click(() => {
       $(this).popover('dispose');
     });
   });
 
   $(event.target).popover('show');
   const response = await fetch(saveRequestTaskInfoUrl);
   const saveRequestTaskInfo = await response.json();
 
   let content;
   if ($.isEmptyObject(saveRequestTaskInfo)) {
     content = 'Not available';
   } else {
-    let saveRequestInfo = [];
+    const saveRequestInfo = [];
     const taskData = {
       'Type': ['raw', 'type'],
       'Visit status': ['raw', 'visit_status'],
       'Arguments': ['json', 'arguments'],
       'Id': ['raw', 'id'],
       'Backend id': ['raw', 'backend_id'],
       'Scheduling date': ['date', 'scheduled'],
       'Start date': ['date', 'started'],
       'Completion date': ['date', 'ended'],
       'Duration': ['duration', 'duration'],
       'Runner': ['raw', 'worker'],
       'Log': ['raw', 'message']
     };
     for (const [title, [type, property]] of Object.entries(taskData)) {
       if (saveRequestTaskInfo.hasOwnProperty(property)) {
         saveRequestInfo.push({
           key: title,
           value: formatValuePerType(type, saveRequestTaskInfo[property])
         });
       }
     }
     content = '<table class="table"><tbody>';
-    for (let info of saveRequestInfo) {
+    for (const info of saveRequestInfo) {
       content +=
             `<tr>
               <th class="swh-metadata-table-row swh-metadata-table-key">${info.key}</th>
               <td class="swh-metadata-table-row swh-metadata-table-value">
                 <pre>${info.value}</pre>
               </td>
             </tr>`;
     }
     content += '</tbody></table>';
   }
   $('.swh-popover').html(content);
   $(event.target).popover('update');
 }
 
 export function fillSaveRequestFormAndScroll(visitType, originUrl) {
   $('#swh-input-origin-url').val(originUrl);
   let originTypeFound = false;
   $('#swh-input-visit-type option').each(function() {
-    let val = $(this).val();
+    const val = $(this).val();
     if (val && originUrl.includes(val)) {
       $(this).prop('selected', true);
       originTypeFound = true;
     }
   });
   if (!originTypeFound) {
     $('#swh-input-visit-type option').each(function() {
-      let val = $(this).val();
+      const val = $(this).val();
       if (val === visitType) {
         $(this).prop('selected', true);
       }
     });
   }
   window.scrollTo(0, 0);
 }
diff --git a/assets/src/bundles/vault/vault-create-tasks.js b/assets/src/bundles/vault/vault-create-tasks.js
index c043a90e..e1aa4062 100644
--- a/assets/src/bundles/vault/vault-create-tasks.js
+++ b/assets/src/bundles/vault/vault-create-tasks.js
@@ -1,155 +1,155 @@
 /**
  * Copyright (C) 2018-2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {handleFetchError, csrfPost, htmlAlert} from 'utils/functions';
 
 const alertStyle = {
   'position': 'fixed',
   'left': '1rem',
   'bottom': '1rem',
   'z-index': '100000'
 };
 
 export async function vaultRequest(objectType, objectId) {
   let vaultUrl;
   if (objectType === 'directory') {
     vaultUrl = Urls.api_1_vault_cook_directory(objectId);
   } else {
     vaultUrl = Urls.api_1_vault_cook_revision_gitfast(objectId);
   }
   // check if object has already been cooked
   const response = await fetch(vaultUrl);
   const data = await response.json();
 
   // object needs to be cooked
   if (data.exception === 'NotFoundExc' || data.status === 'failed') {
     // if last cooking has failed, remove previous task info from localStorage
     // in order to force the recooking of the object
     swh.vault.removeCookingTaskInfo([objectId]);
     $(`#vault-cook-${objectType}-modal`).modal('show');
     // object has been cooked and should be in the vault cache,
     // it will be asked to cook it again if it is not
   } else if (data.status === 'done') {
     $(`#vault-fetch-${objectType}-modal`).modal('show');
   } else {
     const cookingServiceDownAlert =
           $(htmlAlert('danger',
                       'Archive cooking service is currently experiencing issues.<br/>' +
                       'Please try again later.',
                       true));
     cookingServiceDownAlert.css(alertStyle);
     $('body').append(cookingServiceDownAlert);
   }
 }
 
 async function addVaultCookingTask(cookingTask) {
 
   const swhidsContext = swh.webapp.getSwhIdsContext();
   cookingTask.origin = swhidsContext[cookingTask.object_type].context.origin;
   cookingTask.path = swhidsContext[cookingTask.object_type].context.path;
   cookingTask.browse_url = swhidsContext[cookingTask.object_type].swhid_with_context_url;
   if (!cookingTask.browse_url) {
     cookingTask.browse_url = swhidsContext[cookingTask.object_type].swhid_url;
   }
 
   let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
   if (!vaultCookingTasks) {
     vaultCookingTasks = [];
   }
   if (vaultCookingTasks.find(val => {
     return val.object_type === cookingTask.object_type &&
             val.object_id === cookingTask.object_id;
   }) === undefined) {
     let cookingUrl;
     if (cookingTask.object_type === 'directory') {
       cookingUrl = Urls.api_1_vault_cook_directory(cookingTask.object_id);
     } else {
       cookingUrl = Urls.api_1_vault_cook_revision_gitfast(cookingTask.object_id);
     }
     if (cookingTask.email) {
       cookingUrl += '?email=' + cookingTask.email;
     }
 
     try {
       const response = await csrfPost(cookingUrl);
       handleFetchError(response);
       vaultCookingTasks.push(cookingTask);
       localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
       $('#vault-cook-directory-modal').modal('hide');
       $('#vault-cook-revision-modal').modal('hide');
       const cookingTaskCreatedAlert =
           $(htmlAlert('success',
                       'Archive cooking request successfully submitted.<br/>' +
                       `Go to the <a href="${Urls.browse_vault()}">Downloads</a> page ` +
                       'to get the download link once it is ready.',
                       true));
       cookingTaskCreatedAlert.css(alertStyle);
       $('body').append(cookingTaskCreatedAlert);
     } catch (_) {
       $('#vault-cook-directory-modal').modal('hide');
       $('#vault-cook-revision-modal').modal('hide');
       const cookingTaskFailedAlert =
           $(htmlAlert('danger',
                       'Archive cooking request submission failed.',
                       true));
       cookingTaskFailedAlert.css(alertStyle);
       $('body').append(cookingTaskFailedAlert);
     }
   }
 }
 
 function validateEmail(email) {
-  let re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+  const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
   return re.test(String(email).toLowerCase());
 }
 
 export function cookDirectoryArchive(directoryId) {
-  let email = $('#swh-vault-directory-email').val().trim();
+  const email = $('#swh-vault-directory-email').val().trim();
   if (!email || validateEmail(email)) {
-    let cookingTask = {
+    const cookingTask = {
       'object_type': 'directory',
       'object_id': directoryId,
       'email': email,
       'status': 'new'
     };
     addVaultCookingTask(cookingTask);
 
   } else {
     $('#invalid-email-modal').modal('show');
   }
 }
 
 export async function fetchDirectoryArchive(directoryId) {
   $('#vault-fetch-directory-modal').modal('hide');
   const vaultUrl = Urls.api_1_vault_cook_directory(directoryId);
   const response = await fetch(vaultUrl);
   const data = await response.json();
   swh.vault.fetchCookedObject(data.fetch_url);
 }
 
 export function cookRevisionArchive(revisionId) {
-  let email = $('#swh-vault-revision-email').val().trim();
+  const email = $('#swh-vault-revision-email').val().trim();
   if (!email || validateEmail(email)) {
-    let cookingTask = {
+    const cookingTask = {
       'object_type': 'revision',
       'object_id': revisionId,
       'email': email,
       'status': 'new'
     };
     addVaultCookingTask(cookingTask);
   } else {
     $('#invalid-email-modal').modal('show');
   }
 }
 
 export async function fetchRevisionArchive(revisionId) {
   $('#vault-fetch-directory-modal').modal('hide');
   const vaultUrl = Urls.api_1_vault_cook_revision_gitfast(revisionId);
   const response = await fetch(vaultUrl);
   const data = await response.json();
   swh.vault.fetchCookedObject(data.fetch_url);
 }
diff --git a/assets/src/bundles/vault/vault-ui.js b/assets/src/bundles/vault/vault-ui.js
index 22ca093c..9cd669c7 100644
--- a/assets/src/bundles/vault/vault-ui.js
+++ b/assets/src/bundles/vault/vault-ui.js
@@ -1,241 +1,241 @@
 /**
  * Copyright (C) 2018-2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {handleFetchError, handleFetchErrors, csrfPost} from 'utils/functions';
 import vaultTableRowTemplate from './vault-table-row.ejs';
 
-let progress =
+const progress =
   `<div class="progress">
     <div class="progress-bar progress-bar-success progress-bar-striped"
           role="progressbar" aria-valuenow="100" aria-valuemin="0"
           aria-valuemax="100" style="width: 100%;height: 100%;">
     </div>
   </div>;`;
 
-let pollingInterval = 5000;
+const pollingInterval = 5000;
 let checkVaultId;
 
 function updateProgressBar(progressBar, cookingTask) {
   if (cookingTask.status === 'new') {
     progressBar.css('background-color', 'rgba(128, 128, 128, 0.5)');
   } else if (cookingTask.status === 'pending') {
     progressBar.css('background-color', 'rgba(0, 0, 255, 0.5)');
   } else if (cookingTask.status === 'done') {
     progressBar.css('background-color', '#5cb85c');
   } else if (cookingTask.status === 'failed') {
     progressBar.css('background-color', 'rgba(255, 0, 0, 0.5)');
     progressBar.css('background-image', 'none');
   }
   progressBar.text(cookingTask.progress_message || cookingTask.status);
   if (cookingTask.status === 'new' || cookingTask.status === 'pending') {
     progressBar.addClass('progress-bar-animated');
   } else {
     progressBar.removeClass('progress-bar-striped');
   }
 }
 
 let recookTask;
 
 // called when the user wants to download a cooked archive
 export async function fetchCookedObject(fetchUrl) {
   recookTask = null;
   // first, check if the link is still available from the vault
   const response = await fetch(fetchUrl);
 
   // link is still alive, proceed to download
   if (response.ok) {
     $('#vault-fetch-iframe').attr('src', fetchUrl);
     // link is dead
   } else {
     // get the associated cooking task
-    let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
+    const vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
     for (let i = 0; i < vaultCookingTasks.length; ++i) {
       if (vaultCookingTasks[i].fetch_url === fetchUrl) {
         recookTask = vaultCookingTasks[i];
         break;
       }
     }
     // display a modal asking the user if he wants to recook the archive
     $('#vault-recook-object-modal').modal('show');
   }
 }
 
 // called when the user wants to recook an archive
 // for which the download link is not available anymore
 export async function recookObject() {
   if (recookTask) {
     // stop cooking tasks status polling
     clearTimeout(checkVaultId);
     // build cook request url
     let cookingUrl;
     if (recookTask.object_type === 'directory') {
       cookingUrl = Urls.api_1_vault_cook_directory(recookTask.object_id);
     } else {
       cookingUrl = Urls.api_1_vault_cook_revision_gitfast(recookTask.object_id);
     }
     if (recookTask.email) {
       cookingUrl += '?email=' + recookTask.email;
     }
     try {
     // request archive cooking
       const response = await csrfPost(cookingUrl);
       handleFetchError(response);
 
       // update task status
       recookTask.status = 'new';
-      let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
+      const vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
       for (let i = 0; i < vaultCookingTasks.length; ++i) {
         if (vaultCookingTasks[i].object_id === recookTask.object_id) {
           vaultCookingTasks[i] = recookTask;
           break;
         }
       }
       // save updated tasks to local storage
       localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
       // restart cooking tasks status polling
       checkVaultCookingTasks();
       // hide recook archive modal
       $('#vault-recook-object-modal').modal('hide');
     } catch (_) {
       // something went wrong
       checkVaultCookingTasks();
       $('#vault-recook-object-modal').modal('hide');
     }
   }
 }
 
 async function checkVaultCookingTasks() {
-  let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
+  const vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
   if (!vaultCookingTasks || vaultCookingTasks.length === 0) {
     $('.swh-vault-table tbody tr').remove();
     checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval);
     return;
   }
-  let cookingTaskRequests = [];
-  let tasks = {};
-  let currentObjectIds = [];
+  const cookingTaskRequests = [];
+  const tasks = {};
+  const currentObjectIds = [];
 
   for (let i = 0; i < vaultCookingTasks.length; ++i) {
-    let cookingTask = vaultCookingTasks[i];
+    const cookingTask = vaultCookingTasks[i];
     currentObjectIds.push(cookingTask.object_id);
     tasks[cookingTask.object_id] = cookingTask;
     let cookingUrl;
     if (cookingTask.object_type === 'directory') {
       cookingUrl = Urls.api_1_vault_cook_directory(cookingTask.object_id);
     } else {
       cookingUrl = Urls.api_1_vault_cook_revision_gitfast(cookingTask.object_id);
     }
     if (cookingTask.status !== 'done' && cookingTask.status !== 'failed') {
       cookingTaskRequests.push(fetch(cookingUrl));
     }
   }
   $('.swh-vault-table tbody tr').each((i, row) => {
-    let objectId = $(row).find('.vault-object-info').data('object-id');
+    const objectId = $(row).find('.vault-object-info').data('object-id');
     if ($.inArray(objectId, currentObjectIds) === -1) {
       $(row).remove();
     }
   });
   try {
     const responses = await Promise.all(cookingTaskRequests);
     handleFetchErrors(responses);
     const cookingTasks = await Promise.all(responses.map(r => r.json()));
 
-    let table = $('#vault-cooking-tasks tbody');
+    const table = $('#vault-cooking-tasks tbody');
     for (let i = 0; i < cookingTasks.length; ++i) {
-      let cookingTask = tasks[cookingTasks[i].obj_id];
+      const cookingTask = tasks[cookingTasks[i].obj_id];
       cookingTask.status = cookingTasks[i].status;
       cookingTask.fetch_url = cookingTasks[i].fetch_url;
       cookingTask.progress_message = cookingTasks[i].progress_message;
     }
     for (let i = 0; i < vaultCookingTasks.length; ++i) {
-      let cookingTask = vaultCookingTasks[i];
-      let rowTask = $(`#vault-task-${cookingTask.object_id}`);
+      const cookingTask = vaultCookingTasks[i];
+      const rowTask = $(`#vault-task-${cookingTask.object_id}`);
 
       if (!rowTask.length) {
 
         let browseUrl = cookingTask.browse_url;
         if (!browseUrl) {
           if (cookingTask.object_type === 'directory') {
             browseUrl = Urls.browse_directory(cookingTask.object_id);
           } else {
             browseUrl = Urls.browse_revision(cookingTask.object_id);
           }
         }
 
-        let progressBar = $.parseHTML(progress)[0];
-        let progressBarContent = $(progressBar).find('.progress-bar');
+        const progressBar = $.parseHTML(progress)[0];
+        const progressBarContent = $(progressBar).find('.progress-bar');
         updateProgressBar(progressBarContent, cookingTask);
         table.prepend(vaultTableRowTemplate({
           browseUrl: browseUrl,
           cookingTask: cookingTask,
           progressBar: progressBar,
           Urls: Urls,
           swh: swh
         }));
       } else {
-        let progressBar = rowTask.find('.progress-bar');
+        const progressBar = rowTask.find('.progress-bar');
         updateProgressBar(progressBar, cookingTask);
-        let downloadLink = rowTask.find('.vault-dl-link');
+        const downloadLink = rowTask.find('.vault-dl-link');
         if (cookingTask.status === 'done') {
           downloadLink[0].innerHTML =
               '<button class="btn btn-default btn-sm" ' +
               `onclick="swh.vault.fetchCookedObject('${cookingTask.fetch_url}')">` +
               '<i class="mdi mdi-download mdi-fw" aria-hidden="true"></i>Download</button>';
         } else {
           downloadLink[0].innerHTML = '';
         }
       }
     }
     localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
     checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval);
 
   } catch (error) {
     console.log('Error when fetching vault cooking tasks:', error);
   }
 }
 
 export function removeCookingTaskInfo(tasksToRemove) {
   let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
   if (!vaultCookingTasks) {
     return;
   }
   vaultCookingTasks = $.grep(vaultCookingTasks, task => {
     return $.inArray(task.object_id, tasksToRemove) === -1;
   });
   localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
 }
 
 export function initUi() {
 
   $('#vault-tasks-toggle-selection').change(event => {
     $('.vault-task-toggle-selection').prop('checked', event.currentTarget.checked);
   });
 
   $('#vault-remove-tasks').click(() => {
     clearTimeout(checkVaultId);
-    let tasksToRemove = [];
+    const tasksToRemove = [];
     $('.swh-vault-table tbody tr').each((i, row) => {
-      let taskSelected = $(row).find('.vault-task-toggle-selection').prop('checked');
+      const taskSelected = $(row).find('.vault-task-toggle-selection').prop('checked');
       if (taskSelected) {
-        let objectId = $(row).find('.vault-object-info').data('object-id');
+        const objectId = $(row).find('.vault-object-info').data('object-id');
         tasksToRemove.push(objectId);
         $(row).remove();
       }
     });
     removeCookingTaskInfo(tasksToRemove);
     $('#vault-tasks-toggle-selection').prop('checked', false);
     checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval);
   });
 
   checkVaultCookingTasks();
 
   window.onfocus = () => {
     clearTimeout(checkVaultId);
     checkVaultCookingTasks();
   };
 
 }
diff --git a/assets/src/bundles/webapp/code-highlighting.js b/assets/src/bundles/webapp/code-highlighting.js
index d9505c30..5a873869 100644
--- a/assets/src/bundles/webapp/code-highlighting.js
+++ b/assets/src/bundles/webapp/code-highlighting.js
@@ -1,114 +1,114 @@
 /**
  * Copyright (C) 2018-2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {removeUrlFragment} from 'utils/functions';
 
 export async function highlightCode(showLineNumbers = true) {
 
   await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
 
   // keep track of the first highlighted line
   let firstHighlightedLine = null;
   // highlighting color
-  let lineHighlightColor = 'rgb(193, 255, 193)';
+  const lineHighlightColor = 'rgb(193, 255, 193)';
 
   // function to highlight a line
   function highlightLine(i) {
-    let lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`);
+    const lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`);
     lineTd.css('background-color', lineHighlightColor);
     return lineTd;
   }
 
   // function to reset highlighting
   function resetHighlightedLines() {
     firstHighlightedLine = null;
     $('.hljs-ln-line[data-line-number]').css('background-color', 'inherit');
   }
 
   function scrollToLine(lineDomElt) {
     if ($(lineDomElt).closest('.swh-content').length > 0) {
       $('html, body').animate({
         scrollTop: $(lineDomElt).offset().top - 70
       }, 500);
     }
   }
 
   // function to highlight lines based on a url fragment
   // in the form '#Lx' or '#Lx-Ly'
   function parseUrlFragmentForLinesToHighlight() {
-    let lines = [];
-    let linesRegexp = new RegExp(/L(\d+)/g);
+    const lines = [];
+    const linesRegexp = new RegExp(/L(\d+)/g);
     let line = linesRegexp.exec(window.location.hash);
     if (line === null) {
       return;
     }
     while (line) {
       lines.push(parseInt(line[1]));
       line = linesRegexp.exec(window.location.hash);
     }
     resetHighlightedLines();
     if (lines.length === 1) {
       firstHighlightedLine = parseInt(lines[0]);
       scrollToLine(highlightLine(lines[0]));
     } else if (lines[0] < lines[lines.length - 1]) {
       firstHighlightedLine = parseInt(lines[0]);
       scrollToLine(highlightLine(lines[0]));
       for (let i = lines[0] + 1; i <= lines[lines.length - 1]; ++i) {
         highlightLine(i);
       }
     }
   }
 
   $(document).ready(() => {
     // highlight code and add line numbers
     $('code').each((i, elt) => {
       hljs.highlightElement(elt);
       if (showLineNumbers) {
         hljs.lineNumbersElement(elt, {singleLine: true});
       }
     });
 
     if (!showLineNumbers) {
       return;
     }
 
     // click handler to dynamically highlight line(s)
     // when the user clicks on a line number (lines range
     // can also be highlighted while holding the shift key)
     $('.swh-content').click(evt => {
       if (evt.target.classList.contains('hljs-ln-n')) {
-        let line = parseInt($(evt.target).data('line-number'));
+        const line = parseInt($(evt.target).data('line-number'));
         if (evt.shiftKey && firstHighlightedLine && line > firstHighlightedLine) {
-          let firstLine = firstHighlightedLine;
+          const firstLine = firstHighlightedLine;
           resetHighlightedLines();
           for (let i = firstLine; i <= line; ++i) {
             highlightLine(i);
           }
           firstHighlightedLine = firstLine;
           window.location.hash = `#L${firstLine}-L${line}`;
         } else {
           resetHighlightedLines();
           highlightLine(line);
           window.location.hash = `#L${line}`;
           scrollToLine(evt.target);
         }
       } else if ($(evt.target).closest('.hljs-ln').length) {
         resetHighlightedLines();
         removeUrlFragment();
       }
     });
 
     // update lines highlighting when the url fragment changes
     $(window).on('hashchange', () => parseUrlFragmentForLinesToHighlight());
 
     // schedule lines highlighting if any as hljs.lineNumbersElement() is async
     setTimeout(() => {
       parseUrlFragmentForLinesToHighlight();
     });
 
   });
 }
diff --git a/assets/src/bundles/webapp/notebook-rendering.js b/assets/src/bundles/webapp/notebook-rendering.js
index 6bc1d3d1..73578002 100644
--- a/assets/src/bundles/webapp/notebook-rendering.js
+++ b/assets/src/bundles/webapp/notebook-rendering.js
@@ -1,136 +1,136 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import 'script-loader!notebookjs';
 import AnsiUp from 'ansi_up';
 import './notebook.css';
 
 const ansiup = new AnsiUp();
 ansiup.escape_for_html = false;
 
 function escapeHTML(text) {
   text = text.replace(/</g, '&lt;');
   text = text.replace(/>/g, '&gt;');
   return text;
 }
 
 function unescapeHTML(text) {
   text = text.replace(/&lt;/g, '<');
   text = text.replace(/&gt;/g, '>');
   return text;
 }
 
 function escapeLaTeX(text) {
 
-  let blockMath = /\$\$(.+?)\$\$|\\\\\[(.+?)\\\\\]/msg;
-  let inlineMath = /\$(.+?)\$|\\\\\((.+?)\\\\\)/g;
-  let latexEnvironment = /\\begin\{([a-z]*\*?)\}(.+?)\\end\{\1\}/msg;
+  const blockMath = /\$\$(.+?)\$\$|\\\\\[(.+?)\\\\\]/msg;
+  const inlineMath = /\$(.+?)\$|\\\\\((.+?)\\\\\)/g;
+  const latexEnvironment = /\\begin\{([a-z]*\*?)\}(.+?)\\end\{\1\}/msg;
 
-  let mathTextFound = [];
+  const mathTextFound = [];
   let bm;
   while ((bm = blockMath.exec(text)) !== null) {
     mathTextFound.push(bm[1]);
   }
 
   let im;
   while ((im = inlineMath.exec(text)) !== null) {
     mathTextFound.push(im[1]);
   }
 
   let le;
   while ((le = latexEnvironment.exec(text)) !== null) {
     mathTextFound.push(le[1]);
   }
 
-  for (let mathText of mathTextFound) {
+  for (const mathText of mathTextFound) {
     // showdown will remove line breaks in LaTex array and
     // some escaping sequences when converting md to html.
     // So we use the following escaping hacks to keep them in the html
     // output and avoid MathJax typesetting errors.
     let escapedText = mathText.replace('\\\\', '\\\\\\\\');
-    for (let specialLaTexChar of ['{', '}', '#', '%', '&', '_']) {
+    for (const specialLaTexChar of ['{', '}', '#', '%', '&', '_']) {
       escapedText = escapedText.replace(new RegExp(`\\\\${specialLaTexChar}`, 'g'),
                                         `\\\\${specialLaTexChar}`);
     }
 
     // some html escaping is also needed
     escapedText = escapeHTML(escapedText);
 
     // hack to prevent showdown to replace _ characters
     // by html em tags as it will break some math typesetting
     // (setting the literalMidWordUnderscores option is not
     // enough as iy only works for _ characters contained in words)
     escapedText = escapedText.replace(/_/g, '{@}underscore{@}');
 
     if (mathText !== escapedText) {
       text = text.replace(mathText, escapedText);
     }
   }
 
   return text;
 }
 
 export async function renderNotebook(nbJsonUrl, domElt) {
 
-  let showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
+  const showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
 
   await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
 
   function renderMarkdown(text) {
-    let converter = new showdown.Converter({
+    const converter = new showdown.Converter({
       tables: true,
       simplifiedAutoLink: true,
       rawHeaderId: true,
       literalMidWordUnderscores: true
     });
 
     // some LaTeX escaping is required to get correct math typesetting
     text = escapeLaTeX(text);
 
     // render markdown
     let rendered = converter.makeHtml(text);
 
     // restore underscores in rendered HTML (see escapeLaTeX function)
     rendered = rendered.replace(/{@}underscore{@}/g, '_');
 
     return rendered;
   }
 
   function highlightCode(text, preElt, codeElt, lang) {
     // no need to unescape text processed by ansiup
     if (text.indexOf('<span style="color:rgb(') === -1) {
       text = unescapeHTML(text);
     }
     if (lang && hljs.getLanguage(lang)) {
       return hljs.highlight(text, {language: lang}).value;
     } else {
       return text;
     }
   }
 
   function renderAnsi(text) {
     return ansiup.ansi_to_html(text);
   }
 
   nb.markdown = renderMarkdown;
   nb.highlighter = highlightCode;
   nb.ansi = renderAnsi;
 
   const response = await fetch(nbJsonUrl);
   const nbJson = await response.json();
 
   // parse the notebook
-  let notebook = nb.parse(nbJson);
+  const notebook = nb.parse(nbJson);
   // render it to HTML and apply XSS filtering
-  let rendered = swh.webapp.filterXSS(notebook.render());
+  const rendered = swh.webapp.filterXSS(notebook.render());
   // insert rendered notebook in the DOM
   $(domElt).append(rendered);
   // set light red background color for stderr output cells
   $('pre.nb-stderr').parent().css('background', '#fdd');
   // load MathJax library for math typesetting
   swh.webapp.typesetMath();
 }
diff --git a/assets/src/bundles/webapp/pdf-rendering.js b/assets/src/bundles/webapp/pdf-rendering.js
index c92be0d0..f6c2e88e 100644
--- a/assets/src/bundles/webapp/pdf-rendering.js
+++ b/assets/src/bundles/webapp/pdf-rendering.js
@@ -1,107 +1,107 @@
 /**
  * Copyright (C) 2018-2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 // adapted from pdf.js examples located at http://mozilla.github.io/pdf.js/examples/
 
 import {staticAsset} from 'utils/functions';
 
 export async function renderPdf(pdfUrl) {
 
   let pdfDoc = null;
   let pageNum = 1;
   let pageRendering = false;
   let pageNumPending = null;
-  let defaultScale = 1.5;
-  let canvas = $('#pdf-canvas')[0];
-  let ctx = canvas.getContext('2d');
+  const defaultScale = 1.5;
+  const canvas = $('#pdf-canvas')[0];
+  const ctx = canvas.getContext('2d');
 
   // Get page info from document, resize canvas accordingly, and render page.
   async function renderPage(num) {
     pageRendering = true;
     // Using promise to fetch the page
     const page = await pdfDoc.getPage(num);
 
-    let divWidth = $('.swh-content').width();
-    let scale = Math.min(defaultScale, divWidth / page.getViewport({scale: 1.0}).width);
+    const divWidth = $('.swh-content').width();
+    const scale = Math.min(defaultScale, divWidth / page.getViewport({scale: 1.0}).width);
 
-    let viewport = page.getViewport({scale: scale});
+    const viewport = page.getViewport({scale: scale});
     canvas.width = viewport.width;
     canvas.height = viewport.height;
 
     // Render PDF page into canvas context
     const renderContext = {
       canvasContext: ctx,
       viewport: viewport
     };
 
     // Wait for rendering to finish
     await page.render(renderContext);
 
     pageRendering = false;
     if (pageNumPending !== null) {
       // New page rendering is pending
       renderPage(pageNumPending);
       pageNumPending = null;
     }
 
     // Update page counters
     $('#pdf-page-num').text(num);
   }
 
   // If another page rendering in progress, waits until the rendering is
   // finished. Otherwise, executes rendering immediately.
   function queueRenderPage(num) {
     if (pageRendering) {
       pageNumPending = num;
     } else {
       renderPage(num);
     }
   }
 
   // Displays previous page.
   function onPrevPage() {
     if (pageNum <= 1) {
       return;
     }
     pageNum--;
     queueRenderPage(pageNum);
   }
 
   // Displays next page.
   function onNextPage() {
     if (pageNum >= pdfDoc.numPages) {
       return;
     }
     pageNum++;
     queueRenderPage(pageNum);
   }
 
-  let pdfjs = await import(/* webpackChunkName: "pdfjs" */ 'pdfjs-dist');
+  const pdfjs = await import(/* webpackChunkName: "pdfjs" */ 'pdfjs-dist');
 
   pdfjs.GlobalWorkerOptions.workerSrc = staticAsset('js/pdf.worker.min.js');
 
   $(document).ready(async() => {
     $('#pdf-prev').click(onPrevPage);
     $('#pdf-next').click(onNextPage);
     try {
       const pdf = await pdfjs.getDocument(pdfUrl).promise;
       pdfDoc = pdf;
       $('#pdf-page-count').text(pdfDoc.numPages);
       // Initial/first page rendering
       renderPage(pageNum);
     } catch (reason) {
       // PDF loading error
       console.error(reason);
     }
 
     // Render PDF on resize
     $(window).on('resize', function() {
       queueRenderPage(pageNum);
     });
   });
 
 }
diff --git a/assets/src/bundles/webapp/readme-rendering.js b/assets/src/bundles/webapp/readme-rendering.js
index 2651f6c7..3cb56fae 100644
--- a/assets/src/bundles/webapp/readme-rendering.js
+++ b/assets/src/bundles/webapp/readme-rendering.js
@@ -1,118 +1,118 @@
 /**
  * Copyright (C) 2018-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {handleFetchError} from 'utils/functions';
 
 import {decode} from 'html-encoder-decoder';
 
 export async function renderMarkdown(domElt, markdownDocUrl) {
 
-  let showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
+  const showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
   await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
 
   // Adapted from https://github.com/Bloggify/showdown-highlight
   // Copyright (c) 2016-19 Bloggify <support@bloggify.org> (https://bloggify.org)
   function showdownHighlight() {
     return [{
       type: 'output',
       filter: function(text, converter, options) {
-        let left = '<pre><code\\b[^>]*>';
-        let right = '</code></pre>';
-        let flags = 'g';
-        let classAttr = 'class="';
-        let replacement = (wholeMatch, match, left, right) => {
+        const left = '<pre><code\\b[^>]*>';
+        const right = '</code></pre>';
+        const flags = 'g';
+        const classAttr = 'class="';
+        const replacement = (wholeMatch, match, left, right) => {
           match = decode(match);
-          let lang = (left.match(/class="([^ "]+)/) || [])[1];
+          const lang = (left.match(/class="([^ "]+)/) || [])[1];
 
           if (left.includes(classAttr)) {
-            let attrIndex = left.indexOf(classAttr) + classAttr.length;
+            const attrIndex = left.indexOf(classAttr) + classAttr.length;
             left = left.slice(0, attrIndex) + 'hljs ' + left.slice(attrIndex);
           } else {
             left = left.slice(0, -1) + ' class="hljs">';
           }
 
           if (lang && hljs.getLanguage(lang)) {
             return left + hljs.highlight(match, {language: lang}).value + right;
           } else {
             return left + match + right;
           }
         };
 
         return showdown.helper.replaceRecursiveRegExp(text, replacement, left, right, flags);
       }
     }];
   }
 
   $(document).ready(async() => {
-    let converter = new showdown.Converter({
+    const converter = new showdown.Converter({
       tables: true,
       extensions: [showdownHighlight]
     });
 
     try {
       const response = await fetch(markdownDocUrl);
       handleFetchError(response);
       const data = await response.text();
       $(domElt).addClass('swh-showdown');
       $(domElt).html(swh.webapp.filterXSS(converter.makeHtml(data)));
     } catch (_) {
       $(domElt).text('Readme bytes are not available');
     }
   });
 
 }
 
 export async function renderOrgData(domElt, orgDocData) {
 
-  let org = await import(/* webpackChunkName: "org" */ 'utils/org');
+  const org = await import(/* webpackChunkName: "org" */ 'utils/org');
 
-  let parser = new org.Parser();
-  let orgDocument = parser.parse(orgDocData, {toc: false});
-  let orgHTMLDocument = orgDocument.convert(org.ConverterHTML, {});
+  const parser = new org.Parser();
+  const orgDocument = parser.parse(orgDocData, {toc: false});
+  const orgHTMLDocument = orgDocument.convert(org.ConverterHTML, {});
   $(domElt).addClass('swh-org');
   $(domElt).html(swh.webapp.filterXSS(orgHTMLDocument.toString()));
   // remove toc and section numbers to get consistent
   // with other readme renderings
   $('.swh-org ul').first().remove();
   $('.section-number').remove();
 
 }
 
 export function renderOrg(domElt, orgDocUrl) {
   $(document).ready(async() => {
     try {
       const response = await fetch(orgDocUrl);
       handleFetchError(response);
       const data = await response.text();
       renderOrgData(domElt, data);
     } catch (_) {
       $(domElt).text('Readme bytes are not available');
     }
   });
 }
 
 export function renderTxt(domElt, txtDocUrl) {
   $(document).ready(async() => {
     try {
       const response = await fetch(txtDocUrl);
       handleFetchError(response);
       const data = await response.text();
 
-      let orgMode = '-*- mode: org -*-';
+      const orgMode = '-*- mode: org -*-';
       if (data.indexOf(orgMode) !== -1) {
         renderOrgData(domElt, data.replace(orgMode, ''));
       } else {
         $(domElt).addClass('swh-readme-txt');
         $(domElt)
             .html('')
             .append($('<pre></pre>').text(data));
       }
     } catch (_) {
       $(domElt).text('Readme bytes are not available');
     }
   });
 }
diff --git a/assets/src/bundles/webapp/status-widget.js b/assets/src/bundles/webapp/status-widget.js
index 7eb31b3b..f78c72dd 100644
--- a/assets/src/bundles/webapp/status-widget.js
+++ b/assets/src/bundles/webapp/status-widget.js
@@ -1,50 +1,50 @@
 /**
  * Copyright (C) 2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import './status-widget.css';
 
 const statusCodeColor = {
   '100': 'green', // Operational
   '200': 'blue', // Scheduled Maintenance
   '300': 'yellow', // Degraded Performance
   '400': 'yellow', // Partial Service Disruption
   '500': 'red', // Service Disruption
   '600': 'red' // Security Event
 };
 
 export function initStatusWidget(statusDataURL) {
   $('.swh-current-status-indicator').ready(async() => {
     let maxStatusCode = '';
     let maxStatusDescription = '';
     let sc = '';
     let sd = '';
     try {
       const response = await fetch(statusDataURL);
       const data = await response.json();
 
-      for (let s of data.result.status) {
+      for (const s of data.result.status) {
         sc = s.status_code;
         sd = s.status;
         if (maxStatusCode < sc) {
           maxStatusCode = sc;
           maxStatusDescription = sd;
         }
       }
       if (maxStatusCode === '') {
         $('.swh-current-status').remove();
         return;
       }
       $('.swh-current-status-indicator').removeClass('green');
       $('.swh-current-status-indicator').addClass(statusCodeColor[maxStatusCode]);
       $('#swh-current-status-description').text(maxStatusDescription);
 
     } catch (e) {
       console.log(e);
       $('.swh-current-status').remove();
     }
   });
 }
diff --git a/assets/src/bundles/webapp/webapp-utils.js b/assets/src/bundles/webapp/webapp-utils.js
index 797a2469..79a65270 100644
--- a/assets/src/bundles/webapp/webapp-utils.js
+++ b/assets/src/bundles/webapp/webapp-utils.js
@@ -1,400 +1,400 @@
 /**
  * Copyright (C) 2018-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import objectFitImages from 'object-fit-images';
 import {selectText} from 'utils/functions';
 import {BREAKPOINT_MD} from 'utils/constants';
 
 let collapseSidebar = false;
-let previousSidebarState = localStorage.getItem('remember.lte.pushmenu');
+const previousSidebarState = localStorage.getItem('remember.lte.pushmenu');
 if (previousSidebarState !== undefined) {
   collapseSidebar = previousSidebarState === 'sidebar-collapse';
 }
 
 $(document).on('DOMContentLoaded', () => {
   // set state to collapsed on smaller devices
   if ($(window).width() < BREAKPOINT_MD) {
     collapseSidebar = true;
   }
 
   // restore previous sidebar state (collapsed/expanded)
   if (collapseSidebar) {
     // hack to avoid animated transition for collapsing sidebar
     // when loading a page
-    let sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition');
-    let sidebarEltsTransition = $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition');
+    const sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition');
+    const sidebarEltsTransition = $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition');
     $('.main-sidebar, .main-sidebar:before').css('transition', 'none');
     $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', 'none');
     $('body').addClass('sidebar-collapse');
     $('.swh-words-logo-swh').css('visibility', 'visible');
     // restore transitions for user navigation
     setTimeout(() => {
       $('.main-sidebar, .main-sidebar:before').css('transition', sidebarTransition);
       $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', sidebarEltsTransition);
     });
   }
 });
 
 $(document).on('collapsed.lte.pushmenu', event => {
   if ($('body').width() >= BREAKPOINT_MD) {
     $('.swh-words-logo-swh').css('visibility', 'visible');
   }
 });
 
 $(document).on('shown.lte.pushmenu', event => {
   $('.swh-words-logo-swh').css('visibility', 'hidden');
 });
 
 function ensureNoFooterOverflow() {
   $('body').css('padding-bottom', $('footer').outerHeight() + 'px');
 }
 
 $(document).ready(() => {
   // redirect to last browse page if any when clicking on the 'Browse' entry
   // in the sidebar
   $(`.swh-browse-link`).click(event => {
-    let lastBrowsePage = sessionStorage.getItem('last-browse-page');
+    const lastBrowsePage = sessionStorage.getItem('last-browse-page');
     if (lastBrowsePage) {
       event.preventDefault();
       window.location = lastBrowsePage;
     }
   });
 
   const mainSideBar = $('.main-sidebar');
 
   function updateSidebarState() {
     const body = $('body');
     if (body.hasClass('sidebar-collapse') &&
         !mainSideBar.hasClass('swh-sidebar-collapsed')) {
       mainSideBar.removeClass('swh-sidebar-expanded');
       mainSideBar.addClass('swh-sidebar-collapsed');
       $('.swh-words-logo-swh').css('visibility', 'visible');
     } else if (!body.hasClass('sidebar-collapse') &&
                !mainSideBar.hasClass('swh-sidebar-expanded')) {
       mainSideBar.removeClass('swh-sidebar-collapsed');
       mainSideBar.addClass('swh-sidebar-expanded');
       $('.swh-words-logo-swh').css('visibility', 'hidden');
     }
     // ensure correct sidebar state when loading a page
     if (body.hasClass('hold-transition')) {
       setTimeout(() => {
         updateSidebarState();
       });
     }
   }
 
   // set sidebar state after collapse / expand animation
   mainSideBar.on('transitionend', evt => {
     updateSidebarState();
   });
 
   updateSidebarState();
 
   // ensure footer do not overflow main content for mobile devices
   // or after resizing the browser window
   ensureNoFooterOverflow();
   $(window).resize(function() {
     ensureNoFooterOverflow();
     if ($('body').hasClass('sidebar-collapse') && $('body').width() >= BREAKPOINT_MD) {
       $('.swh-words-logo-swh').css('visibility', 'visible');
     }
   });
   // activate css polyfill 'object-fit: contain' in old browsers
   objectFitImages();
 
   // reparent the modals to the top navigation div in order to be able
   // to display them
   $('.swh-browse-top-navigation').append($('.modal'));
 
   let selectedCode = null;
 
   function getCodeOrPreEltUnderPointer(e) {
-    let elts = document.elementsFromPoint(e.clientX, e.clientY);
-    for (let elt of elts) {
+    const elts = document.elementsFromPoint(e.clientX, e.clientY);
+    for (const elt of elts) {
       if (elt.nodeName === 'CODE' || elt.nodeName === 'PRE') {
         return elt;
       }
     }
     return null;
   }
 
   // click handler to set focus on code block for copy
   $(document).click(e => {
     selectedCode = getCodeOrPreEltUnderPointer(e);
   });
 
   function selectCode(event, selectedCode) {
     if (selectedCode) {
-      let hljsLnCodeElts = $(selectedCode).find('.hljs-ln-code');
+      const hljsLnCodeElts = $(selectedCode).find('.hljs-ln-code');
       if (hljsLnCodeElts.length) {
         selectText(hljsLnCodeElts[0], hljsLnCodeElts[hljsLnCodeElts.length - 1]);
       } else {
         selectText(selectedCode.firstChild, selectedCode.lastChild);
       }
       event.preventDefault();
     }
   }
 
   // select the whole text of focused code block when user
   // double clicks or hits Ctrl+A
   $(document).dblclick(e => {
     if ((e.ctrlKey || e.metaKey)) {
       selectCode(e, getCodeOrPreEltUnderPointer(e));
     }
   });
 
   $(document).keydown(e => {
     if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
       selectCode(e, selectedCode);
     }
   });
 
   // show/hide back-to-top button
   let scrollThreshold = 0;
   scrollThreshold += $('.swh-top-bar').height() || 0;
   scrollThreshold += $('.navbar').height() || 0;
   $(window).scroll(() => {
     if ($(window).scrollTop() > scrollThreshold) {
       $('#back-to-top').css('display', 'block');
     } else {
       $('#back-to-top').css('display', 'none');
     }
   });
 
   // navbar search form submission callback
   $('#swh-origins-search-top').submit(event => {
     event.preventDefault();
     if (event.target.checkValidity()) {
       $(event.target).removeClass('was-validated');
-      let searchQueryText = $('#swh-origins-search-top-input').val().trim();
-      let queryParameters = new URLSearchParams();
+      const searchQueryText = $('#swh-origins-search-top-input').val().trim();
+      const queryParameters = new URLSearchParams();
       queryParameters.append('q', searchQueryText);
       queryParameters.append('with_visit', true);
       queryParameters.append('with_content', true);
       window.location = `${Urls.browse_search()}?${queryParameters.toString()}`;
     } else {
       $(event.target).addClass('was-validated');
     }
   });
 });
 
 export function initPage(page) {
 
   $(document).ready(() => {
     // set relevant sidebar link to page active
     $(`.swh-${page}-item`).addClass('active');
     $(`.swh-${page}-link`).addClass('active');
 
     // triggered when unloading the current page
     $(window).on('unload', () => {
       // backup current browse page
       if (page === 'browse') {
         sessionStorage.setItem('last-browse-page', window.location);
       }
     });
   });
 }
 
 export function initHomePage() {
   $(document).ready(async() => {
     $('.swh-coverage-list').iFrameResize({heightCalculationMethod: 'taggedElement'});
     const response = await fetch(Urls.stat_counters());
     const data = await response.json();
 
     if (data.stat_counters && !$.isEmptyObject(data.stat_counters)) {
-      for (let objectType of ['content', 'revision', 'origin', 'directory', 'person', 'release']) {
+      for (const objectType of ['content', 'revision', 'origin', 'directory', 'person', 'release']) {
         const count = data.stat_counters[objectType];
         if (count !== undefined) {
           $(`#swh-${objectType}-count`).html(count.toLocaleString());
         } else {
           $(`#swh-${objectType}-count`).closest('.swh-counter-container').hide();
         }
       }
     } else {
       $('.swh-counter').html('0');
     }
     if (data.stat_counters_history && !$.isEmptyObject(data.stat_counters_history)) {
-      for (let objectType of ['content', 'revision', 'origin']) {
+      for (const objectType of ['content', 'revision', 'origin']) {
         const history = data.stat_counters_history[objectType];
         if (history) {
           swh.webapp.drawHistoryCounterGraph(`#swh-${objectType}-count-history`, history);
         } else {
           $(`#swh-${objectType}-count-history`).hide();
         }
 
       }
     } else {
       $('.swh-counter-history').hide();
     }
   });
   initPage('home');
 }
 
 export function showModalMessage(title, message) {
   $('#swh-web-modal-message .modal-title').text(title);
   $('#swh-web-modal-message .modal-content p').text(message);
   $('#swh-web-modal-message').modal('show');
 }
 
 export function showModalConfirm(title, message, callback) {
   $('#swh-web-modal-confirm .modal-title').text(title);
   $('#swh-web-modal-confirm .modal-content p').text(message);
   $('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').bind('click', () => {
     callback();
     $('#swh-web-modal-confirm').modal('hide');
     $('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').unbind('click');
   });
   $('#swh-web-modal-confirm').modal('show');
 }
 
 export function showModalHtml(title, html) {
   $('#swh-web-modal-html .modal-title').text(title);
   $('#swh-web-modal-html .modal-body').html(html);
   $('#swh-web-modal-html').modal('show');
 }
 
 export function addJumpToPagePopoverToDataTable(dataTableElt) {
   dataTableElt.on('draw.dt', function() {
     $('.paginate_button.disabled').css('cursor', 'pointer');
     $('.paginate_button.disabled').on('click', event => {
       const pageInfo = dataTableElt.page.info();
       let content = '<select class="jump-to-page">';
       for (let i = 1; i <= pageInfo.pages; ++i) {
         let selected = '';
         if (i === pageInfo.page + 1) {
           selected = 'selected';
         }
         content += `<option value="${i}" ${selected}>${i}</option>`;
       }
       content += `</select><span> / ${pageInfo.pages}</span>`;
       $(event.target).popover({
         'title': 'Jump to page',
         'content': content,
         'html': true,
         'placement': 'top',
         'sanitizeFn': swh.webapp.filterXSS
       });
       $(event.target).popover('show');
       $('.jump-to-page').on('change', function() {
         $('.paginate_button.disabled').popover('hide');
         const pageNumber = parseInt($(this).val()) - 1;
         dataTableElt.page(pageNumber).draw('page');
       });
     });
   });
 
   dataTableElt.on('preXhr.dt', () => {
     $('.paginate_button.disabled').popover('hide');
   });
 }
 
 let swhObjectIcons;
 
 export function setSwhObjectIcons(icons) {
   swhObjectIcons = icons;
 }
 
 export function getSwhObjectIcon(swhObjectType) {
   return swhObjectIcons[swhObjectType];
 }
 
 let browsedSwhObjectMetadata = {};
 
 export function setBrowsedSwhObjectMetadata(metadata) {
   browsedSwhObjectMetadata = metadata;
 }
 
 export function getBrowsedSwhObjectMetadata() {
   return browsedSwhObjectMetadata;
 }
 
 // This will contain a mapping between an archived object type
 // and its related SWHID metadata for each object reachable from
 // the current browse view.
 // SWHID metadata contain the following keys:
 //   * object_type: type of archived object
 //   * object_id: sha1 object identifier
 //   * swhid: SWHID without contextual info
 //   * swhid_url: URL to resolve SWHID without contextual info
 //   * context: object describing SWHID context
 //   * swhid_with_context: SWHID with contextual info
 //   * swhid_with_context_url: URL to resolve SWHID with contextual info
 let swhidsContext_ = {};
 
 export function setSwhIdsContext(swhidsContext) {
   swhidsContext_ = {};
-  for (let swhidContext of swhidsContext) {
+  for (const swhidContext of swhidsContext) {
     swhidsContext_[swhidContext.object_type] = swhidContext;
   }
 }
 
 export function getSwhIdsContext() {
   return swhidsContext_;
 }
 
 function setFullWidth(fullWidth) {
   if (fullWidth) {
     $('#swh-web-content').removeClass('container');
     $('#swh-web-content').addClass('container-fluid');
   } else {
     $('#swh-web-content').removeClass('container-fluid');
     $('#swh-web-content').addClass('container');
   }
   localStorage.setItem('swh-web-full-width', JSON.stringify(fullWidth));
   $('#swh-full-width-switch').prop('checked', fullWidth);
 }
 
 export function fullWidthToggled(event) {
   setFullWidth($(event.target).prop('checked'));
 }
 
 export function setContainerFullWidth() {
-  let previousFullWidthState = JSON.parse(localStorage.getItem('swh-web-full-width'));
+  const previousFullWidthState = JSON.parse(localStorage.getItem('swh-web-full-width'));
   if (previousFullWidthState !== null) {
     setFullWidth(previousFullWidthState);
   }
 }
 
 function coreSWHIDIsLowerCase(swhid) {
   const qualifiersPos = swhid.indexOf(';');
   let coreSWHID = swhid;
   if (qualifiersPos !== -1) {
     coreSWHID = swhid.slice(0, qualifiersPos);
   }
   return coreSWHID.toLowerCase() === coreSWHID;
 }
 
 export async function validateSWHIDInput(swhidInputElt) {
   const swhidInput = swhidInputElt.value.trim();
   let customValidity = '';
   if (swhidInput.toLowerCase().startsWith('swh:')) {
     if (coreSWHIDIsLowerCase(swhidInput)) {
       const resolveSWHIDUrl = Urls.api_1_resolve_swhid(swhidInput);
       const response = await fetch(resolveSWHIDUrl);
       const responseData = await response.json();
       if (responseData.hasOwnProperty('exception')) {
         customValidity = responseData.reason;
       }
     } else {
       const qualifiersPos = swhidInput.indexOf(';');
       if (qualifiersPos === -1) {
         customValidity = 'Invalid SWHID: all characters must be in lowercase. ';
         customValidity += `Valid SWHID is ${swhidInput.toLowerCase()}`;
       } else {
         customValidity = 'Invalid SWHID: the core part must be in lowercase. ';
         const coreSWHID = swhidInput.slice(0, qualifiersPos);
         customValidity += `Valid SWHID is ${swhidInput.replace(coreSWHID, coreSWHID.toLowerCase())}`;
       }
     }
   }
   swhidInputElt.setCustomValidity(customValidity);
   $(swhidInputElt).siblings('.invalid-feedback').text(customValidity);
 }
 
 export function isUserLoggedIn() {
   return JSON.parse($('#swh_user_logged_in').text());
 }
diff --git a/assets/src/bundles/webapp/xss-filtering.js b/assets/src/bundles/webapp/xss-filtering.js
index 8eb2a65c..81be4683 100644
--- a/assets/src/bundles/webapp/xss-filtering.js
+++ b/assets/src/bundles/webapp/xss-filtering.js
@@ -1,42 +1,42 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import DOMPurify from 'dompurify';
 
 // we register a hook when performing XSS filtering in order to
 // possibly replace a relative image url with the one for getting
 // the image bytes from the archive content
 DOMPurify.addHook('uponSanitizeAttribute', function(node, data) {
   if (node.nodeName === 'IMG' && data.attrName === 'src') {
 
     // image url does not need any processing here
     if (data.attrValue.startsWith('data:image') ||
         data.attrValue.startsWith('http:') ||
         data.attrValue.startsWith('https:')) {
       return;
     }
 
     // get currently browsed swh object metadata
-    let swhObjectMetadata = swh.webapp.getBrowsedSwhObjectMetadata();
+    const swhObjectMetadata = swh.webapp.getBrowsedSwhObjectMetadata();
 
     // the swh object is provided without any useful context
     // to get the image checksums from the web api
     if (!swhObjectMetadata.hasOwnProperty('directory')) {
       return;
     }
 
     // used internal endpoint as image url to possibly get the image data
     // from the archive content
     let url = Urls.browse_directory_resolve_content_path(swhObjectMetadata.directory);
     url += `?path=${data.attrValue}`;
     data.attrValue = url;
   }
 });
 
 export function filterXSS(html) {
   return DOMPurify.sanitize(html);
 }
diff --git a/assets/src/utils/functions.js b/assets/src/utils/functions.js
index f91856cf..938acda6 100644
--- a/assets/src/utils/functions.js
+++ b/assets/src/utils/functions.js
@@ -1,124 +1,124 @@
 /**
  * Copyright (C) 2018-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 // utility functions
 
 export function handleFetchError(response) {
   if (!response.ok) {
     throw response;
   }
   return response;
 }
 
 export function handleFetchErrors(responses) {
   for (let i = 0; i < responses.length; ++i) {
     if (!responses[i].ok) {
       throw responses[i];
     }
   }
   return responses;
 }
 
 export function staticAsset(asset) {
   return `${__STATIC__}${asset}`;
 }
 
 export function csrfPost(url, headers = {}, body = null) {
   headers['X-CSRFToken'] = Cookies.get('csrftoken');
   return fetch(url, {
     credentials: 'include',
     headers: headers,
     method: 'POST',
     body: body
   });
 }
 
 export function isGitRepoUrl(url, pathPrefix = '/') {
-  let allowedProtocols = ['http:', 'https:', 'git:'];
+  const allowedProtocols = ['http:', 'https:', 'git:'];
   if (allowedProtocols.find(protocol => protocol === url.protocol) === undefined) {
     return false;
   }
   if (!url.pathname.startsWith(pathPrefix)) {
     return false;
   }
-  let re = new RegExp('[\\w\\.-]+\\/?(?!=.git)(?:\\.git\\/?)?$');
+  const re = new RegExp('[\\w\\.-]+\\/?(?!=.git)(?:\\.git\\/?)?$');
   return re.test(url.pathname.slice(pathPrefix.length));
 };
 
 export function removeUrlFragment() {
   history.replaceState('', document.title, window.location.pathname + window.location.search);
 }
 
 export function selectText(startNode, endNode) {
-  let selection = window.getSelection();
+  const selection = window.getSelection();
   selection.removeAllRanges();
-  let range = document.createRange();
+  const range = document.createRange();
   range.setStart(startNode, 0);
   if (endNode.nodeName !== '#text') {
     range.setEnd(endNode, endNode.childNodes.length);
   } else {
     range.setEnd(endNode, endNode.textContent.length);
   }
   selection.addRange(range);
 }
 
 export function htmlAlert(type, message, closable = false) {
   let closeButton = '';
   let extraClasses = '';
   if (closable) {
     closeButton =
       `<button type="button" class="close" data-dismiss="alert" aria-label="Close">
         <span aria-hidden="true">&times;</span>
       </button>`;
     extraClasses = 'alert-dismissible';
   }
   return `<div class="alert alert-${type} ${extraClasses}" role="alert">${message}${closeButton}</div>`;
 }
 
 export function isValidURL(string) {
   try {
     new URL(string);
   } catch (_) {
     return false;
   }
   return true;
 }
 
 export async function isArchivedOrigin(originPath) {
   if (!isValidURL(originPath)) {
     // Not a valid URL, return immediately
     return false;
   } else {
     const response = await fetch(Urls.api_1_origin(originPath));
     return response.ok && response.status === 200; // Success response represents an archived origin
   }
 }
 
 export async function getCanonicalOriginURL(originUrl) {
   let originUrlLower = originUrl.toLowerCase();
   // github.com URL processing
   const ghUrlRegex = /^http[s]*:\/\/github.com\//;
   if (originUrlLower.match(ghUrlRegex)) {
     // remove trailing .git
     if (originUrlLower.endsWith('.git')) {
       originUrlLower = originUrlLower.slice(0, -4);
     }
     // remove trailing slash
     if (originUrlLower.endsWith('/')) {
       originUrlLower = originUrlLower.slice(0, -1);
     }
     // extract {owner}/{repo}
     const ownerRepo = originUrlLower.replace(ghUrlRegex, '');
     // fetch canonical URL from github Web API
     const ghApiResponse = await fetch(`https://api.github.com/repos/${ownerRepo}`);
     if (ghApiResponse.ok && ghApiResponse.status === 200) {
       const ghApiResponseData = await ghApiResponse.json();
       return ghApiResponseData.html_url;
     }
   }
   return originUrl;
 }
diff --git a/cypress/integration/code-highlighting.spec.js b/cypress/integration/code-highlighting.spec.js
index 77470f65..9fdb2b56 100644
--- a/cypress/integration/code-highlighting.spec.js
+++ b/cypress/integration/code-highlighting.spec.js
@@ -1,92 +1,92 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {random} from '../utils';
 
 const $ = Cypress.$;
 
 let origin;
 const lineStart = 32;
 const lineEnd = 42;
 
 let url;
 
 describe('Code highlighting tests', function() {
   before(function() {
     origin = this.origin[0];
     url = `${this.Urls.browse_origin_content()}?origin_url=${origin.url}&path=${origin.content[0].path}`;
   });
 
   it('should highlight source code and add line numbers', function() {
     cy.visit(url);
     cy.get('.hljs-ln-numbers').then(lnNumbers => {
       cy.get('.hljs-ln-code')
         .should('have.length', lnNumbers.length);
     });
   });
 
   it('should emphasize source code lines based on url fragment', function() {
     cy.visit(`${url}/#L${lineStart}-L${lineEnd}`);
     cy.get('.hljs-ln-line').then(lines => {
-      for (let line of lines) {
+      for (const line of lines) {
         const lineElt = $(line);
         const lineNumber = parseInt(lineElt.data('line-number'));
         if (lineNumber >= lineStart && lineNumber <= lineEnd) {
           assert.notEqual(lineElt.css('background-color'), 'rgba(0, 0, 0, 0)');
         } else {
           assert.equal(lineElt.css('background-color'), 'rgba(0, 0, 0, 0)');
         }
       }
     });
   });
 
   it('should emphasize a line by clicking on its number', function() {
     cy.visit(url);
     cy.get('.hljs-ln-numbers').then(lnNumbers => {
       const lnNumber = lnNumbers[random(0, lnNumbers.length)];
       const lnNumberElt = $(lnNumber);
       assert.equal(lnNumberElt.css('background-color'), 'rgba(0, 0, 0, 0)');
       const line = parseInt(lnNumberElt.data('line-number'));
       cy.get(`.hljs-ln-numbers[data-line-number="${line}"]`)
         .click()
         .then(() => {
           assert.notEqual(lnNumberElt.css('background-color'), 'rgba(0, 0, 0, 0)');
         });
     });
   });
 
   it('should emphasize a range of lines by clicking on two line numbers and holding shift', function() {
     cy.visit(url);
 
     cy.get(`.hljs-ln-numbers[data-line-number="${lineStart}"]`)
       .click()
       .get(`.hljs-ln-numbers[data-line-number="${lineEnd}"]`)
       .click({shiftKey: true})
       .get('.hljs-ln-line')
       .then(lines => {
-        for (let line of lines) {
+        for (const line of lines) {
           const lineElt = $(line);
           const lineNumber = parseInt(lineElt.data('line-number'));
           if (lineNumber >= lineStart && lineNumber <= lineEnd) {
             assert.notEqual(lineElt.css('background-color'), 'rgba(0, 0, 0, 0)');
           } else {
             assert.equal(lineElt.css('background-color'), 'rgba(0, 0, 0, 0)');
           }
         }
       });
   });
 
   it('should remove emphasized lines when clicking anywhere in code', function() {
     cy.visit(`${url}/#L${lineStart}-L${lineEnd}`);
 
     cy.get(`.hljs-ln-code[data-line-number="1"]`)
       .click()
       .get('.hljs-ln-line')
       .should('have.css', 'background-color', 'rgba(0, 0, 0, 0)');
   });
 
 });
diff --git a/cypress/integration/content-rendering.spec.js b/cypress/integration/content-rendering.spec.js
index fa532f02..6a693959 100644
--- a/cypress/integration/content-rendering.spec.js
+++ b/cypress/integration/content-rendering.spec.js
@@ -1,99 +1,99 @@
 /**
  * Copyright (C) 2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import {checkLanguageHighlighting, describeSlowTests} from '../utils';
 
 describeSlowTests('Code highlighting tests', function() {
 
   const extensions = require('../fixtures/source-file-extensions.json');
 
   extensions.forEach(ext => {
     it(`should highlight source files with extension ${ext}`, function() {
       cy.request(this.Urls.tests_content_code_extension(ext)).then(response => {
         const data = response.body;
         cy.visit(`${this.Urls.browse_content(data.sha1)}?path=file.${ext}`);
         checkLanguageHighlighting(data.language);
       });
     });
   });
 
   const filenames = require('../fixtures/source-file-names.json');
 
   filenames.forEach(filename => {
     it(`should highlight source files with filenames ${filename}`, function() {
       cy.request(this.Urls.tests_content_code_filename(filename)).then(response => {
         const data = response.body;
         cy.visit(`${this.Urls.browse_content(data.sha1)}?path=${filename}`);
         checkLanguageHighlighting(data.language);
       });
     });
   });
 
 });
 
 describe('Image rendering tests', function() {
   const imgExtensions = ['gif', 'jpeg', 'png', 'webp'];
 
   imgExtensions.forEach(ext => {
     it(`should render image with extension ${ext}`, function() {
       cy.request(this.Urls.tests_content_other_extension(ext)).then(response => {
         const data = response.body;
         cy.visit(`${this.Urls.browse_content(data.sha1)}?path=file.${ext}`);
         cy.get('.swh-content img')
           .should('be.visible');
       });
     });
   });
 
 });
 
 describe('PDF rendering test', function() {
 
   function sum(previousValue, currentValue) {
     return previousValue + currentValue;
   }
 
   it(`should render a PDF file`, function() {
     cy.request(this.Urls.tests_content_other_extension('pdf')).then(response => {
       const data = response.body;
       cy.visit(`${this.Urls.browse_content(data.sha1)}?path=file.pdf`);
       cy.get('.swh-content canvas')
         .wait(2000)
         .then(canvas => {
-          let width = canvas[0].width;
-          let height = canvas[0].height;
-          let context = canvas[0].getContext('2d');
-          let imgData = context.getImageData(0, 0, width, height);
+          const width = canvas[0].width;
+          const height = canvas[0].height;
+          const context = canvas[0].getContext('2d');
+          const imgData = context.getImageData(0, 0, width, height);
           assert.notEqual(imgData.data.reduce(sum), 0);
         });
     });
   });
 
 });
 
 describe('Jupyter notebook rendering test', function() {
 
   it(`should render a notebook file to HTML`, function() {
     cy.request(this.Urls.tests_content_other_extension('ipynb')).then(response => {
       const data = response.body;
       cy.visit(`${this.Urls.browse_content(data.sha1)}?path=file.ipynb`);
       cy.get('.nb-notebook')
         .should('be.visible')
         .and('not.be.empty');
       cy.get('.nb-cell.nb-markdown-cell')
         .should('be.visible')
         .and('not.be.empty');
       cy.get('.nb-cell.nb-code-cell')
         .should('be.visible')
         .and('not.be.empty');
       cy.get('.MathJax')
         .should('be.visible')
         .and('not.be.empty');
     });
   });
 
 });
diff --git a/cypress/integration/deposit-admin.spec.js b/cypress/integration/deposit-admin.spec.js
index b0e78f11..cba933bc 100644
--- a/cypress/integration/deposit-admin.spec.js
+++ b/cypress/integration/deposit-admin.spec.js
@@ -1,156 +1,156 @@
 /**
  * Copyright (C) 2020-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 // data to use as request query response
 let responseDeposits;
 let expectedOrigins;
 
 describe('Test admin deposit page', function() {
   beforeEach(() => {
     responseDeposits = [
       {
         'id': 614,
         'external_id': 'ch-de-1',
         'reception_date': '2020-05-18T13:48:27Z',
         'status': 'done',
         'status_detail': null,
         'swhid': 'swh:1:dir:ef04a768',
         'swhid_context': 'swh:1:dir:ef04a768;origin=https://w.s.o/c-d-1;visit=swh:1:snp:b234be1e;anchor=swh:1:rev:d24a75c9;path=/'
       },
       {
         'id': 613,
         'external_id': 'ch-de-2',
         'reception_date': '2020-05-18T11:20:16Z',
         'status': 'done',
         'status_detail': null,
         'swhid': 'swh:1:dir:181417fb',
         'swhid_context': 'swh:1:dir:181417fb;origin=https://w.s.o/c-d-2;visit=swh:1:snp:8c32a2ef;anchor=swh:1:rev:3d1eba04;path=/'
       },
       {
         'id': 612,
         'external_id': 'ch-de-3',
         'reception_date': '2020-05-18T11:20:16Z',
         'status': 'rejected',
         'status_detail': 'incomplete deposit!',
         'swhid': null,
         'swhid_context': null
       }
     ];
     // those are computed from the
     expectedOrigins = {
       614: 'https://w.s.o/c-d-1',
       613: 'https://w.s.o/c-d-2',
       612: ''
     };
 
   });
 
   it('Should display properly entries', function() {
     cy.adminLogin();
     cy.visit(this.Urls.admin_deposit());
 
-    let testDeposits = responseDeposits;
+    const testDeposits = responseDeposits;
 
     cy.intercept(`${this.Urls.admin_deposit_list()}**`, {
       body: {
         'draw': 10,
         'recordsTotal': testDeposits.length,
         'recordsFiltered': testDeposits.length,
         'data': testDeposits
       }
     }).as('listDeposits');
 
     cy.location('pathname')
       .should('be.equal', this.Urls.admin_deposit());
     cy.url().should('include', '/admin/deposit');
 
     cy.get('#swh-admin-deposit-list')
       .should('exist');
 
     cy.wait('@listDeposits').then((xhr) => {
       cy.log('response:', xhr.response);
       cy.log(xhr.response.body);
-      let deposits = xhr.response.body.data;
+      const deposits = xhr.response.body.data;
       cy.log('Deposits: ', deposits);
       expect(deposits.length).to.equal(testDeposits.length);
 
       cy.get('#swh-admin-deposit-list').find('tbody > tr').as('rows');
 
       // only 2 entries
       cy.get('@rows').each((row, idx, collection) => {
-        let deposit = deposits[idx];
-        let responseDeposit = testDeposits[idx];
+        const deposit = deposits[idx];
+        const responseDeposit = testDeposits[idx];
         assert.isNotNull(deposit);
         assert.isNotNull(responseDeposit);
         expect(deposit.id).to.be.equal(responseDeposit['id']);
         expect(deposit.external_id).to.be.equal(responseDeposit['external_id']);
         expect(deposit.status).to.be.equal(responseDeposit['status']);
         expect(deposit.status_detail).to.be.equal(responseDeposit['status_detail']);
         expect(deposit.swhid).to.be.equal(responseDeposit['swhid']);
         expect(deposit.swhid_context).to.be.equal(responseDeposit['swhid_context']);
 
-        let expectedOrigin = expectedOrigins[deposit.id];
+        const expectedOrigin = expectedOrigins[deposit.id];
         // ensure it's in the dom
         cy.contains(deposit.id).should('be.visible');
         if (deposit.status !== 'rejected') {
           expect(row).to.not.contain(deposit.external_id);
           cy.contains(expectedOrigin).should('be.visible');
         }
 
         cy.contains(deposit.status).should('be.visible');
         // those are hidden by default, so now visible
         if (deposit.status_detail !== null) {
           cy.contains(deposit.status_detail).should('not.exist');
         }
 
         // those are hidden by default
         if (deposit.swhid !== null) {
           cy.contains(deposit.swhid).should('not.exist');
           cy.contains(deposit.swhid_context).should('not.exist');
         }
       });
 
       // toggling all links and ensure, the previous checks are inverted
       cy.get('a.toggle-col').click({'multiple': true}).then(() => {
         cy.get('#swh-admin-deposit-list').find('tbody > tr').as('rows');
 
         cy.get('@rows').each((row, idx, collection) => {
-          let deposit = deposits[idx];
-          let expectedOrigin = expectedOrigins[deposit.id];
+          const deposit = deposits[idx];
+          const expectedOrigin = expectedOrigins[deposit.id];
 
           // ensure it's in the dom
           cy.contains(deposit.id).should('not.exist');
           if (deposit.status !== 'rejected') {
             expect(row).to.not.contain(deposit.external_id);
             expect(row).to.contain(expectedOrigin);
           }
 
           expect(row).to.not.contain(deposit.status);
           // those are hidden by default, so now visible
           if (deposit.status_detail !== null) {
             cy.contains(deposit.status_detail).should('be.visible');
           }
 
           // those are hidden by default, so now they should be visible
           if (deposit.swhid !== null) {
             cy.contains(deposit.swhid).should('be.visible');
             cy.contains(deposit.swhid_context).should('be.visible');
             // check SWHID link text formatting
             cy.contains(deposit.swhid_context).then(elt => {
               expect(elt[0].innerHTML).to.equal(deposit.swhid_context.replace(/;/g, ';<br>'));
             });
           }
         });
       });
 
       cy.get('#swh-admin-deposit-list-error')
         .should('not.contain',
                 'An error occurred while retrieving the list of deposits');
     });
 
   });
 });
diff --git a/cypress/integration/directory.spec.js b/cypress/integration/directory.spec.js
index c5be3bc2..4f1ba217 100644
--- a/cypress/integration/directory.spec.js
+++ b/cypress/integration/directory.spec.js
@@ -1,83 +1,83 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 const $ = Cypress.$;
 
 let origin;
 
 let url;
-let dirs = [];
-let files = [];
+const dirs = [];
+const files = [];
 
 describe('Directory Tests', function() {
   before(function() {
     origin = this.origin[0];
 
     url = `${this.Urls.browse_origin_directory()}?origin_url=${origin.url}`;
 
-    for (let entry of origin.dirContent) {
+    for (const entry of origin.dirContent) {
       if (entry.type === 'file') {
         files.push(entry);
       } else {
         dirs.push(entry);
       }
     }
   });
 
   beforeEach(function() {
     cy.visit(url);
   });
 
   it('should display all files and directories', function() {
     cy.get('.swh-directory')
       .should('have.length', dirs.length)
       .and('be.visible');
     cy.get('.swh-content')
       .should('have.length', files.length)
       .and('be.visible');
   });
 
   it('should display sizes for files', function() {
     cy.get('.swh-content')
       .parent('tr')
       .then((rows) => {
-        for (let row of rows) {
-          let text = $(row).children('td').eq(2).text();
+        for (const row of rows) {
+          const text = $(row).children('td').eq(2).text();
           expect(text.trim()).to.not.be.empty;
         }
       });
   });
 
   it('should display readme when it is present', function() {
     cy.get('#readme-panel > .card-body')
       .should('be.visible')
       .and('have.class', 'swh-showdown')
       .and('not.be.empty')
       .and('not.contain', 'Readme bytes are not available');
   });
 
   it('should open subdirectory when clicked', function() {
     cy.get('.swh-directory')
       .first()
       .children('a')
       .click();
 
     cy.url()
       .should('include', `${url}&path=${dirs[0]['name']}`);
 
     cy.get('.swh-directory-table')
       .should('be.visible');
   });
 
   it('should have metadata available from javascript', function() {
     cy.window().then(win => {
       const metadata = win.swh.webapp.getBrowsedSwhObjectMetadata();
       expect(metadata).to.not.be.empty;
       expect(metadata).to.have.any.keys('directory');
     });
   });
 });
diff --git a/cypress/integration/errors.spec.js b/cypress/integration/errors.spec.js
index ce16fa1b..4708eba7 100644
--- a/cypress/integration/errors.spec.js
+++ b/cypress/integration/errors.spec.js
@@ -1,147 +1,147 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 let origin;
 
 const invalidChecksum = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
 
 const invalidPageUrl = '/invalidPath';
 
 function urlShouldShowError(url, error) {
   cy.visit(url, {
     failOnStatusCode: false
   });
   cy.get('.swh-http-error')
     .should('be.visible');
   cy.get('.swh-http-error-code')
     .should('contain', error.code);
   cy.get('.swh-http-error-desc')
     .should('contain', error.msg);
 }
 
 describe('Test Errors', function() {
   before(function() {
     origin = this.origin[0];
   });
 
   it('should show navigation buttons on error page', function() {
     cy.visit(invalidPageUrl, {
       failOnStatusCode: false
     });
     cy.get('a[onclick="window.history.back();"]')
       .should('be.visible');
     cy.get('a[href="/"')
       .should('be.visible');
   });
 
   context('For unarchived repositories', function() {
     it('should display NotFoundExc for unarchived repo', function() {
       const url = `${this.Urls.browse_origin_directory()}?origin_url=${this.unarchivedRepo.url}`;
 
       urlShouldShowError(url, {
         code: '404',
         msg: 'NotFoundExc: Origin with url ' + this.unarchivedRepo.url + ' not found!'
       });
     });
 
     it('should display NotFoundExc for unarchived content', function() {
       const url = this.Urls.browse_content(`sha1_git:${this.unarchivedRepo.content[0].sha1git}`);
 
       urlShouldShowError(url, {
         code: '404',
         msg: 'NotFoundExc: Content with sha1_git checksum equals to ' + this.unarchivedRepo.content[0].sha1git + ' not found!'
       });
     });
 
     it('should display NotFoundExc for unarchived directory sha1git', function() {
       const url = this.Urls.browse_directory(this.unarchivedRepo.rootDirectory);
 
       urlShouldShowError(url, {
         code: '404',
         msg: 'NotFoundExc: Directory with sha1_git ' + this.unarchivedRepo.rootDirectory + ' not found'
       });
     });
 
     it('should display NotFoundExc for unarchived revision sha1git', function() {
       const url = this.Urls.browse_revision(this.unarchivedRepo.revision);
 
       urlShouldShowError(url, {
         code: '404',
         msg: 'NotFoundExc: Revision with sha1_git ' + this.unarchivedRepo.revision + ' not found.'
       });
     });
 
     it('should display NotFoundExc for unarchived snapshot sha1git', function() {
       const url = this.Urls.browse_snapshot(this.unarchivedRepo.snapshot);
 
       urlShouldShowError(url, {
         code: '404',
         msg: 'Snapshot with id ' + this.unarchivedRepo.snapshot + ' not found!'
       });
     });
 
   });
 
   context('For archived repositories', function() {
     before(function() {
       const url = `${this.Urls.browse_origin_directory()}?origin_url=${origin.url}`;
       cy.visit(url);
     });
 
     it('should display NotFoundExc for invalid directory from archived repo', function() {
       const subDir = `${this.Urls.browse_origin_directory()}?origin_url=${origin.url}&path=${origin.invalidSubDir}`;
 
       urlShouldShowError(subDir, {
         code: '404',
         msg: 'NotFoundExc: Directory entry with path ' +
               origin.invalidSubDir + ' from root directory ' +
               origin.rootDirectory + ' not found'
       });
     });
 
     it(`should display NotFoundExc for incorrect origin_url
         with correct content hash`, function() {
       const url = this.Urls.browse_content(`sha1_git:${origin.content[0].sha1git}`) +
                   `?origin_url=${this.unarchivedRepo.url}`;
       urlShouldShowError(url, {
         code: '404',
         msg: 'The Software Heritage archive has a content ' +
             'with the hash you provided but the origin ' +
             'mentioned in your request appears broken: ' +
             this.unarchivedRepo.url + '. ' +
             'Please check the URL and try again.\n\n' +
             'Nevertheless, you can still browse the content ' +
             'without origin information: ' +
             '/browse/content/sha1_git:' +
             origin.content[0].sha1git + '/'
       });
     });
   });
 
   context('For invalid data', function() {
     it(`should display 400 for invalid checksum for
         directory, snapshot, revision, content`, function() {
       const types = ['directory', 'snapshot', 'revision', 'content'];
-      for (let type of types) {
+      for (const type of types) {
         const url = this.Urls[`browse_${type}`](invalidChecksum);
         urlShouldShowError(url, {
           code: '400',
           msg: 'BadInputExc: Invalid checksum query string ' +
                 invalidChecksum
         });
       }
     });
 
     it('should show 404 error for invalid path', function() {
       urlShouldShowError(invalidPageUrl, {
         code: '404',
         msg: 'The resource ' + invalidPageUrl +
             ' could not be found on the server.'
       });
     });
   });
 });
diff --git a/cypress/integration/home.spec.js b/cypress/integration/home.spec.js
index 0491e710..1f8c6efd 100644
--- a/cypress/integration/home.spec.js
+++ b/cypress/integration/home.spec.js
@@ -1,108 +1,108 @@
 /**
  * Copyright (C) 2019-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 const $ = Cypress.$;
 
 const url = '/';
 
 describe('Home Page Tests', function() {
 
   it('should have focus on search form after page load', function() {
     cy.visit(url);
 
     cy.get('#swh-origins-url-patterns')
       .should('have.attr', 'autofocus');
     // for some reason, autofocus is not honored when running cypress tests
     // while it is in non controlled browsers
     // .should('have.focus');
   });
 
   it('should display positive stats for each category', function() {
 
     cy.intercept(this.Urls.stat_counters())
       .as('getStatCounters');
 
     cy.visit(url)
       .wait('@getStatCounters')
       .wait(500)
       .get('.swh-counter:visible')
       .then((counters) => {
-        for (let counter of counters) {
-          let innerText = $(counter).text();
+        for (const counter of counters) {
+          const innerText = $(counter).text();
           const value = parseInt(innerText.replace(/,/g, ''));
           assert.isAbove(value, 0);
         }
       });
   });
 
   it('should display null counters and hide history graphs when storage is empty', function() {
 
     cy.intercept(this.Urls.stat_counters(), {
       body: {
         'stat_counters': {},
         'stat_counters_history': {}
       }
     }).as('getStatCounters');
 
     cy.visit(url)
       .wait('@getStatCounters')
       .wait(500)
       .get('.swh-counter:visible')
       .then((counters) => {
-        for (let counter of counters) {
+        for (const counter of counters) {
           const value = parseInt($(counter).text());
           assert.equal(value, 0);
         }
       });
 
     cy.get('.swh-counter-history')
       .should('not.be.visible');
   });
 
   it('should hide counters when data is missing', function() {
 
     cy.intercept(this.Urls.stat_counters(), {
       body: {
         'stat_counters': {
           'content': 150,
           'directory': 45,
           'revision': 78
         },
         'stat_counters_history': {}
       }
     }).as('getStatCounters');
 
     cy.visit(url)
       .wait('@getStatCounters')
       .wait(500);
 
     cy.get('#swh-content-count, #swh-directory-count, #swh-revision-count')
       .should('be.visible');
 
     cy.get('#swh-release-count, #swh-person-count, #swh-origin-count')
       .should('not.be.visible');
 
     cy.get('.swh-counter-history')
       .should('not.be.visible');
   });
 
   it('should redirect to search page when submitting search form', function() {
     const searchText = 'git';
     cy.get('#swh-origins-url-patterns')
       .type(searchText)
       .get('.swh-search-icon')
       .click();
 
     cy.location('pathname')
       .should('equal', this.Urls.browse_search());
 
     cy.location('search')
       .should('equal', `?q=${searchText}&with_visit=true&with_content=true`);
 
   });
 
 });
diff --git a/cypress/integration/layout.spec.js b/cypress/integration/layout.spec.js
index dd17d854..5944b1d5 100644
--- a/cypress/integration/layout.spec.js
+++ b/cypress/integration/layout.spec.js
@@ -1,231 +1,231 @@
 /**
  * Copyright (C) 2019-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 const url = '/browse/help/';
 const statusUrl = 'https://status.softwareheritage.org';
 
 describe('Test top-bar', function() {
   beforeEach(function() {
     cy.clearLocalStorage();
     cy.visit(url);
   });
   it('should should contain all navigation links', function() {
     cy.get('.swh-top-bar a')
       .should('have.length.of.at.least', 4)
       .and('be.visible')
       .and('have.attr', 'href');
   });
 
   it('should show donate button on lg screen', function() {
     cy.get('.swh-donate-link')
       .should('be.visible');
   });
   it('should hide donate button on sm screen', function() {
     cy.viewport(600, 800);
     cy.get('.swh-donate-link')
       .should('not.be.visible');
   });
   it('should hide full width switch on small screens', function() {
     cy.viewport(360, 740);
     cy.get('#swh-full-width-switch-container')
       .should('not.be.visible');
 
     cy.viewport(600, 800);
     cy.get('#swh-full-width-switch-container')
       .should('not.be.visible');
 
     cy.viewport(800, 600);
     cy.get('#swh-full-width-switch-container')
       .should('not.be.visible');
   });
   it('should show full width switch on large screens', function() {
     cy.viewport(1024, 768);
     cy.get('#swh-full-width-switch-container')
       .should('be.visible');
 
     cy.viewport(1920, 1080);
     cy.get('#swh-full-width-switch-container')
       .should('be.visible');
   });
   it('should change container width when toggling Full width switch', function() {
     cy.get('#swh-web-content')
       .should('have.class', 'container')
       .should('not.have.class', 'container-fluid');
 
     cy.should(() => {
       expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.null;
     });
 
     cy.get('#swh-full-width-switch')
       .click({force: true});
 
     cy.get('#swh-web-content')
       .should('not.have.class', 'container')
       .should('have.class', 'container-fluid');
 
     cy.should(() => {
       expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.true;
     });
 
     cy.get('#swh-full-width-switch')
       .click({force: true});
 
     cy.get('#swh-web-content')
       .should('have.class', 'container')
       .should('not.have.class', 'container-fluid');
 
     cy.should(() => {
       expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.false;
     });
   });
   it('should restore container width when loading page again', function() {
     cy.visit(url)
       .get('#swh-web-content')
       .should('have.class', 'container')
       .should('not.have.class', 'container-fluid');
 
     cy.get('#swh-full-width-switch')
       .click({force: true});
 
     cy.visit(url)
       .get('#swh-web-content')
       .should('not.have.class', 'container')
       .should('have.class', 'container-fluid');
 
     cy.get('#swh-full-width-switch')
       .click({force: true});
 
     cy.visit(url)
       .get('#swh-web-content')
       .should('have.class', 'container')
       .should('not.have.class', 'container-fluid');
   });
 
   function genStatusResponse(status, statusCode) {
     return {
       'result': {
         'status': [
           {
             'id': '5f7c4c567f50b304c1e7bd5f',
             'name': 'Save Code Now',
             'updated': '2020-11-30T13:51:21.151Z',
             'status': 'Operational',
             'status_code': 100
           },
           {
             'id': '5f7c4c6f8338bc04b7f476fe',
             'name': 'Source Code Crawlers',
             'updated': '2020-11-30T13:51:21.151Z',
             'status': status,
             'status_code': statusCode
           }
         ]
       }
     };
   }
 
   it('should display swh status widget when data are available', function() {
     const statusTestData = [
       {
         status: 'Operational',
         statusCode: 100,
         color: 'green'
       },
       {
         status: 'Scheduled Maintenance',
         statusCode: 200,
         color: 'blue'
       },
       {
         status: 'Degraded Performance',
         statusCode: 300,
         color: 'yellow'
       },
       {
         status: 'Partial Service Disruption',
         statusCode: 400,
         color: 'yellow'
       },
       {
         status: 'Service Disruption',
         statusCode: 500,
         color: 'red'
       },
       {
         status: 'Security Event',
         statusCode: 600,
         color: 'red'
       }
     ];
 
     const responses = [];
-    for (let std of statusTestData) {
+    for (const std of statusTestData) {
       responses.push(genStatusResponse(std.status, std.statusCode));
     }
 
     let i = 0;
-    for (let std of statusTestData) {
+    for (const std of statusTestData) {
       cy.visit(url);
       // trick to override the response of an intercepted request
       // https://github.com/cypress-io/cypress/issues/9302
       cy.intercept(`${statusUrl}/**`, req => req.reply(responses.shift()))
         .as(`getSwhStatusData${i}`);
       cy.wait(`@getSwhStatusData${i}`);
       cy.get('.swh-current-status-indicator').should('have.class', std.color);
       cy.get('#swh-current-status-description').should('have.text', std.status);
       ++i;
     }
   });
 
   it('should not display swh status widget when data are not available', function() {
     cy.intercept(`${statusUrl}/**`, {
       body: {}
     }).as('getSwhStatusData');
     cy.visit(url);
     cy.wait('@getSwhStatusData');
     cy.get('.swh-current-status').should('not.exist');
   });
 
 });
 
 describe('Test navbar', function() {
   it('should redirect to search page when submitting search form in navbar', function() {
     const keyword = 'python';
     cy.get('#swh-origins-search-top-input')
       .type(keyword);
 
     cy.get('.swh-search-navbar')
       .submit();
 
     cy.url()
       .should('include', `${this.Urls.browse_search()}?q=${keyword}`);
   });
 });
 
 describe('Test footer', function() {
   beforeEach(function() {
     cy.visit(url);
   });
 
   it('should be visible', function() {
     cy.get('footer')
       .should('be.visible');
   });
 
   it('should have correct copyright years', function() {
     const currentYear = new Date().getFullYear();
     const copyrightText = '(C) 2015–' + currentYear.toString();
     cy.get('footer')
       .should('contain', copyrightText);
   });
 
   it('should contain link to Web API', function() {
     cy.get('footer')
       .get(`a[href="${this.Urls.api_1_homepage()}"]`)
       .should('contain', 'Web API');
   });
 });
diff --git a/cypress/integration/origin-save.spec.js b/cypress/integration/origin-save.spec.js
index becb6ed8..b2300530 100644
--- a/cypress/integration/origin-save.spec.js
+++ b/cypress/integration/origin-save.spec.js
@@ -1,711 +1,711 @@
 /**
  * Copyright (C) 2019-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 let url;
 let origin;
 const $ = Cypress.$;
 
 const saveCodeMsg = {
   'success': 'The "save code now" request has been accepted and will be processed as soon as possible.',
   'warning': 'The "save code now" request has been put in pending state and may be accepted for processing after manual review.',
   'rejected': 'The "save code now" request has been rejected because the provided origin url is blacklisted.',
   'rateLimit': 'The rate limit for "save code now" requests has been reached. Please try again later.',
   'not-found': 'The provided url does not exist',
   'unknownError': 'An unexpected error happened when submitting the "save code now request',
   'csrfError': 'CSRF Failed: Referrer checking failed - no Referrer.'
 };
 
 const anonymousVisitTypes = ['git', 'hg', 'svn'];
 const allVisitTypes = ['archives', 'git', 'hg', 'svn'];
 
 function makeOriginSaveRequest(originType, originUrl) {
   cy.get('#swh-input-origin-url')
     .type(originUrl)
     .get('#swh-input-visit-type')
     .select(originType)
     .get('#swh-save-origin-form')
     .submit();
 }
 
 function checkAlertVisible(alertType, msg) {
   cy.get('#swh-origin-save-request-status')
     .should('be.visible')
     .find(`.alert-${alertType}`)
     .should('be.visible')
     .and('contain', msg);
 }
 
 // Stub requests to save an origin
 function stubSaveRequest({
   requestUrl,
   visitType = 'git',
   saveRequestStatus,
   originUrl,
   saveTaskStatus,
   responseStatus = 200,
   // For error code with the error message in the 'reason' key response
   errorMessage = '',
   saveRequestDate = new Date(),
   visitDate = new Date(),
   visitStatus = null
 } = {}) {
   let response;
   if (responseStatus !== 200 && errorMessage) {
     response = {
       'reason': errorMessage
     };
   } else {
     response = genOriginSaveResponse({visitType: visitType,
                                       saveRequestStatus: saveRequestStatus,
                                       originUrl: originUrl,
                                       saveRequestDate: saveRequestDate,
                                       saveTaskStatus: saveTaskStatus,
                                       visitDate: visitDate,
                                       visitStatus: visitStatus
     });
   }
   cy.intercept('POST', requestUrl, {body: response, statusCode: responseStatus})
     .as('saveRequest');
 }
 
 // Mocks API response : /save/(:visit_type)/(:origin_url)
 // visit_type : {'git', 'hg', 'svn', ...}
 function genOriginSaveResponse({
   visitType = 'git',
   saveRequestStatus,
   originUrl,
   saveRequestDate = new Date(),
   saveTaskStatus,
   visitDate = new Date(),
   visitStatus
 } = {}) {
   return {
     'visit_type': visitType,
     'save_request_status': saveRequestStatus,
     'origin_url': originUrl,
     'id': 1,
     'save_request_date': saveRequestDate ? saveRequestDate.toISOString() : null,
     'save_task_status': saveTaskStatus,
     'visit_date': visitDate ? visitDate.toISOString() : null,
     'visit_status': visitStatus
   };
 };
 
 describe('Origin Save Tests', function() {
   before(function() {
     url = this.Urls.origin_save();
     origin = this.origin[0];
     this.originSaveUrl = this.Urls.api_1_save_origin(origin.type, origin.url);
   });
 
   beforeEach(function() {
     cy.fixture('origin-save').as('originSaveJSON');
     cy.fixture('save-task-info').as('saveTaskInfoJSON');
     cy.visit(url);
   });
 
   it('should format appropriately values depending on their type', function() {
-    let inputValues = [ // null values stay null
+    const inputValues = [ // null values stay null
       {type: 'json', value: null, expectedValue: null},
       {type: 'date', value: null, expectedValue: null},
       {type: 'raw', value: null, expectedValue: null},
       {type: 'duration', value: null, expectedValue: null},
       // non null values formatted depending on their type
       {type: 'json', value: '{}', expectedValue: '"{}"'},
       {type: 'date', value: '04/04/2021 01:00:00', expectedValue: '4/4/2021, 1:00:00 AM'},
       {type: 'raw', value: 'value-for-identity', expectedValue: 'value-for-identity'},
       {type: 'duration', value: '10', expectedValue: '10 seconds'},
       {type: 'duration', value: 100, expectedValue: '100 seconds'}
     ];
     cy.window().then(win => {
       inputValues.forEach(function(input, index, array) {
-        let actualValue = win.swh.save.formatValuePerType(input.type, input.value);
+        const actualValue = win.swh.save.formatValuePerType(input.type, input.value);
         assert.equal(actualValue, input.expectedValue);
       });
     });
   });
 
   it('should display accepted message when accepted', function() {
     stubSaveRequest({requestUrl: this.originSaveUrl,
                      saveRequestStatus: 'accepted',
                      originUrl: origin.url,
                      saveTaskStatus: 'not yet scheduled'});
 
     makeOriginSaveRequest(origin.type, origin.url);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('success', saveCodeMsg['success']);
     });
   });
 
   it('should validate gitlab subproject url', function() {
     const gitlabSubProjectUrl = 'https://gitlab.com/user/project/sub/';
     const originSaveUrl = this.Urls.api_1_save_origin('git', gitlabSubProjectUrl);
 
     stubSaveRequest({requestUrl: originSaveUrl,
                      saveRequestStatus: 'accepted',
                      originurl: gitlabSubProjectUrl,
                      saveTaskStatus: 'not yet scheduled'});
 
     makeOriginSaveRequest('git', gitlabSubProjectUrl);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('success', saveCodeMsg['success']);
     });
   });
 
   it('should validate project url with _ in username', function() {
     const gitlabSubProjectUrl = 'https://gitlab.com/user_name/project.git';
     const originSaveUrl = this.Urls.api_1_save_origin('git', gitlabSubProjectUrl);
 
     stubSaveRequest({requestUrl: originSaveUrl,
                      saveRequestStatus: 'accepted',
                      originurl: gitlabSubProjectUrl,
                      saveTaskStatus: 'not yet scheduled'});
 
     makeOriginSaveRequest('git', gitlabSubProjectUrl);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('success', saveCodeMsg['success']);
     });
   });
 
   it('should display warning message when pending', function() {
     stubSaveRequest({requestUrl: this.originSaveUrl,
                      saveRequestStatus: 'pending',
                      originUrl: origin.url,
                      saveTaskStatus: 'not created'});
 
     makeOriginSaveRequest(origin.type, origin.url);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('warning', saveCodeMsg['warning']);
     });
   });
 
   it('should show error when the origin does not exist (status: 400)', function() {
     stubSaveRequest({requestUrl: this.originSaveUrl,
                      originUrl: origin.url,
                      responseStatus: 400,
                      errorMessage: saveCodeMsg['not-found']});
 
     makeOriginSaveRequest(origin.type, origin.url);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('danger', saveCodeMsg['not-found']);
     });
   });
 
   it('should show error when csrf validation failed (status: 403)', function() {
     stubSaveRequest({requestUrl: this.originSaveUrl,
                      saveRequestStatus: 'rejected',
                      originUrl: origin.url,
                      saveTaskStatus: 'not created',
                      responseStatus: 403,
                      errorMessage: saveCodeMsg['csrfError']});
 
     makeOriginSaveRequest(origin.type, origin.url);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('danger', saveCodeMsg['csrfError']);
     });
   });
 
   it('should show error when origin is rejected (status: 403)', function() {
     stubSaveRequest({requestUrl: this.originSaveUrl,
                      saveRequestStatus: 'rejected',
                      originUrl: origin.url,
                      saveTaskStatus: 'not created',
                      responseStatus: 403,
                      errorMessage: saveCodeMsg['rejected']});
 
     makeOriginSaveRequest(origin.type, origin.url);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('danger', saveCodeMsg['rejected']);
     });
   });
 
   it('should show error when rate limited (status: 429)', function() {
     stubSaveRequest({requestUrl: this.originSaveUrl,
                      saveRequestStatus: 'Request was throttled. Expected available in 60 seconds.',
                      originUrl: origin.url,
                      saveTaskStatus: 'not created',
                      responseStatus: 429});
 
     makeOriginSaveRequest(origin.type, origin.url);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('danger', saveCodeMsg['rateLimit']);
     });
   });
 
   it('should show error when unknown error occurs (status other than 200, 403, 429)', function() {
     stubSaveRequest({requestUrl: this.originSaveUrl,
                      saveRequestStatus: 'Error',
                      originUrl: origin.url,
                      saveTaskStatus: 'not created',
                      responseStatus: 406});
 
     makeOriginSaveRequest(origin.type, origin.url);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('danger', saveCodeMsg['unknownError']);
     });
   });
 
   it('should display origin save info in the requests table', function() {
     cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.get('tbody tr').then(rows => {
       let i = 0;
-      for (let row of rows) {
+      for (const row of rows) {
         const cells = row.cells;
         const requestDateStr = new Date(this.originSaveJSON.data[i].save_request_date).toLocaleString();
         const saveStatus = this.originSaveJSON.data[i].save_task_status;
         assert.equal($(cells[0]).text(), requestDateStr);
         assert.equal($(cells[1]).text(), this.originSaveJSON.data[i].visit_type);
         let html = '';
         if (saveStatus === 'succeeded') {
           let browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(this.originSaveJSON.data[i].origin_url)}`;
           browseOriginUrl += `&amp;timestamp=${encodeURIComponent(this.originSaveJSON.data[i].visit_date)}`;
           html += `<a href="${browseOriginUrl}">${this.originSaveJSON.data[i].origin_url}</a>`;
         } else {
           html += this.originSaveJSON.data[i].origin_url;
         }
         html += `&nbsp;<a href="${this.originSaveJSON.data[i].origin_url}">`;
         html += '<i class="mdi mdi-open-in-new" aria-hidden="true"></i></a>';
         assert.equal($(cells[2]).html(), html);
         assert.equal($(cells[3]).text(), this.originSaveJSON.data[i].save_request_status);
         assert.equal($(cells[4]).text(), saveStatus);
         ++i;
       }
     });
   });
 
   it('should not add timestamp to the browse origin URL is no visit date has been found', function() {
     const originUrl = 'https://git.example.org/example.git';
     const saveRequestData = genOriginSaveResponse({
       saveRequestStatus: 'accepted',
       originUrl: originUrl,
       saveTaskStatus: 'succeeded',
       visitDate: null,
       visitStatus: 'full'
     });
     const saveRequestsListData = {
       'recordsTotal': 1,
       'draw': 2,
       'recordsFiltered': 1,
       'data': [saveRequestData]
     };
     cy.intercept('/save/requests/list/**', {body: saveRequestsListData})
       .as('saveRequestsList');
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.wait('@saveRequestsList');
     cy.get('tbody tr').then(rows => {
       const firstRowCells = rows[0].cells;
       const browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(originUrl)}`;
       const browseOriginLink = `<a href="${browseOriginUrl}">${originUrl}</a>`;
       expect($(firstRowCells[2]).html()).to.have.string(browseOriginLink);
     });
   });
 
   it('should display/close task info popover when clicking on the info button', function() {
     cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
     cy.intercept('/save/task/info/**', {fixture: 'save-task-info'});
 
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.get('.swh-save-request-info')
       .eq(0)
       .click();
 
     cy.get('.swh-save-request-info-popover')
       .should('be.visible');
 
     cy.get('.swh-save-request-info')
       .eq(0)
       .click();
 
     cy.get('.swh-save-request-info-popover')
       .should('not.exist');
   });
 
   it('should hide task info popover when clicking on the close button', function() {
     cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
     cy.intercept('/save/task/info/**', {fixture: 'save-task-info'});
 
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.get('.swh-save-request-info')
       .eq(0)
       .click();
 
     cy.get('.swh-save-request-info-popover')
       .should('be.visible');
 
     cy.get('.swh-save-request-info-close')
       .click();
 
     cy.get('.swh-save-request-info-popover')
       .should('not.exist');
   });
 
   it('should fill save request form when clicking on "Save again" button', function() {
     cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
 
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.get('.swh-save-origin-again')
       .eq(0)
       .click();
 
     cy.get('tbody tr').eq(0).then(row => {
       const cells = row[0].cells;
       cy.get('#swh-input-visit-type')
         .should('have.value', $(cells[1]).text());
       cy.get('#swh-input-origin-url')
         .should('have.value', $(cells[2]).text().slice(0, -1));
     });
   });
 
   it('should select correct visit type if possible when clicking on "Save again" button', function() {
     const originUrl = 'https://gitlab.inria.fr/solverstack/maphys/maphys/';
     const badVisitType = 'hg';
     const goodVisitType = 'git';
     cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
     stubSaveRequest({requestUrl: this.Urls.api_1_save_origin(badVisitType, originUrl),
                      visitType: badVisitType,
                      saveRequestStatus: 'accepted',
                      originUrl: originUrl,
                      saveTaskStatus: 'failed',
                      visitStatus: 'failed',
                      responseStatus: 200,
                      errorMessage: saveCodeMsg['accepted']});
 
     makeOriginSaveRequest(badVisitType, originUrl);
 
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.wait('@saveRequest').then(() => {
       cy.get('.swh-save-origin-again')
         .eq(0)
         .click();
 
       cy.get('tbody tr').eq(0).then(row => {
         const cells = row[0].cells;
         cy.get('#swh-input-visit-type')
           .should('have.value', goodVisitType);
         cy.get('#swh-input-origin-url')
           .should('have.value', $(cells[2]).text().slice(0, -1));
       });
     });
   });
 
   it('should create save request for authenticated user', function() {
     cy.userLogin();
     cy.visit(url);
     const originUrl = 'https://git.example.org/account/repo';
     stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl),
                      saveRequestStatus: 'accepted',
                      originUrl: origin.url,
                      saveTaskStatus: 'not yet scheduled'});
 
     makeOriginSaveRequest('git', originUrl);
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('success', saveCodeMsg['success']);
     });
   });
 
   it('should not show user requests filter checkbox for anonymous users', function() {
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.get('#swh-save-requests-user-filter').should('not.exist');
   });
 
   it('should show user requests filter checkbox for authenticated users', function() {
     cy.userLogin();
     cy.visit(url);
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.get('#swh-save-requests-user-filter').should('exist');
   });
 
   it('should show only user requests when filter is activated', function() {
     cy.intercept('POST', '/api/1/origin/save/**')
       .as('saveRequest');
 
     const originAnonymousUser = 'https://some.git.server/project/';
     const originAuthUser = 'https://other.git.server/project/';
 
     // anonymous user creates a save request
     makeOriginSaveRequest('git', originAnonymousUser);
     cy.wait('@saveRequest');
 
     // authenticated user creates another save request
     cy.userLogin();
     cy.visit(url);
     makeOriginSaveRequest('git', originAuthUser);
     cy.wait('@saveRequest');
 
     // user requests filter checkbox should be in the DOM
     cy.get('#swh-origin-save-requests-list-tab').click();
     cy.get('#swh-save-requests-user-filter').should('exist');
 
     // check unfiltered user requests
     cy.get('tbody tr').then(rows => {
       expect(rows.length).to.eq(2);
       expect($(rows[0].cells[2]).text()).to.contain(originAuthUser);
       expect($(rows[1].cells[2]).text()).to.contain(originAnonymousUser);
     });
 
     // activate filter and check filtered user requests
     cy.get('#swh-save-requests-user-filter')
       .click({force: true});
     cy.get('tbody tr').then(rows => {
       expect(rows.length).to.eq(1);
       expect($(rows[0].cells[2]).text()).to.contain(originAuthUser);
     });
 
     // deactivate filter and check unfiltered user requests
     cy.get('#swh-save-requests-user-filter')
       .click({force: true});
     cy.get('tbody tr').then(rows => {
       expect(rows.length).to.eq(2);
     });
 
   });
 
   it('should list unprivileged visit types when not connected', function() {
     cy.visit(url);
     cy.get('#swh-input-visit-type').children('option').then(options => {
       const actual = [...options].map(o => o.value);
       expect(actual).to.deep.eq(anonymousVisitTypes);
     });
   });
 
   it('should list unprivileged visit types when connected as unprivileged user', function() {
     cy.userLogin();
     cy.visit(url);
     cy.get('#swh-input-visit-type').children('option').then(options => {
       const actual = [...options].map(o => o.value);
       expect(actual).to.deep.eq(anonymousVisitTypes);
     });
   });
 
   it('should list privileged visit types when connected as ambassador', function() {
     cy.ambassadorLogin();
     cy.visit(url);
     cy.get('#swh-input-visit-type').children('option').then(options => {
       const actual = [...options].map(o => o.value);
       expect(actual).to.deep.eq(allVisitTypes);
     });
   });
 
   it('should display extra inputs when dealing with \'archives\' visit type', function() {
     cy.ambassadorLogin();
     cy.visit(url);
 
-    for (let visitType of anonymousVisitTypes) {
+    for (const visitType of anonymousVisitTypes) {
       cy.get('#swh-input-visit-type').select(visitType);
       cy.get('.swh-save-origin-archives-form').should('not.be.visible');
     }
 
     // this should display more inputs with the 'archives' type
     cy.get('#swh-input-visit-type').select('archives');
     cy.get('.swh-save-origin-archives-form').should('be.visible');
 
   });
 
   it('should be allowed to submit \'archives\' save request when connected as ambassador', function() {
-    let originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
-    let artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
-    let artifactVersion = '1.1.4';
+    const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
+    const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
+    const artifactVersion = '1.1.4';
     stubSaveRequest({
       requestUrl: this.Urls.api_1_save_origin('archives', originUrl),
       saveRequestStatus: 'accepted',
       originUrl: originUrl,
       saveTaskStatus: 'not yet scheduled'
     });
 
     cy.ambassadorLogin();
     cy.visit(url);
 
     // input new 'archives' information and submit
     cy.get('#swh-input-origin-url')
       .type(originUrl)
       .get('#swh-input-visit-type')
       .select('archives')
       .get('#swh-input-artifact-url-0')
       .type(artifactUrl)
       .get('#swh-input-artifact-version-0')
       .clear()
       .type(artifactVersion)
       .get('#swh-save-origin-form')
       .submit();
 
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('success', saveCodeMsg['success']);
     });
 
   });
 
   it('should submit multiple artifacts for the archives visit type', function() {
-    let originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
-    let artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
-    let artifactVersion = '1.1.4';
-    let artifact2Url = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.5.tar.gz';
-    let artifact2Version = '1.1.5';
+    const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
+    const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
+    const artifactVersion = '1.1.4';
+    const artifact2Url = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.5.tar.gz';
+    const artifact2Version = '1.1.5';
 
     cy.ambassadorLogin();
     cy.visit(url);
 
     cy.get('#swh-input-origin-url')
       .type(originUrl)
       .get('#swh-input-visit-type')
       .select('archives');
 
     // fill first artifact info
     cy.get('#swh-input-artifact-url-0')
       .type(artifactUrl)
       .get('#swh-input-artifact-version-0')
       .clear()
       .type(artifactVersion);
 
     // add new artifact form row
     cy.get('#swh-add-archive-artifact')
       .click();
 
     // check new row is displayed
     cy.get('#swh-input-artifact-url-1')
         .should('exist');
 
     // request removal of newly added row
     cy.get('#swh-remove-archive-artifact-1')
       .click();
 
     // check row has been removed
     cy.get('#swh-input-artifact-url-1')
       .should('not.exist');
 
     // add new artifact form row
     cy.get('#swh-add-archive-artifact')
       .click();
 
     // fill second artifact info
     cy.get('#swh-input-artifact-url-1')
       .type(artifact2Url)
       .get('#swh-input-artifact-version-1')
       .clear()
       .type(artifact2Version);
 
     // setup request interceptor to check POST data and stub response
     cy.intercept('POST', this.Urls.api_1_save_origin('archives', originUrl), (req) => {
       expect(req.body).to.deep.equal({
         archives_data: [
           {artifact_url: artifactUrl, artifact_version: artifactVersion},
           {artifact_url: artifact2Url, artifact_version: artifact2Version}
         ]
       });
       req.reply(genOriginSaveResponse({
         visitType: 'archives',
         saveRequestStatus: 'accepted',
         originUrl: originUrl,
         saveRequestDate: new Date(),
         saveTaskStatus: 'not yet scheduled',
         visitDate: null,
         visitStatus: null
       }));
     }).as('saveRequest');
 
     // submit form
     cy.get('#swh-save-origin-form')
       .submit();
 
     // submission should be successful
     cy.wait('@saveRequest').then(() => {
       checkAlertVisible('success', saveCodeMsg['success']);
     });
 
   });
 
   it('should autofill artifact version when pasting artifact url', function() {
-    let originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
-    let artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
-    let artifactVersion = '3DLDF-1.1.4';
-    let artifact2Url = 'https://example.org/artifact/test/1.3.0.zip';
-    let artifact2Version = '1.3.0';
+    const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
+    const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
+    const artifactVersion = '3DLDF-1.1.4';
+    const artifact2Url = 'https://example.org/artifact/test/1.3.0.zip';
+    const artifact2Version = '1.3.0';
 
     cy.ambassadorLogin();
     cy.visit(url);
 
     cy.get('#swh-input-origin-url')
       .type(originUrl)
       .get('#swh-input-visit-type')
       .select('archives');
 
     // fill first artifact info
     cy.get('#swh-input-artifact-url-0')
       .type(artifactUrl);
 
     // check autofilled version
     cy.get('#swh-input-artifact-version-0')
       .should('have.value', artifactVersion);
 
     // add new artifact form row
     cy.get('#swh-add-archive-artifact')
       .click();
 
     // fill second artifact info
     cy.get('#swh-input-artifact-url-1')
       .type(artifact2Url);
 
     // check autofilled version
     cy.get('#swh-input-artifact-version-1')
       .should('have.value', artifact2Version);
   });
 
   it('should use canonical URL for github repository to save', function() {
     const ownerRepo = 'BIC-MNI/mni_autoreg';
     const canonicalOriginUrl = 'https://github.com/BIC-MNI/mni_autoreg';
 
     // stub call to github Web API fetching canonical repo URL
     cy.intercept(`https://api.github.com/repos/${ownerRepo.toLowerCase()}`, (req) => {
       req.reply({html_url: canonicalOriginUrl});
     }).as('ghWebApiRequest');
 
     // stub save request creation with canonical URL of github repo
     cy.intercept('POST', this.Urls.api_1_save_origin('git', canonicalOriginUrl), (req) => {
       req.reply(genOriginSaveResponse({
         visitType: 'git',
         saveRequestStatus: 'accepted',
         originUrl: canonicalOriginUrl,
         saveRequestDate: new Date(),
         saveTaskStatus: 'not yet scheduled',
         visitDate: null,
         visitStatus: null
       }));
     }).as('saveRequest');
 
-    for (let originUrl of ['https://github.com/BiC-MnI/MnI_AuToReG',
-                           'https://github.com/BiC-MnI/MnI_AuToReG.git',
-                           'https://github.com/BiC-MnI/MnI_AuToReG/']) {
+    for (const originUrl of ['https://github.com/BiC-MnI/MnI_AuToReG',
+                             'https://github.com/BiC-MnI/MnI_AuToReG.git',
+                             'https://github.com/BiC-MnI/MnI_AuToReG/']) {
 
       // enter non canonical URL of github repo
       cy.get('#swh-input-origin-url')
         .clear()
         .type(originUrl);
 
       // submit form
       cy.get('#swh-save-origin-form')
         .submit();
 
       // submission should be successful
       cy.wait('@ghWebApiRequest')
         .wait('@saveRequest').then(() => {
           checkAlertVisible('success', saveCodeMsg['success']);
         });
     }
 
   });
 
 });
diff --git a/cypress/integration/origin-search.spec.js b/cypress/integration/origin-search.spec.js
index 4ecf0b12..6a7f9288 100644
--- a/cypress/integration/origin-search.spec.js
+++ b/cypress/integration/origin-search.spec.js
@@ -1,569 +1,569 @@
 /**
  * Copyright (C) 2019-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 const nonExistentText = 'NoMatchExists';
 
 let origin;
 let url;
 
 function doSearch(searchText, searchInputElt = '#swh-origins-url-patterns') {
   if (searchText.startsWith('swh:')) {
     cy.intercept('**/api/1/resolve/**')
       .as('swhidResolve');
   }
   cy.get(searchInputElt)
     // to avoid sending too much SWHID validation requests
     // as cypress insert character one by one when using type
     .invoke('val', searchText.slice(0, -1))
     .type(searchText.slice(-1))
     .get('.swh-search-icon')
     .click({force: true});
   if (searchText.startsWith('swh:')) {
     cy.wait('@swhidResolve');
   }
 }
 
 function searchShouldRedirect(searchText, redirectUrl) {
   doSearch(searchText);
   cy.location('pathname')
     .should('equal', redirectUrl);
 }
 
 function searchShouldShowNotFound(searchText, msg) {
   doSearch(searchText);
   if (searchText.startsWith('swh:')) {
     cy.get('.invalid-feedback')
       .should('be.visible')
       .and('contain', msg);
   }
 }
 
 function stubOriginVisitLatestRequests(status = 200, response = {type: 'tar'}, aliasSuffix = '') {
   cy.intercept({url: '**/visit/latest/**'}, {
     body: response,
     statusCode: status
   }).as(`originVisitLatest${aliasSuffix}`);
 }
 
 describe('Test origin-search', function() {
   before(function() {
     origin = this.origin[0];
     url = this.Urls.browse_search();
   });
 
   beforeEach(function() {
     cy.visit(url);
   });
 
   it('should have focus on search form after page load', function() {
     cy.get('#swh-origins-url-patterns')
       .should('have.attr', 'autofocus');
     // for some reason, autofocus is not honored when running cypress tests
     // while it is in non controlled browsers
     // .should('have.focus');
   });
 
   it('should redirect to browse when archived URL is searched', function() {
     cy.get('#swh-origins-url-patterns')
       .type(origin.url);
     cy.get('.swh-search-icon')
       .click();
 
     cy.location('pathname')
       .should('eq', this.Urls.browse_origin_directory());
     cy.location('search')
       .should('eq', `?origin_url=${origin.url}`);
   });
 
   it('should not redirect for non valid URL', function() {
     cy.get('#swh-origins-url-patterns')
       .type('www.example'); // Invalid URL
     cy.get('.swh-search-icon')
       .click();
 
     cy.location('pathname')
       .should('eq', this.Urls.browse_search()); // Stay in the current page
   });
 
   it('should not redirect for valid non archived URL', function() {
     cy.get('#swh-origins-url-patterns')
       .type('http://eaxmple.com/test/'); // Valid URL, but not archived
     cy.get('.swh-search-icon')
       .click();
 
     cy.location('pathname')
       .should('eq', this.Urls.browse_search()); // Stay in the current page
   });
 
   it('should remove origin URL with no archived content', function() {
     stubOriginVisitLatestRequests(404);
 
     // Using a non full origin URL here
     // This is because T3354 redirects to the origin in case of a valid, archived URL
     cy.get('#swh-origins-url-patterns')
       .type(origin.url.slice(0, -1));
     cy.get('.swh-search-icon')
       .click();
 
     cy.wait('@originVisitLatest');
 
     cy.get('#origin-search-results')
       .should('be.visible')
       .find('tbody tr').should('have.length', 0);
 
     stubOriginVisitLatestRequests(200, {}, '2');
 
     cy.get('.swh-search-icon')
       .click();
 
     cy.wait('@originVisitLatest2');
 
     cy.get('#origin-search-results')
       .should('be.visible')
       .find('tbody tr').should('have.length', 0);
 
   });
 
   it('should filter origins by visit type', function() {
     cy.intercept('**/visit/latest/**').as('checkOriginVisits');
     cy.get('#swh-origins-url-patterns')
       .type('http');
 
-    for (let visitType of ['git', 'tar']) {
+    for (const visitType of ['git', 'tar']) {
       cy.get('#swh-search-visit-type')
         .select(visitType);
 
       cy.get('.swh-search-icon')
         .click();
 
       cy.wait('@checkOriginVisits');
 
       cy.get('#origin-search-results')
         .should('be.visible');
 
       cy.get('tbody tr td.swh-origin-visit-type').then(elts => {
-        for (let elt of elts) {
+        for (const elt of elts) {
           cy.get(elt).should('have.text', visitType);
         }
       });
     }
   });
 
   it('should show not found message when no repo matches', function() {
     searchShouldShowNotFound(nonExistentText,
                              'No origins matching the search criteria were found.');
   });
 
   it('should add appropriate URL parameters', function() {
     // Check all three checkboxes and check if
     // correct url params are added
     cy.get('#swh-search-origins-with-visit')
       .check({force: true})
       .get('#swh-filter-empty-visits')
       .check({force: true})
       .get('#swh-search-origin-metadata')
       .check({force: true})
       .then(() => {
         const searchText = origin.url;
         doSearch(searchText);
         cy.location('search').then(locationSearch => {
           const urlParams = new URLSearchParams(locationSearch);
           const query = urlParams.get('q');
           const withVisit = urlParams.has('with_visit');
           const withContent = urlParams.has('with_content');
           const searchMetadata = urlParams.has('search_metadata');
 
           assert.strictEqual(query, searchText);
           assert.strictEqual(withVisit, true);
           assert.strictEqual(withContent, true);
           assert.strictEqual(searchMetadata, true);
         });
       });
   });
 
   it('should search in origin intrinsic metadata', function() {
     cy.intercept('GET', '**/origin/metadata-search/**').as(
       'originMetadataSearch'
     );
     cy.get('#swh-search-origins-with-visit')
       .check({force: true})
       .get('#swh-filter-empty-visits')
       .check({force: true})
       .get('#swh-search-origin-metadata')
       .check({force: true})
       .then(() => {
         const searchText = 'plugin';
         doSearch(searchText);
         console.log(searchText);
         cy.wait('@originMetadataSearch').then((req) => {
           expect(req.response.body[0].metadata.metadata.description).to.equal(
             'Line numbering plugin for Highlight.js'
             // metadata is defined in _TEST_ORIGINS variable in swh/web/tests/data.py
           );
         });
       });
   });
 
   it('should not send request to the resolve endpoint', function() {
     cy.intercept(`${this.Urls.api_1_resolve_swhid('').slice(0, -1)}**`)
       .as('resolveSWHID');
 
     cy.intercept(`${this.Urls.api_1_origin_search(origin.url.slice(0, -1))}**`)
       .as('searchOrigin');
 
     cy.get('#swh-origins-url-patterns')
       .type(origin.url.slice(0, -1));
 
     cy.get('.swh-search-icon')
       .click();
 
     cy.wait('@searchOrigin');
 
     cy.xhrShouldBeCalled('resolveSWHID', 0);
     cy.xhrShouldBeCalled('searchOrigin', 1);
   });
 
   context('Test pagination', function() {
     it('should not paginate if there are not many results', function() {
       // Setup search
       cy.get('#swh-search-origins-with-visit')
         .uncheck({force: true})
         .get('#swh-filter-empty-visits')
         .uncheck({force: true})
         .then(() => {
           const searchText = 'libtess';
 
           // Get first page of results
           doSearch(searchText);
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 1);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://github.com/memononen/libtess2');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('have.class', 'disabled');
         });
     });
 
     it('should paginate forward when there are many results', function() {
       stubOriginVisitLatestRequests();
       // Setup search
       cy.get('#swh-search-origins-with-visit')
         .uncheck({force: true})
         .get('#swh-filter-empty-visits')
         .uncheck({force: true})
         .then(() => {
           const searchText = 'many.origins';
 
           // Get first page of results
           doSearch(searchText);
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/1');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/100');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get second page of results
           cy.get('#origins-next-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/101');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/200');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get third (and last) page of results
           cy.get('#origins-next-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 50);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/201');
           cy.get('.swh-search-result-entry#origin-49 td a')
             .should('have.text', 'https://many.origins/250');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('have.class', 'disabled');
         });
     });
 
     it('should paginate backward from a middle page', function() {
       stubOriginVisitLatestRequests();
       // Setup search
       cy.get('#swh-search-origins-with-visit')
         .uncheck({force: true})
         .get('#swh-filter-empty-visits')
         .uncheck({force: true})
         .then(() => {
           const searchText = 'many.origins';
 
           // Get first page of results
           doSearch(searchText);
           cy.wait('@originVisitLatest');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get second page of results
           cy.get('#origins-next-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get first page of results again
           cy.get('#origins-prev-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/1');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/100');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
         });
     });
 
     it('should paginate backward from the last page', function() {
       stubOriginVisitLatestRequests();
       // Setup search
       cy.get('#swh-search-origins-with-visit')
         .uncheck({force: true})
         .get('#swh-filter-empty-visits')
         .uncheck({force: true})
         .then(() => {
           const searchText = 'many.origins';
 
           // Get first page of results
           doSearch(searchText);
           cy.wait('@originVisitLatest');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get second page of results
           cy.get('#origins-next-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get third (and last) page of results
           cy.get('#origins-next-results-button a')
             .click();
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('have.class', 'disabled');
 
           // Get second page of results again
           cy.get('#origins-prev-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/101');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/200');
 
           cy.get('#origins-prev-results-button')
             .should('not.have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
 
           // Get first page of results again
           cy.get('#origins-prev-results-button a')
             .click();
           cy.wait('@originVisitLatest');
 
           cy.get('.swh-search-result-entry')
             .should('have.length', 100);
 
           cy.get('.swh-search-result-entry#origin-0 td a')
             .should('have.text', 'https://many.origins/1');
           cy.get('.swh-search-result-entry#origin-99 td a')
             .should('have.text', 'https://many.origins/100');
 
           cy.get('#origins-prev-results-button')
             .should('have.class', 'disabled');
           cy.get('#origins-next-results-button')
             .should('not.have.class', 'disabled');
         });
     });
   });
 
   context('Test valid SWHIDs', function() {
     it('should resolve directory', function() {
       const redirectUrl = this.Urls.browse_directory(origin.content[0].directory);
       const swhid = `swh:1:dir:${origin.content[0].directory}`;
 
       searchShouldRedirect(swhid, redirectUrl);
     });
 
     it('should resolve revision', function() {
       const redirectUrl = this.Urls.browse_revision(origin.revisions[0]);
       const swhid = `swh:1:rev:${origin.revisions[0]}`;
 
       searchShouldRedirect(swhid, redirectUrl);
     });
 
     it('should resolve snapshot', function() {
       const redirectUrl = this.Urls.browse_snapshot_directory(origin.snapshot);
       const swhid = `swh:1:snp:${origin.snapshot}`;
 
       searchShouldRedirect(swhid, redirectUrl);
     });
 
     it('should resolve content', function() {
       const redirectUrl = this.Urls.browse_content(`sha1_git:${origin.content[0].sha1git}`);
       const swhid = `swh:1:cnt:${origin.content[0].sha1git}`;
 
       searchShouldRedirect(swhid, redirectUrl);
     });
 
     it('should not send request to the search endpoint', function() {
       const swhid = `swh:1:rev:${origin.revisions[0]}`;
 
       cy.intercept(this.Urls.api_1_resolve_swhid(swhid))
         .as('resolveSWHID');
 
       cy.intercept(`${this.Urls.api_1_origin_search('').slice(0, -1)}**`)
         .as('searchOrigin');
 
       cy.get('#swh-origins-url-patterns')
         .type(swhid);
 
       cy.get('.swh-search-icon')
         .click();
 
       cy.wait('@resolveSWHID');
 
       cy.xhrShouldBeCalled('resolveSWHID', 1);
       cy.xhrShouldBeCalled('searchOrigin', 0);
     });
   });
 
   context('Test invalid SWHIDs', function() {
     it('should show not found for directory', function() {
       const swhid = `swh:1:dir:${this.unarchivedRepo.rootDirectory}`;
       const msg = `Directory with sha1_git ${this.unarchivedRepo.rootDirectory} not found`;
 
       searchShouldShowNotFound(swhid, msg);
     });
 
     it('should show not found for snapshot', function() {
       const swhid = `swh:1:snp:${this.unarchivedRepo.snapshot}`;
       const msg = `Snapshot with id ${this.unarchivedRepo.snapshot} not found!`;
 
       searchShouldShowNotFound(swhid, msg);
     });
 
     it('should show not found for revision', function() {
       const swhid = `swh:1:rev:${this.unarchivedRepo.revision}`;
       const msg = `Revision with sha1_git ${this.unarchivedRepo.revision} not found.`;
 
       searchShouldShowNotFound(swhid, msg);
     });
 
     it('should show not found for content', function() {
       const swhid = `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`;
       const msg = `Content with sha1_git checksum equals to ${this.unarchivedRepo.content[0].sha1git} not found!`;
 
       searchShouldShowNotFound(swhid, msg);
     });
 
     function checkInvalidSWHIDReport(url, searchInputElt, swhidInput, validationMessagePattern = '') {
       cy.visit(url);
       doSearch(swhidInput, searchInputElt);
       cy.get(searchInputElt)
         .then($el => $el[0].checkValidity()).should('be.false');
       cy.get(searchInputElt)
         .invoke('prop', 'validationMessage')
         .should('not.equal', '')
         .should('contain', validationMessagePattern);
     }
 
     it('should report invalid SWHID in search page input', function() {
       const swhidInput =
         `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git};lines=45-60/`;
       checkInvalidSWHIDReport(this.Urls.browse_search(), '#swh-origins-url-patterns', swhidInput);
       cy.get('.invalid-feedback')
         .should('be.visible');
     });
 
     it('should report invalid SWHID in top right search input', function() {
       const swhidInput =
         `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git};lines=45-60/`;
       checkInvalidSWHIDReport(this.Urls.browse_help(), '#swh-origins-search-top-input', swhidInput);
     });
 
     it('should report SWHID with uppercase chars in search page input', function() {
       const swhidInput =
         `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`.toUpperCase();
       checkInvalidSWHIDReport(this.Urls.browse_search(), '#swh-origins-url-patterns', swhidInput, swhidInput.toLowerCase());
       cy.get('.invalid-feedback')
         .should('be.visible');
     });
 
     it('should report SWHID with uppercase chars in top right search input', function() {
       let swhidInput =
         `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`.toUpperCase();
       swhidInput += ';lines=45-60/';
       checkInvalidSWHIDReport(this.Urls.browse_help(), '#swh-origins-search-top-input', swhidInput.toLowerCase());
     });
 
   });
 
 });
diff --git a/cypress/integration/persistent-identifiers.spec.js b/cypress/integration/persistent-identifiers.spec.js
index a4d57cfd..5211d963 100644
--- a/cypress/integration/persistent-identifiers.spec.js
+++ b/cypress/integration/persistent-identifiers.spec.js
@@ -1,228 +1,228 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 let origin, originBadgeUrl, originBrowseUrl;
 let url, urlPrefix;
 let cntSWHID, cntSWHIDWithContext;
 let dirSWHID, dirSWHIDWithContext;
 let relSWHID, relSWHIDWithContext;
 let revSWHID, revSWHIDWithContext;
 let snpSWHID, snpSWHIDWithContext;
 let testsData;
 const firstSelLine = 6;
 const lastSelLine = 12;
 
 describe('Persistent Identifiers Tests', function() {
 
   before(function() {
     origin = this.origin[1];
     url = `${this.Urls.browse_origin_content()}?origin_url=${origin.url}&path=${origin.content[0].path}`;
     url = `${url}&release=${origin.release.name}#L${firstSelLine}-L${lastSelLine}`;
     originBadgeUrl = this.Urls.swh_badge('origin', origin.url);
     originBrowseUrl = `${this.Urls.browse_origin()}?origin_url=${origin.url}`;
     cy.visit(url).window().then(win => {
       urlPrefix = `${win.location.protocol}//${win.location.hostname}`;
       if (win.location.port) {
         urlPrefix += `:${win.location.port}`;
       }
       const swhids = win.swh.webapp.getSwhIdsContext();
       cntSWHID = swhids.content.swhid;
       cntSWHIDWithContext = swhids.content.swhid_with_context;
       cntSWHIDWithContext += `;lines=${firstSelLine}-${lastSelLine}`;
       dirSWHID = swhids.directory.swhid;
       dirSWHIDWithContext = swhids.directory.swhid_with_context;
       revSWHID = swhids.revision.swhid;
       revSWHIDWithContext = swhids.revision.swhid_with_context;
       relSWHID = swhids.release.swhid;
       relSWHIDWithContext = swhids.release.swhid_with_context;
       snpSWHID = swhids.snapshot.swhid;
       snpSWHIDWithContext = swhids.snapshot.swhid_with_context;
 
       testsData = [
         {
           'objectType': 'content',
           'objectSWHIDs': [cntSWHIDWithContext, cntSWHID],
           'badgeUrl': this.Urls.swh_badge('content', swhids.content.object_id),
           'badgeSWHIDUrl': this.Urls.swh_badge_swhid(cntSWHID),
           'browseUrl': this.Urls.browse_swhid(cntSWHIDWithContext)
         },
         {
           'objectType': 'directory',
           'objectSWHIDs': [dirSWHIDWithContext, dirSWHID],
           'badgeUrl': this.Urls.swh_badge('directory', swhids.directory.object_id),
           'badgeSWHIDUrl': this.Urls.swh_badge_swhid(dirSWHID),
           'browseUrl': this.Urls.browse_swhid(dirSWHIDWithContext)
         },
         {
           'objectType': 'release',
           'objectSWHIDs': [relSWHIDWithContext, relSWHID],
           'badgeUrl': this.Urls.swh_badge('release', swhids.release.object_id),
           'badgeSWHIDUrl': this.Urls.swh_badge_swhid(relSWHID),
           'browseUrl': this.Urls.browse_swhid(relSWHIDWithContext)
         },
         {
           'objectType': 'revision',
           'objectSWHIDs': [revSWHIDWithContext, revSWHID],
           'badgeUrl': this.Urls.swh_badge('revision', swhids.revision.object_id),
           'badgeSWHIDUrl': this.Urls.swh_badge_swhid(revSWHID),
           'browseUrl': this.Urls.browse_swhid(revSWHIDWithContext)
         },
         {
           'objectType': 'snapshot',
           'objectSWHIDs': [snpSWHIDWithContext, snpSWHID],
           'badgeUrl': this.Urls.swh_badge('snapshot', swhids.snapshot.object_id),
           'badgeSWHIDUrl': this.Urls.swh_badge_swhid(snpSWHID),
           'browseUrl': this.Urls.browse_swhid(snpSWHIDWithContext)
         }
       ];
 
     });
   });
 
   beforeEach(function() {
     cy.visit(url);
   });
 
   it('should open and close identifiers tab when clicking on handle', function() {
     cy.get('#swh-identifiers')
       .should('have.class', 'ui-slideouttab-ready');
 
     cy.get('.ui-slideouttab-handle')
       .click();
 
     cy.get('#swh-identifiers')
       .should('have.class', 'ui-slideouttab-open');
 
     cy.get('.ui-slideouttab-handle')
       .click();
 
     cy.get('#swh-identifiers')
       .should('not.have.class', 'ui-slideouttab-open');
 
   });
 
   it('should display identifiers with permalinks for browsed objects', function() {
     cy.get('.ui-slideouttab-handle')
       .click();
 
-    for (let td of testsData) {
+    for (const td of testsData) {
       cy.get(`a[href="#swhid-tab-${td.objectType}"]`)
         .click();
 
       cy.get(`#swhid-tab-${td.objectType}`)
         .should('be.visible');
 
       cy.get(`#swhid-tab-${td.objectType} .swhid`)
         .should('have.text', td.objectSWHIDs[0].replace(/;/g, ';\n'))
         .should('have.attr', 'href', this.Urls.browse_swhid(td.objectSWHIDs[0]));
 
     }
 
   });
 
   it('should update other object identifiers contextual info when toggling context checkbox', function() {
     cy.get('.ui-slideouttab-handle')
       .click();
 
-    for (let td of testsData) {
+    for (const td of testsData) {
 
       cy.get(`a[href="#swhid-tab-${td.objectType}"]`)
         .click();
 
       cy.get(`#swhid-tab-${td.objectType} .swhid`)
         .should('have.text', td.objectSWHIDs[0].replace(/;/g, ';\n'))
         .should('have.attr', 'href', this.Urls.browse_swhid(td.objectSWHIDs[0]));
 
       cy.get(`#swhid-tab-${td.objectType} .swhid-option`)
         .click();
 
       cy.get(`#swhid-tab-${td.objectType} .swhid`)
         .contains(td.objectSWHIDs[1])
         .should('have.attr', 'href', this.Urls.browse_swhid(td.objectSWHIDs[1]));
 
       cy.get(`#swhid-tab-${td.objectType} .swhid-option`)
         .click();
 
       cy.get(`#swhid-tab-${td.objectType} .swhid`)
         .should('have.text', td.objectSWHIDs[0].replace(/;/g, ';\n'))
         .should('have.attr', 'href', this.Urls.browse_swhid(td.objectSWHIDs[0]));
     }
 
   });
 
   it('should display swh badges in identifiers tab for browsed objects', function() {
     cy.get('.ui-slideouttab-handle')
       .click();
 
     const originBadgeUrl = this.Urls.swh_badge('origin', origin.url);
 
-    for (let td of testsData) {
+    for (const td of testsData) {
       cy.get(`a[href="#swhid-tab-${td.objectType}"]`)
         .click();
 
       cy.get(`#swhid-tab-${td.objectType} .swh-badge-origin`)
         .should('have.attr', 'src', originBadgeUrl);
 
       cy.get(`#swhid-tab-${td.objectType} .swh-badge-${td.objectType}`)
         .should('have.attr', 'src', td.badgeUrl);
 
     }
 
   });
 
   it('should display badge integration info when clicking on it', function() {
 
     cy.get('.ui-slideouttab-handle')
       .click();
 
-    for (let td of testsData) {
+    for (const td of testsData) {
       cy.get(`a[href="#swhid-tab-${td.objectType}"]`)
         .click();
 
       cy.get(`#swhid-tab-${td.objectType} .swh-badge-origin`)
         .click()
         .wait(500);
 
-      for (let badgeType of ['html', 'md', 'rst']) {
+      for (const badgeType of ['html', 'md', 'rst']) {
         cy.get(`.modal .swh-badge-${badgeType}`)
           .contains(`${urlPrefix}${originBrowseUrl}`)
           .contains(`${urlPrefix}${originBadgeUrl}`);
       }
 
       cy.get('.modal.show .close')
         .click()
         .wait(500);
 
       cy.get(`#swhid-tab-${td.objectType} .swh-badge-${td.objectType}`)
         .click()
         .wait(500);
 
-      for (let badgeType of ['html', 'md', 'rst']) {
+      for (const badgeType of ['html', 'md', 'rst']) {
         cy.get(`.modal .swh-badge-${badgeType}`)
           .contains(`${urlPrefix}${td.browseUrl}`)
           .contains(`${urlPrefix}${td.badgeSWHIDUrl}`);
       }
 
       cy.get('.modal.show .close')
         .click()
         .wait(500);
 
     }
   });
 
   it('should be possible to retrieve SWHIDs context from JavaScript', function() {
     cy.window().then(win => {
       const swhIdsContext = win.swh.webapp.getSwhIdsContext();
-      for (let testData of testsData) {
+      for (const testData of testsData) {
         assert.isTrue(swhIdsContext.hasOwnProperty(testData.objectType));
         assert.equal(swhIdsContext[testData.objectType].swhid,
                      testData.objectSWHIDs.slice(-1)[0]);
       }
     });
   });
 
 });
diff --git a/cypress/integration/revision-diff.spec.js b/cypress/integration/revision-diff.spec.js
index 12f45f0c..13852d31 100644
--- a/cypress/integration/revision-diff.spec.js
+++ b/cypress/integration/revision-diff.spec.js
@@ -1,492 +1,492 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 const $ = Cypress.$;
 const origin = 'https://github.com/memononen/libtess2';
 const revision = '98c65dad5e47ad888032b6cdf556f192e0e028d0';
 
 const diffsHighlightingData = {
   'unified': {
     diffId: '3d4c0797cf0e89430410e088339aac384dfa4d82',
     startLines: [913, 915],
     endLines: [0, 979]
   },
   'split-from': {
     diffId: '602cec77c3d3f41d396d9e1083a0bbce0796b505',
     startLines: [192, 0],
     endLines: [198, 0]
   },
   'split-to': {
     diffId: '602cec77c3d3f41d396d9e1083a0bbce0796b505',
     startLines: [0, 120],
     endLines: [0, 130]
   },
   'split-from-top-to-bottom': {
     diffId: 'a00c33990655a93aa2c821c4008bbddda812a896',
     startLines: [63, 0],
     endLines: [0, 68]
   },
   'split-to-top-from-bottom': {
     diffId: 'a00c33990655a93aa2c821c4008bbddda812a896',
     startLines: [0, 63],
     endLines: [67, 0]
   }
 };
 
 let diffData;
 let swh;
 
 describe('Test Revision View', function() {
 
   it('should add/remove #swh-revision-changes url fragment when switching tab', function() {
     const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
     cy.visit(url);
     cy.get('a[data-toggle="tab"]')
       .contains('Changes')
       .click();
     cy.hash().should('be.equal', '#swh-revision-changes');
     cy.get('a[data-toggle="tab"]')
       .contains('Files')
       .click();
     cy.hash().should('be.equal', '');
   });
 
   it('should display Changes tab by default when url ends with #swh-revision-changes', function() {
     const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
     cy.visit(url + '#swh-revision-changes');
     cy.get('#swh-revision-changes-list')
       .should('be.visible');
   });
 });
 
 describe('Test Diffs View', function() {
 
   beforeEach(function() {
     const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
     cy.visit(url);
     cy.window().then(win => {
       swh = win.swh;
       cy.request(win.diffRevUrl)
         .then(res => {
           diffData = res.body;
         });
     });
     cy.get('a[data-toggle="tab"]')
       .contains('Changes')
       .click();
   });
 
   it('should list all files with changes', function() {
-    let files = new Set([]);
-    for (let change of diffData.changes) {
+    const files = new Set([]);
+    for (const change of diffData.changes) {
       files.add(change.from_path);
       files.add(change.to_path);
     }
-    for (let file of files) {
+    for (const file of files) {
       cy.get('#swh-revision-changes-list a')
         .contains(file)
         .should('be.visible');
     }
   });
 
   it('should load diffs when scrolled down', function() {
     cy.get('#swh-revision-changes-list a')
       .each($el => {
         cy.get($el.attr('href'))
           .scrollIntoView()
           .find('.swh-content')
           .should('be.visible');
       });
   });
 
   it('should compute all diffs when selected', function() {
     cy.get('#swh-compute-all-diffs')
       .click();
     cy.get('#swh-revision-changes-list a')
       .each($el => {
         cy.get($el.attr('href'))
           .find('.swh-content')
           .should('be.visible');
       });
   });
 
   it('should have correct links in diff file names', function() {
-    for (let change of diffData.changes) {
+    for (const change of diffData.changes) {
       cy.get(`#swh-revision-changes-list a[href="#diff_${change.id}"`)
         .should('be.visible');
     }
   });
 
   it('should load unified diff by default', function() {
     cy.get('#swh-compute-all-diffs')
       .click();
-    for (let change of diffData.changes) {
+    for (const change of diffData.changes) {
       cy.get(`#${change.id}-unified-diff`)
         .should('be.visible');
       cy.get(`#${change.id}-split-diff`)
         .should('not.be.visible');
     }
   });
 
   it('should switch between unified and side-by-side diff when selected', function() {
     // Test for first diff
     const id = diffData.changes[0].id;
     cy.get(`#diff_${id}`)
       .contains('label', 'Side-by-side')
       .click();
     cy.get(`#${id}-split-diff`)
       .should('be.visible')
       .get(`#${id}-unified-diff`)
       .should('not.be.visible');
   });
 
   function checkDiffHighlighted(diffId, start, end) {
     cy.get(`#${diffId} .hljs-ln-line`)
       .then(lines => {
         let inHighlightedRange = false;
-        for (let line of lines) {
+        for (const line of lines) {
           const lnNumber = $(line).data('line-number');
           if (lnNumber === start || lnNumber === end) {
             inHighlightedRange = true;
           }
           const backgroundColor = $(line).css('background-color');
           const mixBlendMode = $(line).css('mix-blend-mode');
           if (inHighlightedRange && parseInt(lnNumber)) {
             assert.equal(mixBlendMode, 'multiply');
             assert.notEqual(backgroundColor, 'rgba(0, 0, 0, 0)');
           } else {
             assert.equal(mixBlendMode, 'normal');
             assert.equal(backgroundColor, 'rgba(0, 0, 0, 0)');
           }
           if (lnNumber === end) {
             inHighlightedRange = false;
           }
         }
       });
   }
 
   function unifiedDiffHighlightingTest(diffId, startLines, endLines) {
     // render diff
     cy.get(`#diff_${diffId}`)
       .scrollIntoView()
       .get(`#${diffId}-unified-diff`)
       .should('be.visible')
       // ensure all asynchronous treatments in the page have been performed
       // before testing diff highlighting
       .then(() => {
 
         let startLinesStr = swh.revision.formatDiffLineNumbers(diffId, startLines[0], startLines[1]);
         let endLinesStr = swh.revision.formatDiffLineNumbers(diffId, endLines[0], endLines[1]);
 
         // highlight a range of lines
-        let startElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${startLinesStr}"]`;
-        let endElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${endLinesStr}"]`;
+        const startElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${startLinesStr}"]`;
+        const endElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${endLinesStr}"]`;
         cy.get(startElt).click();
         cy.get(endElt).click({shiftKey: true});
 
         // check URL fragment has been updated
         const selectedLinesFragment =
           swh.revision.selectedDiffLinesToFragment(startLines, endLines, true);
         cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
 
         if ($(endElt).position().top < $(startElt).position().top) {
           [startLinesStr, endLinesStr] = [endLinesStr, startLinesStr];
         }
 
         // check lines range is highlighted
         checkDiffHighlighted(`${diffId}-unified-diff`, startLinesStr, endLinesStr);
 
         // check selected diff lines get highlighted when reloading page
         // with highlighting info in URL fragment
         cy.reload();
         cy.get(`#diff_${diffId}`)
           .get(`#${diffId}-unified-diff`)
           .should('be.visible');
 
         checkDiffHighlighted(`${diffId}-unified-diff`, startLinesStr, endLinesStr);
       });
   }
 
   it('should highlight unified diff lines when selecting them from top to bottom', function() {
 
     const diffHighlightingData = diffsHighlightingData['unified'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     unifiedDiffHighlightingTest(diffId, startLines, endLines);
 
   });
 
   it('should highlight unified diff lines when selecting them from bottom to top', function() {
 
     const diffHighlightingData = diffsHighlightingData['unified'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     unifiedDiffHighlightingTest(diffId, endLines, startLines);
 
   });
 
   function singleSpitDiffHighlightingTest(diffId, startLines, endLines, to) {
 
     let singleDiffId = `${diffId}-from`;
     if (to) {
       singleDiffId = `${diffId}-to`;
     }
 
     let startLine = startLines[0] || startLines[1];
     let endLine = endLines[0] || endLines[1];
 
     // render diff
     cy.get(`#diff_${diffId}`)
       .scrollIntoView()
       .get(`#${diffId}-unified-diff`)
       .should('be.visible');
 
     cy.get(`#diff_${diffId}`)
       .contains('label', 'Side-by-side')
       .click()
       // ensure all asynchronous treatments in the page have been performed
       // before testing diff highlighting
       .then(() => {
         // highlight a range of lines
-        let startElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${startLine}"]`;
-        let endElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${endLine}"]`;
+        const startElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${startLine}"]`;
+        const endElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${endLine}"]`;
         cy.get(startElt).click();
         cy.get(endElt).click({shiftKey: true});
 
         const selectedLinesFragment =
           swh.revision.selectedDiffLinesToFragment(startLines, endLines, false);
 
         // check URL fragment has been updated
         cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
 
         if ($(endElt).position().top < $(startElt).position().top) {
           [startLine, endLine] = [endLine, startLine];
         }
 
         // check lines range is highlighted
         checkDiffHighlighted(`${singleDiffId}`, startLine, endLine);
 
         // check selected diff lines get highlighted when reloading page
         // with highlighting info in URL fragment
         cy.reload();
         cy.get(`#diff_${diffId}`)
           .get(`#${diffId}-split-diff`)
           .get(`#${singleDiffId}`)
           .should('be.visible');
         checkDiffHighlighted(`${singleDiffId}`, startLine, endLine);
       });
   }
 
   it('should highlight split diff from lines when selecting them from top to bottom', function() {
     const diffHighlightingData = diffsHighlightingData['split-from'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     singleSpitDiffHighlightingTest(diffId, startLines, endLines, false);
   });
 
   it('should highlight split diff from lines when selecting them from bottom to top', function() {
     const diffHighlightingData = diffsHighlightingData['split-from'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     singleSpitDiffHighlightingTest(diffId, endLines, startLines, false);
   });
 
   it('should highlight split diff to lines when selecting them from top to bottom', function() {
     const diffHighlightingData = diffsHighlightingData['split-to'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     singleSpitDiffHighlightingTest(diffId, startLines, endLines, true);
   });
 
   it('should highlight split diff to lines when selecting them from bottom to top', function() {
 
     const diffHighlightingData = diffsHighlightingData['split-to'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     singleSpitDiffHighlightingTest(diffId, endLines, startLines, true);
   });
 
   function checkSplitDiffHighlighted(diffId, startLines, endLines) {
     let left, right;
     if (startLines[0] && endLines[1]) {
       left = startLines[0];
       right = endLines[1];
     } else {
       left = endLines[0];
       right = startLines[1];
     }
 
     cy.get(`#${diffId}-from .hljs-ln-line`)
       .then(fromLines => {
         cy.get(`#${diffId}-to .hljs-ln-line`)
           .then(toLines => {
             const leftLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${left}"]`);
             const rightLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${right}"]`);
             const leftLineAbove = $(leftLine).position().top < $(rightLine).position().top;
             let inHighlightedRange = false;
             for (let i = 0; i < Math.max(fromLines.length, toLines.length); ++i) {
               const fromLn = fromLines[i];
               const toLn = toLines[i];
               const fromLnNumber = $(fromLn).data('line-number');
               const toLnNumber = $(toLn).data('line-number');
 
               if ((leftLineAbove && fromLnNumber === left) ||
                   (!leftLineAbove && toLnNumber === right) ||
                   (leftLineAbove && toLnNumber === right) ||
                   (!leftLineAbove && fromLnNumber === left)) {
                 inHighlightedRange = true;
               }
 
               if (fromLn) {
                 const fromBackgroundColor = $(fromLn).css('background-color');
                 const fromMixBlendMode = $(fromLn).css('mix-blend-mode');
                 if (inHighlightedRange && fromLnNumber) {
                   assert.equal(fromMixBlendMode, 'multiply');
                   assert.notEqual(fromBackgroundColor, 'rgba(0, 0, 0, 0)');
                 } else {
                   assert.equal(fromMixBlendMode, 'normal');
                   assert.equal(fromBackgroundColor, 'rgba(0, 0, 0, 0)');
                 }
               }
 
               if (toLn) {
                 const toBackgroundColor = $(toLn).css('background-color');
                 const toMixBlendMode = $(toLn).css('mix-blend-mode');
                 if (inHighlightedRange && toLnNumber) {
                   assert.equal(toMixBlendMode, 'multiply');
                   assert.notEqual(toBackgroundColor, 'rgba(0, 0, 0, 0)');
                 } else {
                   assert.equal(toMixBlendMode, 'normal');
                   assert.equal(toBackgroundColor, 'rgba(0, 0, 0, 0)');
                 }
               }
 
               if ((leftLineAbove && toLnNumber === right) ||
                   (!leftLineAbove && fromLnNumber === left)) {
                 inHighlightedRange = false;
               }
             }
           });
       });
   }
 
   function splitDiffHighlightingTest(diffId, startLines, endLines) {
     // render diff
     cy.get(`#diff_${diffId}`)
       .scrollIntoView()
       .find(`#${diffId}-unified-diff`)
       .should('be.visible');
 
     cy.get(`#diff_${diffId}`)
       .contains('label', 'Side-by-side')
       .click()
       // ensure all asynchronous treatments in the page have been performed
       // before testing diff highlighting
       .then(() => {
 
         // select lines range in diff
         let startElt;
         if (startLines[0]) {
           startElt = `#${diffId}-from .hljs-ln-numbers[data-line-number="${startLines[0]}"]`;
         } else {
           startElt = `#${diffId}-to .hljs-ln-numbers[data-line-number="${startLines[1]}"]`;
         }
         let endElt;
         if (endLines[0]) {
           endElt = `#${diffId}-from .hljs-ln-numbers[data-line-number="${endLines[0]}"]`;
         } else {
           endElt = `#${diffId}-to .hljs-ln-numbers[data-line-number="${endLines[1]}"]`;
         }
 
         cy.get(startElt).click();
         cy.get(endElt).click({shiftKey: true});
 
         const selectedLinesFragment =
             swh.revision.selectedDiffLinesToFragment(startLines, endLines, false);
 
         // check URL fragment has been updated
         cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
 
         // check lines range is highlighted
         checkSplitDiffHighlighted(diffId, startLines, endLines);
 
         // check selected diff lines get highlighted when reloading page
         // with highlighting info in URL fragment
         cy.reload();
         cy.get(`#diff_${diffId}`)
             .get(`#${diffId}-split-diff`)
             .get(`#${diffId}-to`)
             .should('be.visible');
 
         checkSplitDiffHighlighted(diffId, startLines, endLines);
       });
   }
 
   it('should highlight split diff from and to lines when selecting them from top-left to bottom-right', function() {
     const diffHighlightingData = diffsHighlightingData['split-from-top-to-bottom'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     splitDiffHighlightingTest(diffId, startLines, endLines);
   });
 
   it('should highlight split diff from and to lines when selecting them from bottom-right to top-left', function() {
     const diffHighlightingData = diffsHighlightingData['split-from-top-to-bottom'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     splitDiffHighlightingTest(diffId, endLines, startLines);
   });
 
   it('should highlight split diff from and to lines when selecting them from top-right to bottom-left', function() {
     const diffHighlightingData = diffsHighlightingData['split-to-top-from-bottom'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     splitDiffHighlightingTest(diffId, startLines, endLines);
   });
 
   it('should highlight split diff from and to lines when selecting them from bottom-left to top-right', function() {
     const diffHighlightingData = diffsHighlightingData['split-to-top-from-bottom'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     splitDiffHighlightingTest(diffId, endLines, startLines);
   });
 
   it('should highlight diff lines properly when a content is browsed in the Files tab', function() {
     const url = this.Urls.browse_revision(revision) + `?origin=${origin}&path=README.md`;
     cy.visit(url);
     cy.get('a[data-toggle="tab"]')
       .contains('Changes')
       .click();
     const diffHighlightingData = diffsHighlightingData['unified'];
     const diffId = diffHighlightingData.diffId;
-    let startLines = diffHighlightingData.startLines;
-    let endLines = diffHighlightingData.endLines;
+    const startLines = diffHighlightingData.startLines;
+    const endLines = diffHighlightingData.endLines;
 
     unifiedDiffHighlightingTest(diffId, startLines, endLines);
 
   });
 
 });
diff --git a/cypress/integration/vault.spec.js b/cypress/integration/vault.spec.js
index 2cfa97df..d2d6be7c 100644
--- a/cypress/integration/vault.spec.js
+++ b/cypress/integration/vault.spec.js
@@ -1,504 +1,504 @@
 /**
  * Copyright (C) 2019-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
-let vaultItems = [];
+const vaultItems = [];
 
 const progressbarColors = {
   'new': 'rgba(128, 128, 128, 0.5)',
   'pending': 'rgba(0, 0, 255, 0.5)',
   'done': 'rgb(92, 184, 92)'
 };
 
 function checkVaultCookingTask(objectType) {
   cy.contains('button', 'Download')
     .click();
 
   cy.contains('.dropdown-item', objectType)
     .click();
 
   cy.wait('@checkVaultCookingTask');
 }
 
 function updateVaultItemList(vaultUrl, vaultItems) {
   cy.visit(vaultUrl)
     .then(() => {
       // Add uncooked task to localStorage
       // which updates it in vault items list
       window.localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultItems));
     });
 }
 
 // Mocks API response : /api/1/vault/(:objectType)/(:hash)
 // objectType : {'directory', 'revision'}
 function genVaultCookingResponse(objectType, objectId, status, message, fetchUrl) {
   return {
     'obj_type': objectType,
     'id': 1,
     'progress_message': message,
     'status': status,
     'obj_id': objectId,
     'fetch_url': fetchUrl
   };
 };
 
 // Tests progressbar color, status
 // And status in localStorage
 function testStatus(taskId, color, statusMsg, status) {
   cy.get(`.swh-vault-table #vault-task-${taskId}`)
     .should('be.visible')
     .find('.progress-bar')
     .should('be.visible')
     .and('have.css', 'background-color', color)
     .and('contain', statusMsg)
     .then(() => {
       // Vault item with object_id as taskId should exist in localStorage
       const currentVaultItems = JSON.parse(window.localStorage.getItem('swh-vault-cooking-tasks'));
       const vaultItem = currentVaultItems.find(obj => obj.object_id === taskId);
 
       assert.isNotNull(vaultItem);
       assert.strictEqual(vaultItem.status, status);
     });
 }
 
 describe('Vault Cooking User Interface Tests', function() {
 
   before(function() {
     const dirInfo = this.origin[0].directory[0];
     this.directory = dirInfo.id;
     this.directoryUrl = this.Urls.browse_origin_directory() +
       `?origin_url=${this.origin[0].url}&path=${dirInfo.path}`;
     this.vaultDirectoryUrl = this.Urls.api_1_vault_cook_directory(this.directory);
     this.vaultFetchDirectoryUrl = this.Urls.api_1_vault_fetch_directory(this.directory);
 
     this.revision = this.origin[1].revisions[0];
     this.revisionUrl = this.Urls.browse_revision(this.revision);
     this.vaultRevisionUrl = this.Urls.api_1_vault_cook_revision_gitfast(this.revision);
     this.vaultFetchRevisionUrl = this.Urls.api_1_vault_fetch_revision_gitfast(this.revision);
 
     const release = this.origin[1].release;
     this.releaseUrl = this.Urls.browse_release(release.id) + `?origin_url=${this.origin[1].url}`;
     this.vaultReleaseDirectoryUrl = this.Urls.api_1_vault_cook_directory(release.directory);
 
     vaultItems[0] = {
       'object_type': 'revision',
       'object_id': this.revision,
       'email': '',
       'status': 'done',
       'fetch_url': `/api/1/vault/revision/${this.revision}/gitfast/raw/`,
       'progress_message': null
     };
   });
 
   beforeEach(function() {
     this.genVaultDirCookingResponse = (status, message = null) => {
       return genVaultCookingResponse('directory', this.directory, status,
                                      message, this.vaultFetchDirectoryUrl);
     };
 
     this.genVaultRevCookingResponse = (status, message = null) => {
       return genVaultCookingResponse('revision', this.revision, status,
                                      message, this.vaultFetchRevisionUrl);
     };
 
   });
 
   it('should report an error when vault service is experiencing issues', function() {
     // Browse a directory
     cy.visit(this.directoryUrl);
 
     // Stub responses when requesting the vault API to simulate
     // an internal server error
     cy.intercept(this.vaultDirectoryUrl, {
       body: {'exception': 'APIError'},
       statusCode: 500
     }).as('checkVaultCookingTask');
 
     cy.contains('button', 'Download')
       .click();
 
     // Check error alert is displayed
     cy.get('.alert-danger')
     .should('be.visible')
     .should('contain', 'Archive cooking service is currently experiencing issues.');
   });
 
   it('should report an error when a cooking task creation failed', function() {
 
     // Browse a directory
     cy.visit(this.directoryUrl);
 
     // Stub responses when requesting the vault API to simulate
     // a task can not be created
     cy.intercept('GET', this.vaultDirectoryUrl, {
       body: {'exception': 'NotFoundExc'}
     }).as('checkVaultCookingTask');
 
     cy.intercept('POST', this.vaultDirectoryUrl, {
       body: {'exception': 'ValueError'},
       statusCode: 500
     }).as('createVaultCookingTask');
 
     cy.contains('button', 'Download')
       .click();
 
     // Create a vault cooking task through the GUI
     cy.get('.modal-dialog')
       .contains('button:visible', 'Ok')
       .click();
 
     cy.wait('@createVaultCookingTask');
 
     // Check error alert is displayed
     cy.get('.alert-danger')
       .should('be.visible')
       .should('contain', 'Archive cooking request submission failed.');
   });
 
   it('should create a directory cooking task and report the success', function() {
 
     // Browse a directory
     cy.visit(this.directoryUrl);
 
     // Stub response to the vault API to simulate archive download
     cy.intercept('GET', this.vaultFetchDirectoryUrl, {
       fixture: `${this.directory}.tar.gz`,
       headers: {
         'Content-disposition': `attachment; filename=${this.directory}.tar.gz`,
         'Content-Type': 'application/gzip'
       }
     }).as('fetchCookedArchive');
 
     // Stub responses when checking vault task status
     const checkVaulResponses = [
       {'exception': 'NotFoundExc'},
       this.genVaultDirCookingResponse('new'),
       this.genVaultDirCookingResponse('pending', 'Processing...'),
       this.genVaultDirCookingResponse('done')
     ];
 
     // trick to override the response of an intercepted request
     // https://github.com/cypress-io/cypress/issues/9302
     cy.intercept('GET', this.vaultDirectoryUrl, req => req.reply(checkVaulResponses.shift()))
       .as('checkVaultCookingTask');
 
     // Stub responses when requesting the vault API to simulate
     // a task has been created
     cy.intercept('POST', this.vaultDirectoryUrl, {
       body: this.genVaultDirCookingResponse('new')
     }).as('createVaultCookingTask');
 
     cy.contains('button', 'Download')
       .click();
 
     cy.window().then(win => {
       const swhIdsContext = win.swh.webapp.getSwhIdsContext();
       const browseDirectoryUrl = swhIdsContext.directory.swhid_with_context_url;
 
       // Create a vault cooking task through the GUI
       cy.get('.modal-dialog')
         .contains('button:visible', 'Ok')
         .click();
 
       cy.wait('@createVaultCookingTask');
 
       // Check success alert is displayed
       cy.get('.alert-success')
         .should('be.visible')
         .should('contain', 'Archive cooking request successfully submitted.');
 
       // Go to Downloads page
       cy.visit(this.Urls.browse_vault());
 
       cy.wait('@checkVaultCookingTask').then(() => {
         testStatus(this.directory, progressbarColors['new'], 'new', 'new');
       });
 
       cy.wait('@checkVaultCookingTask').then(() => {
         testStatus(this.directory, progressbarColors['pending'], 'Processing...', 'pending');
       });
 
       cy.wait('@checkVaultCookingTask').then(() => {
         testStatus(this.directory, progressbarColors['done'], 'done', 'done');
       });
 
       cy.get(`#vault-task-${this.directory} .vault-origin a`)
         .should('contain', this.origin[0].url)
         .should('have.attr', 'href', `${this.Urls.browse_origin()}?origin_url=${this.origin[0].url}`);
 
       cy.get(`#vault-task-${this.directory} .vault-object-info a`)
         .should('have.text', this.directory)
         .should('have.attr', 'href', browseDirectoryUrl);
 
       cy.get(`#vault-task-${this.directory} .vault-dl-link button`)
         .click();
 
       cy.wait('@fetchCookedArchive').then((xhr) => {
         assert.isNotNull(xhr.response.body);
       });
     });
   });
 
   it('should create a revision cooking task and report its status', function() {
     cy.adminLogin();
 
     // Browse a revision
     cy.visit(this.revisionUrl);
 
     // Stub response to the vault API indicating to simulate archive download
     cy.intercept({url: this.vaultFetchRevisionUrl}, {
       fixture: `${this.revision}.gitfast.gz`,
       headers: {
         'Content-disposition': `attachment; filename=${this.revision}.gitfast.gz`,
         'Content-Type': 'application/gzip'
       }
     }).as('fetchCookedArchive');
 
     // Stub responses when checking vault task status
     const checkVaultResponses = [
       {'exception': 'NotFoundExc'},
       this.genVaultRevCookingResponse('new'),
       this.genVaultRevCookingResponse('pending', 'Processing...'),
       this.genVaultRevCookingResponse('done')
     ];
 
     // trick to override the response of an intercepted request
     // https://github.com/cypress-io/cypress/issues/9302
     cy.intercept('GET', this.vaultRevisionUrl, req => req.reply(checkVaultResponses.shift()))
       .as('checkVaultCookingTask');
 
     // Stub responses when requesting the vault API to simulate
     // a task has been created
     cy.intercept('POST', this.vaultRevisionUrl, {
       body: this.genVaultRevCookingResponse('new')
     }).as('createVaultCookingTask');
 
     // Create a vault cooking task through the GUI
     checkVaultCookingTask('as git');
 
     cy.window().then(win => {
       const swhIdsContext = win.swh.webapp.getSwhIdsContext();
       const browseRevisionUrl = swhIdsContext.revision.swhid_url;
 
       // Create a vault cooking task through the GUI
       cy.get('.modal-dialog')
         .contains('button:visible', 'Ok')
         .click();
 
       cy.wait('@createVaultCookingTask');
 
       // Check success alert is displayed
       cy.get('.alert-success')
         .should('be.visible')
         .should('contain', 'Archive cooking request successfully submitted.');
 
       // Go to Downloads page
       cy.visit(this.Urls.browse_vault());
 
       cy.wait('@checkVaultCookingTask').then(() => {
         testStatus(this.revision, progressbarColors['new'], 'new', 'new');
       });
 
       cy.wait('@checkVaultCookingTask').then(() => {
         testStatus(this.revision, progressbarColors['pending'], 'Processing...', 'pending');
       });
 
       cy.wait('@checkVaultCookingTask').then(() => {
         testStatus(this.revision, progressbarColors['done'], 'done', 'done');
       });
 
       cy.get(`#vault-task-${this.revision} .vault-origin`)
         .should('have.text', 'unknown');
 
       cy.get(`#vault-task-${this.revision} .vault-object-info a`)
         .should('have.text', this.revision)
         .should('have.attr', 'href', browseRevisionUrl);
 
       cy.get(`#vault-task-${this.revision} .vault-dl-link button`)
         .click();
 
       cy.wait('@fetchCookedArchive').then((xhr) => {
         assert.isNotNull(xhr.response.body);
       });
     });
   });
 
   it('should create a directory cooking task from the release view', function() {
 
     // Browse a directory
     cy.visit(this.releaseUrl);
 
     // Stub responses when checking vault task status
     const checkVaultResponses = [
       {'exception': 'NotFoundExc'},
       this.genVaultDirCookingResponse('new')
     ];
 
     // trick to override the response of an intercepted request
     // https://github.com/cypress-io/cypress/issues/9302
     cy.intercept('GET', this.vaultReleaseDirectoryUrl, req => req.reply(checkVaultResponses.shift()))
       .as('checkVaultCookingTask');
 
     // Stub responses when requesting the vault API to simulate
     // a task has been created
     cy.intercept('POST', this.vaultReleaseDirectoryUrl, {
       body: this.genVaultDirCookingResponse('new')
     }).as('createVaultCookingTask');
 
     cy.contains('button', 'Download')
       .click();
 
     // Create a vault cooking task through the GUI
     cy.get('.modal-dialog')
         .contains('button:visible', 'Ok')
         .click();
 
     cy.wait('@createVaultCookingTask');
 
     // Check success alert is displayed
     cy.get('.alert-success')
         .should('be.visible')
         .should('contain', 'Archive cooking request successfully submitted.');
   });
 
   it('should offer to recook an archive if no more available to download', function() {
 
     updateVaultItemList(this.Urls.browse_vault(), vaultItems);
 
     // Send 404 when fetching vault item
     cy.intercept({url: this.vaultFetchRevisionUrl}, {
       statusCode: 404,
       body: {
         'exception': 'NotFoundExc',
         'reason': `Revision with ID '${this.revision}' not found.`
       },
       headers: {
         'Content-Type': 'json'
       }
     }).as('fetchCookedArchive');
 
     cy.get(`#vault-task-${this.revision} .vault-dl-link button`)
       .click();
 
     cy.wait('@fetchCookedArchive').then(() => {
       cy.intercept('POST', this.vaultRevisionUrl, {
         body: this.genVaultRevCookingResponse('new')
       }).as('createVaultCookingTask');
 
       cy.intercept(this.vaultRevisionUrl, {
         body: this.genVaultRevCookingResponse('new')
       }).as('checkVaultCookingTask');
 
       cy.get('#vault-recook-object-modal > .modal-dialog')
         .should('be.visible')
         .contains('button:visible', 'Ok')
         .click();
 
       cy.wait('@checkVaultCookingTask')
         .then(() => {
           testStatus(this.revision, progressbarColors['new'], 'new', 'new');
         });
     });
   });
 
   it('should remove selected vault items', function() {
 
     updateVaultItemList(this.Urls.browse_vault(), vaultItems);
 
     cy.get(`#vault-task-${this.revision}`)
       .find('input[type="checkbox"]')
       .click({force: true});
     cy.contains('button', 'Remove selected tasks')
       .click();
 
     cy.get(`#vault-task-${this.revision}`)
       .should('not.exist');
   });
 
   it('should offer to immediately download a directory tarball if already cooked', function() {
 
     // Browse a directory
     cy.visit(this.directoryUrl);
 
     // Stub response to the vault API to simulate archive download
     cy.intercept({url: this.vaultFetchDirectoryUrl}, {
       fixture: `${this.directory}.tar.gz`,
       headers: {
         'Content-disposition': `attachment; filename=${this.directory}.tar.gz`,
         'Content-Type': 'application/gzip'
       }
     }).as('fetchCookedArchive');
 
     // Stub responses when requesting the vault API to simulate
     // the directory tarball has already been cooked
     cy.intercept(this.vaultDirectoryUrl, {
       body: this.genVaultDirCookingResponse('done')
     }).as('checkVaultCookingTask');
 
     // Create a vault cooking task through the GUI
     cy.contains('button', 'Download')
       .click();
 
     // Start archive download through the GUI
     cy.get('.modal-dialog')
       .contains('button:visible', 'Ok')
       .click();
 
     cy.wait('@fetchCookedArchive');
 
   });
 
   it('should offer to immediately download a revision gitfast archive if already cooked', function() {
     cy.adminLogin();
     // Browse a directory
     cy.visit(this.revisionUrl);
 
     // Stub response to the vault API to simulate archive download
     cy.intercept({url: this.vaultFetchRevisionUrl}, {
       fixture: `${this.revision}.gitfast.gz`,
       headers: {
         'Content-disposition': `attachment; filename=${this.revision}.gitfast.gz`,
         'Content-Type': 'application/gzip'
       }
     }).as('fetchCookedArchive');
 
     // Stub responses when requesting the vault API to simulate
     // the directory tarball has already been cooked
     cy.intercept(this.vaultRevisionUrl, {
       body: this.genVaultRevCookingResponse('done')
     }).as('checkVaultCookingTask');
 
     checkVaultCookingTask('as git');
 
     // Start archive download through the GUI
     cy.get('.modal-dialog')
       .contains('button:visible', 'Ok')
       .click();
 
     cy.wait('@fetchCookedArchive');
 
   });
 
   it('should offer to recook an object if previous vault task failed', function() {
 
     cy.visit(this.directoryUrl);
 
     // Stub responses when requesting the vault API to simulate
     // the last cooking of the directory tarball has failed
     cy.intercept(this.vaultDirectoryUrl, {
       body: this.genVaultDirCookingResponse('failed')
     }).as('checkVaultCookingTask');
 
     cy.contains('button', 'Download')
       .click();
 
     // Check that recooking the directory is offered to user
     cy.get('.modal-dialog')
       .contains('button:visible', 'Ok')
       .should('be.visible');
   });
 
 });
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 71ce678a..f21d0d0f 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -1,27 +1,27 @@
 /**
  * Copyright (C) 2019  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 const axios = require('axios');
 const fs = require('fs');
 
 module.exports = (on, config) => {
   require('@cypress/code-coverage/task')(on, config);
   // produce JSON files prior launching browser in order to dynamically generate tests
   on('before:browser:launch', function(browser, launchOptions) {
     return new Promise((resolve) => {
-      let p1 = axios.get(`${config.baseUrl}/tests/data/content/code/extensions/`);
-      let p2 = axios.get(`${config.baseUrl}/tests/data/content/code/filenames/`);
+      const p1 = axios.get(`${config.baseUrl}/tests/data/content/code/extensions/`);
+      const p2 = axios.get(`${config.baseUrl}/tests/data/content/code/filenames/`);
       Promise.all([p1, p2])
         .then(function(responses) {
           fs.writeFileSync('cypress/fixtures/source-file-extensions.json', JSON.stringify(responses[0].data));
           fs.writeFileSync('cypress/fixtures/source-file-names.json', JSON.stringify(responses[1].data));
           resolve();
         });
     });
   });
   return config;
 };
diff --git a/cypress/support/index.js b/cypress/support/index.js
index f5974994..58b45488 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -1,158 +1,158 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import 'cypress-hmr-restarter';
 import '@cypress/code-coverage/support';
 
 import {httpGetJson} from '../utils';
 
 Cypress.Screenshot.defaults({
   screenshotOnRunFailure: false
 });
 
 Cypress.Commands.add('xhrShouldBeCalled', (alias, timesCalled) => {
   const testRoutes = cy.state('routes');
   const aliasRoute = Cypress._.find(testRoutes, {alias});
   expect(Object.keys(aliasRoute.requests || {})).to.have.length(timesCalled);
 });
 
 function loginUser(username, password) {
   const url = '/admin/login/';
   return cy.request({
     url: url,
     method: 'GET'
   }).then(() => {
     cy.getCookie('sessionid').should('not.exist');
     cy.getCookie('csrftoken').its('value').then((token) => {
       cy.request({
         url: url,
         method: 'POST',
         form: true,
         followRedirect: false,
         body: {
           username: username,
           password: password,
           csrfmiddlewaretoken: token
         }
       }).then(() => {
         cy.getCookie('sessionid').should('exist');
         return cy.getCookie('csrftoken').its('value');
       });
     });
   });
 }
 
 Cypress.Commands.add('adminLogin', () => {
   return loginUser('admin', 'admin');
 });
 
 Cypress.Commands.add('userLogin', () => {
   return loginUser('user', 'user');
 });
 
 Cypress.Commands.add('ambassadorLogin', () => {
   return loginUser('ambassador', 'ambassador');
 });
 
 before(function() {
   this.unarchivedRepo = {
     url: 'https://github.com/SoftwareHeritage/swh-web',
     type: 'git',
     revision: '7bf1b2f489f16253527807baead7957ca9e8adde',
     snapshot: 'd9829223095de4bb529790de8ba4e4813e38672d',
     rootDirectory: '7d887d96c0047a77e2e8c4ee9bb1528463677663',
     content: [{
       sha1git: 'b203ec39300e5b7e97b6e20986183cbd0b797859'
     }]
   };
 
   this.origin = [{
     url: 'https://github.com/memononen/libtess2',
     type: 'git',
     content: [{
       path: 'Source/tess.h'
     }, {
       path: 'premake4.lua'
     }],
     directory: [{
       path: 'Source',
       id: 'cd19126d815470b28919d64b2a8e6a3e37f900dd'
     }],
     revisions: [],
     invalidSubDir: 'Source1'
   }, {
     url: 'https://github.com/wcoder/highlightjs-line-numbers.js',
     type: 'git',
     content: [{
       path: 'src/highlightjs-line-numbers.js'
     }],
     directory: [],
     revisions: ['1c480a4573d2a003fc2630c21c2b25829de49972'],
     release: {
       name: 'v2.6.0',
       id: '6877028d6e5412780517d0bfa81f07f6c51abb41',
       directory: '5b61d50ef35ca9a4618a3572bde947b8cccf71ad'
     }
   }];
 
   const getMetadataForOrigin = async originUrl => {
     const originVisitsApiUrl = this.Urls.api_1_origin_visits(originUrl);
     const originVisits = await httpGetJson(originVisitsApiUrl);
     const lastVisit = originVisits[0];
     const snapshotApiUrl = this.Urls.api_1_snapshot(lastVisit.snapshot);
     const lastOriginSnapshot = await httpGetJson(snapshotApiUrl);
     let revision = lastOriginSnapshot.branches.HEAD.target;
     if (lastOriginSnapshot.branches.HEAD.target_type === 'alias') {
       revision = lastOriginSnapshot.branches[revision].target;
     }
     const revisionApiUrl = this.Urls.api_1_revision(revision);
     const lastOriginHeadRevision = await httpGetJson(revisionApiUrl);
     return {
       'directory': lastOriginHeadRevision.directory,
       'revision': lastOriginHeadRevision.id,
       'snapshot': lastOriginSnapshot.id
     };
   };
 
   cy.visit('/').window().then(async win => {
     this.Urls = win.Urls;
 
-    for (let origin of this.origin) {
+    for (const origin of this.origin) {
 
       const metadata = await getMetadataForOrigin(origin.url);
       const directoryApiUrl = this.Urls.api_1_directory(metadata.directory);
       origin.dirContent = await httpGetJson(directoryApiUrl);
       origin.rootDirectory = metadata.directory;
       origin.revisions.push(metadata.revision);
       origin.snapshot = metadata.snapshot;
 
-      for (let content of origin.content) {
+      for (const content of origin.content) {
 
         const contentPathApiUrl = this.Urls.api_1_directory(origin.rootDirectory, content.path);
         const contentMetaData = await httpGetJson(contentPathApiUrl);
 
         content.name = contentMetaData.name.split('/').slice(-1)[0];
         content.sha1git = contentMetaData.target;
         content.directory = contentMetaData.dir_id;
 
         content.rawFilePath = this.Urls.browse_content_raw(`sha1_git:${content.sha1git}`) +
                             `?filename=${encodeURIComponent(content.name)}`;
 
         cy.request(content.rawFilePath)
           .then((response) => {
             const fileText = response.body;
             const fileLines = fileText.split('\n');
             content.numberLines = fileLines.length;
 
             // If last line is empty its not shown
             if (!fileLines[content.numberLines - 1]) content.numberLines -= 1;
           });
       }
 
     }
   });
 });