Initial commit: SREBOT website (Express/EJS + i18n) - extracted from SREBOT monorepo
@@ -0,0 +1,18 @@
|
|||||||
|
# Environment
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Domain Configuration (CORS)
|
||||||
|
PRODUCTION_DOMAIN=https://sre.pawjob.us
|
||||||
|
|
||||||
|
# External API Configuration
|
||||||
|
EXTERNAL_API_URL=http://localhost:6000
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_WINDOW_MS=60000
|
||||||
|
RATE_LIMIT_MAX_REQUESTS=100
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Operating System Files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Debug and test files
|
||||||
|
debug-*.js
|
||||||
|
test-*.js
|
||||||
|
|
||||||
|
# Build output (obfuscated files and generated CSS)
|
||||||
|
public/js/dist/
|
||||||
|
public/css/output.css.map
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 692 B |
|
After Width: | Height: | Size: 593 B |
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Sop
|
||||||
|
|
||||||
|
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.
|
||||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 627 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 819 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 786 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1012 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 773 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1006 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 777 KiB |
|
After Width: | Height: | Size: 818 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 927 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 948 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 849 KiB |
|
After Width: | Height: | Size: 829 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 929 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 858 KiB |
|
After Width: | Height: | Size: 885 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 818 KiB |
|
After Width: | Height: | Size: 5.5 MiB |
@@ -0,0 +1,55 @@
|
|||||||
|
# SREBOT-web
|
||||||
|
|
||||||
|
Toothless SQB Bot Website — Express.js + EJS frontend with Tailwind CSS and i18n support (10 languages).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Squadron leaderboard, profiles, and stats
|
||||||
|
- Player profiles with vehicle stats
|
||||||
|
- Game detail pages with replay visualization
|
||||||
|
- Season timeline and comparison tools
|
||||||
|
- Tournament bracket viewer
|
||||||
|
- Analytics dashboard
|
||||||
|
- Multi-language support (en, ru, fr, it, uk, de, es, pl, cs, zh-CN)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
cp env.example .env # edit with your values
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # builds CSS + obfuscated JS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
node server.js # or: npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## PM2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run pm2:start # cluster mode (3 instances)
|
||||||
|
npm run pm2:logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
See `env.example` for all available variables. Key ones:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|---|---|
|
||||||
|
| `PORT` | Server port (default: 3001) |
|
||||||
|
| `EXTERNAL_API_URL` | SREBOT API URL (default: http://localhost:6000) |
|
||||||
|
| `STORAGE_VOL_PATH` | Storage volume path (required) |
|
||||||
|
| `PYTHON_BIN` | Python binary for replay rendering |
|
||||||
|
| `RECAP_SCRIPT` | Path to render_recap.py (optional) |
|
||||||
|
| `SHARED_ICONS_DIR` | Path to vehicle icons directory |
|
||||||
|
| `SHARED_MINIMAPS_DIR` | Path to minimaps directory |
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const postcss = require('postcss');
|
||||||
|
const tailwindcss = require('tailwindcss');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
const cssnano = require('cssnano');
|
||||||
|
|
||||||
|
const inputPath = './public/css/tailwind.css';
|
||||||
|
const outputPath = './public/css/output.css';
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
async function buildCSS() {
|
||||||
|
console.log('[CSS Build] Reading input file...');
|
||||||
|
const css = fs.readFileSync(inputPath, 'utf8');
|
||||||
|
|
||||||
|
console.log(`[CSS Build] Processing with PostCSS and Tailwind... (${isProduction ? 'production' : 'development'} mode)`);
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
tailwindcss(),
|
||||||
|
autoprefixer()
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only minify in production
|
||||||
|
if (isProduction) {
|
||||||
|
plugins.push(cssnano({
|
||||||
|
preset: ['default', {
|
||||||
|
discardComments: {
|
||||||
|
removeAll: true,
|
||||||
|
},
|
||||||
|
normalizeWhitespace: true,
|
||||||
|
}]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await postcss(plugins).process(css, {
|
||||||
|
from: inputPath,
|
||||||
|
to: outputPath,
|
||||||
|
map: !isProduction ? { inline: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[CSS Build] Writing output file...');
|
||||||
|
fs.writeFileSync(outputPath, result.css);
|
||||||
|
|
||||||
|
if (result.map && !isProduction) {
|
||||||
|
fs.writeFileSync(outputPath + '.map', result.map.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeKB = (Buffer.byteLength(result.css, 'utf8') / 1024).toFixed(2);
|
||||||
|
console.log('[CSS Build] ✓ CSS build complete!');
|
||||||
|
console.log(`[CSS Build] Output: ${outputPath} (${sizeKB} KB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCSS().catch(err => {
|
||||||
|
console.error('[CSS Build] Error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
const JavaScriptObfuscator = require('javascript-obfuscator');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const PUBLIC_JS_DIR = path.join(__dirname, 'public', 'js');
|
||||||
|
const OUTPUT_DIR = path.join(__dirname, 'public', 'js', 'dist');
|
||||||
|
|
||||||
|
// Obfuscation options - balanced between security and performance
|
||||||
|
const obfuscationOptions = {
|
||||||
|
compact: true,
|
||||||
|
controlFlowFlattening: true,
|
||||||
|
controlFlowFlatteningThreshold: 0.75,
|
||||||
|
deadCodeInjection: true,
|
||||||
|
deadCodeInjectionThreshold: 0.4,
|
||||||
|
debugProtection: false,
|
||||||
|
debugProtectionInterval: 0,
|
||||||
|
disableConsoleOutput: true,
|
||||||
|
identifierNamesGenerator: 'hexadecimal',
|
||||||
|
log: false,
|
||||||
|
numbersToExpressions: true,
|
||||||
|
renameGlobals: false,
|
||||||
|
selfDefending: true,
|
||||||
|
simplify: true,
|
||||||
|
splitStrings: true,
|
||||||
|
splitStringsChunkLength: 10,
|
||||||
|
stringArray: true,
|
||||||
|
stringArrayCallsTransform: true,
|
||||||
|
stringArrayEncoding: ['base64'],
|
||||||
|
stringArrayIndexShift: true,
|
||||||
|
stringArrayRotate: true,
|
||||||
|
stringArrayShuffle: true,
|
||||||
|
stringArrayWrappersCount: 2,
|
||||||
|
stringArrayWrappersChainedCalls: true,
|
||||||
|
stringArrayWrappersParametersMaxCount: 4,
|
||||||
|
stringArrayWrappersType: 'function',
|
||||||
|
stringArrayThreshold: 0.75,
|
||||||
|
transformObjectKeys: true,
|
||||||
|
unicodeEscapeSequence: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create output directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||||
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[BUILD] Starting JavaScript obfuscation...');
|
||||||
|
|
||||||
|
// Get all JS files in the public/js directory
|
||||||
|
const SKIP_FILES = ['replay-canvas.js', 'replay-canvas-3d.js'];
|
||||||
|
const jsFiles = fs.readdirSync(PUBLIC_JS_DIR).filter(file =>
|
||||||
|
file.endsWith('.js') && !file.startsWith('.') && !SKIP_FILES.includes(file)
|
||||||
|
);
|
||||||
|
|
||||||
|
let obfuscatedCount = 0;
|
||||||
|
|
||||||
|
jsFiles.forEach(file => {
|
||||||
|
const inputPath = path.join(PUBLIC_JS_DIR, file);
|
||||||
|
const outputPath = path.join(OUTPUT_DIR, file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[BUILD] Obfuscating ${file}...`);
|
||||||
|
|
||||||
|
const sourceCode = fs.readFileSync(inputPath, 'utf8');
|
||||||
|
const obfuscationResult = JavaScriptObfuscator.obfuscate(sourceCode, obfuscationOptions);
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, obfuscationResult.getObfuscatedCode());
|
||||||
|
|
||||||
|
const originalSize = (fs.statSync(inputPath).size / 1024).toFixed(2);
|
||||||
|
const obfuscatedSize = (fs.statSync(outputPath).size / 1024).toFixed(2);
|
||||||
|
|
||||||
|
console.log(`[BUILD] ✓ ${file} (${originalSize}KB → ${obfuscatedSize}KB)`);
|
||||||
|
obfuscatedCount++;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[BUILD] ✗ Failed to obfuscate ${file}:`, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n[BUILD] Obfuscation complete! ${obfuscatedCount}/${jsFiles.length} files processed.`);
|
||||||
|
console.log(`[BUILD] Obfuscated files saved to: ${OUTPUT_DIR}`);
|
||||||
|
console.log('[BUILD] To use obfuscated files in production, set NODE_ENV=production');
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
2026-I
|
||||||
|
|
||||||
|
week 1 (01.01 — 07.01) <t:1767225600:R> max BR 14.3
|
||||||
|
week 2 (08.01 — 14.01) <t:1767830400:R> max BR 12.0
|
||||||
|
week 3 (15.01 — 21.01) <t:1768435200:R> max BR 11.0
|
||||||
|
week 4 (22.01 — 28.01) <t:1769040000:R> max BR 10.0
|
||||||
|
week 5 (29.01 — 04.02) <t:1769644800:R> max BR 9.0
|
||||||
|
week 6 (05.02 — 11.02) <t:1770249600:R> max BR 8.0
|
||||||
|
week 7 (12.02 — 18.02) <t:1770854400:R> max BR 7.0
|
||||||
|
week 8 (19.02 — 23.02) <t:1771459200:R> max BR 6.0
|
||||||
|
until eos (24.02 — 28.02) <t:1771891200:R> max BR 5.0
|
||||||
|
|
||||||
|
2026-II
|
||||||
|
|
||||||
|
week 1 (01.03 — 08.03) <t:1772348400:R> max BR 14.3
|
||||||
|
week 2 (09.03 — 15.03) <t:1773039600:R> max BR 12.0
|
||||||
|
week 3 (16.03 — 22.03) <t:1773644400:R> max BR 10.7
|
||||||
|
week 4 (23.03 — 29.03) <t:1774249200:R> max BR 9.7
|
||||||
|
week 5 (30.03 — 05.04) <t:1774854000:R> max BR 8.7
|
||||||
|
week 6 (06.04 — 12.04) <t:1775458800:R> max BR 7.3
|
||||||
|
week 7 (13.04 — 19.04) <t:1776063600:R> max BR 6.3
|
||||||
|
week 8 (20.04 — 26.04) <t:1776668400:R> max BR 5.7
|
||||||
|
until eos (27.04 — 30.04) <t:1777273200:R> max BR 4.7
|
||||||
|
|
||||||
|
2026-III
|
||||||
|
|
||||||
|
week 1 (01.05 — 07.05) <t:1777618800:R> max BR 14.3
|
||||||
|
week 2 (08.05 — 14.05) <t:1778223600:R> max BR 12.0
|
||||||
|
week 3 (15.05 — 21.05) <t:1778828400:R> max BR 11.0
|
||||||
|
week 4 (22.05 — 28.05) <t:1779433200:R> max BR 10.0
|
||||||
|
week 5 (29.05 — 04.06) <t:1780038000:R> max BR 9.0
|
||||||
|
week 6 (05.06 — 11.06) <t:1780642800:R> max BR 8.0
|
||||||
|
week 7 (12.06 — 18.06) <t:1781247600:R> max BR 7.0
|
||||||
|
week 8 (19.06 — 25.06) <t:1781852400:R> max BR 6.0
|
||||||
|
until eos (26.06 — 30.06) <t:1782457200:R> max BR 5.0
|
||||||
|
|
||||||
|
2026-IV
|
||||||
|
|
||||||
|
week 1 (01.07 — 07.07) <t:1782889200:R> max BR 14.7
|
||||||
|
week 2 (08.07 — 14.07) <t:1783494000:R> max BR 12.0
|
||||||
|
week 3 (15.07 — 21.07) <t:1784098800:R> max BR 10.7
|
||||||
|
week 4 (22.07 — 28.07) <t:1784703600:R> max BR 9.7
|
||||||
|
week 5 (29.07 — 04.08) <t:1785308400:R> max BR 8.7
|
||||||
|
week 6 (05.08 — 11.08) <t:1785913200:R> max BR 7.3
|
||||||
|
week 7 (12.08 — 18.08) <t:1786518000:R> max BR 6.3
|
||||||
|
week 8 (19.08 — 25.08) <t:1787122800:R> max BR 5.7
|
||||||
|
until eos (26.08 — 31.08) <t:1787727600:R> max BR 4.7
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "[DEPLOY] Starting deployment process..."
|
||||||
|
echo "[DEPLOY] Deployment started at: $(date)"
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[DEPLOY] Pulling latest changes from Git..."
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "[ERROR] Git pull failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[DEPLOY] Checking for package.json changes..."
|
||||||
|
PACKAGE_CHANGED=$(git diff HEAD@{1} HEAD --name-only | grep -q "package.json" && echo "yes" || echo "no")
|
||||||
|
|
||||||
|
if [ "$PACKAGE_CHANGED" = "yes" ]; then
|
||||||
|
echo "[DEPLOY] package.json has changed, running npm install..."
|
||||||
|
npm install
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "[ERROR] npm install failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[DEPLOY] Running npm audit fix..."
|
||||||
|
npm audit fix
|
||||||
|
else
|
||||||
|
echo "[DEPLOY] No package.json changes detected, skipping npm install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[DEPLOY] Building CSS with Tailwind..."
|
||||||
|
npm run build:css
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "[ERROR] CSS build failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[DEPLOY] Building obfuscated JavaScript files..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "[WARN] JS build failed, but continuing deployment..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[DEPLOY] Restarting PM2 process 3..."
|
||||||
|
pm2 restart 3
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "[ERROR] PM2 restart failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[DEPLOY] Deployment completed successfully!"
|
||||||
|
echo "[DEPLOY] Deployment finished at: $(date)"
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# ============================================
|
||||||
|
# Environment Configuration Example
|
||||||
|
# Copy this file to .env and fill in your actual values
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3001
|
||||||
|
|
||||||
|
# External API Configuration (srebot-api)
|
||||||
|
EXTERNAL_API_URL=http://localhost:6000
|
||||||
|
|
||||||
|
# Domain Config (used for CORS)
|
||||||
|
PRODUCTION_DOMAIN=https://sre.pawjob.us
|
||||||
|
|
||||||
|
# Path to .env file (defaults to ./env in this directory)
|
||||||
|
# DOTENV_PATH=/path/to/.env
|
||||||
|
|
||||||
|
# Storage volume (required - holds replays, squadrons, entitlements data)
|
||||||
|
STORAGE_VOL_PATH=/mnt/HC_Volume_105581488/STORAGE
|
||||||
|
|
||||||
|
# Legacy replays directory (optional, for backwards compatibility)
|
||||||
|
# LEGACY_REPLAYS_ROOT=/path/to/replays
|
||||||
|
|
||||||
|
# Python binary for replay rendering and recap generation
|
||||||
|
# PYTHON_BIN=/path/to/python3
|
||||||
|
|
||||||
|
# Path to render_recap.py script (optional, enables recap generation)
|
||||||
|
# RECAP_SCRIPT=/path/to/BOT/render_recap.py
|
||||||
|
|
||||||
|
# Root of the SREBOT repo (used as cwd for Python scripts)
|
||||||
|
# BOT_REPO_ROOT=/path/to/SREBOT
|
||||||
|
|
||||||
|
# SHARED icon and minimap directories
|
||||||
|
# SHARED_ICONS_DIR=/path/to/SHARED/ICONS
|
||||||
|
# SHARED_MINIMAPS_DIR=/path/to/SHARED/MAPS/MINIMAPS
|
||||||
|
|
||||||
|
# API Security (optional - auto-generates if not set)
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
API_SECRET=
|
||||||
|
|
||||||
|
# IP Whitelist (optional - comma-separated IPs for production)
|
||||||
|
ALLOWED_IPS=
|
||||||
|
|
||||||
|
# Webhook Configuration (optional - for GitHub auto-deployment)
|
||||||
|
# Generate a secure random string for this
|
||||||
|
WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# PM2 Commands:
|
||||||
|
# ============================================
|
||||||
|
# Start with PM2:
|
||||||
|
# npm run pm2:start
|
||||||
|
#
|
||||||
|
# Other PM2 commands:
|
||||||
|
# npm run pm2:stop - Stop the app
|
||||||
|
# npm run pm2:restart - Restart the app
|
||||||
|
# npm run pm2:reload - Zero-downtime reload
|
||||||
|
# npm run pm2:logs - View logs
|
||||||
|
# npm run pm2:monit - Monitor dashboard
|
||||||
|
# npm run pm2:delete - Remove from PM2
|
||||||
|
#
|
||||||
|
# Auto-start on reboot:
|
||||||
|
# pm2 startup
|
||||||
|
# pm2 save
|
||||||
|
# ============================================
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "toothless-sqb-bot-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Website for Toothless SQB Discord Bot",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "cross-env NODE_ENV=production node server.js",
|
||||||
|
"dev": "npm run build:css && concurrently \"npm run watch:css\" \"cross-env NODE_ENV=development nodemon server.js\"",
|
||||||
|
"build": "npm run build:css && node build.js",
|
||||||
|
"build:css": "node build-css.js",
|
||||||
|
"watch:css": "nodemon --watch public/css/tailwind.css --watch tailwind.config.js --exec \"node build-css.js\"",
|
||||||
|
"build:prod": "npm run build:css && node build.js && cross-env NODE_ENV=production node server.js",
|
||||||
|
"pm2:start": "npm run build && pm2 start ecosystem.config.js",
|
||||||
|
"pm2:stop": "pm2 stop toothless-sqb-web",
|
||||||
|
"pm2:restart": "pm2 restart toothless-sqb-web",
|
||||||
|
"pm2:reload": "pm2 reload toothless-sqb-web",
|
||||||
|
"pm2:delete": "pm2 delete toothless-sqb-web",
|
||||||
|
"pm2:logs": "pm2 logs toothless-sqb-web",
|
||||||
|
"pm2:monit": "pm2 monit",
|
||||||
|
"test": "echo \"No tests specified yet. Please run 'npm run dev' to start the development server.\""
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"discord",
|
||||||
|
"bot",
|
||||||
|
"website",
|
||||||
|
"express",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"author": "Sophie :3",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"compression": "^1.8.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"ejs": "^3.1.9",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"node-fetch": "^2.7.0",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
|
"cssnano": "^7.1.2",
|
||||||
|
"javascript-obfuscator": "^4.1.0",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.18"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Custom Fonts */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'skyquakesymbols';
|
||||||
|
src: url('/Fonts/symbols_skyquake.ttf');
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply m-0 p-0 box-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply font-sans leading-relaxed text-white overflow-x-hidden min-h-screen antialiased;
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url('/images/toothless_face.webp');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply scroll-smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* ========================================
|
||||||
|
COLOR SCHEME:
|
||||||
|
- Background: Dark earth green (#1C1E1D) to graphite (#0A0B0A)
|
||||||
|
- Accent/Primary text: Cream (#F5F5DC)
|
||||||
|
- Secondary/Muted text: Mint green (#90EE90)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Vehicle, Squadron, and Player names use custom font */
|
||||||
|
.vehicle-name,
|
||||||
|
.squadron-name,
|
||||||
|
.squadron-tag,
|
||||||
|
.player-name,
|
||||||
|
.player-nick {
|
||||||
|
@apply font-skyquake;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary Button - Cream gradient */
|
||||||
|
.btn-primary {
|
||||||
|
@apply inline-flex items-center justify-center px-6 py-3 font-bold rounded-lg
|
||||||
|
transition-all duration-300 relative overflow-hidden;
|
||||||
|
background: linear-gradient(135deg, #F5F5DC 0%, #E8E8D0 100%);
|
||||||
|
color: #1E1E1E;
|
||||||
|
box-shadow: 0 4px 20px rgba(245, 245, 220, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
box-shadow: 0 8px 25px rgba(245, 245, 220, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation callouts */
|
||||||
|
.nav-premium {
|
||||||
|
color: #f4d35e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-premium:hover {
|
||||||
|
color: #ffe08a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-rainbow {
|
||||||
|
color: #ff9b8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-rainbow:hover {
|
||||||
|
color: #ffc0b4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-donate {
|
||||||
|
background: linear-gradient(90deg, #ff7a7a 0%, #ffd166 25%, #90ee90 50%, #8fd3ff 75%, #c79bff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-donate:hover {
|
||||||
|
filter: brightness(1.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-donate i {
|
||||||
|
-webkit-text-fill-color: initial;
|
||||||
|
color: #ffd166;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary Button */
|
||||||
|
.btn-secondary {
|
||||||
|
@apply inline-flex items-center justify-center px-6 py-3
|
||||||
|
bg-white/10 text-white font-semibold rounded-lg border-2 border-primary-400/50
|
||||||
|
transition-all duration-300 backdrop-blur-sm
|
||||||
|
hover:-translate-y-0.5 hover:bg-primary-400/20 hover:border-primary-400
|
||||||
|
active:translate-y-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feature Card - used on homepage */
|
||||||
|
.feature-card {
|
||||||
|
background: linear-gradient(135deg, rgba(62, 78, 62, 0.2) 0%, rgba(44, 44, 44, 0.2) 100%);
|
||||||
|
border: 1px solid rgba(245, 245, 220, 0.08);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
background: linear-gradient(135deg, rgba(62, 78, 62, 0.3) 0%, rgba(44, 44, 44, 0.3) 100%);
|
||||||
|
border-color: rgba(144, 238, 144, 0.3);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover i {
|
||||||
|
color: #F5F5DC;
|
||||||
|
transform: scale(1.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Input - Glass effect */
|
||||||
|
.search-input-glass {
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
border: 1px solid rgba(245, 245, 220, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-glass:focus {
|
||||||
|
background: rgba(40, 40, 40, 0.8);
|
||||||
|
border-color: rgba(144, 238, 144, 0.4);
|
||||||
|
box-shadow: 0 0 0 2px rgba(144, 238, 144, 0.1), inset 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Component */
|
||||||
|
.card {
|
||||||
|
@apply bg-dark-100/80 backdrop-blur-md rounded-2xl border border-primary-400/20
|
||||||
|
p-6 transition-all duration-300
|
||||||
|
hover:border-primary-400/40 hover:shadow-lg hover:shadow-primary-400/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Card (more transparent) */
|
||||||
|
.glass-card {
|
||||||
|
@apply bg-dark-100/60 backdrop-blur-xl rounded-2xl border border-primary-400/20
|
||||||
|
p-6 transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat Card */
|
||||||
|
.stat-card {
|
||||||
|
@apply card text-center relative overflow-hidden
|
||||||
|
hover:-translate-y-1 hover:border-primary-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Field */
|
||||||
|
.input-field {
|
||||||
|
@apply w-full px-4 py-3 bg-dark-200/80 border-2 border-primary-400/30
|
||||||
|
rounded-lg text-white placeholder-white/50
|
||||||
|
transition-all duration-300 backdrop-blur-sm
|
||||||
|
focus:outline-none focus:border-primary-400 focus:shadow-lg focus:shadow-primary-400/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select Dropdown */
|
||||||
|
.select-field {
|
||||||
|
@apply input-field cursor-pointer appearance-none pr-10;
|
||||||
|
background-image: url('data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%2339ff14%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3e%3cpolyline points=%226 9 12 15 18 9%22%3e%3c/polyline%3e%3c/svg%3e');
|
||||||
|
background-position: right 0.75rem center;
|
||||||
|
background-size: 1.25rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styles */
|
||||||
|
.data-table {
|
||||||
|
@apply w-full border-collapse bg-dark-200/80 rounded-xl overflow-hidden
|
||||||
|
shadow-xl border border-primary-400/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table thead {
|
||||||
|
@apply bg-gradient-to-r from-primary-400/20 to-primary-500/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
@apply px-4 py-3 text-white font-semibold text-center border-b-2 border-primary-400/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
@apply px-4 py-3 text-center text-white/90 border-b border-primary-400/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr {
|
||||||
|
@apply transition-all duration-300 hover:bg-gradient-to-r hover:from-primary-400/10
|
||||||
|
hover:to-primary-500/10 hover:scale-[1.01];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
|
.spinner {
|
||||||
|
@apply border-4 border-primary-400/30 border-t-primary-400 rounded-full
|
||||||
|
w-10 h-10 animate-spin mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge */
|
||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold
|
||||||
|
bg-primary-400/10 text-primary-400 border border-primary-400/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar */
|
||||||
|
.navbar {
|
||||||
|
@apply fixed top-0 w-full bg-dark-300/95 backdrop-blur-md z-50
|
||||||
|
border-b border-primary-400/20 transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
.container-custom {
|
||||||
|
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Title */
|
||||||
|
.section-title {
|
||||||
|
@apply text-4xl font-bold text-center mb-4 bg-gradient-to-r from-primary-400 via-primary-500 to-primary-400
|
||||||
|
bg-clip-text text-transparent bg-[length:200%_auto] animate-gradient-shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link Hover Effect */
|
||||||
|
.link-hover {
|
||||||
|
@apply relative text-white transition-colors duration-300
|
||||||
|
hover:text-primary-400
|
||||||
|
after:content-[''] after:absolute after:bottom-0 after:left-0
|
||||||
|
after:w-0 after:h-0.5 after:bg-gradient-to-r after:from-primary-400 after:to-primary-500
|
||||||
|
after:transition-all after:duration-300
|
||||||
|
hover:after:w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date Filter Styles */
|
||||||
|
.date-filter-container {
|
||||||
|
@apply glass-card space-y-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button {
|
||||||
|
@apply px-4 py-2 rounded-lg border border-primary-400/30 bg-dark-100/50
|
||||||
|
text-white font-medium transition-all duration-300
|
||||||
|
hover:border-primary-400 hover:bg-primary-400/10
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary-400/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button-active {
|
||||||
|
@apply filter-button border-primary-400 bg-primary-400/20 text-primary-400 shadow-lg shadow-primary-400/20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* Text Gradient */
|
||||||
|
.text-gradient {
|
||||||
|
@apply bg-gradient-to-r from-primary-400 via-primary-500 to-primary-600
|
||||||
|
bg-clip-text text-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Effect */
|
||||||
|
.glass {
|
||||||
|
@apply bg-white/5 backdrop-blur-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow Effect */
|
||||||
|
.glow {
|
||||||
|
@apply shadow-lg shadow-primary-400/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styles */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
@apply w-2 h-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
@apply bg-dark-200 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-primary-400/30 rounded hover:bg-primary-400/50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional custom animations */
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0%, 100% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scrollRight {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
100% { transform: translateX(-50%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row link overlay - makes entire table row clickable with native right-click support */
|
||||||
|
tr.row-link {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
tr.row-link a.row-link-overlay::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
tr.row-link td a:not(.row-link-overlay),
|
||||||
|
tr.row-link td button,
|
||||||
|
tr.row-link td [tabindex],
|
||||||
|
tr.row-link td input,
|
||||||
|
tr.row-link td select,
|
||||||
|
tr.row-link td textarea {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 10 MiB |