First
Some checks failed
Build All Docker Images / changes (push) Has been cancelled
Build and Push App Docker Image / build (push) Has been cancelled
Build and Push Node Docker Image / build (push) Has been cancelled
Test and Lint / test-app (push) Has been cancelled
Test and Lint / test-node (push) Has been cancelled
Test and Lint / lint-dockerfiles (push) Has been cancelled
Test and Lint / security-scan (push) Has been cancelled
Build All Docker Images / build-app (push) Has been cancelled
Build All Docker Images / build-node (push) Has been cancelled
Build All Docker Images / summary (push) Has been cancelled
Some checks failed
Build All Docker Images / changes (push) Has been cancelled
Build and Push App Docker Image / build (push) Has been cancelled
Build and Push Node Docker Image / build (push) Has been cancelled
Test and Lint / test-app (push) Has been cancelled
Test and Lint / test-node (push) Has been cancelled
Test and Lint / lint-dockerfiles (push) Has been cancelled
Test and Lint / security-scan (push) Has been cancelled
Build All Docker Images / build-app (push) Has been cancelled
Build All Docker Images / build-node (push) Has been cancelled
Build All Docker Images / summary (push) Has been cancelled
This commit is contained in:
commit
4169337dd0
68 changed files with 8726 additions and 0 deletions
6
app/.env
Normal file
6
app/.env
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Environment variables for FRP configuration
|
||||
FRPC_SERVER_ADDR=127.0.0.1
|
||||
FRPC_SERVER_PORT=7000
|
||||
FRPC_TOKEN=your-secret-token
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
22
app/.env.example
Normal file
22
app/.env.example
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Environment variables for FRP configuration
|
||||
# Copy this file to .env and update the values
|
||||
|
||||
# FRP Server Configuration
|
||||
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||
FRPC_SERVER_PORT=7000
|
||||
FRPC_TOKEN=your-secret-token
|
||||
|
||||
# Application Configuration
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# Database Configuration (SQLite is used by default)
|
||||
# DATABASE_PATH=./data/tunnels.db
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Node Integration Configuration
|
||||
NODE_URL=http://your-home-server:3001
|
||||
NODE_TOKEN=your-node-secret-token
|
||||
NODE_TIMEOUT=5000
|
||||
24
app/.gitignore
vendored
Normal file
24
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
336
app/DEPLOYMENT.md
Normal file
336
app/DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
# Deployment Guide
|
||||
|
||||
This guide covers different deployment scenarios for the FRP Manager application.
|
||||
|
||||
## Table of Contents
|
||||
1. [Docker Compose Deployment (Recommended)](#docker-compose-deployment)
|
||||
2. [Manual Deployment](#manual-deployment)
|
||||
3. [Development Setup](#development-setup)
|
||||
4. [Production Configuration](#production-configuration)
|
||||
5. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Docker Compose Deployment
|
||||
|
||||
### Prerequisites
|
||||
- Docker and Docker Compose installed
|
||||
- Access to your VPS/server running FRP server
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. **Clone and Setup**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd frp-manager
|
||||
```
|
||||
|
||||
2. **Configure Environment**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your FRP server details:
|
||||
```env
|
||||
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||
FRPC_SERVER_PORT=7000
|
||||
FRPC_TOKEN=your-secret-token
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
3. **Deploy**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
4. **Access Application**
|
||||
- Web Interface: http://localhost:3000
|
||||
- API: http://localhost:3000/api
|
||||
- Health Check: http://localhost:3000/health
|
||||
|
||||
### Docker Compose Configuration
|
||||
|
||||
The `docker-compose.yml` includes:
|
||||
- **app**: Main application container (Express API + React frontend)
|
||||
- **frpc**: FRP client container for tunnel management
|
||||
|
||||
Key volumes:
|
||||
- `./data:/app/data` - Database and configuration files
|
||||
- `./logs:/app/logs` - Application logs
|
||||
- `/var/run/docker.sock:/var/run/docker.sock` - Docker socket for container management
|
||||
|
||||
### Updating the Application
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull origin main
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose down
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Manual Deployment
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+ installed
|
||||
- Docker (for FRPC container)
|
||||
- SQLite3
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. **Install Dependencies**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Build Application**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. **Configure Environment**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
4. **Create Directories**
|
||||
```bash
|
||||
mkdir -p data logs
|
||||
```
|
||||
|
||||
5. **Start Application**
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
|
||||
# Production
|
||||
npm start
|
||||
```
|
||||
|
||||
### Process Management
|
||||
|
||||
For production deployment, use a process manager like PM2:
|
||||
|
||||
```bash
|
||||
# Install PM2
|
||||
npm install -g pm2
|
||||
|
||||
# Start application with PM2
|
||||
pm2 start npm --name "frp-manager" -- start
|
||||
|
||||
# Save PM2 configuration
|
||||
pm2 save
|
||||
|
||||
# Setup auto-restart on reboot
|
||||
pm2 startup
|
||||
```
|
||||
|
||||
### Reverse Proxy Setup
|
||||
|
||||
Example Nginx configuration:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. **Install Dependencies**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Create Environment File**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. **Start Development Server**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Development Features
|
||||
|
||||
- **Hot Reload**: Both frontend and backend auto-reload on changes
|
||||
- **Debug Logging**: Enhanced logging in development mode
|
||||
- **React DevTools**: Included React Query DevTools
|
||||
- **Error Handling**: Detailed error messages in development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── src/
|
||||
│ ├── client/ # React frontend
|
||||
│ │ ├── api/ # API client and types
|
||||
│ │ ├── components/ # Reusable React components
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ └── App.tsx # Main App component
|
||||
│ └── server/ # Express backend
|
||||
│ ├── database.ts # SQLite database operations
|
||||
│ ├── frpc-manager.ts # FRPC container management
|
||||
│ ├── logger.ts # Winston logging configuration
|
||||
│ ├── main.ts # Express server setup
|
||||
│ ├── routes.ts # API route definitions
|
||||
│ └── types.ts # TypeScript type definitions
|
||||
├── public/ # Static assets
|
||||
├── data/ # Database and FRPC config
|
||||
├── logs/ # Application logs
|
||||
├── Dockerfile # Docker container definition
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
└── package.json # Project dependencies and scripts
|
||||
```
|
||||
|
||||
## Production Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
# FRP Server Configuration
|
||||
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||
FRPC_SERVER_PORT=7000
|
||||
FRPC_TOKEN=your-secret-token
|
||||
|
||||
# Application Configuration
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# Security (optional)
|
||||
CORS_ORIGIN=https://your-domain.com
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
|
||||
1. **Authentication**: Consider adding authentication for production use
|
||||
2. **CORS**: Configure appropriate CORS settings
|
||||
3. **HTTPS**: Use HTTPS in production
|
||||
4. **Firewall**: Restrict access to necessary ports only
|
||||
5. **Docker Security**: Run containers with non-root users
|
||||
|
||||
### Monitoring
|
||||
|
||||
The application includes:
|
||||
- **Health Check Endpoint**: `/health`
|
||||
- **Structured Logging**: JSON logs with Winston
|
||||
- **Error Tracking**: Comprehensive error logging
|
||||
- **Service Status**: Built-in service monitoring
|
||||
|
||||
### Backup Strategy
|
||||
|
||||
Important data to backup:
|
||||
- Database: `./data/tunnels.db`
|
||||
- Configuration: `./data/frpc.toml`
|
||||
- Environment: `.env`
|
||||
- Logs: `./logs/` (optional)
|
||||
|
||||
Example backup script:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="./backups/$(date +%Y%m%d_%H%M%S)"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp -r ./data "$BACKUP_DIR/"
|
||||
cp .env "$BACKUP_DIR/"
|
||||
tar -czf "$BACKUP_DIR.tar.gz" "$BACKUP_DIR"
|
||||
rm -rf "$BACKUP_DIR"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Application Won't Start
|
||||
- Check Node.js version (requires 18+)
|
||||
- Verify all dependencies are installed
|
||||
- Check port availability (default: 3000)
|
||||
- Review logs in `./logs/error.log`
|
||||
|
||||
#### Database Issues
|
||||
- Ensure `./data` directory exists and is writable
|
||||
- Check SQLite permissions
|
||||
- Verify database file integrity
|
||||
|
||||
#### FRPC Container Issues
|
||||
- Verify Docker is running
|
||||
- Check FRPC container logs: `docker logs frpc`
|
||||
- Ensure FRPC configuration is valid
|
||||
- Verify server connectivity
|
||||
|
||||
#### API Errors
|
||||
- Check server logs for detailed error messages
|
||||
- Verify API endpoints are accessible
|
||||
- Check CORS configuration for frontend issues
|
||||
|
||||
### Health Check Script
|
||||
|
||||
Use the provided health check script to verify all services:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
./health-check.sh
|
||||
|
||||
# Windows
|
||||
powershell -ExecutionPolicy Bypass -File "health-check.ps1"
|
||||
```
|
||||
|
||||
### Log Analysis
|
||||
|
||||
Application logs are stored in `./logs/`:
|
||||
- `combined.log`: All application logs
|
||||
- `error.log`: Error logs only
|
||||
|
||||
Example log analysis:
|
||||
```bash
|
||||
# View recent errors
|
||||
tail -f ./logs/error.log
|
||||
|
||||
# Search for specific errors
|
||||
grep "Failed to" ./logs/combined.log
|
||||
|
||||
# View API requests
|
||||
grep "API Request" ./logs/combined.log
|
||||
```
|
||||
|
||||
### Support
|
||||
|
||||
For additional support:
|
||||
1. Check the [README.md](README.md) file
|
||||
2. Review application logs
|
||||
3. Use the health check script
|
||||
4. Open an issue on the repository
|
||||
|
||||
### Performance Tuning
|
||||
|
||||
For high-traffic deployments:
|
||||
1. Use a reverse proxy (Nginx, Apache)
|
||||
2. Enable HTTP/2
|
||||
3. Implement caching strategies
|
||||
4. Monitor resource usage
|
||||
5. Scale horizontally if needed
|
||||
|
||||
### Updates and Maintenance
|
||||
|
||||
Regular maintenance tasks:
|
||||
- Update dependencies: `npm update`
|
||||
- Backup database regularly
|
||||
- Monitor disk space for logs
|
||||
- Review and rotate logs
|
||||
- Update Docker images: `docker-compose pull`
|
||||
29
app/Dockerfile
Normal file
29
app/Dockerfile
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Use Node.js 18 LTS
|
||||
FROM node:18-alpine
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Create directories for data and logs
|
||||
RUN mkdir -p data logs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Install Docker CLI to manage frpc container
|
||||
RUN apk add --no-cache docker-cli
|
||||
|
||||
# Start the application
|
||||
CMD ["npm", "start"]
|
||||
270
app/README.md
Normal file
270
app/README.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# FRP Manager
|
||||
|
||||
A fullstack application for managing FRP (Fast Reverse Proxy) tunnel configurations with a React frontend and Express backend. Now with **Node Integration** for remote management of home server FRP clients.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Web Dashboard**: Modern React-based interface for managing tunnels
|
||||
- 🔧 **Tunnel Management**: Create, edit, delete, and toggle tunnel configurations
|
||||
- 📊 **Real-time Status**: Monitor tunnel status and FRPC service health
|
||||
- 🏠 **Node Integration**: Remote control of home server FRP clients
|
||||
- <20> **Push to Node**: Deploy configurations to remote nodes with one click
|
||||
- 🌐 **Live Monitoring**: Real-time node connectivity and status tracking
|
||||
- <20>🐳 **Docker Support**: Complete Docker Compose setup for easy deployment
|
||||
- 📝 **Auto Configuration**: Automatically generates FRPC configuration from active tunnels
|
||||
- 🗄️ **SQLite Database**: Persistent storage for tunnel configurations
|
||||
- 📋 **Service Logs**: View FRPC service logs directly from the web interface
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Frontend:**
|
||||
- React 19
|
||||
- TypeScript
|
||||
- React Router
|
||||
- TanStack Query (React Query)
|
||||
- Lucide React Icons
|
||||
- React Hot Toast
|
||||
|
||||
**Backend:**
|
||||
- Node.js
|
||||
- Express
|
||||
- TypeScript
|
||||
- SQLite (better-sqlite3)
|
||||
- Zod validation
|
||||
- Winston logging
|
||||
- Axios (for node communication)
|
||||
|
||||
**Infrastructure:**
|
||||
- Docker & Docker Compose
|
||||
- FRPC (Fast Reverse Proxy Client)
|
||||
|
||||
## New: Node Integration
|
||||
|
||||
The FRP Manager now supports remote management of home server nodes. This allows you to:
|
||||
|
||||
- **Query Status**: Get real-time status from your home server node
|
||||
- **Push Configs**: Send updated frpc.toml configurations remotely
|
||||
- **Restart Services**: Restart FRP client on the home server
|
||||
- **Monitor Connectivity**: Live status indicators and connection tracking
|
||||
|
||||
### Setup Node Integration
|
||||
|
||||
1. **Configure the app** with node connection details in `.env`:
|
||||
```bash
|
||||
NODE_URL=http://your-home-server:3001
|
||||
NODE_TOKEN=your-node-secret-token
|
||||
NODE_TIMEOUT=5000
|
||||
```
|
||||
|
||||
2. **Set up the home server node** (see `../node/` directory)
|
||||
|
||||
3. **Use the web interface** to push configurations with the "Push to Node" button
|
||||
|
||||
For detailed setup instructions, see [NODE_INTEGRATION.md](../NODE_INTEGRATION.md).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd frp-manager
|
||||
```
|
||||
|
||||
2. Create environment configuration:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. Edit `.env` with your FRP server details:
|
||||
```bash
|
||||
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||
FRPC_SERVER_PORT=7000
|
||||
FRPC_TOKEN=your-secret-token
|
||||
```
|
||||
|
||||
4. Start the application:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
5. Access the web interface at `http://localhost:3000`
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Set up environment variables:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
3. Start development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. Access the application at `http://localhost:3000`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `FRPC_SERVER_ADDR` | FRP server IP address | `your-vps-ip` |
|
||||
| `FRPC_SERVER_PORT` | FRP server port | `7000` |
|
||||
| `FRPC_TOKEN` | FRP authentication token | - |
|
||||
| `NODE_ENV` | Node environment | `development` |
|
||||
| `PORT` | Application port | `3000` |
|
||||
|
||||
### Tunnel Configuration
|
||||
|
||||
Each tunnel requires:
|
||||
- **Name**: Descriptive name (e.g., "Minecraft Server")
|
||||
- **Protocol**: TCP or UDP
|
||||
- **Local IP**: IP address of the service on your network
|
||||
- **Local Port**: Port of the service on your network
|
||||
- **Remote Port**: Port to expose on the FRP server
|
||||
- **Enabled**: Whether the tunnel is active
|
||||
|
||||
## Usage
|
||||
|
||||
### Adding a Tunnel
|
||||
|
||||
1. Navigate to the "Tunnels" page
|
||||
2. Click "Add Tunnel"
|
||||
3. Fill in the tunnel details:
|
||||
- Name: "Minecraft Server"
|
||||
- Protocol: TCP
|
||||
- Local IP: 192.168.1.100
|
||||
- Local Port: 25565
|
||||
- Remote Port: 25565
|
||||
- Enabled: ✓
|
||||
4. Click "Create"
|
||||
|
||||
### Managing Tunnels
|
||||
|
||||
- **Edit**: Click the edit button to modify tunnel settings
|
||||
- **Toggle**: Use the power button to enable/disable tunnels
|
||||
- **Delete**: Remove tunnels you no longer need
|
||||
- **Status**: View real-time tunnel status on the dashboard
|
||||
|
||||
### Service Management
|
||||
|
||||
Use the "Server Status" page to:
|
||||
- Start/Stop/Restart the FRPC service
|
||||
- Regenerate FRPC configuration
|
||||
- View service logs
|
||||
- Monitor system health
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Tunnels
|
||||
- `GET /api/tunnels` - Get all tunnels
|
||||
- `POST /api/tunnels` - Create new tunnel
|
||||
- `GET /api/tunnels/:id` - Get tunnel by ID
|
||||
- `PUT /api/tunnels/:id` - Update tunnel
|
||||
- `DELETE /api/tunnels/:id` - Delete tunnel
|
||||
- `GET /api/tunnels/:id/status` - Get tunnel status
|
||||
|
||||
### FRPC Service
|
||||
- `GET /api/frpc/status` - Get service status
|
||||
- `POST /api/frpc/start` - Start service
|
||||
- `POST /api/frpc/stop` - Stop service
|
||||
- `POST /api/frpc/restart` - Restart service
|
||||
- `POST /api/frpc/regenerate` - Regenerate configuration
|
||||
- `GET /api/frpc/logs` - Get service logs
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── src/
|
||||
│ ├── client/ # React frontend
|
||||
│ │ ├── api/ # API client
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ └── App.tsx # Main App component
|
||||
│ └── server/ # Express backend
|
||||
│ ├── database.ts # Database layer
|
||||
│ ├── frpc-manager.ts # FRPC management
|
||||
│ ├── logger.ts # Logging utilities
|
||||
│ ├── main.ts # Server entry point
|
||||
│ ├── routes.ts # API routes
|
||||
│ └── types.ts # TypeScript types
|
||||
├── public/ # Static assets
|
||||
├── data/ # Database and config files
|
||||
├── logs/ # Application logs
|
||||
└── docker-compose.yml # Docker configuration
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run dev # Start development server
|
||||
|
||||
# Production
|
||||
npm run build # Build application
|
||||
npm start # Start production server
|
||||
|
||||
# Docker
|
||||
docker-compose up # Start all services
|
||||
docker-compose down # Stop all services
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**FRPC service won't start:**
|
||||
- Check if the server address and port are correct
|
||||
- Verify the authentication token
|
||||
- Ensure the FRPC container has the updated configuration
|
||||
|
||||
**Tunnels show as inactive:**
|
||||
- Verify the local service is running
|
||||
- Check firewall settings
|
||||
- Ensure the local IP and port are correct
|
||||
|
||||
**Database errors:**
|
||||
- Check if the `data` directory exists and is writable
|
||||
- Verify SQLite permissions
|
||||
|
||||
### Logs
|
||||
|
||||
Application logs are stored in the `logs` directory:
|
||||
- `combined.log`: All application logs
|
||||
- `error.log`: Error logs only
|
||||
|
||||
FRPC logs can be viewed through the web interface or directly:
|
||||
```bash
|
||||
docker logs frpc
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
1. Check the troubleshooting section
|
||||
2. Review the logs for error messages
|
||||
3. Open an issue on the repository
|
||||
862
app/bun.lock
Normal file
862
app/bun.lock
Normal file
|
|
@ -0,0 +1,862 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "arc-frp",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.13.4",
|
||||
"@tanstack/react-query-devtools": "^5.13.5",
|
||||
"axios": "^1.6.2",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^5.1.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-express": "*",
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.22.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"nodemon": "^3.1.10",
|
||||
"vite": "^6.3.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg=="],
|
||||
|
||||
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||
|
||||
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
|
||||
|
||||
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
|
||||
"@npmcli/fs": ["@npmcli/fs@1.1.1", "", { "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" } }, "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ=="],
|
||||
|
||||
"@npmcli/move-file": ["@npmcli/move-file@1.1.2", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg=="],
|
||||
|
||||
"@remix-run/router": ["@remix-run/router@1.23.0", "", {}, "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.81.5", "", {}, "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q=="],
|
||||
|
||||
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.81.2", "", {}, "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.81.5", "", { "dependencies": { "@tanstack/query-core": "5.81.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw=="],
|
||||
|
||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.81.5", "", { "dependencies": { "@tanstack/query-devtools": "5.81.2" }, "peerDependencies": { "@tanstack/react-query": "^5.81.5", "react": "^18 || ^19" } }, "sha512-lCGMu4RX0uGnlrlLeSckBfnW/UV+KMlTBVqa97cwK7Z2ED5JKnZRSjNXwoma6sQBTJrcULvzgx2K6jEPvNUpDw=="],
|
||||
|
||||
"@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||
|
||||
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
|
||||
|
||||
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
|
||||
|
||||
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||
|
||||
"@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="],
|
||||
|
||||
"abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"aproba": ["aproba@2.0.0", "", {}, "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="],
|
||||
|
||||
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"better-sqlite3": ["better-sqlite3@9.6.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"cacache": ["cacache@15.3.0", "", { "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "glob": "^7.1.4", "infer-owner": "^1.0.4", "lru-cache": "^6.0.0", "minipass": "^3.1.1", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.2", "mkdirp": "^1.0.3", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^8.0.1", "tar": "^6.0.2", "unique-filename": "^1.1.1" } }, "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
||||
|
||||
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
|
||||
|
||||
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
|
||||
|
||||
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
|
||||
|
||||
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.179", "", {}, "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
||||
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||
|
||||
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||
|
||||
"express-static-gzip": ["express-static-gzip@2.2.0", "", { "dependencies": { "parseurl": "^1.3.3", "serve-static": "^1.16.2" } }, "sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg=="],
|
||||
|
||||
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||
|
||||
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
||||
|
||||
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||
|
||||
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||
|
||||
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"goober": ["goober@2.1.16", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@4.0.1", "", { "dependencies": { "@tootallnate/once": "1", "agent-base": "6", "debug": "4" } }, "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||
|
||||
"infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||
|
||||
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.294.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA=="],
|
||||
|
||||
"make-fetch-happen": ["make-fetch-happen@9.1.0", "", { "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^6.0.0", "minipass": "^3.1.3", "minipass-collect": "^1.0.2", "minipass-fetch": "^1.3.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.2", "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" } }, "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
|
||||
|
||||
"minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="],
|
||||
|
||||
"minipass-fetch": ["minipass-fetch@1.4.1", "", { "dependencies": { "minipass": "^3.1.0", "minipass-sized": "^1.0.3", "minizlib": "^2.0.0" }, "optionalDependencies": { "encoding": "^0.1.12" } }, "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw=="],
|
||||
|
||||
"minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="],
|
||||
|
||||
"minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
|
||||
|
||||
"minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
|
||||
|
||||
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
|
||||
|
||||
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||
|
||||
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"node-gyp": ["node-gyp@8.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^9.1.0", "nopt": "^5.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"nodemon": ["nodemon@3.1.10", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw=="],
|
||||
|
||||
"nopt": ["nopt@5.0.0", "", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
||||
|
||||
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
|
||||
|
||||
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="],
|
||||
|
||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
|
||||
"react-hot-toast": ["react-hot-toast@2.5.2", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"react-router": ["react-router@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ=="],
|
||||
|
||||
"react-router-dom": ["react-router-dom@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0", "react-router": "6.30.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
|
||||
"rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
||||
|
||||
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||
|
||||
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||
|
||||
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
|
||||
|
||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||
|
||||
"socks": ["socks@2.8.5", "", { "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww=="],
|
||||
|
||||
"socks-proxy-agent": ["socks-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
|
||||
"sqlite3": ["sqlite3@5.1.7", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { "node-gyp": "8.x" } }, "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog=="],
|
||||
|
||||
"ssri": ["ssri@8.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
|
||||
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
|
||||
|
||||
"tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
|
||||
|
||||
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="],
|
||||
|
||||
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
||||
|
||||
"tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unique-filename": ["unique-filename@1.1.1", "", { "dependencies": { "unique-slug": "^2.0.0" } }, "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ=="],
|
||||
|
||||
"unique-slug": ["unique-slug@2.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||
|
||||
"vite-express": ["vite-express@0.21.1", "", { "dependencies": { "express-static-gzip": "^2.2.0", "picocolors": "^1.1.1" } }, "sha512-/dz1syfdKfWwcNRSl9wxZQmH7dImrvxNR9TptbpYGqrlawWFD+USzbLR1ytWei8XJpDPDRUgOoT8dEIf/vviyQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
|
||||
|
||||
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
||||
|
||||
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
|
||||
"zod": ["zod@3.25.71", "", {}, "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"cacache/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"express-static-gzip/serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
|
||||
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"make-fetch-happen/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"make-fetch-happen/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
|
||||
|
||||
"minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||
|
||||
"express-static-gzip/serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"express-static-gzip/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"express-static-gzip/serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"express-static-gzip/serve-static/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"express-static-gzip/serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
|
||||
"express-static-gzip/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
}
|
||||
}
|
||||
15
app/data/frpc.toml
Normal file
15
app/data/frpc.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[common]
|
||||
server_addr = "127.0.0.1"
|
||||
server_port = 7000
|
||||
token = "your-secret-token"
|
||||
|
||||
# Example tunnel configuration
|
||||
# This file will be automatically generated by the FRP Manager
|
||||
# based on your tunnel configurations in the web interface
|
||||
|
||||
# Example:
|
||||
# [minecraft-server]
|
||||
# type = "tcp"
|
||||
# local_ip = "192.168.1.100"
|
||||
# local_port = 25565
|
||||
# remote_port = 25565
|
||||
BIN
app/data/tunnels.db
Normal file
BIN
app/data/tunnels.db
Normal file
Binary file not shown.
28
app/docker-compose.override.yml
Normal file
28
app/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- FRPC_SERVER_ADDR=127.0.0.1
|
||||
- FRPC_SERVER_PORT=7000
|
||||
- FRPC_TOKEN=your-secret-token
|
||||
depends_on:
|
||||
- frpc
|
||||
restart: unless-stopped
|
||||
|
||||
frpc:
|
||||
image: snowdreamtech/frpc:latest
|
||||
container_name: frpc
|
||||
volumes:
|
||||
- ./data/frpc.toml:/etc/frp/frpc.toml:ro
|
||||
restart: unless-stopped
|
||||
# Use host network for easier local development
|
||||
network_mode: "host"
|
||||
39
app/docker-compose.yml
Normal file
39
app/docker-compose.yml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Main application (Express API + React frontend)
|
||||
app:
|
||||
build: .
|
||||
container_name: frp-manager
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- FRPC_SERVER_ADDR=${FRPC_SERVER_ADDR:-your-vps-ip}
|
||||
- FRPC_SERVER_PORT=${FRPC_SERVER_PORT:-7000}
|
||||
- FRPC_TOKEN=${FRPC_TOKEN}
|
||||
- NODE_URL=${NODE_URL}
|
||||
- NODE_TOKEN=${NODE_TOKEN}
|
||||
- NODE_TIMEOUT=${NODE_TIMEOUT:-5000}
|
||||
depends_on:
|
||||
- frpc
|
||||
restart: unless-stopped
|
||||
|
||||
# FRPC client container
|
||||
frpc:
|
||||
image: snowdreamtech/frpc:latest
|
||||
container_name: frpc
|
||||
volumes:
|
||||
- ./data/frpc.toml:/etc/frp/frpc.toml
|
||||
depends_on:
|
||||
- app
|
||||
restart: unless-stopped
|
||||
network_mode: "host"
|
||||
|
||||
volumes:
|
||||
data:
|
||||
logs:
|
||||
69
app/health-check.ps1
Normal file
69
app/health-check.ps1
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Health check script for FRP Manager (PowerShell)
|
||||
# This script checks if all services are running correctly
|
||||
|
||||
Write-Host "🔍 FRP Manager Health Check" -ForegroundColor Cyan
|
||||
Write-Host "==========================" -ForegroundColor Cyan
|
||||
|
||||
# Check if main application is running
|
||||
Write-Host "📡 Checking main application..." -ForegroundColor Yellow
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:3000/health" -UseBasicParsing -TimeoutSec 5
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Host "✅ Main application is running" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ Main application returned status code: $($response.StatusCode)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host "❌ Main application is not accessible: $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if API is responding
|
||||
Write-Host "🔌 Checking API endpoints..." -ForegroundColor Yellow
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:3000/api/tunnels" -UseBasicParsing -TimeoutSec 5
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Host "✅ API is responding" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ API returned status code: $($response.StatusCode)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host "❌ API is not responding: $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if FRPC container is running (if Docker is available)
|
||||
if (Get-Command docker -ErrorAction SilentlyContinue) {
|
||||
Write-Host "🐳 Checking FRPC container..." -ForegroundColor Yellow
|
||||
$dockerPs = docker ps 2>$null
|
||||
if ($dockerPs -and ($dockerPs | Select-String "frpc")) {
|
||||
Write-Host "✅ FRPC container is running" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ FRPC container is not running (this is expected in development)" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "⚠️ Docker is not available, skipping FRPC container check" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Check if database is accessible
|
||||
Write-Host "🗄️ Checking database..." -ForegroundColor Yellow
|
||||
if (Test-Path "./data/tunnels.db") {
|
||||
Write-Host "✅ Database file exists" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "ℹ️ Database file doesn't exist yet (will be created on first use)" -ForegroundColor Blue
|
||||
}
|
||||
|
||||
# Check if configuration directory exists
|
||||
Write-Host "📁 Checking configuration directory..." -ForegroundColor Yellow
|
||||
if (Test-Path "./data") {
|
||||
Write-Host "✅ Configuration directory exists" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ Configuration directory is missing" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🎉 Health check completed successfully!" -ForegroundColor Green
|
||||
Write-Host "👉 Open http://localhost:3000 to access the FRP Manager" -ForegroundColor Cyan
|
||||
58
app/health-check.sh
Normal file
58
app/health-check.sh
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Health check script for FRP Manager
|
||||
# This script checks if all services are running correctly
|
||||
|
||||
echo "🔍 FRP Manager Health Check"
|
||||
echo "=========================="
|
||||
|
||||
# Check if main application is running
|
||||
echo "📡 Checking main application..."
|
||||
if curl -s http://localhost:3000/health > /dev/null; then
|
||||
echo "✅ Main application is running"
|
||||
else
|
||||
echo "❌ Main application is not accessible"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if API is responding
|
||||
echo "🔌 Checking API endpoints..."
|
||||
if curl -s http://localhost:3000/api/tunnels > /dev/null; then
|
||||
echo "✅ API is responding"
|
||||
else
|
||||
echo "❌ API is not responding"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if FRPC container is running (if Docker is available)
|
||||
if command -v docker &> /dev/null; then
|
||||
echo "🐳 Checking FRPC container..."
|
||||
if docker ps | grep -q frpc; then
|
||||
echo "✅ FRPC container is running"
|
||||
else
|
||||
echo "⚠️ FRPC container is not running (this is expected in development)"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Docker is not available, skipping FRPC container check"
|
||||
fi
|
||||
|
||||
# Check if database is accessible
|
||||
echo "🗄️ Checking database..."
|
||||
if [ -f "./data/tunnels.db" ]; then
|
||||
echo "✅ Database file exists"
|
||||
else
|
||||
echo "ℹ️ Database file doesn't exist yet (will be created on first use)"
|
||||
fi
|
||||
|
||||
# Check if configuration directory exists
|
||||
echo "📁 Checking configuration directory..."
|
||||
if [ -d "./data" ]; then
|
||||
echo "✅ Configuration directory exists"
|
||||
else
|
||||
echo "❌ Configuration directory is missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 Health check completed successfully!"
|
||||
echo "👉 Open http://localhost:3000 to access the FRP Manager"
|
||||
13
app/index.html
Normal file
13
app/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/client/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
44
app/package.json
Normal file
44
app/package.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "arc-frp",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nodemon -w src/server -x tsx src/server/main.ts",
|
||||
"start": "cross-env NODE_ENV=production tsx src/server/main.ts",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-express": "*",
|
||||
"cross-env": "^7.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"sqlite3": "^5.1.6",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"zod": "^3.22.4",
|
||||
"winston": "^3.11.0",
|
||||
"uuid": "^9.0.1",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"@tanstack/react-query": "^5.13.4",
|
||||
"@tanstack/react-query-devtools": "^5.13.5",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"axios": "^1.6.2",
|
||||
"lucide-react": "^0.294.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"nodemon": "^3.1.10",
|
||||
"vite": "^6.3.3",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/uuid": "^9.0.7"
|
||||
}
|
||||
}
|
||||
1
app/public/vite.svg
Normal file
1
app/public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
922
app/src/client/App.css
Normal file
922
app/src/client/App.css
Normal file
|
|
@ -0,0 +1,922 @@
|
|||
/* Base styles */
|
||||
:root {
|
||||
--primary: #0066cc;
|
||||
--primary-hover: #0052a3;
|
||||
--secondary: #6c757d;
|
||||
--success: #28a745;
|
||||
--danger: #dc3545;
|
||||
--warning: #ffc107;
|
||||
--info: #17a2b8;
|
||||
--light: #f8f9fa;
|
||||
--dark: #343a40;
|
||||
--border: #dee2e6;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #6c757d;
|
||||
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.App {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
background-color: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.navbar-icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--secondary);
|
||||
border-color: var(--secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--success);
|
||||
border-color: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--danger);
|
||||
border-color: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: var(--warning);
|
||||
border-color: var(--warning);
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
.dashboard {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
font-size: 1.125rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-content h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-description {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dashboard-sections {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tunnel-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tunnel-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.tunnel-info h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.tunnel-info p {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-content h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.status-content p {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Tunnel Manager */
|
||||
.tunnel-manager {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tunnel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.tunnel-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.node-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.node-indicator {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.node-indicator.online {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.node-indicator.offline {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.tunnels-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.tunnel-card {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.tunnel-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tunnel-title h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tunnel-badges {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tunnel-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tunnel-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.tunnel-error {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 0.875rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Form overlay */
|
||||
.form-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.form-modal {
|
||||
background-color: var(--bg-primary);
|
||||
border-radius: 0.5rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.form-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tunnel-form {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Server Status */
|
||||
.server-status {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.server-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.server-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.server-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.server-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.control-section {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.control-section h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.status-section h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Logs */
|
||||
.logs-section {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.logs-header h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logs-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logs-select {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
background-color: var(--dark);
|
||||
color: var(--light);
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.logs-content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.logs-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* System info */
|
||||
.system-info {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.system-info h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.info-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-card-header h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-card-content p {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-detail {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.text-green-500 {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.text-yellow-500 {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.text-blue-500 {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.navbar {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-sections {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.server-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tunnels-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-modal {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tunnel-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
51
app/src/client/App.tsx
Normal file
51
app/src/client/App.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import Navbar from './components/Navbar';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import TunnelManager from './pages/TunnelManager';
|
||||
import ServerStatus from './pages/ServerStatus';
|
||||
import './App.css';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 2,
|
||||
staleTime: 5 * 1000, // 5 seconds
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<Navbar />
|
||||
<main className="main-content">
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/tunnels" element={<TunnelManager />} />
|
||||
<Route path="/status" element={<ServerStatus />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#363636',
|
||||
color: '#fff',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Router>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
199
app/src/client/api/client.ts
Normal file
199
app/src/client/api/client.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export interface TunnelConfig {
|
||||
id?: string;
|
||||
name: string;
|
||||
protocol: 'TCP' | 'UDP';
|
||||
localIp: string;
|
||||
localPort: number;
|
||||
remotePort: number;
|
||||
enabled: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface TunnelStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
lastChecked: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface FrpcStatus {
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
export interface FrpcLogs {
|
||||
logs: string;
|
||||
}
|
||||
|
||||
export interface NodeStatus {
|
||||
status: string;
|
||||
timestamp: string;
|
||||
uptime?: number;
|
||||
memory?: {
|
||||
used: number;
|
||||
total: number;
|
||||
};
|
||||
cpu?: {
|
||||
usage: number;
|
||||
};
|
||||
connection?: {
|
||||
url: string;
|
||||
isOnline: boolean;
|
||||
lastConnectionTime: Date | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NodeConnection {
|
||||
url: string;
|
||||
isOnline: boolean;
|
||||
lastConnectionTime: Date | null;
|
||||
isReachable: boolean;
|
||||
}
|
||||
|
||||
const API_BASE_URL = '/api';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Request interceptor for logging
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
console.error('API Request Error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor for error handling
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error('API Response Error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export const tunnelApi = {
|
||||
// Get all tunnels
|
||||
getAllTunnels: async (): Promise<TunnelConfig[]> => {
|
||||
const response = await apiClient.get('/tunnels');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get tunnel by ID
|
||||
getTunnelById: async (id: string): Promise<TunnelConfig> => {
|
||||
const response = await apiClient.get(`/tunnels/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create new tunnel
|
||||
createTunnel: async (tunnel: Omit<TunnelConfig, 'id' | 'createdAt' | 'updatedAt'>): Promise<TunnelConfig> => {
|
||||
const response = await apiClient.post('/tunnels', tunnel);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update tunnel
|
||||
updateTunnel: async (id: string, tunnel: Partial<TunnelConfig>): Promise<TunnelConfig> => {
|
||||
const response = await apiClient.put(`/tunnels/${id}`, tunnel);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete tunnel
|
||||
deleteTunnel: async (id: string): Promise<void> => {
|
||||
await apiClient.delete(`/tunnels/${id}`);
|
||||
},
|
||||
|
||||
// Get tunnel status
|
||||
getTunnelStatus: async (id: string): Promise<TunnelStatus> => {
|
||||
const response = await apiClient.get(`/tunnels/${id}/status`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get all tunnel statuses
|
||||
getAllTunnelStatuses: async (): Promise<TunnelStatus[]> => {
|
||||
const response = await apiClient.get('/tunnels-status');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const frpcApi = {
|
||||
// Get frpc status
|
||||
getStatus: async (): Promise<FrpcStatus> => {
|
||||
const response = await apiClient.get('/frpc/status');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Control frpc service
|
||||
start: async (): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/frpc/start');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
stop: async (): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/frpc/stop');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
restart: async (): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/frpc/restart');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
regenerate: async (): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/frpc/regenerate');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get frpc logs
|
||||
getLogs: async (lines: number = 50): Promise<FrpcLogs> => {
|
||||
const response = await apiClient.get(`/frpc/logs?lines=${lines}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const nodeApi = {
|
||||
// Get node status
|
||||
getStatus: async (): Promise<NodeStatus> => {
|
||||
const response = await apiClient.get('/node/status');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get node connection info
|
||||
getConnection: async (): Promise<NodeConnection> => {
|
||||
const response = await apiClient.get('/node/connection');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Push configuration to node
|
||||
pushConfig: async (): Promise<{ message: string; tunnelCount: number; nodeResponse: any }> => {
|
||||
const response = await apiClient.post('/node/push-config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Restart frpc on node
|
||||
restartFrpc: async (): Promise<{ message: string; nodeResponse: any }> => {
|
||||
const response = await apiClient.post('/node/restart-frpc');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Push config and restart frpc on node
|
||||
pushAndRestart: async (): Promise<{
|
||||
message: string;
|
||||
tunnelCount: number;
|
||||
configResponse: any;
|
||||
restartResponse: any
|
||||
}> => {
|
||||
const response = await apiClient.post('/node/push-and-restart');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default apiClient;
|
||||
1
app/src/client/assets/react.svg
Normal file
1
app/src/client/assets/react.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
43
app/src/client/components/Navbar.tsx
Normal file
43
app/src/client/components/Navbar.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Server, Settings, Activity } from 'lucide-react';
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
return (
|
||||
<nav className="navbar">
|
||||
<div className="navbar-brand">
|
||||
<Server className="navbar-icon" />
|
||||
<span>FRP Manager</span>
|
||||
</div>
|
||||
<div className="navbar-nav">
|
||||
<Link
|
||||
to="/"
|
||||
className={`nav-link ${isActive('/') ? 'active' : ''}`}
|
||||
>
|
||||
<Activity size={18} />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/tunnels"
|
||||
className={`nav-link ${isActive('/tunnels') ? 'active' : ''}`}
|
||||
>
|
||||
<Settings size={18} />
|
||||
Tunnels
|
||||
</Link>
|
||||
<Link
|
||||
to="/status"
|
||||
className={`nav-link ${isActive('/status') ? 'active' : ''}`}
|
||||
>
|
||||
<Server size={18} />
|
||||
Server Status
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
232
app/src/client/components/TunnelForm.tsx
Normal file
232
app/src/client/components/TunnelForm.tsx
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { tunnelApi, TunnelConfig } from '../api/client';
|
||||
import { X, Save, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
interface TunnelFormProps {
|
||||
tunnel?: TunnelConfig | null;
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const TunnelForm: React.FC<TunnelFormProps> = ({ tunnel, onClose, onSubmit }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const isEditing = Boolean(tunnel);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: tunnel?.name || '',
|
||||
protocol: tunnel?.protocol || 'TCP' as 'TCP' | 'UDP',
|
||||
localIp: tunnel?.localIp || '127.0.0.1',
|
||||
localPort: tunnel?.localPort || 8080,
|
||||
remotePort: tunnel?.remotePort || 8080,
|
||||
enabled: tunnel?.enabled ?? true,
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: tunnelApi.createTunnel,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||
toast.success('Tunnel created successfully');
|
||||
onSubmit();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Create error:', error);
|
||||
toast.error('Failed to create tunnel');
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, updates }: { id: string; updates: Partial<TunnelConfig> }) =>
|
||||
tunnelApi.updateTunnel(id, updates),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||
toast.success('Tunnel updated successfully');
|
||||
onSubmit();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Update error:', error);
|
||||
toast.error('Failed to update tunnel');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
toast.error('Please enter a name');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.localPort < 1 || formData.localPort > 65535) {
|
||||
toast.error('Local port must be between 1 and 65535');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.remotePort < 1 || formData.remotePort > 65535) {
|
||||
toast.error('Remote port must be between 1 and 65535');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditing && tunnel) {
|
||||
updateMutation.mutate({
|
||||
id: tunnel.id!,
|
||||
updates: formData,
|
||||
});
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
|
||||
if (type === 'checkbox') {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: checked,
|
||||
}));
|
||||
} else if (type === 'number') {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: parseInt(value) || 0,
|
||||
}));
|
||||
} else {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
return (
|
||||
<div className="form-overlay">
|
||||
<div className="form-modal">
|
||||
<div className="form-header">
|
||||
<h2>{isEditing ? 'Edit Tunnel' : 'Create New Tunnel'}</h2>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={onClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="tunnel-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., Minecraft Server"
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="protocol">Protocol</label>
|
||||
<select
|
||||
id="protocol"
|
||||
name="protocol"
|
||||
value={formData.protocol}
|
||||
onChange={handleChange}
|
||||
disabled={isPending}
|
||||
>
|
||||
<option value="TCP">TCP</option>
|
||||
<option value="UDP">UDP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="localIp">Local IP</label>
|
||||
<input
|
||||
type="text"
|
||||
id="localIp"
|
||||
name="localIp"
|
||||
value={formData.localIp}
|
||||
onChange={handleChange}
|
||||
placeholder="127.0.0.1"
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="localPort">Local Port</label>
|
||||
<input
|
||||
type="number"
|
||||
id="localPort"
|
||||
name="localPort"
|
||||
value={formData.localPort}
|
||||
onChange={handleChange}
|
||||
min="1"
|
||||
max="65535"
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="remotePort">Remote Port</label>
|
||||
<input
|
||||
type="number"
|
||||
id="remotePort"
|
||||
name="remotePort"
|
||||
value={formData.remotePort}
|
||||
onChange={handleChange}
|
||||
min="1"
|
||||
max="65535"
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="enabled"
|
||||
checked={formData.enabled}
|
||||
onChange={handleChange}
|
||||
disabled={isPending}
|
||||
/>
|
||||
<span>Enable this tunnel</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={onClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Loader2 className="animate-spin" size={16} />}
|
||||
<Save size={16} />
|
||||
{isEditing ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TunnelForm;
|
||||
69
app/src/client/index.css
Normal file
69
app/src/client/index.css
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
12
app/src/client/main.tsx
Normal file
12
app/src/client/main.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import "./index.css";
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
213
app/src/client/pages/Dashboard.tsx
Normal file
213
app/src/client/pages/Dashboard.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { tunnelApi, frpcApi, nodeApi } from '../api/client';
|
||||
import { CheckCircle, XCircle, AlertCircle, RefreshCw, Wifi, WifiOff } from 'lucide-react';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { data: tunnels, isLoading: tunnelsLoading } = useQuery({
|
||||
queryKey: ['tunnels'],
|
||||
queryFn: tunnelApi.getAllTunnels,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const { data: tunnelStatuses, isLoading: statusLoading } = useQuery({
|
||||
queryKey: ['tunnel-statuses'],
|
||||
queryFn: tunnelApi.getAllTunnelStatuses,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const { data: frpcStatus, isLoading: frpcLoading } = useQuery({
|
||||
queryKey: ['frpc-status'],
|
||||
queryFn: frpcApi.getStatus,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const { data: nodeConnection } = useQuery({
|
||||
queryKey: ['node-connection'],
|
||||
queryFn: nodeApi.getConnection,
|
||||
refetchInterval: 30000,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const { data: nodeStatus } = useQuery({
|
||||
queryKey: ['node-status'],
|
||||
queryFn: nodeApi.getStatus,
|
||||
refetchInterval: 30000,
|
||||
retry: false,
|
||||
enabled: !!nodeConnection?.isReachable,
|
||||
});
|
||||
|
||||
const activeTunnels = tunnels?.filter(t => t.enabled) || [];
|
||||
const activeTunnelStatuses = tunnelStatuses?.filter(s => s.active) || [];
|
||||
|
||||
if (tunnelsLoading || statusLoading || frpcLoading) {
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<div className="loading">
|
||||
<RefreshCw className="animate-spin" size={24} />
|
||||
<span>Loading dashboard...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<div className="dashboard-header">
|
||||
<h1>FRP Manager Dashboard</h1>
|
||||
<p>Manage your tunnel configurations and monitor their status</p>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">
|
||||
<CheckCircle className="text-green-500" size={24} />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<h3>Active Tunnels</h3>
|
||||
<p className="stat-number">{activeTunnelStatuses.length}</p>
|
||||
<p className="stat-description">Currently running</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">
|
||||
<AlertCircle className="text-yellow-500" size={24} />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<h3>Total Tunnels</h3>
|
||||
<p className="stat-number">{tunnels?.length || 0}</p>
|
||||
<p className="stat-description">Configured tunnels</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">
|
||||
{frpcStatus?.running ? (
|
||||
<CheckCircle className="text-green-500" size={24} />
|
||||
) : (
|
||||
<XCircle className="text-red-500" size={24} />
|
||||
)}
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<h3>FRPC Service</h3>
|
||||
<p className="stat-number">{frpcStatus?.running ? 'Running' : 'Stopped'}</p>
|
||||
<p className="stat-description">Service status</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">
|
||||
<CheckCircle className="text-blue-500" size={24} />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<h3>Enabled Tunnels</h3>
|
||||
<p className="stat-number">{activeTunnels.length}</p>
|
||||
<p className="stat-description">Ready to connect</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">
|
||||
{nodeConnection?.isReachable ? (
|
||||
<Wifi className="text-green-500" size={24} />
|
||||
) : (
|
||||
<WifiOff className="text-red-500" size={24} />
|
||||
)}
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<h3>Node Status</h3>
|
||||
<p className="stat-number">{nodeConnection?.isReachable ? 'Online' : 'Offline'}</p>
|
||||
<p className="stat-description">Home server agent</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-sections">
|
||||
<div className="section">
|
||||
<h2>Recent Tunnels</h2>
|
||||
<div className="tunnel-list">
|
||||
{activeTunnels.slice(0, 5).map(tunnel => (
|
||||
<div key={tunnel.id} className="tunnel-item">
|
||||
<div className="tunnel-info">
|
||||
<h4>{tunnel.name}</h4>
|
||||
<p>{tunnel.protocol} • {tunnel.localIp}:{tunnel.localPort} → :{tunnel.remotePort}</p>
|
||||
</div>
|
||||
<div className="tunnel-status">
|
||||
{tunnelStatuses?.find(s => s.id === tunnel.id)?.active ? (
|
||||
<span className="status-badge status-active">Active</span>
|
||||
) : (
|
||||
<span className="status-badge status-inactive">Inactive</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{activeTunnels.length === 0 && (
|
||||
<p className="empty-state">No active tunnels configured</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>System Status</h2>
|
||||
<div className="status-list">
|
||||
<div className="status-item">
|
||||
<div className="status-indicator">
|
||||
{frpcStatus?.running ? (
|
||||
<CheckCircle className="text-green-500" size={20} />
|
||||
) : (
|
||||
<XCircle className="text-red-500" size={20} />
|
||||
)}
|
||||
</div>
|
||||
<div className="status-content">
|
||||
<h4>FRPC Service</h4>
|
||||
<p>{frpcStatus?.running ? 'Service is running normally' : 'Service is stopped'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<div className="status-indicator">
|
||||
<CheckCircle className="text-green-500" size={20} />
|
||||
</div>
|
||||
<div className="status-content">
|
||||
<h4>API Server</h4>
|
||||
<p>API is responding normally</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<div className="status-indicator">
|
||||
<CheckCircle className="text-green-500" size={20} />
|
||||
</div>
|
||||
<div className="status-content">
|
||||
<h4>Database</h4>
|
||||
<p>Database connection is healthy</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<div className="status-indicator">
|
||||
{nodeConnection?.isReachable ? (
|
||||
<Wifi className="text-green-500" size={20} />
|
||||
) : (
|
||||
<WifiOff className="text-red-500" size={20} />
|
||||
)}
|
||||
</div>
|
||||
<div className="status-content">
|
||||
<h4>Home Server Node</h4>
|
||||
<p>
|
||||
{nodeConnection?.isReachable
|
||||
? `Connected • Last seen: ${nodeConnection.lastConnectionTime ? new Date(nodeConnection.lastConnectionTime).toLocaleTimeString() : 'Now'}`
|
||||
: 'Disconnected or unreachable'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
253
app/src/client/pages/ServerStatus.tsx
Normal file
253
app/src/client/pages/ServerStatus.tsx
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { frpcApi } from '../api/client';
|
||||
import { Server, Play, Square, RotateCcw, RefreshCw, FileText, Activity } from 'lucide-react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
const ServerStatus: React.FC = () => {
|
||||
const [logsLines, setLogsLines] = useState(50);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: frpcStatus, isLoading: statusLoading } = useQuery({
|
||||
queryKey: ['frpc-status'],
|
||||
queryFn: frpcApi.getStatus,
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
const { data: logs, isLoading: logsLoading } = useQuery({
|
||||
queryKey: ['frpc-logs', logsLines],
|
||||
queryFn: () => frpcApi.getLogs(logsLines),
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const startMutation = useMutation({
|
||||
mutationFn: frpcApi.start,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
|
||||
toast.success('FRPC service started');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Start error:', error);
|
||||
toast.error('Failed to start FRPC service');
|
||||
},
|
||||
});
|
||||
|
||||
const stopMutation = useMutation({
|
||||
mutationFn: frpcApi.stop,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
|
||||
toast.success('FRPC service stopped');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Stop error:', error);
|
||||
toast.error('Failed to stop FRPC service');
|
||||
},
|
||||
});
|
||||
|
||||
const restartMutation = useMutation({
|
||||
mutationFn: frpcApi.restart,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
|
||||
toast.success('FRPC service restarted');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Restart error:', error);
|
||||
toast.error('Failed to restart FRPC service');
|
||||
},
|
||||
});
|
||||
|
||||
const regenerateMutation = useMutation({
|
||||
mutationFn: frpcApi.regenerate,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
|
||||
toast.success('FRPC configuration regenerated');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Regenerate error:', error);
|
||||
toast.error('Failed to regenerate FRPC configuration');
|
||||
},
|
||||
});
|
||||
|
||||
const handleStart = () => startMutation.mutate();
|
||||
const handleStop = () => stopMutation.mutate();
|
||||
const handleRestart = () => restartMutation.mutate();
|
||||
const handleRegenerate = () => regenerateMutation.mutate();
|
||||
|
||||
const isLoading = statusLoading || logsLoading;
|
||||
const isAnyMutationPending =
|
||||
startMutation.isPending ||
|
||||
stopMutation.isPending ||
|
||||
restartMutation.isPending ||
|
||||
regenerateMutation.isPending;
|
||||
|
||||
return (
|
||||
<div className="server-status">
|
||||
<div className="server-header">
|
||||
<h1>Server Status</h1>
|
||||
<div className="server-info">
|
||||
<div className="status-indicator">
|
||||
<Activity
|
||||
size={24}
|
||||
className={frpcStatus?.running ? 'text-green-500' : 'text-red-500'}
|
||||
/>
|
||||
<span className={`status-text ${frpcStatus?.running ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{frpcStatus?.running ? 'Running' : 'Stopped'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="server-controls">
|
||||
<div className="control-section">
|
||||
<h2>Service Controls</h2>
|
||||
<div className="control-buttons">
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={handleStart}
|
||||
disabled={isAnyMutationPending || frpcStatus?.running}
|
||||
>
|
||||
<Play size={18} />
|
||||
Start
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleStop}
|
||||
disabled={isAnyMutationPending || !frpcStatus?.running}
|
||||
>
|
||||
<Square size={18} />
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={handleRestart}
|
||||
disabled={isAnyMutationPending}
|
||||
>
|
||||
<RotateCcw size={18} />
|
||||
Restart
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleRegenerate}
|
||||
disabled={isAnyMutationPending}
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
Regenerate Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="status-section">
|
||||
<h2>Service Information</h2>
|
||||
<div className="info-grid">
|
||||
<div className="info-item">
|
||||
<span className="info-label">Status:</span>
|
||||
<span className={`info-value ${frpcStatus?.running ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{frpcStatus?.running ? 'Running' : 'Stopped'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Container:</span>
|
||||
<span className="info-value">frpc</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Last Updated:</span>
|
||||
<span className="info-value">
|
||||
{new Date().toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="logs-section">
|
||||
<div className="logs-header">
|
||||
<h2>
|
||||
<FileText size={20} />
|
||||
Service Logs
|
||||
</h2>
|
||||
<div className="logs-controls">
|
||||
<select
|
||||
value={logsLines}
|
||||
onChange={(e) => setLogsLines(parseInt(e.target.value))}
|
||||
className="logs-select"
|
||||
>
|
||||
<option value={25}>Last 25 lines</option>
|
||||
<option value={50}>Last 50 lines</option>
|
||||
<option value={100}>Last 100 lines</option>
|
||||
<option value={200}>Last 200 lines</option>
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={() => queryClient.invalidateQueries({ queryKey: ['frpc-logs'] })}
|
||||
disabled={logsLoading}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="logs-container">
|
||||
{logsLoading ? (
|
||||
<div className="logs-loading">
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
<span>Loading logs...</span>
|
||||
</div>
|
||||
) : (
|
||||
<pre className="logs-content">
|
||||
{logs?.logs || 'No logs available'}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="system-info">
|
||||
<h2>System Information</h2>
|
||||
<div className="info-cards">
|
||||
<div className="info-card">
|
||||
<div className="info-card-header">
|
||||
<Server size={24} />
|
||||
<h3>FRPC Service</h3>
|
||||
</div>
|
||||
<div className="info-card-content">
|
||||
<p>Fast Reverse Proxy Client for tunneling services</p>
|
||||
<p className="info-detail">
|
||||
Status: <span className={frpcStatus?.running ? 'text-green-500' : 'text-red-500'}>
|
||||
{frpcStatus?.running ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="info-card">
|
||||
<div className="info-card-header">
|
||||
<Activity size={24} />
|
||||
<h3>API Server</h3>
|
||||
</div>
|
||||
<div className="info-card-content">
|
||||
<p>RESTful API for managing tunnel configurations</p>
|
||||
<p className="info-detail">
|
||||
Status: <span className="text-green-500">Running</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="info-card">
|
||||
<div className="info-card-header">
|
||||
<FileText size={24} />
|
||||
<h3>Configuration</h3>
|
||||
</div>
|
||||
<div className="info-card-content">
|
||||
<p>Tunnel configurations stored in SQLite database</p>
|
||||
<p className="info-detail">
|
||||
Auto-generated FRPC config from active tunnels
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerStatus;
|
||||
244
app/src/client/pages/TunnelManager.tsx
Normal file
244
app/src/client/pages/TunnelManager.tsx
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { tunnelApi, nodeApi, TunnelConfig } from '../api/client';
|
||||
import { Plus, Edit2, Trash2, Power, PowerOff, Settings, Send, RefreshCw } from 'lucide-react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import TunnelForm from '../components/TunnelForm';
|
||||
|
||||
const TunnelManager: React.FC = () => {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingTunnel, setEditingTunnel] = useState<TunnelConfig | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: tunnels, isLoading } = useQuery({
|
||||
queryKey: ['tunnels'],
|
||||
queryFn: tunnelApi.getAllTunnels,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const { data: tunnelStatuses } = useQuery({
|
||||
queryKey: ['tunnel-statuses'],
|
||||
queryFn: tunnelApi.getAllTunnelStatuses,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const { data: nodeConnection } = useQuery({
|
||||
queryKey: ['node-connection'],
|
||||
queryFn: nodeApi.getConnection,
|
||||
refetchInterval: 30000,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: tunnelApi.deleteTunnel,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||
toast.success('Tunnel deleted successfully');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Delete error:', error);
|
||||
toast.error('Failed to delete tunnel');
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, updates }: { id: string; updates: Partial<TunnelConfig> }) =>
|
||||
tunnelApi.updateTunnel(id, updates),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||
toast.success('Tunnel updated successfully');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Update error:', error);
|
||||
toast.error('Failed to update tunnel');
|
||||
},
|
||||
});
|
||||
|
||||
const pushToNodeMutation = useMutation({
|
||||
mutationFn: nodeApi.pushAndRestart,
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Successfully pushed ${data.tunnelCount} tunnels to node and restarted frpc`);
|
||||
queryClient.invalidateQueries({ queryKey: ['node-connection'] });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Push to node error:', error);
|
||||
toast.error(error.response?.data?.error || 'Failed to push configuration to node');
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (window.confirm('Are you sure you want to delete this tunnel?')) {
|
||||
deleteMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (tunnel: TunnelConfig) => {
|
||||
setEditingTunnel(tunnel);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleToggleEnabled = (tunnel: TunnelConfig) => {
|
||||
updateMutation.mutate({
|
||||
id: tunnel.id!,
|
||||
updates: { enabled: !tunnel.enabled },
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormClose = () => {
|
||||
setShowForm(false);
|
||||
setEditingTunnel(null);
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
handleFormClose();
|
||||
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||
};
|
||||
|
||||
const handlePushToNode = () => {
|
||||
if (window.confirm('Push current tunnel configuration to node and restart frpc?')) {
|
||||
pushToNodeMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="tunnel-manager">
|
||||
<div className="loading">Loading tunnels...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tunnel-manager">
|
||||
<div className="tunnel-header">
|
||||
<h1>Tunnel Manager</h1>
|
||||
<div className="header-actions">
|
||||
{nodeConnection && (
|
||||
<div className="node-status">
|
||||
<span className={`node-indicator ${nodeConnection.isReachable ? 'online' : 'offline'}`}>
|
||||
Node: {nodeConnection.isReachable ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handlePushToNode}
|
||||
disabled={pushToNodeMutation.isPending || !nodeConnection.isReachable}
|
||||
title="Push configuration to node and restart frpc"
|
||||
>
|
||||
{pushToNodeMutation.isPending ? (
|
||||
<RefreshCw size={18} className="spinning" />
|
||||
) : (
|
||||
<Send size={18} />
|
||||
)}
|
||||
Push to Node
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowForm(true)}
|
||||
>
|
||||
<Plus size={18} />
|
||||
Add Tunnel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tunnels-grid">
|
||||
{tunnels?.map(tunnel => {
|
||||
const status = tunnelStatuses?.find(s => s.id === tunnel.id);
|
||||
return (
|
||||
<div key={tunnel.id} className="tunnel-card">
|
||||
<div className="tunnel-card-header">
|
||||
<div className="tunnel-title">
|
||||
<h3>{tunnel.name}</h3>
|
||||
<div className="tunnel-badges">
|
||||
<span className={`badge ${tunnel.enabled ? 'badge-success' : 'badge-secondary'}`}>
|
||||
{tunnel.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
<span className={`badge ${status?.active ? 'badge-active' : 'badge-inactive'}`}>
|
||||
{status?.active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tunnel-actions">
|
||||
<button
|
||||
className={`btn btn-sm ${tunnel.enabled ? 'btn-warning' : 'btn-success'}`}
|
||||
onClick={() => handleToggleEnabled(tunnel)}
|
||||
disabled={updateMutation.isPending}
|
||||
>
|
||||
{tunnel.enabled ? <PowerOff size={16} /> : <Power size={16} />}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={() => handleEdit(tunnel)}
|
||||
>
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => handleDelete(tunnel.id!)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tunnel-info">
|
||||
<div className="info-row">
|
||||
<span className="info-label">Protocol:</span>
|
||||
<span className="info-value">{tunnel.protocol}</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">Local:</span>
|
||||
<span className="info-value">{tunnel.localIp}:{tunnel.localPort}</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">Remote:</span>
|
||||
<span className="info-value">:{tunnel.remotePort}</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">Created:</span>
|
||||
<span className="info-value">
|
||||
{tunnel.createdAt ? new Date(tunnel.createdAt).toLocaleDateString() : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status?.error && (
|
||||
<div className="tunnel-error">
|
||||
<span className="error-text">Error: {status.error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{tunnels?.length === 0 && (
|
||||
<div className="empty-state">
|
||||
<Settings size={64} className="empty-icon" />
|
||||
<h3>No tunnels configured</h3>
|
||||
<p>Get started by adding your first tunnel configuration</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowForm(true)}
|
||||
>
|
||||
<Plus size={18} />
|
||||
Add Your First Tunnel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
<TunnelForm
|
||||
tunnel={editingTunnel}
|
||||
onClose={handleFormClose}
|
||||
onSubmit={handleFormSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TunnelManager;
|
||||
8
app/src/client/tsconfig.json
Normal file
8
app/src/client/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
1
app/src/client/vite-env.d.ts
vendored
Normal file
1
app/src/client/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
179
app/src/server/database.ts
Normal file
179
app/src/server/database.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import Database from 'better-sqlite3';
|
||||
import { TunnelConfig, TunnelConfigSchema, TunnelConfigUpdate } from './types.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import path from 'path';
|
||||
|
||||
export class TunnelDatabase {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(dbPath: string = 'data/tunnels.db') {
|
||||
this.db = new Database(dbPath);
|
||||
this.initializeDatabase();
|
||||
}
|
||||
|
||||
private initializeDatabase() {
|
||||
// Create tunnels table
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS tunnels (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
protocol TEXT NOT NULL CHECK(protocol IN ('TCP', 'UDP')),
|
||||
local_ip TEXT NOT NULL,
|
||||
local_port INTEGER NOT NULL,
|
||||
remote_port INTEGER NOT NULL,
|
||||
enabled BOOLEAN DEFAULT 1,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Create trigger to update updated_at column
|
||||
this.db.exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_tunnels_updated_at
|
||||
AFTER UPDATE ON tunnels
|
||||
BEGIN
|
||||
UPDATE tunnels SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END
|
||||
`);
|
||||
}
|
||||
|
||||
// Get all tunnel configurations
|
||||
getAllTunnels(): TunnelConfig[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
protocol,
|
||||
local_ip as localIp,
|
||||
local_port as localPort,
|
||||
remote_port as remotePort,
|
||||
enabled,
|
||||
created_at as createdAt,
|
||||
updated_at as updatedAt
|
||||
FROM tunnels
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
return stmt.all() as TunnelConfig[];
|
||||
}
|
||||
|
||||
// Get tunnel by ID
|
||||
getTunnelById(id: string): TunnelConfig | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
protocol,
|
||||
local_ip as localIp,
|
||||
local_port as localPort,
|
||||
remote_port as remotePort,
|
||||
enabled,
|
||||
created_at as createdAt,
|
||||
updated_at as updatedAt
|
||||
FROM tunnels
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const result = stmt.get(id) as TunnelConfig | undefined;
|
||||
return result || null;
|
||||
}
|
||||
|
||||
// Create new tunnel configuration
|
||||
createTunnel(config: Omit<TunnelConfig, 'id' | 'createdAt' | 'updatedAt'>): TunnelConfig {
|
||||
const id = uuidv4();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO tunnels (id, name, protocol, local_ip, local_port, remote_port, enabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(id, config.name, config.protocol, config.localIp, config.localPort, config.remotePort, config.enabled);
|
||||
|
||||
return {
|
||||
id,
|
||||
...config,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
}
|
||||
|
||||
// Update tunnel configuration
|
||||
updateTunnel(id: string, updates: Partial<TunnelConfig>): TunnelConfig | null {
|
||||
const existing = this.getTunnelById(id);
|
||||
if (!existing) return null;
|
||||
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (updates.name !== undefined) {
|
||||
fields.push('name = ?');
|
||||
values.push(updates.name);
|
||||
}
|
||||
if (updates.protocol !== undefined) {
|
||||
fields.push('protocol = ?');
|
||||
values.push(updates.protocol);
|
||||
}
|
||||
if (updates.localIp !== undefined) {
|
||||
fields.push('local_ip = ?');
|
||||
values.push(updates.localIp);
|
||||
}
|
||||
if (updates.localPort !== undefined) {
|
||||
fields.push('local_port = ?');
|
||||
values.push(updates.localPort);
|
||||
}
|
||||
if (updates.remotePort !== undefined) {
|
||||
fields.push('remote_port = ?');
|
||||
values.push(updates.remotePort);
|
||||
}
|
||||
if (updates.enabled !== undefined) {
|
||||
fields.push('enabled = ?');
|
||||
values.push(updates.enabled);
|
||||
}
|
||||
|
||||
if (fields.length === 0) return existing;
|
||||
|
||||
values.push(id);
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE tunnels
|
||||
SET ${fields.join(', ')}
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(...values);
|
||||
return this.getTunnelById(id);
|
||||
}
|
||||
|
||||
// Delete tunnel configuration
|
||||
deleteTunnel(id: string): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM tunnels WHERE id = ?');
|
||||
const result = stmt.run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
// Get enabled tunnels only
|
||||
getEnabledTunnels(): TunnelConfig[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
protocol,
|
||||
local_ip as localIp,
|
||||
local_port as localPort,
|
||||
remote_port as remotePort,
|
||||
enabled,
|
||||
created_at as createdAt,
|
||||
updated_at as updatedAt
|
||||
FROM tunnels
|
||||
WHERE enabled = 1
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
return stmt.all() as TunnelConfig[];
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
close() {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
145
app/src/server/frpc-manager.ts
Normal file
145
app/src/server/frpc-manager.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { TunnelConfig, FrpcConfig } from './types.js';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export class FrpcManager {
|
||||
private configPath: string;
|
||||
private containerName: string;
|
||||
|
||||
constructor(configPath: string = 'data/frpc.toml', containerName: string = 'frpc') {
|
||||
this.configPath = configPath;
|
||||
this.containerName = containerName;
|
||||
}
|
||||
|
||||
// Generate frpc.toml configuration from tunnel configs
|
||||
async generateConfig(tunnels: TunnelConfig[], serverAddr: string, serverPort: number = 7000, token?: string): Promise<void> {
|
||||
try {
|
||||
const config: FrpcConfig = {
|
||||
serverAddr,
|
||||
serverPort,
|
||||
token,
|
||||
proxies: {}
|
||||
};
|
||||
|
||||
// Add enabled tunnels to config
|
||||
for (const tunnel of tunnels.filter(t => t.enabled)) {
|
||||
config.proxies[tunnel.name] = {
|
||||
type: tunnel.protocol.toLowerCase() as 'tcp' | 'udp',
|
||||
localIP: tunnel.localIp,
|
||||
localPort: tunnel.localPort,
|
||||
remotePort: tunnel.remotePort
|
||||
};
|
||||
}
|
||||
|
||||
const tomlContent = this.generateTomlContent(config);
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(this.configPath);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
|
||||
await fs.writeFile(this.configPath, tomlContent);
|
||||
logger.info(`Generated frpc configuration with ${Object.keys(config.proxies).length} tunnels`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate frpc configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private generateTomlContent(config: FrpcConfig): string {
|
||||
let toml = `[common]
|
||||
server_addr = "${config.serverAddr}"
|
||||
server_port = ${config.serverPort}
|
||||
`;
|
||||
|
||||
if (config.token) {
|
||||
toml += `token = "${config.token}"\n`;
|
||||
}
|
||||
|
||||
toml += '\n';
|
||||
|
||||
// Add proxy configurations
|
||||
for (const [name, proxy] of Object.entries(config.proxies)) {
|
||||
toml += `[${name}]
|
||||
type = "${proxy.type}"
|
||||
local_ip = "${proxy.localIP}"
|
||||
local_port = ${proxy.localPort}
|
||||
remote_port = ${proxy.remotePort}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
return toml;
|
||||
}
|
||||
|
||||
// Check if frpc container is running
|
||||
async isRunning(): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync(`docker ps --filter "name=${this.containerName}" --format "{{.Names}}"`);
|
||||
return stdout.trim() === this.containerName;
|
||||
} catch (error) {
|
||||
logger.error('Failed to check frpc container status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Start frpc container
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
await execAsync(`docker start ${this.containerName}`);
|
||||
logger.info(`Started frpc container: ${this.containerName}`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to start frpc container:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop frpc container
|
||||
async stop(): Promise<void> {
|
||||
try {
|
||||
await execAsync(`docker stop ${this.containerName}`);
|
||||
logger.info(`Stopped frpc container: ${this.containerName}`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to stop frpc container:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Restart frpc container
|
||||
async restart(): Promise<void> {
|
||||
try {
|
||||
await execAsync(`docker restart ${this.containerName}`);
|
||||
logger.info(`Restarted frpc container: ${this.containerName}`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to restart frpc container:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get frpc container logs
|
||||
async getLogs(lines: number = 50): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execAsync(`docker logs --tail ${lines} ${this.containerName}`);
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get frpc container logs:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Check tunnel status by attempting to connect
|
||||
async checkTunnelStatus(tunnel: TunnelConfig): Promise<boolean> {
|
||||
try {
|
||||
// This is a basic implementation - you might want to implement actual connectivity checks
|
||||
// For now, we'll just check if the container is running
|
||||
return await this.isRunning();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to check tunnel status for ${tunnel.name}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
app/src/server/logger.ts
Normal file
42
app/src/server/logger.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import winston from 'winston';
|
||||
import path from 'path';
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
const logDir = 'logs';
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: { service: 'frpc-manager' },
|
||||
transports: [
|
||||
// Write all logs with level 'error' and below to error.log
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'error.log'),
|
||||
level: 'error',
|
||||
handleExceptions: true,
|
||||
handleRejections: true
|
||||
}),
|
||||
// Write all logs with level 'info' and below to combined.log
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'combined.log'),
|
||||
handleExceptions: true,
|
||||
handleRejections: true
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// If we're not in production, also log to console with simple format
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
export default logger;
|
||||
55
app/src/server/main.ts
Normal file
55
app/src/server/main.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import express from "express";
|
||||
import ViteExpress from "vite-express";
|
||||
import cors from "cors";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { logger } from "./logger.js";
|
||||
import routes from "./routes.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
// Get __dirname in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Logging middleware
|
||||
app.use((req, res, next) => {
|
||||
logger.info(`${req.method} ${req.path}`, {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.use("/api", routes);
|
||||
|
||||
// Health check endpoint
|
||||
app.get("/health", (_, res) => {
|
||||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Legacy hello endpoint
|
||||
app.get("/hello", (_, res) => {
|
||||
res.send("Hello Vite + React + TypeScript!");
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.error('Unhandled error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '3000');
|
||||
|
||||
ViteExpress.listen(app, PORT, () => {
|
||||
logger.info(`Server is listening on port ${PORT}`);
|
||||
console.log(`Server is listening on port ${PORT}...`);
|
||||
});
|
||||
114
app/src/server/node-client.ts
Normal file
114
app/src/server/node-client.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import axios, { AxiosResponse } from 'axios';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
export interface NodeStatus {
|
||||
status: string;
|
||||
timestamp: string;
|
||||
uptime?: number;
|
||||
memory?: {
|
||||
used: number;
|
||||
total: number;
|
||||
};
|
||||
cpu?: {
|
||||
usage: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NodeConfig {
|
||||
url: string;
|
||||
token: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export class NodeClient {
|
||||
private config: NodeConfig;
|
||||
private lastConnectionTime: Date | null = null;
|
||||
private isOnline: boolean = false;
|
||||
|
||||
constructor(config: NodeConfig) {
|
||||
this.config = {
|
||||
...config,
|
||||
timeout: config.timeout || 5000,
|
||||
};
|
||||
}
|
||||
|
||||
private async makeRequest<T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
endpoint: string,
|
||||
data?: any
|
||||
): Promise<T> {
|
||||
try {
|
||||
const response: AxiosResponse<T> = await axios({
|
||||
method,
|
||||
url: `${this.config.url}${endpoint}`,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.config.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data,
|
||||
timeout: this.config.timeout,
|
||||
});
|
||||
|
||||
this.lastConnectionTime = new Date();
|
||||
this.isOnline = true;
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.isOnline = false;
|
||||
logger.error(`Node request failed: ${method} ${endpoint}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get node status
|
||||
async getStatus(): Promise<NodeStatus> {
|
||||
return this.makeRequest<NodeStatus>('GET', '/api/status');
|
||||
}
|
||||
|
||||
// Send updated frpc.toml config to the node
|
||||
async updateConfig(config: string): Promise<{ success: boolean; message: string }> {
|
||||
return this.makeRequest<{ success: boolean; message: string }>('POST', '/api/frpc/update-config', {
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
// Restart the FRP client on the node
|
||||
async restartFrpc(): Promise<{ success: boolean; message: string }> {
|
||||
return this.makeRequest<{ success: boolean; message: string }>('POST', '/api/frpc/restart');
|
||||
}
|
||||
|
||||
// Check if node is reachable
|
||||
async isReachable(): Promise<boolean> {
|
||||
try {
|
||||
await this.makeRequest<any>('GET', '/health');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get connection info
|
||||
getConnectionInfo() {
|
||||
return {
|
||||
url: this.config.url,
|
||||
isOnline: this.isOnline,
|
||||
lastConnectionTime: this.lastConnectionTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function to create node client with environment variables
|
||||
export function createNodeClient(): NodeClient {
|
||||
const nodeUrl = process.env.NODE_URL;
|
||||
const nodeToken = process.env.NODE_TOKEN;
|
||||
|
||||
if (!nodeUrl || !nodeToken) {
|
||||
throw new Error('NODE_URL and NODE_TOKEN environment variables are required');
|
||||
}
|
||||
|
||||
return new NodeClient({
|
||||
url: nodeUrl,
|
||||
token: nodeToken,
|
||||
timeout: parseInt(process.env.NODE_TIMEOUT || '5000'),
|
||||
});
|
||||
}
|
||||
376
app/src/server/routes.ts
Normal file
376
app/src/server/routes.ts
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import { TunnelDatabase } from './database.js';
|
||||
import { FrpcManager } from './frpc-manager.js';
|
||||
import { createNodeClient } from './node-client.js';
|
||||
import { TunnelConfigSchema, TunnelConfigUpdateSchema, TunnelConfig, TunnelStatus } from './types.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Initialize services
|
||||
const db = new TunnelDatabase();
|
||||
const frpcManager = new FrpcManager();
|
||||
|
||||
// Initialize node client if configured
|
||||
let nodeClient: ReturnType<typeof createNodeClient> | null = null;
|
||||
try {
|
||||
nodeClient = createNodeClient();
|
||||
logger.info('Node client initialized successfully');
|
||||
} catch (error) {
|
||||
logger.warn('Node client not configured:', error instanceof Error ? error.message : 'Unknown error');
|
||||
}
|
||||
|
||||
// Async handler wrapper
|
||||
const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
// Get all tunnels
|
||||
router.get('/tunnels', asyncHandler(async (req: Request, res: Response) => {
|
||||
const tunnels = db.getAllTunnels();
|
||||
res.json(tunnels);
|
||||
}));
|
||||
|
||||
// Get tunnel by ID
|
||||
router.get('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const tunnel = db.getTunnelById(req.params.id);
|
||||
if (!tunnel) {
|
||||
return res.status(404).json({ error: 'Tunnel not found' });
|
||||
}
|
||||
res.json(tunnel);
|
||||
}));
|
||||
|
||||
// Create new tunnel
|
||||
router.post('/tunnels', asyncHandler(async (req: Request, res: Response) => {
|
||||
const validation = TunnelConfigSchema.safeParse(req.body);
|
||||
if (!validation.success) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid tunnel configuration',
|
||||
details: validation.error.errors
|
||||
});
|
||||
}
|
||||
|
||||
const tunnel = db.createTunnel(validation.data);
|
||||
|
||||
// Regenerate frpc config if tunnel is enabled
|
||||
if (tunnel.enabled) {
|
||||
await regenerateFrpcConfig();
|
||||
}
|
||||
|
||||
res.status(201).json(tunnel);
|
||||
}));
|
||||
|
||||
// Update tunnel
|
||||
router.put('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const validation = TunnelConfigUpdateSchema.safeParse({
|
||||
...req.body,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
if (!validation.success) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid tunnel configuration',
|
||||
details: validation.error.errors
|
||||
});
|
||||
}
|
||||
|
||||
const tunnel = db.updateTunnel(req.params.id, validation.data);
|
||||
if (!tunnel) {
|
||||
return res.status(404).json({ error: 'Tunnel not found' });
|
||||
}
|
||||
|
||||
// Regenerate frpc config
|
||||
await regenerateFrpcConfig();
|
||||
|
||||
res.json(tunnel);
|
||||
}));
|
||||
|
||||
// Delete tunnel
|
||||
router.delete('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const deleted = db.deleteTunnel(req.params.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: 'Tunnel not found' });
|
||||
}
|
||||
|
||||
// Regenerate frpc config
|
||||
await regenerateFrpcConfig();
|
||||
|
||||
res.status(204).send();
|
||||
}));
|
||||
|
||||
// Get tunnel status
|
||||
router.get('/tunnels/:id/status', asyncHandler(async (req: Request, res: Response) => {
|
||||
const tunnel = db.getTunnelById(req.params.id);
|
||||
if (!tunnel) {
|
||||
return res.status(404).json({ error: 'Tunnel not found' });
|
||||
}
|
||||
|
||||
const active = await frpcManager.checkTunnelStatus(tunnel);
|
||||
const status: TunnelStatus = {
|
||||
id: tunnel.id!,
|
||||
name: tunnel.name,
|
||||
active,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(status);
|
||||
}));
|
||||
|
||||
// Get all tunnel statuses
|
||||
router.get('/tunnels-status', asyncHandler(async (req: Request, res: Response) => {
|
||||
const tunnels = db.getAllTunnels();
|
||||
const statuses: TunnelStatus[] = [];
|
||||
|
||||
for (const tunnel of tunnels) {
|
||||
try {
|
||||
const active = await frpcManager.checkTunnelStatus(tunnel);
|
||||
statuses.push({
|
||||
id: tunnel.id!,
|
||||
name: tunnel.name,
|
||||
active,
|
||||
lastChecked: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
statuses.push({
|
||||
id: tunnel.id!,
|
||||
name: tunnel.name,
|
||||
active: false,
|
||||
lastChecked: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json(statuses);
|
||||
}));
|
||||
|
||||
// Control frpc service
|
||||
router.post('/frpc/:action', asyncHandler(async (req: Request, res: Response) => {
|
||||
const { action } = req.params;
|
||||
|
||||
switch (action) {
|
||||
case 'start':
|
||||
await frpcManager.start();
|
||||
break;
|
||||
case 'stop':
|
||||
await frpcManager.stop();
|
||||
break;
|
||||
case 'restart':
|
||||
await frpcManager.restart();
|
||||
break;
|
||||
case 'regenerate':
|
||||
await regenerateFrpcConfig();
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: 'Invalid action' });
|
||||
}
|
||||
|
||||
res.json({ message: `frpc ${action} completed successfully` });
|
||||
}));
|
||||
|
||||
// Get frpc status
|
||||
router.get('/frpc/status', asyncHandler(async (req: Request, res: Response) => {
|
||||
const running = await frpcManager.isRunning();
|
||||
res.json({ running });
|
||||
}));
|
||||
|
||||
// Get frpc logs
|
||||
router.get('/frpc/logs', asyncHandler(async (req: Request, res: Response) => {
|
||||
const lines = parseInt(req.query.lines as string) || 50;
|
||||
const logs = await frpcManager.getLogs(lines);
|
||||
res.json({ logs });
|
||||
}));
|
||||
|
||||
// Helper function to regenerate frpc config and restart if needed
|
||||
async function regenerateFrpcConfig() {
|
||||
const enabledTunnels = db.getEnabledTunnels();
|
||||
|
||||
// Get server configuration from environment variables
|
||||
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
|
||||
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
|
||||
const token = process.env.FRPC_TOKEN;
|
||||
|
||||
await frpcManager.generateConfig(enabledTunnels, serverAddr, serverPort, token);
|
||||
|
||||
// Restart frpc if it's running
|
||||
if (await frpcManager.isRunning()) {
|
||||
await frpcManager.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// Node management endpoints
|
||||
router.get('/node/status', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!nodeClient) {
|
||||
return res.status(503).json({ error: 'Node client not configured' });
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await nodeClient.getStatus();
|
||||
const connectionInfo = nodeClient.getConnectionInfo();
|
||||
|
||||
res.json({
|
||||
...status,
|
||||
connection: connectionInfo
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get node status:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to connect to node',
|
||||
connection: nodeClient.getConnectionInfo()
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
router.get('/node/connection', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!nodeClient) {
|
||||
return res.status(503).json({ error: 'Node client not configured' });
|
||||
}
|
||||
|
||||
const connectionInfo = nodeClient.getConnectionInfo();
|
||||
const isReachable = await nodeClient.isReachable();
|
||||
|
||||
res.json({
|
||||
...connectionInfo,
|
||||
isReachable
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('/node/push-config', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!nodeClient) {
|
||||
return res.status(503).json({ error: 'Node client not configured' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Regenerate frpc config
|
||||
await regenerateFrpcConfig();
|
||||
|
||||
// Read the generated config
|
||||
const enabledTunnels = db.getEnabledTunnels();
|
||||
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
|
||||
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
|
||||
const token = process.env.FRPC_TOKEN;
|
||||
|
||||
// Generate config content
|
||||
let tomlContent = `[common]
|
||||
server_addr = "${serverAddr}"
|
||||
server_port = ${serverPort}
|
||||
`;
|
||||
|
||||
if (token) {
|
||||
tomlContent += `token = "${token}"\n`;
|
||||
}
|
||||
|
||||
tomlContent += '\n';
|
||||
|
||||
// Add proxy configurations
|
||||
for (const tunnel of enabledTunnels) {
|
||||
tomlContent += `[${tunnel.name}]
|
||||
type = "${tunnel.protocol.toLowerCase()}"
|
||||
local_ip = "${tunnel.localIp}"
|
||||
local_port = ${tunnel.localPort}
|
||||
remote_port = ${tunnel.remotePort}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
// Send config to node
|
||||
const result = await nodeClient.updateConfig(tomlContent);
|
||||
|
||||
logger.info('Configuration pushed to node successfully');
|
||||
res.json({
|
||||
message: 'Configuration pushed to node successfully',
|
||||
tunnelCount: enabledTunnels.length,
|
||||
nodeResponse: result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to push config to node:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to push configuration to node',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
router.post('/node/restart-frpc', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!nodeClient) {
|
||||
return res.status(503).json({ error: 'Node client not configured' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await nodeClient.restartFrpc();
|
||||
|
||||
logger.info('frpc restarted on node successfully');
|
||||
res.json({
|
||||
message: 'frpc restarted on node successfully',
|
||||
nodeResponse: result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to restart frpc on node:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to restart frpc on node',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
router.post('/node/push-and-restart', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!nodeClient) {
|
||||
return res.status(503).json({ error: 'Node client not configured' });
|
||||
}
|
||||
|
||||
try {
|
||||
// First push the config
|
||||
await regenerateFrpcConfig();
|
||||
|
||||
// Read the generated config
|
||||
const enabledTunnels = db.getEnabledTunnels();
|
||||
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
|
||||
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
|
||||
const token = process.env.FRPC_TOKEN;
|
||||
|
||||
// Generate config content
|
||||
let tomlContent = `[common]
|
||||
server_addr = "${serverAddr}"
|
||||
server_port = ${serverPort}
|
||||
`;
|
||||
|
||||
if (token) {
|
||||
tomlContent += `token = "${token}"\n`;
|
||||
}
|
||||
|
||||
tomlContent += '\n';
|
||||
|
||||
// Add proxy configurations
|
||||
for (const tunnel of enabledTunnels) {
|
||||
tomlContent += `[${tunnel.name}]
|
||||
type = "${tunnel.protocol.toLowerCase()}"
|
||||
local_ip = "${tunnel.localIp}"
|
||||
local_port = ${tunnel.localPort}
|
||||
remote_port = ${tunnel.remotePort}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
// Send config to node and restart in one call
|
||||
const result = await nodeClient.updateConfig(tomlContent);
|
||||
const restartResult = await nodeClient.restartFrpc();
|
||||
|
||||
logger.info('Configuration pushed and frpc restarted on node successfully');
|
||||
res.json({
|
||||
message: 'Configuration pushed and frpc restarted on node successfully',
|
||||
tunnelCount: enabledTunnels.length,
|
||||
configResponse: result,
|
||||
restartResponse: restartResult
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to push config and restart frpc on node:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to push configuration and restart frpc on node',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
export default router;
|
||||
43
app/src/server/types.ts
Normal file
43
app/src/server/types.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// Zod schemas for validation
|
||||
export const TunnelConfigSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
protocol: z.enum(['TCP', 'UDP']),
|
||||
localIp: z.string().min(1, 'Local IP is required'),
|
||||
localPort: z.number().int().min(1).max(65535),
|
||||
remotePort: z.number().int().min(1).max(65535),
|
||||
enabled: z.boolean().default(true),
|
||||
createdAt: z.string().optional(),
|
||||
updatedAt: z.string().optional(),
|
||||
});
|
||||
|
||||
export const TunnelConfigUpdateSchema = TunnelConfigSchema.partial().extend({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export type TunnelConfig = z.infer<typeof TunnelConfigSchema>;
|
||||
export type TunnelConfigUpdate = z.infer<typeof TunnelConfigUpdateSchema>;
|
||||
|
||||
export interface TunnelStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
lastChecked: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface FrpcConfig {
|
||||
serverAddr: string;
|
||||
serverPort: number;
|
||||
token?: string;
|
||||
proxies: {
|
||||
[key: string]: {
|
||||
type: 'tcp' | 'udp';
|
||||
localIP: string;
|
||||
localPort: number;
|
||||
remotePort: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
19
app/tsconfig.json
Normal file
19
app/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
app/vite.config.ts
Normal file
7
app/vite.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue