Compare commits
3 Commits
0160e887ef
...
6874210d6f
| Author | SHA1 | Date |
|---|---|---|
|
|
6874210d6f | |
|
|
06554328b1 | |
|
|
7c6e5476fa |
|
|
@ -0,0 +1 @@
|
||||||
|
JWT_SECRET=secret
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
.env
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
const { verifyToken } = require("../utils/jwt");
|
||||||
|
|
||||||
|
const authenticateToken = (req, res, next) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
const token = authHeader?.split(" ")[1];
|
||||||
|
|
||||||
|
if (!token) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
req.user = verifyToken(token);
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(403).json({ message: "Invalid token" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { authenticateToken };
|
||||||
|
|
@ -10,12 +10,14 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"cheerio": "^1.1.1",
|
"cheerio": "^1.1.1",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.14.2",
|
"mysql2": "^3.14.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
|
|
@ -267,6 +269,29 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.3.0",
|
||||||
|
"node-gyp-build": "^4.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bcrypt/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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/better-sqlite3": {
|
"node_modules/better-sqlite3": {
|
||||||
"version": "12.2.0",
|
"version": "12.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz",
|
||||||
|
|
@ -396,6 +421,12 @@
|
||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
|
@ -853,6 +884,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
|
@ -1665,6 +1705,97 @@
|
||||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^3.2.2",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsonwebtoken/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^1.4.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/long": {
|
"node_modules/long": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
|
@ -2045,6 +2176,17 @@
|
||||||
"node": ">= 10.12.0"
|
"node": ">= 10.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"cheerio": "^1.1.1",
|
"cheerio": "^1.1.1",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.14.2",
|
"mysql2": "^3.14.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,524 @@
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #1e88e5;
|
||||||
|
--primary-light: #e3f2fd;
|
||||||
|
--gray-light: #f8f9fa;
|
||||||
|
--gray-border: #e0e0e0;
|
||||||
|
--text-color: #333;
|
||||||
|
--bg-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Segoe UI", Roboto, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background-color: var(--gray-light);
|
||||||
|
border-right: 1px solid var(--gray-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 16px;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pid-button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pid-button:hover {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pid-button.selected {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
select,
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px;
|
||||||
|
/* margin-bottom: 16px; */
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus,
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-block {
|
||||||
|
background: #fafafa;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-section h3 {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-section h3 > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn {
|
||||||
|
font-size: 13px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:hover:not(:disabled) {
|
||||||
|
background-color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th,
|
||||||
|
table td {
|
||||||
|
padding: 12px 14px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr:hover {
|
||||||
|
background-color: #f1f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background-color: #ffdb38;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#selectAndVersionDiv {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#selectedPIDDiv {
|
||||||
|
width: 23%;
|
||||||
|
}
|
||||||
|
#selectedVersionDiv {
|
||||||
|
width: 53%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
background-color: #8d7df3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 14px;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:active {
|
||||||
|
background-color: #004a99;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Page */
|
||||||
|
.login-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
padding: 20px;
|
||||||
|
background-image: url("../images/backgroundLogin.jpg");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-wrapper {
|
||||||
|
background-color: rgba(255, 255, 255, 0.93);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--gray-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: #1565c0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:hover:not(:disabled) {
|
||||||
|
background-color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
background: #e03131;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn:hover:not(:disabled) {
|
||||||
|
background-color: #c92a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-btn {
|
||||||
|
background: #f08c00;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-btn:hover:not(:disabled) {
|
||||||
|
background-color: #e67700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-btn {
|
||||||
|
font-size: 22px;
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
top: 2px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-item-btn {
|
||||||
|
font-size: 20px;
|
||||||
|
right: 2px;
|
||||||
|
top: 2px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-error {
|
||||||
|
color: red;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 50%;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-15px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-with-badge {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-with-badge .badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -10px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #dc3545;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-toggle {
|
||||||
|
background-color: white;
|
||||||
|
border: none;
|
||||||
|
color: black;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
min-width: 160px;
|
||||||
|
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-content form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-content button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-content button:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown:hover .user-dropdown-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background-color: #323232;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 9999;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease, bottom 0.3s ease;
|
||||||
|
}
|
||||||
|
.toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
bottom: 40px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,587 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>PID Log Viewer</title>
|
||||||
|
|
||||||
|
<!-- Link to CSS file -->
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="logToast" class="toast hidden"></div>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3>PIDs (<span id="pidCount">0</span>)</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="pidSearch"
|
||||||
|
placeholder="🔍 Search PID..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-list" id="pidListQuick"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<!-- <h3>📊 PID Log Viewer</h3> -->
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="user-dropdown">
|
||||||
|
<button
|
||||||
|
id="welcome-user"
|
||||||
|
class="user-dropdown-toggle"
|
||||||
|
></button>
|
||||||
|
|
||||||
|
<div class="user-dropdown-content">
|
||||||
|
<button onclick="logout()">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="selectAndVersionDiv">
|
||||||
|
<div id="selectedPIDDiv">
|
||||||
|
<label for="pidList">PID</label>
|
||||||
|
<select id="pidList" disabled></select>
|
||||||
|
</div>
|
||||||
|
<div id="selectedVersionDiv">
|
||||||
|
<label for="versionList">Select Version</label>
|
||||||
|
<select id="versionList"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width: 23%; align-items: center">
|
||||||
|
<button id="copyBtn" class="action-btn">📋 Copy</button>
|
||||||
|
<button id="downloadBtn" class="action-btn">
|
||||||
|
⬇️ Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="logConfirmDeleteBtn"
|
||||||
|
type="button"
|
||||||
|
class="btn-with-badge danger-btn"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
<span class="badge" id="logDeleteBadge">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorTableContainer" class="command-section">
|
||||||
|
<h3>⚠️ Error History for PID</h3>
|
||||||
|
<table border="1" cellspacing="0" cellpadding="5" width="100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="text-align: left">Logging</th>
|
||||||
|
<th style="text-align: left">Pattern</th>
|
||||||
|
<th style="text-align: left">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="errorTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="logSections"></div>
|
||||||
|
|
||||||
|
<!-- Log Delete Modal -->
|
||||||
|
<div id="logDeleteModal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2 class="modal-title">Confirm Deletion</h2>
|
||||||
|
|
||||||
|
<table class="delete-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>PID</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>Command</th>
|
||||||
|
<th style="text-align: center">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="logDeleteListContainer"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="logCloseModalBtn" class="btn secondary-btn">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button id="logSubmitDeleteBtn" class="btn danger-btn">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if (!localStorage.getItem("token")) {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"welcome-user",
|
||||||
|
).innerHTML = `Welcome, <strong>${
|
||||||
|
JSON.parse(localStorage.getItem("user")).name
|
||||||
|
}</strong> ▼`;
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("user");
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Get param pid, version from URL
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const pidParam = params.get("pid");
|
||||||
|
const versionParam = params.get("version");
|
||||||
|
|
||||||
|
const pidSearchInput = document.getElementById("pidSearch");
|
||||||
|
const pidList = document.getElementById("pidList");
|
||||||
|
const versionList = document.getElementById("versionList");
|
||||||
|
const pidListQuick = document.getElementById("pidListQuick");
|
||||||
|
const logSections = document.getElementById("logSections");
|
||||||
|
const commands = ["inventory", "version", "license", "logging"];
|
||||||
|
|
||||||
|
const logsCache = {}; // Store logs and current index for each command
|
||||||
|
|
||||||
|
async function loadPIDs(pidParam, versionParam) {
|
||||||
|
const res = await fetch("/api/pids");
|
||||||
|
const pids = await res.json();
|
||||||
|
document.getElementById("pidCount").textContent = pids.length;
|
||||||
|
pidListQuick.innerHTML = "";
|
||||||
|
|
||||||
|
pids.forEach((pid) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = "pid-button";
|
||||||
|
btn.textContent = pid;
|
||||||
|
btn.onclick = () => selectPID(pid);
|
||||||
|
pidListQuick.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pidParam) {
|
||||||
|
selectPID(pidParam, versionParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectPID(pid, versionParam) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".pid-button")
|
||||||
|
.forEach((b) => b.classList.remove("selected"));
|
||||||
|
const target = [
|
||||||
|
...document.querySelectorAll(".pid-button"),
|
||||||
|
].find((b) => b.textContent === pid);
|
||||||
|
if (target) target.classList.add("selected");
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/pid/${encodeURIComponent(pid)}/versions`,
|
||||||
|
);
|
||||||
|
const versions = await res.json();
|
||||||
|
|
||||||
|
pidList.innerHTML = `<option value="${pid}">${pid}</option>`;
|
||||||
|
versionList.innerHTML = versions
|
||||||
|
.map((v) => {
|
||||||
|
const cmds = Object.entries(v.commands)
|
||||||
|
.filter(([_, count]) => count > 0)
|
||||||
|
.map(([cmd, count]) => `${cmd} (${count})`)
|
||||||
|
.join(", ");
|
||||||
|
return `<option value="${v.version}">${v.version} - ${
|
||||||
|
cmds || "No commands"
|
||||||
|
}</option>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Set param pid, version to URL
|
||||||
|
const currentParams = new URLSearchParams(
|
||||||
|
window.location.search,
|
||||||
|
);
|
||||||
|
currentParams.set("pid", pid);
|
||||||
|
|
||||||
|
// Có param version thì chọn
|
||||||
|
if (versionParam) {
|
||||||
|
versionList.value = versionParam;
|
||||||
|
versionList.dispatchEvent(new Event("change"));
|
||||||
|
currentParams.set("version", versionParam);
|
||||||
|
} else {
|
||||||
|
// ✅ Default chọn version đầu tiên
|
||||||
|
if (versions.length > 0) {
|
||||||
|
versionList.value = versions[0].version;
|
||||||
|
versionList.dispatchEvent(new Event("change"));
|
||||||
|
currentParams.set("version", versions[0].version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replaceState(null, "", `?${currentParams.toString()}`);
|
||||||
|
|
||||||
|
await loadErrorTable(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
pidSearchInput.addEventListener("input", async () => {
|
||||||
|
const keyword = pidSearchInput.value.toLowerCase().trim();
|
||||||
|
const res = await fetch("/api/pids");
|
||||||
|
const pids = await res.json();
|
||||||
|
const filtered = pids.filter((pid) =>
|
||||||
|
pid.toLowerCase().includes(keyword),
|
||||||
|
);
|
||||||
|
|
||||||
|
pidListQuick.innerHTML = "";
|
||||||
|
filtered.forEach((pid) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = "pid-button";
|
||||||
|
btn.textContent = pid;
|
||||||
|
btn.onclick = () => selectPID(pid);
|
||||||
|
pidListQuick.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
pidList.innerHTML = filtered
|
||||||
|
.map((pid) => `<option value="${pid}">${pid}</option>`)
|
||||||
|
.join("");
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
pidList.value = filtered[0];
|
||||||
|
selectPID(filtered[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pidList.addEventListener("change", () => {
|
||||||
|
const pid = pidList.value;
|
||||||
|
if (pid) selectPID(pid);
|
||||||
|
});
|
||||||
|
|
||||||
|
versionList.addEventListener("change", () => {
|
||||||
|
const pid = pidList.value;
|
||||||
|
const version = versionList.value;
|
||||||
|
if (pid && version) {
|
||||||
|
// Set param pid, version to URL
|
||||||
|
const currentParams = new URLSearchParams(
|
||||||
|
window.location.search,
|
||||||
|
);
|
||||||
|
currentParams.set("pid", pid);
|
||||||
|
currentParams.set("version", version);
|
||||||
|
history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
`?${currentParams.toString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
loadLogs(pid, version);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadLogs(pid, version) {
|
||||||
|
logSections.innerHTML = "";
|
||||||
|
logsCache[`${pid}_${version}`] = {};
|
||||||
|
|
||||||
|
for (const cmd of commands) {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/device/${encodeURIComponent(
|
||||||
|
pid,
|
||||||
|
)}/${encodeURIComponent(version)}/${cmd}`,
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data || data.length === 0) continue;
|
||||||
|
|
||||||
|
logsCache[`${pid}_${version}`][cmd] = {
|
||||||
|
logs: data,
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const section = document.createElement("div");
|
||||||
|
section.className = "command-section";
|
||||||
|
section.id = `section-${cmd}`;
|
||||||
|
section.innerHTML = `
|
||||||
|
<h3>
|
||||||
|
${"show " + cmd} (${data.length})
|
||||||
|
<div>
|
||||||
|
<button class="next-btn" id="btn-prev-${cmd}">Previous</button>
|
||||||
|
<span id="page-info-${cmd}">Output 1/${data.length}</span>
|
||||||
|
<button class="next-btn" id="btn-next-${cmd}">Next</button>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<div class="log-block" id="log-${cmd}">
|
||||||
|
<button type="button" class="danger-btn trash-btn" title="Add to delete list">🗑</button>
|
||||||
|
<b>${data[0].filename}</b>\n${data[0].output}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
logSections.appendChild(section);
|
||||||
|
|
||||||
|
// Navigation logic
|
||||||
|
const updateDisplay = () => {
|
||||||
|
const info = logsCache[`${pid}_${version}`][cmd];
|
||||||
|
const curr = info.logs[info.index];
|
||||||
|
|
||||||
|
const logBlock = document.getElementById(`log-${cmd}`);
|
||||||
|
logBlock.innerHTML = `
|
||||||
|
<button type="button" class="danger-btn trash-btn" title="Add to delete list">🗑</button>
|
||||||
|
<b>${curr.filename}</b>\n${curr.output}
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
`page-info-${cmd}`,
|
||||||
|
).textContent = `Output ${info.index + 1}/${
|
||||||
|
info.logs.length
|
||||||
|
}`;
|
||||||
|
document.getElementById(`btn-prev-${cmd}`).disabled =
|
||||||
|
info.index === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById(`btn-next-${cmd}`)
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
const info = logsCache[`${pid}_${version}`][cmd];
|
||||||
|
info.index = (info.index + 1) % info.logs.length;
|
||||||
|
updateDisplay();
|
||||||
|
});
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById(`btn-prev-${cmd}`)
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
const info = logsCache[`${pid}_${version}`][cmd];
|
||||||
|
info.index =
|
||||||
|
(info.index - 1 + info.logs.length) %
|
||||||
|
info.logs.length;
|
||||||
|
updateDisplay();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPIDs(pidParam, versionParam);
|
||||||
|
|
||||||
|
const copyBtn = document.getElementById("copyBtn");
|
||||||
|
const downloadBtn = document.getElementById("downloadBtn");
|
||||||
|
|
||||||
|
function getExportText() {
|
||||||
|
const pid = pidList.value || "";
|
||||||
|
const version = versionList.value || "";
|
||||||
|
const inventory = getCommandOutput("inventory");
|
||||||
|
const versionOut = getCommandOutput("version");
|
||||||
|
const logging = getCommandOutput("logging");
|
||||||
|
const license = getCommandOutput("license");
|
||||||
|
|
||||||
|
return `PID: ${pid}
|
||||||
|
Version: ${version}
|
||||||
|
|
||||||
|
${inventory}
|
||||||
|
|
||||||
|
${versionOut}
|
||||||
|
|
||||||
|
${logging}
|
||||||
|
|
||||||
|
${license}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume you store outputs in memory or fetch based on UI state
|
||||||
|
function getCommandOutput(command) {
|
||||||
|
const pid = pidList.value;
|
||||||
|
const version = versionList.value;
|
||||||
|
const key = `${pid}_${version}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
logsCache[key] &&
|
||||||
|
logsCache[key][command] &&
|
||||||
|
logsCache[key][command].logs.length > 0
|
||||||
|
) {
|
||||||
|
const { logs, index } = logsCache[key][command];
|
||||||
|
return logs[index].output || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "No data";
|
||||||
|
}
|
||||||
|
|
||||||
|
copyBtn.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(getExportText());
|
||||||
|
alert("Copied to clipboard!");
|
||||||
|
} catch (err) {
|
||||||
|
alert("Failed to copy.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadBtn.addEventListener("click", () => {
|
||||||
|
const content = getExportText();
|
||||||
|
const blob = new Blob([content], { type: "text/plain" });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `log-${pidList.value || "unknown"}.txt`;
|
||||||
|
a.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadErrorTable(pid) {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/errors/${encodeURIComponent(pid)}`,
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const tbody = document.getElementById("errorTableBody");
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="5">✅ No known issues for this PID</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const err of data) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td><code>${err.highlighted_message}</code></td>
|
||||||
|
<td><code>${err.regex_pattern}</code></td>
|
||||||
|
<td><code>${err.description}</code></td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const STORAGE_KEY = "logDeleteList";
|
||||||
|
const logConfirmDeleteBtn = document.getElementById(
|
||||||
|
"logConfirmDeleteBtn",
|
||||||
|
);
|
||||||
|
const logDeleteModal = document.getElementById("logDeleteModal");
|
||||||
|
const logDeleteListContainer = document.getElementById(
|
||||||
|
"logDeleteListContainer",
|
||||||
|
);
|
||||||
|
const logCloseModalBtn =
|
||||||
|
document.getElementById("logCloseModalBtn");
|
||||||
|
const logSubmitDeleteBtn =
|
||||||
|
document.getElementById("logSubmitDeleteBtn");
|
||||||
|
|
||||||
|
function getDeleteList() {
|
||||||
|
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDeleteList(list) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
||||||
|
updateDeleteBtn();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDeleteBtn() {
|
||||||
|
const deleteList = getDeleteList();
|
||||||
|
const badge = document.getElementById("logDeleteBadge");
|
||||||
|
badge.textContent = deleteList.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (e.target.classList.contains("trash-btn")) {
|
||||||
|
const section = e.target.closest(".command-section");
|
||||||
|
const pid = pidList.value;
|
||||||
|
const version = versionList.value;
|
||||||
|
const command = section.id.replace("section-", "");
|
||||||
|
const index = logsCache[`${pid}_${version}`][command].index;
|
||||||
|
const log =
|
||||||
|
logsCache[`${pid}_${version}`][command].logs[index];
|
||||||
|
|
||||||
|
const currentList = getDeleteList();
|
||||||
|
if (!currentList.some((item) => item.id === log.id)) {
|
||||||
|
currentList.push({
|
||||||
|
id: log.id,
|
||||||
|
pid,
|
||||||
|
version,
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
saveDeleteList(currentList);
|
||||||
|
|
||||||
|
showToast("Added to delete list");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderDeleteModal() {
|
||||||
|
const deleteList = getDeleteList();
|
||||||
|
logDeleteListContainer.innerHTML = "";
|
||||||
|
deleteList.forEach((item, i) => {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${item.id}</td>
|
||||||
|
<td>${item.pid}</td>
|
||||||
|
<td>${item.version}</td>
|
||||||
|
<td>${item.command}</td>
|
||||||
|
<td style="text-align: center;"><button type="button" class="danger-btn remove-item-btn" data-index="${i}" title="Remove">🗑</button></td>
|
||||||
|
`;
|
||||||
|
logDeleteListContainer.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logDeleteListContainer.addEventListener("click", (e) => {
|
||||||
|
if (e.target.classList.contains("remove-item-btn")) {
|
||||||
|
const index = parseInt(e.target.getAttribute("data-index"));
|
||||||
|
const list = getDeleteList();
|
||||||
|
list.splice(index, 1);
|
||||||
|
saveDeleteList(list);
|
||||||
|
renderDeleteModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open delete modal
|
||||||
|
logConfirmDeleteBtn.addEventListener("click", () => {
|
||||||
|
renderDeleteModal();
|
||||||
|
logDeleteModal.classList.remove("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open close modal
|
||||||
|
logCloseModalBtn.addEventListener("click", () => {
|
||||||
|
logDeleteModal.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit delete
|
||||||
|
logSubmitDeleteBtn.addEventListener("click", async () => {
|
||||||
|
const deleteList = getDeleteList();
|
||||||
|
if (deleteList.length === 0) {
|
||||||
|
alert("Please select at least one item to delete.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
`Are you sure you want to delete ${deleteList.length} item(s)?`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const deletedItems = deleteList.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
command: item.command,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
await fetch("/api/delete-logs", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ items: deletedItems }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
alert("Deleted successfully!");
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
alert("Delete failed.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateDeleteBtn();
|
||||||
|
|
||||||
|
function showToast(message = "Announce something") {
|
||||||
|
const toast = document.getElementById("logToast");
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.classList.add("show");
|
||||||
|
toast.classList.remove("hidden");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove("show");
|
||||||
|
toast.classList.add("hidden");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
|
|
@ -1,522 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>PID Log Viewer</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--primary: #1e88e5;
|
|
||||||
--primary-light: #e3f2fd;
|
|
||||||
--gray-light: #f8f9fa;
|
|
||||||
--gray-border: #e0e0e0;
|
|
||||||
--text-color: #333;
|
|
||||||
--bg-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Segoe UI", Roboto, sans-serif;
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 280px;
|
|
||||||
background-color: var(--gray-light);
|
|
||||||
border-right: 1px solid var(--gray-border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
padding: 16px;
|
|
||||||
background: white;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
flex-shrink: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pid-button {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
margin: 4px 0;
|
|
||||||
text-align: left;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pid-button:hover {
|
|
||||||
background-color: var(--primary-light);
|
|
||||||
border-color: var(--primary);
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pid-button.selected {
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
padding: 24px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background-color: #fefefe;
|
|
||||||
}
|
|
||||||
|
|
||||||
select,
|
|
||||||
input[type="text"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px;
|
|
||||||
/* margin-bottom: 16px; */
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
select:focus,
|
|
||||||
input[type="text"]:focus {
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-block {
|
|
||||||
background: #fafafa;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border-left: 4px solid var(--primary);
|
|
||||||
font-family: monospace;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-section {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-section h3 {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-section h3 > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-btn {
|
|
||||||
font-size: 13px;
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-btn:hover:not(:disabled) {
|
|
||||||
background-color: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-btn:disabled {
|
|
||||||
background-color: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
table th,
|
|
||||||
table td {
|
|
||||||
padding: 12px 14px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 14px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
table tbody tr:hover {
|
|
||||||
background-color: #f1f8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: #f4f4f4;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
mark {
|
|
||||||
background-color: #ffdb38;
|
|
||||||
padding: 0 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#selectAndVersionDiv {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#selectedPIDDiv {
|
|
||||||
width: 23%;
|
|
||||||
}
|
|
||||||
#selectedVersionDiv {
|
|
||||||
width: 53%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
background-color: #8d7df3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 14px;
|
|
||||||
margin: 5px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:active {
|
|
||||||
background-color: #004a99;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h3>PIDs (<span id="pidCount">0</span>)</h3>
|
|
||||||
<input type="text" id="pidSearch" placeholder="🔍 Search PID..." />
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-list" id="pidListQuick"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<!-- <h3>📊 PID Log Viewer</h3> -->
|
|
||||||
<div id="selectAndVersionDiv">
|
|
||||||
<div id="selectedPIDDiv">
|
|
||||||
<label for="pidList">PID</label>
|
|
||||||
<select id="pidList" disabled></select>
|
|
||||||
</div>
|
|
||||||
<div id="selectedVersionDiv">
|
|
||||||
<label for="versionList">Select Version</label>
|
|
||||||
<select id="versionList"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="width: 23%; align-items: center">
|
|
||||||
<button id="copyBtn" class="action-btn">📋 Copy</button>
|
|
||||||
<button id="downloadBtn" class="action-btn">⬇️ Download</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="errorTableContainer" class="command-section">
|
|
||||||
<h3>⚠️ Error History for PID</h3>
|
|
||||||
<table border="1" cellspacing="0" cellpadding="5" width="100%">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="text-align: left">Logging</th>
|
|
||||||
<th style="text-align: left">Pattern</th>
|
|
||||||
<th style="text-align: left">Description</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="errorTableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div id="logSections"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const pidSearchInput = document.getElementById("pidSearch");
|
|
||||||
const pidList = document.getElementById("pidList");
|
|
||||||
const versionList = document.getElementById("versionList");
|
|
||||||
const pidListQuick = document.getElementById("pidListQuick");
|
|
||||||
const logSections = document.getElementById("logSections");
|
|
||||||
const commands = ["inventory", "version", "license", "logging"];
|
|
||||||
|
|
||||||
const logsCache = {}; // Store logs and current index for each command
|
|
||||||
|
|
||||||
async function loadPIDs() {
|
|
||||||
const res = await fetch("/api/pids");
|
|
||||||
const pids = await res.json();
|
|
||||||
document.getElementById("pidCount").textContent = pids.length;
|
|
||||||
pidListQuick.innerHTML = "";
|
|
||||||
|
|
||||||
pids.forEach((pid) => {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.className = "pid-button";
|
|
||||||
btn.textContent = pid;
|
|
||||||
btn.onclick = () => selectPID(pid);
|
|
||||||
pidListQuick.appendChild(btn);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectPID(pid) {
|
|
||||||
document
|
|
||||||
.querySelectorAll(".pid-button")
|
|
||||||
.forEach((b) => b.classList.remove("selected"));
|
|
||||||
const target = [...document.querySelectorAll(".pid-button")].find(
|
|
||||||
(b) => b.textContent === pid
|
|
||||||
);
|
|
||||||
if (target) target.classList.add("selected");
|
|
||||||
|
|
||||||
const res = await fetch(`/api/pid/${encodeURIComponent(pid)}/versions`);
|
|
||||||
const versions = await res.json();
|
|
||||||
|
|
||||||
pidList.innerHTML = `<option value="${pid}">${pid}</option>`;
|
|
||||||
versionList.innerHTML = versions
|
|
||||||
.map((v) => {
|
|
||||||
const cmds = Object.entries(v.commands)
|
|
||||||
.filter(([_, count]) => count > 0)
|
|
||||||
.map(([cmd, count]) => `${cmd} (${count})`)
|
|
||||||
.join(", ");
|
|
||||||
return `<option value="${v.version}">${v.version} - ${
|
|
||||||
cmds || "No commands"
|
|
||||||
}</option>`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
// ✅ Default chọn version đầu tiên
|
|
||||||
if (versions.length > 0) {
|
|
||||||
versionList.value = versions[0].version;
|
|
||||||
versionList.dispatchEvent(new Event("change"));
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadErrorTable(pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
pidSearchInput.addEventListener("input", async () => {
|
|
||||||
const keyword = pidSearchInput.value.toLowerCase().trim();
|
|
||||||
const res = await fetch("/api/pids");
|
|
||||||
const pids = await res.json();
|
|
||||||
const filtered = pids.filter((pid) =>
|
|
||||||
pid.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
|
|
||||||
pidListQuick.innerHTML = "";
|
|
||||||
filtered.forEach((pid) => {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.className = "pid-button";
|
|
||||||
btn.textContent = pid;
|
|
||||||
btn.onclick = () => selectPID(pid);
|
|
||||||
pidListQuick.appendChild(btn);
|
|
||||||
});
|
|
||||||
|
|
||||||
pidList.innerHTML = filtered
|
|
||||||
.map((pid) => `<option value="${pid}">${pid}</option>`)
|
|
||||||
.join("");
|
|
||||||
if (filtered.length > 0) {
|
|
||||||
pidList.value = filtered[0];
|
|
||||||
selectPID(filtered[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pidList.addEventListener("change", () => {
|
|
||||||
const pid = pidList.value;
|
|
||||||
if (pid) selectPID(pid);
|
|
||||||
});
|
|
||||||
|
|
||||||
versionList.addEventListener("change", () => {
|
|
||||||
const pid = pidList.value;
|
|
||||||
const version = versionList.value;
|
|
||||||
if (pid && version) loadLogs(pid, version);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadLogs(pid, version) {
|
|
||||||
logSections.innerHTML = "";
|
|
||||||
logsCache[`${pid}_${version}`] = {};
|
|
||||||
|
|
||||||
for (const cmd of commands) {
|
|
||||||
const res = await fetch(
|
|
||||||
`/api/device/${encodeURIComponent(pid)}/${encodeURIComponent(
|
|
||||||
version
|
|
||||||
)}/${cmd}`
|
|
||||||
);
|
|
||||||
const data = await res.json();
|
|
||||||
if (!data || data.length === 0) continue;
|
|
||||||
|
|
||||||
logsCache[`${pid}_${version}`][cmd] = {
|
|
||||||
logs: data,
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const section = document.createElement("div");
|
|
||||||
section.className = "command-section";
|
|
||||||
section.id = `section-${cmd}`;
|
|
||||||
section.innerHTML = `
|
|
||||||
<h3>
|
|
||||||
${"show " + cmd} (${data.length})
|
|
||||||
<div>
|
|
||||||
<button class="next-btn" id="btn-prev-${cmd}">Previous</button>
|
|
||||||
<span id="page-info-${cmd}">Output 1/${data.length}</span>
|
|
||||||
<button class="next-btn" id="btn-next-${cmd}">Next</button>
|
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
<div class="log-block" id="log-${cmd}">
|
|
||||||
<b>${data[0].filename}</b>\n${data[0].output}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
logSections.appendChild(section);
|
|
||||||
|
|
||||||
// Navigation logic
|
|
||||||
const updateDisplay = () => {
|
|
||||||
const info = logsCache[`${pid}_${version}`][cmd];
|
|
||||||
const curr = info.logs[info.index];
|
|
||||||
document.getElementById(
|
|
||||||
`log-${cmd}`
|
|
||||||
).innerHTML = `<b>${curr.filename}</b>\n${curr.output}`;
|
|
||||||
document.getElementById(`page-info-${cmd}`).textContent = `Output ${
|
|
||||||
info.index + 1
|
|
||||||
}/${info.logs.length}`;
|
|
||||||
document.getElementById(`btn-prev-${cmd}`).disabled =
|
|
||||||
info.index === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById(`btn-next-${cmd}`)
|
|
||||||
.addEventListener("click", () => {
|
|
||||||
const info = logsCache[`${pid}_${version}`][cmd];
|
|
||||||
info.index = (info.index + 1) % info.logs.length;
|
|
||||||
updateDisplay();
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById(`btn-prev-${cmd}`)
|
|
||||||
.addEventListener("click", () => {
|
|
||||||
const info = logsCache[`${pid}_${version}`][cmd];
|
|
||||||
info.index =
|
|
||||||
(info.index - 1 + info.logs.length) % info.logs.length;
|
|
||||||
updateDisplay();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPIDs();
|
|
||||||
|
|
||||||
const copyBtn = document.getElementById("copyBtn");
|
|
||||||
const downloadBtn = document.getElementById("downloadBtn");
|
|
||||||
|
|
||||||
function getExportText() {
|
|
||||||
const pid = pidList.value || "";
|
|
||||||
const version = versionList.value || "";
|
|
||||||
const inventory = getCommandOutput("inventory");
|
|
||||||
const versionOut = getCommandOutput("version");
|
|
||||||
const logging = getCommandOutput("logging");
|
|
||||||
const license = getCommandOutput("license");
|
|
||||||
|
|
||||||
return `PID: ${pid}
|
|
||||||
Version: ${version}
|
|
||||||
|
|
||||||
${inventory}
|
|
||||||
|
|
||||||
${versionOut}
|
|
||||||
|
|
||||||
${logging}
|
|
||||||
|
|
||||||
${license}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assume you store outputs in memory or fetch based on UI state
|
|
||||||
function getCommandOutput(command) {
|
|
||||||
const pid = pidList.value;
|
|
||||||
const version = versionList.value;
|
|
||||||
const key = `${pid}_${version}`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
logsCache[key] &&
|
|
||||||
logsCache[key][command] &&
|
|
||||||
logsCache[key][command].logs.length > 0
|
|
||||||
) {
|
|
||||||
const { logs, index } = logsCache[key][command];
|
|
||||||
return logs[index].output || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "No data";
|
|
||||||
}
|
|
||||||
|
|
||||||
copyBtn.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(getExportText());
|
|
||||||
alert("Copied to clipboard!");
|
|
||||||
} catch (err) {
|
|
||||||
alert("Failed to copy.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
downloadBtn.addEventListener("click", () => {
|
|
||||||
const content = getExportText();
|
|
||||||
const blob = new Blob([content], { type: "text/plain" });
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = URL.createObjectURL(blob);
|
|
||||||
a.download = `log-${pidList.value || "unknown"}.txt`;
|
|
||||||
a.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadErrorTable(pid) {
|
|
||||||
const res = await fetch(`/api/errors/${encodeURIComponent(pid)}`);
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
const tbody = document.getElementById("errorTableBody");
|
|
||||||
tbody.innerHTML = "";
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
tbody.innerHTML = `<tr><td colspan="5">✅ No known issues for this PID</td></tr>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const err of data) {
|
|
||||||
const tr = document.createElement("tr");
|
|
||||||
tr.innerHTML = `
|
|
||||||
<td><code>${err.highlighted_message}</code></td>
|
|
||||||
<td><code>${err.regex_pattern}</code></td>
|
|
||||||
<td><code>${err.description}</code></td>
|
|
||||||
`;
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Login</title>
|
||||||
|
|
||||||
|
<!-- Link to CSS file -->
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section class="login-section">
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="form-wrapper">
|
||||||
|
<div class="form-box">
|
||||||
|
<h1 class="form-title">Login</h1>
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="user@gmail.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Your password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="error-msg text-error"
|
||||||
|
style="display: none"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<button type="submit" class="primary-btn">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if (localStorage.getItem("token")) {
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document
|
||||||
|
.getElementById("loginForm")
|
||||||
|
.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const email = document.getElementById("email").value;
|
||||||
|
const password = document.getElementById("password").value;
|
||||||
|
const errorMsg = document.querySelector(".error-msg");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMsg.textContent =
|
||||||
|
data.message || "Login failed";
|
||||||
|
errorMsg.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("token", data.token);
|
||||||
|
localStorage.setItem("user", JSON.stringify(data.user));
|
||||||
|
|
||||||
|
window.location.href = "/";
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Login failed:", err);
|
||||||
|
errorMsg.textContent = "Something went wrong.";
|
||||||
|
errorMsg.style.display = "block";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
const express = require("express");
|
||||||
|
const path = require("path");
|
||||||
|
const bcrypt = require("bcrypt");
|
||||||
|
const { createToken } = require("../utils/jwt");
|
||||||
|
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
module.exports = (app, db) => {
|
||||||
|
router.get("/", (_, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "../public/dashboard.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/login", (_, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "../public/login.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/api/login", async (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
"SELECT * FROM users WHERE email = ?",
|
||||||
|
[email],
|
||||||
|
);
|
||||||
|
const user = rows[0];
|
||||||
|
const isMatch = await bcrypt.compare(
|
||||||
|
password,
|
||||||
|
user?.password || "!@#",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user || !isMatch) {
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.json({ message: "Invalid email or password" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = createToken({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
});
|
||||||
|
return res
|
||||||
|
.status(200)
|
||||||
|
.json({ token, user: { name: user.name, email: user.email } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Login error:", err);
|
||||||
|
return res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/", router);
|
||||||
|
};
|
||||||
410
server.js
410
server.js
|
|
@ -4,43 +4,74 @@ const bodyParser = require("body-parser");
|
||||||
const dotenv = require("dotenv");
|
const dotenv = require("dotenv");
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const stringSimilarity = require('string-similarity');
|
const stringSimilarity = require("string-similarity");
|
||||||
|
const bcrypt = require("bcrypt");
|
||||||
|
const inititalWebRoute = require("./route/web");
|
||||||
|
const { authenticateToken } = require("./middleware/auth");
|
||||||
|
|
||||||
// Nhận mảng message đầu vào, trả về mảng message đã loại bỏ trùng lặp
|
// Nhận mảng message đầu vào, trả về mảng message đã loại bỏ trùng lặp
|
||||||
function deduplicateErrors(errors, threshold = 0.3) {
|
function deduplicateErrors(errors, threshold = 0.3) {
|
||||||
const unique = [];
|
const unique = [];
|
||||||
|
|
||||||
for (const err of errors) {
|
for (const err of errors) {
|
||||||
const isDuplicate = unique.some(u => {
|
const isDuplicate = unique.some((u) => {
|
||||||
const similarity = stringSimilarity.compareTwoStrings(err.error_message, u.error_message);
|
const similarity = stringSimilarity.compareTwoStrings(
|
||||||
return similarity >= threshold;
|
err.error_message,
|
||||||
});
|
u.error_message,
|
||||||
|
);
|
||||||
|
return similarity >= threshold;
|
||||||
|
});
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
unique.push(err);
|
unique.push(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return unique;
|
return unique;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 4000;
|
const PORT = 4000;
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
const db = mysql.createPool({
|
const db = mysql.createPool({
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
user: "admin",
|
user: "admin",
|
||||||
password: "Work1234",
|
password: "Work1234",
|
||||||
database: "log_analysis",
|
database: "log_analysis",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function columnExists(table, column) {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
`SELECT COUNT(*) AS count
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_NAME = ? AND COLUMN_NAME = ? AND TABLE_SCHEMA = DATABASE()`,
|
||||||
|
[table, column],
|
||||||
|
);
|
||||||
|
return rows[0].count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkAndInsertAdminUser = async () => {
|
||||||
|
const [rows] = await db.query(`SELECT id FROM users WHERE email = ?`, [
|
||||||
|
"admin@apactech.io",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
const hashedPassword = await bcrypt.hash("admin0312", 10);
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO users (name, email, password) VALUES (?, ?, ?)`,
|
||||||
|
["Admin", "admin@apactech.io", hashedPassword],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// === Tạo bảng
|
// === Tạo bảng
|
||||||
(async () => {
|
(async () => {
|
||||||
await db.query(`
|
await db.query(`
|
||||||
CREATE TABLE IF NOT EXISTS devices (
|
CREATE TABLE IF NOT EXISTS devices (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
pid VARCHAR(100) NOT NULL,
|
pid VARCHAR(100) NOT NULL,
|
||||||
|
|
@ -53,8 +84,20 @@ const db = mysql.createPool({
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const createOutputTable = async (command) => {
|
if (!(await columnExists("devices", "updated_at"))) {
|
||||||
await db.query(`
|
await db.query(
|
||||||
|
`ALTER TABLE devices ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await columnExists("devices", "updated_by"))) {
|
||||||
|
await db.query(
|
||||||
|
`ALTER TABLE devices ADD COLUMN updated_by VARCHAR(100) DEFAULT NULL`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOutputTable = async (command) => {
|
||||||
|
await db.query(`
|
||||||
CREATE TABLE IF NOT EXISTS ${command}_outputs (
|
CREATE TABLE IF NOT EXISTS ${command}_outputs (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
device_id INT NOT NULL,
|
device_id INT NOT NULL,
|
||||||
|
|
@ -65,148 +108,233 @@ const db = mysql.createPool({
|
||||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE
|
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
|
||||||
await createOutputTable("inventory");
|
await createOutputTable("inventory");
|
||||||
await createOutputTable("version");
|
await createOutputTable("version");
|
||||||
await createOutputTable("license");
|
await createOutputTable("license");
|
||||||
await createOutputTable("logging");
|
await createOutputTable("logging");
|
||||||
|
|
||||||
|
await db.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
confirm_count INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await checkAndInsertAdminUser();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// === APIs ===
|
// === APIs ===
|
||||||
|
|
||||||
app.get("/api/pids", async (req, res) => {
|
app.get("/api/pids", async (req, res) => {
|
||||||
const [rows] = await db.query("SELECT DISTINCT pid FROM devices ORDER BY pid");
|
const [rows] = await db.query(
|
||||||
res.json(rows.map((r) => r.pid));
|
"SELECT DISTINCT pid FROM devices ORDER BY pid",
|
||||||
|
);
|
||||||
|
res.json(rows.map((r) => r.pid));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/pid/:pid/versions", async (req, res) => {
|
app.get("/api/pid/:pid/versions", async (req, res) => {
|
||||||
const { pid } = req.params;
|
const { pid } = req.params;
|
||||||
|
|
||||||
// Lấy danh sách các version tương ứng với PID
|
// Lấy danh sách các version tương ứng với PID
|
||||||
const [devices] = await db.query(
|
const [devices] = await db.query(
|
||||||
"SELECT id, version FROM devices WHERE pid = ?",
|
"SELECT id, version FROM devices WHERE pid = ?",
|
||||||
[pid]
|
[pid],
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (const device of devices) {
|
for (const device of devices) {
|
||||||
const deviceId = device.id;
|
const deviceId = device.id;
|
||||||
const version = device.version;
|
const version = device.version;
|
||||||
|
|
||||||
const [[inv]] = await db.query("SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?", [deviceId]);
|
const [[inv]] = await db.query(
|
||||||
const [[ver]] = await db.query("SELECT COUNT(*) AS c FROM version_outputs WHERE device_id = ?", [deviceId]);
|
"SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?",
|
||||||
const [[lic]] = await db.query("SELECT COUNT(*) AS c FROM license_outputs WHERE device_id = ?", [deviceId]);
|
[deviceId],
|
||||||
const [[log]] = await db.query("SELECT COUNT(*) AS c FROM logging_outputs WHERE device_id = ?", [deviceId]);
|
);
|
||||||
|
const [[ver]] = await db.query(
|
||||||
results.push({
|
"SELECT COUNT(*) AS c FROM version_outputs WHERE device_id = ?",
|
||||||
version,
|
[deviceId],
|
||||||
commands: {
|
);
|
||||||
inventory: inv.c,
|
const [[lic]] = await db.query(
|
||||||
version: ver.c,
|
"SELECT COUNT(*) AS c FROM license_outputs WHERE device_id = ?",
|
||||||
license: lic.c,
|
[deviceId],
|
||||||
logging: log.c,
|
);
|
||||||
}
|
const [[log]] = await db.query(
|
||||||
});
|
"SELECT COUNT(*) AS c FROM logging_outputs WHERE device_id = ?",
|
||||||
}
|
[deviceId],
|
||||||
|
);
|
||||||
res.json(results);
|
|
||||||
});
|
results.push({
|
||||||
|
version,
|
||||||
|
commands: {
|
||||||
|
inventory: inv.c,
|
||||||
|
version: ver.c,
|
||||||
|
license: lic.c,
|
||||||
|
logging: log.c,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/device/:pid/:version/:command", async (req, res) => {
|
app.get("/api/device/:pid/:version/:command", async (req, res) => {
|
||||||
const { pid, version, command } = req.params;
|
const { pid, version, command } = req.params;
|
||||||
|
|
||||||
const [[device]] = await db.query(
|
const [[device]] = await db.query(
|
||||||
`SELECT id FROM devices WHERE pid = ? AND version = ?`,
|
`SELECT id FROM devices WHERE pid = ? AND version = ?`,
|
||||||
[pid, version]
|
[pid, version],
|
||||||
);
|
);
|
||||||
if (!device) return res.status(404).json({ message: "Not found" });
|
if (!device) return res.status(404).json({ message: "Not found" });
|
||||||
|
|
||||||
const [logs] = await db.query(
|
const [logs] = await db.query(
|
||||||
`SELECT filename, output, created_at FROM ${command}_outputs WHERE device_id = ? ORDER BY created_at DESC`,
|
`SELECT id, filename, output, created_at FROM ${command}_outputs WHERE device_id = ? ORDER BY created_at DESC`,
|
||||||
[device.id]
|
[device.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json(logs);
|
res.json(logs);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/delete-logs", authenticateToken, async (req, res) => {
|
||||||
|
const items = req.body.items;
|
||||||
|
const userId = req.user?.id;
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
return res.status(400).json({ message: "Invalid payload" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const item of items) {
|
||||||
|
const { command, id } = item;
|
||||||
|
|
||||||
|
if (!command || typeof id !== "number") continue;
|
||||||
|
|
||||||
|
const allowedCommands = [
|
||||||
|
"inventory",
|
||||||
|
"version",
|
||||||
|
"license",
|
||||||
|
"logging",
|
||||||
|
];
|
||||||
|
if (!allowedCommands.includes(command)) continue;
|
||||||
|
|
||||||
|
const [rows] = await db.query(
|
||||||
|
`SELECT id FROM \`${command}_outputs\` WHERE id = ?`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete log
|
||||||
|
await db.query(`DELETE FROM \`${command}_outputs\` WHERE id = ?`, [
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
await db.query(
|
||||||
|
"UPDATE users SET confirm_count = confirm_count + 1 WHERE id = ?",
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Delete error:", error);
|
||||||
|
res.status(500).json({ success: false, message: "Server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Danh sách regex lọc lỗi
|
// Danh sách regex lọc lỗi
|
||||||
const errorPatterns = [
|
const errorPatterns = [
|
||||||
{
|
{
|
||||||
regex: /%\w+-[123]-\w+:/i,
|
regex: /%\w+-[123]-\w+:/i,
|
||||||
description: 'Lỗi mức độ 1–3 (critical, error, alert) trong định dạng syslog Cisco',
|
description:
|
||||||
},
|
"Lỗi mức độ 1–3 (critical, error, alert) trong định dạng syslog Cisco",
|
||||||
{
|
},
|
||||||
regex: /%\w+-4-\w+:/i,
|
{
|
||||||
description: 'Lỗi mức độ cảnh báo (level 4 - warning)',
|
regex: /%\w+-4-\w+:/i,
|
||||||
},
|
description: "Lỗi mức độ cảnh báo (level 4 - warning)",
|
||||||
{
|
},
|
||||||
regex: /(fail|error|unexpected|badcfg|invalid|denied|disabled|timeout|panic)/i,
|
{
|
||||||
description: 'Từ khóa phổ biến của lỗi trong nội dung log',
|
regex: /(fail|error|unexpected|badcfg|invalid|denied|disabled|timeout|panic)/i,
|
||||||
},
|
description: "Từ khóa phổ biến của lỗi trong nội dung log",
|
||||||
{
|
},
|
||||||
regex: /^\d{1,2}:\d{2}:\d{2}:.*?%\w+-[1-4]-\w+:/i,
|
{
|
||||||
description: 'Dòng log bắt đầu bằng timestamp có mã lỗi Cisco',
|
regex: /^\d{1,2}:\d{2}:\d{2}:.*?%\w+-[1-4]-\w+:/i,
|
||||||
},
|
description: "Dòng log bắt đầu bằng timestamp có mã lỗi Cisco",
|
||||||
];
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// API: Trả về các dòng lỗi từ bảng logging_outputs theo PID
|
// API: Trả về các dòng lỗi từ bảng logging_outputs theo PID
|
||||||
app.get('/api/errors/:pid', async (req, res) => {
|
app.get("/api/errors/:pid", async (req, res) => {
|
||||||
const pid = req.params.pid;
|
const pid = req.params.pid;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.execute(`
|
const [rows] = await db.execute(
|
||||||
|
`
|
||||||
SELECT lo.output, d.pid, lo.filename
|
SELECT lo.output, d.pid, lo.filename
|
||||||
FROM logging_outputs lo
|
FROM logging_outputs lo
|
||||||
JOIN devices d ON lo.device_id = d.id
|
JOIN devices d ON lo.device_id = d.id
|
||||||
WHERE d.pid = ?
|
WHERE d.pid = ?
|
||||||
`, [pid]);
|
`,
|
||||||
|
[pid],
|
||||||
const matchedLines = [];
|
);
|
||||||
|
|
||||||
for (const row of rows) {
|
const matchedLines = [];
|
||||||
const lines = row.output.split(/\r?\n/);
|
|
||||||
|
for (const row of rows) {
|
||||||
for (const line of lines.filter((l) => l.includes("*") || l.includes("%"))) {
|
const lines = row.output.split(/\r?\n/);
|
||||||
for (const pattern of errorPatterns) {
|
|
||||||
const regex = new RegExp(pattern.regex.source, 'gi');
|
for (const line of lines.filter(
|
||||||
if (regex.test(line)) {
|
(l) => l.includes("*") || l.includes("%"),
|
||||||
// Reset regex to start (because `.test()` may advance position in some cases)
|
)) {
|
||||||
regex.lastIndex = 0;
|
for (const pattern of errorPatterns) {
|
||||||
const highlighted = line.replace(regex, match => `<mark>${match}</mark>`);
|
const regex = new RegExp(pattern.regex.source, "gi");
|
||||||
|
if (regex.test(line)) {
|
||||||
matchedLines.push({
|
// Reset regex to start (because `.test()` may advance position in some cases)
|
||||||
pid: row.pid,
|
regex.lastIndex = 0;
|
||||||
filename: row.filename,
|
const highlighted = line.replace(
|
||||||
error_message: line.trim(),
|
regex,
|
||||||
highlighted_message: highlighted.trim(),
|
(match) => `<mark>${match}</mark>`,
|
||||||
regex_pattern: pattern.regex.source,
|
);
|
||||||
description: pattern.description,
|
|
||||||
});
|
matchedLines.push({
|
||||||
|
pid: row.pid,
|
||||||
break;
|
filename: row.filename,
|
||||||
}
|
error_message: line.trim(),
|
||||||
}
|
highlighted_message: highlighted.trim(),
|
||||||
}
|
regex_pattern: pattern.regex.source,
|
||||||
}
|
description: pattern.description,
|
||||||
|
});
|
||||||
const filtered = deduplicateErrors(matchedLines, 0.6);
|
|
||||||
res.json(filtered);
|
break;
|
||||||
} catch (err) {
|
}
|
||||||
console.error(err);
|
}
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const filtered = deduplicateErrors(matchedLines, 0.6);
|
||||||
app.get('/api/test', async (req, res) => {
|
res.json(filtered);
|
||||||
console.log(req)
|
} catch (err) {
|
||||||
res.send("ok")
|
console.error(err);
|
||||||
})
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/test", async (req, res) => {
|
||||||
|
console.log(req);
|
||||||
|
res.send("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Web Route
|
||||||
|
inititalWebRoute(app, db);
|
||||||
|
|
||||||
app.listen(PORT, () =>
|
app.listen(PORT, () =>
|
||||||
console.log(`🚀 Server running at http://localhost:${PORT}`)
|
console.log(`🚀 Server running at http://localhost:${PORT}`),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
335
server_old.js
335
server_old.js
|
|
@ -1,335 +0,0 @@
|
||||||
// === app.js ===
|
|
||||||
const express = require("express");
|
|
||||||
const mysql = require("mysql2/promise");
|
|
||||||
const bodyParser = require("body-parser");
|
|
||||||
const axios = require("axios");
|
|
||||||
const cheerio = require("cheerio");
|
|
||||||
const dotenv = require("dotenv");
|
|
||||||
dotenv.config();
|
|
||||||
const app = express();
|
|
||||||
const PORT = 4000;
|
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use(express.static("public"));
|
|
||||||
|
|
||||||
// === MySQL Connection ===
|
|
||||||
const db = mysql.createPool({
|
|
||||||
host: "localhost",
|
|
||||||
user: "root",
|
|
||||||
password: "",
|
|
||||||
database: "log_analysis",
|
|
||||||
});
|
|
||||||
|
|
||||||
// === Create Table If Not Exists ===
|
|
||||||
(async () => {
|
|
||||||
await db.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS pid_logs (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
pid VARCHAR(100),
|
|
||||||
version VARCHAR(100),
|
|
||||||
log TEXT,
|
|
||||||
commands JSON,
|
|
||||||
errors LONGTEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
})();
|
|
||||||
|
|
||||||
// === Regex-based command detection ===
|
|
||||||
function extractCommands(log) {
|
|
||||||
return {
|
|
||||||
inventory: /sh(ow)?\s+inv(entory)?/i.test(log),
|
|
||||||
version: /sh(ow)?\s+ver(sion)?/i.test(log),
|
|
||||||
license: /sh(ow)?\s+lic(ense)?/i.test(log),
|
|
||||||
logging: /sh(ow)?\s+log(ging)?/i.test(log),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractPIDVersion(log) {
|
|
||||||
const pidMatch = log.match(/PID:\s*([A-Z0-9\-]+)/);
|
|
||||||
const versionMatch = log.match(/Version\s+([\d\.A-Za-z\-]+)/);
|
|
||||||
return {
|
|
||||||
pid: pidMatch ? pidMatch[1] : "UNKNOWN",
|
|
||||||
version: versionMatch ? versionMatch[1] : "UNKNOWN",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractLogging(log) {
|
|
||||||
return log
|
|
||||||
.split("\n")
|
|
||||||
.filter(
|
|
||||||
(line) =>
|
|
||||||
line.startsWith("$ *") ||
|
|
||||||
line.startsWith("$ %") ||
|
|
||||||
line.startsWith("%") ||
|
|
||||||
line.includes("ALARM")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractRelevantLog(log, commands) {
|
|
||||||
const lines = log.split('\n');
|
|
||||||
const commandPatterns = [
|
|
||||||
{ key: 'inventory', regex: /sh(ow)?\s+inv(entory)?/i },
|
|
||||||
{ key: 'version', regex: /sh(ow)?\s+ver(sion)?/i },
|
|
||||||
{ key: 'license', regex: /sh(ow)?\s+lic(ense)?/i },
|
|
||||||
{ key: 'logging', regex: /sh(ow)?\s+log(ging)?/i },
|
|
||||||
];
|
|
||||||
|
|
||||||
let extracted = '';
|
|
||||||
const included = new Set();
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
for (const cmd of commandPatterns) {
|
|
||||||
const { key, regex } = cmd;
|
|
||||||
|
|
||||||
if (commands[key] && !included.has(key) && regex.test(lines[i])) {
|
|
||||||
included.add(key);
|
|
||||||
// console.log(log);
|
|
||||||
// Tách prompt và command
|
|
||||||
const match = lines[i].match(/^(.+?[#>])\s*(sh(ow)?\s+\w+)/i);
|
|
||||||
let prompt = '', command = '';
|
|
||||||
if (match) {
|
|
||||||
prompt = match[1]; // phần như "Switch#", "Router>", ...
|
|
||||||
command = match[2]; // phần như "show inventory", ...
|
|
||||||
}
|
|
||||||
console.log({prompt, command, lines: [lines[i], lines[i+1]]});
|
|
||||||
let block = [lines[i]];
|
|
||||||
|
|
||||||
for (let j = i + 1; j < lines.length; j++) {
|
|
||||||
// Nếu phát hiện dòng chứa command khác chưa được xử lý, thì dừng lại
|
|
||||||
// const isNextCommand = commandPatterns.some(c =>
|
|
||||||
// commands[c.key] && !included.has(c.key) && c.regex.test(lines[j])
|
|
||||||
// );
|
|
||||||
if (lines[j].includes(prompt)) break;
|
|
||||||
// if (isNextCommand) break;
|
|
||||||
|
|
||||||
block.push(lines[j]);
|
|
||||||
}
|
|
||||||
|
|
||||||
extracted += block.join('\n') + '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return extracted.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// === Search PID ===
|
|
||||||
app.get("/api/search", async (req, res) => {
|
|
||||||
const { pid } = req.query;
|
|
||||||
const [rows] = await db.query("SELECT * FROM pid_logs WHERE pid LIKE ?", [
|
|
||||||
`%${pid}%`,
|
|
||||||
]);
|
|
||||||
res.json(rows);
|
|
||||||
});
|
|
||||||
|
|
||||||
// === Add New Block ===
|
|
||||||
app.post("/api/add", async (req, res) => {
|
|
||||||
const { pid, version, log } = req.body;
|
|
||||||
const commands = extractCommands(log);
|
|
||||||
const errors = extractLogging(log);
|
|
||||||
const relevantLog = extractRelevantLog(log, commands);
|
|
||||||
await db.query(
|
|
||||||
`INSERT INTO pid_logs (pid, version, log, commands, errors) VALUES (?, ?, ?, ?, ?)`,
|
|
||||||
[
|
|
||||||
pid,
|
|
||||||
version,
|
|
||||||
`[${filename}]\n${relevantLog}`,
|
|
||||||
JSON.stringify(commands),
|
|
||||||
errors.join('\n'),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// === Ask AI to extract errors ===
|
|
||||||
app.post("/api/extract-errors", async (req, res) => {
|
|
||||||
const { log } = req.body;
|
|
||||||
const response = await axios.post(
|
|
||||||
"https://api.openai.com/v1/chat/completions",
|
|
||||||
{
|
|
||||||
model: "gpt-4o-mini",
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content:
|
|
||||||
"Extract and summarize hardware or configuration errors from this Cisco log block.",
|
|
||||||
},
|
|
||||||
{ role: "user", content: log },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
res.json({ errors: response.data.choices[0].message.content });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get versions for a specific PID
|
|
||||||
app.get("/api/pid/:pid/versions", async (req, res) => {
|
|
||||||
const { pid } = req.params;
|
|
||||||
const [rows] = await db.query(
|
|
||||||
"SELECT version, commands FROM pid_logs WHERE pid = ?",
|
|
||||||
[pid]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Nhóm version nếu bị trùng, lấy unique
|
|
||||||
const seen = new Set();
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
if (seen.has(row.version)) continue;
|
|
||||||
seen.add(row.version);
|
|
||||||
result.push({
|
|
||||||
version: row.version,
|
|
||||||
commands: row.commands,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Get full log + errors by pid and version
|
|
||||||
app.get("/api/pid/:pid/version/:version", async (req, res) => {
|
|
||||||
const { pid, version } = req.params;
|
|
||||||
const [rows] = await db.query(
|
|
||||||
"SELECT log, errors FROM pid_logs WHERE pid = ? AND version = ?",
|
|
||||||
[pid, version]
|
|
||||||
);
|
|
||||||
if (rows.length === 0) return res.status(404).json({ message: "Not found" });
|
|
||||||
res.json(rows[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/api/pids", async (req, res) => {
|
|
||||||
const [rows] = await db.query("SELECT DISTINCT pid FROM pid_logs ORDER BY pid");
|
|
||||||
res.json(rows.map((row) => row.pid));
|
|
||||||
});
|
|
||||||
|
|
||||||
// === Auto crawl logs from internal directory ===
|
|
||||||
async function fetchLogFile(url) {
|
|
||||||
const res = await axios.get(url);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function crawlLogs() {
|
|
||||||
const BASE_URL = "http://172.16.5.7:8080";
|
|
||||||
console.log(`[${new Date().toISOString()}] Start crawl ${BASE_URL}`);
|
|
||||||
const res = await axios.get(BASE_URL);
|
|
||||||
const $ = cheerio.load(res.data);
|
|
||||||
const links = $("a")
|
|
||||||
.map((_, el) => $(el).attr("href"))
|
|
||||||
.get()
|
|
||||||
.filter((href) => href.endsWith(".log"));
|
|
||||||
|
|
||||||
const splitBlocks = (logContent) =>
|
|
||||||
logContent
|
|
||||||
.split(/(?=^.*?[#>]\s*sh(ow)?\s+inv(entory)?)/gim)
|
|
||||||
.map((b) => b?.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
for (const link of links) {
|
|
||||||
const fileUrl = `${BASE_URL}/${link}`;
|
|
||||||
const filename = link.replace(/\//g, "_");
|
|
||||||
// const [exists] = await db.query(
|
|
||||||
// "SELECT COUNT(*) as count FROM pid_logs WHERE log LIKE ?",
|
|
||||||
// [`%${filename}%`]
|
|
||||||
// );
|
|
||||||
// if (exists[0].count > 0) {
|
|
||||||
// console.log(`🟡 Skipped: ${filename}`);
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
try {
|
|
||||||
const log = await fetchLogFile(fileUrl);
|
|
||||||
const blocks = splitBlocks(log);
|
|
||||||
|
|
||||||
for (const block of blocks) {
|
|
||||||
const { pid, version } = extractPIDVersion(block);
|
|
||||||
const commands = extractCommands(block);
|
|
||||||
const errors = extractLogging(block);
|
|
||||||
|
|
||||||
if (pid === "UNKNOWN" || version === "UNKNOWN") continue;
|
|
||||||
|
|
||||||
const [existing] = await db.query(
|
|
||||||
"SELECT * FROM pid_logs WHERE pid = ? AND version = ?",
|
|
||||||
[pid, version]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Nếu CHƯA CÓ -> INSERT mới toàn bộ
|
|
||||||
if (!existing[0]) {
|
|
||||||
const relevantLog = extractRelevantLog(block, commands);
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
`INSERT INTO pid_logs (pid, version, log, commands, errors) VALUES (?, ?, ?, ?, ?)`,
|
|
||||||
[
|
|
||||||
pid,
|
|
||||||
version,
|
|
||||||
`[${filename}]\n${relevantLog}`,
|
|
||||||
JSON.stringify(commands),
|
|
||||||
errors.join('\n'),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Added ${pid} ${version} (${filename})`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nếu ĐÃ CÓ -> Kiểm tra và UPDATE nếu cần
|
|
||||||
const oldCommands = existing[0].commands;
|
|
||||||
|
|
||||||
const isSameCommands = Object.keys(commands).every(
|
|
||||||
(key) => commands[key] === oldCommands[key]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isSameCommands) {
|
|
||||||
console.log(`🟡 Skipped existing PID+Version: ${pid} ${version}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCommands = {};
|
|
||||||
for (const key of Object.keys(commands)) {
|
|
||||||
if (commands[key] === true && oldCommands[key] === false) {
|
|
||||||
newCommands[key] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(newCommands).length === 0) {
|
|
||||||
console.log(`🟠 No new true commands → skip ${pid} ${version}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newRelevantLog = extractRelevantLog(block, newCommands);
|
|
||||||
const updatedCommands = { ...oldCommands, ...newCommands };
|
|
||||||
const updatedLog = `${existing[0].log}\n[${filename}]\n${newRelevantLog}`;
|
|
||||||
|
|
||||||
await db.query(
|
|
||||||
`UPDATE pid_logs SET log = ?, commands = ?, errors = ? WHERE pid = ? AND version = ?`,
|
|
||||||
[
|
|
||||||
updatedLog,
|
|
||||||
JSON.stringify(updatedCommands),
|
|
||||||
`${existing[0].errors || ''}\n${errors.join('\n')}`.trim(),
|
|
||||||
pid,
|
|
||||||
version,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`🟢 Updated with new command(s) for ${pid} ${version}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`❌ Error ${filename}: ${err.message} ${err.stack}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Run crawl on start ===
|
|
||||||
// crawlLogs();
|
|
||||||
|
|
||||||
app.listen(PORT, () =>
|
|
||||||
console.log(`Server running at http://localhost:${PORT}`)
|
|
||||||
);
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
|
||||||
|
const createToken = (payload) => {
|
||||||
|
return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyToken = (token) => {
|
||||||
|
return jwt.verify(token, JWT_SECRET);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { createToken, verifyToken };
|
||||||
Loading…
Reference in New Issue