deploy cicd
All checks were successful
Deploy to Production / deploy (push) Successful in 2m6s

This commit is contained in:
Đặng Minh Quang
2026-02-27 11:59:50 +07:00
parent 52dc0d818b
commit c3280dcb66
10 changed files with 275 additions and 31 deletions

View File

@@ -0,0 +1,90 @@
name: Deploy to Production
on:
push:
branches:
- main
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Deploy to server
run: |
echo "🚀 Deploying to /var/www/html/games"
echo "📁 Current directory: $(pwd)"
echo "📁 GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
# Read game info from readme.md
if [ -f "$GITHUB_WORKSPACE/readme.md" ]; then
GAME_TITLE=$(sed -n '1p' "$GITHUB_WORKSPACE/readme.md")
GAME_DESC=$(sed -n '2p' "$GITHUB_WORKSPACE/readme.md")
GAME_TYPE=$(sed -n '3p' "$GITHUB_WORKSPACE/readme.md")
else
GAME_TITLE="Untitled Game"
GAME_DESC="No description"
GAME_TYPE="quiz"
fi
# Create base directory
mkdir -p /var/www/html/games/
# Find and deploy all game folders (exclude source, .git, .gitea, etc.)
DEPLOYED_FOLDERS=""
DEPLOYED_URLS=""
for folder in $GITHUB_WORKSPACE/*/; do
folder_name=$(basename "$folder")
# Skip excluded folders
if [[ "$folder_name" == "source" ]] || \
[[ "$folder_name" == ".git" ]] || \
[[ "$folder_name" == ".gitea" ]] || \
[[ "$folder_name" == "node_modules" ]] || \
[[ "$folder_name" == "logs" ]] || \
[[ "$folder_name" == "uploads" ]]; then
echo "⏭️ Skipping: $folder_name"
continue
fi
# Deploy the folder
echo "📦 Deploying: $folder_name"
rsync -av --delete "$folder" "/var/www/html/games/$folder_name/"
DEPLOYED_FOLDERS="$DEPLOYED_FOLDERS $folder_name"
GAME_URL="https://senaai.tech/games/$folder_name/"
DEPLOYED_URLS="$DEPLOYED_URLS\n 🔗 $GAME_URL"
# Get thumbnail (first image found or default)
THUMBNAIL=$(find "$folder" -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" \) 2>/dev/null | head -n 1 || true)
if [ -z "$THUMBNAIL" ]; then
THUMBNAIL="https://senaai.tech/games/$folder_name/images/default.png"
else
THUMBNAIL="https://senaai.tech/games/$folder_name/$(basename "$THUMBNAIL")"
fi
# Submit game info to API
echo "📤 Submitting game info to API..."
curl --location 'http://senaai.tech:10000/api/games/save-with-check' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "title=$GAME_TITLE" \
--data-urlencode "description=$GAME_DESC" \
--data-urlencode "url=$GAME_URL" \
--data-urlencode "thumbnail=$THUMBNAIL" \
--data-urlencode "type=$GAME_TYPE" || true
echo ""
done
# Show deployment summary
echo ""
echo "=========================================="
echo "🎉 Deployment Completed Successfully!"
echo "=========================================="
echo ""
echo "📍 Deployed URLs:"
echo -e "$DEPLOYED_URLS"
echo ""
echo "=========================================="

View File

@@ -19347,6 +19347,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
this.bgMusicPlaying = false; this.bgMusicPlaying = false;
this.bgMusicPaused = false; this.bgMusicPaused = false;
this.calculatedPositions = []; this.calculatedPositions = [];
this.customData = {
data1: "",
data2: "",
data3: "",
data4: "",
data5: ""
};
this.lastMessageData = null;
this.lastSenderUUID = "";
}; };
instanceProto.onDestroy = function () { instanceProto.onDestroy = function () {
if (this.sdk) { if (this.sdk) {
@@ -19382,6 +19391,9 @@ cr.plugins_.SenaPlugin = function (runtime) {
Cnds.prototype.OnPairWrong = function () { Cnds.prototype.OnPairWrong = function () {
return true; return true;
}; };
Cnds.prototype.OnMessage = function () {
return true;
};
pluginProto.cnds = new Cnds(); pluginProto.cnds = new Cnds();
function Acts() {} function Acts() {}
Acts.prototype.Load = function () { Acts.prototype.Load = function () {
@@ -19389,6 +19401,16 @@ cr.plugins_.SenaPlugin = function (runtime) {
var gameCode = this.properties[0] || "G2510S1T30"; var gameCode = this.properties[0] || "G2510S1T30";
if (window["SenaSDK"]) { if (window["SenaSDK"]) {
this.sdk = new window["SenaSDK"](gameCode); this.sdk = new window["SenaSDK"](gameCode);
this.sdk.onCustomMessage = function (data, senderUuid) {
console.log('Runtime received custom message from:', senderUuid);
console.log('Data:', data);
self.lastMessageData = data;
self.lastSenderUUID = senderUuid;
window["SenaTrigger"].runtime.trigger(
cr.plugins_.SenaPlugin.prototype.cnds.OnMessage,
window["SenaTrigger"]
);
};
this.sdk.load(function (success) { this.sdk.load(function (success) {
if (success) { if (success) {
console.log("SDK loaded successfully"); console.log("SDK loaded successfully");
@@ -19512,16 +19534,46 @@ cr.plugins_.SenaPlugin = function (runtime) {
objectWidth, objectWidth,
margin, margin,
maxWidth, maxWidth,
rowBreak,
rowGap,
type,
groupGap
) { ) {
var self = this;
this.calculatedPositions = []; this.calculatedPositions = [];
var totalWidth = count * objectWidth + (count - 1) * margin; if (count <= 0) return;
var startX = (maxWidth - totalWidth) / 2; var rows = [];
for (var i = 0; i < count; i++) { if (rowBreak > 0) {
var posX = startX + i * (objectWidth + margin) + objectWidth / 2; for (var i = 0; i < count; i += rowBreak) {
this.calculatedPositions.push(posX); rows.push(Math.min(rowBreak, count - i));
}
} else {
if (count <= 5) {
rows.push(count);
} else {
var top = Math.ceil((count + 1) / 2);
var bottom = count - top;
rows.push(top);
rows.push(bottom);
}
} }
console.log("Calculated positions:", this.calculatedPositions); var baseY = 0;
if (type === "word") {
baseY = groupGap || (rowGap * rows.length); // word always below slot
}
var index = 0;
for (var r = 0; r < rows.length; r++) {
var itemsInRow = rows[r];
var rowWidth = itemsInRow * objectWidth + (itemsInRow - 1) * margin;
var startX = (maxWidth - rowWidth) / 2;
for (var i = 0; i < itemsInRow; i++) {
this.calculatedPositions.push({
x: startX + i * (objectWidth + margin) + objectWidth / 2,
y: baseY + r * rowGap
});
index++;
}
}
console.log("Calculated positions (multi-row):", this.calculatedPositions);
}; };
Acts.prototype.LoadLevelG5 = function (levelIndex) { Acts.prototype.LoadLevelG5 = function (levelIndex) {
if (this.sdk && this.sdk.loadLevelG5) { if (this.sdk && this.sdk.loadLevelG5) {
@@ -19546,6 +19598,29 @@ cr.plugins_.SenaPlugin = function (runtime) {
}); });
} }
}; };
Acts.prototype.SetData = function (data1, data2, data3, data4, data5) {
this.customData.data1 = data1 || "";
this.customData.data2 = data2 || "";
this.customData.data3 = data3 || "";
this.customData.data4 = data4 || "";
this.customData.data5 = data5 || "";
console.log('Custom data set:', this.customData);
};
Acts.prototype.PostMessage = function () {
if (this.sdk && this.sdk.sendMessageToParent) {
var dataToSend = {
data1: this.customData.data1,
data2: this.customData.data2,
data3: this.customData.data3,
data4: this.customData.data4,
data5: this.customData.data5
};
this.sdk.sendMessageToParent(dataToSend);
console.log('Posted message to parent:', dataToSend);
} else {
console.error('SDK not initialized or sendMessageToParent not available');
}
};
pluginProto.acts = new Acts(); pluginProto.acts = new Acts();
function Exps() {} function Exps() {}
Exps.prototype.getQuestionValue = function (ret) { Exps.prototype.getQuestionValue = function (ret) {
@@ -19687,12 +19762,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
} }
}; };
Exps.prototype.getPosXbyIndex = function (ret, index) { Exps.prototype.getPosXbyIndex = function (ret, index) {
if ( if (this.calculatedPositions[index]) {
this.calculatedPositions && ret.set_float(this.calculatedPositions[index].x);
index >= 0 && } else {
index < this.calculatedPositions.length ret.set_float(0);
) { }
ret.set_float(this.calculatedPositions[index]); };
Exps.prototype.getPosYbyIndex = function (ret, index) {
if (this.calculatedPositions[index]) {
ret.set_float(this.calculatedPositions[index].y);
} else { } else {
ret.set_float(0); ret.set_float(0);
} }
@@ -19754,7 +19832,11 @@ cr.plugins_.SenaPlugin = function (runtime) {
} }
}; };
Exps.prototype.GetCardType = function (ret, index) { Exps.prototype.GetCardType = function (ret, index) {
ret.set_string(this.sdk.getCardType(index)); if (this.sdk && this.sdk.getCardType) {
ret.set_string(this.sdk.getCardType(index));
} else {
ret.set_string("");
}
}; };
Exps.prototype.getCardID = function (ret, index) { Exps.prototype.getCardID = function (ret, index) {
if (this.sdk && this.sdk.getCardID) { if (this.sdk && this.sdk.getCardID) {
@@ -19763,6 +19845,24 @@ cr.plugins_.SenaPlugin = function (runtime) {
ret.set_string(""); ret.set_string("");
} }
}; };
Exps.prototype.getData = function (ret, dataIndex) {
var dataKey = "data" + dataIndex;
if (this.customData && this.customData[dataKey] !== undefined) {
ret.set_string(this.customData[dataKey]);
} else {
ret.set_string("");
}
};
Exps.prototype.getLastSenderUUID = function (ret) {
ret.set_string(this.lastSenderUUID || "");
};
Exps.prototype.getLastMessageJSON = function (ret) {
if (this.lastMessageData) {
ret.set_string(JSON.stringify(this.lastMessageData));
} else {
ret.set_string("{}");
}
};
pluginProto.exps = new Exps(); pluginProto.exps = new Exps();
})(); })();
; ;

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -81,7 +81,7 @@
<!-- Construct 2 exported games require jQuery. --> <!-- Construct 2 exported games require jQuery. -->
<script src="jquery-3.4.1.min.js"></script> <script src="jquery-3.4.1.min.js"></script>
<script src="tdv_sdk.js"></script>
<script src="sena_sdk.js"></script> <script src="sena_sdk.js"></script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -1,5 +1,5 @@
{ {
"version": 1770529725, "version": 1772168306,
"fileList": [ "fileList": [
"data.js", "data.js",
"c2runtime.js", "c2runtime.js",
@@ -21,7 +21,6 @@
"icon-128.png", "icon-128.png",
"icon-256.png", "icon-256.png",
"loading-logo.png", "loading-logo.png",
"bg.mp4",
"sena_sdk.js" "sena_sdk.js"
] ]
} }

View File

@@ -50,6 +50,7 @@ function SenaSDK(gid = "G2510S1T30") {
// 'preview' - Timeout 5s rồi fallback sample (testing với data thật) // 'preview' - Timeout 5s rồi fallback sample (testing với data thật)
// 'dev' - Load sample ngay lập tức (development) // 'dev' - Load sample ngay lập tức (development)
this.mode = "preview"; // Default mode this.mode = "preview"; // Default mode
this.role = "student"; // Default role
} }
/** /**
@@ -118,6 +119,7 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
console.warn("⚠️ Sena SDK: tdv_sdk not found, storing data locally"); console.warn("⚠️ Sena SDK: tdv_sdk not found, storing data locally");
self.data = inputJson.data; self.data = inputJson.data;
self.correctAnswer = inputJson.answer; self.correctAnswer = inputJson.answer;
self._parseGameCode(); // Thêm dòng này để SDK đọc đuôi thời gian T30
if (callback) callback(true); if (callback) callback(true);
} }
return true; return true;
@@ -373,9 +375,15 @@ SenaSDK.prototype.load = function (callback, template = "G2510S1T30") {
self.mode = urlMode.toLowerCase(); self.mode = urlMode.toLowerCase();
} }
// THÊM 2 DÒNG NÀY: Lấy role từ URL
const urlRole = urlParams.get("role");
if (urlRole) self.role = urlRole.toLowerCase();
console.log( console.log(
"🎮 Sena SDK: Mode =", "🎮 Sena SDK: Mode =",
self.mode.toUpperCase(), self.mode.toUpperCase(),
"| Role =",
self.role || "student",
"| GameCode =", "| GameCode =",
self.gameCode, self.gameCode,
); );
@@ -878,14 +886,12 @@ SenaSDK.prototype.guide = function () {
return guide; return guide;
}; };
/** /**
* Get the question text * Get the question text/url
* @returns {string} Question or request text * @returns {string} Question, request text, or URL
*/ */
SenaSDK.prototype.getQuestionValue = function () { SenaSDK.prototype.getQuestionValue = function () {
var q = String(this.data.question || "").trim(); var q = String(this.data.question || "").trim();
if (q.toLowerCase().startsWith("http")) { // Đã bỏ chặn URL để có thể lấy link ảnh/audio
return "";
}
return q; return q;
}; };
/** /**
@@ -1138,10 +1144,13 @@ SenaSDK.prototype.end = function (answer, callback) {
} }
} }
// 3. COMPARE (UNORDERED - So sánh không cần thứ tự) // 3. COMPARE
// Sort cả 2 mảng để so sánh tập hợp // Nếu là Game Type 2 (Sort) thì giữ nguyên thứ tự, nếu không thì sort (unordered)
const sortedUser = [...userAnswers].sort(); const isStrictOrder = self.gameType === 2;
const sortedCorrect = [...correctAnswers].sort(); const finalUser = isStrictOrder ? [...userAnswers] : [...userAnswers].sort();
const finalCorrect = isStrictOrder
? [...correctAnswers]
: [...correctAnswers].sort();
let isCorrect = false; let isCorrect = false;
@@ -1155,9 +1164,9 @@ SenaSDK.prototype.end = function (answer, callback) {
} }
}; };
if (sortedUser.length === sortedCorrect.length) { if (finalUser.length === finalCorrect.length) {
isCorrect = sortedUser.every((uVal, index) => { isCorrect = finalUser.every((uVal, index) => {
let cVal = sortedCorrect[index]; let cVal = finalCorrect[index];
if (uVal === cVal) return true; if (uVal === cVal) return true;
// Fuzzy match cho URL (so sánh tên file ảnh) // Fuzzy match cho URL (so sánh tên file ảnh)
if (uVal.startsWith("http") || cVal.startsWith("http")) { if (uVal.startsWith("http") || cVal.startsWith("http")) {
@@ -1170,7 +1179,12 @@ SenaSDK.prototype.end = function (answer, callback) {
// ----------------------------------------------------------- // -----------------------------------------------------------
// [BƯỚC 1] Kiểm tra Time Limit TRƯỚC (Sửa biến isCorrect) // [BƯỚC 1] Kiểm tra Time Limit TRƯỚC (Sửa biến isCorrect)
// ----------------------------------------------------------- // -----------------------------------------------------------
if (self.timeLimit > 0 && duration > self.timeLimit) { // THÊM ĐIỀU KIỆN: Nếu là teacher thì bỏ qua kiểm tra thời gian
if (
self.role !== "teacher" &&
self.timeLimit > 0 &&
duration > self.timeLimit
) {
isCorrect = false; // CHỈ sửa biến boolean, KHÔNG gọi result.isCorrect isCorrect = false; // CHỈ sửa biến boolean, KHÔNG gọi result.isCorrect
console.log("🎮 Sena SDK: Time Limit Exceeded -> Result set to False"); console.log("🎮 Sena SDK: Time Limit Exceeded -> Result set to False");
} }
@@ -1373,6 +1387,17 @@ SenaSDK.prototype.registerPostMessageListener = function () {
} }
break; break;
case "SEQUENCE_SYNC":
console.log("📥 Sena SDK: Received SEQUENCE_SYNC", event.data);
if (event.data.uuid === self.uuid) {
console.log("🔄 Sena SDK: Own message echoed back, processing...");
}
if (typeof self.onCustomMessage === "function") {
self.onCustomMessage(event.data.data, event.data.uuid);
}
break;
case "SDK_ERROR": case "SDK_ERROR":
// Server gửi error // Server gửi error
console.error("❌ Sena SDK: Received SDK_ERROR", event.data.payload); console.error("❌ Sena SDK: Received SDK_ERROR", event.data.payload);
@@ -1855,6 +1880,33 @@ SenaSDK.prototype.checkPair = function (idx1, idx2, callback) {
if (callback) callback(isMatch); if (callback) callback(isMatch);
}; };
/**
* [NEW v2.2] Gửi Custom Data lên Parent Window
* @param {Object} data - Object chứa 5 trường data1 -> data5
*/
SenaSDK.prototype.sendMessageToParent = function (data) {
let self = this;
// Tự động tạo UUID cho session nếu chưa có
if (!self.uuid) {
self.uuid =
"session-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
}
// Đóng gói payload đúng chuẩn tài liệu v2.2
let payload = {
type: "SEQUENCE_SYNC",
uuid: self.uuid,
data: data,
timestamp: Date.now(),
};
console.log("📤 Sena SDK: Sending SEQUENCE_SYNC to parent:", payload);
// Gửi lên Parent Window (Backend/Iframe parent)
window.parent.postMessage(payload, "*");
};
if (typeof module !== "undefined" && module.exports) { if (typeof module !== "undefined" && module.exports) {
module.exports = SenaSDK; module.exports = SenaSDK;
} else if (typeof define === "function" && define.amd) { } else if (typeof define === "function" && define.amd) {

3
readme.md Normal file
View File

@@ -0,0 +1,3 @@
Sequence Word TextOnly
Mô tả của trò chơi
Sequence

Binary file not shown.