req() in the frontend api client called res.json() on every non-204 2xx
response, which throws SyntaxError on the empty body returned by
POST .../customs (201) and similar endpoints. Read the response body once
as text and parse it only when non-empty, so any bodyless 2xx resolves
instead of rejecting. Added web/src/api/client.test.ts coverage that
exercises the real req() against a stubbed fetch for both the 201
(addCustom) and 204 (removeCustom) empty-body cases.
Also brought internal/store/queries/domains.sql's DeleteDomainsNotInZones
query back in sync with the hand-maintained internal/store/db/domains.sql.go
(RETURNING * -> explicit column list) since sqlc isn't installed in this
environment and the two files are kept aligned by hand.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yr8frsaxBgab1Aa7yfPuU
Document the two Task 1-9 features in CLAUDE.md invariants and README
features. Also closes two review gaps: a Go test proving importResponse
marshals created/removed as [] (never null) on a no-op import, and a
frontend test-race fix in DomainsPage.test.tsx where the import button
was clicked before its disabled state cleared.
Add a "Clone" action to the templates table: it fills the create form
with a copy of the selected template's name and records instead of
creating the copy right away, so the operator can adjust the name or
records before saving. No new API — the existing POST /templates is
reused.
Cloning resets editingId so the submit goes to createTemplate even when
edit mode was active, and the copy name is picked to be free among the
listed templates ("X (копия)", then "X (копия 2)"). Template names have
no unique constraint in the DB, so this is a UX hint, not a guarantee.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yr8frsaxBgab1Aa7yfPuU
RecordRow now splits into a top line (badge/name/read-only, unaffected
by value length) and a plain block-level values line below it, so a
~400-char unbreakable DKIM key wraps via break-all instead of stretching
the flex row and forcing page-wide horizontal scroll. Zone records table
gets break-all on the values cell too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
toChangesetResponse initialises updates/prunes/readOnly so a zone matching
its template exactly (e.g. right after 'create template from zone') marshals
arrays, not null. DiffView/DomainDiffPage also normalise null defensively.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
Replace the single API-key field with 4 IAM service-user fields
(username, password, account_id, project_name) matching the new
backend contract; map 400 "invalid provider credentials" to a
user-facing message.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3