* feat(dns): support IP version selection for DDNS
- Add ip_version setting (ipv4 / ipv6 / ipv4_ipv6 / ipv6_ipv4 /
both_required) persisted on DDNSConfig and exposed via the API
- Validate target record types against the selected version and reject
inconsistent combinations on save
- Probe public IPv4 and IPv6 endpoints concurrently so a stalled family
no longer eats the shared timeout budget
- Atomically create A/AAAA records when adding missing names and roll
back partial successes on failure
- Surface per-family resolution warnings instead of failing the whole
tick when at least one family resolves
- Frontend exposes an IP version selector and filters record options to
the families allowed by the active selection
* fix(dns): preserve DDNS missing record creation
Co-authored-by: Jacky <me@jackyu.cn>
* fix(dns): restore best-effort dual-stack semantics in record creation
Two fixes after reviewing a cursor agent commit that introduced
regressions:
- DDNSManager.vue: revert mode="tags" back to mode="multiple" and
drop the now-dead findSelectedRecordType helper. The placeholder
text is also updated to remove the misleading "Type or" prefix
since multiple-mode does not allow free input.
- createDDNSRecordsForMissingName: revert the unified "create whatever
family is available" behaviour. best-effort modes (ipv4_ipv6 /
ipv6_ipv4) again create only the first available family in policy
order; both_required keeps creating both atomically.
* refactor(dns): remove both_required DDNS mode
The "all-or-nothing" runtime semantic of both_required conflicts with the
user-accessibility goal of the upcoming sibling cleanup logic, and the
mode itself overlaps with dual-stack best-effort once cleanup exists.
Remove the constant, policy case, validation function, runtime
short-circuit, and error code.
* test(dns): drop both_required test coverage
* feat(dns/ui): drop both_required option from DDNS mode select
* feat(dns): persist cleanup flag and family failure timestamps
Add CleanupConflictingRecords (default true), IPv4FailedSince, and
IPv6FailedSince to model.DDNSConfig and the request/response DTOs.
toDDNSResponse seeds CleanupConflictingRecords=true for unconfigured
domains so the frontend form starts in the desired default state.
The new fields are wired through but no behavior changes yet.
* feat(dns): add isDualStackMode helper
* refactor(dns): silently skip records outside policy during save
UI filters records to the active IP version policy, so an explicit
mismatch error only fires for stale form state or direct API misuse.
Silent skip is more graceful and lets the existing empty-targets
check surface the real failure mode (ErrDDNSTargetRequired).
* feat(dns): auto-pair existing sibling records on save (dual-stack, flag on)
* feat(dns): auto-create missing sibling records on save (dual-stack, flag on)
* feat(dns): delete records of unreachable families on save
When dual-stack mode is active and CleanupConflictingRecords is on,
sibling records at managed names whose family is currently unreachable
get deleted from the provider. The handler returns the deleted-record
list so the frontend can surface a confirmation toast.
* test(dns): cover flag-off and single-stack save-time behavior
* feat(dns): track per-family IP detection failure timestamps
* feat(dns): evict targets of persistently failed families
Dual-stack DDNS configs with CleanupConflictingRecords enabled now
delete records of any family whose public IP has been undetectable for
longer than ddnsFamilyFailureGrace (default 1 hour). Single-stack
modes and the flag-off path skip this branch entirely.
* test(dns): cover runtime no-eviction and delete-failure retry paths
* feat(dns/ui): add cleanup conflicting records toggle (dual-stack only)
* feat(dns/ui): notify users of unmanaged sibling records in single-stack modes
* feat(dns/ui): toast when conflicting records are removed on save
* style(dns/ui): fix indent-binary-ops lint warning
* fix(dns): refuse save when no public IP detected (dual-stack cleanup mode)
Previously the §6.3 completion phase would happily delete every existing
sibling record of unreachable families even when neither family was
detected, leaving cfg.Targets empty and the domain at NXDOMAIN. Now we
short-circuit with ErrDDNSIPUnavailable before running §6.3, preserving
the user's DNS state until they recover connectivity.
* fix(dns): delete in-target records when family IP becomes unreachable
The §6.3 completion phase short-circuited on containsTargetForName
before checking whether the family's IP was still reachable. That meant
a user-selected A record at "home" survived a save under ipv4_ipv6 +
cleanup-on even when IPv4 stopped resolving, contradicting the
Appendix A "✗ / ✓ / on" row of the design spec.
Move the in-targets check inside the IP-detected branch so the
IP-undetected branch can still delete the stale record and pull it
out of targets. Adds tests covering the cross-family pivot plus the
previously-missing spec §10.2 cases (#4, #7, #11, #12, #13).
* fix(dns): persist cleanup flag and preserve failure timestamps on save
UpdateDDNSConfigWithDetails was constructing the new cfg without
carrying CleanupConflictingRecords from the input and without
preserving IPv4FailedSince / IPv6FailedSince from the existing
config. The former meant runtime eviction never triggered in
production (the cfg was always persisted with the flag at zero
value); the latter meant every save reset the family failure
grace timer to nil, indefinitely delaying eviction.
Both gaps slipped through the test matrix because no test
reloaded the cfg from the database after save. Add round-trip
regression tests for both fields plus the new GetDDNSConfig
default alignment.
Also surface delete-record provider failures via a dedicated
ErrDDNSRecordDeleteFailed code so users can distinguish them
from genuine "record not found" cases.
* fix(dns): use standard RFC3339 time format
Co-authored-by: Jacky <me@jackyu.cn>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jacky <me@jackyu.cn>
* feat: dns management
* refactor(dns): streamline domain management functions and enhance validation
* feat(dns): add value suggestions for DNS record input with autocomplete functionality
* fix(dns): handle edge case in record listing pagination
* fix(dns): update credential property name for consistency and add cleanup on component unmount
* feat(dns): implement DDNS management #1194, #1140