Compare commits

...

4 commits

Author SHA1 Message Date
Pakin
45b2470fb4 Add profile slot 17
Some checks failed
CI for GGS-CRON / build-docker (push) Successful in 1m27s
CI for GGS-CRON / build-and-test (push) Has been cancelled
- price times 4

Signed-off-by: Pakin <pakin.t@forth.co.th>
2025-11-17 11:15:19 +07:00
Pakin
9df9d1b75f add NPROC for communication with updater 2025-09-02 17:47:30 +07:00
6ccc186e97 Updated workflow 2025-08-11 17:42:31 +07:00
Pakin
8e5421ed17 add dashboard for testing
All checks were successful
CI for GGS-CRON / build-docker (push) Successful in 1m38s
CI for GGS-CRON / build-and-test (push) Successful in 3m22s
2025-08-11 17:38:45 +07:00
13 changed files with 327 additions and 16 deletions

View file

@ -1,8 +1,6 @@
name: CI for GGS-CRON
on:
push:
branches:
- master
tags:
- 'v*'
- 'release-*'

35
app.js
View file

@ -15,11 +15,17 @@ const {
getTestSpreadSheet,
GoogleFunctions,
PluginsManager,
test_drive
} = require("./lib/common");
const { SyncText } = require("./lib/sync_text");
const { startup } = require("./lib/zmq");
const { CronJobs } = require("./cron-jobs");
const { google } = require("googleapis");
const { EventEmitter } = require("stream");
const fs = require('fs/promises');
// const nproc = require('./lib/nproc');
require("dotenv").config();
@ -36,7 +42,7 @@ app
.use(express.json())
.use(express.urlencoded({ extended: false }))
.use(cookieParser())
.use(express.static(path.join(__dirname, "public")));
.use(express.static('public'));
app.use("/", indexRouter);
app.use("/users", usersRouter);
@ -53,6 +59,7 @@ var cronTasks =
const auth = GoogleFunctions.auth();
const sheet = GoogleFunctions.SpreadSheets(auth);
const drive = GoogleFunctions.Drive(auth);
// let heartbeatTask = CronJobs.doEveryMinute(() => {
// Log.debug(`All running => ${JSON.stringify(cronTasks)}`);
@ -84,12 +91,33 @@ const sheet = GoogleFunctions.SpreadSheets(auth);
// GoogleFunctions.getPriceSlotValues(sheet, "tha");
// v3 !!!!
GoogleFunctions.syncProfilePrice(sheet, "tha", false);
// GoogleFunctions.syncProfilePrice(sheet, "tha", false);
//
// Test Taobin SyncText
// SyncText.run(sheet, "uae", false);
// Test drive
// test_drive(drive);
// startup();
//
//
// let client = new nproc.NprocClient("127.0.0.1:36540", () => {
// client.subscribe("self");
// client.publish("self", {
// msg: "test"
// });
// if(client.history.length > 1){
// Log.info("connected!");
// }
// }, () => {
// Log.debug("closed connection");
// });
const pm = new PluginsManager(cronTasks, CronJobs);
pm.load();
@ -109,6 +137,9 @@ for (let scriptName of Object.keys(pm.scripts)) {
endpointMap = undefined; // reset
}
// const filePath = new URL('./package.json', import.meta.url);
// const contents = await readFile(filePath, { encoding: 'utf8' });
// pm.loop()
// catch 404 and forward to error handler

View file

