Compare commits

...

22 Commits

Author SHA1 Message Date
sagitchu 37341af2d1 fix(kcp): prevent zero-value override of window/buffer params causing 10x perf drop
The metadata parser unconditionally assigned GetInt results (which
return 0 for missing keys) to all KCP config fields, overwriting
defaults like SndWnd=1024, RcvWnd=1024, StreamBuf=2097152 with 0.

KCP's WndSize only applies positive values, so snd_wnd=0 caused
the write path condition waitsnd < s.kcp.snd_wnd to never succeed,
effectively stalling throughput at ~10Mbps instead of 400Mbps+.

- Only apply metadata values when the key actually exists (IsExists check)
- Use Clone() instead of sharing the global DefaultConfig pointer
- Add Config.Clone() deep-copy method
2026-04-21 10:20:21 +08:00
sagitchu bb48ab00bd fix(udp): mark connection active on WriteQueue to prevent idle timeout race 2026-04-21 09:55:18 +08:00
sagitchu f6d2ab3270 fix(frontend): fix blank page after logout and login page branding 2026-04-21 09:33:04 +08:00
sagitchu b382caa347 refactor(frontend): remove deprecated /settings route, redirect to /config 2026-04-21 09:26:44 +08:00
sagitchu 288c5d7152 fix: restore SSRF/security protections after glass UI cherry-pick 2026-04-21 09:20:04 +08:00
sagitchu b42516cea1 fix(ui): refine glassmorphism and page transition animations
- Added custom light/dark background images to replace unsplash URLs.
- Enhanced active sidebar item shadow and backdrop blur for better visibility.
- Fixed double loading flickers on user, node, group, and panel-sharing pages.
- Removed opacity transitions from AnimatedPage to prevent backdrop-filter rendering issues during page navigation.
2026-04-21 09:19:02 +08:00
sagitchu 74ed74ba00 style: apply frosted glass backgrounds to inner cards and nested elements
- Replaced solid backgrounds (bg-white, bg-default-50) with high-blur translucent glass (backdrop-blur-3xl) inside tunnel topology nodes, dashboard forward rules, and flow charts.
- Matched the aesthetic of the inner components to inherit the adaptive light/dark mode root backgrounds.
2026-04-21 09:19:02 +08:00
sagitchu 7c1f5ca660 style: improve glassmorphism UI for background adaptability
- Increase backdrop blur and lower white/black background opacity on inner elements (tables, charts, tabs)
- Apply higher blur to admin layout sidebar and H5 layout navigation bar
- Remove solid backgrounds inside nested cards to inherit background themes
2026-04-21 09:19:02 +08:00
sagitchu 26013f8dfa fix(ui): improve background handling and PWA update prompts
- Add adaptive solid color background support for dark/light themes.
- Replace mesh gradient with semi-transparent blurred background images.
- Fix background scrolling issue on mobile browsers.
- Remove borders from specific cards for consistent UI styling.
- Prevent caching of PWA service worker files in Nginx.
- Introduce interactive toast prompt for PWA updates instead of silent refresh.
2026-04-21 09:19:02 +08:00
sagitchu ab0f36ba7b style: optimize card and table backgrounds for better custom background visibility 2026-04-21 09:19:02 +08:00
sagitchu cde379a1f4 fix: remove unused imports and leftover jsx in settings 2026-04-21 09:19:02 +08:00
sagitchu 96fc790ed7 feat: add global background image setting to config page 2026-04-21 09:19:02 +08:00
sagitchu f0351107dd fix: frontend browser caching issue
Disable caching for index.html and reduce static assets cache to 30d.
2026-04-21 09:19:02 +08:00
sagit 534d36f0a1 feat(ui): add persistent custom background image upload feature (#415) 2026-04-21 09:19:02 +08:00
sagit 7668e24f4b feat(ui): redesign with transparent liquid glass (#414)
* docs: add design spec for iOS 26 Liquid Glass UI redesign

* docs: add implementation plan for iOS 26 Liquid Glass UI redesign

* feat(ui): add liquid glass CSS variables and update root layouts

* feat(ui): update Card and Modal components to iOS 26 Liquid Glass style

* feat(ui): redesign dashboard page to Liquid Glass style

* feat(ui): redesign node management page cards and charts

* feat(ui): redesign monitor page to Liquid Glass style

* feat(ui): redesign group and panel-sharing cards to Liquid Glass

* feat(ui): redesign settings and user components to Liquid Glass

* feat(ui): redesign profile and auth pages to Liquid Glass

* fix(ui): revert to LineChart to fix compilation issues

* refactor(ui): extract dashboard user header and cleanup admin layout

* feat(ui): refine monitor page hero metrics and fix build

* fix(ui): real data for monitor hero metrics and card padding adjustments

* fix(ui): apply global card padding and fix index layouts

* feat(ui): implement liquid-glass-react for all Card components

* fix(ui): resolve liquid-glass-react rendering issues causing invisible text and broken layout

* fix(ui): native CSS liquid-glass effect replacing NPM package

* fix(ui): cleanup duplicate closing tags and complete liquid glass tables

* fix(ui): apply perfect rounded corners to list views and table headers in non-compact mode

* fix(ui): apply liquid glass styling to grouped rules view in forward page

* fix(ui): re-implement liquid-glass natively to prevent text distortion and remove package dependency

* fix(ui): remove mouse distortion and preserve proper liquid glass texture

* fix(ui): remove svg filter causing text distortion and replace with pure css liquid glass

* fix(ui): apply properly scoped border radius to table headers

* fix(ui): remove hardcoded bg-default-50/60 from TableHeader

* fix(ui): apply global liquid glass styling and transparent overlays to all modals and dialogs

* fix(ui): remove absolute/relative class conflicts hiding Dialog modales and implement pure transparent layout

* feat(ui): apply global liquid glass styling to cards, buttons, and diagnosis modals
2026-04-21 09:19:02 +08:00
sagitchu c1f96180f5 docs: simplify AGENTS.md files, remove stale info and redundancy 2026-04-21 00:17:31 +08:00
sagitchu 630e012ec1 fix(tunnel): resolve UDP stream interruption and add KCP protocol support
- Increase UDP listener default TTL from 5s to 30s to prevent idle disconnect
- Add mux keepalive config (15s interval, 45s timeout) to tunnel relay handler
- Add KCP as tunnel chain transport protocol with keepalive and UDP mode default
- Add KCP protocol option to tunnel UI (frontend)
- Remove generic 'tcp' fallback key from KCP metadata to prevent false TCP mode
- Simplify forward service config by removing unused tunnelTLSProtocol parameter
2026-04-20 23:57:43 +08:00
sagitchu 7f14bd30fa fix(ci): update Dockerfile to Go 1.25 2026-04-20 20:55:56 +08:00
sagitchu 8611748c46 fix(security): patch SSRF and info disclosure vulnerabilities 2026-04-20 11:31:17 +08:00
sagitchu 77b7f066f3 fix(security): update vulnerable dependencies in go-backend 2026-04-20 10:46:27 +08:00
sagitchu 1970a74f6a fix(security): update vulnerable dependencies in go-gost/x 2026-04-20 10:46:27 +08:00
sagitchu b66c4966ba fix(security): update vulnerable dependencies in go-gost 2026-04-20 10:46:27 +08:00
71 changed files with 2384 additions and 1264 deletions
+49 -110
View File
@@ -1,123 +1,62 @@
# PROJECT KNOWLEDGE BASE
# AGENTS
**Generated:** Tue Mar 24 2026
**Commit:** 8ebde9d
**Branch:** main
**Tag:** 2.1.9-rc10
FLVX — traffic forwarding panel: Go admin API + Vite/React UI + Go agent.
## OVERVIEW
FLVX (formerly Flux Panel) is a traffic forwarding management system built on a forked GOST v3 stack. It ships as a Go-based admin API (SQLite/PostgreSQL) + Vite/React UI + Go forwarding agent, with optional mobile WebView wrappers.
## Structure
## STRUCTURE
```
./
├── go-gost/ # Go forwarding agent (forked gost + local x/)
│ └── x/ # Local fork of github.com/go-gost/x (replace => ./x)
├── go-backend/ # Go Admin API (GORM + SQLite/PostgreSQL, net/http)
│ └── tests/contract/ # Integration/contract tests
├── vite-frontend/ # React/Vite dashboard (shadcn bridge + Tailwind v4)
│ └── src/shadcn-bridge/heroui/ # HeroUI-compatible facade
├── docker-compose-v4.yml # Panel deploy (IPv4-only bridge)
├── docker-compose-v6.yml # Panel deploy (IPv6-enabled bridge)
├── panel_install.sh # Panel installer/upgrader (downloads compose)
├── install.sh # Node installer/upgrader (downloads gost binary)
└── .github/workflows/ # CI: build/test + Docker push + release artifacts
```
| Dir | Role | Entry |
|-----|------|-------|
| `go-backend/` | Admin API (GORM + SQLite/PG, net/http) | `cmd/paneld/main.go` |
| `go-gost/` | Forwarding agent (forked GOST) | `main.go` |
| `go-gost/x/` | Protocol handlers/dialers/listeners (own module) | — |
| `vite-frontend/` | React dashboard (shadcn bridge + Tailwind v4) | `src/App.tsx` |
## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| **Deploy (Docker)** | `docker-compose-v4.yml` | Env: `JWT_SECRET`, `BACKEND_PORT`, `FRONTEND_PORT` |
| **Deploy (IPv6)** | `docker-compose-v6.yml` | Same as v4 + IPv6-enabled bridge |
| **Panel install** | `panel_install.sh` | Picks v4/v6, generates `JWT_SECRET`, downloads compose |
| **Node install** | `install.sh` | Installs `/etc/flux_agent/flux_agent` + writes `config.json`/`gost.json` + systemd `flux_agent.service` |
| **Admin API** | `go-backend/` | Go Admin API (SQLite/PostgreSQL) |
| **Web UI** | `vite-frontend/` | React/Vite dashboard (shadcn bridge + Tailwind v4) |
| **UI Compatibility** | `vite-frontend/src/shadcn-bridge/heroui/` | HeroUI-compatible API wrappers backed by shadcn/radix |
| **Theme Tokens** | `vite-frontend/src/styles/tailwind-theme.pcss` | Tailwind v4 `@theme inline` semantic color mapping |
| **Go Agent** | `go-gost/` | Forwarding agent (forked gost + local x/) |
| **Go Core** | `go-gost/x/` | Handlers/listeners/dialers + management API |
| **Repository Layer** | `go-backend/internal/store/repo/` | GORM data access (repository.go 83k LOC) |
| **Contract Tests** | `go-backend/tests/contract/` | Integration tests for auth, federation, tunnels |
| **CI Workflows** | `.github/workflows/` | ci-build.yml, docker-build.yml, deploy-docs.yml |
`go-gost/go.mod` uses `replace github.com/go-gost/x => ./x`.
## CODE MAP
| Symbol | Type | Location | Role |
|--------|------|----------|------|
| `flvx` | Project | `.` | Root directory |
| `main` | Func | `go-backend/cmd/paneld/main.go` | Backend Entry |
| `App` | Component | `vite-frontend/src/App.tsx` | Frontend Entry |
| `main` | Func | `go-gost/main.go` | Agent Entry |
| `Repository` | Struct | `go-backend/internal/store/repo/repository.go` | Data Access Layer |
| `Handler` | Struct | `go-backend/internal/http/handler/handler.go` | HTTP Handlers |
| `websocket_reporter` | Func | `go-gost/x/socket/websocket_reporter.go` | Panel Telemetry |
## Commands
## CONVENTIONS
- **Skills & MCP**: Always prefer using available skills (via `skill` tool) and MCP tools when applicable. Check for relevant skills before implementing from scratch.
- **Auth**: `Authorization` header carries the raw JWT token (no `Bearer` prefix) between `vite-frontend/` and `go-backend/`.
- **Module Fork**: `go-gost/` uses `replace github.com/go-gost/x => ./x` and `go-gost/x/` is also its own Go module.
- **Encryption**: Agent-to-panel communication uses AES encryption with node `secret` as PSK.
- **API Envelope**: All REST responses follow `{code, msg, data, ts}` structure (code 0 = success).
- **Frontend UI Layer**: Import UI primitives from `src/shadcn-bridge/heroui/*` (legacy-compatible facade), not direct `@heroui/*` packages.
- **Tailwind v4 Semantic Colors**: `src/styles/globals.css` must import `src/styles/tailwind-theme.pcss`; removing it breaks semantic classes like `bg-primary`, `text-foreground`, and `border-input`.
- **Go Versions**: `go-backend` uses Go 1.24, `go-gost` uses Go 1.23, `go-gost/x` uses Go 1.22.
## ANTI-PATTERNS (THIS PROJECT)
- **DO NOT EDIT** generated protobuf output: `go-gost/x/internal/util/grpc/proto/*.pb.go`, `go-gost/x/internal/util/grpc/proto/*_grpc.pb.go`.
- **DO NOT ADD** `Bearer` prefix to Authorization header - expects raw JWT token.
- **DO NOT MODIFY** `install.sh` or `panel_install.sh` locally - CI overwrites these on release.
- **DO NOT** let backend handlers call `repo.DB()` directly — add a Repository method instead.
- **DO NOT ADD** frontend tests - project has no test infrastructure (Vitest/Jest not configured).
- **DO NOT REINTRODUCE** `@heroui/*` or `@nextui-org/*` dependencies; migration is now shadcn bridge-based.
- **DO NOT** use `type:jsonb` or `type:serial` in GORM tags (SQLite incompatible).
- **DO NOT** omit `TableName()` on new models — GORM pluralizes by default.
## COMMANDS
```bash
# Panel (Docker)
docker compose -f docker-compose-v4.yml up -d
docker compose -f docker-compose-v6.yml up -d
# Release-based install scripts
./panel_install.sh
./install.sh
# Local dev (per subproject)
# Backend
(cd go-backend && go run ./cmd/paneld) # SERVER_ADDR defaults to :6365
(cd go-backend && make build)
(cd vite-frontend && npm run dev)
(cd go-gost && go run .)
# Testing
(cd go-backend && go test ./...)
(cd go-backend && go test ./tests/contract/...)
# Frontend
(cd vite-frontend && npm install --legacy-peer-deps)
(cd vite-frontend && npm run dev) # host 0.0.0.0:3000
(cd vite-frontend && npm run build) # tsc && vite build
(cd vite-frontend && npm run lint) # eslint --fix (no typecheck command)
# Agent
(cd go-gost && go run .)
```
## UNIQUE STYLES
- **Flat Monorepo**: Language-prefixed dirs (`go-backend`, `go-gost`, `vite-frontend`) instead of `apps/`/`libs/`.
- **Asymmetric Go Layout**: `go-backend` follows `cmd/<app>/main.go` while `go-gost` uses `root/main.go`.
- **Frontend Hybrid Mode**: `App.tsx` detects "H5 mode" (mobile WebView) vs desktop, dictating layout strategy.
- **Experimental Bundler**: `vite-frontend` uses `rolldown-vite` (Rust-based) instead of standard Vite.
- **Non-minified Builds**: `vite.config.ts` sets `minify: false`, `treeshake: false` for debugging.
## Conventions
## NOTES
- LSP servers are not installed in this environment (gopls/typescript-language-server); rely on grep-based navigation.
- `vite-frontend/vite.config.ts` sets `minify: false` and disables treeshake; expect larger bundles.
- `vite-frontend` uses `rolldown-vite` (experimental Rust bundler) instead of standard Vite.
- Install scripts (`install.sh`, `panel_install.sh`) self-delete after execution - common pattern in one-liner installs.
- CI uses UPX compression (`--best --lzma`) on Go binaries before release.
- CI dynamically injects `PINNED_VERSION` into install scripts and docker-compose files during releases.
- `panel_install.sh` auto-detects IPv6 and modifies `/etc/docker/daemon.json` to enable IPv6 bridge.
- Download proxy `https://gcode.hostcentral.cc/` used for GitHub downloads in China/restricted environments.
- Backend has contract tests in `go-backend/tests/contract/` - frontend has no test infrastructure.
- `analysis/3x-ui/` contains a separate git repo for reference/comparison - not part of FLVX core.
- CI workflows: `ci-build.yml` (build check), `docker-build.yml` (multi-arch images + release), `deploy-docs.yml` (MkDocs).
- PostgreSQL migration supported via `panel_install.sh` menu option using pgloader.
- Repository layer is large: `repository.go` (83k LOC), `repository_mutations.go` (43k LOC).
- Button visual parity relies on `vite-frontend/src/shadcn-bridge/heroui/button.tsx` color mapping + `vite-frontend/src/styles/tailwind-theme.pcss` token export.
- **Auth**: raw JWT in `Authorization` header — **no `Bearer` prefix** (both frontend and backend).
- **API envelope**: all responses `{code, msg, data, ts}` (code 0 = success).
- **Frontend UI**: import from `src/shadcn-bridge/heroui/*`, never `@heroui/*` or `@nextui-org/*`.
- **Tailwind theme**: `globals.css` must import `tailwind-theme.pcss` or semantic classes break.
- **Backend DB**: handlers use Repository methods, never `repo.DB()` directly.
- **GORM models**: always define `TableName()` (GORM pluralizes by default).
- **GORM tags**: no `type:jsonb` or `type:serial` (SQLite incompatible).
- **Go versions**: `go.mod` says 1.25.0 for all three modules; CI builds with 1.23.
## PLAN DOCUMENT RULE
- Every new implementation plan must have a dedicated Markdown plan document.
- Store plan documents under `plans/`.
- Use an incrementing numeric prefix and a short plan-summary name: `NNN-<plan-summary>.md` (for example, `001-auth-refactor.md`, `002-federation-api-cleanup.md`).
- The numeric prefix must increase by 1 for each new plan.
- In each plan document, keep a task checklist and mark each task as completed immediately after finishing it.
## Anti-patterns
- Don't edit `install.sh` or `panel_install.sh` locally (CI overwrites on release).
- Don't edit `go-gost/x/internal/util/grpc/proto/*.pb.go` (generated).
- Don't add frontend tests (no Vitest/Jest configured).
- Don't reintroduce `@heroui/*` or `@nextui-org/*` packages.
## Testing
- Backend: `(cd go-backend && go test ./...)` — includes contract tests in `tests/contract/`.
- Frontend: no test infrastructure.
- CI runs one PostgreSQL contract test: env var `FLVX_POSTGRES_TEST_DSN`.
## Build quirks
- `vite-frontend` uses `rolldown-vite` (Rust bundler), not standard Vite.
- `vite.config.ts`: `minify: false`, `treeshake: false` (debugging mode).
- CI builds `go-gost` with `CGO_ENABLED=0` then compresses with UPX `--best --lzma`.
@@ -0,0 +1,140 @@
# Flvx iOS 26 Liquid Glass UI Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Transform the Flvx frontend interface entirely into an "Apple iOS 26 Liquid Glass" visual style by utilizing high-radius squircles, heavy background blurs, mesh gradients, and highly semantic translucent containers.
**Architecture:** We will approach this from the ground up: first defining the global TailwindCSS design tokens and the base mesh-gradient layout, then systematically replacing the structural styling inside each React page component (`vite-frontend/src/pages/*.tsx`).
**Tech Stack:** React DOM, TailwindCSS (v4), shadcn-bridge (HeroUI), Vite
---
### Task 1: Setup Global CSS Variables and App Shell
**Files:**
- Modify: `vite-frontend/src/styles/globals.css` (or `index.css`)
- Modify: `vite-frontend/tailwind.config.js`
- Modify: `vite-frontend/src/App.tsx` (or `main.tsx` / `layouts` depending on structural entry point)
- [ ] **Step 1: Inject Liquid Glass theme variables**
Open the main CSS file and add variables for the new blur radius, box-shadows, and background gradients.
```css
:root {
--glass-bg: rgba(255, 255, 255, 0.6);
--glass-border: rgba(255, 255, 255, 0.8);
--glass-card: rgba(255, 255, 255, 0.7);
--glass-overlay: rgba(0, 0, 0, 0.3);
}
.dark {
--glass-bg: rgba(30, 30, 30, 0.6);
--glass-border: rgba(255, 255, 255, 0.15);
--glass-card: rgba(40, 40, 40, 0.6);
}
.bg-mesh-gradient {
background: radial-gradient(at 0% 0%, #ff9a9e 0%, transparent 50%),
radial-gradient(at 100% 0%, #fecfef 0%, transparent 50%),
radial-gradient(at 100% 100%, #c2e9fb 0%, transparent 50%),
radial-gradient(at 0% 100%, #a1c4fd 0%, transparent 50%);
background-color: #f2f2f7;
}
```
- [ ] **Step 2: Update App Layout**
Modify the root app container to use `.bg-mesh-gradient` and ensure the main container occupies `min-h-screen`.
### Task 2: Refactor Global Components (Card & Modal)
**Files:**
- Modify: `vite-frontend/src/shadcn-bridge/heroui/card.tsx`
- Modify: `vite-frontend/src/shadcn-bridge/heroui/modal.tsx`
- [ ] **Step 1: Liquid Card Base**
Update the default className string for `Card` to incorporate: `backdrop-blur-3xl bg-white/60 dark:bg-zinc-900/60 border border-white/80 dark:border-white/10 rounded-2xl shadow-[0_10px_30px_rgba(0,0,0,0.1)]`.
- [ ] **Step 2: Modal Overlay Base**
Update the default overlay className for `Modal` to use `bg-black/30 backdrop-blur-sm`, and its content panel to use the same `glass_card` classes as the Card component but with `rounded-3xl`.
### Task 3: Redesign Dashboard Page
**Files:**
- Modify: `vite-frontend/src/pages/dashboard.tsx`
- [ ] **Step 1: Replace hardcoded borders/bg with glass semantics**
Find hardcoded `bg-white`, `border-gray-200`, `shadow-md` inside `DashboardPage` and `MetricCard`, replace with `bg-white/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] border-white/80 rounded-2xl`.
- [ ] **Step 2: Adjust spacing**
Ensure all metric cards have uniform `h-48` equivalent height and are strictly `rounded-2xl` with `p-6` padding.
- [ ] **Step 3: Update Flow Chart Card**
Replace standard grid backgrounds in the flow chart with transparent spacing and vibrant `bg-blue-500` squircle bars without harsh borders.
### Task 4: Redesign Node Management Page
**Files:**
- Modify: `vite-frontend/src/pages/node.tsx`
- [ ] **Step 1: Replace standard List/Table view with Grid Cards**
Update the node rendering map to output `glass_card` containers (`rounded-2xl`, blur, padding `p-6`).
- [ ] **Step 2: Apply semantic status highlights**
Refactor the Online/Offline badges into pill-shapes (`rounded-full`) using the defined semantic colors (e.g. `bg-green-500/20 text-green-600` with a 6px inner dot `bg-green-500`).
- [ ] **Step 3: Embed Micro-charts**
For CPU/RAM data inside the node card, switch standard progress bars to ultra-thin (height 4px) continuous lines utilizing standard brand colors.
### Task 5: Redesign Tunnels & Rules Configuration
**Files:**
- Modify: `vite-frontend/src/pages/tunnel.tsx`
- Modify: `vite-frontend/src/pages/forward.tsx`
- [ ] **Step 1: Update Tunnel lists into nested Glass Panels**
Encapsulate each tunnel configuration into a wide `glass_card`.
- [ ] **Step 2: Create Visual Rule Tags**
For the Forwarding rules, wrap the target IP/Port logic into visual badges: `bg-green-500/20` for Entry and `bg-blue-500/20` for Target.
- [ ] **Step 3: Refactor the "Add Rule" Floating action**
Ensure the plus button follows the squircle format (`rounded-full`) with a prominent diffused shadow (`shadow-[0_4px_12px_rgba(0,122,255,0.3)]`).
### Task 6: Redesign Monitor Page
**Files:**
- Modify: `vite-frontend/src/pages/monitor.tsx`
- [ ] **Step 1: Style the Top Hero Metrics**
Replace flat stat boxes with high-contrast, large typography inside `glass_card` backgrounds.
- [ ] **Step 2: Refactor Latency Indicators**
Format the connection list rows as `bg-white/50 dark:bg-black/30` strips with pill-shaped status tags (`Healthy`, `Warning`, `Offline`) mapping exactly to the green/orange/red semantics from the design spec.
### Task 7: Redesign Group & Sharing Pages
**Files:**
- Modify: `vite-frontend/src/pages/group.tsx`
- Modify: `vite-frontend/src/pages/panel-sharing.tsx`
- [ ] **Step 1: Update Tab Switchers**
Refactor the internal navigation tabs (e.g., "Tunnel Groups" vs "User Groups") into an encapsulated `p-1 rounded-xl bg-white/40 backdrop-blur-lg` container with animated active states (`shadow-sm bg-white`).
- [ ] **Step 2: Style Share Cards**
Transform flat panel sharing list items into rich `glass_card` entities. Highlight expiration dates with the accent text color.
### Task 8: Redesign Settings, Config, and User Management
**Files:**
- Modify: `vite-frontend/src/pages/config.tsx`
- Modify: `vite-frontend/src/pages/settings.tsx`
- Modify: `vite-frontend/src/pages/user.tsx`
- Modify: `vite-frontend/src/pages/limit.tsx`
- [ ] **Step 1: Flatten Forms**
Convert traditional input groups into `rounded-xl bg-white/50 border border-white/60` containers. Remove outer boxing for standard `label + input` pairs.
- [ ] **Step 2: iOS Toggle Switches**
Ensure that any `<Switch>` or `<Checkbox>` components use the new Accent brand color (`#007aff`) with full `rounded-full` geometry.
- [ ] **Step 3: Refactor User Badges**
In `user.tsx`, replace text-based role columns with circular Avatar badges (e.g., `w-10 h-10 rounded-full bg-blue-500 text-white` with the first two letters of the username).
### Task 9: Profile & Password Modal Restyling
**Files:**
- Modify: `vite-frontend/src/pages/profile.tsx`
- Modify: `vite-frontend/src/pages/change-password.tsx`
- [ ] **Step 1: Apply Profile Card Structure**
Create a split view on desktop using flex: Left side (Avatar + User Info + Admin Shortcuts), Right side (Password Form).
- [ ] **Step 2: Restyle Auth Inputs**
Ensure all password inputs use `bg-white/50 backdrop-blur-md border border-white/60` and the update button has heavy shadow-glow.
@@ -0,0 +1,60 @@
# Flvx iOS 26 Liquid Glass UI Redesign Spec
## 1. Overview
This document specifies the comprehensive UI/UX redesign of the Flvx frontend using an "Apple iOS 26 Liquid Glass" design language. The goal is to elevate the visual quality of the entire application, making it modern, spatially aware, and highly legible through extensive use of blur, translucency, squircle borders, and semantic contrast.
## 2. Scope
The redesign covers 100% of the frontend routing pages and overlay components under `vite-frontend/src/pages/` and global UI modules:
- Dashboard (`dashboard.tsx`)
- Node Management (`node.tsx`)
- Tunnel & Rule Configurations (`tunnel.tsx`, `forward.tsx`)
- System Monitor (`monitor.tsx`)
- User Management (`user.tsx`)
- Speed Limit Management (`limit.tsx`)
- Group Management (`group.tsx`)
- Panel Sharing (`panel-sharing.tsx`)
- Global Settings & Config (`config.tsx`, `settings.tsx`)
- Profile & Change Password (`profile.tsx`, `change-password.tsx`)
- All related Modals, Drawers, and floating UI (e.g., "Create Node", "Add Rule" forms).
## 3. Design System & Tokens
The new UI replaces traditional solid-color borders and flat surfaces with the following spatial design tokens:
### 3.1. Corner Radii (Squircles)
- **Outer Shell / Viewports**: 32px (`rounded-3xl` equivalent)
- **Cards / Containers**: 24px (`rounded-2xl`)
- **Buttons / Inputs**: 16px (`rounded-xl` or `rounded-full`)
- **Badges / Tags**: 6px or fully rounded.
### 3.2. Backgrounds & Blurs
- **Global Background**: A mesh gradient blending soft pinks and blues (`#ff9a9e`, `#fecfef`, `#a1c4fd`, `#c2e9fb`).
- **Glass Base (Primary Containers)**: `backdrop-blur-3xl` with an ultra-thin white overlay (`rgba(255, 255, 255, 0.6)` or `#ffffff99`).
- **Glass Inner Glow (Borders)**: 1px solid `rgba(255, 255, 255, 0.8)` (`#ffffffcc`).
- **Modals Background Overlay**: 30% black overlay (`#0000004d`).
### 3.3. Semantic Colors
- **Brand / Active / Primary / TCP**: Blue `#007aff`
- **Healthy / Success / Online**: Green `#34c759`
- **Warning / Wait / UDP**: Orange `#ff9500`
- **Danger / Offline / Delete**: Red `#ff3b30`
- **Secondary / Purple / Data**: Purple `#af52de`
- **Text**: Primary (`#1d1d1f`), Secondary (`#86868b`).
### 3.4. Elevation & Shadow
- Soft, highly diffused drop shadows rather than sharp lines: e.g., `box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1)`.
## 4. Implementation Strategy
We will implement the redesign systematically across the React + TailwindCSS + shadcn/HeroUI stack:
1. **CSS Variables / Tailwind Config**: Inject the new Liquid Glass design tokens (colors, extended radiuses, customized backdrop blurs, box shadows) into `tailwind.config.js` and `globals.css`.
2. **Global App Shell**: Update the root layout (`index.tsx` or main `App` layout) to host the dynamic mesh gradient background and the new translucent sidebar.
3. **Component Re-styling**:
- Override HeroUI default card, input, and modal styles using custom `classNames`.
- Update `MetricCard`, `PageEmptyState`, `PageLoadingState`, and other base components to support the `glass_card` spec.
4. **Page-by-Page Integration**: Rewrite the JSX of each page to utilize the new layout structure, ensuring all existing interactive state and API logic is seamlessly preserved.
5. **Modal System Update**: Apply the transparent `#0000004d` overlay and 480px width glassy card style to all global dialogs.
## 5. Success Criteria
- [ ] No regression in business logic; all forms, interactions, and data rendering operate exactly as before.
- [ ] The visual system consistently employs the `glass_bg`, `glass_card`, and corresponding squircle radiuses across 100% of the UI.
- [ ] Modals and Overlays correctly blur the background mesh gradient.
- [ ] All responsive layouts appropriately wrap the card components on smaller displays.
+28 -62
View File
@@ -1,72 +1,38 @@
# GO BACKEND KNOWLEDGE BASE
# go-backend
**Generated:** Fri Mar 20 2026
**Commit:** f45f960
**Branch:** main
**Tag:** 2.1.9-beta6
Admin API for FLVX. Go + net/http + GORM (SQLite/PostgreSQL).
## OVERVIEW
Go-based Admin API for FLVX. Replaced legacy Spring Boot backend.
**Stack:** Go 1.24, net/http (std lib), GORM + SQLite/PostgreSQL (glebarez/sqlite - CGO-free).
## Structure
## STRUCTURE
```
go-backend/
├── cmd/paneld/main.go # Entry point; starts HTTP server + WebSocket
├── internal/
│ ├── http/ # HTTP layer
│ │ ├── router.go # Routes (NewServeMux) + Middleware chain
│ │ ├── handler/ # API Handlers (User, Tunnel, Node, etc.)
│ │ ├── middleware/ # JWT, CORS, Logging, Recover
│ │ └── response/ # JSON response helpers
│ ├── store/
│ │ ├── model/model.go # GORM model structs (single source of truth)
│ │ └── repo/ # Data Access Layer (Repository pattern, GORM)
│ │ ├── repository.go # Core queries, Open/OpenPostgres, AutoMigrate (83k LOC)
│ │ ├── repository_mutations.go # Mutation helpers (user/node/tunnel/forward CRUD, 43k LOC)
│ │ ├── repository_federation.go # Federation-specific queries
│ │ ├── repository_flow.go # Flow/forward status queries
│ │ ├── repository_control.go # Control plane queries
│ │ └── repository_groups.go # Group management queries
│ └── auth/ # Auth logic
├── tests/contract/ # Integration/contract tests (14 tests)
├── Dockerfile # Multi-stage build (golang:1.24-bookworm → debian:bookworm-slim)
└── Makefile # Build commands
```
| Dir | Role |
|-----|------|
| `cmd/paneld/main.go` | Entry point, HTTP server + WebSocket |
| `internal/http/router.go` | Route registration (`http.ServeMux`) + middleware chain |
| `internal/http/handler/` | API handlers |
| `internal/http/middleware/` | JWT, CORS, logging, recover |
| `internal/http/response/` | JSON envelope helpers |
| `internal/store/model/model.go` | All GORM models (single file) |
| `internal/store/repo/` | Repository layer (never access DB directly) |
| `internal/auth/` | Auth logic |
| `tests/contract/` | Integration tests |
## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| **API Routes** | `go-backend/internal/http/router.go` | Registers handlers to `http.ServeMux` |
| **DB Models** | `go-backend/internal/store/model/model.go` | GORM structs with `TableName()` methods |
| **Repository** | `go-backend/internal/store/repo/` | GORM-based queries, all DB ops encapsulated |
| **Auth Middleware** | `go-backend/internal/http/middleware/jwt.go` | Extracts `Authorization` header |
| **WebSocket** | `go-backend/internal/ws/` | Real-time updates (traffic, status) |
| **Contract Tests** | `go-backend/tests/contract/` | Integration tests for auth, federation, tunnels |
## Conventions
## CONVENTIONS
- **GORM ORM**: Uses GORM with `glebarez/sqlite` (CGO-free) and `gorm.io/driver/postgres`.
- **AutoMigrate**: Schema created at startup via `autoMigrateAll()` — no hand-written DDL.
- **TableName()**: All models define explicit `TableName()` returning singular snake_case names.
- **Repository Pattern**: Handlers never access `*gorm.DB` directly — all queries go through `repo.Repository` methods.
- **Standard Lib**: Uses `net/http` for routing (Go 1.22+ patterns).
- **Auth**: Expects raw JWT in `Authorization` header (no `Bearer` prefix).
- **API Envelope**: All responses use `response.R{code, msg, data, ts}` structure.
- **Config**: Loaded from environment variables (see `cmd/paneld/main.go`).
- **SQLite Constraints**: `MaxOpenConns(1)`, WAL mode, busy_timeout=5000.
- **PostgreSQL**: Supported via `DB_TYPE=postgres` and `DATABASE_URL` env vars.
- **Auth**: raw JWT in `Authorization` header — no `Bearer` prefix.
- **API envelope**: `{code, msg, data, ts}`, code 0 = success.
- **Repository pattern**: handlers call repo methods, never `repo.DB()` directly.
- **GORM**: `TableName()` on every model (GORM pluralizes by default).
- **GORM tags**: no `type:jsonb` or `type:serial` (SQLite incompatible).
- **SQLite**: `MaxOpenConns(1)`, WAL mode, `busy_timeout=5000`.
- **Schema**: created via `autoMigrateAll()` at startup, no hand-written DDL.
- **PostgreSQL**: set `DB_TYPE=postgres` and `DATABASE_URL` env vars.
- **Config**: all from environment variables.
## ANTI-PATTERNS
- **DO NOT** let handlers call `repo.DB()` directly — add a Repository method instead.
- **DO NOT CHANGE** handler signatures without updating `router.go`.
- **DO NOT** use `type:jsonb` or `type:serial` in GORM tags (SQLite incompatible).
- **DO NOT** omit `TableName()` on new models — GORM pluralizes by default.
## Commands
## COMMANDS
```bash
cd go-backend
go run ./cmd/paneld # Default: SERVER_ADDR=:6365
go test ./... # Unit tests
go test ./tests/contract/... # Contract tests
go run ./cmd/paneld # SERVER_ADDR defaults to :6365
make build
go test ./... # includes contract tests
go test ./tests/contract/... # contract tests only
```
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.24-bookworm AS builder
FROM golang:1.25-bookworm AS builder
WORKDIR /src
COPY go.mod go.sum ./
+5 -7
View File
@@ -1,8 +1,6 @@
module go-backend
go 1.24.0
toolchain go1.24.4
go 1.25.0
require (
github.com/glebarez/sqlite v1.11.0
@@ -24,11 +22,11 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
modernc.org/libc v1.65.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
+12 -12
View File
@@ -38,21 +38,21 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
@@ -185,14 +185,6 @@ func (h *Handler) listForwardPorts(forwardID int64) ([]forwardPortRecord, error)
return h.repo.ListForwardPorts(forwardID)
}
func (h *Handler) isTunnelSelectedTLSProtocol(tunnelID int64) (bool, error) {
protocol, err := h.repo.GetTunnelOutProtocol(tunnelID)
if err != nil {
return false, err
}
return isTLSTunnelProtocol(protocol), nil
}
func (h *Handler) getNodeRecord(nodeID int64) (*nodeRecord, error) {
n, err := h.repo.GetNodeRecord(nodeID)
if err != nil {
@@ -272,10 +264,6 @@ func (h *Handler) syncForwardServicesWithWarnings(forward *forwardRecord, method
}
serviceBase := buildForwardServiceBaseWithResolvedUserTunnel(forward.ID, forward.UserID, userTunnelID)
tunnelTLSProtocol, err := h.isTunnelSelectedTLSProtocol(forward.TunnelID)
if err != nil {
return nil, err
}
for _, fp := range ports {
if limiterID != nil && speed != nil {
@@ -298,7 +286,7 @@ func (h *Handler) syncForwardServicesWithWarnings(forward *forwardRecord, method
if err != nil {
return nil, err
}
services := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, strings.TrimSpace(fp.InIP), limiterID, tunnelTLSProtocol)
services := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, strings.TrimSpace(fp.InIP), limiterID)
_, err = h.sendNodeCommand(node.ID, method, services, true, false)
if err != nil && allowFallbackAdd && method == "UpdateService" {
if isNotFoundError(err) {
@@ -313,7 +301,7 @@ func (h *Handler) syncForwardServicesWithWarnings(forward *forwardRecord, method
}
if err != nil && strings.EqualFold(strings.TrimSpace(method), "UpdateService") && isCannotAssignRequestedAddressError(err) {
var warning string
warning, err = h.fallbackForwardPortToDefaultBind(forward, tunnel, node, fp, serviceBase, limiterID, tunnelTLSProtocol)
warning, err = h.fallbackForwardPortToDefaultBind(forward, tunnel, node, fp, serviceBase, limiterID)
if err == nil && warning != "" {
warnings = append(warnings, warning)
}
@@ -339,7 +327,7 @@ func (h *Handler) syncForwardServicesWithWarnings(forward *forwardRecord, method
return warnings, nil
}
func (h *Handler) fallbackForwardPortToDefaultBind(forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, fp forwardPortRecord, serviceBase string, limiterID *int64, tunnelTLSProtocol bool) (string, error) {
func (h *Handler) fallbackForwardPortToDefaultBind(forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, fp forwardPortRecord, serviceBase string, limiterID *int64) (string, error) {
if h == nil || forward == nil || tunnel == nil || node == nil {
return "", errors.New("invalid bind fallback context")
}
@@ -356,7 +344,7 @@ func (h *Handler) fallbackForwardPortToDefaultBind(forward *forwardRecord, tunne
}
time.Sleep(150 * time.Millisecond)
defaultServices := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, "", limiterID, tunnelTLSProtocol)
defaultServices := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, "", limiterID)
if _, err := h.sendNodeCommand(node.ID, "AddService", defaultServices, true, false); err != nil {
return "", err
}
@@ -1561,7 +1549,7 @@ func compactErrorMessage(msg string) string {
return strings.Join(strings.Fields(strings.ToLower(msg)), "")
}
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, bindIP string, limiterID *int64, tunnelTLSProtocol bool) []map[string]interface{} {
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, bindIP string, limiterID *int64) []map[string]interface{} {
protocols := []string{"tcp", "udp"}
services := make([]map[string]interface{}, 0, 2)
targets := splitRemoteTargets(forward.RemoteAddr)
@@ -1605,9 +1593,9 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel
},
}
if protocol == "udp" {
listenerMetadata := map[string]interface{}{"keepAlive": true}
if tunnelTLSProtocol {
listenerMetadata["ttl"] = "10s"
listenerMetadata := map[string]interface{}{
"keepAlive": true,
"ttl": "30s",
}
service["listener"].(map[string]interface{})["metadata"] = listenerMetadata
}
@@ -378,7 +378,7 @@ func TestRetryTunnelServiceAddWithCleanupReturnsCleanupError(t *testing.T) {
func TestBuildForwardServiceConfigs_UsesBindIPForListen(t *testing.T) {
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 22000, "10.9.8.7", nil, false)
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 22000, "10.9.8.7", nil)
if len(services) != 2 {
t.Fatalf("expected 2 services, got %d", len(services))
}
@@ -393,7 +393,7 @@ func TestBuildForwardServiceConfigs_UsesBindIPForListen(t *testing.T) {
func TestBuildForwardServiceConfigs_DefaultListenAddrWhenBindIPEmpty(t *testing.T) {
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
node := &nodeRecord{TCPListenAddr: "0.0.0.0", UDPListenAddr: "[::]"}
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 22001, "", nil, false)
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 22001, "", nil)
if len(services) != 2 {
t.Fatalf("expected 2 services, got %d", len(services))
}
@@ -409,7 +409,7 @@ func TestBuildForwardServiceConfigs_DefaultListenAddrWhenBindIPEmpty(t *testing.
func TestBuildForwardServiceConfigs_BindIPAlreadyContainsPort(t *testing.T) {
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 55555, "3.3.3.3:12345", nil, false)
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 55555, "3.3.3.3:12345", nil)
if len(services) != 2 {
t.Fatalf("expected 2 services, got %d", len(services))
}
@@ -464,7 +464,7 @@ func TestBuildForwardServiceConfigs_IPv6BindIP(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, tt.port, tt.bindIP, nil, false)
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, tt.port, tt.bindIP, nil)
if len(services) != 2 {
t.Fatalf("expected 2 services, got %d", len(services))
}
+36 -12
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
@@ -143,17 +144,24 @@ type remoteUsageNodeItem struct {
func buildFederationServiceConfig(serviceName, addr, protocol, role, chainName string, targetCount int, interfaceName string) map[string]interface{} {
service := map[string]interface{}{
"name": serviceName,
"addr": addr,
"name": serviceName,
"addr": addr,
"handler": map[string]interface{}{
"type": "relay",
},
"listener": map[string]interface{}{
"type": protocol,
},
"listener": buildTunnelListenerConfig(protocol),
}
if isTLSTunnelProtocol(protocol) {
service["handler"].(map[string]interface{})["metadata"] = map[string]interface{}{"nodelay": true}
if isTCPTunnelProtocol(protocol) {
service["handler"].(map[string]interface{})["metadata"] = map[string]interface{}{
"nodelay": true,
"mux.keepaliveInterval": "15s",
"mux.keepaliveTimeout": "45s",
}
}
if isKCPTunnelProtocol(protocol) {
service["handler"].(map[string]interface{})["metadata"] = map[string]interface{}{
"connectTimeout": "30s",
}
}
if role == "middle" {
service["handler"].(map[string]interface{})["chain"] = chainName
@@ -635,6 +643,15 @@ func (h *Handler) nodeImport(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("Remote URL and Token are required"))
return
}
rURL, err := url.Parse(req.RemoteURL)
if err != nil || (rURL.Scheme != "http" && rURL.Scheme != "https") {
response.WriteJSON(w, response.ErrDefault("Invalid Remote URL format"))
return
}
if err := IsSafeRemoteAddr(rURL.Host); err != nil {
response.WriteJSON(w, response.Err(403, "禁止将远程节点地址设置为内部网络或保留地址"))
return
}
domainCfg, _ := h.repo.GetConfigByName("panel_domain")
localDomain := ""
@@ -1085,16 +1102,23 @@ func (h *Handler) federationRuntimeApplyRole(w http.ResponseWriter, r *http.Requ
connector := map[string]interface{}{
"type": "relay",
}
if isTLSTunnelProtocol(targetProtocol) {
connector["metadata"] = map[string]interface{}{"nodelay": true}
if isTCPTunnelProtocol(targetProtocol) {
connector["metadata"] = map[string]interface{}{
"nodelay": true,
"mux.keepaliveInterval": "15s",
"mux.keepaliveTimeout": "45s",
}
}
if isKCPTunnelProtocol(targetProtocol) {
connector["metadata"] = map[string]interface{}{
"connectTimeout": "30s",
}
}
nodeItems = append(nodeItems, map[string]interface{}{
"name": fmt.Sprintf("node_%d", i+1),
"addr": processServerAddress(fmt.Sprintf("%s:%d", host, target.Port)),
"connector": connector,
"dialer": map[string]interface{}{
"type": targetProtocol,
},
"dialer": buildTunnelDialerConfig(targetProtocol),
})
}
+14 -1
View File
@@ -27,6 +27,7 @@ import (
"github.com/google/uuid"
)
type Handler struct {
repo *repo.Repository
jwtSecret string
@@ -356,6 +357,12 @@ func (h *Handler) getConfigByName(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
return
}
configName := strings.ToLower(strings.TrimSpace(req.Name))
switch configName {
case "license_key", "cloudflare_secret_key", "jwt_secret":
response.WriteJSON(w, response.Err(403, "禁止访问敏感配置"))
return
}
cfg, err := h.repo.GetConfigByName(req.Name)
if err != nil {
@@ -381,6 +388,12 @@ func (h *Handler) getConfigs(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
ctxClaims := r.Context().Value(middleware.ClaimsContextKey)
if claims, ok := ctxClaims.(auth.Claims); !ok || claims.RoleID != 0 {
delete(cfgMap, "license_key")
delete(cfgMap, "cloudflare_secret_key")
delete(cfgMap, "jwt_secret")
}
response.WriteJSON(w, response.OK(cfgMap))
}
@@ -852,7 +865,7 @@ func (h *Handler) licenseActivate(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("设备绑定失败: "+err.Error()))
return
}
// Validation might still fail with scope if we don't query via machine id, but since activate machine succeeded
// we can consider the license valid for our simple usecase
} else {
+84 -14
View File
@@ -301,6 +301,10 @@ func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("节点名称和地址不能为空"))
return
}
if err := IsValidNodeAddress(serverIP); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
now := time.Now().UnixMilli()
inx := h.repo.NextIndex("node")
@@ -365,6 +369,13 @@ func (h *Handler) nodeUpdate(w http.ResponseWriter, r *http.Request) {
newHTTP := asInt(req["http"], currentHTTP)
newTLS := asInt(req["tls"], currentTLS)
newSocks := asInt(req["socks"], currentSocks)
serverIP := asString(req["serverIp"])
if serverIP != "" {
if err := IsValidNodeAddress(serverIP); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
}
if currentStatus == 1 && (newHTTP != currentHTTP || newTLS != currentTLS || newSocks != currentSocks) {
if err := h.applyNodeProtocolChange(id, newHTTP, newTLS, newSocks); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
@@ -375,7 +386,7 @@ func (h *Handler) nodeUpdate(w http.ResponseWriter, r *http.Request) {
now := time.Now().UnixMilli()
if err := h.repo.UpdateNode(id,
asString(req["name"]),
asString(req["serverIp"]),
serverIP,
nullableText(asString(req["serverIpV4"])),
nullableText(asString(req["serverIpV6"])),
defaultString(asString(req["port"]), "1000-65535"),
@@ -1686,6 +1697,10 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
return
}
if roleID != 0 {
if err := IsSafeRemoteAddr(remoteAddr); err != nil {
response.WriteJSON(w, response.Err(403, "禁止将目标地址设置为内部网络或保留地址"))
return
}
if speedIDVal, ok := req["speedId"]; ok && speedIDVal != nil {
response.WriteJSON(w, response.Err(-1, "普通用户无法设置限速规则"))
return
@@ -1805,6 +1820,12 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
if remoteAddr == "" {
remoteAddr = forward.RemoteAddr
}
if actorRole != 0 {
if err := IsSafeRemoteAddr(remoteAddr); err != nil {
response.WriteJSON(w, response.Err(403, "禁止将目标地址设置为内部网络或保留地址"))
return
}
}
strategy := strings.TrimSpace(asString(req["strategy"]))
if strategy == "" {
strategy = forward.Strategy
@@ -3431,16 +3452,23 @@ func buildTunnelChainConfig(tunnelID int64, fromNodeID int64, targets []tunnelRu
connector := map[string]interface{}{
"type": "relay",
}
if isTLSTunnelProtocol(protocol) {
connector["metadata"] = map[string]interface{}{"nodelay": true}
connectorMetadata := map[string]interface{}{}
if isTCPTunnelProtocol(protocol) {
connectorMetadata["nodelay"] = true
connectorMetadata["mux.keepaliveInterval"] = "15s"
connectorMetadata["mux.keepaliveTimeout"] = "45s"
}
if isKCPTunnelProtocol(protocol) {
connectorMetadata["connectTimeout"] = "30s"
}
if len(connectorMetadata) > 0 {
connector["metadata"] = connectorMetadata
}
nodeItems = append(nodeItems, map[string]interface{}{
"name": fmt.Sprintf("node_%d", idx+1),
"addr": processServerAddress(fmt.Sprintf("%s:%d", host, port)),
"connector": connector,
"dialer": map[string]interface{}{
"type": protocol,
},
"dialer": buildTunnelDialerConfig(protocol),
})
}
@@ -3472,19 +3500,26 @@ func buildTunnelChainServiceConfig(tunnelID int64, chainNode tunnelRuntimeNode,
handlerCfg := map[string]interface{}{
"type": "relay",
}
if isTLSTunnelProtocol(protocol) {
handlerCfg["metadata"] = map[string]interface{}{"nodelay": true}
handlerMetadata := map[string]interface{}{}
if isTCPTunnelProtocol(protocol) {
handlerMetadata["nodelay"] = true
handlerMetadata["mux.keepaliveInterval"] = "15s"
handlerMetadata["mux.keepaliveTimeout"] = "45s"
}
if isKCPTunnelProtocol(protocol) {
handlerMetadata["connectTimeout"] = "30s"
}
if len(handlerMetadata) > 0 {
handlerCfg["metadata"] = handlerMetadata
}
if nextHopCandidateCount > 1 {
handlerCfg["retries"] = nextHopCandidateCount - 1
}
service := map[string]interface{}{
"name": fmt.Sprintf("%d_tls", tunnelID),
"addr": processServerAddress(fmt.Sprintf("%s:%d", defaultString(strings.TrimSpace(chainNode.ConnectIP), node.TCPListenAddr), chainNode.Port)),
"handler": handlerCfg,
"listener": map[string]interface{}{
"type": protocol,
},
"name": fmt.Sprintf("%d_%s", tunnelID, protocol),
"addr": processServerAddress(fmt.Sprintf("%s:%d", defaultString(strings.TrimSpace(chainNode.ConnectIP), node.TCPListenAddr), chainNode.Port)),
"handler": handlerCfg,
"listener": buildTunnelListenerConfig(protocol),
}
if chainNode.ChainType == 2 {
service["handler"].(map[string]interface{})["chain"] = fmt.Sprintf("chains_%d", tunnelID)
@@ -3567,10 +3602,45 @@ func nodeDisplayName(node *nodeRecord) string {
return fmt.Sprintf("node_%d", node.ID)
}
func isTCPTunnelProtocol(protocol string) bool {
p := strings.ToLower(strings.TrimSpace(defaultString(protocol, "tls")))
return p == "tls" || p == "mtls" || p == "mtcp"
}
func isKCPTunnelProtocol(protocol string) bool {
return strings.EqualFold(strings.TrimSpace(protocol), "kcp")
}
func isTLSTunnelProtocol(protocol string) bool {
return strings.EqualFold(strings.TrimSpace(defaultString(protocol, "tls")), "tls")
}
func buildTunnelDialerConfig(protocol string) map[string]interface{} {
dialer := map[string]interface{}{
"type": protocol,
}
if isKCPTunnelProtocol(protocol) {
dialer["metadata"] = map[string]interface{}{
"kcp.keepalive": 10,
"kcp.tcp": false,
}
}
return dialer
}
func buildTunnelListenerConfig(protocol string) map[string]interface{} {
listener := map[string]interface{}{
"type": protocol,
}
if isKCPTunnelProtocol(protocol) {
listener["metadata"] = map[string]interface{}{
"kcp.keepalive": 10,
"kcp.tcp": false,
}
}
return listener
}
func nodeSupportsV4(node *nodeRecord) bool {
if node == nil {
return false
@@ -0,0 +1,60 @@
package handler
import (
"fmt"
"net"
"strings"
)
// DisableSafeRemoteAddrCheckForTesting allows bypassing the safety check during integration tests.
var DisableSafeRemoteAddrCheckForTesting = false
// IsSafeRemoteAddr checks if a given address is safe to connect to (prevents SSRF/Open Proxy).
// It resolves domains to IPs to prevent DNS rebinding attacks pointing to internal networks.
func IsSafeRemoteAddr(addr string) error {
if DisableSafeRemoteAddrCheckForTesting {
return nil
}
host, _, err := net.SplitHostPort(addr)
if err != nil {
if strings.Contains(err.Error(), "missing port in address") {
host = addr
} else {
return fmt.Errorf("invalid address format: %v", err)
}
}
ips, err := net.LookupIP(host)
if err != nil {
return fmt.Errorf("could not resolve address: %v", err)
}
for _, ip := range ips {
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast() {
return fmt.Errorf("address resolves to internal or reserved IP: %s", ip.String())
}
}
return nil
}
// IsValidNodeAddress ensures the address is strictly a host or host:port.
// It explicitly denies schemes (http://, https://), paths (/...), and query params (?).
func IsValidNodeAddress(addr string) error {
addr = strings.TrimSpace(addr)
if strings.Contains(addr, "://") {
return fmt.Errorf("address must not contain scheme (e.g. http://)")
}
if strings.ContainsAny(addr, "/?") {
return fmt.Errorf("address must not contain path or query parameters")
}
_, _, err := net.SplitHostPort(addr)
if err != nil {
if !strings.Contains(err.Error(), "missing port in address") {
return fmt.Errorf("invalid address format")
}
}
return nil
}
+1 -1
View File
@@ -109,7 +109,7 @@ func requiresAdmin(path string) bool {
return true
}
if strings.HasPrefix(path, "/api/v1/federation/share/") {
if strings.HasPrefix(path, "/api/v1/federation/share/") || strings.HasPrefix(path, "/api/v1/federation/node/") {
return true
}
@@ -6,9 +6,14 @@ import (
"strings"
"testing"
"go-backend/internal/http/handler"
"go-backend/internal/store/repo"
)
func init() {
handler.DisableSafeRemoteAddrCheckForTesting = true
}
func mustLastInsertID(t *testing.T, r *repo.Repository, label string) int64 {
t.Helper()
var id int64
+18 -34
View File
@@ -1,46 +1,30 @@
# GO-GOST SERVICE KNOWLEDGE BASE
# go-gost
**Generated:** Fri Mar 20 2026
**Commit:** f45f960
**Branch:** main
**Tag:** 2.1.9-beta6
Forwarding agent (forked GOST v3). Uses local `x/` module via `replace github.com/go-gost/x => ./x`.
## OVERVIEW
Forwarding agent built on GOST v3 with a local fork of `github.com/go-gost/x` under `x/`.
**Stack:** Go 1.23, github.com/go-gost/core v0.3.1, local `go-gost/x` module.
## Structure
## STRUCTURE
```
go-gost/
├── main.go # Entry; reads panel config.json; starts svc.Run(program)
├── config.go # Panel config.json loader (addr/secret + ports)
├── program.go # GOST runtime: parse config, run/reload services
├── x/ # Local fork of github.com/go-gost/x (has its own go.mod)
└── go.mod # replace github.com/go-gost/x => ./x
```
| File | Role |
|------|------|
| `main.go` | Entry point, reads `config.json`, starts reporter + service |
| `config.go` | Panel integration config loader (addr, secret, ports) |
| `program.go` | GOST runtime: parse config, run/reload services (SIGHUP) |
| `x/` | Local fork of `github.com/go-gost/x` (own `go.mod`) |
## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| **Panel integration config** | `go-gost/config.go` | Expects `config.json` in cwd by default |
| **Service lifecycle/reload** | `go-gost/program.go` | Parses config; handles SIGHUP reload |
| **WebSocket reporting** | `go-gost/main.go` | Starts reporter + sets HTTP report URL |
| **Protocol behaviors** | `go-gost/x/` | Handlers/listeners/dialers live here |
| **Build** | `go-gost/Makefile` | Cross-compile targets for amd64/arm64 |
## Conventions
## CONVENTIONS
- Two configs exist: panel integration uses `config.json`; forwarding services use GOST config (defaults to `gost.{json,yaml}` via viper search paths).
- `go-gost/x/` is the primary extension surface; avoid editing vendored deps.
- Agent communicates with panel via WebSocket (real-time commands) + HTTP (batch traffic reports).
- Two config files: panel integration uses `config.json`; forwarding uses GOST config (`gost.{json,yaml}`).
- `x/` is the extension surface — add handlers/listeners/dialers there, not in vendored deps.
- Agent→panel: WebSocket (real-time commands) + HTTP (batch traffic reports).
- All panel communication uses AES encryption with node `secret` as PSK.
- CI builds with `CGO_ENABLED=0` for static binaries, then compresses with UPX.
## ANTI-PATTERNS
- **DO NOT EDIT** generated protobuf in `x/internal/util/grpc/proto/`.
## Anti-patterns
- Don't edit `x/internal/util/grpc/proto/*.pb.go` (generated protobuf).
## Commands
## COMMANDS
```bash
cd go-gost
go run .
go test ./...
go build .
+14 -28
View File
@@ -1,8 +1,6 @@
module github.com/go-gost/gost
go 1.23.0
toolchain go1.23.4
go 1.25.0
require (
github.com/go-gost/core v0.3.1
@@ -16,7 +14,6 @@ require (
github.com/alessio/shellescape v1.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -25,8 +22,6 @@ require (
github.com/coreos/go-iptables v0.7.0 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/expr-lang/expr v1.16.3 // indirect
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/cors v1.7.2 // indirect
@@ -47,17 +42,13 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/reedsolomon v1.11.8 // indirect
@@ -65,8 +56,6 @@ require (
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/netlink v1.6.0 // indirect
github.com/mdlayher/socket v0.1.1 // indirect
github.com/miekg/dns v1.1.61 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -87,7 +76,7 @@ require (
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/quic-go/quic-go v0.49.1 // indirect
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rs/xid v1.3.0 // indirect
@@ -102,7 +91,6 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
@@ -124,27 +112,25 @@ require (
github.com/zalando/go-keyring v0.2.4 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/go-gost/x => ./x
+48 -64
View File
@@ -10,8 +10,6 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
@@ -27,7 +25,6 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -40,10 +37,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/expr-lang/expr v1.16.3 h1:NLldf786GffptcXNxxJx5dQ+FzeWDKChBDqOOwyK8to=
github.com/expr-lang/expr v1.16.3/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf h1:NqGS3vTHzVENbIfd87cXZwdpO6MB2R1PjHMJLi4Z3ow=
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf/go.mod h1:eSnAor2YCfMCVYrVNEhkLGN/r1L+J4uDjc0EUy0tfq4=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -70,8 +63,10 @@ github.com/go-gost/relay v0.5.0 h1:JG1tgy/KWiVXS0ukuVXvbM0kbYuJTWxYpJ5JwzsCf/c=
github.com/go-gost/relay v0.5.0/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8=
github.com/go-gost/tls-dissector v0.1.1 h1:2zUOTPzCQAUQ54Rpy0UEi3JPMQSYsIFSeFeKrzmkCoU=
github.com/go-gost/tls-dissector v0.1.1/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -103,6 +98,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
@@ -112,9 +109,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866 h1:NaJi58bCZZh0jjPw78EqDZekPEfhlzYE01C5R+zh1tE=
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866/go.mod h1:riddUzxTSBpJXk3qBHtYr4qOhFhT6k/1c0E3qkQjQpA=
@@ -126,16 +122,10 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf h1:C1GPyPJrOlJlIrcaBBiBpDsqZena2Ks8spa5xZqr1XQ=
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf/go.mod h1:zXqxTI6jXDdKnlf8s+nT+3c8LrwUEy3yNpO4XJL90lA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/judwhite/go-svc v1.2.1 h1:a7fsJzYUa33sfDJRF2N/WXhA+LonCEEY8BJb1tuS5tA=
@@ -158,10 +148,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI=
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -213,8 +199,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/quic-go/quic-go v0.49.1 h1:e5JXpUyF0f2uFjckQzD8jTghZrOUK1xxDqqZhlwixo0=
github.com/quic-go/quic-go v0.49.1/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@@ -223,7 +209,6 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
@@ -249,8 +234,6 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
@@ -312,14 +295,22 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68=
github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
@@ -331,8 +322,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
@@ -342,8 +333,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -353,21 +344,18 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -380,11 +368,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -392,25 +376,24 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -419,37 +402,38 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+26 -41
View File
@@ -1,50 +1,35 @@
# GO-GOST/X KNOWLEDGE BASE
# go-gost/x
**Generated:** Fri Mar 20 2026
**Commit:** f45f960
**Branch:** main
**Tag:** 2.1.9-beta6
Local fork of `github.com/go-gost/x`. Standalone Go module, used by `go-gost/` via `replace => ./x`.
## OVERVIEW
Local fork of `github.com/go-gost/x` used by `go-gost/` via `replace github.com/go-gost/x => ./x`. Most protocol/runtime behavior changes happen here. 30+ top-level packages - framework-style layout.
## Key packages
## STRUCTURE
```
go-gost/x/
├── api/ # Gin management API + embedded swagger docs (22 files)
├── config/ # Config model + parsing/load/reload
├── connector/ # Outbound connect implementations
├── dialer/ # Outbound dialers (tcp/tls/ws/quic/...)
├── handler/ # Protocol handlers (socks/http/tunnel/relay/...)
├── listener/ # Inbound listeners (tcp/udp/tun/tap/redirect/...)
├── limiter/ # Traffic/rate/conn limiters
├── registry/ # Registries for services/handlers/listeners/etc (20 files)
├── service/ # Service wrappers + reporting hooks
├── socket/ # WebSocket reporter / panel integration (6 files)
└── internal/ # Shared internals (grpc proto, net utils, sniffing, tls, ...)
```
| Dir | Role |
|-----|------|
| `handler/` | Protocol handlers (socks, http, tunnel, relay, ...) |
| `listener/` | Inbound listeners (tcp, udp, tun, tap, redirect, ...) |
| `dialer/` | Outbound dialers (tcp, tls, ws, quic, ...) |
| `connector/` | Outbound connect implementations |
| `service/` | Service wrappers + reporting hooks |
| `socket/` | WebSocket reporter / panel integration |
| `config/` | Config model + parsing/load/reload |
| `registry/` | Component registries (`Register{Type}(name, creator)`) |
| `api/` | Gin management API + embedded swagger docs |
| `limiter/` | Traffic/rate/conn limiters |
| `internal/` | gRPC proto, net utils, sniffing, TLS |
## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| **Management API routes/auth** | `go-gost/x/api/api.go` | `/docs`, `/config/*`; BasicAuth + interceptor |
| **Service config parsing** | `go-gost/x/config/parsing/` | Converts config to running services |
| **Add a handler** | `go-gost/x/handler/` | Per-protocol subdirs |
| **Add a listener/dialer** | `go-gost/x/listener/`, `go-gost/x/dialer/` | Transport variants |
| **Panel reporting** | `go-gost/x/socket/` | WebSocket + HTTP report URL hooks |
| **Register new component** | `go-gost/x/registry/` | `Register{Type}(name, creator)` |
## Conventions
## CONVENTIONS
- `go-gost/x/` is a standalone Go module (`go-gost/x/go.mod`); run go tooling from this dir when debugging module resolution.
- Generated gRPC/proto code lives under `go-gost/x/internal/util/grpc/proto/`.
- Handlers/listeners/dialers follow consistent pattern: `{type}.go` + `metadata.go` per protocol.
- OS-specific code uses `name_[os].go` suffix (e.g., `tun_linux.go`, `tun_darwin.go`).
- Each protocol follows `{type}.go` + `metadata.go` pattern.
- OS-specific code uses `name_[os].go` suffix (e.g. `tun_linux.go`).
- Run Go tooling from this directory for module resolution issues.
## ANTI-PATTERNS
- Do not edit generated files in `go-gost/x/internal/util/grpc/proto/` (`*.pb.go`, `*_grpc.pb.go`).
## Anti-patterns
- Don't edit `internal/util/grpc/proto/*.pb.go` or `*_grpc.pb.go` (generated).
## Commands
## COMMANDS
```bash
cd go-gost/x
go test ./...
```
```
+45 -14
View File
@@ -40,21 +40,52 @@ func (d *kcpDialer) parseMetadata(md mdata.Metadata) (err error) {
d.md.config = cfg
}
if d.md.config == nil {
d.md.config = kcp_util.DefaultConfig
d.md.config = kcp_util.DefaultConfig.Clone()
} else {
d.md.config = d.md.config.Clone()
}
if md != nil {
if md.IsExists("kcp.tcp") {
d.md.config.TCP = mdutil.GetBool(md, "kcp.tcp")
}
if md.IsExists("kcp.key") {
d.md.config.Key = mdutil.GetString(md, "kcp.key")
}
if md.IsExists("kcp.crypt") {
d.md.config.Crypt = mdutil.GetString(md, "kcp.crypt")
}
if md.IsExists("kcp.mode") {
d.md.config.Mode = mdutil.GetString(md, "kcp.mode")
}
if md.IsExists("kcp.keepalive") {
d.md.config.KeepAlive = mdutil.GetInt(md, "kcp.keepalive")
}
if md.IsExists("kcp.interval") {
d.md.config.Interval = mdutil.GetInt(md, "kcp.interval")
}
if md.IsExists("kcp.mtu") {
d.md.config.MTU = mdutil.GetInt(md, "kcp.mtu")
}
if md.IsExists("kcp.rcvwnd") {
d.md.config.RcvWnd = mdutil.GetInt(md, "kcp.rcvwnd")
}
if md.IsExists("kcp.sndwnd") {
d.md.config.SndWnd = mdutil.GetInt(md, "kcp.sndwnd")
}
if md.IsExists("kcp.smuxver") {
d.md.config.SmuxVer = mdutil.GetInt(md, "kcp.smuxver")
}
if md.IsExists("kcp.smuxbuf") {
d.md.config.SmuxBuf = mdutil.GetInt(md, "kcp.smuxbuf")
}
if md.IsExists("kcp.streambuf") {
d.md.config.StreamBuf = mdutil.GetInt(md, "kcp.streambuf")
}
if md.IsExists("kcp.nocomp") {
d.md.config.NoComp = mdutil.GetBool(md, "kcp.nocomp")
}
}
d.md.config.TCP = mdutil.GetBool(md, "kcp.tcp", "tcp")
d.md.config.Key = mdutil.GetString(md, "kcp.key")
d.md.config.Crypt = mdutil.GetString(md, "kcp.crypt")
d.md.config.Mode = mdutil.GetString(md, "kcp.mode")
d.md.config.KeepAlive = mdutil.GetInt(md, "kcp.keepalive")
d.md.config.Interval = mdutil.GetInt(md, "kcp.interval")
d.md.config.MTU = mdutil.GetInt(md, "kcp.mtu")
d.md.config.RcvWnd = mdutil.GetInt(md, "kcp.rcvwnd")
d.md.config.SndWnd = mdutil.GetInt(md, "kcp.sndwnd")
d.md.config.SmuxVer = mdutil.GetInt(md, "kcp.smuxver")
d.md.config.SmuxBuf = mdutil.GetInt(md, "kcp.smuxbuf")
d.md.config.StreamBuf = mdutil.GetInt(md, "kcp.streambuf")
d.md.config.NoComp = mdutil.GetBool(md, "kcp.nocomp")
d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout)
return
+13 -15
View File
@@ -1,8 +1,6 @@
module github.com/go-gost/x
go 1.22.0
toolchain go1.23.4
go 1.25.0
require github.com/shirou/gopsutil/v3 v3.24.5
@@ -28,7 +26,7 @@ require (
github.com/pion/dtls/v2 v2.2.6
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.48.2
github.com/quic-go/quic-go v0.49.1
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66
github.com/rs/xid v1.3.0
github.com/shadowsocks/go-shadowsocks2 v0.1.5
@@ -45,15 +43,15 @@ require (
github.com/xtaci/tcpraw v1.2.25
github.com/yl2chen/cidranger v1.0.2
github.com/zalando/go-keyring v0.2.4
golang.org/x/crypto v0.31.0
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20241210194714-1829a127f884
golang.org/x/net v0.33.0
golang.org/x/sys v0.28.0
golang.org/x/text v0.21.0
golang.org/x/net v0.53.0
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
golang.org/x/time v0.5.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.35.1
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
)
@@ -128,11 +126,11 @@ require (
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
+46 -28
View File
@@ -63,8 +63,10 @@ github.com/go-gost/relay v0.5.0 h1:JG1tgy/KWiVXS0ukuVXvbM0kbYuJTWxYpJ5JwzsCf/c=
github.com/go-gost/relay v0.5.0/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8=
github.com/go-gost/tls-dissector v0.1.1 h1:2zUOTPzCQAUQ54Rpy0UEi3JPMQSYsIFSeFeKrzmkCoU=
github.com/go-gost/tls-dissector v0.1.1/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -96,6 +98,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
@@ -105,8 +109,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
@@ -193,8 +197,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/quic-go/quic-go v0.49.1 h1:e5JXpUyF0f2uFjckQzD8jTghZrOUK1xxDqqZhlwixo0=
github.com/quic-go/quic-go v0.49.1/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@@ -286,6 +290,18 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68=
github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -301,8 +317,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
@@ -312,8 +328,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -326,15 +342,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -352,22 +368,22 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -378,8 +394,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -387,27 +403,29 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+1
View File
@@ -227,6 +227,7 @@ func (c *conn) SetIdle(idle bool) {
func (c *conn) WriteQueue(b []byte) error {
select {
case c.rc <- b:
c.SetIdle(false)
return nil
case <-c.closed:
+10 -1
View File
@@ -40,7 +40,7 @@ var (
SnmpLog: "",
SnmpPeriod: 60,
Signal: false,
TCP: false,
TCP: false, // default UDP mode (use tcpraw when true)
}
)
@@ -89,6 +89,15 @@ func ParseFromFile(filename string) (*Config, error) {
return config, nil
}
// Clone returns a deep copy of the Config.
func (c *Config) Clone() *Config {
if c == nil {
return nil
}
clone := *c
return &clone
}
// Init initializes the KCP config.
func (c *Config) Init() {
switch c.Mode {
+45 -14
View File
@@ -43,21 +43,52 @@ func (l *kcpListener) parseMetadata(md mdata.Metadata) (err error) {
}
if l.md.config == nil {
l.md.config = kcp_util.DefaultConfig
l.md.config = kcp_util.DefaultConfig.Clone()
} else {
l.md.config = l.md.config.Clone()
}
if md != nil {
if md.IsExists("kcp.tcp") {
l.md.config.TCP = mdutil.GetBool(md, "kcp.tcp")
}
if md.IsExists("kcp.key") {
l.md.config.Key = mdutil.GetString(md, "kcp.key")
}
if md.IsExists("kcp.crypt") {
l.md.config.Crypt = mdutil.GetString(md, "kcp.crypt")
}
if md.IsExists("kcp.mode") {
l.md.config.Mode = mdutil.GetString(md, "kcp.mode")
}
if md.IsExists("kcp.keepalive") {
l.md.config.KeepAlive = mdutil.GetInt(md, "kcp.keepalive")
}
if md.IsExists("kcp.interval") {
l.md.config.Interval = mdutil.GetInt(md, "kcp.interval")
}
if md.IsExists("kcp.mtu") {
l.md.config.MTU = mdutil.GetInt(md, "kcp.mtu")
}
if md.IsExists("kcp.rcvwnd") {
l.md.config.RcvWnd = mdutil.GetInt(md, "kcp.rcvwnd")
}
if md.IsExists("kcp.sndwnd") {
l.md.config.SndWnd = mdutil.GetInt(md, "kcp.sndwnd")
}
if md.IsExists("kcp.smuxver") {
l.md.config.SmuxVer = mdutil.GetInt(md, "kcp.smuxver")
}
if md.IsExists("kcp.smuxbuf") {
l.md.config.SmuxBuf = mdutil.GetInt(md, "kcp.smuxbuf")
}
if md.IsExists("kcp.streambuf") {
l.md.config.StreamBuf = mdutil.GetInt(md, "kcp.streambuf")
}
if md.IsExists("kcp.nocomp") {
l.md.config.NoComp = mdutil.GetBool(md, "kcp.nocomp")
}
}
l.md.config.TCP = mdutil.GetBool(md, "kcp.tcp", "tcp")
l.md.config.Key = mdutil.GetString(md, "kcp.key")
l.md.config.Crypt = mdutil.GetString(md, "kcp.crypt")
l.md.config.Mode = mdutil.GetString(md, "kcp.mode")
l.md.config.KeepAlive = mdutil.GetInt(md, "kcp.keepalive")
l.md.config.Interval = mdutil.GetInt(md, "kcp.interval")
l.md.config.MTU = mdutil.GetInt(md, "kcp.mtu")
l.md.config.RcvWnd = mdutil.GetInt(md, "kcp.rcvwnd")
l.md.config.SndWnd = mdutil.GetInt(md, "kcp.sndwnd")
l.md.config.SmuxVer = mdutil.GetInt(md, "kcp.smuxver")
l.md.config.SmuxBuf = mdutil.GetInt(md, "kcp.smuxbuf")
l.md.config.StreamBuf = mdutil.GetInt(md, "kcp.streambuf")
l.md.config.NoComp = mdutil.GetBool(md, "kcp.nocomp")
l.md.backlog = mdutil.GetInt(md, backlog)
if l.md.backlog <= 0 {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
)
const (
defaultTTL = 5 * time.Second
defaultTTL = 30 * time.Second
defaultReadBufferSize = 8192
defaultReadQueueSize = 1024
defaultBacklog = 128
+1 -1
View File
@@ -8,7 +8,7 @@ import (
)
const (
defaultTTL = 5 * time.Second
defaultTTL = 30 * time.Second
defaultReadBufferSize = 8192
defaultReadQueueSize = 128
defaultBacklog = 128
+4 -4
View File
@@ -81,7 +81,7 @@ func (p *httpPlugin) Register(ctx context.Context, service *sd.Service, opts ...
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf(resp.Status)
return fmt.Errorf("%s", resp.Status)
}
return nil
@@ -119,7 +119,7 @@ func (p *httpPlugin) Deregister(ctx context.Context, service *sd.Service) error
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf(resp.Status)
return fmt.Errorf("%s", resp.Status)
}
return nil
@@ -157,7 +157,7 @@ func (p *httpPlugin) Renew(ctx context.Context, service *sd.Service) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf(resp.Status)
return fmt.Errorf("%s", resp.Status)
}
return nil
@@ -189,7 +189,7 @@ func (p *httpPlugin) Get(ctx context.Context, name string) (services []*sd.Servi
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(resp.Status)
return nil, fmt.Errorf("%s", resp.Status)
}
res := &httpGetResponse{}
+87
View File
@@ -0,0 +1,87 @@
# 069: UDP 隧道转发断流修复 + KCP 协议支持
## 问题分析
GOST 隧道转发 UDP 断流的根因涉及三个超时层:
### 1. UDP 虚拟连接 TTL 过短(5秒)— 主因
- `go-gost/x/listener/udp/metadata.go:11`: `defaultTTL = 5s`
- `go-gost/x/internal/net/udp/pool.go:79-115`: `idleCheck()` 每 TTL 周期标记空闲连接,下一周期关闭
- **影响**: UDP 流无数据包超过 ~10 秒即被静默关闭
- **后端配置**: `control_plane.go:1607-1613` 设置了 `keepAlive: true`,但仅在 `tunnelTLSProtocol=true` 时设 `ttl: "10s"`,常规隧道转发不设 TTL 覆盖
### 2. Smux Keepalive 未显式配置
- smux `DefaultConfig()`: `KeepAliveInterval=10s`, `KeepAliveTimeout=30s`
- 后端 `buildTunnelChainServiceConfig` 生成 relay handler 时不设置 mux 参数
- 依赖 smux 默认值,但中间 NAT/防火墙可能先于 30s 断开 TCP 连接
### 3. 隧道仅依赖 TCP 传输 — 无 UDP 备选
- 隧道链节点间仅支持 TLS/TCP 传输
- TCP 队头阻塞 + 单连接承载所有流 + 无 UDP 级别的容错
## 修复方案
### Part A: 修复现有问题(低风险,即时生效)
#### A1. 增大 UDP listener 默认 TTL
- **文件**: `go-gost/x/listener/udp/metadata.go`
- **改动**: `defaultTTL = 30s`(从 5s)
- **原因**: 5s 对实际场景太激进,DNS 等长间隔 UDP 应用会被误杀
#### A2. 后端生成 UDP forward 配置时强制设置 TTL
- **文件**: `go-backend/internal/http/handler/control_plane.go`
- **改动**: `buildForwardServiceConfigs` 中始终设置 `listener.metadata.ttl = "30s"`
- **原因**: 确保配置显式覆盖,不依赖 go-gost 默认值
#### A3. 后端隧道链服务配置加入 mux keepalive
- **文件**: `go-backend/internal/http/handler/mutations.go`
- **改动**: `buildTunnelChainServiceConfig` 中的 relay handler 加入 mux metadata:
```go
"mux.keepaliveInterval": "15s",
"mux.keepaliveTimeout": "45s",
```
### Part B: 新增 KCP 协议支持(新功能)
#### B1. 后端支持 `kcp` 作为隧道链传输协议
- **文件**: `go-backend/internal/http/handler/mutations.go`
- **改动**:
- `buildTunnelChainConfig`: 识别 `kcp` 协议,生成对应的 connector/dialer
- `buildTunnelChainServiceConfig`: 识别 `kcp` 协议,生成 KCP listener
- `isTLSTunnelProtocol` → `isTCPTunnelProtocol`(包含 TLS)
- KCP dialer 设置 `kcp.keepalive` 参数
- KCP 配置无需 `nodelay`(那是 TLS 特有)
#### B2. 前端支持选择 `kcp` 协议
- **文件**: `vite-frontend/src/pages/tunnel/` 相关表单
- **改动**: 协议选择器中加入 KCP 选项
#### B3. 确认 go-gost KCP 组件正常
- KCP dialer: `go-gost/x/dialer/kcp/` — 已存在
- KCP listener: `go-gost/x/listener/kcp/` — 已存在
- KCP connector: 使用 `relay` connector 通过 KCP dialer 连接
- 验证 KCP 配置的 metadata 参数传递正确
## 任务清单
- [x] A1: 增大 UDP listener 默认 TTL (5s→30s)
- [x] A2: 后端 forward 配置强制设置 UDP TTL=30s
- [x] A3: 后端隧道链 relay handler 加入 mux keepalive
- [x] B1: 后端支持 kcp 作为隧道链协议
- [x] B2: 前端协议选择器加入 KCP
- [x] B3: 验证 go-gost KCP 组件配置正确
- [x] C1: go-backend 合约测试 (187 passed)
- [x] C2: go-gost 编译验证 (passed)
- [x] C3: vite-frontend 编译验证 (passed)
## 文件变更清单
| 文件 | 变更类型 |
|------|----------|
| `go-gost/x/listener/udp/metadata.go` | 修改 defaultTTL: 5s→30s |
| `go-gost/x/listener/rudp/metadata.go` | 修改 defaultTTL: 5s→30s |
| `go-backend/internal/http/handler/control_plane.go` | buildForwardServiceConfigs: 强制设置 UDP TTL=30s, 移除 tunnelTLSProtocol 参数 |
| `go-backend/internal/http/handler/control_plane_test.go` | 更新测试调用签名 |
| `go-backend/internal/http/handler/mutations.go` | 新增 isTCPTunnelProtocol/isKCPTunnelProtocol/buildTunnelDialerConfig/buildTunnelListenerConfig; 隧道链配置加入 mux keepalive + KCP 支持 |
| `go-backend/internal/http/handler/federation.go` | federation 配置同步加入 mux keepalive + KCP 支持 |
| `vite-frontend/src/pages/tunnel.tsx` | 协议选择器新增 KCP 选项 (3 处)
+30 -60
View File
@@ -1,70 +1,40 @@
# VITE FRONTEND KNOWLEDGE BASE
# vite-frontend
**Generated:** Fri Mar 20 2026
**Commit:** f45f960
**Branch:** main
**Tag:** 2.1.9-beta6
React dashboard for FLVX. rolldown-vite + TypeScript + Tailwind v4 + shadcn/radix.
## OVERVIEW
Web management console for FLVX.
**Stack:** React 18, rolldown-vite, TypeScript, Tailwind CSS v4, shadcn/radix primitives with HeroUI-compatible bridge.
## Structure
## STRUCTURE
```
vite-frontend/
├── src/
│ ├── api/ # Axios wrapper + typed endpoint helpers
│ ├── components/ui/ # shadcn/radix primitive components
│ ├── shadcn-bridge/heroui/ # HeroUI-compatible facade (23 components)
│ ├── pages/ # Route views + page modules (forward/node/tunnel)
│ ├── hooks/ # H5/WebView/mobile hooks
│ ├── styles/
│ │ ├── globals.css # Base styles + imports tailwind-theme.pcss
│ │ └── tailwind-theme.pcss # Tailwind v4 @theme inline semantic token mapping
│ ├── App.tsx # Routes + ProtectedRoute + H5 layout selection
│ ├── main.tsx # ReactDOM + BrowserRouter + Provider
│ └── provider.tsx # Toast/theme/provider composition
├── components.json # shadcn/ui config
├── tailwind.config.js # Compatibility config for migration scaffolding
├── vite.config.ts # base '/', host 0.0.0.0:3000; minify/treeshake disabled
└── package.json
```
| Dir/File | Role |
|----------|------|
| `src/App.tsx` | Routes + ProtectedRoute + H5 layout selection |
| `src/main.tsx` | Entry: ReactDOM + BrowserRouter |
| `src/api/` | Axios wrapper, sends raw JWT in `Authorization` |
| `src/pages/` | Route views (forward, node, tunnel, settings, ...) |
| `src/shadcn-bridge/heroui/` | HeroUI-compatible facade — **import from here only** |
| `src/components/ui/` | shadcn/radix primitives |
| `src/styles/globals.css` | Base styles — **must import `tailwind-theme.pcss`** |
| `src/styles/tailwind-theme.pcss` | Tailwind v4 `@theme inline` semantic tokens |
| `vite.config.ts` | host `0.0.0.0:3000`, `minify: false`, `treeshake: false` |
## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| **Route definitions** | `src/App.tsx` | React Router v6 + ProtectedRoute |
| **API Client/Auth header** | `src/api/network.ts` | Sends raw JWT in `Authorization` header |
| **Login Flow** | `src/pages/index.tsx` | Calls `login()`, stores `localStorage.token` |
| **Auth helpers** | `src/utils/auth.ts`, `src/utils/jwt.ts` | Role checks + token expiration parsing |
| **UI bridge usage** | `src/shadcn-bridge/heroui/` | Import from bridge, not `@heroui/*` |
| **Button parity mapping** | `src/shadcn-bridge/heroui/button.tsx` | Legacy `color`/`variant` mapped to shadcn classes |
| **Semantic theme tokens** | `src/styles/tailwind-theme.pcss` | Restores classes like `bg-primary`, `border-input` |
| **Theme wiring** | `src/styles/globals.css` | Must import `./tailwind-theme.pcss` |
## Conventions
## CONVENTIONS
- **Auth Header**: Use raw JWT token (no `Bearer` prefix).
- **API Envelope**: Responses follow `{code, msg, data, ts}`.
- **UI Imports**: Use `src/shadcn-bridge/heroui/*` in app pages/layouts for compatibility.
- **Semantic Colors**: Keep `globals.css -> tailwind-theme.pcss` import intact or semantic classes break.
- **Build profile**: `minify: false`, `treeshake: false` for debugging.
- **Layout mode**: H5/mobile mode controlled by existing route/query and hook logic.
- **Auth**: raw JWT in `Authorization` header — no `Bearer` prefix.
- **API envelope**: `{code, msg, data, ts}`, code 0 = success.
- **UI imports**: `src/shadcn-bridge/heroui/*` only, never `@heroui/*` or `@nextui-org/*`.
- **Theme**: don't remove `tailwind-theme.pcss` import from `globals.css` — breaks semantic classes (`bg-primary`, `text-foreground`, `border-input`).
## ANTI-PATTERNS
- **DO NOT ADD** `Bearer` to auth header in frontend requests.
- **DO NOT REINTRODUCE** `@heroui/*` or `@nextui-org/*` dependencies.
- **DO NOT REMOVE** `src/styles/tailwind-theme.pcss` import from `src/styles/globals.css`.
- **DO NOT ADD** frontend tests; no Vitest/Jest setup exists.
## Anti-patterns
## NOTES
- Uses `rolldown-vite` (experimental Rust bundler) instead of standard Vite.
- Build outputs are non-minified (debugging mode).
- No test infrastructure exists (Vitest/Jest not configured).
- Don't add `Bearer` prefix to auth header.
- Don't reintroduce `@heroui/*` or `@nextui-org/*` packages.
- Don't add frontend tests (no test infrastructure).
- Don't remove `tailwind-theme.pcss` import.
## Commands
## COMMANDS
```bash
cd vite-frontend
npm run dev
npm run build
npm run lint
npm install --legacy-peer-deps
npm run dev # http://0.0.0.0:3000
npm run build # tsc && vite build
npm run lint # eslint --fix
```
+3
View File
@@ -2,6 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta name="theme-color" content="#2563eb" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
File diff suppressed because one or more lines are too long
+17 -1
View File
@@ -43,12 +43,28 @@ http {
root /usr/share/nginx/html;
index index.html;
# PWA 和 Service Worker 不缓存
location ~* (sw\.js|service-worker\.js|workbox-.*\.js|manifest\.webmanifest)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}
# 静态文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
expires 30d;
add_header Cache-Control "public, immutable";
}
# HTML 文件不缓存
location ~* \.(?:htm|html)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri $uri/ /index.html;
}
# Vue Router的history模式支持
location / {
try_files $uri $uri/ /index.html;
Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

+39 -13
View File
@@ -1,4 +1,4 @@
import { Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { Route, Routes, useLocation, useNavigate, Navigate } from "react-router-dom";
import { useEffect } from "react";
import { AnimatePresence } from "framer-motion";
@@ -15,7 +15,6 @@ import ProfilePage from "@/pages/profile";
import LimitPage from "@/pages/limit";
import ConfigPage from "@/pages/config";
import PanelSharingPage from "@/pages/panel-sharing";
import { SettingsPage } from "@/pages/settings";
import AdminLayout from "@/layouts/admin";
import H5Layout from "@/layouts/h5";
import H5SimpleLayout from "@/layouts/h5-simple";
@@ -45,11 +44,7 @@ const ProtectedRoute = ({
}, [authenticated, navigate]);
if (!authenticated) {
return (
<div className="flex items-center justify-center min-h-screen bg-white dark:bg-black">
<div className="text-lg text-gray-700 dark:text-gray-200" />
</div>
);
return <Navigate replace to="/" />;
}
// 如果跳过布局,直接返回子组件
@@ -77,11 +72,7 @@ const LoginRoute = () => {
}, [authenticated, navigate]);
if (authenticated) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-black">
<div className="text-lg text-gray-700 dark:text-gray-200" />
</div>
);
return <Navigate replace to="/dashboard" />;
}
return <IndexPage />;
@@ -90,6 +81,42 @@ const LoginRoute = () => {
function App() {
const location = useLocation();
// 处理自定义背景图片
useEffect(() => {
const updateBg = () => {
const customBg = siteConfig.app_bg_image;
if (customBg) {
if (customBg === "theme") {
document.documentElement.style.removeProperty("--custom-bg-image");
document.documentElement.style.removeProperty("--custom-bg-color");
document.documentElement.classList.add("has-theme-bg");
document.documentElement.classList.remove("has-custom-bg");
} else if (customBg.startsWith("http") || customBg.startsWith("data:") || customBg.startsWith("/") || customBg.startsWith("blob:")) {
document.documentElement.style.setProperty("--custom-bg-image", `url(${customBg})`);
document.documentElement.style.setProperty("--custom-bg-color", "transparent");
document.documentElement.classList.add("has-custom-bg");
document.documentElement.classList.remove("has-theme-bg");
} else {
// Assume solid color like "#ffffff", "white", etc.
document.documentElement.style.setProperty("--custom-bg-image", "none");
document.documentElement.style.setProperty("--custom-bg-color", customBg);
document.documentElement.classList.add("has-custom-bg");
document.documentElement.classList.remove("has-theme-bg");
}
} else {
document.documentElement.style.removeProperty("--custom-bg-image");
document.documentElement.style.removeProperty("--custom-bg-color");
document.documentElement.classList.remove("has-custom-bg");
document.documentElement.classList.remove("has-theme-bg");
}
};
updateBg();
window.addEventListener("site-config-updated", updateBg);
return () => {
window.removeEventListener("site-config-updated", updateBg);
};
}, []);
// 立即设置页面标题(使用已从缓存读取的配置)
useEffect(() => {
document.title = siteConfig.name;
@@ -207,7 +234,6 @@ function App() {
}
path="/panel-sharing"
/>
<Route element={<SettingsPage />} path="/settings" />
</Routes>
</AnimatePresence>
);
@@ -13,10 +13,10 @@ export const AnimatedPage = ({
className?: string;
}) => (
<motion.div
animate={{ opacity: 1, y: 0 }}
animate={{ y: 0 }}
className={className}
exit={{ opacity: 0, y: -8 }}
initial={{ opacity: 0, y: 16 }}
exit={{ y: -8 }}
initial={{ y: 16 }}
transition={{ duration: 0.28, ease: [0.25, 0.46, 0.45, 0.94] }}
>
{children}
@@ -28,9 +28,8 @@ export const AnimatedPage = ({
* Children should use `staggerItem` as their `variants` prop.
*/
export const staggerContainer = {
hidden: { opacity: 0 },
hidden: {},
show: {
opacity: 1,
transition: {
staggerChildren: 0.05,
delayChildren: 0.05,
@@ -42,9 +41,8 @@ export const staggerContainer = {
* Individual stagger item variant — fade-in + slide-up.
*/
export const staggerItem = {
hidden: { opacity: 0, y: 12 },
hidden: { y: 12 },
show: {
opacity: 1,
y: 0,
transition: { duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] },
},
+1 -1
View File
@@ -81,7 +81,7 @@ export const Navbar = () => {
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
title="面板设置"
onClick={() => navigate("/settings")}
onClick={() => navigate("/config")}
>
<svg
className="w-5 h-5"
@@ -54,7 +54,7 @@ export const ThemeSettings: React.FC = () => {
};
return (
<Card className="border border-gray-200 dark:border-gray-700">
<Card className="shadow-md">
<CardBody className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-5">
主题设置
+12 -5
View File
@@ -1,14 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
function Card({ className, style, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"rounded-xl border border-default-200 bg-white text-card-foreground shadow-sm dark:bg-default-50/20",
className,
"relative flex flex-col transition-all duration-300",
"rounded-[24px] text-card-foreground shadow-[0_12px_40px_rgba(0,0,0,0.12)] dark:shadow-[0_12px_40px_rgba(0,0,0,0.4)]",
className
)}
style={{
...style,
background: "linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0.1) 40%, rgba(255,255,255,0.05) 60%, rgba(255,255,255,0.2) 100%)",
boxShadow: "inset 0 1px 1px rgba(255,255,255,0.8), inset 0 0 0 1px rgba(255,255,255,0.3), inset 0 -1px 1px rgba(0,0,0,0.1), 0 12px 40px rgba(0,0,0,0.12)",
backdropFilter: "blur(24px) saturate(180%)",
WebkitBackdropFilter: "blur(24px) saturate(180%)",
}}
data-slot="card"
{...props}
/>
@@ -57,7 +64,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("p-6 pt-0", className)}
className={cn("p-6", className)}
data-slot="card-content"
{...props}
/>
+19 -9
View File
@@ -35,7 +35,7 @@ function DialogOverlay({
return (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/30 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
data-slot="dialog-overlay"
@@ -57,19 +57,29 @@ function DialogContent({
<DialogOverlay />
<DialogPrimitive.Content
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-default-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-bottom-4 data-[state=closed]:slide-out-to-bottom-2 sm:rounded-lg dark:bg-default-50",
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-bottom-4 data-[state=closed]:slide-out-to-bottom-2",
"flex flex-col transition-all duration-300",
"rounded-[24px] text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.5)] border border-white/80 dark:border-white/10",
className,
)}
style={{
background: "linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0.1) 40%, rgba(255,255,255,0.05) 60%, rgba(255,255,255,0.2) 100%)",
boxShadow: "inset 0 1px 1px rgba(255,255,255,0.8), inset 0 0 0 1px rgba(255,255,255,0.3), inset 0 -1px 1px rgba(0,0,0,0.1), 0 20px 60px rgba(0,0,0,0.2)",
backdropFilter: "blur(24px) saturate(180%)",
WebkitBackdropFilter: "blur(24px) saturate(180%)",
}}
data-slot="dialog-content"
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none">
<XIcon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
<div className="flex flex-col flex-1 min-h-0 gap-4 p-6 rounded-[inherit] overflow-hidden bg-white/40 dark:bg-zinc-900/40 w-full h-full relative z-10 pointer-events-auto">
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none">
<XIcon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</div>
</DialogPrimitive.Content>
</DialogPortal>
);
+15 -1
View File
@@ -20,6 +20,7 @@ const getInitialConfig = () => {
github_repo: GITHUB_REPO,
app_logo: "",
app_favicon: "",
app_bg_image: "",
is_commercial: false,
hide_footer_brand: false,
};
@@ -29,6 +30,8 @@ const getInitialConfig = () => {
const cachedAppLogo = localStorage.getItem(CACHE_PREFIX + "app_logo") || "";
const cachedAppFavicon =
localStorage.getItem(CACHE_PREFIX + "app_favicon") || "";
const cachedAppBgImage =
localStorage.getItem(CACHE_PREFIX + "app_bg_image") || "";
const isCommercial = localStorage.getItem(CACHE_PREFIX + "is_commercial") === "true";
const hideFooterBrand = localStorage.getItem(CACHE_PREFIX + "hide_footer_brand") === "true";
@@ -40,6 +43,7 @@ const getInitialConfig = () => {
github_repo: GITHUB_REPO,
app_logo: cachedAppLogo,
app_favicon: cachedAppFavicon,
app_bg_image: cachedAppBgImage,
is_commercial: isCommercial,
hide_footer_brand: hideFooterBrand,
};
@@ -52,6 +56,7 @@ const getInitialConfig = () => {
github_repo: GITHUB_REPO,
app_logo: cachedAppLogo,
app_favicon: cachedAppFavicon,
app_bg_image: cachedAppBgImage,
is_commercial: isCommercial,
hide_footer_brand: hideFooterBrand,
};
@@ -123,7 +128,7 @@ export const getCachedConfig = async (key: string): Promise<string | null> => {
// 获取所有配置(优先从缓存)
export const getCachedConfigs = async (): Promise<Record<string, string>> => {
// 尝试从缓存获取所有配置
const configKeys = ["app_name", "app_logo", "app_favicon"];
const configKeys = ["app_name", "app_logo", "app_favicon", "app_bg_image"];
const cachedConfigs: Record<string, string> = {};
let hasCachedData = false;
@@ -263,6 +268,10 @@ export const updateSiteConfig = async (configMap?: Record<string, string>) => {
resolvedConfigMap,
"app_favicon",
);
const hasAppBgImage = Object.prototype.hasOwnProperty.call(
resolvedConfigMap,
"app_bg_image",
);
const appName = hasAppName
? String(resolvedConfigMap.app_name || "").trim()
@@ -273,6 +282,9 @@ export const updateSiteConfig = async (configMap?: Record<string, string>) => {
const appFavicon = hasAppFavicon
? String(resolvedConfigMap.app_favicon || "").trim()
: (siteConfig.app_favicon || "").trim();
const appBgImage = hasAppBgImage
? String(resolvedConfigMap.app_bg_image || "").trim()
: (siteConfig.app_bg_image || "").trim();
if (appName && appName !== siteConfig.name) {
siteConfig.name = appName;
@@ -280,11 +292,13 @@ export const updateSiteConfig = async (configMap?: Record<string, string>) => {
siteConfig.app_logo = appLogo;
siteConfig.app_favicon = appFavicon;
siteConfig.app_bg_image = appBgImage;
siteConfig.is_commercial = resolvedConfigMap.is_commercial === "true";
siteConfig.hide_footer_brand = resolvedConfigMap.hide_footer_brand === "true";
if (typeof document !== "undefined") {
document.title = siteConfig.name;
window.dispatchEvent(new Event("site-config-updated"));
}
updateDocumentFavicon(siteConfig.app_favicon);
};
+56 -143
View File
@@ -4,12 +4,6 @@ import { toast } from "react-hot-toast";
import { AnimatePresence, motion } from "framer-motion";
import { Button } from "@/shadcn-bridge/heroui/button";
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
} from "@/shadcn-bridge/heroui/dropdown";
import {
Modal,
ModalContent,
@@ -25,7 +19,7 @@ import { getMonitorAccess, updatePassword } from "@/api";
import { safeLogout } from "@/utils/logout";
import { siteConfig } from "@/config/site";
import { useMobileBreakpoint } from "@/hooks/useMobileBreakpoint";
import { getAdminFlag, getSessionName } from "@/utils/session";
import { getAdminFlag } from "@/utils/session";
interface MenuItem {
path: string;
@@ -48,14 +42,12 @@ export default function AdminLayout({
}) {
const navigate = useNavigate();
const location = useLocation();
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const { isOpen, onOpenChange } = useDisclosure();
const [mobileMenuVisible, setMobileMenuVisible] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(
() => localStorage.getItem("sidebar_collapsed") === "true",
);
const [username, setUsername] = useState("");
const [isAdmin, setIsAdmin] = useState(false);
const [isAdmin, setIsAdmin] = useState(() => getAdminFlag());
const [monitorAllowed, setMonitorAllowed] = useState<boolean | null>(null);
const [monitorAccessReason, setMonitorAccessReason] = useState<string | null>(
null,
@@ -196,10 +188,8 @@ export default function AdminLayout({
useEffect(() => {
// 获取用户信息
const name = getSessionName() || "Admin";
const adminFlag = getAdminFlag();
setUsername(name);
setIsAdmin(adminFlag);
// Monitor permission is not strictly role-based; non-admin users may be
@@ -368,13 +358,13 @@ export default function AdminLayout({
return (
<div
className={`flex ${isMobile ? "min-h-screen" : "h-screen"} bg-gray-100 dark:bg-black`}
className={`flex ${isMobile ? "min-h-screen p-0" : "h-screen p-6 gap-6"} bg-mesh-gradient overflow-hidden`}
>
{/* 移动端遮罩层 */}
{isMobile && mobileMenuVisible && (
<button
aria-label="关闭菜单"
className="fixed inset-0 backdrop-blur-sm bg-white/50 dark:bg-black/30 z-40"
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-40"
type="button"
onClick={hideMobileMenu}
/>
@@ -383,36 +373,33 @@ export default function AdminLayout({
{/* 左侧菜单栏 */}
<aside
className={`
${isMobile ? "fixed" : "relative"}
${isMobile ? "fixed h-screen top-0 left-0 rounded-r-3xl" : "relative h-full rounded-3xl"}
${isMobile && !mobileMenuVisible ? "-translate-x-full" : "translate-x-0"}
${isMobile ? "w-64" : isCollapsed ? "w-20" : "w-72"}
bg-white dark:bg-black
shadow-lg
border-r border-gray-200 dark:border-gray-600
z-50
${isMobile ? "w-64" : isCollapsed ? "w-20" : "w-[260px]"}
bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl
shadow-[0_10px_30px_rgba(0,0,0,0.1)]
border border-white/80 dark:border-white/10
z-50
transition-all duration-300 ease-in-out
flex flex-col
${isMobile ? "h-screen" : "h-full"}
${isMobile ? "top-0 left-0" : ""}
`}
flex flex-col flex-shrink-0 `}
>
{/* Logo 区域 */}
<div className="px-5 h-14 flex items-center overflow-hidden whitespace-nowrap box-border">
<div className="flex-shrink-0 flex items-center justify-center w-10">
<BrandLogo size={28} />
<div className="px-6 py-8 flex items-center overflow-hidden whitespace-nowrap box-border">
<div className="flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-xl bg-primary text-white">
<BrandLogo size={24} />
</div>
<div
className={`transition-all duration-300 overflow-hidden ${isCollapsed ? "max-w-0 opacity-0 ml-0" : "max-w-[180px] opacity-100 ml-2"}`}
className={`transition-all duration-300 overflow-hidden ${isCollapsed ? "max-w-0 opacity-0 ml-0" : "max-w-[180px] opacity-100 ml-3"}`}
>
<h1 className="text-sm font-bold text-foreground overflow-hidden whitespace-nowrap text-ellipsis">
<h1 className="text-xl font-bold text-foreground overflow-hidden whitespace-nowrap text-ellipsis">
{siteConfig.name}
</h1>
</div>
</div>
{/* 菜单导航 */}
<nav className="flex-1 px-3 py-6 overflow-y-auto overflow-x-hidden">
<ul className="space-y-1">
<nav className="flex-1 px-4 overflow-y-auto overflow-x-hidden [scrollbar-width:none]">
<ul className="space-y-2">
{filteredMenuItems.map((item) => {
const isActive = location.pathname === item.path;
const isMonitor = item.path === "/monitor";
@@ -422,15 +409,15 @@ export default function AdminLayout({
<li key={item.path}>
<motion.button
className={`
w-full flex items-center p-2 rounded-lg text-left
relative min-h-[44px] overflow-hidden transition-colors
w-full flex items-center p-3 rounded-2xl text-left
relative min-h-[48px] overflow-hidden transition-colors
${isMonitorBlocked ? "opacity-60" : ""}
${
isActive
? "text-primary-600 dark:text-primary-300"
? "text-primary dark:text-primary-400 font-semibold"
: isMonitorBlocked
? "text-gray-500 dark:text-gray-400"
: "text-gray-700 dark:text-gray-200"
? "text-gray-500 dark:text-gray-400 font-medium"
: "text-gray-600 dark:text-gray-300 font-medium"
}
`}
aria-disabled={isMonitorBlocked}
@@ -446,7 +433,7 @@ export default function AdminLayout({
>
{isActive && (
<motion.div
className="absolute inset-0 rounded-lg bg-primary-100 dark:bg-primary-600/20"
className="absolute inset-0 rounded-2xl bg-white/60 dark:bg-white/10 backdrop-blur-xl shadow-[0_12px_36px_rgba(0,0,0,0.18)] border border-white dark:border-white/20"
layoutId="sidebar-active"
transition={{
type: "spring",
@@ -457,18 +444,18 @@ export default function AdminLayout({
)}
{!isActive && (
<motion.div
className="absolute inset-0 rounded-lg bg-gray-100 dark:bg-gray-900 opacity-0"
className="absolute inset-0 rounded-2xl bg-white/40 dark:bg-white/5 opacity-0"
transition={{ duration: 0.15 }}
whileHover={{ opacity: 1 }}
/>
)}
<div className="flex-shrink-0 w-10 h-10 flex items-center justify-center relative z-10">
<div className="flex-shrink-0 w-6 h-6 flex items-center justify-center relative z-10">
{item.icon}
</div>
<div
className={`transition-all duration-300 overflow-hidden flex items-center ${isCollapsed ? "max-w-0 opacity-0 ml-0" : "max-w-[200px] opacity-100 ml-2"}`}
className={`transition-all duration-300 overflow-hidden flex items-center ${isCollapsed ? "max-w-0 opacity-0 ml-0" : "max-w-[200px] opacity-100 ml-3"}`}
>
<span className="font-medium text-sm relative z-10 whitespace-nowrap">
<span className="relative z-10 whitespace-nowrap">
{item.label}
</span>
</div>
@@ -480,7 +467,7 @@ export default function AdminLayout({
</nav>
{/* 底部版权信息和折叠按钮 */}
<div className="px-5 py-2 pb-4 mt-auto flex-shrink-0 flex items-center justify-between overflow-hidden whitespace-nowrap box-border">
<div className="px-5 py-6 mt-auto flex-shrink-0 flex flex-col gap-4 overflow-hidden whitespace-nowrap box-border">
<div
className={`transition-all duration-300 overflow-hidden flex items-center ${isCollapsed ? "max-w-0 opacity-0" : "max-w-[200px] opacity-100"}`}
>
@@ -496,13 +483,12 @@ export default function AdminLayout({
{!isMobile && (
<Button
isIconOnly
className="flex-shrink-0 text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 min-w-0 w-10 h-10 rounded-full ml-auto"
className="flex-shrink-0 text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 min-w-0 w-10 h-10 rounded-full mx-auto"
size="sm"
variant="light"
onPress={toggleCollapse}
>
{isCollapsed ? (
// 向右扩展的提示
<svg
className="w-5 h-5"
fill="none"
@@ -517,7 +503,6 @@ export default function AdminLayout({
/>
</svg>
) : (
// 向左收起的提示
<svg
className="w-5 h-5"
fill="none"
@@ -539,106 +524,34 @@ export default function AdminLayout({
{/* 主内容区域 */}
<div
className={`flex flex-col flex-1 ${isMobile ? "min-h-0" : "h-full overflow-hidden"}`}
className={`flex flex-col flex-1 ${isMobile ? "min-h-0" : "h-full overflow-hidden"} relative`}
>
{/* 顶部导航栏 */}
<header className="bg-white dark:bg-black shadow-md border-b border-gray-200 dark:border-gray-600 h-14 flex items-center justify-between px-4 lg:px-6 relative z-10">
<div className="flex items-center gap-4">
{/* 移动端菜单按钮 */}
{isMobile && (
<Button
isIconOnly
className="lg:hidden"
variant="light"
onPress={toggleMobileMenu}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M4 6h16M4 12h16M4 18h16"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
</Button>
)}
</div>
<div className="flex items-center gap-3">
{/* 用户菜单 */}
<Dropdown placement="bottom-end">
<DropdownTrigger>
<Button
className="text-sm font-medium text-foreground"
variant="light"
>
{username}
<svg
className="w-4 h-4 ml-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
clipRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
fillRule="evenodd"
/>
</svg>
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="用户菜单">
<DropdownItem
key="change-password"
startContent={
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
clipRule="evenodd"
d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z"
fillRule="evenodd"
/>
</svg>
}
onPress={onOpen}
>
修改密码
</DropdownItem>
<DropdownItem
key="logout"
className="text-danger"
color="danger"
startContent={
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
clipRule="evenodd"
d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z"
fillRule="evenodd"
/>
</svg>
}
onPress={handleLogout}
>
退出登录
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</header>
{/* 移动端菜单按钮 (替代Header) */}
{isMobile && (
<Button
isIconOnly
className="absolute top-4 left-4 z-40 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-md shadow-sm border border-white/80 dark:border-white/10"
variant="flat"
onPress={toggleMobileMenu}
>
<svg
className="w-5 h-5 text-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M4 6h16M4 12h16M4 18h16"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
</Button>
)}
{/* 主内容 */}
<main className="flex-1 bg-gray-100 dark:bg-black overflow-y-auto">
<main className="flex-1 overflow-y-auto [scrollbar-width:none]">
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
+1 -1
View File
@@ -6,7 +6,7 @@ export default function DefaultLayout({
children: React.ReactNode;
}) {
return (
<div className="relative flex flex-col min-h-screen bg-white dark:bg-black">
<div className="relative flex flex-col min-h-screen bg-mesh-gradient">
<Navbar />
<main className="container mx-auto max-w-7xl px-4 sm:px-6 flex-grow pt-4 sm:pt-16">
{children}
+3 -3
View File
@@ -21,9 +21,9 @@ export default function H5SimpleLayout({
};
return (
<div className="flex flex-col min-h-screen bg-gray-100 dark:bg-black">
<div className="flex flex-col min-h-screen bg-mesh-gradient">
{/* 顶部导航栏 */}
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
<header className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-xl shadow-sm border-b border-white/80 dark:border-white/10 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
<div className="flex items-center gap-2">
<Button isIconOnly size="sm" variant="light" onPress={handleBack}>
<BackIcon className="w-5 h-5" />
@@ -38,7 +38,7 @@ export default function H5SimpleLayout({
</header>
{/* 主内容区域 */}
<main className="flex-1 bg-gray-100 dark:bg-black pb-0">{children}</main>
<main className="flex-1 pb-0">{children}</main>
</div>
);
}
+5 -5
View File
@@ -18,7 +18,7 @@ interface TabItem {
export default function H5Layout({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
const location = useLocation();
const [isAdmin, setIsAdmin] = useState(false);
const [isAdmin, setIsAdmin] = useState(() => getAdminFlag());
const [monitorAllowed, setMonitorAllowed] = useState<boolean | null>(null);
const [monitorAccessReason, setMonitorAccessReason] = useState<string | null>(
null,
@@ -166,9 +166,9 @@ export default function H5Layout({ children }: { children: React.ReactNode }) {
);
return (
<div className="flex flex-col min-h-screen bg-gray-100 dark:bg-black">
<div className="flex flex-col min-h-screen bg-mesh-gradient">
{/* 顶部导航栏 */}
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
<header className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-xl shadow-sm border-b border-white/80 dark:border-white/10 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
<div className="flex items-center gap-2">
<BrandLogo size={20} />
<h1 className="text-sm font-bold text-foreground">
@@ -180,13 +180,13 @@ export default function H5Layout({ children }: { children: React.ReactNode }) {
</header>
{/* 主内容区域 */}
<main className="flex-1 bg-gray-100 dark:bg-black">{children}</main>
<main className="flex-1">{children}</main>
{/* 用于给固定 Tabbar 腾出空间的占位元素 */}
<div aria-hidden className="h-[calc(4rem+var(--safe-area-bottom))]" />
{/* 底部Tabbar */}
<nav className="bg-white dark:bg-black border-t border-gray-200 dark:border-gray-600 h-[calc(4rem+var(--safe-area-bottom))] flex-shrink-0 flex items-center justify-around px-2 fixed bottom-0 left-0 right-0 z-30">
<nav className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-2xl border-t border-white/80 dark:border-white/10 h-[calc(4rem+var(--safe-area-bottom))] flex-shrink-0 flex items-center justify-around px-2 fixed bottom-0 left-0 right-0 z-30">
{filteredTabItems.map((item) => {
const isActive = location.pathname === item.path;
const isMonitor = item.path === "/monitor";
+31 -1
View File
@@ -1,12 +1,42 @@
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { registerSW } from "virtual:pwa-register";
import toast from "react-hot-toast";
import App from "./App.tsx";
import { Provider } from "./provider.tsx";
import "@/styles/globals.css";
registerSW({ immediate: true });
const updateSW = registerSW({
immediate: true,
onNeedRefresh() {
toast(
(t) => (
<div className="flex flex-col gap-3">
<span className="text-sm font-medium text-foreground">发现新版本,是否立即刷新以应用更新?</span>
<div className="flex gap-2 justify-end">
<button
className="px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary/90 rounded-md transition-colors"
onClick={() => {
updateSW(true);
toast.dismiss(t.id);
}}
>
刷新
</button>
<button
className="px-3 py-1.5 text-xs font-medium bg-default-200 text-default-700 hover:bg-default-300 rounded-md transition-colors"
onClick={() => toast.dismiss(t.id)}
>
稍后
</button>
</div>
</div>
),
{ duration: Infinity, position: "bottom-right" }
);
},
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<BrowserRouter>
+161 -1
View File
@@ -65,7 +65,7 @@ interface ConfigItem {
label: string;
placeholder?: string;
description?: string;
type: "input" | "switch" | "select";
type: "input" | "switch" | "select" | "bg_image";
options?: { label: string; value: string; description?: string }[];
dependsOn?: string; // 依赖的配置项key
dependsValue?: string; // 依赖的配置项值
@@ -86,6 +86,12 @@ const toBrandAssetKind = (key: BrandPreviewKey): BrandAssetKind => {
// 网站配置项定义
const CONFIG_ITEMS: ConfigItem[] = [
{
key: "app_bg_image",
label: "自定义背景",
description: "上传自定义背景图片(建议使用深色/浅色均可看清的图片,或使用半透明模糊效果)",
type: "bg_image",
},
{
key: "ip",
label: "面板后端地址",
@@ -254,6 +260,8 @@ export default function ConfigPage() {
const logoFileInputRef = useRef<HTMLInputElement>(null);
const faviconFileInputRef = useRef<HTMLInputElement>(null);
const bgImageFileInputRef = useRef<HTMLInputElement>(null);
const [bgImageUploading, setBgImageUploading] = useState(false);
const [announcement, setAnnouncement] = useState<AnnouncementData>({
content: "",
@@ -547,6 +555,155 @@ export default function ConfigPage() {
}
};
const handleBgImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("只能上传图片文件");
return;
}
setBgImageUploading(true);
try {
const compressedImage = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
let width = img.width;
let height = img.height;
const MAX_WIDTH = 1920;
const MAX_HEIGHT = 1080;
if (width > height) {
if (width > MAX_WIDTH) {
height = Math.round((height * MAX_WIDTH) / width);
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width = Math.round((width * MAX_HEIGHT) / height);
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Canvas context is null"));
return;
}
ctx.drawImage(img, 0, 0, width, height);
// Output as webp for better compression
const dataUrl = canvas.toDataURL("image/webp", 0.8);
resolve(dataUrl);
};
img.onerror = () => reject(new Error("图片加载失败"));
img.src = event.target?.result as string;
};
reader.onerror = () => reject(new Error("文件读取失败"));
reader.readAsDataURL(file);
});
handleConfigChange("app_bg_image", compressedImage);
toast.success("自定义背景上传成功");
} catch (error) {
toast.error(error instanceof Error ? error.message : "图片处理失败");
} finally {
setBgImageUploading(false);
e.target.value = "";
}
};
const renderBgImageUploader = () => {
const bgImage = configs["app_bg_image"] || "";
const isImage = bgImage.startsWith("http") || bgImage.startsWith("data:") || bgImage.startsWith("/") || bgImage.startsWith("blob:");
const isTheme = bgImage === "theme";
const isSolidColor = bgImage && !isImage && !isTheme;
return (
<div className="flex flex-col gap-4 w-full">
<div className="flex flex-wrap items-center gap-4">
<input
type="file"
accept="image/*"
ref={bgImageFileInputRef}
className="hidden"
onChange={handleBgImageUpload}
/>
<Button
color="primary"
variant="flat"
onPress={() => bgImageFileInputRef.current?.click()}
isLoading={bgImageUploading}
>
上传图片
</Button>
<Button
color="secondary"
variant="flat"
onPress={() => handleConfigChange("app_bg_image", "theme")}
isDisabled={bgImageUploading || isTheme}
>
自适应纯色 (跟随深色模式)
</Button>
<Button
color="default"
variant="flat"
onPress={() => handleConfigChange("app_bg_image", "#ffffff")}
isDisabled={bgImageUploading || bgImage === "#ffffff"}
>
白色纯色
</Button>
{bgImage && (
<Button
color="danger"
variant="flat"
onPress={() => handleConfigChange("app_bg_image", "")}
isDisabled={bgImageUploading}
>
恢复默认
</Button>
)}
</div>
{bgImage && isImage && (
<div className="relative rounded-xl overflow-hidden border border-divider">
<img
src={bgImage}
alt="背景预览"
className="w-full max-h-48 object-cover opacity-80"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent flex items-end p-4">
<span className="text-white text-sm font-medium">预览效果</span>
</div>
</div>
)}
{bgImage && isTheme && (
<div className="relative rounded-xl border border-divider bg-background h-32 flex items-center justify-center">
<span className="text-foreground text-sm font-medium">当前为自适应纯色背景</span>
</div>
)}
{bgImage && isSolidColor && (
<div
className="relative rounded-xl border border-divider h-32 flex items-center justify-center"
style={{ backgroundColor: bgImage }}
>
<span className="text-gray-500 bg-white/80 dark:bg-black/80 px-2 py-1 rounded text-sm font-medium border border-gray-200 dark:border-gray-800 shadow-sm">
纯色背景 ({bgImage})
</span>
</div>
)}
</div>
);
};
const renderBrandPreview = (key: BrandPreviewKey) => {
const previewUrl = (configs[key] || "").trim();
const appNamePreview = (configs.app_name || "").trim() || "应用名称";
@@ -699,6 +856,9 @@ export default function ConfigPage() {
const isCommercialDisabled = ["app_name", "app_logo", "app_favicon", "hide_footer_brand"].includes(item.key) && configs.is_commercial !== "true";
switch (item.type) {
case "bg_image":
return renderBgImageUploader();
case "input":
if (isBrandPreviewKey(item.key)) {
return renderBrandAssetUploader(item.key, isChanged);
+65 -5
View File
@@ -1,9 +1,16 @@
import { useState } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { AnimatedPage } from "@/components/animated-page";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
} from "@/shadcn-bridge/heroui/dropdown";
import {
Modal,
ModalContent,
@@ -15,6 +22,8 @@ import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-ba
import { AnnouncementModal } from "@/pages/dashboard/components/announcement-modal";
import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card";
import { MetricCard } from "@/pages/dashboard/components/metric-card";
import { getSessionName } from "@/utils/session";
import { safeLogout } from "@/utils/logout";
import {
formatNodeRenewalTime,
getNodeRenewalCycleLabel,
@@ -35,6 +44,15 @@ interface AddressItem {
}
export default function DashboardPage() {
const navigate = useNavigate();
const username = getSessionName() || "User";
const handleLogout = () => {
safeLogout();
toast.success("已退出登录");
navigate("/");
};
const {
loading,
userInfo,
@@ -630,6 +648,48 @@ export default function DashboardPage() {
return (
<AnimatedPage className="px-3 lg:px-6 py-2 lg:py-4">
{/* 顶部个人状态和下拉菜单 */}
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-bold text-foreground">Good morning, {username}</h1>
<p className="text-sm text-default-500 mt-1">Here's what's happening with your network today.</p>
</div>
<div className="flex items-center gap-3">
<Dropdown placement="bottom-end">
<DropdownTrigger>
<Button
className="rounded-full w-10 h-10 min-w-0 bg-primary text-white font-bold text-sm shadow-[0_4px_12px_rgba(0,122,255,0.3)]"
isIconOnly
variant="solid"
>
{username.slice(0, 2).toUpperCase()}
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="用户菜单" className="bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl border border-white/50 dark:border-white/10 rounded-2xl shadow-[0_10px_30px_rgba(0,0,0,0.1)]">
<DropdownItem
key="profile"
startContent={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
}
onPress={() => navigate("/profile")}
>
个人资料
</DropdownItem>
<DropdownItem
key="logout"
className="text-danger"
color="danger"
startContent={
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd" /></svg>
}
onPress={handleLogout}
>
退出登录
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
{announcement && <AnnouncementBanner announcement={announcement} />}
{announcement && (
<AnnouncementModal
@@ -823,7 +883,7 @@ export default function DashboardPage() {
{/* 隧道权限 - 管理员不显示 */}
{!isAdmin && (
<Card className="mb-6 lg:mb-8 border border-gray-200 dark:border-default-200 shadow-md">
<Card className="mb-6 lg:mb-8">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<svg
@@ -945,7 +1005,7 @@ export default function DashboardPage() {
)}
{/* 规则配置 */}
<Card className="border border-gray-200 dark:border-default-200 shadow-md">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<svg
@@ -1007,7 +1067,7 @@ export default function DashboardPage() {
{group.forwards.map((forward) => (
<div
key={forward.id}
className="bg-white dark:bg-default-100/50 border border-gray-200 dark:border-default-200 rounded-lg p-3 hover:shadow-md transition-shadow"
className="bg-white/10 dark:bg-black/10 backdrop-blur-md border border-white/20 dark:border-white/10 rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300"
>
<div className="space-y-3">
<div>
@@ -1016,7 +1076,7 @@ export default function DashboardPage() {
</h4>
<div className="space-y-1">
<button
className={`block px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 rounded font-mono text-xs truncate ${hasMultipleIps(forward.inIp) ? "cursor-pointer hover:bg-green-200 dark:hover:bg-green-500/30" : ""}`}
className={`block px-2 py-1 bg-white/20 dark:bg-black/20 backdrop-blur-md rounded-lg border border-white/20 dark:border-white/10 font-mono text-xs truncate text-foreground transition-all duration-300 ${hasMultipleIps(forward.inIp) ? "cursor-pointer hover:bg-white/30 dark:hover:bg-white/10" : ""}`}
disabled={!hasMultipleIps(forward.inIp)}
title={formatInAddress(
forward.inIp,
@@ -1037,7 +1097,7 @@ export default function DashboardPage() {
↓
</div>
<button
className={`block px-2 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded font-mono text-xs truncate ${hasMultipleRemoteAddresses(forward.remoteAddr) ? "cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-500/30" : ""}`}
className={`block px-2 py-1 bg-white/20 dark:bg-black/20 backdrop-blur-md rounded-lg border border-white/20 dark:border-white/10 font-mono text-xs truncate text-foreground transition-all duration-300 ${hasMultipleRemoteAddresses(forward.remoteAddr) ? "cursor-pointer hover:bg-white/30 dark:hover:bg-white/10" : ""}`}
disabled={
!hasMultipleRemoteAddresses(
forward.remoteAddr,
@@ -1,5 +1,4 @@
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
@@ -28,7 +27,7 @@ export const FlowChartCard = ({
formatFlow,
}: FlowChartCardProps) => {
return (
<Card className="mb-6 lg:mb-8 border border-gray-200 dark:border-default-200 shadow-md">
<Card className="mb-6 lg:mb-8">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<svg
@@ -51,17 +50,16 @@ export const FlowChartCard = ({
) : (
<div className="h-64 lg:h-80 w-full">
<ResponsiveContainer height="100%" width="100%">
<LineChart data={chartData}>
<CartesianGrid className="opacity-30" strokeDasharray="3 3" />
<LineChart data={chartData} margin={{ top: 20, right: 0, left: -20, bottom: 0 }}>
<XAxis
axisLine={{ stroke: "#e5e7eb", strokeWidth: 1 }}
axisLine={false}
dataKey="time"
tick={{ fontSize: 12 }}
tick={{ fill: "currentColor", opacity: 0.5, fontSize: 12 }}
tickLine={false}
/>
<YAxis
axisLine={{ stroke: "#e5e7eb", strokeWidth: 1 }}
tick={{ fontSize: 12 }}
axisLine={false}
tick={{ fill: "currentColor", opacity: 0.5, fontSize: 12 }}
tickFormatter={(value: number | string) => {
const v = typeof value === "number" ? value : Number(value);
@@ -97,27 +95,20 @@ export const FlowChartCard = ({
: 0;
return (
<div className="bg-white dark:bg-default-100 border border-default-200 rounded-lg shadow-lg p-3">
<div className="bg-white/20 dark:bg-black/20 backdrop-blur-3xl border border-white/50 rounded-2xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-4">
<p className="font-medium text-foreground">{`时间: ${label ?? ""}`}</p>
<p className="text-primary">{`流量: ${formatFlow(flowValue)}`}</p>
<p className="text-primary font-semibold mt-1">{`流量: ${formatFlow(flowValue)}`}</p>
</div>
);
}
return null;
}}
cursor={{ fill: "rgba(0, 122, 255, 0.05)" }}
/>
<Line
activeDot={{
r: 4,
stroke: "#8b5cf6",
strokeWidth: 2,
fill: "#fff",
}}
dataKey="flow"
dot={false}
stroke="#8b5cf6"
strokeWidth={3}
stroke="#007aff"
type="monotone"
/>
</LineChart>
@@ -18,22 +18,24 @@ export const MetricCard = ({
bottomContent,
}: MetricCardProps) => {
return (
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
<CardBody className="p-3 lg:p-4">
<div className="flex flex-col space-y-2">
<Card className="h-48 flex flex-col justify-between overflow-hidden">
<CardBody className="!p-4 md:!p-6 h-full flex flex-col justify-between overflow-visible">
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between">
<p className="text-xs lg:text-sm text-default-600 truncate">
<p className="text-sm font-medium text-default-600 truncate">
{title}
</p>
<div
className={`p-1.5 lg:p-2 rounded-lg flex-shrink-0 ${iconClassName}`}
className={`p-2 rounded-xl flex-shrink-0 ${iconClassName}`}
>
{icon}
</div>
</div>
<p className="text-base lg:text-xl font-bold text-foreground truncate">
<p className="text-2xl lg:text-3xl font-bold text-foreground truncate mt-2">
{value}
</p>
</div>
<div className="mt-auto pt-4">
{bottomContent}
</div>
</CardBody>
+24 -23
View File
@@ -3689,7 +3689,7 @@ export default function ForwardPage() {
return (
<Card
key={forward.id}
className="group h-full flex flex-col shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden"
className="group h-full flex flex-col overflow-hidden bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
>
<CardHeader className="pb-2 md:pb-2">
<div className="flex justify-between items-start w-full">
@@ -3746,7 +3746,7 @@ export default function ForwardPage() {
{/* 地址信息 */}
<div className="space-y-1">
<button
className={`cursor-pointer px-2 py-1 bg-default-50 dark:bg-default-100/50 rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
className={`cursor-pointer px-2 py-1 bg-white/20 dark:bg-black/20 backdrop-blur-md rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
hasMultipleAddresses(forward.inIp)
? "hover:bg-default-100 dark:hover:bg-default-200/50"
: ""
@@ -3786,7 +3786,7 @@ export default function ForwardPage() {
</button>
<button
className={`cursor-pointer px-2 py-1 bg-default-50 dark:bg-default-100/50 rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
className={`cursor-pointer px-2 py-1 bg-white/20 dark:bg-black/20 backdrop-blur-md rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
hasMultipleAddresses(forward.remoteAddr)
? "hover:bg-default-100 dark:hover:bg-default-200/50"
: ""
@@ -3966,7 +3966,7 @@ export default function ForwardPage() {
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between mb-6 gap-3">
<div className="flex-1 flex items-center gap-2">
<Button
color={activeFilterCount > 0 ? "primary" : "default"}
className="bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 font-semibold hover:bg-default/20 dark:hover:bg-default/30"
startContent={
<svg
className="w-4 h-4"
@@ -3982,7 +3982,6 @@ export default function ForwardPage() {
/>
</svg>
}
variant={activeFilterCount > 0 ? "flat" : "bordered"}
onPress={() => setIsSearchModalOpen(true)}
>
高级筛选 {activeFilterCount > 0 ? `(${activeFilterCount})` : ""}
@@ -4177,7 +4176,7 @@ export default function ForwardPage() {
{sortedForwards.length} 条规则
</span>
</div>
<div className="overflow-hidden rounded-xl border border-divider bg-content1 shadow-md">
<Card className="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
<DndContext
collisionDetection={pointerWithin}
sensors={sensors}
@@ -4190,9 +4189,10 @@ export default function ForwardPage() {
<Table
aria-label="全部规则列表"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/10 dark:hover:bg-white/5 transition-colors group",
}}
>
<TableHeader>
@@ -4253,10 +4253,10 @@ export default function ForwardPage() {
</Table>
</SortableContext>
</DndContext>
</div>
</Card>
</>
) : (
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
暂无规则配置
@@ -4302,7 +4302,7 @@ export default function ForwardPage() {
</DndContext>
</>
) : (
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
暂无规则配置
@@ -4327,9 +4327,9 @@ export default function ForwardPage() {
return (
<div
key={`grouped-table-${group.userId}-${group.userName}`}
className="overflow-hidden rounded-xl border border-divider bg-content1 shadow-md"
className="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
>
<div className="flex items-center justify-between border-b border-divider bg-default-100/40 px-4 py-3">
<div className="flex items-center justify-between border-b border-white/20 dark:border-white/10 bg-white/20 dark:bg-black/20 backdrop-blur-3xl px-5 py-4">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-foreground">
{group.userName}
@@ -4415,10 +4415,10 @@ export default function ForwardPage() {
collapsed={collapsed}
countClassName="text-xs text-default-600"
groupUserId={group.userId}
headerClassName="flex items-center justify-between border-b border-divider bg-default-100/60 px-4 py-2.5"
headerClassName="flex items-center justify-between border-b border-white/20 dark:border-white/10 bg-white/20 dark:bg-black/20 backdrop-blur-3xl px-4 py-2.5"
titleClassName="truncate text-sm font-semibold text-default-700"
tunnel={tunnel}
wrapperClassName="overflow-hidden rounded-lg border border-divider bg-default-50/60"
wrapperClassName="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
onToggleCollapsed={() =>
toggleTunnelGroupCollapsed(
group.userId,
@@ -4439,9 +4439,10 @@ export default function ForwardPage() {
aria-label={`${group.userName}-${tunnel.tunnelName}规则列表`}
className={`table-fixed ${FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS}`}
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
@@ -4561,7 +4562,7 @@ export default function ForwardPage() {
})}
</div>
) : (
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
暂无规则配置
@@ -4637,10 +4638,10 @@ export default function ForwardPage() {
collapsed={collapsed}
countClassName="text-xs text-default-600"
groupUserId={group.userId}
headerClassName="flex items-center justify-between rounded-lg bg-default-100/60 px-3 py-2"
headerClassName="flex items-center justify-between rounded-lg bg-white/10 dark:bg-black/10 backdrop-blur-md border border-white/20 dark:border-white/10 px-3 py-2"
titleClassName="truncate text-sm font-semibold text-default-700"
tunnel={tunnel}
wrapperClassName="rounded-xl border border-divider bg-default-50/60 space-y-3"
wrapperClassName="rounded-xl border border-divider bg-white/5 dark:bg-black/5 space-y-3"
onToggleCollapsed={() =>
toggleTunnelGroupCollapsed(
group.userId,
@@ -4682,7 +4683,7 @@ export default function ForwardPage() {
})}
</div>
) : (
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
<Card>
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
暂无规则配置
@@ -5415,7 +5416,7 @@ export default function ForwardPage() {
</div>
)}
</ModalHeader>
<ModalBody className="bg-content1">
<ModalBody className="bg-transparent">
{diagnosisResult ? (
<div className="space-y-4">
{diagnosisLoading && (
@@ -5939,7 +5940,7 @@ export default function ForwardPage() {
</div>
)}
</ModalBody>
<ModalFooter className="bg-content1 border-t border-divider">
<ModalFooter className="bg-transparent border-t border-white/20 dark:border-white/10">
<Button variant="light" onPress={onClose}>
关闭
</Button>
+39 -13
View File
@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { AnimatedPage } from "@/components/animated-page";
import { PageLoadingState } from "@/components/page-state";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Input } from "@/shadcn-bridge/heroui/input";
@@ -22,7 +24,7 @@ import {
TableRow,
} from "@/shadcn-bridge/heroui/table";
import { Chip } from "@/shadcn-bridge/heroui/chip";
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
import {
assignGroupPermission,
assignTunnelsToGroup,
@@ -469,14 +471,12 @@ export default function GroupPage() {
}
return (
<div className="px-3 lg:px-6 py-8 space-y-6">
{loading && (
<div className="flex justify-center py-10">
<Spinner size="lg" />
</div>
)}
<Card>
<AnimatedPage className="px-3 lg:px-6 py-8 space-y-6">
{loading ? (
<PageLoadingState message="正在加载..." />
) : (
<>
<Card>
<CardHeader className="flex flex-row items-center gap-3 pb-2">
<h3 className="text-lg font-semibold">隧道分组</h3>
<Button
@@ -489,7 +489,15 @@ export default function GroupPage() {
</Button>
</CardHeader>
<CardBody>
<Table aria-label="隧道分组列表">
<Table
aria-label="隧道分组列表"
classNames={{
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
<TableColumn>名称</TableColumn>
<TableColumn>隧道</TableColumn>
@@ -561,7 +569,15 @@ export default function GroupPage() {
</Button>
</CardHeader>
<CardBody>
<Table aria-label="用户分组列表">
<Table
aria-label="用户分组列表"
classNames={{
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
<TableColumn>名称</TableColumn>
<TableColumn>用户</TableColumn>
@@ -665,7 +681,15 @@ export default function GroupPage() {
</Button>
</div>
<Table aria-label="分组权限列表">
<Table
aria-label="分组权限列表"
classNames={{
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
<TableColumn>ID</TableColumn>
<TableColumn>用户分组</TableColumn>
@@ -898,6 +922,8 @@ export default function GroupPage() {
</ModalFooter>
</ModalContent>
</Modal>
</div>
</>
)}
</AnimatedPage>
);
}
+26 -20
View File
@@ -8,9 +8,8 @@ import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Button } from "@/shadcn-bridge/heroui/button";
import { siteConfig } from "@/config/site";
import { title } from "@/components/primitives";
import { VersionFooter } from "@/components/version-footer";
import DefaultLayout from "@/layouts/default";
import { BrandLogo } from "@/components/brand-logo";
import { login, LoginData, checkCaptcha, getConfigByName } from "@/api";
import { writeLoginSession } from "@/utils/session";
import { useWebViewMode } from "@/hooks/useWebViewMode";
@@ -152,29 +151,34 @@ export default function IndexPage() {
};
return (
<DefaultLayout>
<section className="flex flex-col items-center justify-center gap-4 py-4 sm:py-8 md:py-10 pb-20 min-h-[calc(100dvh-120px)] sm:min-h-[calc(100dvh-200px)]">
<div className="relative flex flex-col min-h-screen bg-mesh-gradient overflow-hidden">
<section className="flex flex-col items-center justify-center flex-1 w-full p-4 relative z-10">
<motion.div
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md px-4 sm:px-0"
className="w-full max-w-[420px] px-4 sm:px-0"
initial={{ opacity: 0, y: 24 }}
transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<Card className="w-full">
<Card className="w-full bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_20px_40px_rgba(0,0,0,0.15)] border-white/80 dark:border-white/10 rounded-[32px] p-2 sm:p-4">
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
<h1 className={title({ size: "sm" })}>登陆</h1>
<p className="text-small text-default-500 mt-2">
请输入您的账号信息
<BrandLogo className="w-14 h-14 rounded-2xl mb-4" size={56} />
<h1 className="text-2xl font-bold tracking-tight text-foreground">{siteConfig.name}</h1>
<p className="text-sm text-default-500 mt-2 font-medium">
Sign in to manage your networks
</p>
</CardHeader>
<CardBody className="px-6 py-6">
<div className="flex flex-col gap-4">
<CardBody className="px-6 py-6 mt-2">
<div className="flex flex-col gap-5">
<Input
classNames={{
inputWrapper: "bg-white/50 dark:bg-black/50 backdrop-blur-md border border-white/60 dark:border-white/10 h-12 shadow-sm rounded-xl",
input: "text-base font-medium",
}}
errorMessage={errors.username}
isDisabled={loading}
isInvalid={!!errors.username}
label="用户名"
placeholder="请输入用户名"
label="Username"
placeholder="admin"
value={form.username}
variant="bordered"
onChange={(e) =>
@@ -184,10 +188,14 @@ export default function IndexPage() {
/>
<Input
classNames={{
inputWrapper: "bg-white/50 dark:bg-black/50 backdrop-blur-md border border-white/60 dark:border-white/10 h-12 shadow-sm rounded-xl",
input: "text-base font-medium tracking-wider",
}}
isDisabled={loading}
isInvalid={!!errors.password}
label="密码"
placeholder="请输入密码"
label="Password"
placeholder="••••••••"
type="password"
value={form.password}
variant="bordered"
@@ -198,14 +206,12 @@ export default function IndexPage() {
/>
<Button
className="mt-2"
color="primary"
className="mt-4 h-12 rounded-xl bg-primary text-white font-bold text-base shadow-[0_8px_16px_rgba(0,122,255,0.3)] transition-transform active:scale-[0.98]"
disabled={loading}
isLoading={loading}
size="lg"
onPress={handleLogin}
>
{loading ? (showCaptcha ? "验证中..." : "登录中...") : "登录"}
{loading ? (showCaptcha ? "Verifying..." : "Signing in...") : "Sign In"}
</Button>
</div>
</CardBody>
@@ -275,6 +281,6 @@ export default function IndexPage() {
</div>
)}
</section>
</DefaultLayout>
</div>
);
}
+5 -4
View File
@@ -267,9 +267,10 @@ export default function LimitPage() {
aria-label="限速规则列表"
className="overflow-x-auto min-w-full"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
@@ -326,7 +327,7 @@ export default function LimitPage() {
<StaggerList className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{filteredRules.map((rule) => (
<StaggerItem key={rule.id}>
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden h-full">
<Card className="shadow-sm overflow-hidden h-full">
<CardHeader className="pb-2 md:pb-2">
<div className="flex justify-between items-start w-full">
<div>
@@ -412,7 +413,7 @@ export default function LimitPage() {
)
) : (
/* 空状态 */
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
<Card className="shadow-sm bg-default-50/50">
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
暂无限速规则
+150 -11
View File
@@ -10,6 +10,7 @@ import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { getMonitorNodes } from "@/api";
import { MonitorView } from "@/pages/node/monitor-view";
import { TunnelMonitorView } from "@/pages/node/tunnel-monitor-view";
import { useNodeRealtime } from "@/pages/node/use-node-realtime";
type MonitorNode = {
id: number;
@@ -20,6 +21,16 @@ type MonitorNode = {
type MonitorTab = "nodes" | "tunnels";
const formatBytesPerSecond = (bytesPerSecond: number): string => {
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "0 B/s";
const k = 1024;
const sizes = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"];
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
return `${parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
export default function MonitorPage() {
const [nodes, setNodes] = useState<MonitorNodeApiItem[]>([]);
const [nodesLoading, setNodesLoading] = useState(false);
@@ -27,6 +38,55 @@ export default function MonitorPage() {
const [viewMode, setViewMode] = useState<"list" | "grid">("list");
const [activeTab, setActiveTab] = useState<MonitorTab>("nodes");
const [realtimeNodeStatus, setRealtimeNodeStatus] = useState<Record<number, "online" | "offline">>({});
const [realtimeNodeMetrics, setRealtimeNodeMetrics] = useState<Record<number, any>>({});
const handleRealtimeMessage = useCallback((message: any) => {
const nodeId = Number(message?.id ?? 0);
if (!nodeId || Number.isNaN(nodeId)) return;
const type = String(message?.type ?? "");
const payload = message?.data;
if (type === "status") {
const status = Number(payload);
setRealtimeNodeStatus((prev) => ({
...prev,
[nodeId]: status === 1 ? "online" : "offline",
}));
return;
}
if (type === "metric") {
let raw = payload;
if (typeof raw === "string") {
try {
raw = JSON.parse(raw);
} catch {
return;
}
}
if (!raw || typeof raw !== "object") return;
const metric = raw as any;
setRealtimeNodeMetrics((prev) => ({
...prev,
[nodeId]: {
cpuUsage: Number(metric.cpuUsage ?? metric.cpu_usage ?? 0),
netInSpeed: Number(metric.netInSpeed ?? metric.net_in_speed ?? 0),
netOutSpeed: Number(metric.netOutSpeed ?? metric.net_out_speed ?? 0),
tcpConns: Number(metric.tcpConns ?? metric.tcp_conns ?? 0),
udpConns: Number(metric.udpConns ?? metric.udp_conns ?? 0),
}
}));
}
}, []);
useNodeRealtime({
onMessage: handleRealtimeMessage,
enabled: true,
});
const loadNodes = useCallback(async (options?: { silent?: boolean }) => {
const silent = options?.silent ?? false;
if (!silent) setNodesLoading(true);
@@ -73,22 +133,101 @@ export default function MonitorPage() {
.map((n) => ({
id: Number(n.id),
name: String(n.name ?? ""),
connectionStatus: n.status === 1 ? "online" : "offline",
connectionStatus: realtimeNodeStatus[Number(n.id)] || (n.status === 1 ? "online" : "offline"),
version: n.version,
}));
return new Map<number, MonitorNode>(list.map((n) => [n.id, n]));
}, [nodes]);
}, [nodes, realtimeNodeStatus]);
const aggregateMetrics = useMemo(() => {
let totalCpu = 0;
let totalConns = 0;
let totalNetIn = 0;
let totalNetOut = 0;
let onlineCount = 0;
nodeMap.forEach((node) => {
if (node.connectionStatus === "online") {
onlineCount++;
const metric = realtimeNodeMetrics[node.id];
if (metric) {
totalCpu += metric.cpuUsage;
totalConns += metric.tcpConns + metric.udpConns;
totalNetIn += metric.netInSpeed;
totalNetOut += metric.netOutSpeed;
}
}
});
const avgCpu = onlineCount > 0 ? totalCpu / onlineCount : 0;
return {
avgCpu,
totalConns,
totalBandwidth: totalNetIn + totalNetOut,
};
}, [nodeMap, realtimeNodeMetrics]);
return (
<AnimatedPage className="px-3 lg:px-6 py-8">
{/* 顶部英雄数据指标 (Hero Metrics) */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
<div className="flex justify-between items-center z-10 relative">
<span className="text-default-600 font-medium text-sm">System Load</span>
</div>
<div className="z-10 relative">
<span className="text-4xl font-bold text-foreground">{aggregateMetrics.avgCpu.toFixed(1)}%</span>
</div>
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
<div className="w-full bg-primary/40 h-2 rounded-t-sm" />
<div className="w-full bg-primary/40 h-3 rounded-t-sm" />
<div className="w-full bg-primary/40 h-1.5 rounded-t-sm" />
<div className="w-full bg-primary/40 h-4 rounded-t-sm" />
<div className="w-full bg-primary/40 h-2.5 rounded-t-sm" />
<div className="w-full bg-primary h-5 rounded-t-sm shadow-[0_0_10px_rgba(0,122,255,0.5)]" />
</div>
</div>
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
<div className="flex justify-between items-center z-10 relative">
<span className="text-default-600 font-medium text-sm">Active Connections</span>
</div>
<div className="z-10 relative">
<span className="text-4xl font-bold text-foreground">{aggregateMetrics.totalConns}</span>
</div>
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
<div className="w-full bg-success/40 h-3.5 rounded-t-sm" />
<div className="w-full bg-success/40 h-4.5 rounded-t-sm" />
<div className="w-full bg-success/40 h-2.5 rounded-t-sm" />
<div className="w-full bg-success/40 h-6 rounded-t-sm" />
<div className="w-full bg-success/40 h-5.5 rounded-t-sm" />
<div className="w-full bg-success h-7 rounded-t-sm shadow-[0_0_10px_rgba(52,199,89,0.5)]" />
</div>
</div>
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
<div className="flex justify-between items-center z-10 relative">
<span className="text-default-600 font-medium text-sm">Bandwidth</span>
</div>
<div className="z-10 relative">
<span className="text-4xl font-bold text-foreground">{formatBytesPerSecond(aggregateMetrics.totalBandwidth)}</span>
</div>
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
<div className="w-full bg-secondary/40 h-2 rounded-t-sm" />
<div className="w-full bg-secondary/40 h-3 rounded-t-sm" />
<div className="w-full bg-secondary/40 h-5 rounded-t-sm" />
<div className="w-full bg-secondary/40 h-8 rounded-t-sm" />
<div className="w-full bg-secondary/40 h-7 rounded-t-sm" />
<div className="w-full bg-secondary h-10 rounded-t-sm shadow-[0_0_10px_rgba(175,82,222,0.5)]" />
</div>
</div>
</div>
<div className="mb-6 space-y-3">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<h2 className="text-xl font-semibold truncate">监控</h2>
<div className="text-xs text-default-500 truncate">
实时节点状态 + 隧道质量检测 + 历史指标图表 + 服务监控(TCP/ICMP)
</div>
<h2 className="text-xl font-semibold truncate">监控面板</h2>
</div>
<div className="flex items-center gap-2">
<Button
@@ -114,11 +253,11 @@ export default function MonitorPage() {
</div>
{/* Tab Switcher */}
<div className="flex items-center gap-1 p-1 rounded-xl bg-default-100 dark:bg-default-50/10 w-fit">
<div className="flex items-center gap-1 p-1 rounded-2xl bg-white/20 dark:bg-black/20 backdrop-blur-3xl border border-white/50 dark:border-white/10 w-fit shadow-sm">
<button
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${
activeTab === "nodes"
? "bg-background shadow-sm text-foreground"
? "bg-white dark:bg-zinc-800 shadow-[0_2px_8px_rgba(0,0,0,0.08)] text-foreground"
: "text-default-500 hover:text-foreground"
}`}
onClick={() => setActiveTab("nodes")}
@@ -127,9 +266,9 @@ export default function MonitorPage() {
节点
</button>
<button
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${
activeTab === "tunnels"
? "bg-background shadow-sm text-foreground"
? "bg-white dark:bg-zinc-800 shadow-[0_2px_8px_rgba(0,0,0,0.08)] text-foreground"
: "text-default-500 hover:text-foreground"
}`}
onClick={() => setActiveTab("tunnels")}
+18 -10
View File
@@ -288,7 +288,7 @@ const SortableItem = ({
export default function NodePage() {
const [nodeList, setNodeList] = useState<Node[]>([]);
const [nodeOrder, setNodeOrder] = useState<number[]>([]);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [localSearchKeyword, setLocalSearchKeyword] = useLocalStorageState(
"node-search-keyword-local",
"",
@@ -1767,9 +1767,10 @@ export default function NodePage() {
aria-label="节点列表"
className="overflow-x-auto min-w-full"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/10 dark:hover:bg-white/5 transition-colors",
}}
>
<TableHeader>
@@ -1949,7 +1950,7 @@ export default function NodePage() {
{(listeners) => (
<Card
key={node.id}
className={`group relative overflow-visible shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 h-full flex flex-col ${node.expiryReminderDismissed ? "" : expiryMeta.accentClassName}`}
className={`group relative overflow-visible h-full flex flex-col ${node.expiryReminderDismissed ? "" : expiryMeta.accentClassName}`}
data-node-card="true"
>
<CardHeader className="pb-3 md:pb-3">
@@ -1979,13 +1980,20 @@ export default function NodePage() {
)}
<div className="min-w-0 flex-1 pt-0.5">
<div className="flex items-center gap-2 min-w-0">
<h3 className="font-semibold text-foreground truncate text-sm leading-5">
<h3 className="font-semibold text-foreground truncate text-lg leading-5">
{node.name}
</h3>
<span
className={`h-2.5 w-2.5 rounded-full flex-shrink-0 ${connectionStatusMeta.color === "success" ? "bg-emerald-500" : "bg-rose-500"}`}
<div
className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium flex-shrink-0
${connectionStatusMeta.color === "success" ? "bg-emerald-500/20 text-emerald-600" : "bg-rose-500/20 text-rose-600"}
`}
title={connectionStatusMeta.text}
/>
>
<span
className={`h-1.5 w-1.5 rounded-full ${connectionStatusMeta.color === "success" ? "bg-emerald-500" : "bg-rose-500"}`}
/>
{connectionStatusMeta.text}
</div>
</div>
</div>
</div>
@@ -2241,7 +2249,7 @@ export default function NodePage() {
)}
</span>
</div>
<div className="max-h-20 overflow-y-auto rounded bg-white/70 dark:bg-black/20 p-1.5 [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
<div className="max-h-20 overflow-y-auto rounded bg-white/10 dark:bg-black/10 p-1.5 [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
{remoteUsage.usedPorts.length > 0 ? (
<div className="flex flex-wrap gap-1">
{remoteUsage.usedPorts.map((port) => (
@@ -2277,7 +2285,7 @@ export default function NodePage() {
remoteUsage.bindings.map((binding) => (
<div
key={binding.bindingId}
className="rounded border border-default-200 dark:border-default-100/30 bg-white/70 dark:bg-black/20 p-2"
className="rounded border border-default-200 dark:border-default-100/30 bg-white/10 dark:bg-black/10 p-2"
>
<div className="flex items-center justify-between gap-2">
<span
+11 -8
View File
@@ -174,7 +174,7 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
return (
<Card
className="group relative overflow-hidden shadow-sm border border-divider dark:border-default-100 hover:-translate-y-1 hover:shadow-lg transition-all duration-300 h-full flex flex-col cursor-pointer bg-background"
className="group relative hover:-translate-y-1 hover:shadow-[0_20px_40px_rgba(0,0,0,0.15)] transition-all duration-300 h-full flex flex-col cursor-pointer"
onClick={onPress}
>
{/* Dynamic top gradient bar based on status */}
@@ -208,8 +208,8 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
<span className="font-mono font-medium">{isOnline && metric ? `${metric.cpuUsage.toFixed(1)}%` : "-"}</span>
</div>
<Progress
className="h-1"
color={getColorByUsage(metric?.cpuUsage)}
size="sm"
value={isOnline && metric ? metric.cpuUsage : 0}
/>
</div>
@@ -219,6 +219,7 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
<span className="font-mono font-medium">{isOnline && metric ? `${metric.memoryUsage.toFixed(1)}%` : "-"}</span>
</div>
<Progress
className="h-1"
color={getColorByUsage(metric?.memoryUsage)}
size="sm"
value={isOnline && metric ? metric.memoryUsage : 0}
@@ -1157,9 +1158,10 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
aria-label="节点列表"
className="overflow-x-auto min-w-full"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
@@ -1321,7 +1323,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
{ label: "↑ 上行速度", value: formatBytesPerSecond(detailRealtimeMetric.netOutSpeed), color: "primary" as const },
{ label: "运行时间", value: formatUptime(detailRealtimeMetric.uptime), color: "default" as const },
].map((item) => (
<Card key={item.label} className="border border-divider/60 shadow-sm hover:shadow-md transition-shadow bg-gradient-to-br from-background to-default-50/50">
<Card key={item.label} className="hover:shadow-[0_15px_40px_rgba(0,0,0,0.15)] transition-shadow">
<CardBody className="py-3 px-4 flex flex-col items-center justify-center min-h-[5rem]">
<span className="text-[11px] text-default-500 mb-1.5">{item.label}</span>
<span className={`text-sm font-semibold font-mono ${item.color === 'danger' ? 'text-danger' : item.color === 'warning' ? 'text-warning' : item.color === 'success' ? 'text-success' : item.color === 'primary' ? 'text-primary' : ''}`}>
@@ -1444,7 +1446,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
{/* Active monitor info bar */}
{resolvedActiveMonitor && (
<div className="flex items-center justify-between gap-3 p-3 rounded-lg bg-default-50 dark:bg-default-100/30 border border-divider">
<div className="flex items-center justify-between gap-3 p-3 rounded-2xl bg-white/20 dark:bg-black/20 backdrop-blur-3xl border border-white/50 dark:border-white/10 shadow-[0_4px_12px_rgba(0,0,0,0.05)]">
<div className="flex items-center gap-3 min-w-0 flex-wrap">
<Chip size="sm" color="primary" variant="flat">{resolvedActiveMonitor.type.toUpperCase()}</Chip>
<span className="font-mono text-xs text-default-500">{resolvedActiveMonitor.target}</span>
@@ -1584,9 +1586,10 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
aria-label="监控记录"
className="w-full overflow-x-auto"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
@@ -987,9 +987,10 @@ export function TunnelMonitorView({ viewMode = "grid" }: TunnelMonitorViewProps)
aria-label="隧道列表"
className="overflow-x-auto min-w-full"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
+3 -3
View File
@@ -89,7 +89,7 @@ export default function PanelSharingPage() {
[],
);
const [nodes, setNodes] = useState<Node[]>([]);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [remoteUsageLoading, setRemoteUsageLoading] = useState(false);
// Modals
@@ -459,7 +459,7 @@ export default function PanelSharingPage() {
{shares.map((share) => (
<Card
key={share.id}
className="border border-divider shadow-sm"
className="overflow-hidden"
>
<CardHeader className="flex justify-between pb-2 md:pb-2">
<h3 className="font-bold">{share.name}</h3>
@@ -606,7 +606,7 @@ export default function PanelSharingPage() {
{remoteUsageNodes.map((node) => (
<Card
key={node.nodeId}
className="border border-divider shadow-sm"
className="overflow-hidden"
>
<CardHeader className="flex justify-between pb-2 md:pb-2">
<h3 className="font-bold">{node.nodeName}</h3>
+2 -2
View File
@@ -202,7 +202,7 @@ export default function ProfilePage() {
<div className="px-3 lg:px-6 py-8 flex flex-col h-full">
<div className="space-y-6 flex-1">
{/* 用户信息卡片 */}
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
<Card>
<CardBody className="p-4">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
@@ -242,7 +242,7 @@ export default function ProfilePage() {
</Card>
{/* 功能网格 */}
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
<Card>
<CardBody className="p-4">
<div className="grid grid-cols-3 gap-3">
{/* 管理员功能 */}
-341
View File
@@ -1,341 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { Input } from "@/shadcn-bridge/heroui/input";
import { Button } from "@/shadcn-bridge/heroui/button";
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
import { Switch } from "@/shadcn-bridge/heroui/switch";
import { reinitializeBaseURL } from "@/api/network";
import { getConfigByName, updateConfig } from "@/api";
import { BackIcon } from "@/components/icons";
import {
type UpdateReleaseChannel,
getUpdateReleaseChannel,
setUpdateReleaseChannel,
} from "@/utils/version-update";
import { isAdmin } from "@/utils/auth";
import {
getPanelAddresses,
savePanelAddress,
setCurrentPanelAddress,
deletePanelAddress,
validatePanelAddress,
} from "@/utils/panel";
interface PanelAddress {
name: string;
address: string;
inx: boolean;
}
const FORWARD_COMPACT_MODE_CONFIG_KEY = "forward_compact_mode";
const parseBooleanConfig = (value: unknown, defaultValue: boolean) =>
typeof value === "string" ? value === "true" : defaultValue;
export const SettingsPage = () => {
const navigate = useNavigate();
const [panelAddresses, setPanelAddresses] = useState<PanelAddress[]>([]);
const [newName, setNewName] = useState("");
const [newAddress, setNewAddress] = useState("");
const [updateChannel, setUpdateChannel] = useState<UpdateReleaseChannel>(
getUpdateReleaseChannel(),
);
const [forwardCompactMode, setForwardCompactMode] = useState(false);
const [forwardCompactModeSaving, setForwardCompactModeSaving] =
useState(false);
const admin = isAdmin();
const setPanelAddressesFunc = (newAddress: PanelAddress[]) => {
setPanelAddresses(newAddress);
};
useEffect(() => {
(window as any).setPanelAddresses = setPanelAddressesFunc;
return () => {
delete (window as any).setPanelAddresses;
};
}, []);
// 加载面板地址列表
const loadPanelAddresses = async () => {
getPanelAddresses();
};
// 添加新面板地址
const addPanelAddress = async () => {
if (!newName.trim() || !newAddress.trim()) {
toast.error("请输入名称和地址");
return;
}
// 验证地址格式
if (!validatePanelAddress(newAddress.trim())) {
toast.error(
"地址格式不正确,请检查:\n• 必须是完整的URL格式\n• 必须以 http:// 或 https:// 开头\n• 支持域名、IPv4、IPv6 地址\n• 端口号范围:1-65535\n• 示例:http://192.168.1.100:3000",
);
return;
}
savePanelAddress(newName.trim(), newAddress.trim());
setNewName("");
setNewAddress("");
toast.success("添加成功");
};
// 设置当前面板地址
const setCurrentPanel = async (name: string) => {
setCurrentPanelAddress(name);
reinitializeBaseURL();
};
// 删除面板地址
const handleDeletePanelAddress = async (name: string) => {
deletePanelAddress(name);
reinitializeBaseURL();
toast.success("删除成功");
};
// 页面加载时获取数据
useEffect(() => {
loadPanelAddresses();
loadForwardCompactMode();
}, []);
const loadForwardCompactMode = async () => {
try {
const res = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY);
setForwardCompactMode(parseBooleanConfig(res.data?.value, false));
} catch {
setForwardCompactMode(false);
}
};
const handleForwardCompactModeChange = async (enabled: boolean) => {
if (!admin || forwardCompactModeSaving) {
return;
}
const previous = forwardCompactMode;
setForwardCompactMode(enabled);
setForwardCompactModeSaving(true);
try {
const response = await updateConfig(
FORWARD_COMPACT_MODE_CONFIG_KEY,
enabled ? "true" : "false",
);
if (response.code === 0) {
toast.success(`规则页面精简模式已${enabled ? "开启" : "关闭"}`);
window.dispatchEvent(
new CustomEvent("forwardCompactModeChanged", {
detail: { enabled },
}),
);
} else {
setForwardCompactMode(previous);
toast.error(response.msg || "保存精简模式失败");
}
} catch {
setForwardCompactMode(previous);
toast.error("保存精简模式失败");
} finally {
setForwardCompactModeSaving(false);
}
};
const handleUpdateChannelChange = (channel: UpdateReleaseChannel) => {
setUpdateChannel(channel);
setUpdateReleaseChannel(channel);
toast.success(
`更新通道已切换为${channel === "stable" ? "稳定版" : "开发版"}`,
);
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-black">
{/* 顶部导航 */}
<div className="bg-white dark:bg-black border-b border-gray-200 dark:border-gray-700">
<div className="max-w-4xl mx-auto px-4 py-4">
<div className="flex items-center gap-3">
<Button
isIconOnly
aria-label="返回上一页"
className="text-gray-600 dark:text-gray-300"
variant="light"
onPress={() => navigate(-1)}
>
<BackIcon className="w-5 h-5" />
</Button>
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
面板设置
</h1>
</div>
</div>
</div>
{/* 内容区域 */}
<div className="max-w-4xl mx-auto px-4 py-6">
<div className="space-y-6">
<Card className="border border-gray-200 dark:border-gray-700">
<CardBody className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
更新设置
</h2>
<div className="space-y-2">
<Select
label="更新通道"
selectedKeys={[updateChannel]}
onSelectionChange={(keys) => {
const selected =
(Array.from(keys)[0] as UpdateReleaseChannel) || "stable";
handleUpdateChannelChange(selected);
}}
>
<SelectItem key="stable" textValue="stable">
稳定版(纯数字版本,如 2.1.4)
</SelectItem>
<SelectItem key="dev" textValue="dev">
开发版(含 alpha / beta / rc)
</SelectItem>
</Select>
<p className="text-xs text-gray-500 dark:text-gray-400">
版本提示会根据该通道检查最新版本。
</p>
</div>
</CardBody>
</Card>
<Card className="border border-gray-200 dark:border-gray-700">
<CardBody className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
显示设置
</h2>
<div className="space-y-3">
<div className="rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3">
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
规则页面精简模式
</p>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
开启后,规则页面列表使用 2.1.6-alpha8 样式。{" "}
</p>
</div>
<Switch
color="primary"
isDisabled={!admin || forwardCompactModeSaving}
isSelected={forwardCompactMode}
onValueChange={handleForwardCompactModeChange}
/>
</div>
{!admin && (
<p className="mt-2 text-xs text-amber-600 dark:text-amber-400">
仅管理员可修改该全局配置。
</p>
)}
</div>
</div>
</CardBody>
</Card>
{/* 添加新地址 */}
<Card className="border border-gray-200 dark:border-gray-700">
<CardBody className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
添加新面板地址
</h2>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
label="名称"
placeholder="请输入面板名称"
value={newName}
onChange={(e) => setNewName(e.target.value)}
/>
<Input
label="地址"
placeholder="http://192.168.1.100:3000"
value={newAddress}
onChange={(e) => setNewAddress(e.target.value)}
/>
</div>
<Button color="primary" onPress={addPanelAddress}>
添加
</Button>
</div>
</CardBody>
</Card>
{/* 地址列表 */}
<Card className="border border-gray-200 dark:border-gray-700">
<CardBody className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
已保存的面板地址
</h2>
{panelAddresses.length === 0 ? (
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
暂无保存的面板地址
</p>
) : (
<div className="space-y-3">
{panelAddresses.map((panel, index) => (
<div
key={index}
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900 dark:text-white">
{panel.name}
</span>
{panel.inx && (
<span className="px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 text-xs rounded">
当前
</span>
)}
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{panel.address}
</p>
</div>
<div className="flex items-center gap-2">
{!panel.inx && (
<Button
color="primary"
size="sm"
variant="flat"
onPress={() => setCurrentPanel(panel.name)}
>
设为当前
</Button>
)}
<Button
color="danger"
size="sm"
variant="light"
onPress={() => handleDeletePanelAddress(panel.name)}
>
删除
</Button>
</div>
</div>
</div>
))}
</div>
)}
</CardBody>
</Card>
</div>
</div>
</div>
);
};
+17 -14
View File
@@ -90,7 +90,7 @@ import {
interface ChainTunnel {
nodeId: number;
protocol?: string; // 'tls' | 'wss' | 'tcp' | 'mtls' | 'mwss' | 'mtcp' - 转发链协议
protocol?: string; // 'tls' | 'wss' | 'tcp' | 'mtls' | 'mwss' | 'mtcp' | 'kcp' - 转发链协议
strategy?: string; // 'fifo' | 'round' | 'rand' - 仅转发链需要
chainType?: number; // 1: 入口, 2: 转发链, 3: 出口
inx?: number; // 转发链序号
@@ -1578,14 +1578,15 @@ export default function TunnelPage() {
{/* 隧道卡片网格 */}
{tunnels.length > 0 ? (
viewMode === "list" ? (
<Card>
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
<Table
aria-label="隧道列表"
className="overflow-x-auto min-w-full"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/10 dark:hover:bg-white/5 transition-colors",
}}
>
<TableHeader>
@@ -1720,7 +1721,7 @@ export default function TunnelPage() {
{(listeners) => (
<Card
key={tunnel.id}
className="group shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden"
className="group overflow-hidden bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
>
<CardHeader className="pb-2 md:pb-2">
<div className="flex justify-between items-start w-full">
@@ -1770,7 +1771,7 @@ export default function TunnelPage() {
<div className="pt-2 border-t border-divider">
<div className="flex items-center justify-center gap-2 text-xs">
{/* 入口节点 */}
<div className="flex items-center gap-1 px-2 py-1 bg-primary-50 dark:bg-primary-100/20 rounded border border-primary-200 dark:border-primary-300/20">
<div className="flex items-center gap-1 px-2 py-1 bg-primary-50/30 dark:bg-primary-100/20 backdrop-blur-md rounded border border-primary-200/50 dark:border-primary-300/20">
<svg
aria-hidden="true"
className="w-3 h-3 text-primary-600"
@@ -1805,7 +1806,7 @@ export default function TunnelPage() {
</svg>
{/* 转发链 */}
<div className="flex items-center gap-1 px-2 py-1 bg-secondary-50 dark:bg-secondary-100/20 rounded border border-secondary-200 dark:border-secondary-300/20">
<div className="flex items-center gap-1 px-2 py-1 bg-secondary-50/30 dark:bg-secondary-100/20 backdrop-blur-md rounded border border-secondary-200/50 dark:border-secondary-300/20">
<svg
aria-hidden="true"
className="w-3 h-3 text-secondary-600"
@@ -1843,7 +1844,7 @@ export default function TunnelPage() {
</svg>
{/* 出口节点 */}
<div className="flex items-center gap-1 px-2 py-1 bg-success-50 dark:bg-success-100/20 rounded border border-success-200 dark:border-success-300/20">
<div className="flex items-center gap-1 px-2 py-1 bg-success-50/30 dark:bg-success-100/20 backdrop-blur-md rounded border border-success-200/50 dark:border-success-300/20">
<svg
aria-hidden="true"
className="w-3 h-3 text-success-600"
@@ -1870,7 +1871,7 @@ export default function TunnelPage() {
<div
className={`grid gap-2 ${tunnel.type === 2 && tunnel.ipPreference ? "grid-cols-3" : "grid-cols-2"}`}
>
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100/30 rounded">
<div className="text-center p-1.5 bg-white/5 dark:bg-black/5 backdrop-blur-3xl rounded-lg border border-divider">
<div className="text-xs text-default-500">
流量计算
</div>
@@ -1878,7 +1879,7 @@ export default function TunnelPage() {
{getTunnelFlowDisplay(tunnel.flow)}
</div>
</div>
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100/30 rounded">
<div className="text-center p-1.5 bg-white/5 dark:bg-black/5 backdrop-blur-3xl rounded-lg border border-divider">
<div className="text-xs text-default-500">
流量倍率
</div>
@@ -1887,7 +1888,7 @@ export default function TunnelPage() {
</div>
</div>
{tunnel.type === 2 && tunnel.ipPreference && (
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100/30 rounded">
<div className="text-center p-1.5 bg-white/5 dark:bg-black/5 backdrop-blur-3xl rounded-lg border border-divider">
<div className="text-xs text-default-500">
连接偏好
</div>
@@ -1985,7 +1986,7 @@ export default function TunnelPage() {
)
) : (
/* 空状态 */
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
暂无隧道配置
@@ -2456,6 +2457,7 @@ export default function TunnelPage() {
<SelectItem key="mtls">MTLS</SelectItem>
<SelectItem key="mwss">MWSS</SelectItem>
<SelectItem key="mtcp">MTCP</SelectItem>
<SelectItem key="kcp">KCP</SelectItem>
</Select>
{/* 负载策略 - 25% */}
@@ -2756,6 +2758,7 @@ export default function TunnelPage() {
<SelectItem key="mtls">MTLS</SelectItem>
<SelectItem key="mwss">MWSS</SelectItem>
<SelectItem key="mtcp">MTCP</SelectItem>
<SelectItem key="kcp">KCP</SelectItem>
</Select>
{/* 负载策略 - 25% */}
@@ -3150,7 +3153,7 @@ export default function TunnelPage() {
</div>
)}
</ModalHeader>
<ModalBody className="bg-content1">
<ModalBody className="bg-transparent">
{diagnosisResult ? (
<div className="space-y-4">
{diagnosisLoading && (
@@ -3672,7 +3675,7 @@ export default function TunnelPage() {
</div>
)}
</ModalBody>
<ModalFooter className="bg-content1 border-t border-divider">
<ModalFooter className="bg-transparent border-t border-white/20 dark:border-white/10">
<Button variant="light" onPress={onClose}>
关闭
</Button>
+25 -25
View File
@@ -185,7 +185,7 @@ const normalizeUserTunnelItem = (item: Partial<UserTunnel>): UserTunnel => {
export default function UserPage() {
// 状态管理
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [searchKeyword, setSearchKeyword] = useLocalStorageState(
"user-search-keyword",
"",
@@ -466,10 +466,6 @@ export default function UserPage() {
void loadMonitorPermissions();
}, [loadMonitorPermissions, loadSpeedLimits, loadTunnels, loadUserGroups]);
useEffect(() => {
void loadUsers();
}, [loadUsers]);
useEffect(() => {
if (searchDebounceRef.current) {
clearTimeout(searchDebounceRef.current);
@@ -1084,9 +1080,10 @@ export default function UserPage() {
aria-label="用户列表"
className="overflow-x-auto min-w-full"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
@@ -1108,14 +1105,11 @@ export default function UserPage() {
return (
<TableRow key={user.id}>
<TableCell>
<div className="flex items-center gap-2">
<div
className={`shrink-0 w-2 h-2 rounded-full ${
userStatus.color === "success"
? "bg-success"
: "bg-danger"
}`}
/>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center shrink-0 w-10 h-10 rounded-full bg-primary text-white font-bold text-sm relative">
{(user.name || user.user).slice(0, 2).toUpperCase()}
<span className={`absolute bottom-0 right-0 w-3 h-3 border-2 border-white dark:border-zinc-900 rounded-full ${userStatus.color === "success" ? "bg-success" : "bg-danger"}`} />
</div>
<div className="flex flex-col">
<span className="font-medium text-foreground text-sm">
{user.name || user.user}
@@ -1252,16 +1246,22 @@ export default function UserPage() {
return (
<StaggerItem key={user.id}>
<Card className="shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden h-full">
<Card className="overflow-hidden h-full">
<CardHeader className="pb-2 md:pb-2">
<div className="flex justify-between items-start w-full">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground truncate text-sm">
{user.name || user.user}
</h3>
<p className="text-xs text-default-500 truncate">
@{user.user}
</p>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center shrink-0 w-10 h-10 rounded-full bg-primary text-white font-bold text-sm relative">
{(user.name || user.user).slice(0, 2).toUpperCase()}
<span className={`absolute bottom-0 right-0 w-3 h-3 border-2 border-white dark:border-zinc-900 rounded-full ${userStatus.color === "success" ? "bg-success" : "bg-danger"}`} />
</div>
<div className="flex flex-col min-w-0">
<h3 className="font-semibold text-foreground truncate text-sm">
{user.name || user.user}
</h3>
<p className="text-xs text-default-500 truncate">
@{user.user}
</p>
</div>
</div>
<div className="flex items-center gap-1.5 ml-2">
<Chip
@@ -1896,9 +1896,9 @@ export default function UserPage() {
<Table
aria-label="用户隧道权限列表"
classNames={{
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
tr: "hover:bg-default-50/50 transition-colors",
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
}}
>
<TableHeader>
@@ -84,14 +84,22 @@ function mapSize(
const solidColorClassMap: Partial<Record<HeroButtonColor, string>> = {
default:
"bg-default-200 text-default-800 hover:bg-default-300 dark:bg-default-100 dark:text-default-800 dark:hover:bg-default-200",
success: "bg-success text-white hover:bg-success-600",
warning: "bg-warning text-warning-900 hover:bg-warning-400",
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-800 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
success:
"bg-success/10 dark:bg-success/20 backdrop-blur-md text-success-700 dark:text-success-400 hover:bg-success/20 dark:hover:bg-success/30",
warning:
"bg-warning/10 dark:bg-warning/20 backdrop-blur-md text-warning-700 dark:text-warning-400 hover:bg-warning/20 dark:hover:bg-warning/30",
primary:
"bg-primary/10 dark:bg-primary/20 backdrop-blur-md text-primary-700 dark:text-primary-400 hover:bg-primary/20 dark:hover:bg-primary/30",
secondary:
"bg-secondary/10 dark:bg-secondary/20 backdrop-blur-md text-secondary-700 dark:text-secondary-400 hover:bg-secondary/20 dark:hover:bg-secondary/30",
danger:
"bg-danger/10 dark:bg-danger/20 backdrop-blur-md text-danger-700 dark:text-danger-400 hover:bg-danger/20 dark:hover:bg-danger/30",
};
const borderedColorClassMap: Record<HeroButtonColor, string> = {
default:
"border-default-300 text-default-700 hover:bg-default-100 dark:border-default-300 dark:text-default-600 dark:hover:bg-default-200/40",
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
primary:
"border-primary text-primary hover:bg-primary-50 dark:border-primary-500/60 dark:text-primary-300 dark:hover:bg-primary-900/20",
secondary:
@@ -121,17 +129,17 @@ const lightColorClassMap: Record<HeroButtonColor, string> = {
const flatColorClassMap: Record<HeroButtonColor, string> = {
default:
"bg-default-100 text-default-700 hover:bg-default-200 dark:bg-default-100/45 dark:text-default-700 dark:hover:bg-default-200/60",
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
primary:
"bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/45",
"bg-primary/10 dark:bg-primary/20 backdrop-blur-md text-primary-700 dark:text-primary-400 hover:bg-primary/20 dark:hover:bg-primary/30",
secondary:
"bg-secondary-100 text-secondary-700 hover:bg-secondary-200 dark:bg-secondary-900/30 dark:text-secondary-300 dark:hover:bg-secondary-900/45",
"bg-secondary/10 dark:bg-secondary/20 backdrop-blur-md text-secondary-700 dark:text-secondary-400 hover:bg-secondary/20 dark:hover:bg-secondary/30",
success:
"bg-success-100 text-success-700 hover:bg-success-200 dark:bg-success-900/30 dark:text-success-300 dark:hover:bg-success-900/45",
"bg-success/10 dark:bg-success/20 backdrop-blur-md text-success-700 dark:text-success-400 hover:bg-success/20 dark:hover:bg-success/30",
warning:
"bg-warning-100 text-warning-700 hover:bg-warning-200 dark:bg-warning-900/30 dark:text-warning-300 dark:hover:bg-warning-900/45",
"bg-warning/10 dark:bg-warning/20 backdrop-blur-md text-warning-700 dark:text-warning-400 hover:bg-warning/20 dark:hover:bg-warning/30",
danger:
"bg-danger-100 text-danger-700 hover:bg-danger-200 dark:bg-danger-900/30 dark:text-danger-300 dark:hover:bg-danger-900/45",
"bg-danger/10 dark:bg-danger/20 backdrop-blur-md text-danger-700 dark:text-danger-400 hover:bg-danger/20 dark:hover:bg-danger/30",
};
const shadowColorClassMap: Record<HeroButtonColor, string> = {
@@ -21,7 +21,7 @@ export function CardHeader({
export function CardBody({ className, ...props }: React.ComponentProps<"div">) {
return (
<CardContent
className={cn("p-4 pt-0 md:p-6 md:pt-0", className)}
className={cn("p-4 md:p-6", className)}
{...props}
/>
);
@@ -44,13 +44,14 @@ export function DropdownTrigger({ children }: { children: React.ReactNode }) {
export interface DropdownMenuProps {
"aria-label"?: string;
className?: string;
children: React.ReactNode;
}
export function DropdownMenu({ children }: DropdownMenuProps) {
export function DropdownMenu({ children, className }: DropdownMenuProps) {
const { align } = React.useContext(DropdownContext);
return <DropdownMenuContent align={align}>{children}</DropdownMenuContent>;
return <DropdownMenuContent align={align} className={className}>{children}</DropdownMenuContent>;
}
export interface DropdownItemProps {
@@ -149,10 +149,10 @@ export function ModalContent({
mapSize(resolvedSize),
context?.classNames?.base,
resolvedScrollBehavior === "outside"
? "max-h-[90vh] overflow-y-auto"
? "max-h-[90vh] overflow-y-auto scrollbar-hide"
: "",
resolvedScrollBehavior === "inside"
? "max-h-[90vh] flex flex-col overflow-hidden [&>[data-slot=modal-body]]:min-h-0 [&>[data-slot=modal-body]]:flex-1 [&>[data-slot=modal-body]]:overflow-y-auto"
? "max-h-[90vh] flex flex-col overflow-hidden [&_[data-slot=modal-body]]:min-h-0 [&_[data-slot=modal-body]]:flex-1 [&_[data-slot=modal-body]]:overflow-y-auto [&_[data-slot=modal-body]]:scrollbar-hide"
: "",
className,
)}
@@ -57,7 +57,7 @@ export function TableHeader({
);
return (
<thead className={cn("border-b bg-default-50/60", className)} {...props}>
<thead className={cn("border-b", className)} {...props}>
{hasRow ? children : <TableRow>{children}</TableRow>}
</thead>
);
+80
View File
@@ -211,3 +211,83 @@ body {
opacity: 1;
}
}
/* Liquid Glass Variables */
:root {
--glass-bg: rgba(255, 255, 255, 0.6);
--glass-border: rgba(255, 255, 255, 0.8);
--glass-card: rgba(255, 255, 255, 0.7);
--glass-overlay: rgba(0, 0, 0, 0.3);
}
.dark {
--glass-bg: rgba(30, 30, 30, 0.6);
--glass-border: rgba(255, 255, 255, 0.15);
--glass-card: rgba(40, 40, 40, 0.6);
}
.bg-mesh-gradient {
background-color: #f2f2f7;
position: relative;
z-index: 0;
}
.bg-mesh-gradient::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
background: url('/images/bg-light.jpg') no-repeat center center;
background-size: cover;
}
.dark .bg-mesh-gradient {
background-color: #0b1020;
}
.dark .bg-mesh-gradient::before {
background: url('/images/bg-dark.jpg') no-repeat center center;
background-size: cover;
}
:root.has-theme-bg .bg-mesh-gradient {
background-image: none !important;
background-color: #f6f7fb !important;
}
:root.dark.has-theme-bg .bg-mesh-gradient {
background-color: #0b1020 !important;
}
:root.has-theme-bg .bg-mesh-gradient::before {
background-image: none !important;
}
:root.has-custom-bg .bg-mesh-gradient {
background-image: none !important;
background-color: var(--custom-bg-color, transparent) !important;
}
:root.has-custom-bg .bg-mesh-gradient::before {
background-image: var(--custom-bg-image, none) !important;
background-size: cover !important;
background-position: center center !important;
background-repeat: no-repeat !important;
}
@layer utilities {
.scrollbar-hide {
/* IE and Edge */
-ms-overflow-style: none;
/* Firefox */
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: "autoUpdate",
registerType: "prompt",
injectRegister: "auto",
includeAssets: ["favicon.ico", "apple-touch-icon.png"],
manifest: {