diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a09ae517..54a6d109 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,10 +86,11 @@ Open [http://localhost:2053](http://localhost:2053) and log in with `admin` / `a ### Inside VS Code -The repo ships a launch profile in `.vscode/launch.json` (gitignored — copy from the snippet below if absent): +The repo checks in two VS Code launch profiles in `.vscode/launch.json`: **Run 3x-ui (Debug)** for the default SQLite setup, and **Run 3x-ui (Postgres)** which points `XUI_DB_TYPE`/`XUI_DB_DSN` at a local PostgreSQL. The Postgres profile also prepends the PostgreSQL `bin` to `PATH` so the panel can find `pg_dump`/`pg_restore` (the `postgresql-client` tools used for DB backup/restore) — adjust the DSN and that path to your machine: ```jsonc { + "$schema": "vscode://schemas/launch", "version": "0.2.0", "configurations": [ { @@ -106,6 +107,23 @@ The repo ships a launch profile in `.vscode/launch.json` (gitignored — copy fr "XUI_BIN_FOLDER": "x-ui" }, "console": "integratedTerminal" + }, + { + "name": "Run 3x-ui (Postgres)", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "cwd": "${workspaceFolder}", + "env": { + "XUI_DEBUG": "true", + "XUI_LOG_FOLDER": "x-ui", + "XUI_BIN_FOLDER": "x-ui", + "XUI_DB_TYPE": "postgres", + "XUI_DB_DSN": "postgres://xui:xuipass@127.0.0.1:5432/xui?sslmode=disable", + "PATH": "C:\\Program Files\\PostgreSQL\\18\\bin;${env:PATH}" + }, + "console": "integratedTerminal" } ] } @@ -117,14 +135,21 @@ The panel UI is a **React 19 + Ant Design 6 + TypeScript** app under `frontend/` ### Architecture -The frontend is a **multi-page application**, not a SPA. Every panel route (`/panel`, `/panel/inbounds`, `/panel/clients`, `/panel/xray`, `/panel/settings`, `/panel/nodes`, `/panel/api-docs`, `/panel/sub`, plus `login`) has its own HTML entry in `frontend/*.html` and its own bootstrap in `src/entries/.tsx`. Vite emits each entry into `web/dist/`, and the Go binary embeds that directory at compile time via `embed.FS`. Each panel navigation is a real document load, but every per-page bundle is small enough to keep the experience responsive. There is no React Router and no global store; the surface area does not justify either. +The frontend ships **three Vite bundles**, each emitted into `web/dist/` and embedded into the Go binary at compile time via `embed.FS`: + +- **`index.html`** — the admin panel, a **single-page app**. `src/main.tsx` mounts a `react-router` `createBrowserRouter` (see `src/routes.tsx`) under the `/panel` basename; every route (`/panel`, `/panel/inbounds`, `/panel/clients`, `/panel/groups`, `/panel/nodes`, `/panel/settings`, `/panel/xray`, `/panel/api-docs`) is lazy-loaded inside a shared `PanelLayout` (sidebar + header + ``). +- **`login.html`** — the login + 2FA screen (`src/entries/login.tsx`), a standalone bundle. +- **`subpage.html`** — the public subscription viewer (`src/entries/subpage.tsx`), a standalone bundle. + +Panel navigation happens client-side through React Router, and per-route code is lazy-split so the initial panel load stays small. `login` and `subpage` stay separate documents because they are reached without an authenticated panel session. ### State and data flow -- **No global store.** State lives in the page that owns it. Cross-page data (settings, current user, theme) is re-fetched on each page load — the backend is local and responses are inexpensive. -- **Hooks** in `src/hooks/` encapsulate reactive logic worth sharing inside a page (`useTheme`, `useStatus`, `useNodes`, `useWebSocket`, `useDatepicker`, …). Prefer extending an existing hook over introducing a new global. -- **Domain models** in `src/models/` (`Inbound`, `DBInbound`, `Outbound`, `Status`, …) own the protocol-specific logic — link generation, settings JSON shape, TLS/Reality stream handling. React components stay declarative; they ask the model "what is my link?" and render the answer. -- **HTTP** goes through `src/utils/index.js`'s `HttpUtil`, a thin Axios wrapper that handles CSRF, response toasts, and a `silent: true` opt-out for bulk operations that would otherwise spam toasts. The Axios setup itself lives in `src/api/axios-init.js`. +- **Server state via TanStack Query.** API reads go through `@tanstack/react-query` (`QueryProvider` in `src/main.tsx`, keys in `src/api/queryKeys.ts`); responses are cached and invalidated on mutation rather than blindly re-fetched, and WebSocket pushes feed back into the cache via `src/api/websocketBridge.ts`. +- **Local UI state stays in the page** (`useState`); shared concerns go through contexts and hooks in `src/hooks/` (`useTheme`, `useWebSocket`, `useClients`, `useDatepicker`, …). Prefer extending an existing hook over introducing a new global. +- **Zod is the single source of truth.** Schemas in `src/schemas/` define the xray config model; every API response is parsed through them, every form field validates against them, and TypeScript types are inferred with `z.infer` — never hand-written. Go-side types are mirrored into `src/generated/` by `npm run gen:zod` (do not hand-edit that folder). +- **xray domain logic** — link generation, protocol defaults, form ⇄ wire adapters — lives as pure functions in `src/lib/xray/`. `src/models/` keeps only thin legacy types still being migrated onto schemas. +- **HTTP** goes through `HttpUtil` in `src/utils/index.ts`, a thin Axios wrapper that handles CSRF, response toasts, and a `silent: true` opt-out for bulk operations that would otherwise spam toasts. The Axios setup itself lives in `src/api/axios-init.ts`. ### i18n @@ -134,21 +159,22 @@ Locale strings live in `web/translation/.json`, **not** under `frontend/ | Goal | Command | |------|---------| -| Iterate on UI changes with HMR | `cd frontend && npm run dev` (Vite on `:5173`, proxies `/panel/*` and `/api/*` to the Go panel on `:2053`). Start the Go panel first. | +| Iterate on UI changes with HMR | `cd frontend && npm run dev` (Vite on `:5173`, proxies `/panel/*` and the WebSocket to the Go panel on `:2053`). Start the Go panel first. | | Verify what end users actually see | `cd frontend && npm run build`, then `go run .`. The Go binary serves the built bundle — embedded in release mode, off disk in debug mode. | -The Vite dev proxy rewrites the sidebar's production-style links (`/panel`, `/panel/inbounds`, `/panel/clients`, …) to the matching Vite-served HTML, so navigation behaves identically to production without round-tripping through Go. The allowlist lives in `MIGRATED_ROUTES` in `vite.config.js` — register every new page there. +The Vite dev proxy serves the admin SPA for any `/panel/*` URL — `bypassMigratedRoute` in `vite.config.js` rewrites those requests to `index.html` and lets React Router take over — while forwarding `/panel/api/*`, `/panel/setting/*`, `/panel/xray/*`, and the WebSocket to the Go panel. Because routing is now client-side, new panel routes need no proxy or allowlist changes. > **`XUI_DEBUG=true` gotcha** — in debug mode the panel serves HTML from the embedded FS (frozen at the last `go build` / `go run`) but JS/CSS off disk. Re-running `npm run build` without restarting Go leaves the embedded HTML pointing at the *old* hashed asset names, producing a blank page with 404s in the console. Always restart `go run .` after a frontend rebuild. ### Adding a new page -1. Create `frontend/.html` (copy an existing entry and adjust the title and the imported `