This commit is contained in:
hibna
2026-02-21 13:02:41 +03:00
commit 2215003a4d
59 changed files with 6100 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@source/api",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "eslint src/"
},
"dependencies": {
"@fastify/cookie": "^11.0.0",
"@fastify/cors": "^10.0.0",
"@fastify/jwt": "^9.0.0",
"@fastify/websocket": "^11.0.0",
"@sinclair/typebox": "^0.34.0",
"@source/database": "workspace:*",
"@source/shared": "workspace:*",
"argon2": "^0.41.0",
"fastify": "^5.2.0",
"pino-pretty": "^13.0.0",
"socket.io": "^4.8.0"
},
"devDependencies": {
"tsx": "^4.19.0"
}
}
+33
View File
@@ -0,0 +1,33 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import cookie from '@fastify/cookie';
const app = Fastify({
logger: {
transport: {
target: 'pino-pretty',
},
},
});
await app.register(cors, {
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
});
await app.register(cookie);
app.get('/api/health', async () => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
const PORT = Number(process.env.PORT) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
try {
await app.listen({ port: PORT, host: HOST });
app.log.info(`API server running on http://${HOST}:${PORT}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
+39
View File
@@ -0,0 +1,39 @@
[package]
name = "gamepanel-daemon"
version = "0.1.0"
edition = "2021"
description = "GamePanel daemon - manages game server containers"
[dependencies]
# gRPC
tonic = "0.12"
prost = "0.13"
prost-types = "0.13"
# Async runtime
tokio = { version = "1", features = ["full"] }
# Docker
bollard = "0.18"
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
# HTTP client (for CDN uploads, API callbacks)
reqwest = { version = "0.12", features = ["json"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
# Error handling
anyhow = "1"
thiserror = "2"
# UUID
uuid = { version = "1", features = ["v4"] }
[build-dependencies]
tonic-build = "0.12"
+4
View File
@@ -0,0 +1,4 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("../../packages/proto/daemon.proto")?;
Ok(())
}
+83
View File
@@ -0,0 +1,83 @@
use anyhow::Result;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Deserialize)]
pub struct DaemonConfig {
pub api_url: String,
pub node_token: String,
#[serde(default = "default_grpc_port")]
pub grpc_port: u16,
#[serde(default)]
pub docker: DockerConfig,
#[serde(default = "default_data_path")]
pub data_path: PathBuf,
#[serde(default = "default_backup_path")]
pub backup_path: PathBuf,
}
#[derive(Debug, Deserialize)]
pub struct DockerConfig {
#[serde(default = "default_docker_socket")]
pub socket: String,
#[serde(default = "default_docker_network")]
pub network: String,
#[serde(default = "default_docker_subnet")]
pub network_subnet: String,
}
impl Default for DockerConfig {
fn default() -> Self {
Self {
socket: default_docker_socket(),
network: default_docker_network(),
network_subnet: default_docker_subnet(),
}
}
}
fn default_grpc_port() -> u16 {
50051
}
fn default_data_path() -> PathBuf {
PathBuf::from("/var/lib/gamepanel/servers")
}
fn default_backup_path() -> PathBuf {
PathBuf::from("/var/lib/gamepanel/backups")
}
fn default_docker_socket() -> String {
"/var/run/docker.sock".to_string()
}
fn default_docker_network() -> String {
"gamepanel_nw".to_string()
}
fn default_docker_subnet() -> String {
"172.18.0.0/16".to_string()
}
impl DaemonConfig {
pub fn load() -> Result<Self> {
let config_path =
std::env::var("DAEMON_CONFIG").unwrap_or_else(|_| "/etc/gamepanel/config.yml".into());
let content = std::fs::read_to_string(&config_path)
.unwrap_or_else(|_| {
tracing::warn!("Config file not found at {}, using defaults", config_path);
// Minimal default config for development
r#"
api_url: "http://localhost:3000"
node_token: "dev-token"
grpc_port: 50051
"#
.to_string()
});
let config: DaemonConfig = serde_yaml::from_str(&content)?;
Ok(config)
}
}
+32
View File
@@ -0,0 +1,32 @@
use anyhow::Result;
use tracing::info;
use tracing_subscriber::EnvFilter;
mod config;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
info!("GamePanel Daemon starting...");
let config = config::DaemonConfig::load()?;
info!(grpc_port = config.grpc_port, "Configuration loaded");
// TODO: Initialize Docker client
// TODO: Start gRPC server
// TODO: Begin heartbeat loop
info!("GamePanel Daemon ready");
// Keep the process running
tokio::signal::ctrl_c().await?;
info!("Shutting down...");
Ok(())
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GamePanel</title>
</head>
<body class="min-h-screen bg-background text-foreground antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@source/web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint src/"
},
"dependencies": {
"@source/shared": "workspace:*",
"@source/ui": "workspace:*",
"@tanstack/react-query": "^5.62.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.1.0",
"socket.io-client": "^4.8.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.0",
"vite": "^6.0.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+33
View File
@@ -0,0 +1,33 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Routes, Route } from 'react-router';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
retry: 1,
},
},
});
export function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold">GamePanel</h1>
<p className="mt-2 text-muted-foreground">Game Server Management Panel</p>
</div>
</div>
}
/>
</Routes>
</BrowserRouter>
</QueryClientProvider>
);
}
+55
View File
@@ -0,0 +1,55 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+47
View File
@@ -0,0 +1,47 @@
import type { Config } from 'tailwindcss';
export default {
darkMode: 'class',
content: ['./index.html', './src/**/*.{ts,tsx}', '../../packages/ui/src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
plugins: [],
} satisfies Config;
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+25
View File
@@ -0,0 +1,25 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/socket.io': {
target: 'http://localhost:3000',
ws: true,
},
},
},
});