diff --git a/AGENTS.md b/AGENTS.md
index 882ec265b..699688a18 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3115,6 +3115,83 @@ contacting every contributor individually.
preserves the script's effect doesn't trip this — see "Updater
migrations" above for the per-script contract.
- String literals for action names → `Actions.PascalName`.
+- Hardcoding `sb.api.endpoint = './api.php'` (or any other bare
+ document-relative path that depends on the URL of the page loading
+ api.js) → the iframe-routed surfaces
+ (`pages/admin.kickit.php` / `pages/admin.blockit.php`) sit one
+ directory deep, so a document-relative `./api.php` resolves
+ against the iframe's URL to `/pages/api.php` (404 — Apache doesn't
+ rewrite that path) and every kick / block iframe round-trip dies
+ silently with a `bad_response` envelope. The iframe templates'
+ load handlers `return` silently on `!r.ok`, leaving rows at the
+ initial "Waiting…" text forever (#1433 bugs 1 + 2 — kick via panel
+ AND kick-on-ban). `web/scripts/api.js` resolves the endpoint
+ against `document.currentScript.src` instead — `new URL('../api.php',
+ SCRIPT_SRC).href` lands on the panel-root `/api.php` for top-level
+ page renders AND iframe contexts AND subdir installs (`https://host/sourcebans/`).
+ `document.currentScript` is null inside async handlers / promises,
+ so capture the value at IIFE-top before any deferred work.
+ Regression guards: `web/tests/integration/ApiJsEndpointResolutionTest.php`
+ (static — asserts api.js references `document.currentScript` AND
+ `new URL('../api.php'` AND does NOT bind the bare literal to
+ `sb.api.endpoint` at construction time) +
+ `web/tests/e2e/specs/flows/kickit-iframe.spec.ts` (runtime — loads
+ `/pages/admin.kickit.php?check=…&type=0`, asserts the
+ `KickitLoadServers` POST targets `/api.php` NOT `/pages/api.php`,
+ and the iframe rows transition past "Waiting…").
+- Loading `web/scripts/api.js` via dynamic injection
+ (`document.createElement('script')`, ``,
+ async loaders, ES-module `import()`, third-party bundlers) → the IIFE
+ reads `document.currentScript.src` at script-load time to compute the
+ endpoint URL, but `document.currentScript` is `null` when a script
+ is appended programmatically (it's only non-null while the parent
+ static ``, async loaders, ES-module `import()`), and a null `currentScript` collapses `SCRIPT_SRC` to the empty string and silently falls back to the bare-relative `./api.php` — i.e. the exact pre-#1433 bug. The three static load sites in the default theme are `core/header.tpl` (top-level panel chrome → `./scripts/api.js`), `page_kickit.tpl`, and `page_blockit.tpl` (iframe surfaces → `../scripts/api.js`); a theme fork that wants to lazy-load needs its own paired endpoint resolver. Pinned by `web/tests/integration/ApiJsEndpointResolutionTest.php` (static) + `web/tests/e2e/specs/flows/kickit-iframe.spec.ts` (runtime). |
+| Validate a server-address input (IPv4 / IPv6 / hostname) on either Add-Server (`api_servers_add`) or Edit-Server (`admin.edit.server.php`) | Shared validator pattern: `filter_var($x, FILTER_VALIDATE_IP) \|\| filter_var($x, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)`. Both surfaces MUST share the same predicate so a value either round-trips through Add AND Edit or fails on both (#1433); pre-fix the JSON dispatcher ran IP-only while the page handler ran a too-loose hand-rolled `^[a-zA-Z0-9.\-]+$` regex. The matching schema-width gate (`strlen($x) > 64`, surfacing as `validation` envelope or `$validationErrors['address']`) is paired with the IP/hostname check because `:prefix_servers.ip` is `VARCHAR(64) NOT NULL` (see `web/install/includes/sql/struc.sql`) — well below RFC 1035's 253-char hostname max, and MariaDB strict mode would otherwise raise `SQLSTATE[22001] 1406 Data too long for column 'ip'` mid-INSERT (generic 500 + no audit-log entry). Bumping the column to `VARCHAR(255)` is a paired schema-migration follow-up. Pinned by `web/tests/api/ServersTest.php::testAddAcceptsHostname` / `testAddAcceptsFqdn` / `testAddAcceptsBareIPv6` / `testAddRejectsAddressExceedingSchemaWidth` / `testAddRefusesDuplicateHostnamePort` / `testAddRejectsWhitespaceInAddress`. |
| Add or rename a permission | `web/configs/permissions/web.json`, then regen contract |
| Render a page | `web/pages/.php` + `web/includes/View/*View.php` |
| Add a new edit page in the admin.edit.* cluster (e.g. `admin.edit..php`) | `web/pages/admin.edit..php` (the page handler — thin "validate input, build View, render" shape) + `web/includes/View/AdminEditView.php` (typed View DTO) + `web/themes/default/page_admin_edit_.tpl` (template). Shared helpers live in `web/pages/_admin_edit_helpers.php` (`sbpp_admin_edit_die_with_toast()` for permission / not-found guards, `sbpp_admin_edit_emit_tail_script()` for form-success / validation-error feedback that fires `window.SBPP.showToast()` and writes errors into `.msg` divs, `sbpp_admin_edit_collect_rehash_sids()` for the post-save Rehash Admins step). Anti-patterns to avoid: inline `echo '