This commit is contained in:
Heidi
2026-05-15 00:15:40 +01:00
parent ba8755fc84
commit 6b2e15f7bf
11 changed files with 3407 additions and 1 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
.env
*.log
+38 -1
View File
@@ -1 +1,38 @@
# linkweb # Toothless' Bot Home
React + Vite + Tailwind link home running on port `3020`.
## Local
```sh
npm install
npm run build
npm run serve
```
Open `http://localhost:3020`.
## PM2
```sh
npm run build
pm2 start ecosystem.config.cjs
```
The app name is `linkweb`. The GitHub webhook receiver is:
```text
POST /webhook/github
```
Set `WEBHOOK_SECRET` in `ecosystem.config.cjs` if you want GitHub `x-hub-signature-256` verification:
```js
env: {
NODE_ENV: 'production',
PORT: '3020',
PM2_APP_NAME: 'linkweb',
WEBHOOK_PATH: '/webhook/github',
WEBHOOK_SECRET: 'your-secret',
}
```
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
apps: [
{
name: 'linkweb',
script: 'server.js',
env: {
NODE_ENV: 'production',
PORT: '3020',
PM2_APP_NAME: 'linkweb',
WEBHOOK_PATH: '/webhook/github',
},
},
],
};
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#1e0618" />
<title>Toothless' Bot Home</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+3059
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "toothless-bot-home",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 3020",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0 --port 3020",
"serve": "node server.js"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"@vitejs/plugin-react": "^5.0.0",
"express": "^5.1.0",
"lucide-react": "^0.511.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.7",
"vite": "^6.3.5"
}
}
+71
View File
@@ -0,0 +1,71 @@
import crypto from 'node:crypto';
import { execFile } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import express from 'express';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const port = Number(process.env.PORT || 3020);
const webhookPath = process.env.WEBHOOK_PATH || '/webhook/github';
const webhookSecret = process.env.WEBHOOK_SECRET || '';
const pm2AppName = process.env.PM2_APP_NAME || 'linkweb';
const pm2Command = process.env.PM2_BIN || (process.platform === 'win32' ? 'pm2.cmd' : 'pm2');
function verifyGitHubSignature(req) {
if (!webhookSecret) {
return true;
}
const signature = req.get('x-hub-signature-256');
if (!signature?.startsWith('sha256=')) {
return false;
}
const digest = `sha256=${crypto
.createHmac('sha256', webhookSecret)
.update(req.body)
.digest('hex')}`;
const received = Buffer.from(signature);
const expected = Buffer.from(digest);
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
app.post(webhookPath, express.raw({ type: '*/*' }), (req, res) => {
if (!verifyGitHubSignature(req)) {
res.status(401).json({ ok: false, error: 'Invalid webhook signature' });
return;
}
res.status(202).json({ ok: true, restarting: pm2AppName });
execFile(pm2Command, ['restart', pm2AppName], (error, stdout, stderr) => {
if (error) {
console.error('PM2 restart failed:', error.message);
if (stderr) {
console.error(stderr);
}
return;
}
if (stdout) {
console.log(stdout);
}
});
});
app.get('/healthz', (_req, res) => {
res.json({ ok: true });
});
app.use(express.static(path.join(__dirname, 'dist')));
app.use((_req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
app.listen(port, () => {
console.log(`Toothless' Bot Home listening on port ${port}`);
});
+90
View File
@@ -0,0 +1,90 @@
import { Bot, ExternalLink, MessageCircle, Sparkles } from 'lucide-react';
const links = [
{
name: 'TSSBot',
href: 'https://tss.pawjob.us/',
description: 'TSSBot - War Thunder Tournaments Scheduling Service',
accent: 'bg-cerulean-500',
icon: Bot,
},
{
name: 'SREBot',
href: 'https://sre.pawjob.us/',
description: 'SREBot - The best squadron battles bot',
accent: 'bg-yale-blue-500',
icon: Bot,
},
{
name: 'Spectra',
href: 'https://spectra.artvv.dev/',
description: 'Spectra - WE ARE POWERED BY THEM',
accent: 'bg-prussian-blue-400',
icon: Sparkles,
},
{
name: 'Support Discord',
href: 'https://discord.gg/VXPcQaJerE',
description: 'For support, questions and contact.',
accent: 'bg-white-400',
icon: MessageCircle,
},
];
function App() {
return (
<main className="min-h-screen bg-deep-navy-950 text-yale-blue-50">
<section className="mx-auto flex min-h-screen w-full max-w-5xl flex-col justify-center px-5 py-10 sm:px-8">
<div className="mb-8 max-w-3xl border-b border-prussian-blue-700 pb-6">
<p className="mb-3 text-sm font-semibold uppercase tracking-[0.16em] text-cerulean-300">
Gateway
</p>
<h1 className="text-4xl font-bold leading-tight text-white sm:text-5xl">
Toothless&apos; Bot Home.
</h1>
<p className="mt-4 max-w-2xl text-base leading-7 text-yale-blue-100 sm:text-lg">
Our projects and affiliates.
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{links.map((link) => {
const Icon = link.icon;
return (
<a
className="group relative min-h-36 rounded-lg border border-prussian-blue-700 bg-prussian-blue-900 p-5 transition duration-150 hover:border-cerulean-400 hover:bg-prussian-blue-800 focus:outline-none focus:ring-2 focus:ring-cerulean-300"
href={link.href}
key={link.name}
rel="noreferrer"
target="_blank"
>
<span className={`absolute bottom-0 left-0 top-0 w-1 rounded-l-lg ${link.accent}`} />
<span className="flex h-full flex-col justify-between gap-8">
<span className="flex items-start justify-between gap-4">
<span className="grid size-11 place-items-center rounded-md border border-prussian-blue-600 bg-deep-navy-900 text-cerulean-200">
<Icon aria-hidden="true" className="size-6" strokeWidth={2.2} />
</span>
<ExternalLink
aria-hidden="true"
className="size-5 text-yale-blue-200/70 transition group-hover:text-cerulean-200"
/>
</span>
<span>
<span className="block text-2xl font-extrabold text-white">{link.name}</span>
<span className="mt-2 block text-sm leading-6 text-yale-blue-100/82">
{link.description}
</span>
</span>
</span>
</a>
);
})}
</div>
</section>
</main>
);
}
export default App;
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './styles.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);
+79
View File
@@ -0,0 +1,79 @@
@import "tailwindcss";
@theme {
--color-prussian-blue-50: #ebeefa;
--color-prussian-blue-100: #d6ddf5;
--color-prussian-blue-200: #adbceb;
--color-prussian-blue-300: #859ae0;
--color-prussian-blue-400: #5c78d6;
--color-prussian-blue-500: #3357cc;
--color-prussian-blue-600: #2945a3;
--color-prussian-blue-700: #1f347a;
--color-prussian-blue-800: #142352;
--color-prussian-blue-900: #0a1129;
--color-prussian-blue-950: #070c1d;
--color-deep-navy-50: #e5efff;
--color-deep-navy-100: #ccdfff;
--color-deep-navy-200: #99beff;
--color-deep-navy-300: #669eff;
--color-deep-navy-400: #337eff;
--color-deep-navy-500: #005eff;
--color-deep-navy-600: #004bcc;
--color-deep-navy-700: #003899;
--color-deep-navy-800: #002566;
--color-deep-navy-900: #001333;
--color-deep-navy-950: #000d24;
--color-yale-blue-50: #e6f3fe;
--color-yale-blue-100: #cde6fe;
--color-yale-blue-200: #9ccefc;
--color-yale-blue-300: #6ab5fb;
--color-yale-blue-400: #389cfa;
--color-yale-blue-500: #0684f9;
--color-yale-blue-600: #0569c7;
--color-yale-blue-700: #044f95;
--color-yale-blue-800: #033563;
--color-yale-blue-900: #011a32;
--color-yale-blue-950: #011223;
--color-cerulean-50: #e8f8fc;
--color-cerulean-100: #d1f1fa;
--color-cerulean-200: #a3e3f5;
--color-cerulean-300: #75d5f0;
--color-cerulean-400: #47c7eb;
--color-cerulean-500: #19b9e6;
--color-cerulean-600: #1494b8;
--color-cerulean-700: #0f6f8a;
--color-cerulean-800: #0a4a5c;
--color-cerulean-900: #05252e;
--color-cerulean-950: #041a20;
--color-white-50: #faf0eb;
--color-white-100: #f5e0d6;
--color-white-200: #ebc2ad;
--color-white-300: #e0a385;
--color-white-400: #d6855c;
--color-white-500: #cc6633;
--color-white-600: #a35229;
--color-white-700: #7a3d1f;
--color-white-800: #522914;
--color-white-900: #29140a;
--color-white-950: #1d0e07;
}
:root {
color-scheme: dark;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
background: #000d24;
}
body {
margin: 0;
}
* {
box-sizing: border-box;
}
+7
View File
@@ -0,0 +1,7 @@
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});