From 0234201fb338fc239d6f613c677fa932c7c3697c Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 16 Mar 2020 03:40:33 +0100
Subject: [PATCH] Refactor models (merge Players in Games), add cursor to
 correspondance games. Finished but not working yet

---
 client/src/components/GameList.vue |   7 +-
 client/src/views/Analyse.vue       |   2 +-
 client/src/views/Game.vue          |  19 +-
 client/src/views/Hall.vue          |  89 +++++--
 client/src/views/MyGames.vue       |  88 +++++--
 client/src/views/News.vue          |  13 +-
 server/app.js                      |  11 +-
 server/config/parameters.js.dist   |   3 +-
 server/db/create.sql               |  17 +-
 server/models/Challenge.js         |  24 +-
 server/models/Game.js              | 370 +++++++++++++++++------------
 server/models/News.js              |   5 +-
 server/models/Problem.js           |  23 +-
 server/models/User.js              |  57 ++---
 server/models/Variant.js           |   6 +-
 server/routes/challenges.js        |  28 +--
 server/routes/games.js             |  95 ++++----
 server/routes/news.js              |  11 +-
 server/routes/problems.js          |  22 +-
 server/routes/users.js             |  36 +--
 server/sockets.js                  |   2 +-
 server/utils/access.js             |  37 ++-
 server/utils/database.js           |   3 +-
 server/utils/mailer.js             |  13 +-
 server/utils/tokenGenerator.js     |  11 +-
 25 files changed, 550 insertions(+), 442 deletions(-)

diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue
index 1a50e956..549e2a1d 100644
--- a/client/src/components/GameList.vue
+++ b/client/src/components/GameList.vue
@@ -62,7 +62,7 @@ export default {
         );
       if (
         this.st.user.sid == g.players[0].sid ||
-        this.st.user.id == g.players[0].uid
+        this.st.user.id == g.players[0].id
       )
         return g.players[1].name || "@nonymous";
       return g.players[0].name || "@nonymous";
