chore: init
This commit is contained in:
commit
1d518fe1d8
29 changed files with 12867 additions and 0 deletions
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Build output
|
||||
/dist/
|
||||
|
||||
# Backend configuration
|
||||
/src/config.php
|
||||
|
||||
# Node.JS development dependencies
|
||||
node_modules/
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
|
||||
# Editor directories and files
|
||||
*.swa-p
|
2
.husky/commit-msg
Executable file
2
.husky/commit-msg
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
npx --no -- commitlint --edit "$1"
|
2
.husky/pre-commit
Executable file
2
.husky/pre-commit
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
npx lint-staged
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 SVR.JS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"]
|
||||
};
|
63
db/database.sql
Normal file
63
db/database.sql
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*!999999\- enable the sandbox mode */
|
||||
-- MariaDB dump 10.19 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: statistics
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.6.18-MariaDB-0ubuntu0.22.04.1
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `entries`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `entries`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `entries` (
|
||||
`id` int(10) NOT NULL AUTO_INCREMENT,
|
||||
`ip` varchar(100) NOT NULL,
|
||||
`time` datetime NOT NULL,
|
||||
`version` varchar(100) NOT NULL,
|
||||
`runtime` varchar(100) NOT NULL,
|
||||
`runtime_version` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `entries_mods`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `entries_mods`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `entries_mods` (
|
||||
`id` int(10) NOT NULL AUTO_INCREMENT,
|
||||
`entry_id` int(10) NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`version` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2024-08-06 14:32:28
|
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
8
frontend/README.md
Normal file
8
frontend/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
40
frontend/eslint.config.js
Normal file
40
frontend/eslint.config.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import react from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import prettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
|
||||
export default [
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
files: ["**/*.{js,jsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: "module"
|
||||
}
|
||||
},
|
||||
settings: { react: { version: "18.3" } },
|
||||
plugins: {
|
||||
react,
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs["jsx-runtime"].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
prettierRecommended
|
||||
];
|
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SVR.JS statistics</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>SVR.JS statistics dashboard requires JavaScript to work correctly.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
3
frontend/lint-staged.config.js
Normal file
3
frontend/lint-staged.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
"src/**/*": "eslint --cache --fix"
|
||||
};
|
7991
frontend/package-lock.json
generated
Normal file
7991
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
42
frontend/package.json
Normal file
42
frontend/package.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "svrjs-statistics-server-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/poppins": "^5.1.0",
|
||||
"apexcharts": "^4.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"unfetch": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cssnano": "^7.0.6",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.14",
|
||||
"globals": "^15.12.0",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-css-variables": "^0.19.0",
|
||||
"prettier": "^3.4.2",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"vite": "^6.0.1"
|
||||
}
|
||||
}
|
8
frontend/postcss.config.js
Normal file
8
frontend/postcss.config.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
"postcss-css-variables": {},
|
||||
cssnano: {}
|
||||
}
|
||||
};
|
15
frontend/prettier.config.js
Normal file
15
frontend/prettier.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
// prettier.config.js, .prettierrc.js, prettier.config.cjs, or .prettierrc.cjs
|
||||
|
||||
/**
|
||||
* @see https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
trailingComma: "none",
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
endOfLine: "lf"
|
||||
};
|
||||
|
||||
export default config;
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
279
frontend/src/App.jsx
Normal file
279
frontend/src/App.jsx
Normal file
|
@ -0,0 +1,279 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
|
||||
function generateChartOptions(data) {
|
||||
return {
|
||||
xaxis: {
|
||||
categories: Object.keys(data)
|
||||
},
|
||||
yaxis: {},
|
||||
theme: {
|
||||
mode: "dark",
|
||||
monochrome: {
|
||||
enabled: true,
|
||||
color: "#007000"
|
||||
}
|
||||
},
|
||||
chart: {
|
||||
fontFamily: "Poppins, sans-serif"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function generateChartSeries(data, series) {
|
||||
return [
|
||||
{
|
||||
name: series,
|
||||
data: Object.keys(data).map((key) => data[key])
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [dailyStarts, setDailyStarts] = useState(0);
|
||||
const [monthlyStarts, setMonthlyStarts] = useState(0);
|
||||
const [yearlyStarts, setYearlyStarts] = useState(0);
|
||||
const [totalStarts, setTotalStarts] = useState(0);
|
||||
const [versionDistribution, setVersionDistribution] = useState({});
|
||||
const [modDistribution, setModDistribution] = useState({});
|
||||
const [jsRuntimeDistribution, setJsRuntimeDistribution] = useState({});
|
||||
const [jsRuntimeVersionDistribution, setJsRuntimeVersionDistribution] =
|
||||
useState({});
|
||||
const [latestStarts, setLatestStarts] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async (endpoint) => {
|
||||
const res = await fetch(endpoint);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.message);
|
||||
return data;
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
setDailyStarts(
|
||||
(await fetchData("./api.php?scope=count&period=daily")).count
|
||||
);
|
||||
setMonthlyStarts(
|
||||
(await fetchData("./api.php?scope=count&period=monthly")).count
|
||||
);
|
||||
setYearlyStarts(
|
||||
(await fetchData("./api.php?scope=count&period=yearly")).count
|
||||
);
|
||||
setTotalStarts(
|
||||
(await fetchData("./api.php?scope=count&period=total")).count
|
||||
);
|
||||
setVersionDistribution(
|
||||
(
|
||||
await fetchData(
|
||||
"./api.php?scope=usage&subscope=versiondistribution"
|
||||
)
|
||||
).usage
|
||||
);
|
||||
setModDistribution(
|
||||
(await fetchData("./api.php?scope=usage&subscope=modsdistribution"))
|
||||
.usage
|
||||
);
|
||||
setJsRuntimeDistribution(
|
||||
(
|
||||
await fetchData(
|
||||
"./api.php?scope=usage&subscope=jsruntimedistribution"
|
||||
)
|
||||
).usage
|
||||
);
|
||||
setJsRuntimeVersionDistribution(
|
||||
(
|
||||
await fetchData(
|
||||
"./api.php?scope=usage&subscope=jsruntimeversiondistribution"
|
||||
)
|
||||
).usage
|
||||
);
|
||||
setLatestStarts(await fetchData("./api.php?scope=starts"));
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
load();
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl mb-4 md:mb-6 font-bold">
|
||||
Unexpected error
|
||||
</h1>
|
||||
<p className="md:text-lg lg:text-xl text-muted-foreground my-4 md:my-6">
|
||||
An unexpected error occurred: {error.message}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
} else if (loading) {
|
||||
return (
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl mb-4 md:mb-6 font-bold">
|
||||
Loading...
|
||||
</h1>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl mb-4 md:mb-6 font-bold">
|
||||
SVR.JS statistics
|
||||
</h1>
|
||||
<p className="md:text-lg lg:text-xl text-muted-foreground my-4 md:my-6">
|
||||
The statistics will be reloaded every minute.
|
||||
</p>
|
||||
<h2 className="text-2xl md:text-3xl lg:text-4xl my-4 md:my-6 font-bold">
|
||||
Insights
|
||||
</h2>
|
||||
<div className="flex flex-col md:flex-row md:flex-wrap w-full">
|
||||
<div className="w-full md:w-1/2 p-1 md:p-2">
|
||||
<div className="p-2 border border-border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-base md:text-xl font-bold mb-2">
|
||||
Daily SVR.JS starts
|
||||
</h3>
|
||||
<span className="text-4xl md:text-5xl">{dailyStarts}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 p-1 md:p-2">
|
||||
<div className="p-2 border border-border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-base md:text-xl font-bold mb-2">
|
||||
Monthly SVR.JS starts
|
||||
</h3>
|
||||
<span className="text-4xl md:text-5xl">{monthlyStarts}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 p-1 md:p-2">
|
||||
<div className="p-2 border border-border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-base md:text-xl font-bold mb-2">
|
||||
Yearly SVR.JS starts
|
||||
</h3>
|
||||
<span className="text-4xl md:text-5xl">{yearlyStarts}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 p-1 md:p-2">
|
||||
<div className="p-2 border border-border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-base md:text-xl font-bold mb-2">
|
||||
Total SVR.JS starts
|
||||
</h3>
|
||||
<span className="text-4xl md:text-5xl">{totalStarts}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 p-1 md:p-2">
|
||||
<div className="p-2 border border-border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-base md:text-xl font-bold mb-2">
|
||||
SVR.JS version distribution (monthly)
|
||||
</h3>
|
||||
<Chart
|
||||
type="bar"
|
||||
options={generateChartOptions(versionDistribution)}
|
||||
series={generateChartSeries(
|
||||
versionDistribution,
|
||||
"SVR.JS version distribution (monthly)"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 p-1 md:p-2">
|
||||
<div className="p-2 border border-border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-base md:text-xl font-bold mb-2">
|
||||
SVR.JS mods distribution (monthly)
|
||||
</h3>
|
||||
<Chart
|
||||
type="bar"
|
||||
options={generateChartOptions(modDistribution)}
|
||||
series={generateChartSeries(
|
||||
modDistribution,
|
||||
"SVR.JS mods distribution (monthly)"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 p-1 md:p-2">
|
||||
<div className="p-2 border border-border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-base md:text-xl font-bold mb-2">
|
||||
JavaScript runtime distribution (monthly)
|
||||
</h3>
|
||||
<Chart
|
||||
type="bar"
|
||||
options={generateChartOptions(jsRuntimeDistribution)}
|
||||
series={generateChartSeries(
|
||||
jsRuntimeDistribution,
|
||||
"JavaScript runtime distribution (monthly)"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 p-1 md:p-2">
|
||||
<div className="p-2 border border-border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-base md:text-xl font-bold mb-2">
|
||||
JS runtime version distribution (monthly)
|
||||
</h3>
|
||||
<Chart
|
||||
type="bar"
|
||||
options={generateChartOptions(jsRuntimeVersionDistribution)}
|
||||
series={generateChartSeries(
|
||||
jsRuntimeVersionDistribution,
|
||||
"JS runtime version distribution (monthly)"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl lg:text-4xl my-4 md:my-6 font-bold">
|
||||
{latestStarts.limit} latest SVR.JS starts
|
||||
</h2>
|
||||
{latestStarts.starts.map((start, index) => {
|
||||
return (
|
||||
<div
|
||||
className="my-2 w-full bg-card border-border border rounded-lg p-1 md:p-2"
|
||||
key={index}
|
||||
>
|
||||
<h3 className="text-2xl md:text-3xl my-1 md:mb-2 font-bold">
|
||||
{start.ip}
|
||||
</h3>
|
||||
<p className="text-sm md:text-base my-1">
|
||||
<b className="font-bold">SVR.JS version:</b> {start.version} |{" "}
|
||||
<b className="font-bold">JavaScript runtime:</b> {start.runtime}{" "}
|
||||
| <b className="font-bold">JavaScript runtime version:</b>{" "}
|
||||
{start.runtimeVersion}
|
||||
</p>
|
||||
{start.mods && start.mods.length > 0 ? (
|
||||
<>
|
||||
<h4 className="text-lg md:text-xl font-bold">
|
||||
Installed mods:
|
||||
</h4>
|
||||
<ul className="border-border border bg-background text-foreground rounded-lg my-1">
|
||||
{start.mods.map((mod, index) => {
|
||||
return (
|
||||
<li
|
||||
className={`text-sm md:text-base p-1 border-border ${index == start.mods.length - 1 ? "" : "border-b"}`}
|
||||
key={index}
|
||||
>
|
||||
<b className="font-bold">{mod.name}</b> {mod.version}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
64
frontend/src/index.css
Normal file
64
frontend/src/index.css
Normal file
|
@ -0,0 +1,64 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-family: "Poppins", sans-serif;
|
||||
background-color: hsla(var(--background), 1);
|
||||
color: hsla(var(--foreground), 1);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--svg-fill: white;
|
||||
--svg-background: black;
|
||||
--background: 0, 0%, 100%;
|
||||
--foreground: 240, 10%, 3.9%;
|
||||
--card: 0, 0%, 100%;
|
||||
--card-foreground: 240, 10%, 3.9%;
|
||||
--popover: 0, 0%, 100%;
|
||||
--popover-foreground: 240, 10%, 3.9%;
|
||||
--primary: 142.1, 76.2%, 36.3%;
|
||||
--primary-foreground: 355.7, 100%, 97.3%;
|
||||
--secondary: 240, 4.8%, 95.9%;
|
||||
--secondary-foreground: 240, 5.9%, 10%;
|
||||
--muted: 240, 4.8%, 95.9%;
|
||||
--muted-foreground: 240, 3.8%, 46.1%;
|
||||
--accent: 240, 4.8%, 95.9%;
|
||||
--accent-foreground: 240, 5.9%, 10%;
|
||||
--destructive: 0, 84.2%, 60.2%;
|
||||
--destructive-foreground: 0, 0%, 98%;
|
||||
--border: 240, 5.9%, 90%;
|
||||
--input: 240, 5.9%, 90%;
|
||||
--ring: 142.1, 76.2%, 36.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--svg-fill: black;
|
||||
--svg-background: white;
|
||||
--background: 20, 14.3%, 4.1%;
|
||||
--foreground: 0, 0%, 95%;
|
||||
--card: 24, 9.8%, 10%;
|
||||
--card-foreground: 0, 0%, 95%;
|
||||
--popover: 0, 0%, 9%;
|
||||
--popover-foreground: 0, 0%, 95%;
|
||||
--primary: 142.1, 70.6%, 45.3%;
|
||||
--primary-foreground: 144.9, 80.4%, 10%;
|
||||
--secondary: 240, 3.7%, 15.9%;
|
||||
--secondary-foreground: 0, 0%, 98%;
|
||||
--muted: 0, 0%, 15%;
|
||||
--muted-foreground: 240, 5%, 64.9%;
|
||||
--accent: 12, 6.5%, 15.1%;
|
||||
--accent-foreground: 0, 0%, 98%;
|
||||
--destructive: 0, 62.8%, 30.6%;
|
||||
--destructive-foreground: 0, 85.7%, 97.3%;
|
||||
--border: 240, 3.7%, 15.9%;
|
||||
--input: 240, 3.7%, 15.9%;
|
||||
--ring: 142.4, 71.8%, 29.2%;
|
||||
}
|
||||
}
|
||||
}
|
14
frontend/src/main.jsx
Normal file
14
frontend/src/main.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "@fontsource/poppins";
|
||||
import "@fontsource/poppins/700.css";
|
||||
import "./index.css";
|
||||
import App from "./App.jsx";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<StrictMode>
|
||||
<div className="max-w-screen-lg mx-auto lg:my-8 xl:my-16 p-2">
|
||||
<App />
|
||||
</div>
|
||||
</StrictMode>
|
||||
);
|
63
frontend/tailwind.config.js
Normal file
63
frontend/tailwind.config.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsla(var(--border), 1)",
|
||||
input: "hsla(var(--input), 1)",
|
||||
ring: "hsla(var(--ring), 1)",
|
||||
background: "hsla(var(--background), 1)",
|
||||
foreground: "hsla(var(--foreground), 1)",
|
||||
primary: {
|
||||
DEFAULT: "hsla(var(--primary), 1)",
|
||||
foreground: "hsla(var(--primary-foreground), 1)"
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsla(var(--secondary), 1)",
|
||||
foreground: "hsla(var(--secondary-foreground), 1)"
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsla(var(--destructive), 1)",
|
||||
foreground: "hsla(var(--destructive-foreground), 1)"
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsla(var(--muted), 1)",
|
||||
foreground: "hsla(var(--muted-foreground), 1)"
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsla(var(--accent), 1)",
|
||||
foreground: "hsla(var(--accent-foreground), 1)"
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsla(var(--popover), 1)",
|
||||
foreground: "hsla(var(--popover-foreground), 1)"
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsla(var(--card), 1)",
|
||||
foreground: "hsla(var(--card-foreground), 1)"
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: `var(--radius)`,
|
||||
md: `calc(var(--radius) - 2px)`,
|
||||
sm: "calc(var(--radius) - 4px)"
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" }
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out"
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
32
frontend/vite.config.js
Normal file
32
frontend/vite.config.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import legacy from "@vitejs/plugin-legacy";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
legacy({
|
||||
targets: ["firefox >= 52", "chrome >= 49", "safari >= 11", "edge >= 12"],
|
||||
additionalLegacyPolyfills: ["unfetch/polyfill"]
|
||||
})
|
||||
],
|
||||
build: {
|
||||
outDir: "../dist",
|
||||
emptyOutDir: false,
|
||||
chunkSizeWarningLimit: 800
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src"
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"^/(?:(?!\\.php(?:$|[?#])).)+.php(?:$|[?#])": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
3
lint-staged.config.js
Normal file
3
lint-staged.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
"src/**/*.php": "prettier --list-different"
|
||||
};
|
3638
package-lock.json
generated
Normal file
3638
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
package.json
Normal file
39
package.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "svrjs-statistics-server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:frontend": "cd frontend && npm run build",
|
||||
"build:backend": "node scripts/copyBackend.js",
|
||||
"build": "rimraf dist && npm run build:backend && npm run build:frontend",
|
||||
"cz": "cz",
|
||||
"dev": "concurrently \"php -t src/ -S localhost:8000\" \"cd frontend && npm run dev\"",
|
||||
"lint:backend": "prettier src/**/*.php --list-different",
|
||||
"lint:backend-fix": "prettier src/**/*.php --write",
|
||||
"lint:frontend": "cd frontend && npm run lint",
|
||||
"lint:frontend-fix": "cd frontend && npm run lint:fix",
|
||||
"lint": "npm run lint:backend && npm run lint:frontend",
|
||||
"lint:fix": "npm run lint:backend-fix && npm run lint:frontend-fix",
|
||||
"postinstall": "cd frontend && npm install",
|
||||
"prepare": "husky",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.6.1",
|
||||
"@commitlint/config-conventional": "^19.6.0",
|
||||
"@prettier/plugin-php": "^0.22.2",
|
||||
"commitizen": "^4.3.1",
|
||||
"concurrently": "^9.1.0",
|
||||
"cpr": "^3.0.1",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.11",
|
||||
"prettier": "^3.4.2",
|
||||
"rimraf": "^5.0.10"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
16
prettier.config.js
Normal file
16
prettier.config.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
// prettier.config.js, .prettierrc.js, prettier.config.cjs, or .prettierrc.cjs
|
||||
|
||||
/**
|
||||
* @see https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
plugins: ["@prettier/plugin-php"],
|
||||
trailingComma: "none",
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
endOfLine: "lf"
|
||||
};
|
||||
|
||||
module.exports = config;
|
22
scripts/copyBackend.js
Normal file
22
scripts/copyBackend.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
const fs = require("fs");
|
||||
const cpr = require("cpr");
|
||||
const path = require("path");
|
||||
|
||||
const srcFolder = path.resolve(__dirname, "..", "src");
|
||||
const dbFolder = path.resolve(__dirname, "..", "db");
|
||||
const distFolder = path.resolve(__dirname, "..", "dist");
|
||||
|
||||
fs.mkdir(distFolder, (err) => {
|
||||
if (err && err.code != "EEXIST") throw err;
|
||||
cpr(srcFolder, distFolder, {
|
||||
overwrite: true,
|
||||
filter: (pathname) => pathname != path.resolve(srcFolder, "config.php")
|
||||
}, (err) => {
|
||||
if (err) throw err;
|
||||
cpr(dbFolder, distFolder, {
|
||||
overwrite: true
|
||||
}, (err) => {
|
||||
if (err) throw err;
|
||||
})
|
||||
})
|
||||
});
|
264
src/api.php
Normal file
264
src/api.php
Normal file
|
@ -0,0 +1,264 @@
|
|||
<?php
|
||||
|
||||
include "./config.php";
|
||||
|
||||
$mysqlDriver = new mysqli_driver();
|
||||
$mysqlDriver->report_mode = MYSQLI_REPORT_OFF;
|
||||
|
||||
header("Content-Type: application/json");
|
||||
|
||||
if (!isset($_GET["scope"])) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
"status" => 400,
|
||||
"message" => "No scope defined.",
|
||||
]);
|
||||
exit();
|
||||
} elseif ($_GET["scope"] == "count") {
|
||||
$query = "";
|
||||
if (!isset($_GET["period"])) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
"status" => 400,
|
||||
"message" => "No period defined.",
|
||||
]);
|
||||
exit();
|
||||
} elseif ($_GET["period"] == "daily") {
|
||||
$query =
|
||||
"SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);";
|
||||
} elseif ($_GET["period"] == "weekly") {
|
||||
$query =
|
||||
"SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 WEEK);";
|
||||
} elseif ($_GET["period"] == "monthly") {
|
||||
$query =
|
||||
"SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH);";
|
||||
} elseif ($_GET["period"] == "yearly" || $_GET["period"] == "annual") {
|
||||
$query =
|
||||
"SELECT COUNT(*) AS 'count' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 YEAR);";
|
||||
} elseif ($_GET["period"] == "total" || $_GET["period"] == "all") {
|
||||
$query = "SELECT COUNT(*) AS 'count' FROM entries;";
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
"status" => 400,
|
||||
"message" => "Invalid period.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$connection = new mysqli(
|
||||
MYSQL_HOST,
|
||||
MYSQL_USERNAME,
|
||||
MYSQL_PASSWORD,
|
||||
MYSQL_DATABASE,
|
||||
MYSQL_PORT
|
||||
);
|
||||
|
||||
if ($connection->connect_error) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->connect_error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$result = $connection->query($query);
|
||||
if (!$result) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
if (!$row || !isset($row["count"])) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: The row is either missing or invalid.");
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
"status" => 200,
|
||||
"count" => intval($row["count"]),
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
} elseif ($_GET["scope"] == "usage") {
|
||||
$query = "";
|
||||
if (!isset($_GET["subscope"])) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
"status" => 400,
|
||||
"message" => "No subscope defined.",
|
||||
]);
|
||||
exit();
|
||||
} elseif ($_GET["subscope"] == "versiondistribution") {
|
||||
$query =
|
||||
"SELECT CONCAT('SVR.JS ', version) AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY version;";
|
||||
} elseif ($_GET["subscope"] == "modsdistribution") {
|
||||
$query =
|
||||
"SELECT entries_mods.name AS 'key', COUNT(*) AS 'value' FROM entries_mods INNER JOIN entries ON entries_mods.entry_id = entries.id WHERE entries.time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY entries_mods.name;";
|
||||
} elseif ($_GET["subscope"] == "jsruntimedistribution") {
|
||||
$query =
|
||||
"SELECT runtime AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY runtime;";
|
||||
} elseif ($_GET["subscope"] == "jsruntimeversiondistribution") {
|
||||
$query =
|
||||
"SELECT CONCAT(runtime, ' ', runtime_version) AS 'key', COUNT(*) AS 'value' FROM entries WHERE time >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY runtime, runtime_version;";
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
"status" => 400,
|
||||
"message" => "Invalid subscope.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$connection = new mysqli(
|
||||
MYSQL_HOST,
|
||||
MYSQL_USERNAME,
|
||||
MYSQL_PASSWORD,
|
||||
MYSQL_DATABASE,
|
||||
MYSQL_PORT
|
||||
);
|
||||
|
||||
if ($connection->connect_error) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->connect_error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$result = $connection->query($query);
|
||||
if (!$result) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
$usage = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
if (isset($row["key"]) && isset($row["value"])) {
|
||||
$usage[$row["key"]] = $row["value"];
|
||||
}
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
"status" => 200,
|
||||
"usage" => $usage,
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
} elseif ($_GET["scope"] == "starts") {
|
||||
$connection = new mysqli(
|
||||
MYSQL_HOST,
|
||||
MYSQL_USERNAME,
|
||||
MYSQL_PASSWORD,
|
||||
MYSQL_DATABASE,
|
||||
MYSQL_PORT
|
||||
);
|
||||
|
||||
if ($connection->connect_error) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->connect_error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$result = $connection->query(
|
||||
"SELECT id, ip, time, version, runtime, runtime_version FROM entries ORDER BY id DESC LIMIT 10;"
|
||||
);
|
||||
if (!$result) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
$starts = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$start = [
|
||||
"ip" => $row["ip"],
|
||||
"time" => $row["time"],
|
||||
"version" => $row["version"],
|
||||
"runtime" => $row["runtime"],
|
||||
"runtimeVersion" => $row["runtime_version"],
|
||||
"mods" => [],
|
||||
];
|
||||
|
||||
$startID = intval($row["id"]);
|
||||
$result2 = $connection->query(
|
||||
"SELECT name, version FROM entries_mods WHERE entry_id = $startID;"
|
||||
);
|
||||
if (!$result2) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
}
|
||||
while ($row2 = $result2->fetch_assoc()) {
|
||||
array_push($start["mods"], [
|
||||
"name" => $row2["name"],
|
||||
"version" => $row2["version"],
|
||||
]);
|
||||
}
|
||||
array_push($starts, $start);
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
"status" => 200,
|
||||
"limit" => 10,
|
||||
"starts" => $starts,
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
"status" => 400,
|
||||
"message" => "Invalid scope.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
?>
|
172
src/collect.php
Normal file
172
src/collect.php
Normal file
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
|
||||
include "./config.php";
|
||||
|
||||
$mysqlDriver = new mysqli_driver();
|
||||
$mysqlDriver->report_mode = MYSQLI_REPORT_OFF;
|
||||
|
||||
header("Content-Type: application/json");
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
if (
|
||||
isset($_SERVER["CONTENT_TYPE"]) &&
|
||||
!preg_match('/^application\/json(?:$|;)/', $_SERVER["CONTENT_TYPE"])
|
||||
) {
|
||||
header("Accept: application/json", true, 415);
|
||||
echo json_encode([
|
||||
"status" => 415,
|
||||
"message" => "Only JSON is supported.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$jsonData = file_get_contents("php://input");
|
||||
$parsedJsonData = json_decode($jsonData, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
"status" => 400,
|
||||
"message" => "JSON parse error.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$isValid = true;
|
||||
if (
|
||||
!isset($parsedJsonData["version"]) ||
|
||||
!isset($parsedJsonData["runtime"]) ||
|
||||
!isset($parsedJsonData["runtimeVersion"]) ||
|
||||
!isset($parsedJsonData["mods"]) ||
|
||||
!is_string($parsedJsonData["version"]) ||
|
||||
!is_string($parsedJsonData["runtime"]) ||
|
||||
!is_string($parsedJsonData["runtimeVersion"]) ||
|
||||
!is_array($parsedJsonData["mods"])
|
||||
) {
|
||||
$isValid = false;
|
||||
} elseif (!in_array($parsedJsonData["runtime"], ["Node.js", "Bun", "Deno"])) {
|
||||
$isValid = false;
|
||||
} else {
|
||||
foreach ($parsedJsonData["mods"] as $mod) {
|
||||
if (
|
||||
!isset($mod["version"]) ||
|
||||
!is_string($mod["name"]) ||
|
||||
!is_string($mod["version"])
|
||||
) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isValid) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
"status" => 400,
|
||||
"message" => "Invalid data.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$requestIP = $_SERVER["REMOTE_ADDR"];
|
||||
|
||||
$dnsRecords = dns_get_record(DOMAIN, DNS_A + DNS_AAAA);
|
||||
|
||||
if ($dnsRecords) {
|
||||
$ipAddresses = array_map(
|
||||
function ($record) {
|
||||
return isset($record["ip"]) ? $record["ip"] : $record["ipv6"];
|
||||
},
|
||||
array_filter($dnsRecords, function ($record) {
|
||||
return isset($record["ip"]) || isset($record["ipv6"]);
|
||||
})
|
||||
);
|
||||
|
||||
if (in_array($requestIP, $ipAddresses)) {
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
"status" => 200,
|
||||
"message" => "The statistics are added successfully.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$connection = new mysqli(
|
||||
MYSQL_HOST,
|
||||
MYSQL_USERNAME,
|
||||
MYSQL_PASSWORD,
|
||||
MYSQL_DATABASE,
|
||||
MYSQL_PORT
|
||||
);
|
||||
|
||||
if ($connection->connect_error) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->connect_error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$escapedRequestIP = $connection->real_escape_string($requestIP);
|
||||
$version = $connection->real_escape_string($parsedJsonData["version"]);
|
||||
$runtime = $connection->real_escape_string($parsedJsonData["runtime"]);
|
||||
$runtimeVersion = $connection->real_escape_string(
|
||||
$parsedJsonData["runtimeVersion"]
|
||||
);
|
||||
|
||||
$query = "INSERT INTO entries (ip, time, version, runtime, runtime_version) VALUES ('$escapedRequestIP', NOW(), '$version', '$runtime', '$runtimeVersion')";
|
||||
if (!$connection->query($query)) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
$entryId = intval($connection->insert_id);
|
||||
$entriesToInsert = [];
|
||||
foreach ($parsedJsonData["mods"] as $mod) {
|
||||
$name = $connection->real_escape_string($mod["name"]);
|
||||
$modVersion = $connection->real_escape_string($mod["version"]);
|
||||
$entriesToInsert[] = "($entryId, '$name', '$modVersion')";
|
||||
}
|
||||
|
||||
if (!empty($entriesToInsert)) {
|
||||
$query =
|
||||
"INSERT INTO entries_mods (entry_id, name, version) VALUES " .
|
||||
implode(", ", $entriesToInsert);
|
||||
if (!$connection->query($query)) {
|
||||
error_log("There was an error while processing the request!");
|
||||
error_log("Error: " . $connection->error);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
"status" => 500,
|
||||
"message" => "An unexpected error occurred.",
|
||||
]);
|
||||
$connection->close();
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
"status" => 200,
|
||||
"message" => "The statistics are added successfully.",
|
||||
]);
|
||||
$connection->close();
|
||||
} else {
|
||||
header("Allow: POST", true, 405);
|
||||
echo json_encode([
|
||||
"status" => 405,
|
||||
"message" => "Invalid HTTP method.",
|
||||
]);
|
||||
}
|
||||
?>
|
11
src/config.example.php
Normal file
11
src/config.example.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
// MySQL/MariaDB configuration
|
||||
define("MYSQL_HOST", "localhost");
|
||||
define("MYSQL_USERNAME", "statistics");
|
||||
define("MYSQL_PASSWORD", "statistics");
|
||||
define("MYSQL_DATABASE", "statistics");
|
||||
define("MYSQL_PORT", null);
|
||||
|
||||
// Domain name of the statistics server to ignore statistics request from
|
||||
define("DOMAIN", "localhost");
|
||||
?>
|
Loading…
Reference in a new issue