├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── build.sh ├── client ├── .env.example ├── .gitignore ├── README.md ├── bun.lock ├── components.json ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── atoms │ │ └── user.tsx │ ├── components │ │ ├── app-sidebar.tsx │ │ ├── login-form.tsx │ │ ├── nav-main.tsx │ │ ├── nav-projects.tsx │ │ ├── nav-user.tsx │ │ ├── team-switcher.tsx │ │ ├── theme-provider.tsx │ │ ├── tooltip-warper.tsx │ │ └── ui │ │ │ ├── alert-dialog.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── chart.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── switch.tsx │ │ │ ├── tabs.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ ├── forms │ │ └── alert │ │ │ ├── alert.tsx │ │ │ └── schema.ts │ ├── hooks │ │ └── use-mobile.tsx │ ├── index.css │ ├── lib │ │ ├── api.ts │ │ └── utils.ts │ ├── main.tsx │ ├── models │ │ └── alert.ts │ ├── pages │ │ ├── auth │ │ │ └── login.tsx │ │ ├── layout │ │ │ ├── DashbordLayout.tsx │ │ │ └── root.tsx │ │ └── nodes │ │ │ ├── components │ │ │ ├── alert-card.tsx │ │ │ ├── alert-form.tsx │ │ │ ├── alert-tab.tsx │ │ │ ├── animate-dot.tsx │ │ │ ├── button-bar.tsx │ │ │ ├── cpu-chart.tsx │ │ │ ├── memory-chart.tsx │ │ │ ├── metrics-tab.tsx │ │ │ ├── name-edit-dialog.tsx │ │ │ ├── netwrok-chart.tsx │ │ │ ├── node-card.tsx │ │ │ └── node-view-tabs.tsx │ │ │ ├── index.tsx │ │ │ └── view.tsx │ ├── router │ │ ├── protectedRoute.tsx │ │ └── router.tsx │ ├── schema │ │ └── node.ts │ ├── types │ │ └── node_type.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── config.xrun.json ├── docs └── readme_draft.md ├── server ├── .air.toml ├── .env.example ├── .gitignore ├── cmd │ ├── app │ │ └── app.go │ └── cli │ │ └── cli.go ├── go.mod ├── go.sum ├── internal │ ├── db │ │ ├── alerts.sql.go │ │ ├── custom_queries.go │ │ ├── db.go │ │ ├── models.go │ │ ├── net_stat.sql.go │ │ ├── node.sql.go │ │ ├── repo.go │ │ ├── sql │ │ │ ├── migrations │ │ │ │ ├── 20250202111257_user_table.down.sql │ │ │ │ ├── 20250202111257_user_table.up.sql │ │ │ │ ├── 20250202120019_node_table.down.sql │ │ │ │ ├── 20250202120019_node_table.up.sql │ │ │ │ ├── 20250203171934_node_sys_info_table.down.sql │ │ │ │ ├── 20250203171934_node_sys_info_table.up.sql │ │ │ │ ├── 20250203172228_node_disk_info_table.down.sql │ │ │ │ ├── 20250203172228_node_disk_info_table.up.sql │ │ │ │ ├── 20250207145808_system_stats_table.down.sql │ │ │ │ ├── 20250207145808_system_stats_table.up.sql │ │ │ │ ├── 20250215115558_enable_tablefunc_extension.down.sql │ │ │ │ ├── 20250215115558_enable_tablefunc_extension.up.sql │ │ │ │ ├── 20250216175622_system_stat_retention_policy.down.sql │ │ │ │ ├── 20250216175622_system_stat_retention_policy.up.sql │ │ │ │ ├── 20250216180054_net_stats.down.sql │ │ │ │ ├── 20250216180054_net_stats.up.sql │ │ │ │ ├── 20250216180502_net_stat_retention_policy.down.sql │ │ │ │ ├── 20250216180502_net_stat_retention_policy.up.sql │ │ │ │ ├── 20250228161056_alert_table.down.sql │ │ │ │ └── 20250228161056_alert_table.up.sql │ │ │ └── query │ │ │ │ ├── alerts.sql │ │ │ │ ├── net_stat.sql │ │ │ │ ├── node.sql │ │ │ │ ├── system_stat.sql │ │ │ │ └── user.sql │ │ ├── system_stat.sql.go │ │ └── user.sql.go │ ├── dto │ │ ├── alert_dto.go │ │ ├── node_dto.go │ │ └── user_dto.go │ ├── handlers │ │ ├── alert_handler.go │ │ ├── auth_handler.go │ │ └── node_handler.go │ ├── middleware │ │ ├── auth.go │ │ └── cors.go │ ├── services │ │ ├── alert_service.go │ │ ├── node_service.go │ │ └── user_service.go │ ├── tcpserver │ │ ├── actions.go │ │ ├── alert_backgroud_service.go │ │ ├── alert_sender.go │ │ ├── server.go │ │ └── types.go │ └── utils │ │ ├── bytecovertor.go │ │ ├── password.go │ │ └── token.go ├── main.go ├── makefile ├── sqlc.yml └── test │ └── custom_query_test.go └── tmp └── build-errors.log /.github/FUNDING.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanda0/vps_pilot/33277623bbfd8bd3b88364d4108eb5ad712b4501/.github/FUNDING.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sandakelum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VPS Pilot 2 | 3 | VPS Pilot is a **server monitoring and management platform** designed for private VPS servers. 4 | It provides real-time monitoring, alerting, project management, and (future) cron job automation — all from a single dashboard. 5 | 6 | ![Screenshot from 2025-04-29 08-13-30](https://github.com/user-attachments/assets/fff1c368-9c8e-4bb6-9720-f9a7f46a2910) 7 | 8 | 9 | 10 | ## ✨ Features 11 | 12 | ### 📊 Monitoring 13 | - Agents installed on each node (server). (Agent repo: https://github.com/sanda0/vps_pilot_agent) 14 | - Agents send system metrics to the central server: 15 | - **CPU usage** 16 | - **Memory usage** 17 | - **Network statistics** 18 | - Metrics are visualized in the dashboard with selectable time ranges: 19 | - 5 minutes, 15 minutes, 1 hour, 1 day, 2 days, 7 days. 20 | --- 21 | 22 | ### 🚨 Alerting 23 | - Users can configure alerts based on metric thresholds. 24 | - Notifications sent via: 25 | - **Discord** (✅ Implemented) 26 | - **Email** (✅ Implemented) 27 | - **Slack** (✅ Implemented) 28 | 29 | --- 30 | 31 | ### 🚀 Projects Management (TODO) 32 | - Each node can have multiple projects. 33 | - Projects require a `config.vpspilot.json` file. 34 | - Agents scan disks for project config files and send project metadata to the central server. 35 | - Central server will display available projects and allow: 36 | - Running predefined commands. 37 | - Managing project logs. 38 | - Backing up project directories and databases. 39 | 40 | **Sample `config.vpspilot.json`:** 41 | ```json 42 | { 43 | "name": "meta ads dashboard", 44 | "tech": ["laravel", "react", "mysql"], 45 | "logs": [], 46 | "commands": [ 47 | { "name": "node build", "command": "npm run build" }, 48 | { "name": "php build", "command": "composer install" } 49 | ], 50 | "backups": { 51 | "env_file": ".env", 52 | "zip_file_name": "project_backup", 53 | "database": { 54 | "connection": "DB_CONNECTION", 55 | "host": "DB_HOST", 56 | "port": "DB_PORT", 57 | "username": "DB_USERNAME", 58 | "password": "DB_PASSWORD", 59 | "database_name": "DB_DATABASE" 60 | }, 61 | "dir": [ 62 | "storage/app", 63 | "database/companies" 64 | ] 65 | } 66 | } 67 | ``` 68 | > **Note:** This feature is still under development. 69 | 70 | --- 71 | 72 | ### ⏲️ Cron Jobs Management (TODO) 73 | - Plan to allow users to create and manage cron jobs on nodes remotely from the dashboard. 74 | - Feature is not implemented yet. 75 | 76 | --- 77 | 78 | ## 🛠️ Tech Stack 79 | 80 | | Component | Technology | 81 | |------------------|------------| 82 | | Agent | Golang | 83 | | Central Server | Golang | 84 | | Dashboard | React.js | 85 | | Database | TimescaleDB | 86 | 87 | --- 88 | 89 | ## ⚙️ Configuration 90 | 91 | ### Email Alerts 92 | Configure the following environment variables in your `.env` file for email notifications: 93 | 94 | ```env 95 | MAIL_HOST="your-smtp-host.com" 96 | MAIL_PORT=465 97 | MAIL_USERNAME="your-email@domain.com" 98 | MAIL_PASSWORD="your-email-password" 99 | MAIL_FROM_ADDRESS="noreply@domain.com" 100 | ``` 101 | 102 | ### Slack Alerts 103 | For Slack notifications, configure webhook URLs per alert in the dashboard. To create a Slack webhook: 104 | 1. Go to your Slack workspace 105 | 2. Navigate to Apps → Incoming Webhooks 106 | 3. Create a new webhook for your desired channel 107 | 4. Copy the webhook URL and paste it in the alert configuration 108 | 109 | ### Discord Alerts 110 | For Discord notifications, configure webhook URLs per alert in the dashboard. To create a Discord webhook: 111 | 1. Go to your Discord server settings 112 | 2. Navigate to Integrations → Webhooks 113 | 3. Create a new webhook for your desired channel 114 | 4. Copy the webhook URL and paste it in the alert configuration 115 | 116 | --- 117 | 118 | ## 📦 Installation 119 | 120 | (Instructions will be added soon. Likely via Docker Compose or manual Go/React build.) 121 | 122 | --- 123 | 124 | ## 📅 Roadmap 125 | 126 | - [x] Real-time metrics collection (CPU, Memory, Network) 127 | - [x] Discord alert integration 128 | - [x] Email alert integration 129 | - [x] Slack alert integration 130 | - [ ] Project management via `config.vpspilot.json` 131 | - [ ] Remote command execution for projects 132 | - [ ] Project backups (database + directories) 133 | - [ ] Remote cron job creation and management 134 | 135 | --- 136 | 137 | ## 🧑‍💻 Author 138 | 139 | Made with ❤️ by [Sandakelum](https://github.com/sanda0) 140 | 141 | --- 142 | 143 | ## 📜 License 144 | 145 | This project is licensed under the [MIT License](LICENSE). 146 | 147 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # VPS Pilot Build Script 4 | # This script builds the React UI and embeds it into the Go binary 5 | 6 | set -e 7 | 8 | echo "🏗️ Building VPS Pilot..." 9 | 10 | # Colors for output 11 | RED='\033[0;31m' 12 | GREEN='\033[0;32m' 13 | YELLOW='\033[1;33m' 14 | NC='\033[0m' # No Color 15 | 16 | # Check if we're in the right directory 17 | if [ ! -f "build.sh" ]; then 18 | echo -e "${RED}❌ Error: Please run this script from the project root directory${NC}" 19 | exit 1 20 | fi 21 | 22 | # Step 1: Build the React UI 23 | echo -e "${YELLOW}📦 Building React UI...${NC}" 24 | cd client 25 | 26 | # Check if node_modules exists, if not install dependencies 27 | if [ ! -d "node_modules" ]; then 28 | echo -e "${YELLOW}📥 Installing Node.js dependencies...${NC}" 29 | if command -v bun &> /dev/null; then 30 | bun install 31 | elif command -v npm &> /dev/null; then 32 | npm install 33 | else 34 | echo -e "${RED}❌ Error: Neither bun nor npm found. Please install Node.js and bun/npm.${NC}" 35 | exit 1 36 | fi 37 | fi 38 | 39 | # Build the React app 40 | echo -e "${YELLOW}⚛️ Building React application...${NC}" 41 | if command -v bun &> /dev/null; then 42 | bun run build 43 | elif command -v npm &> /dev/null; then 44 | npm run build 45 | else 46 | echo -e "${RED}❌ Error: Neither bun nor npm found.${NC}" 47 | exit 1 48 | fi 49 | 50 | if [ ! -d "dist" ]; then 51 | echo -e "${RED}❌ Error: React build failed - dist directory not found${NC}" 52 | exit 1 53 | fi 54 | 55 | echo -e "${GREEN}✅ React UI built successfully${NC}" 56 | 57 | # Step 2: Copy built UI to server directory 58 | echo -e "${YELLOW}📁 Copying UI files to server...${NC}" 59 | cd ../server 60 | 61 | # Remove old embedded files if they exist 62 | rm -rf web/dist 63 | mkdir -p web 64 | 65 | # Copy the built React app 66 | cp -r ../client/dist web/ 67 | 68 | echo -e "${GREEN}✅ UI files copied successfully${NC}" 69 | 70 | # Step 3: Build the Go server with embedded UI 71 | echo -e "${YELLOW}🔧 Building Go server with embedded UI...${NC}" 72 | 73 | # Build the Go binary 74 | go build -ldflags "-s -w" -o vps-pilot . 75 | 76 | if [ ! -f "vps-pilot" ]; then 77 | echo -e "${RED}❌ Error: Go build failed${NC}" 78 | exit 1 79 | fi 80 | 81 | echo -e "${GREEN}✅ Go server built successfully${NC}" 82 | 83 | # Step 4: Display success message 84 | echo "" 85 | echo -e "${GREEN}🎉 Build completed successfully!${NC}" 86 | echo -e "${GREEN}📁 Binary location: server/vps-pilot${NC}" 87 | echo "" 88 | echo -e "${YELLOW}📋 Usage:${NC}" 89 | echo " cd server" 90 | echo " ./vps-pilot --help" 91 | echo "" 92 | echo -e "${YELLOW}🚀 To run the server:${NC}" 93 | echo " cd server" 94 | echo " ./vps-pilot" 95 | echo "" 96 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | BASE_URL="http://127.0.0.1:8000/api/v1" -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@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 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "addcn": "bun x --bun shadcn@latest add $1" 12 | }, 13 | "dependencies": { 14 | "@hookform/resolvers": "^4.1.2", 15 | "@radix-ui/react-alert-dialog": "^1.1.6", 16 | "@radix-ui/react-avatar": "^1.1.2", 17 | "@radix-ui/react-collapsible": "^1.1.2", 18 | "@radix-ui/react-dialog": "^1.1.5", 19 | "@radix-ui/react-dropdown-menu": "^2.1.5", 20 | "@radix-ui/react-label": "^2.1.2", 21 | "@radix-ui/react-popover": "^1.1.5", 22 | "@radix-ui/react-select": "^2.1.6", 23 | "@radix-ui/react-separator": "^1.1.1", 24 | "@radix-ui/react-slot": "^1.1.2", 25 | "@radix-ui/react-switch": "^1.1.3", 26 | "@radix-ui/react-tabs": "^1.1.3", 27 | "@radix-ui/react-toggle": "^1.1.2", 28 | "@radix-ui/react-tooltip": "^1.1.8", 29 | "axios": "^1.7.9", 30 | "class-variance-authority": "^0.7.1", 31 | "clsx": "^2.1.1", 32 | "jotai": "^2.11.3", 33 | "lucide-react": "^0.474.0", 34 | "react": "^18.3.1", 35 | "react-dom": "^18.3.1", 36 | "react-hook-form": "^7.54.2", 37 | "react-router": "^7.1.3", 38 | "react-social-icons": "^6.22.0", 39 | "recharts": "^2.15.1", 40 | "tailwind-merge": "^2.6.0", 41 | "tailwindcss-animate": "^1.0.7", 42 | "zod": "^3.24.2" 43 | }, 44 | "devDependencies": { 45 | "@eslint/js": "^9.17.0", 46 | "@types/node": "^22.10.10", 47 | "@types/react": "^18.3.18", 48 | "@types/react-dom": "^18.3.5", 49 | "@vitejs/plugin-react-swc": "^3.5.0", 50 | "autoprefixer": "^10.4.20", 51 | "eslint": "^9.17.0", 52 | "eslint-plugin-react-hooks": "^5.0.0", 53 | "eslint-plugin-react-refresh": "^0.4.16", 54 | "globals": "^15.14.0", 55 | "postcss": "^8.5.1", 56 | "tailwindcss": "3.4.17", 57 | "typescript": "~5.6.2", 58 | "typescript-eslint": "^8.18.2", 59 | "vite": "^6.0.5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider } from "react-router"; 2 | import router from "./router/router"; 3 | import { ThemeProvider } from "./components/theme-provider"; 4 | 5 | function App() { 6 | 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default App 17 | -------------------------------------------------------------------------------- /client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/atoms/user.tsx: -------------------------------------------------------------------------------- 1 | import {atom} from 'jotai' 2 | 3 | export interface User { 4 | id: number; 5 | email: string; 6 | username: string; 7 | } 8 | 9 | export const userAtom = atom(null) -------------------------------------------------------------------------------- /client/src/components/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { 3 | 4 | GalleryVerticalEnd, 5 | Globe, 6 | 7 | ReplaceAll, 8 | Server, 9 | Settings2, 10 | } from "lucide-react" 11 | 12 | import { NavMain } from "@/components/nav-main" 13 | import { NavUser } from "@/components/nav-user" 14 | 15 | import { 16 | Sidebar, 17 | SidebarContent, 18 | SidebarFooter, 19 | SidebarHeader, 20 | SidebarMenuButton, 21 | SidebarRail, 22 | } from "@/components/ui/sidebar" 23 | import { userAtom, User } from "@/atoms/user" 24 | import { useAtom } from "jotai" 25 | 26 | // This is sample data. 27 | const data = { 28 | user: { 29 | name: "shadcn", 30 | email: "m@example.com", 31 | avatar: "/avatars/shadcn.jpg", 32 | }, 33 | company: 34 | { 35 | name: "VPS Pilot", 36 | logo: GalleryVerticalEnd, 37 | plan: "Enterprise", 38 | } 39 | , 40 | navMain: [ 41 | { 42 | title: "Nodes", 43 | url: "/nodes", 44 | icon: Server, 45 | }, 46 | { 47 | title: "Projects", 48 | url: "#", 49 | icon: Globe 50 | }, 51 | { 52 | title: "Cron Jobs", 53 | url: "#", 54 | icon: ReplaceAll 55 | }, 56 | { 57 | title: "Settings", 58 | url: "#", 59 | icon: Settings2, 60 | items: [ 61 | { 62 | title: "Users", 63 | url: "#", 64 | } 65 | ], 66 | }, 67 | ], 68 | } 69 | 70 | export function AppSidebar({ ...props }: React.ComponentProps) { 71 | 72 | const [user] = useAtom(userAtom) 73 | data.user.email = user?.email || data.user.email 74 | data.user.name = user?.username || data.user.name 75 | 76 | 77 | 78 | return ( 79 | 80 | 81 | 85 |
86 | 87 |
88 |
89 | 90 | {data.company.name} 91 | 92 | {data.company.plan} 93 |
94 | 95 |
96 |
97 | 98 | 99 | {/* */} 100 | 101 | 102 | 103 | 104 | 105 |
106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /client/src/components/login-form.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { Button } from "@/components/ui/button" 3 | import { 4 | Card, 5 | CardContent, 6 | CardHeader, 7 | CardTitle, 8 | } from "@/components/ui/card" 9 | import { Input } from "@/components/ui/input" 10 | import { Label } from "@/components/ui/label" 11 | import { useState } from "react" 12 | import api from "@/lib/api" 13 | import { useNavigate } from "react-router" 14 | import { useAtom } from "jotai" 15 | import { userAtom, User } from "@/atoms/user" 16 | 17 | 18 | export function LoginForm({ 19 | className, 20 | ...props 21 | }: React.ComponentPropsWithoutRef<"div">) { 22 | 23 | const [email, setEmail] = useState("") 24 | const [password, setPassword] = useState("") 25 | const [isLoading, setIsLoading] = useState(false) 26 | const [error, setError] = useState("") 27 | const navigate = useNavigate() 28 | 29 | const [_, setUserAtom] = useAtom(userAtom) 30 | 31 | const handleSubmit = async (e: React.FormEvent) => { 32 | e.preventDefault() 33 | setIsLoading(true) 34 | setError("") 35 | 36 | api.post("/auth/login", { email, password }).then((res) => { 37 | if (res.status === 200) { 38 | setIsLoading(false) 39 | setError("") 40 | setUserAtom({ 41 | id: res.data.data.id, 42 | email: res.data.data.email, 43 | username: res.data.data.username 44 | }) 45 | 46 | navigate("/") 47 | } else if (res.status === 401) { 48 | setIsLoading(false) 49 | setError("Invalid email or password") 50 | } 51 | }).catch((err) => { 52 | console.error(err) 53 | setIsLoading(false) 54 | setError("An error occurred") 55 | }) 56 | } 57 | 58 | return ( 59 |
60 | 61 | 62 | Welcome back 63 | 64 | 65 |
66 |
67 |
68 |
69 | 70 | setEmail(e.target.value)} 76 | /> 77 |
78 |
79 |
80 | 81 | 85 | Forgot your password? 86 | 87 |
88 | setPassword(e.target.value)} /> 89 |
90 | 93 |
94 | {/*
95 | Don't have an account?{" "} 96 | 97 | Sign up 98 | 99 |
*/} 100 | {error && ( 101 |
102 | {error} 103 |
104 | )} 105 |
106 |
107 |
108 |
109 | {/*
110 | By clicking continue, you agree to our Terms of Service{" "} 111 | and Privacy Policy. 112 |
*/} 113 |
114 | ) 115 | } 116 | -------------------------------------------------------------------------------- /client/src/components/nav-main.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChevronRight, type LucideIcon } from "lucide-react" 4 | 5 | import { 6 | Collapsible, 7 | CollapsibleContent, 8 | CollapsibleTrigger, 9 | } from "@/components/ui/collapsible" 10 | import { 11 | SidebarGroup, 12 | SidebarGroupLabel, 13 | SidebarMenu, 14 | SidebarMenuButton, 15 | SidebarMenuItem, 16 | SidebarMenuSub, 17 | SidebarMenuSubButton, 18 | SidebarMenuSubItem, 19 | } from "@/components/ui/sidebar" 20 | import { Link } from "react-router" 21 | 22 | export function NavMain({ 23 | items, 24 | }: { 25 | items: { 26 | title: string 27 | url: string 28 | icon?: LucideIcon 29 | isActive?: boolean 30 | items?: { 31 | title: string 32 | url: string 33 | }[] 34 | }[] 35 | }) { 36 | return ( 37 | 38 | Platform 39 | 40 | {items.map((item) => ( 41 | 42 | item.items ? 43 | 49 | 50 | 51 | 52 | {item.icon && } 53 | {item.title} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {item.items?.map((subItem) => ( 62 | 63 | 64 | 65 | {subItem.title} 66 | 67 | 68 | 69 | ))} 70 | 71 | 72 | 73 | : 74 | 75 | 76 | 77 | 78 | {item.icon && } 79 | 80 | 81 | {item.title} 82 | 83 | 84 | 85 | 86 | 87 | 88 | ))} 89 | 90 | 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /client/src/components/nav-projects.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Folder, 3 | Forward, 4 | MoreHorizontal, 5 | Trash2, 6 | type LucideIcon, 7 | } from "lucide-react" 8 | 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuSeparator, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu" 16 | import { 17 | SidebarGroup, 18 | SidebarGroupLabel, 19 | SidebarMenu, 20 | SidebarMenuAction, 21 | SidebarMenuButton, 22 | SidebarMenuItem, 23 | useSidebar, 24 | } from "@/components/ui/sidebar" 25 | 26 | export function NavProjects({ 27 | projects, 28 | }: { 29 | projects: { 30 | name: string 31 | url: string 32 | icon: LucideIcon 33 | }[] 34 | }) { 35 | const { isMobile } = useSidebar() 36 | 37 | return ( 38 | 39 | Projects 40 | 41 | {projects.map((item) => ( 42 | 43 | 44 | 45 | 46 | {item.name} 47 | 48 | 49 | 50 | 51 | 52 | 53 | More 54 | 55 | 56 | 61 | 62 | 63 | View Project 64 | 65 | 66 | 67 | Share Project 68 | 69 | 70 | 71 | 72 | Delete Project 73 | 74 | 75 | 76 | 77 | ))} 78 | 79 | 80 | 81 | More 82 | 83 | 84 | 85 | 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /client/src/components/nav-user.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | BadgeCheck, 5 | Bell, 6 | ChevronsUpDown, 7 | LogOut, 8 | } from "lucide-react" 9 | 10 | import { 11 | Avatar, 12 | AvatarFallback, 13 | AvatarImage, 14 | } from "@/components/ui/avatar" 15 | import { 16 | DropdownMenu, 17 | DropdownMenuContent, 18 | DropdownMenuGroup, 19 | DropdownMenuItem, 20 | DropdownMenuLabel, 21 | DropdownMenuSeparator, 22 | DropdownMenuTrigger, 23 | } from "@/components/ui/dropdown-menu" 24 | import { 25 | SidebarMenu, 26 | SidebarMenuButton, 27 | SidebarMenuItem, 28 | useSidebar, 29 | } from "@/components/ui/sidebar" 30 | 31 | export function NavUser({ 32 | user, 33 | }: { 34 | user: { 35 | name: string 36 | email: string 37 | avatar: string 38 | } 39 | }) { 40 | const { isMobile } = useSidebar() 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | CN 54 | 55 |
56 | {user.name} 57 | {user.email} 58 |
59 | 60 |
61 |
62 | 68 | 69 |
70 | 71 | 72 | CN 73 | 74 |
75 | {user.name} 76 | {user.email} 77 |
78 |
79 |
80 | 81 | 82 | 83 | 84 | Account 85 | 86 | 87 | 88 | 89 | Notifications 90 | 91 | 92 | 93 | 94 | 95 | Log out 96 | 97 |
98 |
99 |
100 |
101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /client/src/components/team-switcher.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronsUpDown, Plus } from "lucide-react" 3 | 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuSeparator, 10 | DropdownMenuShortcut, 11 | DropdownMenuTrigger, 12 | } from "@/components/ui/dropdown-menu" 13 | import { 14 | SidebarMenu, 15 | SidebarMenuButton, 16 | SidebarMenuItem, 17 | useSidebar, 18 | } from "@/components/ui/sidebar" 19 | 20 | export function TeamSwitcher({ 21 | teams, 22 | }: { 23 | teams: { 24 | name: string 25 | logo: React.ElementType 26 | plan: string 27 | }[] 28 | }) { 29 | const { isMobile } = useSidebar() 30 | const [activeTeam, setActiveTeam] = React.useState(teams[0]) 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | 41 |
42 | 43 |
44 |
45 | 46 | {activeTeam.name} 47 | 48 | {activeTeam.plan} 49 |
50 | 51 |
52 |
53 | 59 | 60 | Teams 61 | 62 | {teams.map((team, index) => ( 63 | setActiveTeam(team)} 66 | className="gap-2 p-2" 67 | > 68 |
69 | 70 |
71 | {team.name} 72 | ⌘{index + 1} 73 |
74 | ))} 75 | 76 | 77 |
78 | 79 |
80 |
Add team
81 |
82 |
83 |
84 |
85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /client/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react" 2 | 3 | type Theme = "dark" | "light" | "system" 4 | 5 | type ThemeProviderProps = { 6 | children: React.ReactNode 7 | defaultTheme?: Theme 8 | storageKey?: string 9 | } 10 | 11 | type ThemeProviderState = { 12 | theme: Theme 13 | setTheme: (theme: Theme) => void 14 | } 15 | 16 | const initialState: ThemeProviderState = { 17 | theme: "system", 18 | setTheme: () => null, 19 | } 20 | 21 | const ThemeProviderContext = createContext(initialState) 22 | 23 | export function ThemeProvider({ 24 | children, 25 | defaultTheme = "system", 26 | storageKey = "vite-ui-theme", 27 | ...props 28 | }: ThemeProviderProps) { 29 | const [theme, setTheme] = useState( 30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 31 | ) 32 | 33 | useEffect(() => { 34 | const root = window.document.documentElement 35 | 36 | root.classList.remove("light", "dark") 37 | 38 | if (theme === "system") { 39 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 40 | .matches 41 | ? "dark" 42 | : "light" 43 | 44 | root.classList.add(systemTheme) 45 | return 46 | } 47 | 48 | root.classList.add(theme) 49 | }, [theme]) 50 | 51 | const value = { 52 | theme, 53 | setTheme: (theme: Theme) => { 54 | localStorage.setItem(storageKey, theme) 55 | setTheme(theme) 56 | }, 57 | } 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ) 64 | } 65 | 66 | export const useTheme = () => { 67 | const context = useContext(ThemeProviderContext) 68 | 69 | if (context === undefined) 70 | throw new Error("useTheme must be used within a ThemeProvider") 71 | 72 | return context 73 | } 74 | -------------------------------------------------------------------------------- /client/src/components/tooltip-warper.tsx: -------------------------------------------------------------------------------- 1 | import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip"; 2 | 3 | export function TooltipWarper({ children, tooltipContent }: { children: React.ReactNode, tooltipContent: React.ReactNode }) { 4 | return <> 5 | 6 | 7 | 8 | {children} 9 | 10 | 11 | {tooltipContent} 12 | 13 | 14 | 15 | 16 | } -------------------------------------------------------------------------------- /client/src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { buttonVariants } from "@/components/ui/button" 6 | 7 | const AlertDialog = AlertDialogPrimitive.Root 8 | 9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 10 | 11 | const AlertDialogPortal = AlertDialogPrimitive.Portal 12 | 13 | const AlertDialogOverlay = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, ...props }, ref) => ( 17 | 25 | )) 26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 27 | 28 | const AlertDialogContent = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 | 33 | 34 | 42 | 43 | )) 44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 45 | 46 | const AlertDialogHeader = ({ 47 | className, 48 | ...props 49 | }: React.HTMLAttributes) => ( 50 |
57 | ) 58 | AlertDialogHeader.displayName = "AlertDialogHeader" 59 | 60 | const AlertDialogFooter = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ) 72 | AlertDialogFooter.displayName = "AlertDialogFooter" 73 | 74 | const AlertDialogTitle = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef 77 | >(({ className, ...props }, ref) => ( 78 | 83 | )) 84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 85 | 86 | const AlertDialogDescription = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 95 | )) 96 | AlertDialogDescription.displayName = 97 | AlertDialogPrimitive.Description.displayName 98 | 99 | const AlertDialogAction = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 110 | 111 | const AlertDialogCancel = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 124 | )) 125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 126 | 127 | export { 128 | AlertDialog, 129 | AlertDialogPortal, 130 | AlertDialogOverlay, 131 | AlertDialogTrigger, 132 | AlertDialogContent, 133 | AlertDialogHeader, 134 | AlertDialogFooter, 135 | AlertDialogTitle, 136 | AlertDialogDescription, 137 | AlertDialogAction, 138 | AlertDialogCancel, 139 | } 140 | -------------------------------------------------------------------------------- /client/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )) 19 | Avatar.displayName = AvatarPrimitive.Root.displayName 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )) 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )) 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 47 | 48 | export { Avatar, AvatarImage, AvatarFallback } 49 | -------------------------------------------------------------------------------- /client/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /client/src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>