Caddy previously ran as a host binary in /opt/caddy with a wgui-caddy systemd/OpenRC unit. Only Caddy is containerized; the app still runs on the host as root because it shells out to wg, ip and iptables. - Add caddy-container-commands.ts as the single source of truth for the run/exec/validate/inspect argument arrays. Every value stays a separate argv element, the image is pinned to caddy:2.11.4-alpine, and the entrypoint is set explicitly because some mirrors publish the image with a null Entrypoint (which made Docker exec "run" as a binary). - Rewrite install-caddy.sh: require_docker with distro-specific hints, pull + recreate the container, assert it is running and echo the daemon error instead of discarding it, normalize the admin socket path in existing Caddyfiles, and clean up a legacy native install only after the container is up so the ingress port is never unbound. Add --purge-legacy / --purge for hosts that want to drop the old install without installing the container. - Move the admin socket to /var/lib/wgui/caddy/admin.sock so it lives inside the mounted Caddy dir and reloads work over docker exec. - Validate candidate configs in a throwaway container, mounting the TLS dir read-only as well: provisioning the tls app loads every certificate referenced by a tls directive, so without that mount a route with a certificate failed validation and its Caddyfile was never committed. - Report docker-based installed/version/service_active, skip validate/reload with a warning when the image is absent, and update the system setup tool check to look for docker. - Issue certificates with a docker exec reload command and chown files to root:root, since the container reads them through a read-only mount. - Update the Setup UI copy and docs (README, AGENTS.md). Also drops the unused EmptyContent import and a stray debug log in the client route form, and stops passing the unsupported cancel_href prop to ClientRouteForm from the route add/edit pages.
108 lines
12 KiB
Markdown
108 lines
12 KiB
Markdown
# Agent Instructions
|
||
|
||
WireGuard UI — a self-hosted web admin for WireGuard hosts and clients. The app runs **as root** and executes system commands (`wg`, `ip`, `iptables`, `wg-quick`, firewall managers). Treat every API route and every value that reaches a shell or a WireGuard config as security-sensitive.
|
||
|
||
## Stack
|
||
|
||
- **Runtime:** Bun — not Node. Use `bun` commands, never `npm`/`npx`.
|
||
- **Framework:** `@moduletrace/bunext` — not Next.js. Routing is filesystem-based under `src/pages/`, served by `Bun.serve` in `src/server.ts`.
|
||
- **Database:** `@moduletrace/bun-sqlite` — SQLite. Schema in `db/schema.ts`, generated types in `db/types/db.ts`. Config in `bun-sqlite.config.ts` (DB name `wgui` in `./db`). Import its types from `@moduletrace/bun-sqlite/types` (not `/dist/types`).
|
||
- **UI:** Tailwind CSS v4 + local `twui` components (`src/components/twui`). Global styles in `src/styles/main.css`.
|
||
- **Icons:** `lucide-react`.
|
||
|
||
## MCP Tool Usage Guidelines
|
||
|
||
When building, refactoring, styling, or auditing UI components and layouts, actively invoke these configured MCP servers:
|
||
|
||
### 1. `design-mcp` (UI/UX Engineering & Design Standards)
|
||
|
||
- **When to call:** Before constructing new page layouts, design systems, empty/loading states, or interactive UI flows.
|
||
- **Instruction:** Query `design-mcp` to ensure accessibility (a11y), proper layout hierarchy, visual balance, and consistent interaction patterns across application workflows.
|
||
|
||
### 2. `design-systems` (Design System Specs & Reference Patterns)
|
||
|
||
- **When to call:** When creating or extending reusable component interfaces, verifying design token structures, or inspecting component composition patterns.
|
||
- **Instruction:** Query `design-systems` to pull canonical UI patterns, atomic component guidelines, and standard prop naming conventions before creating custom components.
|
||
|
||
## Entrypoint
|
||
|
||
`src/server.ts` — runs `bunextInit()`, starts `Bun.serve` on `0.0.0.0` (port from `SiteData.ServerPort`), serves `/media/*` via `handleMediaServer`, then runs `syncWireguardHosts()` to bring host tunnels in line with the DB. Caddy's data plane runs as the `wgui-caddy` Docker container (host networking, `/var/lib/wgui/caddy` and `/var/lib/wgui/tls` bind-mounted); reloads go through `docker exec`, and the admin socket lives at `/var/lib/wgui/caddy/admin.sock`.
|
||
|
||
On-disk state: WireGuard keys/configs/iptables scripts live under `/var/lib/wgui` (`grabDirNames()` in `src/utils/grab-dir-names.ts`); uploaded media under `DATA_DIR` (default `./.data`).
|
||
|
||
## Commands
|
||
|
||
| Purpose | Command |
|
||
| --------------- | ------------------------------------------------------- |
|
||
| Dev server | `bun run dev` (runs `sudo bun src/server.ts`) |
|
||
| Production | `bun run start` |
|
||
| Build | `bun run build` |
|
||
| Type-check | `bun run typecheck` (`bunx tsc --noEmit`) |
|
||
| Tests | `bun run test` (`bun test`) |
|
||
| DB schema | `bun run db:schema` |
|
||
| DB admin UI | `bun run db:admin` |
|
||
|
||
No linter or formatter configured. Type-check and `bun test` are the verification steps.
|
||
|
||
## Architecture
|
||
|
||
- `src/pages/__root.tsx` — picks the layout (`src/layouts/admin` for `/admin*`, `src/layouts/centered` for auth, 404 and everything else), mounts `AppProviders` (toast + confirm dialog) and provides `AppContext`. Its `Head` renders `ThemeScript` so the theme applies before first paint.
|
||
- **Pages:** `/` redirects to `/admin` or `/auth/login`; `/auth/{login,logout,signup}`; `/admin` (dashboard), `/admin/setup/{status,maintenance,firewall}` (super admin only, guarded per page by `guard-setup-page`; `/admin/setup` redirects to `status`), `/admin/settings/{profile,password}` (`/admin/settings` redirects to `profile`), `/admin/clients`, `/admin/clients/add` (the only add-client form — `?host_id=` preselects the host), `/admin/hosts/*` (add/edit hosts, `[host_id]/clients` list, `[host_id]/clients/[client_id]` tabbed detail + `edit`). `[host_id]/clients/add-client` is a legacy redirect.
|
||
- **Theme:** light/dark/system, stored in the `wgui_theme` cookie (`src/dict/theme-dict.ts`). twui dark styles key off a `.dark` class on `<html>` — toggle it only via `applyTheme`, never replace `className`.
|
||
- **API:** `src/pages/api/admin/*` (host/client setup, network info, firewall status, service restart/update, media), generic CRUD at `src/pages/api/admin/crud/[[...paths]].ts` with per-table handlers in `crud/(tables)/<table>/{get,post,put,del}.ts`, plus `api/auth/login.ts` and `api/create-first-user.ts`.
|
||
- **Every `/api/admin/*` route must authenticate** — nothing guards the prefix globally. Use `adminApiGuard({ req })` (`src/functions/backend/auth/admin-api-guard.ts`) or `userAuth` + `checkUserAccess`.
|
||
- **Shell safety:** never interpolate request/DB values into `execSync` strings. Use `execFileSync` with an args array or `node:fs` APIs, and validate values before they reach a WireGuard config (newlines there can inject `PostUp` commands). Validators live in `src/utils/` (`validate-ipv4`, `validate-ipv4-cidr`, `validate-allowed-ips`, `validate-endpoint-host`, `validate-interface-name`).
|
||
|
||
## Path Alias
|
||
|
||
`@/*` resolves from repo root. Import like `@/src/components/general/admin-card`, `@/db/types/db`.
|
||
|
||
## Page Directory Rules
|
||
|
||
For route-local code under `src/pages`, use non-routed meta folders:
|
||
|
||
- `(sections)` — page sections
|
||
- `(partials)` — components used by sections
|
||
- `(data)` — page-local data
|
||
- `(functions)` — page-local functions
|
||
- `(utils)` — page-local utilities
|
||
- `(hooks)` — page-local hooks
|
||
|
||
Keep route files (e.g. `index.tsx`) thin — compose from route-local meta folders.
|
||
|
||
Dynamic routes use `[param]` syntax (e.g. `[id]`, `[user_type]`, `[key]`). Catch-all routes use `[[...paths]]`.
|
||
|
||
Server functions use `index.server.ts` alongside `index.tsx` (e.g. `single-document/[id]/index.server.ts`).
|
||
|
||
## Shared Code Organization
|
||
|
||
- **`src/functions/backend/`** — Server-only: `auth/`, `db/`, `firewall/` (firewalld/ufw rules, status), `media/`, `setup/` (host/client setup, iptables script builder, network detection), `caddy/` (Caddyfile generation, ingress sync, route validation), `acme/` (manual DNS-01 with `acme-client`, challenge state, certificate status). Certificates are configured per route in the route form; `/admin/setup/tls` is only for the global Caddy installation and status. The route page displays the `_acme-challenge` TXT record and validates after the user adds it at their DNS registrar.
|
||
- **`src/functions/frontend/`** — Client-safe: CRUD/fetch handlers, media upload/delete, client rule label formatting.
|
||
- **`src/functions/server/`** — Request-level server handlers (media server).
|
||
- **`src/hooks/`** — `use-admin-crud-get` (with `refetch`), `use-client-rules-map`, `use-confirm`, `use-toast`, `use-theme`, `use-fetch-get`, `use-form-init`, `use-users-get`.
|
||
- **`src/utils/`** — Cookies, crypto, dir names, IP/CIDR/port/interface validators, WireGuard interface naming. Most have `.test.ts` files.
|
||
- **`src/dict/`**, **`src/data/`** — Constants (`app-data.ts`, `site-data.ts`), user types, client rules, links.
|
||
- **`src/scripts/`** — Shell scripts: `install-wg-ui.sh`, `install-caddy.sh` (idempotent installer that pulls the `caddy:<version>-alpine` image and (re)creates the `wgui-caddy` Docker container plus acme.sh renewal; used by the UI and the full installer), `setup-wireguard.sh`, `wg-quick-manage.sh`.
|
||
- **`src/components/general/`** — Shared app components, all built from twui. Reach for these before writing page markup:
|
||
- Page frame: `page-header` (title, meta, primary action + overflow menu), `page-body`, `panel` (the one card style), `form-section`, `form-actions` (submit is always rendered).
|
||
- Text & status: `muted-text`, `mono-text` (IPs, keys, paths), `section-label`, `status-dot`, `inline-alert`, `empty-state`, `stat-tile`, `key-value-list`, `copy-button`, `copyable-value`.
|
||
- Data: `data-table` (desktop table + mobile card list; the only place raw `<table>` is allowed), `clients-table`, `host-clients-panel`, `action-menu`, `sub-nav` (link tabs between sibling routes).
|
||
- Flows: `client-form` + `client-rules` (shared by add/edit client and the client detail Rules tab), `user-form`, `host-wg-ip-field`, `use-delete-host`.
|
||
- Feedback: `confirm/` + `useConfirm()` and `toast/` + `useToast()` (`src/hooks`). Never use `window.confirm`/`alert`; update lists in place instead of `location.reload()`.
|
||
- **Design tokens** live in `src/styles/main.css`: one accent, WireGuard red `#89161b` (`primary` = `secondary` = `accent`; `error` is a separate brighter red), zinc neutrals, and theme-flipping semantic colours (`bg-surface`, `bg-surface-2`, `border-line`, `text-fg`, `text-muted`, `text-subtle`) that need no `dark:` pair.
|
||
|
||
## Conventions
|
||
|
||
- **Numeric IDs are not booleans — the main WireGuard host has `id = 0`.** Never test an ID with truthiness (`if (!host?.id)`, `if (host_id)`, `id ? ... : ...`); `0` is a valid ID and `AppData.WireguardMainHostID` is `0`. Use `typeof id === "number"` / `Number.isSafeInteger(id)` for numeric IDs, or explicit `id !== undefined && id !== ""` when the ID may arrive as a string. This applies to host lookups especially, but to route/cert IDs too.
|
||
- **Every form that submits must ask for confirmation first.** Use `useConfirm()` and await `confirm({...})` before submitting; never use `window.confirm` or `window.alert`. This applies to create, update, settings, and auth forms.
|
||
- Prefer one function/component per file over monolithic files with multiple exports.
|
||
- **Every rendered array must have a separate component for each array item.** Always extract the item JSX into its own component and render it via `items.map((item) => <ItemComponent {...item} />)` — never inline the item's JSX inside the map callback.
|
||
- Always use object parameters (`{ foo, bar }`) instead of comma-separated positional parameters.
|
||
- Prefer existing patterns over new abstractions.
|
||
- **Modularization is non-negotiable: prioritize modularization over monolithic functions/components.** Repeated UI blocks must be extracted into their own reusable components driven by props (e.g. an info row component taking `{ icon, text }`), never repeated inline markup.
|
||
- **UNCHANGEABLE RULE — twui components always win over raw JSX elements.** ALWAYS use twui components (`H1`–`H5`, `P`, `Span`, `List`, `Img`, `Row`, `Stack`, `Container`, `Section`, `Border`, `Card`, `Button`, `Tag`, `Link`, etc.) before basic JSX elements (`<h1>`–`<h6>`, `<p>`, `<span>`, `<ul>`, `<li>`, `<img>`, `<a>`, `<button>`, `<div>`-as-card, etc.). Never introduce or reintroduce a raw JSX element where a twui equivalent exists. Override twui defaults via `className` (e.g. `mb-0!`) rather than reaching for the raw element.
|
||
- **UNCHANGEABLE RULE — twui components are preferred over shadcn/ui components.** Use twui before `src/components/ui/*` (shadcn) whenever a twui equivalent exists. The long-term goal is to phase out shadcn/ui entirely — do not introduce new shadcn usage where twui covers the need, and prefer migrating existing shadcn call sites to twui when touching them anyway.
|
||
- **LIMIT EDITS TO `src/components/twui/`** — it is a general, publishable library shared across projects (its own `package.json`), not scoped to this repo. Do not modify its components unless strictly necessary. Customize/appearance overrides for this project belong in `src/styles/main.css` (e.g. tweaking `.twui-card`, `--color-*` tokens, font/type styles) rather than editing twui source files.
|
||
- **twui `Input`/`Textarea`: prefer `defaultValue` over `value`.** The component manages internal state and patches the DOM directly (`src/components/twui/form/Input/index.tsx`), so `value` is not a true controlled prop — a static `value` gets reverted on every re-render, wiping in-progress edits. `value` only works well when the parent updates it on each change (live display), not for pre-filling an editable field. Use `defaultValue` to pre-fill editable inputs; `onChange`/`changeHandler` still fire.
|
||
- `.env` is gitignored but exists locally. Do not commit it.
|