# How this console is built Read this through before touching anything. It is short and it saves you the mistakes that have already been made. ## The rule that rules them all: design only, zero functionality This console **replaces the Technitium DNS Server interface without changing what it does**. Same controls, same steps, **same texts**, same validation order. Any behavioural difference from the upstream console is a bug, even when it looks like an improvement. If you catch yourself thinking "while I am here, this could be better": no. That is a different job. The exceptions are few, decided and written down: see [Deliberate deviations from upstream behaviour](#deliberate-deviations-from-upstream-behaviour). The largest is the Blocking section, which replaces Allowed and Blocked. Those that add something carry written limits of their own. ## Where the reference is The console this one replaces is not in this repository. You read it from the server's own repository, which has to be added as a remote once: ```bash git remote add upstream https://github.com/TechnitiumSoftware/DnsServer.git git fetch upstream master git show upstream/master:DnsServerCore/www/js/zone.js git show upstream/master:DnsServerCore/www/index.html ``` **The alert texts are contract.** Pull them out of there with `grep -o 'showAlert("[^"]*", "[^"]*", "[^"]*"'` and copy them literally, without rewording. ## Do not assume the shape of the responses: check it There are two disposable instances in `dev/`: - `dev` at : serves our build - `ref` at : the upstream console, untouched User `admin`, password `technitium-ui-dev`. Bring them up with `docker compose up -d` from `dev/`. ```bash T=$(curl -s "http://127.0.0.1:5381/api/user/login?user=admin&pass=technitium-ui-dev&includeInfo=false" | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") curl -s "http://127.0.0.1:5381/api/zones/list?token=$T" | python3 -m json.tool | head -40 ``` Three assumptions that have already turned out false, so you do not repeat them: 1. **Not everything comes wrapped in `response`.** `user/login`, `user/session/get` and `status` return the payload **flat**. Everything else does wrap it. That is why `apiRequest` **unwraps nothing**: it hands over the JSON as it came. 2. The second-factor literal is **`2fa-required`**, not `two-factor-auth-required`. 3. The statistics are flushed **by the minute**. A `dig` you just ran does not show on the Dashboard until the next flush; do not conclude something is broken. ## Upstream behaviours discovered along the way Write down here whatever you find. What is already known: - **Optional fields that look required.** In `apps/list`, the `updateVersion`, `updateUrl` and `updateAvailable` fields **only exist** if the app is in the store catalog and there is a compatible version there; an app installed from your own zip never brings them, and if the catalog query exhausts its 5 s timeout the list arrives whole but without them on any app. Typing them as required is a guaranteed bug. Be suspicious of every field you have only seen once. - **A text field can come back as `null`.** `apps/config/get` returns `config: null` as soon as someone saves an empty configuration. - **The upload endpoints are POST-only**: by GET the server answers **404**, not a JSON error. - **On uploads, the file field name does not matter**: the server takes `Form.Files[0]`. But **do not set `Content-Type` by hand** or the `boundary` is lost and the server says the file is missing. - **Deleting something that does not exist usually answers `ok`**, not an error. - **There are two different paginations.** `zones/list` paginates on the server (`pageNumber`, `zonesPerPage`) and its paging fields **only appear if you send `pageNumber`**. `zones/records/get`, on the other hand, **does not paginate**: it is asked for with `listZone=true` and paginated on the client. - **Where an alert comes out is not cosmetic**: in upstream, a modal's alerts come out inside the modal and a screen's come out on the screen. Honour it. - **The server can return a domain different from the one you asked for.** When navigating the cache/allowed/blocked tree, `WebServiceOtherZonesApi.cs` **walks down on its own** while the node has no records and has exactly one child. Always draw `response.domain`, **never** the domain you asked for, or the tree and the table fall out of sync. - **The same field can have two types depending on the endpoint.** In `cache/list` the `ttl` is an already-composed string (`"218 (3m38s)"`); in `allowed/list` and `blocked/list` it is a number with `ttlString` apart. Same with a SOA's `refresh`, `retry`, `expire` and `minimum`. Typing the records with a single shape is a guaranteed bug. - **`0001-01-01T00:00:00` is .NET's `default(DateTime)`**: it means "never", not year 1. - **Visibility rules that look the same and are not**: deleting a node is offered in cache if the node is not the root, and in allowed/blocked if the node has records. Do not make them uniform without looking. - **A count may not be where you draw it.** `blocked/list` only reads the manually blocked zones; the downloaded block lists are counted **only** in `dashboard/stats/get` (`blockListZones`). - **Careful with replicating the intent instead of the behaviour.** In `other-zones.js` there are three `domain.toLowerCase();` **without assigning the result**: they do nothing. What the code does is replicated, not what it looks like it wants to do. - **`settings/get` OMITS the null keys**, it does not send them as `null`: fields like `temporaryDisableBlockingTill` or `blockListNextUpdatedOn` simply do not appear on a freshly installed server. Others do arrive as an explicit `null`. Declaring them required fails against a new install. And `blockListNextUpdatedOn` is not only a fresh-install gap: it is written only while a block list update is scheduled (WebServiceSettingsApi.cs:382-386), so saving an empty list makes it vanish from the answer. Settings › Blocking and the Lists tab of Blocking both read its absence as "Not Scheduled". - **`logs/query` filters by ONE `responseType`** (WebServiceLogsApi.cs:182-185), but the Dashboard's `totalBlocked` adds up three: `Blocked`, `UpstreamBlocked` and `UpstreamBlockedCached` (StatsManager.cs:329-341). "The latest blocked queries" is therefore three queries, merged by time: that is what Recently Blocked does (`api/blocking.ts`). - **Careful with `\r\n` when replicating a list textarea.** Upstream builds its textareas with `\r\n`, but the browser normalises to `\n` when reading the value of a `