client_v2/DEVELOPMENT.md
This document explains how to set up a development environment for the
client_v2 frontend, run it locally, and contribute code. It is intended for
developers working on the next-generation AdGuard Home web UI.
For code guidelines, architecture, and project structure, see AGENTS.md. For user-facing documentation, see the root README.md.
Before you begin, install the following tools:
@types/node 22; a current
LTS release is recommended./control API requests to it, so a built AdGuardHome binary in the
repository root (or any reachable instance) is required for the UI to
function. See the root README.md and
HACKING.md for building the Go backend.No global packages are required — all tooling (Webpack, ESLint, Vitest,
Playwright, TypeScript) is installed locally via npm install.
All commands in this guide are run from the client_v2/ directory.
git clone https://github.com/AdguardTeam/AdGuardHome.git
cd AdGuardHome/client_v2
npm install
npm install also runs the Playwright browser install postinstall step. If it
does not, run npx playwright install manually before running e2e tests.
The dev server reads the backend host and port from the root
AdguardHome.yaml file (the bind_host and bind_port fields) and proxies
/control requests to that backend. Make sure a backend config exists at
../AdguardHome.yaml and that the AdGuard Home backend is running.
Start the dev server:
npm run dev
By default the dev server listens on bind_port + 8000 (for example, if the
backend runs on port 3000, the dev server runs on 11000). Override the
port with the DEV_SERVER_PORT environment variable:
DEV_SERVER_PORT=8080 npm run dev
The server opens the main dashboard automatically. The four HTML entry points are:
main — the dashboard / control panel (HashRouter SPA)install.html — the first-run setup wizardlogin.html — the sign-in pageforgot_password.html — the password-reset flowIf the backend is at http://127.0.0.1:3000, the dev server is at
http://127.0.0.1:11000, and you can open install.html, login.html, or
forgot_password.html directly.
The production build emits static assets into ../build/static/, which the Go
binary embeds at compile time.
npm run build-prod
To produce a development build without starting a server:
npm run build-dev
To watch for changes and rebuild continuously:
npm run watch
master (for example,
feature/my-new-feature).Lint the source:
npm run lint
Auto-fix lint issues and format with Prettier:
npm run lint:fix
Linting and formatting rules are defined in .eslintrc files and .prettierrc.
Do not modify these configs without justification. For the full set of code
conventions (path aliases, component structure, CSS modules, reactivity, etc.),
see AGENTS.md.
Run the TypeScript compiler in check-only mode:
npm run typecheck
Watch mode for continuous type checking:
npm run typecheck:watch
Run the full local gate (lint + typecheck + unit tests) with a single command:
npm run check
Unit tests (Vitest, jsdom environment):
npm run test # single run
npm run test:watch # watch mode
Unit tests live under src/__tests__/ and match *.{test,spec}.{ts,tsx}.
Mirror the source path under __tests__/ when adding new tests.
End-to-end tests (Playwright):
npm run test:e2e # run all e2e specs
npm run test:e2e:interactive # open the Playwright UI
npm run test:e2e:debug # debug mode
npm run test:e2e:codegen # generate tests by recording actions
E2E specs live in tests/e2e/. They run against a real AdGuard Home backend
that Playwright starts automatically (see the webServer config in
playwright.config.ts). The ./AdGuardHome binary must be present in the
repository root for this to work. E2E tests are not part of
npm run check; run them explicitly.
The base locale is src/__locales/en.json. After adding or editing
translation keys, verify consistency:
npm run translations:check
This audits source files for intl.getMessage / intl.getPlural usage and
reports missing, unused, and dynamic keys. Other locales are managed
externally via Twosky (configured in the root .twosky.json).
src/components/ (or
src/common/controls/ for primitives, src/common/ui/ for higher-level
UI).index.tsx and a co-located *.module.pcss.blocks/ subfolder.s and compose classes with clsx (imported as cn).panel/* path alias for all src/ imports.src/__tests__/.See the Code Quality section of AGENTS.md for
the full set of conventions.
camelCase.ts module under src/stores/ with a module-scoped
createStore singleton.processing* flag, call apiClient, then setState.addErrorToast from the toasts store.src/__tests__/stores/.Add a typed method to the Api class in src/api/Api.ts. All HTTP calls must
go through this singleton — do not call fetch directly from components or
stores.
src/__locales/en.json.intl.getMessage('key', values?) or
intl.getPlural('key', number, values?).npm run translations:check to verify.eval-source-map).src/ files; VS Code maps them to the bundled
output automatically.npm run test:watch and attach a debugger to the Vitest process.../AdguardHome.yamlThe dev server reads the backend host and port from the root
AdguardHome.yaml. Make sure the file exists at the repository root and
contains bind_host and bind_port. If it is missing, the server falls back
to 0.0.0.0:80 and the proxy will not reach the backend. Start the backend or
create the config file.
The dev server proxies /control to the backend at bind_host:bind_port.
Verify the AdGuard Home backend is running and that the port in
AdguardHome.yaml matches the running instance.
E2e tests require the ./AdGuardHome binary in the repository root. Build the
backend first (see HACKING.md). Playwright starts the backend
automatically; if a server is already running on port 3000, stop it first:
kill $(lsof -ti :3000)
The Vitest config (see vitest.config.ts) forces a single solid-js instance
via dedupe and ssr.noExternal. If you add a dependency that pulls in its
own copy of solid-js, add it to the dedupe and noExternal lists.
Stop any process occupying the dev server port or override it with
DEV_SERVER_PORT:
kill $(lsof -ti :11000)
DEV_SERVER_PORT=8080 npm run dev
Use the panel/* alias (panel/api/Api, panel/initialState, etc.) instead
of deep relative paths. The alias is defined in tsconfig.json and consumed
by Webpack, Vitest, and ESLint.