From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 20 Feb 2020 01:17:04 +0000 (+0100)
Subject: Fixes
X-Git-Url: https://git.auder.net/doc/html/css/scripts/css/index.css?a=commitdiff_plain;h=8477e53d8e78606e4c4e4bf91c77b1011aab583c;p=vchess.git

Fixes
---

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 5ebe4997..295b4cd3 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -43,7 +43,7 @@ export const ChessRules = class ChessRules {
   }
 
   // Some variants cannot have analyse mode
-  static get CanAnalyse() {
+  static get CanAnalyze() {
     return true;
   }
 
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index 07d62cf7..c52f94c8 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -112,14 +112,6 @@ export default {
     "game.fenStart": function() {
       this.re_setVariables();
     },
-    // Received a new move to play:
-    "game.moveToPlay": function(move) {
-      if (move) this.play(move, "receive");
-    },
-    // ...Or to undo (corr game, move not validated)
-    "game.moveToUndo": function(move) {
-      if (move) this.undo(move);
-    }
   },
   computed: {
     showMoves: function() {
@@ -225,26 +217,21 @@ export default {
     },
     re_setVariables: function() {
       this.endgameMessage = "";
-      this.orientation = this.game.mycolor || "w"; //default orientation for observed games
+      // "w": default orientation for observed games
+      this.orientation = this.game.mycolor || "w";
       this.moves = JSON.parse(JSON.stringify(this.game.moves || []));
-      // Post-processing: decorate each move with color + current FEN:
-      // (to be able to jump to any position quickly)
-      let vr_tmp = new V(this.game.fenStart); //vr is already at end of game
-      this.firstMoveNumber = Math.floor(
-        V.ParseFen(this.game.fenStart).movesCount / 2
-      );
+      // Post-processing: decorate each move with color, notation and FEN
+      let vr_tmp = new V(this.game.fenStart);
+      const parsedFen = V.ParseFen(this.game.fenStart);
+      const firstMoveColor = parsedFen.turn;
+      this.firstMoveNumber = Math.floor(parsedFen.movesCount / 2);
       this.moves.forEach(move => {
-        // NOTE: this is doing manually what play() function below achieve,
-        // but in a lighter "fast-forward" way
         move.color = vr_tmp.turn;
         move.notation = vr_tmp.getNotation(move);
         vr_tmp.play(move);
         move.fen = vr_tmp.getFen();
       });
-      if (
-        (this.moves.length > 0 && this.moves[0].color == "b") ||
-        (this.moves.length == 0 && vr_tmp.turn == "b")
-      ) {
+      if (firstMoveColor == "b") {
         // 'end' is required for Board component to check lastMove for e.p.
         this.moves.unshift({
           color: "w",
diff --git a/client/src/components/ComputerGame.vue b/client/src/components/ComputerGame.vue
index 0d566a81..b1d2b370 100644
--- a/client/src/components/ComputerGame.vue
+++ b/client/src/components/ComputerGame.vue
@@ -1,5 +1,6 @@
 <template lang="pug">
 BaseGame(
+  ref="basegame"
   :game="game"
   :vr="vr"
   @newmove="processMove"
@@ -41,7 +42,6 @@ export default {
       }
     }
   },
-  // Modal end of game, and then sub-components
   created: function() {
     // Computer moves web worker logic:
     this.compWorker = new Worker();
@@ -63,7 +63,7 @@ export default {
         let moveIdx = 0;
         let self = this;
         (function executeMove() {
-          self.$set(self.game, "moveToPlay", compMove[moveIdx++]);
+          self.$refs["basegame"].play(compMove[moveIdx++]);
           if (moveIdx >= compMove.length) {
             self.compThink = false;
             if (self.game.score != "*")
@@ -95,6 +95,8 @@ export default {
       if (mycolor != "w" || this.gameInfo.mode == "auto")
         this.playComputerMove();
     },
+    // NOTE: a "goto" action could lead to an error when comp is thinking,
+    // but it's OK because from the user viewpoint the game just stops.
     playComputerMove: function() {
       this.timeStart = Date.now();
       this.compThink = true;
diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue
index fba1f79b..f85c49fe 100644
--- a/client/src/components/GameList.vue
+++ b/client/src/components/GameList.vue
@@ -32,7 +32,8 @@ export default {
   data: function() {
     return {
       st: store.state,
-      showCadence: true
+      deleted: {}, //mark deleted games
+      showCadence: window.innerWidth >= 425 //TODO: arbitrary value
     };
   },
   mounted: function() {
@@ -42,7 +43,7 @@ export default {
       if (!timeoutLaunched) {
         timeoutLaunched = true;
         setTimeout(() => {
-          this.showCadence = window.innerWidth >= 425; //TODO: arbitrary
+          this.showCadence = window.innerWidth >= 425;
           timeoutLaunched = false;
         }, 500);
       }
@@ -53,37 +54,39 @@ export default {
       // Show in order: games where it's my turn, my running games, my games, other games
       let minCreated = Number.MAX_SAFE_INTEGER;
       let maxCreated = 0;
-      let augmentedGames = this.games.map(g => {
-        let priority = 0;
-        let myColor = undefined;
-        if (
-          g.players.some(
-            p => p.uid == this.st.user.id || p.sid == this.st.user.sid
-          )
-        ) {
-          priority++;
-          myColor =
-            g.players[0].uid == this.st.user.id ||
-            g.players[0].sid == this.st.user.sid
-              ? "w"
-              : "b";
-          if (g.score == "*") {
+      let augmentedGames = this.games
+        .filter(g => !this.deleted[g.id])
+        .map(g => {
+          let priority = 0;
+          let myColor = undefined;
+          if (
+            g.players.some(
+              p => p.uid == this.st.user.id || p.sid == this.st.user.sid
+            )
+          ) {
             priority++;
-            // I play in this game, so g.fen will be defined
-            // NOTE: this is a fragile way to detect turn,
-            // but since V isn't defined let's do that for now. (TODO:)
-            //if (V.ParseFen(g.fen).turn == myColor)
-            if (g.fen.match(" " + myColor + " ")) priority++;
+            myColor =
+              g.players[0].uid == this.st.user.id ||
+              g.players[0].sid == this.st.user.sid
+                ? "w"
+                : "b";
+            if (g.score == "*") {
+              priority++;
+              // I play in this game, so g.fen will be defined
+              // NOTE: this is a fragile way to detect turn,
+              // but since V isn't defined let's do that for now. (TODO:)
+              //if (V.ParseFen(g.fen).turn == myColor)
+              if (g.fen.match(" " + myColor + " ")) priority++;
+            }
           }
-        }
-        if (g.created < minCreated) minCreated = g.created;
-        if (g.created > maxCreated) maxCreated = g.created;
-        return Object.assign({}, g, {
-          priority: priority,
-          myTurn: priority == 3,
-          myColor: myColor
+          if (g.created < minCreated) minCreated = g.created;
+          if (g.created > maxCreated) maxCreated = g.created;
+          return Object.assign({}, g, {
+            priority: priority,
+            myTurn: priority == 3,
+            myColor: myColor
+          });
         });
-      });
       const deltaCreated = maxCreated - minCreated;
       return augmentedGames.sort((g1, g2) => {
         return (
@@ -129,7 +132,14 @@ export default {
     },
     deleteGame: function(game, e) {
       if (game.score != "*") {
-        if (confirm(this.st.tr["Remove game?"])) GameStorage.remove(game.id);
+        if (confirm(this.st.tr["Remove game?"])) {
+          GameStorage.remove(
+            game.id,
+            () => {
+              this.$set(this.deleted, game.id, true);
+            }
+          );
+        }
         e.stopPropagation();
       }
     }
diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue
index 30ed2338..48f082f4 100644
--- a/client/src/components/MoveList.vue
+++ b/client/src/components/MoveList.vue
@@ -4,58 +4,6 @@ export default {
   name: "my-move-list",
   props: ["moves", "cursor", "score", "message", "firstNum"],
   render(h) {
-    if (this.moves.length == 0) return h("div");
-    let tableContent = [];
-    let moveCounter = 0;
-    let tableRow = undefined;
-    let moveCells = undefined;
-    let curCellContent = "";
-    let firstIndex = 0;
-    for (let i = 0; i < this.moves.length; i++) {
-      if (this.moves[i].color == "w") {
-        if (i == 0 || (i > 0 && this.moves[i - 1].color == "b")) {
-          if (tableRow) {
-            tableRow.children = moveCells;
-            tableContent.push(tableRow);
-          }
-          moveCells = [
-            h("td", { domProps: { innerHTML: ++moveCounter + "." } })
-          ];
-          tableRow = h("tr", {});
-          curCellContent = "";
-          firstIndex = i;
-        }
-      }
-      // Next condition is fine because even if the first move is black,
-      // there will be the "..." which count as white move.
-      else if (this.moves[i].color == "b" && this.moves[i - 1].color == "w")
-        firstIndex = i;
-      curCellContent += this.moves[i].notation;
-      if (
-        i < this.moves.length - 1 &&
-        this.moves[i + 1].color == this.moves[i].color
-      )
-        curCellContent += ",";
-      //color change
-      else {
-        moveCells.push(
-          h("td", {
-            domProps: { innerHTML: curCellContent },
-            on: { click: () => this.gotoMove(i) },
-            class: {
-              "highlight-lm": this.cursor >= firstIndex && this.cursor <= i
-            }
-          })
-        );
-        curCellContent = "";
-      }
-    }
-    // Complete last row, which might not be full:
-    if (moveCells.length - 1 == 1) {
-      moveCells.push(h("td", { domProps: { innerHTML: "" } }));
-    }
-    tableRow.children = moveCells;
-    tableContent.push(tableRow);
     let rootElements = [];
     if (!!this.score && this.score != "*") {
       const scoreDiv = h(
@@ -70,37 +18,90 @@ export default {
       );
       rootElements.push(scoreDiv);
     }
-    rootElements.push(
-      h(
-        "table",
-        {
-          class: {
-            "moves-list": true
+    if (this.moves.length > 0) {
+      let tableContent = [];
+      let moveCounter = 0;
+      let tableRow = undefined;
+      let moveCells = undefined;
+      let curCellContent = "";
+      let firstIndex = 0;
+      for (let i = 0; i < this.moves.length; i++) {
+        if (this.moves[i].color == "w") {
+          if (i == 0 || (i > 0 && this.moves[i - 1].color == "b")) {
+            if (tableRow) {
+              tableRow.children = moveCells;
+              tableContent.push(tableRow);
+            }
+            moveCells = [
+              h(
+                "div",
+                {
+                  "class": {td: true},
+                  domProps: { innerHTML: ++moveCounter + "." }
+                }
+              )
+            ];
+            tableRow = h("div", {"class": {tr: true}});
+            curCellContent = "";
+            firstIndex = i;
           }
-        },
-        tableContent
-      )
-    );
+        }
+        // Next condition is fine because even if the first move is black,
+        // there will be the "..." which count as white move.
+        else if (this.moves[i].color == "b" && this.moves[i - 1].color == "w")
+          firstIndex = i;
+        curCellContent += this.moves[i].notation;
+        if (
+          i < this.moves.length - 1 &&
+          this.moves[i + 1].color == this.moves[i].color
+        )
+          curCellContent += ",";
+        else {
+          // Color change
+          moveCells.push(
+            h(
+              "div",
+              {
+                "class": {
+                  td: true,
+                  "highlight-lm": this.cursor >= firstIndex && this.cursor <= i
+                },
+                domProps: { innerHTML: curCellContent },
+                on: { click: () => this.gotoMove(i) }
+              }
+            )
+          );
+          curCellContent = "";
+        }
+      }
+      // Complete last row, which might not be full:
+      if (moveCells.length - 1 == 1) {
+        moveCells.push(h("div", {"class": {td: true}}));
+      }
+      tableRow.children = moveCells;
+      tableContent.push(tableRow);
+      rootElements.push(
+        h(
+          "div",
+          {
+            class: {
+              "moves-list": true
+            }
+          },
+          tableContent
+        )
+      );
+    }
     return h("div", {}, rootElements);
   },
   watch: {
     cursor: function(newCursor) {
       if (window.innerWidth <= 767) return; //scrolling would hide chessboard
-      // Count grouped moves until the cursor (if multi-moves):
-      let groupsCount = 0;
-      let curCol = undefined;
-      for (let i = 0; i < newCursor; i++) {
-        const m = this.moves[i];
-        if (m.color != curCol) {
-          groupsCount++;
-          curCol = m.color;
-        }
-      }
       // $nextTick to wait for table > tr to be rendered
       this.$nextTick(() => {
-        let rows = document.querySelectorAll("#movesList tr");
-        if (rows.length > 0) {
-          rows[Math.floor(groupsCount / 2)].scrollIntoView({
+        let curMove = document.querySelector(".td.highlight-lm");
+        if (curMove) {
+          curMove.scrollIntoView({
             behavior: "auto",
             block: "nearest"
           });
@@ -118,67 +119,28 @@ export default {
 
 <style lang="sass" scoped>
 .moves-list
-  min-width: 250px
+  cursor: pointer
+  min-height: 1px
+  max-height: 500px
+  overflow: auto
+  background-color: white
+  width: 280px
+  & > .tr
+    clear: both
+    border-bottom: 1px solid lightgrey
+    & > .td
+      float: left
+      padding: 2% 0 2% 1%
+      &:first-child
+        color: grey
+        width: 15%
+      &:not(first-child)
+        width: 41%
+
+@media screen and (max-width: 767px)
+  .moves-list
+    width: 100%
 
-td.highlight-lm
+.td.highlight-lm
   background-color: plum
 </style>
-
-<!-- TODO: use template function + multi-moves: much easier
-<template lang="pug">
-div
-  #scoreInfo(v-if="score!='*'")
-    p {{ score }}
-    p {{ message }}
-  table.moves-list
-    tbody
-      tr(v-for="moveIdx in evenNumbers")
-        td {{ firstNum + moveIdx / 2 + 1 }}
-        td(:class="{'highlight-lm': cursor == moveIdx}"
-            @click="() => gotoMove(moveIdx)")
-          | {{ moves[moveIdx].notation }}
-        td(v-if="moveIdx < moves.length-1"
-            :class="{'highlight-lm': cursor == moveIdx+1}"
-            @click="() => gotoMove(moveIdx+1)")
-          | {{ moves[moveIdx+1].notation }}
-        // Else: just add an empty cell
-        td(v-else)
-</template>
-
-<script>
-// Component for moves list on the right
-export default {
-  name: 'my-move-list',
-	props: ["moves","cursor","score","message","firstNum"],
-  watch: {
-    cursor: function(newValue) {
-      if (window.innerWidth <= 767)
-        return; //moves list is below: scrolling would hide chessboard
-      if (newValue < 0)
-        newValue = 0; //avoid rows[-1] => error
-      // $nextTick to wait for table > tr to be rendered
-      this.$nextTick( () => {
-        let rows = document.querySelectorAll('#movesList tr');
-        if (rows.length > 0)
-        {
-          rows[Math.floor(newValue/2)].scrollIntoView({
-            behavior: "auto",
-            block: "nearest",
-          });
-        }
-      });
-    },
-  },
-  computed: {
-    evenNumbers: function() {
-      return [...Array(this.moves.length).keys()].filter(i => i%2==0);
-    },
-  },
-  methods: {
-		gotoMove: function(index) {
-			this.$emit("goto-move", index);
-		},
-	},
-};
-</script>
--->
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 5afbc59a..0b2d7892 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -10,6 +10,7 @@ export const translations = {
   Apply: "Apply",
   "Back to list": "Back to list",
   "Black to move": "Black to move",
+  "Black surrender": "Black surrender",
   "Black win": "Black win",
   "Board colors": "Board colors",
   "Board size": "Board size",
@@ -24,7 +25,8 @@ export const translations = {
   Contact: "Contact",
   "Correspondance challenges": "Correspondance challenges",
   "Correspondance games": "Correspondance games",
-  "Database error:": "Database error:",
+  "Database error: stop private browsing, or update your browser":
+    "Database error: stop private browsing, or update your browser",
   Delete: "Delete",
   Download: "Download",
   Draw: "Draw",
@@ -33,15 +35,13 @@ export const translations = {
   Email: "Email",
   "Email sent!": "Email sent!",
   "Empty message": "Empty message",
-  "Error while loading database:": "Error while loading database:",
   "Example game": "Example game",
-  "Game retrieval failed:": "Game retrieval failed:",
-  "Game removal failed:": "Game removal failed:",
   Go: "Go",
   green: "green",
   Hall: "Hall",
   "Highlight last move and checks?": "Highlight last move and checks?",
   Instructions: "Instructions",
+  "is not online": "is not online",
   Language: "Language",
   "Live challenges": "Live challenges",
   "Live games": "Live games",
@@ -96,6 +96,7 @@ export const translations = {
   "Show possible moves?": "Show possible moves?",
   "Show solution": "Show solution",
   Solution: "Solution",
+  Stop: "Stop",
   "Stop game": "Stop game",
   Subject: "Subject",
   "Terminate game?": "Terminate game?",
@@ -109,6 +110,7 @@ export const translations = {
   Variants: "Variants",
   Versus: "Versus",
   "White to move": "White to move",
+  "White surrender": "White surrender",
   "White win": "White win",
   "Who's there?": "Who's there?",
   With: "With",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index fd9f28ea..17438e50 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -11,6 +11,7 @@ export const translations = {
   "Back to list": "Volver a la lista",
   Black: "Negras",
   "Black to move": "Juegan las negras",
+  "Black surrender": "Las negras abandonan",
   "Black win": "Las negras gagnan",
   "Board colors": "Colores del tablero",
   "Board size": "Tamaño del tablero",
@@ -25,7 +26,8 @@ export const translations = {
   Contact: "Contacto",
   "Correspondance challenges": "Desafíos por correspondencia",
   "Correspondance games": "Partidas por correspondencia",
-  "Database error:": "Error de la base de datos:",
+  "Database error: stop private browsing, or update your browser":
+    "Error de la base de datos: detener la navegación privada, o actualizar su navegador",
   Delete: "Borrar",
   Download: "Descargar",
   Draw: "Tablas",
@@ -34,15 +36,13 @@ export const translations = {
   Email: "Email",
   "Email sent!": "¡Email enviado!",
   "Empty message": "Mensaje vacio",
-  "Error while loading database:": "Error al cargar la base de datos:",
   "Example game": "Ejemplo de partida",
-  "Game retrieval failed:": "La recuperación de la partida falló:",
-  "Game removal failed:": "La eliminación de la partida falló:",
   Go: "Go",
   green: "verde",
   Hall: "Salón",
   "Highlight last move and checks?": "¿Resaltar el último movimiento y jaques?",
   Instructions: "Instrucciones",
+  "is not online": "no está en línea",
   Language: "Idioma",
   "Live challenges": "Desafíos en vivo",
   "Live games": "Partidas en vivo",
@@ -97,6 +97,7 @@ export const translations = {
   "Show possible moves?": "¿Mostrar posibles movimientos?",
   "Show solution": "Mostrar la solución",
   Solution: "Solución",
+  Stop: "Interrupción",
   "Stop game": "Terminar la partida",
   Subject: "Asunto",
   "Terminate game?": "¿Terminar la partida?",
@@ -111,6 +112,7 @@ export const translations = {
   Versus: "Contra",
   White: "Blancas",
   "White to move": "Juegan las blancas",
+  "White surrender": "Las blancas abandonan",
   "White win": "Las blancas gagnan",
   "Who's there?": "¿Quién está ahí?",
   With: "Con",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 0d776b4e..e02cf5e9 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -11,6 +11,7 @@ export const translations = {
   "Back to list": "Retour à la liste",
   Black: "Noirs",
   "Black to move": "Trait aux noirs",
+  "Black surrender": "Les noirs abandonnent",
   "Black win": "Les noirs gagnent",
   "Board colors": "Couleurs de l'échiquier",
   "Board size": "Taille de l'échiquier",
@@ -25,7 +26,8 @@ export const translations = {
   Contact: "Contact",
   "Correspondance challenges": "Défis par correspondance",
   "Correspondance games": "Parties par correspondance",
-  "Database error:": "Erreur de base de données :",
+  "Database error: stop private browsing, or update your browser":
+    "Erreur de base de données : arrêtez la navigation privée, ou mettez à jour votre navigateur",
   Delete: "Supprimer",
   Download: "Télécharger",
   Draw: "Nulle",
@@ -35,17 +37,14 @@ export const translations = {
   Email: "Email",
   "Email sent!": "Email envoyé !",
   "Empty message": "Message vide",
-  "Error while loading database:":
-    "Erreur lors du chargement de la base de données :",
   "Example game": "Partie exemple",
-  "Game retrieval failed:": "Échec de la récupération de la partie :",
-  "Game removal failed:": "Échec de la suppresion de la partie :",
   Go: "Go",
   green: "vert",
   Hall: "Salon",
   "Highlight last move and checks?":
     "Mettre en valeur le dernier coup et les échecs ?",
   Instructions: "Instructions",
+  "is not online": "n'est pas en ligne",
   Language: "Langue",
   "Live challenges": "Défis en direct",
   "Live games": "Parties en direct",
@@ -100,6 +99,7 @@ export const translations = {
   "Show possible moves?": "Montrer les coups possibles ?",
   "Show solution": "Montrer la solution",
   Solution: "Solution",
+  Stop: "Arrêt",
   "Stop game": "Arrêter la partie",
   Subject: "Sujet",
   "Terminate game?": "Stopper la partie ?",
@@ -114,6 +114,7 @@ export const translations = {
   Versus: "Contre",
   White: "Blancs",
   "White to move": "Trait aux blancs",
+  "White surrender": "Les blancs abandonnent",
   "White win": "Les blancs gagnent",
   "Who's there?": "Qui est là ?",
   With: "Avec",
diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js
index 6e9fc818..cc7dca5b 100644
--- a/client/src/utils/gameStorage.js
+++ b/client/src/utils/gameStorage.js
@@ -24,25 +24,18 @@ function dbOperation(callback) {
   let DBOpenRequest = window.indexedDB.open("vchess", 4);
 
   DBOpenRequest.onerror = function(event) {
-    alert(store.state.tr["Database error:"] + " " + event.target.errorCode);
+    alert(store.state.tr["Database error: stop private browsing, or update your browser"]);
+    callback("error",null);
   };
 
   DBOpenRequest.onsuccess = function() {
     db = DBOpenRequest.result;
-    callback(db);
+    callback(null,db);
     db.close();
   };
 
   DBOpenRequest.onupgradeneeded = function(event) {
     let db = event.target.result;
-    db.onerror = function(event) {
-      alert(
-        store.state.tr["Error while loading database:"] +
-          " " +
-          event.target.errorCode
-      );
-    };
-    // Create objectStore for vchess->games
     let objectStore = db.createObjectStore("games", { keyPath: "id" });
     objectStore.createIndex("score", "score"); //to search by game result
   };
@@ -51,19 +44,15 @@ function dbOperation(callback) {
 export const GameStorage = {
   // Optional callback to get error status
   add: function(game, callback) {
-    dbOperation(db => {
-      let transaction = db.transaction("games", "readwrite");
-      if (callback) {
-        transaction.oncomplete = function() {
-          callback({}); //everything's fine
-        };
-        transaction.onerror = function() {
-          callback({
-            errmsg:
-              store.state.tr["Game retrieval failed:"] + " " + transaction.error
-          });
-        };
+    dbOperation((err,db) => {
+      if (err) {
+        callback("error");
+        return;
       }
+      let transaction = db.transaction("games", "readwrite");
+      transaction.oncomplete = function() {
+        callback(); //everything's fine
+      };
       let objectStore = transaction.objectStore("games");
       objectStore.add(game);
     });
@@ -88,17 +77,20 @@ export const GameStorage = {
       });
     } else {
       // live
-      dbOperation(db => {
+      dbOperation((err,db) => {
         let objectStore = db
           .transaction("games", "readwrite")
           .objectStore("games");
         objectStore.get(gameId).onsuccess = function(event) {
-          const game = event.target.result;
-          Object.keys(obj).forEach(k => {
-            if (k == "move") game.moves.push(obj[k]);
-            else game[k] = obj[k];
-          });
-          objectStore.put(game); //save updated data
+          // Ignoring error silently: shouldn't happen now. TODO?
+          if (event.target.result) {
+            const game = event.target.result;
+            Object.keys(obj).forEach(k => {
+              if (k == "move") game.moves.push(obj[k]);
+              else game[k] = obj[k];
+            });
+            objectStore.put(game); //save updated data
+          }
         };
       });
     }
@@ -106,7 +98,7 @@ export const GameStorage = {
 
   // Retrieve all local games (running, completed, imported...)
   getAll: function(callback) {
-    dbOperation(db => {
+    dbOperation((err,db) => {
       let objectStore = db.transaction("games").objectStore("games");
       let games = [];
       objectStore.openCursor().onsuccess = function(event) {
@@ -132,42 +124,29 @@ export const GameStorage = {
         });
         callback(game);
       });
-    } //local game
+    }
     else {
-      dbOperation(db => {
+      // Local game
+      dbOperation((err,db) => {
         let objectStore = db.transaction("games").objectStore("games");
         objectStore.get(gameId).onsuccess = function(event) {
-          callback(event.target.result);
+          if (event.target.result)
+            callback(event.target.result);
         };
       });
     }
   },
 
-  getCurrent: function(callback) {
-    dbOperation(db => {
-      let objectStore = db.transaction("games").objectStore("games");
-      objectStore.get("*").onsuccess = function(event) {
-        callback(event.target.result);
-      };
-    });
-  },
-
   // Delete a game in indexedDB
   remove: function(gameId, callback) {
-    dbOperation(db => {
-      let transaction = db.transaction(["games"], "readwrite");
-      if (callback) {
+    dbOperation((err,db) => {
+      if (!err) {
+        let transaction = db.transaction(["games"], "readwrite");
         transaction.oncomplete = function() {
           callback({}); //everything's fine
         };
-        transaction.onerror = function() {
-          callback({
-            errmsg:
-              store.state.tr["Game removal failed:"] + " " + transaction.error
-          });
-        };
+        transaction.objectStore("games").delete(gameId);
       }
-      transaction.objectStore("games").delete(gameId);
     });
   }
 };
diff --git a/client/src/variants/Dark.js b/client/src/variants/Dark.js
index cdacf9d2..d6515efd 100644
--- a/client/src/variants/Dark.js
+++ b/client/src/variants/Dark.js
@@ -4,7 +4,7 @@ import { randInt } from "@/utils/alea";
 
 export const VariantRules = class DarkRules extends ChessRules {
   // Analyse in Dark mode makes no sense
-  static get CanAnalyse() {
+  static get CanAnalyze() {
     return false;
   }
 
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 9a971006..2108493c 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -56,6 +56,7 @@ main
             | {{ game.players[1].name || "@nonymous" }}
           span.time(v-if="game.score=='*'") {{ virtualClocks[1] }}
   BaseGame(
+    ref="basegame"
     :game="game"
     :vr="vr"
     @newmove="processMove"
@@ -138,10 +139,10 @@ export default {
     // Socket init required before loading remote game:
     const socketInit = callback => {
       if (!!this.conn && this.conn.readyState == 1)
-        //1 == OPEN state
+        // 1 == OPEN state
         callback();
-      //socket not ready yet (initial loading)
       else {
+        // Socket not ready yet (initial loading)
         // NOTE: it's important to call callback without arguments,
         // otherwise first arg is Websocket object and loadGame fails.
         this.conn.onopen = () => {
@@ -150,10 +151,10 @@ export default {
       }
     };
     if (!this.gameRef.rid)
-      //game stored locally or on server
+      // Game stored locally or on server
       this.loadGame(null, () => socketInit(this.roomInit));
-    //game stored remotely: need socket to retrieve it
     else {
+      // Game stored remotely: need socket to retrieve it
       // NOTE: the callback "roomInit" will be lost, so we don't provide it.
       // --> It will be given when receiving "fullgame" socket event.
       // A more general approach would be to store it somewhere.
@@ -333,14 +334,16 @@ export default {
             if (this.game.type == "live" && !!this.game.mycolor)
               GameStorage.update(this.gameRef.id, { drawOffer: "" });
           }
-          this.$set(this.game, "moveToPlay", move);
+          this.$refs["basegame"].play(move, "received");
           break;
         }
         case "resign":
-          this.gameOver(data.side == "b" ? "1-0" : "0-1", "Resign");
+          const score = data.side == "b" ? "1-0" : "0-1";
+          const side = data.side == "w" ? "White" : "Black";
+          this.gameOver(score, side + " surrender");
           break;
         case "abort":
-          this.gameOver("?", "Abort");
+          this.gameOver("?", "Stop");
           break;
         case "draw":
           this.gameOver("1/2", data.data);
@@ -368,9 +371,7 @@ export default {
       const L = this.game.moves.length;
       if (data.movesCount > L) {
         // Just got last move from him
-        this.$set(
-          this.game,
-          "moveToPlay",
+        this.$refs["basegame"].play(
           Object.assign({ initime: data.initime }, data.lastMove)
         );
       }
@@ -404,14 +405,16 @@ export default {
     },
     abortGame: function() {
       if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) return;
-      this.gameOver("?", "Abort");
+      this.gameOver("?", "Stop");
       this.send("abort");
     },
     resign: function() {
       if (!this.game.mycolor || !confirm(this.st.tr["Resign the game?"]))
         return;
       this.send("resign", { data: this.game.mycolor });
-      this.gameOver(this.game.mycolor == "w" ? "0-1" : "1-0", "Resign");
+      const score = this.game.mycolor == "w" ? "0-1" : "1-0";
+      const side = this.game.mycolor == "w" ? "White" : "Black";
+      this.gameOver(score, side + " surrender");
     },
     // 3 cases for loading a game:
     //  - from indexedDB (running or completed live game I play)
@@ -437,7 +440,7 @@ export default {
               game.players[0]
             ];
           }
-          // corr game: needs to compute the clocks + initime
+          // corr game: need to compute the clocks + initime
           // NOTE: clocks in seconds, initime in milliseconds
           game.clocks = [tc.mainTime, tc.mainTime];
           game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
@@ -456,31 +459,17 @@ export default {
             }
             if (L >= 1) game.initime[L % 2] = game.moves[L - 1].played;
           }
-          const reformattedMoves = game.moves.map(m => {
-            const s = m.squares;
-            return {
-              appear: s.appear,
-              vanish: s.vanish,
-              start: s.start,
-              end: s.end
-            };
-          });
           // Sort chat messages from newest to oldest
           game.chats.sort((c1, c2) => {
             return c2.added - c1.added;
           });
           if (myIdx >= 0 && game.chats.length > 0) {
-            // TODO: group multi-moves into an array, to deduce color from index
-            // and not need this (also repeated in BaseGame::re_setVariables())
-            let vr_tmp = new V(game.fenStart); //vr is already at end of game
-            for (let i = 0; i < reformattedMoves.length; i++) {
-              game.moves[i].color = vr_tmp.turn;
-              vr_tmp.play(reformattedMoves[i]);
-            }
-            // Blue background on chat button if last chat message arrived after my last move.
+            // Did a chat message arrive after my last move?
+            let vr_tmp = new V(game.fen); //start from last position
             let dtLastMove = 0;
             for (let midx = game.moves.length - 1; midx >= 0; midx--) {
-              if (game.moves[midx].color == mycolor) {
+              vr_tmp.undo(game.moves[midx]);
+              if (vr_tmp.turn == mycolor) {
                 dtLastMove = game.moves[midx].played;
                 break;
               }
@@ -489,10 +478,10 @@ export default {
               document.getElementById("chatBtn").classList.add("somethingnew");
           }
           // Now that we used idx and played, re-format moves as for live games
-          game.moves = reformattedMoves;
+          game.moves = game.moves.map(m => m.squares);
         }
         if (gtype == "live" && game.clocks[0] < 0) {
-          //game unstarted
+          // Game is unstarted
           game.clocks = [tc.mainTime, tc.mainTime];
           if (game.score == "*") {
             game.initime[0] = Date.now();
@@ -507,11 +496,11 @@ export default {
         }
         if (game.drawOffer) {
           if (game.drawOffer == "t")
-            //three repetitions
+            // Three repetitions
             this.drawOffer = "threerep";
           else {
+            // Draw offered by any of the players:
             if (myIdx < 0) this.drawOffer = "received";
-            //by any of the players
             else {
               // I play in this game:
               if (
@@ -519,13 +508,10 @@ export default {
                 (game.drawOffer == "b" && myIdx == 1)
               )
                 this.drawOffer = "sent";
-              //all other cases
               else this.drawOffer = "received";
             }
           }
         }
-        if (game.scoreMsg) game.scoreMsg = this.st.tr[game.scoreMsg]; //stored in english
-        delete game["moveToPlay"]; //in case of!
         this.game = Object.assign(
           {},
           game,
@@ -571,6 +557,7 @@ export default {
         this.send("askfullgame", { target: this.gameRef.rid });
       } else {
         // Local or corr game
+        // NOTE: afterRetrieval() is never called if game not found
         GameStorage.get(this.gameRef.id, afterRetrieval);
       }
     },
@@ -581,6 +568,7 @@ export default {
         return;
       }
       const currentTurn = this.vr.turn;
+      const currentMovesCount = this.game.moves.length;
       const colorIdx = ["w", "b"].indexOf(currentTurn);
       let countdown =
         this.game.clocks[colorIdx] -
@@ -593,13 +581,13 @@ export default {
       let clockUpdate = setInterval(() => {
         if (
           countdown < 0 ||
-          this.vr.turn != currentTurn ||
+          this.game.moves.length > currentMovesCount ||
           this.game.score != "*"
         ) {
           clearInterval(clockUpdate);
           if (countdown < 0)
             this.gameOver(
-              this.vr.turn == "w" ? "0-1" : "1-0",
+              currentTurn == "w" ? "0-1" : "1-0",
               this.st.tr["Time"]
             );
         } else
@@ -610,7 +598,7 @@ export default {
           );
       }, 1000);
     },
-    // Post-process a move (which was just played in BaseGame)
+    // Post-process a (potentially partial) move (which was just played in BaseGame)
     processMove: function(move) {
       if (this.game.type == "corr" && move.color == this.game.mycolor) {
         if (
@@ -622,7 +610,7 @@ export default {
               this.st.tr["Are you sure?"]
           )
         ) {
-          this.$set(this.game, "moveToUndo", move);
+          this.$refs["basegame"].undo(move);
           return;
         }
       }
@@ -631,7 +619,7 @@ export default {
       // https://stackoverflow.com/a/38750895
       if (this.game.mycolor) {
         const allowed_fields = ["appear", "vanish", "start", "end"];
-        // NOTE: 'var' to see this variable outside this block
+        // NOTE: 'var' to see that variable outside this block
         var filtered_move = Object.keys(move)
           .filter(key => allowed_fields.includes(key))
           .reduce((obj, key) => {
@@ -665,7 +653,8 @@ export default {
       this.game.clocks[colorIdx] += addTime;
       // move.initime is set only when I receive a "lastate" move from opponent
       this.game.initime[nextIdx] = move.initime || Date.now();
-      this.re_setClocks();
+      //if (colorIdx != nextIdx)
+        this.re_setClocks();
       // If repetition detected, consider that a draw offer was received:
       const fenObj = V.ParseFen(move.fen);
       let repIdx = fenObj.position + "_" + fenObj.turn;
@@ -676,7 +665,7 @@ export default {
       // Since corr games are stored at only one location, update should be
       // done only by one player for each move:
       if (
-        !!this.game.mycolor &&
+        this.game.mycolor &&
         (this.game.type == "live" || move.color == this.game.mycolor)
       ) {
         let drawCode = "";
@@ -699,10 +688,12 @@ export default {
               played: Date.now(),
               idx: this.game.moves.length - 1
             },
-            drawOffer: drawCode || "n" //"n" for "None" to force reset (otherwise it's ignored)
+            // Code "n" for "None" to force reset (otherwise it's ignored)
+            drawOffer: drawCode || "n"
           });
-        } //live
+        }
         else {
+          // Live game:
           GameStorage.update(this.gameRef.id, {
             fen: move.fen,
             move: filtered_move,
@@ -725,14 +716,12 @@ export default {
     },
     gameOver: function(score, scoreMsg) {
       this.game.score = score;
-      this.game.scoreMsg = this.st.tr[
-        scoreMsg ? scoreMsg : getScoreMessage(score)
-      ];
+      this.$set(this.game, "scoreMsg", scoreMsg || getScoreMessage(score));
       const myIdx = this.game.players.findIndex(p => {
         return p.sid == this.st.user.sid || p.uid == this.st.user.id;
       });
       if (myIdx >= 0) {
-        //OK, I play in this game
+        // OK, I play in this game
         GameStorage.update(this.gameRef.id, {
           score: score,
           scoreMsg: scoreMsg
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index 68d3b48b..6cc58e97 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -28,9 +28,9 @@ main
         fieldset
           label(for="cadence") {{ st.tr["Cadence"] }} *
           div#predefinedCadences
-            button 3+2
-            button 5+3
-            button 15+5
+            button(type="button") 3+2
+            button(type="button") 5+3
+            button(type="button") 15+5
           input#cadence(
             type="text"
             v-model="newchallenge.cadence"
@@ -627,11 +627,24 @@ export default {
     },
     // Challenge lifecycle:
     newChallenge: async function() {
+      if (this.newchallenge.cadence.match(/^[0-9]+$/))
+        this.newchallenge.cadence += "+0"; //assume minutes, no increment
+      const ctype = this.classifyObject(this.newchallenge);
+      // TODO: cadence still unchecked so ctype could be wrong...
       let error = "";
-      if (this.newchallenge.vid == "")
+      if (!this.newchallenge.vid)
         error = this.st.tr["Please select a variant"];
-      else if (!!this.newchallenge.to && this.newchallenge.to == this.st.user.name)
-        error = this.st.tr["Self-challenge is forbidden"];
+      else if (ctype == "corr" && this.st.user.id <= 0)
+        error = this.st.tr["Please log in to play correspondance games"];
+      else if (this.newchallenge.to) {
+        if (this.newchallenge.to == this.st.user.name)
+          error = this.st.tr["Self-challenge is forbidden"];
+        else if (
+          ctype == "live" &&
+          Object.values(this.people).every(p => p.name != this.newchallenge.to)
+        )
+          error = this.newchallenge.to + " " + this.st.tr["is not online"];
+      }
       if (error) {
         alert(error);
         return;
@@ -639,12 +652,7 @@ export default {
       const vname = this.getVname(this.newchallenge.vid);
       const vModule = await import("@/variants/" + vname + ".js");
       window.V = vModule.VariantRules;
-      if (this.newchallenge.cadence.match(/^[0-9]+$/))
-        this.newchallenge.cadence += "+0"; //assume minutes, no increment
-      const ctype = this.classifyObject(this.newchallenge);
       error = checkChallenge(this.newchallenge);
-      if (!error && ctype == "corr" && this.st.user.id <= 0)
-        error = this.st.tr["Please log in to play correspondance games"];
       if (error) {
         alert(error);
         return;
@@ -789,10 +797,14 @@ export default {
         initime: [0, 0], //initialized later
         score: "*"
       });
-      GameStorage.add(game);
-      if (this.st.settings.sound >= 1)
-        new Audio("/sounds/newgame.mp3").play().catch(() => {});
-      this.$router.push("/game/" + gameInfo.id);
+      GameStorage.add(game, (err) => {
+        // If an error occurred, game is not added: abort
+        if (!err) {
+          if (this.st.settings.sound >= 1)
+            new Audio("/sounds/newgame.mp3").play().catch(() => {});
+          this.$router.push("/game/" + gameInfo.id);
+        }
+      });
     }
   }
 };
diff --git a/server/routes/challenges.js b/server/routes/challenges.js
index 5d0068df..4bbce8e2 100644
--- a/server/routes/challenges.js
+++ b/server/routes/challenges.js
@@ -35,7 +35,7 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => {
   {
     UserModel.getOne("name", challenge.to, (err,user) => {
       if (!!err || !user)
-        return res.json(err | {errmsg: "Typo in player name"});
+        return res.json(err || {errmsg: "Typo in player name"});
       challenge.to = user.id; //ready now to insert challenge
       insertChallenge();
       if (user.notify)
diff --git a/server/sockets.js b/server/sockets.js
index 1cc47aeb..42a8840f 100644
--- a/server/sockets.js
+++ b/server/sockets.js
@@ -153,7 +153,7 @@ module.exports = function(wss) {
         {
           const pg = obj.page || page; //required for askidentity and askgame
           // In cas askfullgame to wrong SID for example, would crash:
-          if (!!clients[pg][obj.target])
+          if (clients[pg] && clients[pg][obj.target])
           {
             const tmpIds = Object.keys(clients[pg][obj.target]);
             if (obj.target == sid) //targetting myself
@@ -207,7 +207,10 @@ module.exports = function(wss) {
         case "lastate":
         {
           const pg = obj.target[2] || page; //required for identity and game
-          send(clients[pg][obj.target[0]][obj.target[1]], {code:obj.code, data:obj.data});
+          // NOTE: if in game we ask identity to opponent still in Hall,
+          // but leaving Hall, clients[pg] or clients[pg][target] could be ndefined
+          if (clients[pg] && clients[pg][obj.target[0]])
+            send(clients[pg][obj.target[0]][obj.target[1]], {code:obj.code, data:obj.data});
           break;
         }
       }