Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions chessServer/src/api/completeActivity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Sends a PUT request to the middleware to mark an activity as complete.
* @param {string} username - The student's username
* @param {string} credentials - The student's auth token (JWT)
* @param {string} activityName - The activity identifier (e.g. 'captureQueen')
* @returns {Response} The fetch response
*/
const completeActivity = async (username, credentials, activityName) => {
const url = `${process.env.MIDDLEWARE_URL}/activities/${username}/activity`;
const response = await fetch(url, {
method: "PUT",
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${credentials}`,
},
body: JSON.stringify({ activityName }),
});
return response;
};

module.exports = { completeActivity };
50 changes: 28 additions & 22 deletions chessServer/src/managers/EventHandlers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const GameManager = require("./GameManager");
const { completeActivity } = require("../api/completeActivity");

const gameManager = new GameManager();

Expand Down Expand Up @@ -45,15 +46,25 @@ const registerSocketHandlers = (socket, io) => {
socket.on("newPuzzle", (msg) => {
try {
const parsed = JSON.parse(msg);
console.log('data',parsed, msg);
// create the new puzzle
console.log('data', parsed, msg);
gameManager.createOrJoinPuzzle({
student: parsed.student,
mentor: parsed.mentor,
role: parsed.role,
socketId: socket.id,
credentials: parsed.credentials,
}, io);

// Student joining a puzzle session with their mentor counts as attending a session
if (parsed.role === "student" && parsed.credentials && parsed.student) {
completeActivity(parsed.student, parsed.credentials, "attendSession")
.then(r => {
if (r.status === 200) {
socket.emit("completeActivity");
}
})
.catch(e => console.log("attendSession error:", e));
}
}
catch (err) {
socket.emit("gameerror", err.message);
Expand All @@ -72,38 +83,33 @@ const registerSocketHandlers = (socket, io) => {
const state = res.result;
gameManager.broadcastBoardState(res.result, io);
console.log('Move: ', res);
if(!computerMove && credentials) {
const activityEvents = res.activityEvents;
// Gap 5: guard username too — it can be null on early moves before startRecording resolves
if (!computerMove && credentials && username) {
const activityEvents = res.activityEvents;
if (activityEvents && activityEvents.length > 0) {
const studentId = state.studentId;
const studentId = state.studentId;
const payload = {
activities: activityEvents,
activities: activityEvents,
lastMove: { from, to, san: state.move?.san }
};
console.log('Payload', payload);
// Gap 1: resolve the student socket so we always notify the student, not the mover
const studentSocket = io.sockets.sockets.get(studentId);
//console.log('student socket', studentSocket);
if (studentSocket) {
try {
console.log('route:', `${process.env.MIDDLEWARE_URL}/activities/${username}/activity`);
const response = await fetch(`${process.env.MIDDLEWARE_URL}/activities/${username}/activity`, {
method: "PUT",
headers: {
'Content-Type': 'application/json',
'Authentication' : `Bearer ${credentials}`,
},
body: JSON.stringify({
activityName: payload.activities[0].name,
})
});
console.log('response',response);
socket.emit("completeActivity");
for (const event of payload.activities) {
const response = await completeActivity(username, credentials, event.name);
console.log('completeActivity response', response.status);
// Gap 2: only notify on confirmed success; Gap 1: emit to studentSocket
if (response.status === 200) {
studentSocket.emit("completeActivity");
}
}
} catch (e) {
console.log('Error: ', e);
console.log('Error: ', e);
}
}
}

}

/*
Expand Down
9 changes: 9 additions & 0 deletions chessServer/src/managers/GameManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ class GameManager {
at: Date.now()
});
}

// Game over (checkmate or stalemate) — credit playMatch
if (board.isGameOver()) {
activityEvents.push({
name: "playMatch",
meta: { san: moveResult.san, result: board.isCheckmate() ? "checkmate" : "draw" },
at: Date.now()
});
}
//console.log(activityEvents);
//console.log('student info',game.student);
return {
Expand Down
7 changes: 3 additions & 4 deletions middlewareNode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"passport": "^0.7.0",
"passport-jwt": "^4.0.0",
"request-ip": "^3.3.0",
"uuid": "^8.3.2"
"uuid": "^8.3.2",
"node-schedule": "^2.1.1"
},
"scripts": {
"start": "nodemon src/server.js"
Expand All @@ -29,7 +30,5 @@
"volta": {
"node": "18.20.8"
},
"devDependencies": {
"node-schedule": "^2.1.1"
}
"devDependencies": {}
}
40 changes: 40 additions & 0 deletions middlewareNode/src/models/defaultLessons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Default lesson progress entries for newly registered users.
* Matches the canonical lesson list used across the platform.
* All lessons start at lessonNumber: 0 (no progress).
*
* Used as the Mongoose default for users.lessonsCompleted.
* Must stay in sync with the lesson names in the newLessons collection.
*/
module.exports = [
{ piece: 'Piece Checkmate 1 Basic checkmates', lessonNumber: 0 },
{ piece: 'Checkmate Pattern 1 Recognize the patterns', lessonNumber: 0 },
{ piece: 'Checkmate Pattern 2 Recognize the patterns', lessonNumber: 0 },
{ piece: 'Checkmate Pattern 3 Recognize the patterns', lessonNumber: 0 },
{ piece: 'Checkmate Pattern 4 Recognize the patterns', lessonNumber: 0 },
{ piece: 'Piece checkmates 2 Challenging checkmates', lessonNumber: 0 },
{ piece: 'Knight and Bishop Mate interactive lesson', lessonNumber: 0 },
{ piece: 'The Pin Pin it to win it', lessonNumber: 0 },
{ piece: 'The Skewer Yum - Skewers!', lessonNumber: 0 },
{ piece: 'The Fork Use the fork, Luke', lessonNumber: 0 },
{ piece: 'Discovered Attacks Including discovered checks', lessonNumber: 0 },
{ piece: 'Double Check A very powerfull tactic', lessonNumber: 0 },
{ piece: 'Overloaded Pieces They have too much work', lessonNumber: 0 },
{ piece: 'Zwischenzug In-between moves', lessonNumber: 0 },
{ piece: 'X-Ray Attacking through an enemy piece', lessonNumber: 0 },
{ piece: 'Zugzwang Being forced to move', lessonNumber: 0 },
{ piece: 'Interference Interpose a piece to great effect', lessonNumber: 0 },
{ piece: 'Greek Gift Study the greek gift scrifice', lessonNumber: 0 },
{ piece: 'Deflection Distracting a defender', lessonNumber: 0 },
{ piece: 'Attraction Lure a piece to bad square', lessonNumber: 0 },
{ piece: 'Underpromotion Promote - but not to a queen!', lessonNumber: 0 },
{ piece: 'Desperado A piece is lost, but it can still help',lessonNumber: 0 },
{ piece: 'Counter Check Respond to a check with a check', lessonNumber: 0 },
{ piece: 'Undermining Remove the defending piece', lessonNumber: 0 },
{ piece: 'Clearance Get out of the way!', lessonNumber: 0 },
{ piece: 'Key Squares Reach the key square', lessonNumber: 0 },
{ piece: 'Opposition take the opposition', lessonNumber: 0 },
{ piece: '7th-Rank Rook Pawn Versus a Queen', lessonNumber: 0 },
{ piece: '7th-Rank Rook Pawn And Passive Rook vs Rook', lessonNumber: 0 },
{ piece: 'Basic Rook Endgames Lucena and Philidor', lessonNumber: 0 },
];
41 changes: 32 additions & 9 deletions middlewareNode/src/routes/activities.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

const config = require("config");
const express = require('express');
const passport = require("passport");
const router = express.Router({mergeParams: true});
const { MongoClient, ObjectId } = require('mongodb');
require('dotenv').config();
Expand Down Expand Up @@ -65,6 +66,11 @@ router.get("/:username", async (req, res) => {
const userActivities = await activities.findOne(
{ userId }, { projection: {activities: 1, _id: 0}}
);
// Return a safe empty structure if no activities document exists yet
if (!userActivities) {
return res.status(200).json({ activities: { activities: [] } });
}
res.setHeader('Cache-Control', 'no-store');
return res.status(200).json({activities: userActivities});

} catch (err) {
Expand Down Expand Up @@ -93,28 +99,45 @@ router.get("/:username/dates", async (req, res) => {
});


router.put("/:username/activity", async (req, res) => {
router.put("/:username/activity", passport.authenticate("jwt", { session: false }), async (req, res) => {
try {
const db = await getDb();
const { username } = req.params;
const username = req.user.username;
const { activityName } = req.body;
const userId = await getUserId(db, username);
if(!userId) {
return res.status(404).json({error:'User not found'});
}
const activities = db.collection("activities");
const activityIncomplete = await activities.findOne(
{ userId, "activities.name": activityName },
{ activities: {$elemMatch: { name: activityName }}, _id:0},
const alreadyCompleted = await activities.findOne(
{ userId, activities: { $elemMatch: { name: activityName, completed: true } } }
);
if(activityIncomplete) {
console.log('incomplete activity: ', activityName);
if (alreadyCompleted) {
return res.status(200).json({ message: 'already completed' });
}
await activities.updateOne(
const updateResult = await activities.updateOne(
{ userId, "activities.name": activityName },
{ $set: { "activities.$.completed": true } }
);
return res.status(200).json({message:'success'});
if (updateResult.modifiedCount === 0) {
// Activity not in today's random selection — add it as completed
const activityTypes = db.collection("activityTypes");
const activityType = await activityTypes.findOne({ _id: activityName });
const type = activityType?.type || "session";
await activities.updateOne(
{ userId },
{ $push: { activities: { name: activityName, type, completed: true } } }
);
}
const updatedDoc = await activities.findOne({ userId });
const allDone = updatedDoc.activities.every(activity => activity.completed);
if (allDone) {
await activities.updateOne(
{ userId },
{ $push: { completedDates: new Date() } }
);
}
return res.status(200).json({ message: 'success' });
} catch (err) {
console.error('Error updating activities: ', err);
return res.status(500).json({error: 'Server error'});
Expand Down
18 changes: 18 additions & 0 deletions middlewareNode/src/routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ router.post(
});
await mainUser.save();

// Create an activities document for students who register directly
if (role === "student") {
const newActivities = await selectActivities();
const activitiesEntry = new Activities({
userId: mainUser.id,
activities: newActivities,
completedDates: [],
});
await activitiesEntry.save();
}

res.status(200).json("Added users");
} catch (error) {
console.error(error.message);
Expand Down Expand Up @@ -231,6 +242,13 @@ router.post(
recordingList: [],
});
await newStudent.save();
const newActivities = await selectActivities();
const activitiesEntry = new Activities({
userId: newStudent.id,
activities: newActivities,
completedDates: [],
});
await activitiesEntry.save();
return res.status(200).json("Added student");
} catch (error) {
console.error(error.message);
Expand Down
10 changes: 6 additions & 4 deletions middlewareNode/src/scheduler/activitiesScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
const schedule = require("node-schedule");
const config = require("config");
const { MongoClient } = require('mongodb');
const { selectActivities } = require("../utils/activities");
const { getActivityTypes, selectActivitiesFromList } = require("../utils/activities");
require('dotenv').config();

// Cache database client to prevent repeated connections
Expand Down Expand Up @@ -45,11 +45,13 @@ const activityScheduler = schedule.scheduleJob('0 0 * * *',

// Get all user activity documents
const activitiesArray = await (activities.find({})).toArray();


// Fetch activity types once and reuse for every user
const activityTypes = await getActivityTypes();

// Reset activities for each user
for(const userActivity of activitiesArray) {
// Generate new random activities for the day
const newActivities = await selectActivities();
const newActivities = selectActivitiesFromList(activityTypes);
const id = userActivity._id;

// Update user's activities in database
Expand Down
62 changes: 36 additions & 26 deletions middlewareNode/src/utils/activities.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,38 +25,48 @@ async function getDb() {
}

/**
* Selects 4 random unique activities for a user's daily challenges
*
* Randomly picks activities from the activityTypes collection without duplicates.
* Each activity is initialized with completed: false.
*
* @returns {Array} Array of 4 activity objects with name, type, and completed fields
* Fetches all available activity types from the database.
* Call once and pass the result to selectActivitiesFromList to avoid repeated queries.
* @returns {Array} Array of activity type documents from MongoDB
*/
const selectActivities = async () => {
const getActivityTypes = async () => {
const db = await getDb();

// Fetch all available activity types
const activityList = await (db.collection("activityTypes").find({})).toArray();
const chosenActivites = [];
return (db.collection("activityTypes").find({})).toArray();
};

/**
* Selects 4 random unique activities from a pre-fetched activity list.
* Pure function — no DB access, safe to call in a loop.
* @param {Array} activityList - Array of activity type documents
* @returns {Array} Array of 4 activity objects with name, type, and completed fields
*/
const selectActivitiesFromList = (activityList) => {
const chosenIds = [];
const newActivities = [];

// Select 4 unique random activities

while (newActivities.length < 4) {
const activity = {};

// Randomly select an activity
const selectedActivity = activityList[Math.floor(Math.random() * activityList.length)];
// Only add if not already selected (avoid duplicates)
if(!chosenActivites.includes(selectedActivity._id)) {
chosenActivites.push(selectedActivity._id);
activity.name = selectedActivity._id;
activity.type = selectedActivity.type;
activity.completed = false;
newActivities.push(activity);

if (!chosenIds.includes(selectedActivity._id)) {
chosenIds.push(selectedActivity._id);
newActivities.push({
name: selectedActivity._id,
type: selectedActivity.type,
completed: false,
});
}
}
return newActivities;
}
};

/**
* Convenience wrapper — queries DB then selects 4 activities.
* Use this for one-off calls. Use getActivityTypes + selectActivitiesFromList in loops.
* @returns {Array} Array of 4 activity objects
*/
const selectActivities = async () => {
const activityList = await getActivityTypes();
return selectActivitiesFromList(activityList);
};

module.exports = { selectActivities };
module.exports = { selectActivities, getActivityTypes, selectActivitiesFromList };
Loading
Loading