@ -181,6 +181,8 @@ function getSlotFunctionByIndex(index) {
return (price) => "HIDE";
case 9:
return (price) => price * 0.60;
case 17:
return (price) => price * 4.0;
default:
return (price) => price;
}
@ -200,6 +202,7 @@ const ProfilePrice = {
changelogs: [
"2/4/25 initialized version 2, fix timeout on trigger",
"8/4/25 initialized version 3, express server",
"17/11/25 add slot 17 price times 4"
],
maximum_slots: MAXIMUM_SLOTS,
};
@ -293,7 +296,8 @@ function saveJsonToFile(filename, content) {
// GOOGLE
// ======================================================================
const { google, sheets_v4 } = require("googleapis");
const { google, sheets_v4, drive_v3 } = require("googleapis");
const { GoogleAuth } = require("google-auth-library");
function authorize() {
const oauthClient = new google.auth.GoogleAuth({
@ -301,7 +305,7 @@ function authorize() {
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/gm, "\n"),
},
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
scopes: ["https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/drive"],
});
return oauthClient;
}
@ -325,6 +329,7 @@ function getCountrySpreadSheetId(cnt) {
const GoogleFunctions = {
auth: authorize,
SpreadSheets: (auth) => google.sheets({ version: "v4", auth }),
Drive: (auth) => google.drive({ version: 'v3', auth }),
GetCountrySpreadSheet: getCountrySpreadSheetById,
getAllSheetNamesByCountry: getAllSheetNamesByCountry,
getCountrySheetByName: getCountrySheetByName,
@ -1282,6 +1287,40 @@ async function _finalizeSyncProfilePrice(
}
}
// ======================================================================
// DRIVE
// ======================================================================
/**
* @param {drive_v3.Drive} drive instance
*/
async function test_drive(drive){
try {
await drive.files.create({
requestBody: {
name: "test",
mimeType: "text/plain",
parents: [process.env.TAOBIN_ADMIN_SERVER_DRIVE_FOLDER]
},
media: {
mimeType: "text/plain",
body: "Hello from js"
}
}).then((x) => Log.info(`Created file response: ${JSON.parse(x)}`));
} catch(error){
Log.err(`Error test drive: ${error}`);
return JSON.stringify({
error: error
});
}
return JSON.stringify({
status: "success"
});
}
// special
// ======================================================================
@ -1406,4 +1445,5 @@ module.exports = {
getCountrySheetByName,
diff2DArraysCustom,
saveJsonToFile,
test_drive
};

85
lib/nproc.js Normal file
View file

@ -0,0 +1,85 @@
const net = require('net');
// TODO: must change to read by env
const API_KEY = Buffer.from('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
function frame(msgType, topicStr, bodyBuf) {
const topic = Buffer.from(topicStr);
const payloadLen = 1 + 2 + 4 + topic.length + bodyBuf.length;
const totalBytes = payloadLen + 32;
const buf = Buffer.alloc(4 + payloadLen + 32);
buf.writeUInt32BE(totalBytes, 0);
buf.writeUInt8(msgType, 4);
buf.writeUInt16BE(topic.length, 5);
buf.writeUInt32BE(bodyBuf.length, 7);
topic.copy(buf, 11);
bodyBuf.copy(buf, 11 + topic.length);
API_KEY.copy(buf, 4 + payloadLen);
return buf;
}
class NprocClient {
MESSAGE_TYPE = {
SUB: 1,
UNSUB: 2,
PUB: 3,
PING: 4
};
constructor(
addr,
onConnect,
onClose
){
let _addr = addr.toString().split(":");
let host = _addr[0];
let port = parseInt(_addr[1]);
this.client = net.createConnection({
host: host,
port: port
}, () => onConnect());
this.history = [];
this.acc = Buffer.alloc(0);
this.client.on('data', chunk => {
this.acc = Buffer.concat([this.acc, chunk]);
while(this.acc.length >= 4){
const total = this.acc.readUInt32BE(0);
if (this.acc.length < 4 + total) break;
const frm = this.acc.subarray(0, 4 + total);
this.acc = this.acc.subarray(4 + total);
const msgType = frm.readUInt8(4);
const topicLen = frm.readUInt16BE(5);
const bodyLen = frm.readUInt32BE(7);
const topic = frm.subarray(11, 11 + topicLen).toString();
const body = frm.subarray(11 + topicLen, 11 + topicLen + bodyLen);
let res = {
msgType, topic, body: body.toString()
};
console.log(`get msg! ${JSON.stringify(res)}`);
this.history.push(res);
}
});
this.client.on('close', () => onClose());
}
subscribe(topic){
this.client.write(frame(this.MESSAGE_TYPE.SUB, topic, Buffer.alloc(0)));
}
publish(topic, payload){
this.client.write(frame(this.MESSAGE_TYPE.PUB, topic, Buffer.from(JSON.stringify(payload))));
}
// TODO: unsub
// TODO: ping
}
module.exports = {
NprocClient
};

0
lib/package_manager.js Normal file
View file

28
lib/zmq.js Normal file
View file

@ -0,0 +1,28 @@
const zmq = require('zeromq');
const uuid = require('uuid');
/**
* Publish simple message to server to notify service is ready.
*/
async function startup(){
const pub = new zmq.Publisher();
await pub.connect("tcp://127.0.0.1:36541");
const msg = {
id: uuid.v4(),
topic: "news",
message_type: "Data",
data: { content: "GGS Ready!" },
timestamp: Math.floor(Date.now() / 1000)
};
await pub.send(["news", JSON.stringify(msg)]);
console.log("startup send!")
pub.close();
}
module.exports = {
startup
};

35
package-lock.json generated
View file

@ -18,7 +18,11 @@
"morgan": "~1.9.1",
"node-cron": "^3.0.3",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
"winston-daily-rotate-file": "^5.0.0",
"zeromq": "^6.5.0"
},
"devDependencies": {
"esbuild": "^0.25.8"
}
},
"node_modules/@colors/colors": {
@ -720,6 +724,14 @@
"wordwrap": "0.0.2"
}
},
"node_modules/cmake-ts": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cmake-ts/-/cmake-ts-1.0.2.tgz",
"integrity": "sha512-5l++JHE7MxFuyV/OwJf3ek7ZZN1aGPFPM5oUz6AnK5inQAPe4TFXRMz5sA2qg2FRgByPWdqO+gSfIPo8GzoKNQ==",
"bin": {
"cmake-ts": "build/main.js"
}
},
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
@ -1631,6 +1643,14 @@
"node": ">= 0.6"
}
},
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-cron": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
@ -2273,6 +2293,19 @@
"decamelize": "^1.0.0",
"window-size": "0.1.0"
}
},
"node_modules/zeromq": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/zeromq/-/zeromq-6.5.0.tgz",
"integrity": "sha512-vWOrt19lvcXTxu5tiHXfEGQuldSlU+qZn2TT+4EbRQzaciWGwNZ99QQTolQOmcwVgZLodv+1QfC6UZs2PX/6pQ==",
"hasInstallScript": true,
"dependencies": {
"cmake-ts": "1.0.2",
"node-addon-api": "^8.3.1"
},
"engines": {
"node": ">= 12"
}
}
}
}