@@ -113,8 +113,7 @@ export default {
       if (
         // My game ?
         game.players.some(p =>
-          p.sid == this.st.user.sid ||
-          p.uid == this.st.user.id
+          p.sid == this.st.user.sid || p.id == this.st.user.id
         )
       ) {
         const message =
@@ -131,7 +130,7 @@ export default {
             GameStorage.remove(game.id, afterDelete);
           else {
             const mySide =
-              game.players[0].uid == this.st.user.id
+              game.players[0].id == this.st.user.id
                 ? "White"
                 : "Black";
             game["deletedBy" + mySide] = true;
diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue
index f662d614..becc4de0 100644
--- a/client/src/views/Analyse.vue
+++ b/client/src/views/Analyse.vue
@@ -80,10 +80,10 @@ export default {
       .catch((err) => { this.alertAndQuit("Mispelled variant name", true); });
     },
     loadGame: function(orientation) {
-      // NOTE: no need to set score (~unused)
       this.game.vname = this.gameRef.vname;
       this.game.fenStart = this.gameRef.fen;
       this.game.fen = this.gameRef.fen;
+      this.game.score = "*"; //never change
       this.curFen = this.game.fen;
       this.adjustFenSize();
       this.game.mycolor = orientation || V.ParseFen(this.gameRef.fen).turn;
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index e7db9729..a401e464 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -352,7 +352,7 @@ export default {
     isConnected: function(index) {
       const player = this.game.players[index];
       // Is it me ? In this case no need to bother with focus
-      if (this.st.user.sid == player.sid || this.st.user.id == player.uid)
+      if (this.st.user.sid == player.sid || this.st.user.id == player.id)
         // Still have to check for name (because of potential multi-accounts
         // on same browser, although this should be rare...)
         return (!this.st.user.name || this.st.user.name == player.name);
@@ -365,9 +365,9 @@ export default {
         )
         ||
         (
-          player.uid &&
+          player.id &&
           Object.values(this.people).some(p =>
-            p.id == player.uid && p.focus)
+            p.id == player.id && p.focus)
         )
       );
     },
@@ -415,7 +415,7 @@ export default {
         {
           data: data,
           targets: this.game.players.map(p => {
-            return { sid: p.sid, uid: p.uid };
+            return { sid: p.sid, id: p.id };
           })
         }
       );
@@ -693,7 +693,7 @@ export default {
             this.addAndGotoLiveGame(gameInfo);
           } else if (
             gameType == "corr" &&
-            gameInfo.players.some(p => p.uid == this.st.user.id)
+            gameInfo.players.some(p => p.id == this.st.user.id)
           ) {
             this.$router.push("/game/" + gameInfo.id);
           } else {
@@ -918,7 +918,7 @@ export default {
         const gtype = this.getGameType(game);
         const tc = extractTime(game.cadence);
         const myIdx = game.players.findIndex(p => {
-          return p.sid == this.st.user.sid || p.uid == this.st.user.id;
+          return p.sid == this.st.user.sid || p.id == this.st.user.id;
         });
         const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers
         if (!game.chats) game.chats = []; //live games don't have chat history
@@ -1031,7 +1031,7 @@ export default {
             // opponent sid not strictly required (or available), but easier
             // at least oppsid or oppid is available anyway:
             oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid,
-            oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].uid
+            oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].id
           },
           game,
         );
@@ -1085,6 +1085,9 @@ export default {
                 g.moves.forEach(m => {
                   m.squares = JSON.parse(m.squares);
                 });
+                g.players = [{ id: g.white }, { id: g.black }];
+                delete g["white"];
+                delete g["black"];
                 afterRetrieval(g);
               }
             }
@@ -1366,7 +1369,7 @@ export default {
       this.game.scoreMsg = scoreMsg;
       this.$set(this.game, "scoreMsg", scoreMsg);
       const myIdx = this.game.players.findIndex(p => {
-        return p.sid == this.st.user.sid || p.uid == this.st.user.id;
+        return p.sid == this.st.user.sid || p.id == this.st.user.id;
       });
       if (myIdx >= 0) {
         // OK, I play in this game
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index f9519fe2..b9262d12 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -185,12 +185,17 @@ main
           :showBoth="true"
           @show-game="showGame"
         )
-        GameList(
-          v-show="gdisplay=='corr'"
-          :games="filterGames('corr')"
-          :showBoth="true"
-          @show-game="showGame"
-        )
+        div(v-show="gdisplay=='corr'")
+          GameList(
+            :games="filterGames('corr')"
+            :showBoth="true"
+            @show-game="showGame"
+          )
+          button#loadMoreBtn(
+            v-if="hasMore"
+            @click="loadMore()"
+          )
+            | {{ st.tr["Load more"] }}
 </template>
 
 <script>
@@ -218,6 +223,10 @@ export default {
       st: store.state,
       cdisplay: "live", //or corr
       gdisplay: "live",
+      // timestamp of last showed (oldest) corr game:
+      cursor: Number.MAX_SAFE_INTEGER,
+      // hasMore == TRUE: a priori there could be more games to load
+      hasMore: true,
       games: [],
       challenges: [],
       people: {},
@@ -321,10 +330,13 @@ export default {
     this.setDisplay('g', showGtype);
     // Ask server for current corr games (all but mines)
     ajax(
-      "/games",
+      "/observedgames",
       "GET",
       {
-        data: { uid: this.st.user.id, excluded: true },
+        data: {
+          uid: this.st.user.id,
+          cursor: this.cursor
+        },
         success: (response) => {
           if (
             response.games.length > 0 &&
@@ -337,13 +349,12 @@ export default {
           }
           this.games = this.games.concat(
             response.games.map(g => {
-              const type = this.classifyObject(g);
               const vname = this.getVname(g.vid);
               return Object.assign(
                 {},
                 g,
                 {
-                  type: type,
+                  type: "corr",
                   vname: vname
                 }
               );
@@ -766,7 +777,7 @@ export default {
           const game = data.data;
           // Ignore games where I play (will go in MyGames page)
           if (game.players.every(p =>
-            p.sid != this.st.user.sid && p.uid != this.st.user.id))
+            p.sid != this.st.user.sid && p.id != this.st.user.id))
           {
             let locGame = this.games.find(g => g.id == game.id);
             if (!locGame) {
@@ -830,6 +841,36 @@ export default {
       this.conn.addEventListener("message", this.socketMessageListener);
       this.conn.addEventListener("close", this.socketCloseListener);
     },
+    loadMore: function() {
+      ajax(
+        "/observedgames",
+        "GET",
+        {
+          data: {
+            uid: this.st.user.id,
+            cursor: this.cursor
+          },
+          success: (res) => {
+            if (res.games.length > 0) {
+              const L = res.games.length;
+              this.cursor = res.games[L - 1].created;
+              let moreGames = res.games.map(g => {
+                const vname = this.getVname(g.vid);
+                return Object.assign(
+                  {},
+                  g,
+                  {
+                    type: "corr",
+                    vname: vname
+                  }
+                );
+              });
+              this.games = this.games.concat(moreGames);
+            } else this.hasMore = false;
+          }
+        }
+      );
+    },
     // Challenge lifecycle:
     addChallenge: function(chall) {
       // NOTE about next condition: see "askchallenges" case.
@@ -1098,16 +1139,11 @@ export default {
         !!c.mycolor
           ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat])
           : shuffle([c.from, c.seat]);
-      // Convention for players IDs in stored games is 'uid'
-      players.forEach(p => {
-        let pWithUid = p;
-        pWithUid["uid"] = p.id;
-        delete pWithUid["id"];
-      });
       // These game informations will be shared
       let gameInfo = {
         id: getRandString(),
         fen: c.fen || V.GenRandInitFen(c.randomness),
+        randomness: c.randomness, //for rematch
         // White player index 0, black player index 1:
         players: c.mycolor
           ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat])
@@ -1126,7 +1162,7 @@ export default {
           {
             data: gameInfo,
             targets: gameInfo.players.map(p => {
-              return { sid: p.sid, uid: p.uid };
+              return { sid: p.sid, id: p.id };
             })
           }
         );
@@ -1174,13 +1210,12 @@ export default {
         () => {
           GameStorage.add(game, (err) => {
             // If an error occurred, game is not added: a tab already
-            // added the game and (if focused) is redirected toward it.
-            // If no error and the tab is hidden: do not show anything.
-            if (!err && !document.hidden) {
-              if (this.st.settings.sound)
-                new Audio("/sounds/newgame.flac").play().catch(() => {});
-              this.$router.push("/game/" + gameInfo.id);
-            }
+            // added the game. Maybe a focused one, maybe not.
+            // We know for sure that it emitted the gong start sound.
+            // ==> Do not play it again.
+            if (!err && this.st.settings.sound)
+              new Audio("/sounds/newgame.flac").play().catch(() => {});
+            this.$router.push("/game/" + gameInfo.id);
           });
         },
         document.hidden ? 500 + 1000 * Math.random() : 0
@@ -1292,6 +1327,10 @@ tr > td
   h4
     margin: 5px 0
 
+button#loadMoreBtn
+  margin-top: 0
+  margin-bottom: 0
+
 td.remove-preset
   background-color: lightgrey
   text-align: center
diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue
index 1362cb24..628d72eb 100644
--- a/client/src/views/MyGames.vue
+++ b/client/src/views/MyGames.vue
@@ -14,13 +14,18 @@ main
         @show-game="showGame"
         @abortgame="abortGame"
       )
-      GameList(
-        ref="corrgames"
-        v-show="display=='corr'"
-        :games="corrGames"
-        @show-game="showGame"
-        @abortgame="abortGame"
-      )
+      div(v-show="display=='corr'")
+        GameList(
+          ref="corrgames"
+          :games="corrGames"
+          @show-game="showGame"
+          @abortgame="abortGame"
+        )
+        button#loadMoreBtn(
+          v-if="hasMore"
+          @click="loadMore()"
+        )
+          | {{ st.tr["Load more"] }}
 </template>
 
 <script>
@@ -42,6 +47,10 @@ export default {
       display: "live",
       liveGames: [],
       corrGames: [],
+      // timestamp of last showed (oldest) corr game:
+      cursor: Number.MAX_SAFE_INTEGER,
+      // hasMore == TRUE: a priori there could be more games to load
+      hasMore: true,
       conn: null,
       connexionString: ""
     };
@@ -89,23 +98,35 @@ export default {
       this.decorate(localGames);
       this.liveGames = localGames;
       if (this.st.user.id > 0) {
+        // Ask running corr games first
         ajax(
-          "/games",
+          "/runninggames",
           "GET",
           {
-            data: { uid: this.st.user.id },
             success: (res) => {
-              let serverGames = res.games.filter(g => {
-                const mySide =
-                  g.players[0].uid == this.st.user.id
-                    ? "White"
-                    : "Black";
-                return !g["deletedBy" + mySide];
-              });
-              serverGames.forEach(g => g.type = "corr");
-              this.decorate(serverGames);
-              this.corrGames = serverGames;
-              adjustAndSetDisplay();
+              // These games are garanteed to not be deleted
+              this.corrGames = res.games;
+              this.corrGames.forEach(g => g.type = "corr");
+              this.decorate(this.corrGames);
+              // Now ask completed games (partial list)
+              ajax(
+                "/completedgames",
+                "GET",
+                {
+                  data: { cursor: this.cursor },
+                  success: (res2) => {
+                    if (res2.games.length > 0) {
+                      const L = res2.games.length;
+                      this.cursor = res2.games[L - 1].created;
+                      let completedGames = res2.games;
+                      completedGames.forEach(g => g.type = "corr");
+                      this.decorate(completedGames);
+                      this.corrGames = this.corrGames.concat(completedGames);
+                      adjustAndSetDisplay();
+                    }
+                  }
+                }
+              );
             }
           }
         );
@@ -140,7 +161,7 @@ export default {
     decorate: function(games) {
       games.forEach(g => {
         g.myColor =
-          (g.type == "corr" && g.players[0].uid == this.st.user.id) ||
+          (g.type == "corr" && g.players[0].id == this.st.user.id) ||
           (g.type == "live" && g.players[0].sid == this.st.user.sid)
             ? 'w'
             : 'b';
@@ -193,7 +214,7 @@ export default {
             gameInfo
           );
           game.myTurn =
-            (type == "corr" && game.players[0].uid == this.st.user.id) ||
+            (type == "corr" && game.players[0].id == this.st.user.id) ||
             (type == "live" && game.players[0].sid == this.st.user.sid);
           gamesArrays[type].push(game);
           if (game.myTurn) this.tryShowNewsIndicator(type);
@@ -260,6 +281,25 @@ export default {
           }
         );
       }
+    },
+    loadMore: function() {
+      ajax(
+        "/completedgames",
+        "GET",
+        {
+          data: { cursor: this.cursor },
+          success: (res) => {
+            if (res.games.length > 0) {
+              const L = res.games.length;
+              this.cursor = res.games[L - 1].created;
+              let moreGames = res.games;
+              moreGames.forEach(g => g.type = "corr");
+              this.decorate(moreGames);
+              this.corrGames = this.corrGames.concat(moreGames);
+            } else this.hasMore = false;
+          }
+        }
+      );
     }
   }
 };
@@ -275,6 +315,10 @@ export default {
 table.game-list
   max-height: 100%
 
+button#loadMoreBtn
+  margin-top: 0
+  margin-bottom: 0
+
 .somethingnew
   background-color: #c5fefe !important
 </style>
diff --git a/client/src/views/News.vue b/client/src/views/News.vue
index ec1517df..0e3c3c7a 100644
--- a/client/src/views/News.vue
+++ b/client/src/views/News.vue
@@ -48,8 +48,10 @@ export default {
     return {
       devs: [1], //for now the only dev is me
       st: store.state,
-      cursor: 0, //ID of last showed news
-      hasMore: true, //a priori there could be more news to load
+      // timestamp of oldest showed news:
+      cursor: Number.MAX_SAFE_INTEGER,
+      // hasMore == TRUE: a priori there could be more news to load
+      hasMore: true,
       curnews: { id: 0, content: "" },
       newsList: [],
       infoMsg: ""
@@ -62,9 +64,10 @@ export default {
       {
         data: { cursor: this.cursor },
         success: (res) => {
-          this.newsList = res.newsList.sort((n1, n2) => n2.added - n1.added);
+          // The returned list is sorted from most recent to oldest
+          this.newsList = res.newsList;
           const L = res.newsList.length;
-          if (L > 0) this.cursor = this.newsList[0].id;
+          if (L > 0) this.cursor = res.newsList[L - 1].added;
         }
       }
     );
@@ -169,7 +172,7 @@ export default {
             if (res.newsList.length > 0) {
               this.newsList = this.newsList.concat(res.newsList);
               const L = res.newsList.length;
-              if (L > 0) this.cursor = res.newsList[L - 1].id;
+              if (L > 0) this.cursor = res.newsList[L - 1].added;
             } else this.hasMore = false;
           }
         }
diff --git a/server/app.js b/server/app.js
index 785088b3..df15fa20 100644
--- a/server/app.js
+++ b/server/app.js
@@ -11,12 +11,9 @@ let app = express();
 app.use(favicon(path.join(__dirname, "static", "favicon.ico")));
 
 if (app.get('env') === 'development')
-{
   // Full logging in development mode
   app.use(logger('dev'));
-}
-else
-{
+else {
   // http://dev.rdybarra.com/2016/06/23/Production-Logging-With-Morgan-In-Express/
   app.set('trust proxy', true);
   // In prod, only log error responses (https://github.com/expressjs/morgan)
@@ -34,8 +31,7 @@ app.use(express.static(path.join(__dirname, 'static')));
 app.use(express.static(path.join(__dirname, 'fallback')));
 
 // In development stage the client side has its own server
-if (params.cors.enable)
-{
+if (params.cors.enable) {
   app.use(function(req, res, next) {
     res.header("Access-Control-Allow-Origin", params.cors.allowedOrigin);
     res.header("Access-Control-Allow-Credentials", true); //for cookies
@@ -63,8 +59,7 @@ app.use(function(req, res, next) {
 // Error handler
 app.use(function(err, req, res, next) {
   res.status(err.status || 500);
-  if (app.get('env') === 'development')
-    console.log(err.stack);
+  if (app.get('env') === 'development') console.log(err.stack);
   res.send(
     "<h1>" + err.message + "</h1>" +
     "<h2>" + err.status + "</h2>"
diff --git a/server/config/parameters.js.dist b/server/config/parameters.js.dist
index 264fac50..c1908411 100644
--- a/server/config/parameters.js.dist
+++ b/server/config/parameters.js.dist
@@ -1,5 +1,4 @@
-module.exports =
-{
+module.exports = {
   // For mail sending. NOTE: *no trailing slash*
   siteURL: "http://localhost:8080",
 
diff --git a/server/db/create.sql b/server/db/create.sql
index 5aba8ff0..533e0f24 100644
--- a/server/db/create.sql
+++ b/server/db/create.sql
@@ -56,6 +56,8 @@ create table Games (
   vid integer,
   fenStart varchar, --initial state
   fen varchar, --current state
+  white integer,
+  black integer,
   score varchar default '*',
   scoreMsg varchar,
   cadence varchar,
@@ -65,7 +67,9 @@ create table Games (
   rematchOffer character default '',
   deletedByWhite boolean,
   deletedByBlack boolean,
-  foreign key (vid) references Variants(id)
+  foreign key (vid) references Variants(id),
+  foreign key (white) references Users(id),
+  foreign key (black) references Users(id)
 );
 
 create table Chats (
@@ -75,15 +79,6 @@ create table Chats (
   added datetime
 );
 
--- Store informations about players in a corr game
-create table Players (
-  gid integer,
-  uid integer,
-  color character,
-  foreign key (gid) references Games(id),
-  foreign key (uid) references Users(id)
-);
-
 create table Moves (
   gid integer,
   squares varchar, --description, appear/vanish/from/to
@@ -92,4 +87,6 @@ create table Moves (
   foreign key (gid) references Games(id)
 );
 
+create index scoreIdx on Games(score);
+
 pragma foreign_keys = on;
diff --git a/server/models/Challenge.js b/server/models/Challenge.js
index 2be700bd..dea0ac39 100644
--- a/server/models/Challenge.js
+++ b/server/models/Challenge.js
@@ -13,21 +13,18 @@ const UserModel = require("./User");
  *   cadence: string (3m+2s, 7d ...)
  */
 
-const ChallengeModel =
-{
-  checkChallenge: function(c)
-  {
+const ChallengeModel = {
+  checkChallenge: function(c) {
     return (
       c.vid.toString().match(/^[0-9]+$/) &&
       c.cadence.match(/^[0-9dhms +]+$/) &&
       c.randomness.toString().match(/^[0-2]$/) &&
       c.fen.match(/^[a-zA-Z0-9, /-]*$/) &&
-      (!c.to || UserModel.checkNameEmail({name: c.to}))
+      (!c.to || UserModel.checkNameEmail({ name: c.to }))
     );
   },
 
-  create: function(c, cb)
-  {
+  create: function(c, cb) {
     db.serialize(function() {
       const query =
         "INSERT INTO Challenges " +
@@ -37,14 +34,13 @@ const ChallengeModel =
           "(" + Date.now() + "," + c.uid + "," + (c.to ? c.to + "," : "") +
           c.vid + "," + c.randomness + ",'" + c.fen + "','" + c.cadence + "')";
       db.run(query, function(err) {
-        cb(err, {cid: this.lastID});
+        cb(err, { id: this.lastID });
       });
     });
   },
 
   // All challenges related to user with ID uid
-  getByUser: function(uid, cb)
-  {
+  getByUser: function(uid, cb) {
     db.serialize(function() {
       const query =
         "SELECT * " +
@@ -52,14 +48,13 @@ const ChallengeModel =
         "WHERE target IS NULL" +
           " OR uid = " + uid +
           " OR target = " + uid;
-      db.all(query, (err,challenges) => {
+      db.all(query, (err, challenges) => {
         cb(err, challenges);
       });
     });
   },
 
-  remove: function(id)
-  {
+  remove: function(id) {
     db.serialize(function() {
       const query =
         "DELETE FROM Challenges " +
@@ -68,8 +63,7 @@ const ChallengeModel =
     });
   },
 
-  safeRemove: function(id, uid)
-  {
+  safeRemove: function(id, uid) {
     db.serialize(function() {
       const query =
         "SELECT 1 " +
diff --git a/server/models/Game.js b/server/models/Game.js
index caa15c39..65aedfde 100644
--- a/server/models/Game.js
+++ b/server/models/Game.js
@@ -7,16 +7,17 @@ const UserModel = require("./User");
  *   vid: integer (variant id)
  *   fenStart: varchar (initial position)
  *   fen: varchar (current position)
+ *   white: integer
+ *   black: integer
  *   cadence: string
  *   score: varchar (result)
  *   scoreMsg: varchar ("Time", "Mutual agreement"...)
  *   created: datetime
  *   drawOffer: char ('w','b' or '' for none)
- *
- * Structure table Players:
- *   gid: ref game id
- *   uid: ref user id
- *   color: character
+ *   rematchOffer: char (similar to drawOffer)
+ *   randomness: integer
+ *   deletedByWhite: boolean
+ *   deletedByBlack: boolean
  *
  * Structure table Moves:
  *   gid: ref game id
@@ -37,174 +38,234 @@ const GameModel =
     return (
       g.vid.toString().match(/^[0-9]+$/) &&
       g.cadence.match(/^[0-9dhms +]+$/) &&
+      g.randomness.match(/^[0-2]$/) &&
       g.fen.match(/^[a-zA-Z0-9, /-]*$/) &&
       g.players.length == 2 &&
-      g.players.every(p => p.uid.toString().match(/^[0-9]+$/))
+      g.players.every(p => p.toString().match(/^[0-9]+$/))
     );
   },
 
-  create: function(vid, fen, cadence, players, cb)
-  {
+  create: function(vid, fen, randomness, cadence, players, cb) {
     db.serialize(function() {
       let query =
         "INSERT INTO Games " +
-        "(vid, fenStart, fen, cadence, created) " +
+        "(" +
+          "vid, fenStart, fen, randomness, " +
+          "white, black, " +
+          "cadence, created" +
+        ") " +
         "VALUES " +
-        "(" + vid + ",'" + fen + "','" + fen + "','" + cadence + "'," + Date.now() + ")";
+        "(" +
+          vid + ",'" + fen + "','" + fen + "'," + randomness + "," +
+          "'" + players[0] + "','" + players[1] + "," +
+          "'" + cadence + "'," + Date.now() +
+        ")";
       db.run(query, function(err) {
-        if (err)
-          cb(err)
-        else
-        {
-          players.forEach((p,idx) => {
-            const color = (idx==0 ? "w" : "b");
-            query =
-              "INSERT INTO Players VALUES " +
-              "(" + this.lastID + "," + p.uid + ",'" + color + "')";
-            db.run(query);
-          });
-          cb(null, {gid: this.lastID});
-        }
+        cb(err, { id: this.lastId });
       });
     });
   },
 
   // TODO: some queries here could be async
-  getOne: function(id, cb)
-  {
+  getOne: function(id, cb) {
     // NOTE: ignoring errors (shouldn't happen at this stage)
     db.serialize(function() {
       let query =
-        "SELECT g.id, g.vid, g.fen, g.fenStart, g.cadence, g.created, g.score, " +
-          "g.scoreMsg, g.drawOffer, g.rematchOffer, v.name AS vname " +
+        "SELECT " +
+          "g.id, g.vid, g.fen, g.fenStart, g.cadence, g.created, " +
+          "g.white, g.black, g.score, g.scoreMsg, " +
+          "g.drawOffer, g.rematchOffer, v.name AS vname " +
         "FROM Games g " +
         "JOIN Variants v " +
         "  ON g.vid = v.id " +
         "WHERE g.id = " + id;
       db.get(query, (err, gameInfo) => {
         query =
-          "SELECT p.uid, p.color, u.name " +
-          "FROM Players p " +
-          "JOIN Users u " +
-          "  ON p.uid = u.id " +
-          "WHERE p.gid = " + id;
-        db.all(query, (err2, players) => {
+          "SELECT squares, played, idx " +
+          "FROM Moves " +
+          "WHERE gid = " + id;
+        db.all(query, (err3, moves) => {
           query =
-            "SELECT squares, played, idx " +
-            "FROM Moves " +
+            "SELECT msg, name, added " +
+            "FROM Chats " +
             "WHERE gid = " + id;
-          db.all(query, (err3, moves) => {
-            query =
-              "SELECT msg, name, added " +
-              "FROM Chats " +
-              "WHERE gid = " + id;
-            db.all(query, (err4, chats) => {
-              const game = Object.assign(
-                {},
-                gameInfo,
-                {
-                  players: players,
-                  moves: moves,
-                  chats: chats,
-                }
-              );
-              cb(null, game);
-            });
+          db.all(query, (err4, chats) => {
+            const game = Object.assign(
+              {},
+              gameInfo,
+              {
+                moves: moves,
+                chats: chats
+              }
+            );
+            cb(null, game);
           });
         });
       });
     });
   },
 
-  // For display on MyGames or Hall: no need for moves or chats
-  getByUser: function(uid, excluded, cb)
-  {
-    // Some fields are not required when showing a games list:
-    const getOneLight = (id, cb2) => {
+  // For display on Hall: no need for moves or chats
+  getObserved: function(uid, cursor, cb) {
+    db.serialize(function() {
+      let query =
+        "SELECT g.id, g.vid, g.cadence, g.created, " +
+        "       g.score, g.white, g.black " +
+        "FROM Games g ";
+      if (uid > 0) query +=
+        "WHERE " +
+        "  created < " + cursor + " AND " +
+        "  white <> " + uid + " AND " +
+        "  black <> " + uid + " ";
+      query +=
+        "ORDER BY created DESC " +
+        "LIMIT 20"; //TODO: 20 hard-coded...
+      db.all(query, (err, games) => {
+        // Query players names
+        let pids = {};
+        games.forEach(g => {
+          if (!pids[g.white]) pids[g.white] = true;
+          if (!pids[g.black]) pids[g.black] = true;
+        });
+        UserModel.getByIds(Object.keys(pids), (err2, users) => {
+          let names = {};
+          users.forEach(u => { names[u.id] = u.name; });
+          cb(
+            games.map(
+              g => {
+                return {
+                  id: g.id,
+                  cadence: g.cadence,
+                  vname: g.vname,
+                  created: g.created,
+                  score: g.score,
+                  white: names[g.white],
+                  black: names[g.black]
+                };
+              }
+            )
+          );
+        });
+      });
+    });
+  },
+
+  // For display on MyGames: registered user only
+  getRunning: function(uid, cb) {
+    db.serialize(function() {
       let query =
-        "SELECT g.id, g.vid, g.fen, g.cadence, g.created, g.score, " +
-          "g.scoreMsg, g.deletedByWhite, g.deletedByBlack, v.name AS vname " +
+        "SELECT g.id, g.cadence, g.created, g.score, " +
+          "g.white, g.black, v.name AS vname " +
         "FROM Games g " +
         "JOIN Variants v " +
         "  ON g.vid = v.id " +
-        "WHERE g.id = " + id;
-      db.get(query, (err, gameInfo) => {
+        "WHERE white = " + uid + " OR black = " + uid;
+      db.all(query, (err, games) => {
+        // Get movesCount (could be done in // with next query)
         query =
-          "SELECT p.uid, p.color, u.name " +
-          "FROM Players p " +
-          "JOIN Users u " +
-          "  ON p.uid = u.id " +
-          "WHERE p.gid = " + id;
-        db.all(query, (err2, players) => {
-          query =
-            "SELECT COUNT(*) AS nbMoves " +
-            "FROM Moves " +
-            "WHERE gid = " + id;
-          db.get(query, (err,ret) => {
-            const game = Object.assign(
-              {},
-              gameInfo,
-              {
-                players: players,
-                movesCount: ret.nbMoves
-              }
+          "SELECT gid, COUNT(*) AS nbMoves " +
+          "FROM Moves " +
+          "WHERE gid IN " + "(" + games.map(g => g.id).join(",") + ") " +
+          "GROUP BY gid";
+        db.all(query, (err, mstats) => {
+          let movesCounts = {};
+          mstats.forEach(ms => { movesCounts[ms.gid] = ms.nbMoves; });
+          // Query player names
+          let pids = {};
+          games.forEach(g => {
+            if (!pids[g.white]) pids[g.white] = true;
+            if (!pids[g.black]) pids[g.black] = true;
+          });
+          UserModel.getByIds(pids, (err2, users) => {
+            let names = {};
+            users.forEach(u => { names[u.id] = u.name; });
+            cb(
+              games.map(
+                g => {
+                  return {
+                    id: g.id,
+                    cadence: g.cadence,
+                    vname: g.vname,
+                    created: g.created,
+                    score: g.score,
+                    movesCount: movesCounts[g.id],
+                    white: names[g.white],
+                    black: names[g.black]
+                  };
+                }
+              )
             );
-            cb2(game);
           });
         });
       });
-    };
+    });
+  },
+
+  // These games could be deleted on some side. movesCount not required
+  getCompleted: function(uid, cursor, cb) {
     db.serialize(function() {
-      let query = "";
-      if (uid == 0) {
-        // Special case anonymous user: show all games
-        query =
-          "SELECT id AS gid " +
-          "FROM Games";
-      }
-      else {
-        // Registered user:
-        query =
-          "SELECT gid " +
-          "FROM Players " +
-          "GROUP BY gid " +
-          "HAVING COUNT(uid = " + uid + " OR NULL) " +
-          (excluded ? " = 0" : " > 0");
-      }
-      db.all(query, (err,gameIds) => {
-        if (err || gameIds.length == 0) cb(err, []);
-        else {
-          let gameArray = [];
-          let gCounter = 0;
-          for (let i=0; i<gameIds.length; i++) {
-            getOneLight(gameIds[i]["gid"], (game) => {
-              gameArray.push(game);
-              gCounter++; //TODO: let's hope this is atomic?!
-              // Call callback function only when gameArray is complete:
-              if (gCounter == gameIds.length)
-                cb(null, gameArray);
-            });
-          }
-        }
+      let query =
+        "SELECT g.id, g.cadence, g.created, g.score, g.scoreMsg, " +
+          "g.white, g.black, g.deletedByWhite, g.deletedByBlack, " +
+          "v.name AS vname " +
+        "FROM Games g " +
+        "JOIN Variants v " +
+        "  ON g.vid = v.id " +
+        "WHERE " +
+        "  created < " + cursor + " AND " +
+        "  (" +
+        "    (" + uid + " = white AND NOT deletedByWhite) OR " +
+        "    (" + uid + " = black AND NOT deletedByBlack)" +
+        "  ) ";
+      query +=
+        "ORDER BY created DESC " +
+        "LIMIT 20";
+      db.all(query, (err, games) => {
+        // Query player names
+        let pids = {};
+        games.forEach(g => {
+          if (!pids[g.white]) pids[g.white] = true;
+          if (!pids[g.black]) pids[g.black] = true;
+        });
+        UserModel.getByIds(pids, (err2, users) => {
+          let names = {};
+          users.forEach(u => { names[u.id] = u.name; });
+          cb(
+            games.map(
+              g => {
+                return {
+                  id: g.id,
+                  cadence: g.cadence,
+                  vname: g.vname,
+                  created: g.created,
+                  score: g.score,
+                  scoreMsg: g.scoreMsg,
+                  white: names[g.white],
+                  black: names[g.black],
+                  deletedByWhite: g.deletedByWhite,
+                  deletedByBlack: g.deletedByBlack
+                };
+              }
+            )
+          );
+        });
       });
     });
   },
 
-  getPlayers: function(id, cb)
-  {
+  getPlayers: function(id, cb) {
     db.serialize(function() {
       const query =
-        "SELECT uid " +
-        "FROM Players " +
+        "SELECT white, black " +
+        "FROM Games " +
         "WHERE gid = " + id;
-      db.all(query, (err,players) => {
+      db.all(query, (err, players) => {
         return cb(err, players);
       });
     });
   },
 
-  checkGameUpdate: function(obj)
-  {
+  checkGameUpdate: function(obj) {
     // Check all that is possible (required) in obj:
     return (
       (
@@ -229,8 +290,7 @@ const GameModel =
   },
 
   // obj can have fields move, chat, fen, drawOffer and/or score + message
-  update: function(id, obj, cb)
-  {
+  update: function(id, obj, cb) {
     db.parallelize(function() {
       let query =
         "UPDATE Games " +
@@ -320,56 +380,72 @@ const GameModel =
     });
   },
 
-  remove: function(id)
-  {
+  remove: function(id_s) {
+    const suffix =
+      Array.isArray(id_s)
+        ? " IN (" + id_s.join(",") + ")"
+        : " = " + id_s;
     db.parallelize(function() {
       let query =
         "DELETE FROM Games " +
-        "WHERE id = " + id;
-      db.run(query);
-      query =
-        "DELETE FROM Players " +
-        "WHERE gid = " + id;
+        "WHERE id " + suffix;
       db.run(query);
       query =
         "DELETE FROM Moves " +
-        "WHERE gid = " + id;
+        "WHERE gid " + suffix;
       db.run(query);
       query =
         "DELETE FROM Chats " +
-        "WHERE gid = " + id;
+        "WHERE gid " + suffix;
       db.run(query);
     });
   },
 
-  cleanGamesDb: function()
-  {
+  cleanGamesDb: function() {
     const tsNow = Date.now();
     // 86400000 = 24 hours in milliseconds
     const day = 86400000;
     db.serialize(function() {
       let query =
         "SELECT id, created " +
-        "FROM Games ";
-      db.all(query, (err,games) => {
-        games.forEach(g => {
-          query =
-            "SELECT count(*) as nbMoves, max(played) AS lastMaj " +
-            "FROM Moves " +
-            "WHERE gid = " + g.id;
-          db.get(query, (err2,mstats) => {
-            // Remove games still not really started,
-            // with no action in the last 3 months:
-            if ((mstats.nbMoves == 0 && tsNow - g.created > 91*day) ||
-              (mstats.nbMoves == 1 && tsNow - mstats.lastMaj > 91*day))
-            {
-              GameModel.remove(g.id);
+        "FROM Games";
+      db.all(query, (err, games) => {
+        query =
+          "SELECT gid, count(*) AS nbMoves, MAX(played) AS lastMaj " +
+          "FROM Moves " +
+          "GROUP BY gid";
+        db.get(query, (err2, mstats) => {
+          // Reorganize moves data to avoid too many array lookups:
+          let movesGroups = {};
+          mstats.forEach(ms => {
+            movesGroups[ms.gid] = {
+              nbMoves: ms.nbMoves,
+              lastMaj: ms.lastMaj
+            };
+          });
+          // Remove games still not really started,
+          // with no action in the last 3 months:
+          let toRemove = [];
+          games.forEach(g => {
+            if (
+              (
+                !movesGroups[g.id] &&
+                tsNow - g.created > 91*day
+              )
+              ||
+              (
+                movesGroups[g.id].nbMoves == 1 &&
+                tsNow - movesGroups[g.id].lastMaj > 91*day
+              )
+            ) {
+              toRemove.push(g.id);
             }
           });
+          if (toRemove.length > 0) GameModel.remove(toRemove);
         });
       });
     });
-  },
+  }
 }
 
 module.exports = GameModel;
diff --git a/server/models/News.js b/server/models/News.js
index 3fa3caae..2dde566f 100644
--- a/server/models/News.js
+++ b/server/models/News.js
@@ -19,7 +19,7 @@ const NewsModel =
           "VALUES " +
         "(" + Date.now() + "," + uid + ",?)";
       db.run(query, content, function(err) {
-        cb(err, {nid: this.lastID});
+        cb(err, { id: this.lastID });
       });
     });
   },
@@ -30,7 +30,8 @@ const NewsModel =
       const query =
         "SELECT * " +
         "FROM News " +
-        "WHERE id > " + cursor + " " +
+        "WHERE added < " + cursor + " " +
+        "ORDER BY added DESC " +
         "LIMIT 10"; //TODO: 10 currently hard-coded
       db.all(query, (err,newsList) => {
         cb(err, newsList);
diff --git a/server/models/Problem.js b/server/models/Problem.js
index 136fb649..5c9af0b1 100644
--- a/server/models/Problem.js
+++ b/server/models/Problem.js
@@ -11,10 +11,8 @@ const db = require("../utils/database");
  *   solution: text
  */
 
-const ProblemModel =
-{
-  checkProblem: function(p)
-  {
+const ProblemModel = {
+  checkProblem: function(p) {
     return (
       p.id.toString().match(/^[0-9]+$/) &&
       p.vid.toString().match(/^[0-9]+$/) &&
@@ -22,8 +20,7 @@ const ProblemModel =
     );
   },
 
-  create: function(p, cb)
-  {
+  create: function(p, cb) {
     db.serialize(function() {
       const query =
         "INSERT INTO Problems " +
@@ -31,13 +28,12 @@ const ProblemModel =
           "VALUES " +
         "(" + Date.now() + "," + p.uid + "," + p.vid + ",'" + p.fen  + "',?,?)";
       db.run(query, [p.instruction,p.solution], function(err) {
-        cb(err, {pid: this.lastID});
+        cb(err, { id: this.lastID });
       });
     });
   },
 
-  getAll: function(cb)
-  {
+  getAll: function(cb) {
     db.serialize(function() {
       const query =
         "SELECT * " +
@@ -48,8 +44,7 @@ const ProblemModel =
     });
   },
 
-  getOne: function(id, cb)
-  {
+  getOne: function(id, cb) {
     db.serialize(function() {
       const query =
         "SELECT * " +
@@ -61,8 +56,7 @@ const ProblemModel =
     });
   },
 
-  safeUpdate: function(prob, uid)
-  {
+  safeUpdate: function(prob, uid) {
     db.serialize(function() {
       const query =
         "UPDATE Problems " +
@@ -76,8 +70,7 @@ const ProblemModel =
     });
   },
 
-  safeRemove: function(id, uid)
-  {
+  safeRemove: function(id, uid) {
     db.serialize(function() {
       const query =
         "DELETE FROM Problems " +
diff --git a/server/models/User.js b/server/models/User.js
index 021cadcb..0ff8e1c8 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -13,34 +13,31 @@ const sendEmail = require('../utils/mailer');
  *   sessionToken: token in cookies for authentication
  *   notify: boolean (send email notifications for corr games)
  *   created: datetime
+ *   newsRead: datetime
  */
 
-const UserModel =
-{
-  checkNameEmail: function(o)
-  {
+const UserModel = {
+  checkNameEmail: function(o) {
     return (
       (!o.name || !!(o.name.match(/^[\w-]+$/))) &&
       (!o.email || !!(o.email.match(/^[\w.+-]+@[\w.+-]+$/)))
     );
   },
 
-  create: function(name, email, notify, cb)
-  {
+  create: function(name, email, notify, cb) {
     db.serialize(function() {
       const query =
         "INSERT INTO Users " +
         "(name, email, notify, created) VALUES " +
         "('" + name + "','" + email + "'," + notify + "," + Date.now() + ")";
       db.run(query, function(err) {
-        cb(err, {uid: this.lastID});
+        cb(err, { id: this.lastID });
       });
     });
   },
 
   // Find one user by id, name, email, or token
-  getOne: function(by, value, cb)
-  {
+  getOne: function(by, value, cb) {
     const delimiter = (typeof value === "string" ? "'" : "");
     db.serialize(function() {
       const query =
@@ -64,24 +61,22 @@ const UserModel =
   /////////
   // MODIFY
 
-  setLoginToken: function(token, uid)
-  {
+  setLoginToken: function(token, id) {
     db.serialize(function() {
       const query =
         "UPDATE Users " +
         "SET loginToken = '" + token + "',loginTime = " + Date.now() + " " +
-        "WHERE id = " + uid;
+        "WHERE id = " + id;
       db.run(query);
     });
   },
 
-  setNewsRead: function(uid)
-  {
+  setNewsRead: function(id) {
     db.serialize(function() {
       const query =
         "UPDATE Users " +
         "SET newsRead = " + Date.now() + " " +
-        "WHERE id = " + uid;
+        "WHERE id = " + id;
       db.run(query);
     });
   },
@@ -89,13 +84,12 @@ const UserModel =
   // Set session token only if empty (first login)
   // NOTE: weaker security (but avoid to re-login everywhere after each logout)
   // TODO: option would be to reset all tokens periodically, e.g. every 3 months
-  trySetSessionToken: function(uid, cb)
-  {
+  trySetSessionToken: function(id, cb) {
     db.serialize(function() {
       let query =
         "SELECT sessionToken " +
         "FROM Users " +
-        "WHERE id = " + uid;
+        "WHERE id = " + id;
       db.get(query, (err,ret) => {
         const token = ret.sessionToken || genToken(params.token.length);
         query =
@@ -103,15 +97,14 @@ const UserModel =
           // Also empty the login token to invalidate future attempts
           "SET loginToken = NULL" +
           (!ret.sessionToken ? (", sessionToken = '" + token + "'") : "") + " " +
-          "WHERE id = " + uid;
+          "WHERE id = " + id;
         db.run(query);
         cb(token);
       });
     });
   },
 
-  updateSettings: function(user)
-  {
+  updateSettings: function(user) {
     db.serialize(function() {
       const query =
         "UPDATE Users " +
@@ -126,27 +119,23 @@ const UserModel =
   /////////////////
   // NOTIFICATIONS
 
-  notify: function(user, message)
-  {
+  notify: function(user, message) {
     const subject = "vchess.club - notification";
     const body = "Hello " + user.name + " !" + `
 ` + message;
     sendEmail(params.mail.noreply, user.email, subject, body);
   },
 
-  tryNotify: function(id, message)
-  {
+  tryNotify: function(id, message) {
     UserModel.getOne("id", id, (err,user) => {
-      if (!err && user.notify)
-        UserModel.notify(user, message);
+      if (!err && user.notify) UserModel.notify(user, message);
     });
   },
 
   ////////////
   // CLEANING
 
-  cleanUsersDb: function()
-  {
+  cleanUsersDb: function() {
     const tsNow = Date.now();
     // 86400000 = 24 hours in milliseconds
     const day = 86400000;
@@ -155,8 +144,9 @@ const UserModel =
         "SELECT id, sessionToken, created, name, email " +
         "FROM Users";
       db.all(query, (err, users) => {
+        let toRemove = [];
         users.forEach(u => {
-          // Remove unlogged users for > 24h
+          // Remove users unlogged for > 24h
           if (!u.sessionToken && tsNow - u.created > day)
           {
             notify(
@@ -164,9 +154,14 @@ const UserModel =
               "Your account has been deleted because " +
               "you didn't log in for 24h after registration"
             );
-            db.run("DELETE FROM Users WHERE id = " + u.id);
           }
         });
+        if (toRemove.length > 0) {
+          db.run(
+            "DELETE FROM Users " +
+            "WHERE id IN (" + toRemove.join(",") + ")"
+          );
+        }
       });
     });
   },
diff --git a/server/models/Variant.js b/server/models/Variant.js
index d9911071..85abf5ae 100644
--- a/server/models/Variant.js
+++ b/server/models/Variant.js
@@ -7,10 +7,8 @@ const db = require("../utils/database");
  *   description: varchar
  */
 
-const VariantModel =
-{
-  getAll: function(callback)
-  {
+const VariantModel = {
+  getAll: function(callback) {
     db.serialize(function() {
       const query =
         "SELECT * " +
diff --git a/server/routes/challenges.js b/server/routes/challenges.js
index 680a69fd..d27f81ee 100644
--- a/server/routes/challenges.js
+++ b/server/routes/challenges.js
@@ -5,10 +5,8 @@ const UserModel = require("../models/User"); //for name check
 const params = require("../config/parameters");
 
 router.post("/challenges", access.logged, access.ajax, (req,res) => {
-  if (ChallengeModel.checkChallenge(req.body.chall))
-  {
-    let challenge =
-    {
+  if (ChallengeModel.checkChallenge(req.body.chall)) {
+    let challenge = {
       fen: req.body.chall.fen,
       cadence: req.body.chall.cadence,
       randomness: req.body.chall.randomness,
@@ -17,17 +15,15 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => {
       to: req.body.chall.to, //string: user name (may be empty)
     };
     const insertChallenge = () => {
-      ChallengeModel.create(challenge, (err,ret) => {
-        res.json(err || {cid:ret.cid});
+      ChallengeModel.create(challenge, (err, ret) => {
+        res.json(err || ret);
       });
     };
-    if (req.body.chall.to)
-    {
+    if (req.body.chall.to) {
       UserModel.getOne("name", challenge.to, (err,user) => {
         if (err || !user)
           res.json(err || {errmsg: "Typo in player name"});
-        else
-        {
+        else {
           challenge.to = user.id; //ready now to insert challenge
           insertChallenge();
           if (user.notify)
@@ -36,26 +32,22 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => {
               "New challenge: " + params.siteURL + "/#/?disp=corr");
         }
       });
-    }
-    else
-      insertChallenge();
+    } else insertChallenge();
   }
 });
 
 router.get("/challenges", access.ajax, (req,res) => {
   const uid = req.query.uid;
-  if (uid.match(/^[0-9]+$/))
-  {
+  if (uid.match(/^[0-9]+$/)) {
     ChallengeModel.getByUser(uid, (err,challenges) => {
-      res.json(err || {challenges:challenges});
+      res.json(err || { challenges: challenges });
     });
   }
 });
 
 router.delete("/challenges", access.logged, access.ajax, (req,res) => {
   const cid = req.query.id;
-  if (cid.match(/^[0-9]+$/))
-  {
+  if (cid.match(/^[0-9]+$/)) {
     ChallengeModel.safeRemove(cid, req.userId);
     res.json({});
   }
diff --git a/server/routes/games.js b/server/routes/games.js
index a86a76dd..77877a2e 100644
--- a/server/routes/games.js
+++ b/server/routes/games.js
@@ -12,46 +12,58 @@ router.post("/games", access.logged, access.ajax, (req,res) => {
   const cid = req.body.cid;
   if (
     Array.isArray(gameInfo.players) &&
-    gameInfo.players.some(p => p.uid == req.userId) &&
+    gameInfo.players.some(p => p.id == req.userId) &&
     (!cid || cid.toString().match(/^[0-9]+$/)) &&
     GameModel.checkGameInfo(gameInfo)
   ) {
     if (!!cid) ChallengeModel.remove(cid);
     GameModel.create(
       gameInfo.vid, gameInfo.fen, gameInfo.cadence, gameInfo.players,
-      (err,ret) => {
+      (err, ret) => {
         const oppIdx = (gameInfo.players[0].id == req.userId ? 1 : 0);
         const oppId = gameInfo.players[oppIdx].id;
         UserModel.tryNotify(oppId,
-          "Game started: " + params.siteURL + "/#/game/" + ret.gid);
-        res.json({gameId: ret.gid});
+          "Game started: " + params.siteURL + "/#/game/" + ret.id);
+        res.json(err || ret);
       }
     );
   }
 });
 
+// Get only one game (for Game page)
 router.get("/games", access.ajax, (req,res) => {
   const gameId = req.query["gid"];
-  if (gameId)
-  {
-    if (gameId.match(/^[0-9]+$/))
-    {
-      GameModel.getOne(gameId, (err,game) => {
-        res.json({game: game});
-      });
-    }
+  if (!!gameId && gameId.match(/^[0-9]+$/)) {
+    GameModel.getOne(gameId, (err, game) => {
+      res.json({ game: game });
+    });
   }
-  else
-  {
-    // Get by (non-)user ID:
-    const userId = req.query["uid"];
-    if (userId.match(/^[0-9]+$/))
-    {
-      const excluded = !!req.query["excluded"];
-      GameModel.getByUser(userId, excluded, (err,games) => {
-        res.json({games: games});
-      });
-    }
+});
+
+// Get by (non-)user ID, for Hall
+router.get("/observedgames", access.ajax, (req,res) => {
+  const userId = req.query["uid"];
+  const cursor = req.query["cursor"];
+  if (!!userId.match(/^[0-9]+$/) && !!cursor.match(/^[0-9]+$/)) {
+    GameModel.getObserved(userId, (err, games) => {
+      res.json({ games: games });
+    });
+  }
+});
+
+// Get by user ID, for MyGames page
+router.get("/runninggames", access.ajax, access.logged, (req,res) => {
+  GameModel.getRunning(req.userId, (err, games) => {
+    res.json({ games: games });
+  });
+});
+
+router.get("/completedgames", access.ajax, access.logged, (req,res) => {
+  const cursor = req.query["cursor"];
+  if (!!cursor.match(/^[0-9]+$/)) {
+    GameModel.getCompleted(req.userId, cursor, (err, games) => {
+      res.json({ games: games });
+    });
   }
 });
 
@@ -59,28 +71,29 @@ router.get("/games", access.ajax, (req,res) => {
 router.put("/games", access.logged, access.ajax, (req,res) => {
   const gid = req.body.gid;
   let obj = req.body.newObj;
-  if (gid.toString().match(/^[0-9]+$/) && GameModel.checkGameUpdate(obj))
-  {
+  if (gid.toString().match(/^[0-9]+$/) && GameModel.checkGameUpdate(obj)) {
     GameModel.getPlayers(gid, (err,players) => {
-      const myIdx = players.findIndex(p => p.uid == req.userId)
-      if (myIdx >= 0) {
+      let myColor = '';
+      if (players.white == req.userId) myColor = 'w';
+      else if (players.black == req.userId) myColor = 'b';
+      if (!!myColor) {
         // Did I mark the game for deletion?
         if (!!obj.removeFlag) {
-          obj.deletedBy = ["w","b"][myIdx];
+          obj.deletedBy = myColor;
           delete obj["removeFlag"];
         }
         GameModel.update(gid, obj, (err) => {
-          if (!err && (!!obj.move || !!obj.score))
-          {
+          if (!err && (!!obj.move || !!obj.score)) {
             // Notify opponent if he enabled notifications:
-            const oppid = players[0].uid == req.userId
-              ? players[1].uid
-              : players[0].uid;
-            const messagePrefix = obj.move
-              ? "New move in game: "
-              : "Game ended: ";
-            UserModel.tryNotify(oppid,
-              messagePrefix + params.siteURL + "/#/game/" + gid);
+            const oppid = (myColor == 'w' ? players.black : players.white);
+            const messagePrefix =
+              !!obj.move
+                ? "New move in game: "
+                : "Game ended: ";
+            UserModel.tryNotify(
+              oppid,
+              messagePrefix + params.siteURL + "/#/game/" + gid
+            );
           }
           res.json(err || {});
         });
@@ -93,10 +106,10 @@ router.put("/games", access.logged, access.ajax, (req,res) => {
 // Moves update also could, although logical unit in a game.
 router.delete("/chats", access.logged, access.ajax, (req,res) => {
   const gid = req.query["gid"];
-  GameModel.getPlayers(gid, (err,players) => {
-    if (players.some(p => p.uid == req.userId))
+  GameModel.getPlayers(gid, (err, players) => {
+    if ([players.white, players.black].includes(req.userId))
     {
-      GameModel.update(gid, {delchat: true}, () => {
+      GameModel.update(gid, { delchat: true }, () => {
         res.json({});
       });
     }
diff --git a/server/routes/news.js b/server/routes/news.js
index e1efbdd9..4c2a74e5 100644
--- a/server/routes/news.js
+++ b/server/routes/news.js
@@ -5,19 +5,18 @@ const sanitizeHtml = require('sanitize-html');
 const devs = [1]; //hard-coded list of developers IDs, allowed to post news
 
 router.post("/news", access.logged, access.ajax, (req,res) => {
-  if (devs.includes(req.userId))
-  {
+  if (devs.includes(req.userId)) {
     const content = sanitizeHtml(req.body.news.content);
-    NewsModel.create(content, req.userId, (err,ret) => {
-      res.json(err || { id: ret.nid });
+    NewsModel.create(content, req.userId, (err, ret) => {
+      res.json(err || ret);
     });
   }
 });
 
 router.get("/news", access.ajax, (req,res) => {
   const cursor = req.query["cursor"];
-  if (cursor.match(/^[0-9]+$/)) {
-    NewsModel.getNext(cursor, (err,newsList) => {
+  if (!!cursor.match(/^[0-9]+$/)) {
+    NewsModel.getNext(cursor, (err, newsList) => {
       res.json(err || { newsList: newsList });
     });
   }
diff --git a/server/routes/problems.js b/server/routes/problems.js
index 732ea710..6cebb8f0 100644
--- a/server/routes/problems.js
+++ b/server/routes/problems.js
@@ -4,18 +4,16 @@ const ProblemModel = require("../models/Problem");
 const sanitizeHtml = require('sanitize-html');
 
 router.post("/problems", access.logged, access.ajax, (req,res) => {
-  if (ProblemModel.checkProblem(req.body.prob))
-  {
-    const problem =
-    {
+  if (ProblemModel.checkProblem(req.body.prob)) {
+    const problem = {
       vid: req.body.prob.vid,
       fen: req.body.prob.fen,
       uid: req.userId,
       instruction: sanitizeHtml(req.body.prob.instruction),
       solution: sanitizeHtml(req.body.prob.solution),
     };
-    ProblemModel.create(problem, (err,ret) => {
-      res.json(err || {id:ret.pid});
+    ProblemModel.create(problem, (err, ret) => {
+      res.json(err || ret);
     });
   }
   else
@@ -24,24 +22,20 @@ router.post("/problems", access.logged, access.ajax, (req,res) => {
 
 router.get("/problems", access.ajax, (req,res) => {
   const probId = req.query["pid"];
-  if (probId && probId.match(/^[0-9]+$/))
-  {
+  if (probId && probId.match(/^[0-9]+$/)) {
     ProblemModel.getOne(req.query["pid"], (err,problem) => {
       res.json(err || {problem: problem});
     });
-  }
-  else
-  {
+  } else {
     ProblemModel.getAll((err,problems) => {
-      res.json(err || {problems:problems});
+      res.json(err || { problems: problems });
     });
   }
 });
 
 router.put("/problems", access.logged, access.ajax, (req,res) => {
   let obj = req.body.prob;
-  if (ProblemModel.checkProblem(obj))
-  {
+  if (ProblemModel.checkProblem(obj)) {
     obj.instruction = sanitizeHtml(obj.instruction);
     obj.solution = sanitizeHtml(obj.solution);
     ProblemModel.safeUpdate(obj, req.userId);
diff --git a/server/routes/users.js b/server/routes/users.js
index fc29730c..7898ac8a 100644
--- a/server/routes/users.js
+++ b/server/routes/users.js
@@ -9,20 +9,16 @@ router.post('/register', access.unlogged, access.ajax, (req,res) => {
   const name = req.body.name;
   const email = req.body.email;
   const notify = !!req.body.notify;
-  if (UserModel.checkNameEmail({name: name, email: email}))
-  {
-    UserModel.create(name, email, notify, (err,ret) => {
-      if (err)
-      {
+  if (UserModel.checkNameEmail({name: name, email: email})) {
+    UserModel.create(name, email, notify, (err, ret) => {
+      if (!!err) {
         const msg = err.code == "SQLITE_CONSTRAINT"
           ? "User name or email already in use"
           : "User creation failed. Try again";
         res.json({errmsg: msg});
-      }
-      else
-      {
+      } else {
         const user = {
-          id: ret.uid,
+          id: ret.id,
           name: name,
           email: email,
         };
@@ -51,10 +47,8 @@ router.get("/whoami", access.ajax, (req,res) => {
     notify: false,
     newsRead: 0
   };
-  if (!req.cookies.token)
-    callback(anonymous);
-  else if (req.cookies.token.match(/^[a-z0-9]+$/))
-  {
+  if (!req.cookies.token) callback(anonymous);
+  else if (req.cookies.token.match(/^[a-z0-9]+$/)) {
     UserModel.getOne("sessionToken", req.cookies.token, (err, user) => {
       callback(user || anonymous);
     });
@@ -64,8 +58,8 @@ router.get("/whoami", access.ajax, (req,res) => {
 // NOTE: this method is safe because only IDs and names are returned
 router.get("/users", access.ajax, (req,res) => {
   const ids = req.query["ids"];
-  if (ids.match(/^([0-9]+,?)+$/)) //NOTE: slightly too permissive
-  {
+  // NOTE: slightly too permissive RegExp
+  if (ids.match(/^([0-9]+,?)+$/)) {
     UserModel.getByIds(ids, (err,users) => {
       res.json({users:users});
     });
@@ -75,8 +69,7 @@ router.get("/users", access.ajax, (req,res) => {
 router.put('/update', access.logged, access.ajax, (req,res) => {
   const name = req.body.name;
   const email = req.body.email;
-  if (UserModel.checkNameEmail({name: name, email: email}));
-  {
+  if (UserModel.checkNameEmail({name: name, email: email})) {
     const user = {
       id: req.userId,
       name: name,
@@ -97,8 +90,7 @@ router.put('/newsread', access.logged, access.ajax, (req,res) => {
 // Authentication-related methods:
 
 // to: object user (to who we send an email)
-function setAndSendLoginToken(subject, to, res)
-{
+function setAndSendLoginToken(subject, to, res) {
   // Set login token and send welcome(back) email with auth link
   const token = genToken(params.token.length);
   UserModel.setLoginToken(token, to.id);
@@ -115,8 +107,7 @@ function setAndSendLoginToken(subject, to, res)
 router.get('/sendtoken', access.unlogged, access.ajax, (req,res) => {
   const nameOrEmail = decodeURIComponent(req.query.nameOrEmail);
   const type = (nameOrEmail.indexOf('@') >= 0 ? "email" : "name");
-  if (UserModel.checkNameEmail({[type]: nameOrEmail}))
-  {
+  if (UserModel.checkNameEmail({[type]: nameOrEmail})) {
     UserModel.getOne(type, nameOrEmail, (err,user) => {
       access.checkRequest(res, err, user, "Unknown user", () => {
         setAndSendLoginToken("Token for " + params.siteURL, user, res);
@@ -134,8 +125,7 @@ router.get('/authenticate', access.unlogged, access.ajax, (req,res) => {
       // If token older than params.tokenExpire, do nothing
       if (Date.now() > user.loginTime + params.token.expire)
         res.json({errmsg: "Token expired"});
-      else
-      {
+      else {
         // Generate session token (if not exists) + destroy login token
         UserModel.trySetSessionToken(user.id, (token) => {
           res.cookie("token", token, {
diff --git a/server/sockets.js b/server/sockets.js
index b16135bd..7de7abb9 100644
--- a/server/sockets.js
+++ b/server/sockets.js
@@ -269,7 +269,7 @@ module.exports = function(wss) {
         case "notifynewgame":
           if (!!clients["/mygames"]) {
             obj.targets.forEach(t => {
-              const k = t.sid || idToSid[t.uid];
+              const k = t.sid || idToSid[t.id];
               if (!!clients["/mygames"][k]) {
                 Object.keys(clients["/mygames"][k]).forEach(x => {
                   send(
diff --git a/server/utils/access.js b/server/utils/access.js
index 36511dba..732353f2 100644
--- a/server/utils/access.js
+++ b/server/utils/access.js
@@ -10,22 +10,16 @@ module.exports =
       else next();
     };
     let loggedIn = undefined;
-    if (!req.cookies.token)
-    {
+    if (!req.cookies.token) {
       loggedIn = false;
       callback();
-    }
-    else
-    {
+    } else {
       UserModel.getOne("sessionToken", req.cookies.token, function(err, user) {
-        if (!!user)
-        {
+        if (!!user) {
           req.userId = user.id;
           req.userName = user.name;
           loggedIn = true;
-        }
-        else
-        {
+        } else {
           // Token in cookies presumably wrong: erase it
           res.clearCookie("token");
           loggedIn = false;
@@ -39,28 +33,25 @@ module.exports =
   unlogged: function(req, res, next) {
     // Just a quick heuristic, which should be enough
     const loggedIn = !!req.cookies.token;
-    if (loggedIn)
-      res.json({errmsg: "Error: try to delete cookies"});
+    if (loggedIn) res.json({errmsg: "Error: try to delete cookies"});
     else next();
   },
 
   // Prevent direct access to AJAX results
   ajax: function(req, res, next) {
-    if (!req.xhr)
-      res.json({errmsg: "Unauthorized access"});
+    if (!req.xhr) res.json({errmsg: "Unauthorized access"});
     else next();
   },
 
   // Check for errors before callback (continue page loading). TODO: better name.
   checkRequest: function(res, err, out, msg, cb) {
-    if (err)
-      res.json({errmsg: err.errmsg || err.toString()});
-    else if (!out
-      || (Array.isArray(out) && out.length == 0)
-      || (typeof out === "object" && Object.keys(out).length == 0))
-    {
+    if (!!err) res.json({errmsg: err.errmsg || err.toString()});
+    else if (
+      !out ||
+      (Array.isArray(out) && out.length == 0) ||
+      (typeof out === "object" && Object.keys(out).length == 0)
+    ) {
       res.json({errmsg: msg});
-    }
-    else cb();
-  },
+    } else cb();
+  }
 }
diff --git a/server/utils/database.js b/server/utils/database.js
index 2904f2ae..7c20d521 100644
--- a/server/utils/database.js
+++ b/server/utils/database.js
@@ -1,8 +1,7 @@
 const sqlite3 = require('sqlite3');
 const params = require("../config/parameters")
 
-if (params.env == "development")
-  sqlite3.verbose();
+if (params.env == "development") sqlite3.verbose();
 
 const DbPath = __dirname.replace("/utils", "/db/vchess.sqlite");
 const db = new sqlite3.Database(DbPath);
diff --git a/server/utils/mailer.js b/server/utils/mailer.js
index b0a0bace..df60e699 100644
--- a/server/utils/mailer.js
+++ b/server/utils/mailer.js
@@ -1,24 +1,21 @@
 const nodemailer = require('nodemailer');
 const params = require("../config/parameters");
 
-module.exports = function(from, to, subject, body, cb)
-{
+module.exports = function(from, to, subject, body, cb) {
   // Avoid the actual sending in development mode
-  if (params.env === 'development')
-  {
+  if (params.env === 'development') {
     console.log("New mail: from " + from + " / to " + to);
     console.log("Subject: " + subject);
     console.log(body);
-    if (!cb)
-      cb = (err) => { if (err) console.log(err); }
+    if (!cb) cb = (err) => { if (err) console.log(err); }
     cb();
     return;
   }
 
   // Production-only code from here:
 
-  if (!cb)
-    cb = () => {}; //default: do nothing (TODO: log somewhere)
+  // Default: do nothing (TODO: log somewhere)
+  if (!cb) cb = () => {};
 
   // Create reusable transporter object using the default SMTP transport
   const transporter = nodemailer.createTransport({
diff --git a/server/utils/tokenGenerator.js b/server/utils/tokenGenerator.js
index 2c21b4e5..d89b4ccf 100644
--- a/server/utils/tokenGenerator.js
+++ b/server/utils/tokenGenerator.js
@@ -1,14 +1,11 @@
-function randString()
-{
+function randString() {
   return Math.random().toString(36).substr(2); // remove `0.`
 }
 
-module.exports = function(tokenLength)
-{
+module.exports = function(tokenLength) {
   let res = "";
   // 10 = min length of a rand() string
-  let nbRands = Math.ceil(tokenLength/10);
-  for (let i = 0; i < nbRands; i++)
-    res += randString();
+  const nbRands = Math.ceil(tokenLength/10);
+  for (let i = 0; i < nbRands; i++) res += randString();
   return res.substr(0, tokenLength);
 }
-- 
2.44.0