Rebuild every page on a shared set of twui-based primitives with a dense ops-console look, WireGuard red accent and light/dark/system theming. - Add design tokens (WireGuard red accent, zinc neutrals, theme-flipping semantic colours) and a no-flash theme cookie + toggle - Add shared components: page header, panel, sub-nav, data table with mobile card list, stat tile, status dot, key-value list, copy button, inline alert, empty state, form section/actions, confirm dialog and toast providers - New admin shell (sidebar, sticky header, mobile drawer) and a centered shell for auth and 404; strip the public template pages - Unify adding clients at /admin/clients/add with a host picker; tabbed client detail (Overview / Rules / Config & QR) with config copy/download - Re-run client setup after rule changes on the detail page so the firewall matches the DB - Split Setup into status/maintenance/firewall and Settings into profile/password subpages - Replace window.confirm/alert and page reloads with in-app dialogs, toasts and in-place list updates - Fix nested rule form submitting the client form, prefilled signup passwords, hidden submit buttons, fields not updating after "next free" lookups, and TinyMCE white-on-white in dark mode Co-Authored-By: Claude Opus 5.5 <[email protected]>
11 KiB
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
buncommands, nevernpm/npx. - Framework:
@moduletrace/bunext— not Next.js. Routing is filesystem-based undersrc/pages/, served byBun.serveinsrc/server.ts. - Database:
@moduletrace/bun-sqlite— SQLite. Schema indb/schema.ts, generated types indb/types/db.ts. Config inbun-sqlite.config.ts(DB namewguiin./db). Import its types from@moduletrace/bun-sqlite/types(not/dist/types). - UI: Tailwind CSS v4 + local
twuicomponents (src/components/twui). Global styles insrc/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-mcpto 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-systemsto 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.
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/adminfor/admin*,src/layouts/centeredfor auth, 404 and everything else), mountsAppProviders(toast + confirm dialog) and providesAppContext. ItsHeadrendersThemeScriptso the theme applies before first paint.- Pages:
/redirects to/adminor/auth/login;/auth/{login,logout,signup};/admin(dashboard),/admin/setup/{status,maintenance,firewall}(super admin only, guarded per page byguard-setup-page;/admin/setupredirects tostatus),/admin/settings/{profile,password}(/admin/settingsredirects toprofile),/admin/clients,/admin/clients/add(the only add-client form —?host_id=preselects the host),/admin/hosts/*(add/edit hosts,[host_id]/clientslist,[host_id]/clients/[client_id]tabbed detail +edit).[host_id]/clients/add-clientis a legacy redirect. - Theme: light/dark/system, stored in the
wgui_themecookie (src/dict/theme-dict.ts). twui dark styles key off a.darkclass on<html>— toggle it only viaapplyTheme, never replaceclassName. - API:
src/pages/api/admin/*(host/client setup, network info, firewall status, service restart/update, media), generic CRUD atsrc/pages/api/admin/crud/[[...paths]].tswith per-table handlers incrud/(tables)/<table>/{get,post,put,del}.ts, plusapi/auth/login.tsandapi/create-first-user.ts. - Every
/api/admin/*route must authenticate — nothing guards the prefix globally. UseadminApiGuard({ req })(src/functions/backend/auth/admin-api-guard.ts) oruserAuth+checkUserAccess. - Shell safety: never interpolate request/DB values into
execSyncstrings. UseexecFileSyncwith an args array ornode:fsAPIs, and validate values before they reach a WireGuard config (newlines there can injectPostUpcommands). Validators live insrc/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).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(withrefetch),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.tsfiles.src/dict/,src/data/— Constants (app-data.ts,site-data.ts), user types, client rules, links.src/scripts/— Shell scripts:install-wg-ui.sh,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()andtoast/+useToast()(src/hooks). Never usewindow.confirm/alert; update lists in place instead oflocation.reload().
- Page frame:
- Design tokens live in
src/styles/main.css: one accent, WireGuard red#89161b(primary=secondary=accent;erroris 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 nodark:pair.
Conventions
- 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 viaclassName(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 ownpackage.json), not scoped to this repo. Do not modify its components unless strictly necessary. Customize/appearance overrides for this project belong insrc/styles/main.css(e.g. tweaking.twui-card,--color-*tokens, font/type styles) rather than editing twui source files. - twui
Input/Textarea: preferdefaultValueovervalue. The component manages internal state and patches the DOM directly (src/components/twui/form/Input/index.tsx), sovalueis not a true controlled prop — a staticvaluegets reverted on every re-render, wiping in-progress edits.valueonly works well when the parent updates it on each change (live display), not for pre-filling an editable field. UsedefaultValueto pre-fill editable inputs;onChange/changeHandlerstill fire. .envis gitignored but exists locally. Do not commit it.