View file

@ -16,7 +16,8 @@
"morgan": "~1.9.1",
"node-cron": "^3.0.3",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
"winston-daily-rotate-file": "^5.0.0",
"zeromq": "^6.5.0"
},
"devDependencies": {
"esbuild": "^0.25.8"

View file

@ -12,7 +12,10 @@ const heartbeatApiInfo = {
let heartbeatTask = CronJobs.doEveryMinute(() => {
Log.debug("[hb] test heartbeat");
Log.debug(`[hb] current running tasks: ${JSON.stringify(CronJobs.getAllRunning.size)}`);
heartbeatTask.stop();
client.publish("log", {
msg: "heartbeat",
status: 200
});
}, 'heartbeat');
heartbeatTask.on('stop-heartbeat', () => heartbeatTask.stop());
@ -28,4 +31,7 @@ endpointMap = {
res.render("heartbeat");
},
'info': function(req, res, next){
res.json(heartbeatApiInfo);
}
};

View file

@ -26,12 +26,19 @@ endpointMap = {
res.render("apiInfo");
},
"run/now": function (req, res, next) {
GoogleFunctions.syncProfilePrice(sheet, "tha", false);
res.send("Force run `Sync Thai Price Slots`");
"info": function(req, res, next) {
res.json(syncThaiProfilePriceSlotApiInfo);
},
"run/test": function (req, res, next) {
GoogleFunctions.syncProfilePrice(sheet, "tha", true);
res.send("Force run `Sync Thai Price Slots` test mode");
"run/now": async function (req, res, next) {
// res.json({ status: "running" });
await GoogleFunctions.syncProfilePrice(sheet, "tha", false);
res.json({ status: "success", message: "Force run `Sync Thai Price Slots`" });
// res.send("Force run `Sync Thai Price Slots`");
},
"run/test": async function (req, res, next) {
// res.json({ status: "running" });
await GoogleFunctions.syncProfilePrice(sheet, "tha", true);
res.json({ status: "success", message: "Force run `Sync Thai Price Slots` test mode" });
// res.send("Force run `Sync Thai Price Slots` test mode");
},
};

58
public/index.html Normal file
View file

@ -0,0 +1,58 @@
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css"
integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls"
crossorigin="anonymous"
/>
</head>
<body>
<script src="scripts/test.js"></script>
<div class="pure-g">
<div class="pure-u-1-2">
<h1>Heartbeat</h1>
<button
class="pure-button pure-button-primary"
onclick="getHeartBeatInfo()"
>
Info
</button>
<h1>Sync Price Slot (THA)</h1>
<button
class="pure-button pure-button-primary"
onclick="getSyncPriceInfo()"
>
Info
</button>
<button
class="pure-button pure-button-primary"
onclick="setSyncPriceRunTest()"
>
Test
</button>
<button
class="pure-button pure-button-primary"
onclick="setSyncPriceRunNow()"
>
Run Now
</button>
</div>
<div class="pure-u-1-2" style="background-color: aliceblue">
<!-- show output -->
<h1>Output</h1>
<textarea
id="output"
cols="100"
rows="45"
readonly
style="resize: none"
></textarea>
</div>
</div>
</body>
</html>

24
public/scripts/test.js Normal file
View file

@ -0,0 +1,24 @@
console.log("Hello, World!");
function sendToOutput(message){
document.getElementById("output").value += message + "\n";
}
function getHeartBeatInfo(){
fetch("http://localhost:36530/heartbeat/info").then(response => response.json()).then(data => sendToOutput(JSON.stringify(data)));
}
// Sync Price Slot Command Shortcuts
//
//
function getSyncPriceInfo(){
fetch("http://localhost:36530/syncProfilePriceSlotSheet/info").then(response => response.json()).then(data => sendToOutput(JSON.stringify(data)));
}
function setSyncPriceRunTest(){
fetch("http://localhost:36530/syncProfilePriceSlotSheet/run/test").then(response => response.json()).then(data => sendToOutput(JSON.stringify(data)));
}
function setSyncPriceRunNow(){
fetch("http://localhost:36530/syncProfilePriceSlotSheet/run/now").then(response => response.json()).then(data => sendToOutput(JSON.stringify(data)));
}

View file

@ -1,2 +1,2 @@
#!/bin/bash
DEBUG=server:* PORT=36530 npm start
DEBUG=server:* PORT=36531 npm start