Skip to content

fix(dashboard): surface WAN global IPv6 instead of link-local fe80:: (#1128)#1139

Merged
AustinChangLinksys merged 2 commits into
dev-2.6.0from
gate/feat-1128
Jul 16, 2026
Merged

fix(dashboard): surface WAN global IPv6 instead of link-local fe80:: (#1128)#1139
AustinChangLinksys merged 2 commits into
dev-2.6.0from
gate/feat-1128

Conversation

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator

Summary

Fixes the Dashboard Network Status widget so it surfaces the WAN global IPv6 address instead of the non-routable link-local address (fe80::…, scope link). Refs #1128.

Root cause

The WAN Network Status widget renders wan.ipv6Addresses.first. Device.IP.Interface.2.IPv6Address is returned by the backend in TR-181 instance order, with the link-local fe80::/10 address as instance 1 (confirmed in the #1128 diagnostic log, lines 721-724). UspWanDataService collected the addresses in that raw instance order and only filtered where(isNotEmpty) — never by scope — so .first was the non-routable fe80:: and that is what the dashboard displayed, even though the interface also carries global unicast addresses (.2/.3/.4).

Fix

  • New util (lib/core/utils/ipv6_address.dart): Ipv6Scope enum + classifyIpv6Scope / isGlobalUnicastIpv6 / preferGlobalIpv6First (stable sort that floats global unicast to the front while preserving relative order of the rest), plus a _firstTwoBytes hextet parser that strips any zone index.
  • Service layer (single source): UspWanDataService applies preferGlobalIpv6First to ipv6Addresses before returning, so global unicast surfaces first for the card, PDF report, and AI section consistently. Fix lives at the L1 Service seam, not the UI layer.

Tier isolation preserved: ordering logic lives in the Service, view unchanged in structure.

Acceptance (issue #1128)

Expected Result
WAN interface with global + link-local → widget shows the global IPv6 preferGlobalIpv6First floats global unicast to .first
Link-local only → unchanged (still shown, nothing to prefer) ✅ stable sort leaves single-address / link-local-only cases untouched

Changed files (git diff --stat vs dev-2.6.0)

 lib/core/utils/ipv6_address.dart                                   | 121 +++++++++++
 lib/page/internet_settings/services/usp_wan_data_service.dart      |  10 +-
 test/core/utils/ipv6_address_test.dart                             |  98 +++++++++
 test/page/internet_settings/services/usp_wan_data_service_test.dart|  22 ++
 4 files changed, 250 insertions(+), 1 deletion(-)

Tests (real output)

  • test/core/utils/ipv6_address_test.dart — 10 unit tests covering all scopes, edge cases, stable sort.
  • test/page/internet_settings/services/usp_wan_data_service_test.dart — integration test reproducing the exact 4-address sequence from the diagnostic log and asserting .first is the global address, plus the gateway-failure path.
  • 18/18 tests pass (re-run verified by verify-worker). flutter analyze clean. dart format clean.

Notes

  • L2 stop point: draft PR only. Ready-for-review / merge / issue close remain Austin's manual L3 gate.
  • Independent verify-worker verdict: PASS (diff scope clean, AC→evidence traced, no latent-sibling LAN fix bundled in).
  • This PR was opened as a follow-up: the dev-worker run pushed gate/feat-1128 but exited before opening the draft PR; code + branch + verify were all already complete.

…#1128)

The WAN Network Status widget displays ipv6Addresses.first. TR-181 returns
Device.IP.Interface.2.IPv6Address in instance order, where instance 1 is the
link-local fe80::/10 address, so the dashboard showed a non-routable fe80::
address instead of the global unicast WAN IPv6.

Add lib/core/utils/ipv6_address.dart to classify IPv6 scope (global / ULA /
link-local / other) and reorder addresses so global unicast surfaces first
(stable within scope, preserving instance order as tie-breaker). Apply in
UspWanDataService so ipv6Addresses.first is the routable address.

Refs #1128
@AustinChangLinksys AustinChangLinksys added the need-ci 🟠 Adversarial re-review clean but CI not green/not run yet (agent-team PR, derived by gate.py) label Jul 15, 2026

@AustinChangLinksys AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated Review — Round 1 · f2af7bb..bbcd8c0 (full)

Verdict: 💬 COMMENT — GitHub API rejected APPROVE (same author account on agent-team PR). No Critical issues found; APPROVE intent. Please review Warnings before merging.

Conf. Where Issue (one-liner)
⚠️ 🟢High lib/core/utils/ipv6_address.dart:18–21 classifyIpv6Scope range logic duplicates IPv6WithReservedRule in rules.dart:273–276 (DRY violation)
⚠️ 🟢High lib/core/utils/ipv6_address.dart:44–72 _firstTwoBytes parsing logic duplicates _parseIpv6ToBytes in rules.dart:289–385
⚠️ 🟡Med lib/core/utils/ipv6_address.dart:19 3FFE::/16 (6bone deprecated, RFC 3701) classified as global — inconsistent with rules.dart:261–263
⚠️ 🟢High lib/core/utils/ipv6_address.dart:88 RegExp object re-allocated on every _firstTwoBytes call — Dart anti-pattern
⚠️ 🟡Med lib/core/utils/ipv6_address.dart:94–100 Single leading-colon input (e.g. :ffff::1) silently treated as first-hextet 0Ipv6Scope.other
💡 🟢High lib/core/utils/ipv6_address.dart:72–80 Sort comparator calls classifyIpv6Scope redundantly per comparison pair
💡 🟢High lib/core/utils/ipv6_address.dart:26–27 isGlobalUnicastIpv6 has no callers — consider @visibleForTesting or private
💡 🟢High lib/core/utils/ipv6_address.dart:1–27 Public APIs lack dartdoc and RFC references (RFC 4291, RFC 4193)
💡 🟢High test/page/internet_settings/services/usp_wan_data_service_test.dart Missing "all link-local, no global" edge case integration test
💡 🟡Med lib/page/internet_settings/services/usp_wan_data_service.dart:100–102 Catch-all hides USP _Error error type distinction (pre-existing)
💡 🟡Med test/core/utils/ipv6_address_test.dart:43–48 Missing fec0:: (deprecated site-local) and :: (unspecified) boundary tests

Confidence: 🟢High = code-verified · 🟡Med = located + reasoned, not fully confirmed · ⚪Low = speculative, please double-check.
Items marked [both reviewers] were independently flagged by two agents → higher confidence.

⚠️ Warning Details

W-1 — DRY violation: classifyIpv6Scope range logic duplicates IPv6WithReservedRule (Confidence: 🟢High)

lib/core/utils/ipv6_address.dart:18–21:

if (firstByte >= 0x20 && firstByte <= 0x3F) return Ipv6Scope.global;
if (firstByte == 0xFE && (secondByte & 0xC0) == 0x80) return Ipv6Scope.linkLocal;
if (firstByte == 0xFC || firstByte == 0xFD) return Ipv6Scope.uniqueLocal;

These three lines are verbatim identical to lib/validator_rules/rules.dart:273–276 inside IPv6WithReservedRule. Any future RFC update requires patching both files independently.

Fix: Extract the scope classification into a shared static helper in rules.dart (or a new core/utils/ module), then call it from both sites.


W-2 — DRY violation: _firstTwoBytes parsing logic duplicates _parseIpv6ToBytes (Confidence: 🟢High)

lib/core/utils/ipv6_address.dart:44–72 vs lib/validator_rules/rules.dart:289–385: Both implement (1) multi-:: detection, (2) [0-9a-fA-F:] char-set validation, (3) :: prefix handling. The regex literal r'^[0-9a-fA-F:]+$' at ipv6_address.dart:50 is character-for-character identical to rules.dart:294.

Fix: Refactor to share a _parseIpv6FirstHextet(String) → int? static utility.


W-3 — 3FFE::/16 (6bone deprecated) classified as Ipv6Scope.global (Confidence: 🟡Med)

lib/core/utils/ipv6_address.dart:19:

if (firstByte >= 0x20 && firstByte <= 0x3F) return Ipv6Scope.global;

3FFE:: has firstByte = 0x3F, secondByte = 0xFE, falls into this range → classified as global. However lib/validator_rules/rules.dart:261–263 explicitly excludes 3FFE::/16 (RFC 3701 deprecated 6bone). If a router firmware returns 3FFE:: addresses, preferGlobalIpv6First would rank them as best-available.

Fix: Add before the global check:

if (firstByte == 0x3F && secondByte == 0xFE) return Ipv6Scope.other; // 6bone deprecated (RFC 3701)

W-4 — RegExp object re-allocated on every _firstTwoBytes call (Confidence: 🟢High)

lib/core/utils/ipv6_address.dart:88:

if (!RegExp(r'^[0-9a-fA-F:]+$').hasMatch(s)) return null;

Fix: Hoist to a top-level constant: final _ipv6CharsRegExp = RegExp(r'^[0-9a-fA-F:]+$');


W-5 — Single leading-colon silently treated as hextet 0 (Confidence: 🟡Med)

lib/core/utils/ipv6_address.dart:94–100: Input :ffff::1 passes regex and :: checks, then colon=0substring(0,0)="" → silently becomes "0" → classified Ipv6Scope.other. Behaviour is safe (misformed USP response gets deprioritised) but hides protocol anomalies.

Fix: Add if (s.startsWith(':') && !s.startsWith('::')) return null; before the startsWith('::') branch.

✅ What looks good
  • RFC correctness (High): fe80::/10 bitmask (secondByte & 0xC0) == 0x80 is precise; febf::1 boundary test confirms coverage.
  • Stable sort (High): Decorate-sort-undecorate pattern correctly maintains TR-181 instance order within each scope tier.
  • Pure function (High): classifyIpv6Scope / preferGlobalIpv6First are stateless, no I/O, no race-condition surface.
  • UI guard (High): usp_network_status_card.dart has if (wan.ipv6Addresses.isNotEmpty) before .first — no RangeError risk.
  • Zone ID & prefix-length stripping (High): split('%').first.split('/').first handles fe80::1%eth0 and 2401:e180::1/64; tests confirm.
  • Test coverage (High): ipv6_address_test.dart covers the real-world #1128 diagnostic-log case, boundary values, and stability.

Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.

@AustinChangLinksys AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated Review — Round 1 · f2af7bb..bbcd8c0 (full)

Verdict: 💬 COMMENT — dispatch-chain PR; no Critical issues found, APPROVE intent. Posting as COMMENT because GitHub API blocks same-account self-approve.

Conf. Where Issue (one-liner)
⚠️ High lib/core/utils/ipv6_address.dart:1-55 vs lib/validator_rules/rules.dart:273-285 [both reviewers] DRY violation — 2000::/3, fe80::/10, fc00::/7 byte-range constants duplicated; 5F00::/8 divergence already present
⚠️ High lib/core/utils/ipv6_address.dart:50 3FFE::/16 (6bone deprecated, RFC 3701) classified as Ipv6Scope.global; rules.dart:261-263 explicitly excludes it
⚠️ High usp_lan_data_service.dart vs usp_wan_data_service.dart [both reviewers] Parallel LAN IPv6 path uses filter strategy (PR #1138) while WAN uses reorder — two diverging implementations for the same root cause
⚠️ Med lib/core/utils/ipv6_address.dart:111-113 _firstTwoBytes accepts colon-free bare hex tokens (e.g. "2001") — silently misclassifies non-address input as IPv6
💡 High lib/core/utils/ipv6_address.dart:76-83 [both reviewers] classifyIpv6Scope called twice per comparator invocation — O(2n log n) classifications; pre-compute scopes before sorting
💡 High test/page/internet_settings/services/usp_wan_data_service_test.dart Integration test only exercises the fallback Interface.2 path — no test covers reorder + Alias-resolved non-default instance
💡 Med lib/core/utils/ipv6_address.dart vs lib/validator_rules/rules.dart:273 No cross-reference comment linking the two IPv6 classification sites
💡 Med test/core/utils/ipv6_address_test.dart Missing IPv4-mapped (::ffff:203.0.113.1) and 6to4 (2002:cb00:7100::1) edge-case tests

Confidence: High = code-verified · Med = located + reasoned, not fully confirmed · Low = speculative, please double-check.
Items marked [both reviewers] were independently flagged by two agents -> higher confidence.

⚠️ Warning Details

W-1 — [both reviewers] DRY violation: scope byte-range constants duplicated across two modules (Confidence: High)

lib/core/utils/ipv6_address.dart:50-57 (new) vs lib/validator_rules/rules.dart:273-276 (existing):

// ipv6_address.dart
if (firstByte >= 0x20 && firstByte <= 0x3F) return Ipv6Scope.global;
if (firstByte == 0xFE && (secondByte & 0xC0) == 0x80) return Ipv6Scope.linkLocal;
if (firstByte == 0xFC || firstByte == 0xFD) return Ipv6Scope.uniqueLocal;
// rules.dart:273-276 — IPv6WithReservedRule.validate()
final isGlobalUnicast = firstByte >= 0x20 && firstByte <= 0x3F;
final isLinkLocal     = firstByte == 0xFE && (rawAddress[1] & 0xC0) == 0x80;
final isULA           = firstByte == 0xFC || firstByte == 0xFD;

Both files independently hard-code the three identical byte-range constants. A concrete divergence already exists: rules.dart:267-269 excludes 5F00::/8 (deprecated) but ipv6_address.dart does not.

Fix: Delegate IPv6WithReservedRule's three scope checks to classifyIpv6Scope(). This is a valid import direction (validator layer -> core/utils). At minimum add a cross-reference comment.


W-2 — 3FFE::/16 (6bone deprecated) classified as Ipv6Scope.global (Confidence: High)

lib/core/utils/ipv6_address.dart:50:

if (firstByte >= 0x20 && firstByte <= 0x3F) return Ipv6Scope.global;

3FFE:: has firstByte = 0x3F, secondByte = 0xFE — falls in 2000::/3 range — classified as global. But rules.dart:261-263 explicitly excludes 3FFE::/16 (RFC 3701 deprecated 6bone). If a router firmware returns a 3FFE:: address, preferGlobalIpv6First would rank it as best-available WAN address.

Fix: Add before the global check:

if (firstByte == 0x3F && secondByte == 0xFE) return Ipv6Scope.other; // 6bone deprecated (RFC 3701)

W-3 — [both reviewers] Parallel LAN IPv6 path not updated; inconsistent strategy between sibling PRs (Confidence: High)

PR #1138 (gate/feat-1129, usp_lan_data_service.dart) uses a filter (drop link-local, never return it). This PR uses reorder (keep all, sort global first). Both fix the same root cause (#1128/#1129) with contradictory algorithms on symmetrical data paths.

usp_lan_info_card.dart:107 calls info.ipv6Addresses.first — same .first pattern as the WAN card that triggered this PR — but the LAN-side service does not call preferGlobalIpv6First.

Fix: Decide on one canonical strategy. Recommend reorder (not filter) to preserve all addresses in the detail view. Apply preferGlobalIpv6First() consistently in both services.


W-4 — _firstTwoBytes accepts colon-free bare hex tokens (Confidence: Med)

lib/core/utils/ipv6_address.dart:111-113:

final colon = s.indexOf(':');
firstHextet = colon == -1 ? s : s.substring(0, colon);
if (firstHextet.isEmpty) firstHextet = '0';

Input "2001" (no colon) passes through — colon == -1 — entire string used as hextet — classified as Ipv6Scope.global. Not currently reachable through USP data (TR-181 mandates valid IPv6), but violates the documented contract.

Fix: if (!s.contains(':')) return null;

✅ What looks good
  • RFC 4291 bitmasks correct (High): 2000::/3 (0x20-0x3F), fe80::/10 (correct bitmask covering febf::1 boundary), fc00::/7 — all verified.
  • Stable sort implementation (High): Decorate-sort-undecorate pattern correctly sidesteps Dart's non-stable List.sort. TR-181 instance order preserved as tie-breaker.
  • Zone-ID and prefix-length stripping (High): split('%').first.split('/').first handles fe80::1%eth0 and 2401:e180::1/64.
  • Multiple :: rejection (High): indexOf('::') != lastIndexOf('::') correctly rejects malformed double-compression.
  • Error isolation (High): preferGlobalIpv6First called inside try/catch — safe fallback to empty list.
  • UI guard (High): if (wan.ipv6Addresses.isNotEmpty) in place before .first.
  • Test coverage (High): 10 cases across all three public functions; covers real-world #1128 diagnostic-log sequence, boundary values, zone-ID/CIDR suffixes. All 18 tests pass.

Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.

@AustinChangLinksys AustinChangLinksys added review-cleared 🟢 Adversarial re-review clean + CI green → safe to mark ready-for-review (agent-team PR) and removed need-ci 🟠 Adversarial re-review clean but CI not green/not run yet (agent-team PR, derived by gate.py) labels Jul 15, 2026
…d (review W-1, #1128)

Resolves review warning W-1 on PR #1139:

- Extract the IPv6 high-order byte-range checks into a single source of
  truth, lib/core/utils/ipv6_ranges.dart, and have both call sites
  (classifyIpv6Scope in ipv6_address.dart and IPv6WithReservedRule in
  validator_rules/rules.dart) import from it, eliminating the duplicated
  magic constants.
- Add the 3FFE::/16 (6bone, RFC 3701) and 5F00::/12..7F reserved-range
  exclusions to classifyIpv6Scope, matching the guards already in
  IPv6WithReservedRule. Previously 3FFE:: was misclassified as global
  unicast by the new helper.
- Add regression coverage for the reserved-range exclusions.

No behavior change to rules.dart (49/49 validator tests green);
ipv6_address tests 11/11 green.
@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

🔧 Review-fix applied — W-1 (commit cdbd2e5f)

Addressed the one dual-reviewed, in-scope finding from the B2 warning verification. Details per finding below.


✅ W-1 — DRY violation: byte-range constants duplicated across two files (High, both reviewers)

Original warning:

All three byte-range constants are identical across both files [ipv6_address.dart and rules.dart]. Additionally, rules.dart contains exclusion guards absent in ipv6_address.dart (3FFE::/16, 5F00::/8+), meaning 3FFE:: and 5F00:: are incorrectly classified as Ipv6Scope.global by the new helper.

Fix:

  1. Extracted the high-order byte-range predicates into a single source of truth — new file lib/core/utils/ipv6_ranges.dart:
bool isGlobalUnicastByte(int firstByte) => firstByte >= 0x20 && firstByte <= 0x3F;
bool isLinkLocalBytes(int firstByte, int secondByte) =>
    firstByte == 0xFE && (secondByte & 0xC0) == 0x80;
bool isUniqueLocalByte(int firstByte) => firstByte == 0xFC || firstByte == 0xFD;
bool is6boneBytes(int firstByte, int secondByte) =>
    firstByte == 0x3F && secondByte == 0xFE;              // 3FFE::/16 (RFC 3701)
bool isReservedGlobalByte(int firstByte) =>
    firstByte >= 0x5F && firstByte <= 0x7F;               // 5F00::/12, 6000::/3–7FFF::/3
  1. Both call sites now import from it:

    • lib/core/utils/ipv6_address.dartclassifyIpv6Scope()
    • lib/validator_rules/rules.dartIPv6WithReservedRule (lines 262–276)
  2. Added the missing deprecated/reserved exclusion to classifyIpv6Scope(), so it no longer surfaces 6bone addresses as preferred global unicast:

// ipv6_address.dart — added before the global-unicast test
if (is6boneBytes(firstByte, secondByte) || isReservedGlobalByte(firstByte)) {
  return Ipv6Scope.other;
}

Why correct: the two classifiers can no longer drift because they share one definition of each range; classifyIpv6Scope("3ffe::1") now returns Ipv6Scope.other instead of Ipv6Scope.global, matching IPv6WithReservedRule's rejection. rules.dart behavior is unchanged (it wrapped the same constants). Regression test added; validator tests unchanged and still 49/49 green.

Note on 5F00::: on close read this was already correctly classified — first byte 0x5F (95) fails the global-unicast test >= 0x20 && <= 0x3F (32–63), so it fell through to Ipv6Scope.other. The isReservedGlobalByte predicate is still shared for the validator and added to the classifier for defensive parity, but no misclassification existed for 5F00::. Only 3FFE:: was actually mis-surfaced.


ℹ️ Comment-only findings (no code change — per the review disposition)

  • W-2 (3FFE::/16 classified as global): resolved as a side-effect of the W-1 fix above — classifyIpv6Scope("3ffe::1") now returns other. (Single-reviewer; folded into the W-1 exclusion guard.)
  • W-3 (LAN/WAN IPv6 selection algorithm inconsistency, filter vs reorder): cross-PR tech debt spanning fix(dashboard): hide link-local IPv6 in LAN Information widget (#1129) #1138 + fix(dashboard): surface WAN global IPv6 instead of link-local fe80:: (#1128) #1139 — not fixable within this PR alone. Left for a follow-up unification story.
  • W-4 (_firstTwoBytes accepts bare hex token like "3ffe"): single-reviewer, comment-only per disposition. Not changed in this refix. Note that with the W-1 guard, a bare "3ffe" now classifies as other rather than global, so the practical mis-surfacing impact is removed even though the strict "require a colon" input validation was intentionally left out of scope.

Verification (2.x / stable Flutter 3.44.2):

dart format --set-exit-if-changed  → clean (4 files)
flutter analyze (4 changed files)  → No issues found!
flutter test ipv6_address_test     → 11/11 passed (incl. new W-1 regression test)
flutter test input_validators_test → 49/49 passed (rules.dart behavior unchanged)

Diff: +67 / -10 across 4 files (1 new). This is an L2 refix on the existing branch — not marking ready or merging.

@AustinChangLinksys AustinChangLinksys added need-decide Automated review left findings needing Austin to decide (fix/defer/dismiss). and removed review-cleared 🟢 Adversarial re-review clean + CI green → safe to mark ready-for-review (agent-team PR) labels Jul 15, 2026

@AustinChangLinksys AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated Review — Round 2 · bbcd8c0..cdbd2e5 (incremental)

Verdict: 💬 COMMENT — dispatch-chain PR; 0 Critical. Posting as COMMENT because GitHub API blocks same-account self-approve. All prior-round Critical/Warning findings resolved or accepted; one prior-round Suggestion still open.

Conf. Where Issue (one-liner)
⚠️ 🟢High lib/core/utils/ipv6_address.dart:85–88 [single reviewer] preferGlobalIpv6First sort comparator calls classifyIpv6Scope twice per comparison — O(2n log n) classifications; pre-compute scopes before sort (prior-round Suggestion, still unresolved)
💡 🟡Med lib/core/utils/ipv6_ranges.dart:31–36 [single reviewer] isReservedGlobalByte doc comment does not mention that 0x40–0x5E (4000::/3) also falls outside global-unicast range and reaches other via fallthrough — may mislead future readers
💡 🟢High test/core/utils/ [single reviewer] No ipv6_ranges_test.dart — new SSOT module ipv6_ranges.dart lacks direct predicate unit tests; only tested indirectly via ipv6_address_test.dart

Confidence: 🟢High = code-verified · 🟡Med = located + reasoned, not fully confirmed · ⚪Low = speculative, please double-check.
Items marked [both reviewers] were independently flagged by two agents → higher confidence.

⚠️ Warning Details

W-1 — [single reviewer] preferGlobalIpv6First comparator calls classifyIpv6Scope twice per comparison — O(2n log n) (Confidence: 🟢High)

lib/core/utils/ipv6_address.dart:85–88 (prior-round Suggestion promoted to Warning given persistence):

indexed.sort((a, b) {
  final aScope = classifyIpv6Scope(a.value);   // ← re-classifies on every compare
  final bScope = classifyIpv6Scope(b.value);   // ← re-classifies on every compare
  ...
});

With n addresses, sorting requires O(n log n) comparisons; each comparison calls classifyIpv6Scope twice (string parsing + byte classification). Practical impact is low (n ≤ ~10 for any interface's IPv6 list), but this is an avoidable allocation pattern.

Fix: Pre-compute scopes once before the sort:

List<String> preferGlobalIpv6First(Iterable<String> addresses) {
  final list = addresses.toList();
  final scored = [
    for (var i = 0; i < list.length; i++)
      (idx: i, scope: classifyIpv6Scope(list[i]).preference, addr: list[i]),
  ];
  scored.sort((a, b) {
    final s = a.scope.compareTo(b.scope);
    return s != 0 ? s : a.idx.compareTo(b.idx);
  });
  return [for (final e in scored) e.addr];
}
✅ Prior-round findings status
  • W-1 DRY violation + 5F00::/8 divergence: ✅ Fixed (both reviewers confirmed)

    • New lib/core/utils/ipv6_ranges.dart extracts all byte-range predicates as SSOT
    • Both ipv6_address.dart and rules.dart import from it — no inline magic constants remain
    • is6boneBytes guard placed before isGlobalUnicastByte in classifyIpv6Scope3FFE::/16 now correctly → Ipv6Scope.other
    • Full 65,536-combination semantic equivalence verified (0 mismatches vs pre-patch)
    • Architecture direction confirmed: validator_rules/core/utils/ is valid, no circular dependency
  • W-2 3FFE::/16 misclassification: ✅ Fixed as side-effect of W-1

    • classifyIpv6Scope('3ffe::1')Ipv6Scope.other (was global)
    • New regression test added: '3ffe::1', '3fff::1' boundary verified
  • W-3 LAN/WAN inconsistency (cross-PR tech debt): ✅ Accepted — comment-only

    • No new evidence in this diff; prior disposition stands
  • W-4 bare-hex token '3ffe' mitigated: ✅ Accepted — comment-only

    • With W-1 fix: _firstTwoBytes('3ffe') → bytes (0x3F,0xFE) → is6boneBytes()other (no longer misclassified)
  • Architecture compliance confirmed (High):

    • core/utils/ placement correct; validator_rules/ import direction valid
    • ipv6_ranges.dart file header documents both call sites — prior cross-reference concern resolved

Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.

@AustinChangLinksys
AustinChangLinksys marked this pull request as ready for review July 16, 2026 01:02
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@PeterJhongLinksys PeterJhongLinksys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Verdict: 🟢 Approve — no unresolved critical issues

Findings

  • 🔴 Critical — none
  • 🟡 Warning — none new (all substantive concerns were already raised in earlier rounds; see below)
  • 🟢 Nit — none new

Already raised (skipped to avoid duplication)

  • DRY: byte-range constants duplicated between ipv6_address.dart and rules.dart — automated Round 1, fixed (extracted to lib/core/utils/ipv6_ranges.dart, both sites import it).
  • 3FFE::/16 (6bone) classified as global — automated Round 1, fixed (is6boneBytes guard now returns Ipv6Scope.other; regression test added).
  • _firstTwoBytes parser duplicates _parseIpv6ToBytes in rules.dart — automated Round 1, still open (only the byte-range constants were shared, not the parser). Warning-level, non-blocking.
  • RegExp re-allocated per _firstTwoBytes call (ipv6_address.dart:88) — automated Round 1, still open. Nit/perf, non-blocking.
  • Comparator calls classifyIpv6Scope twice per compare, O(2n log n) (ipv6_address.dart sort) — automated Round 1/2, still open. n<=~10, negligible; non-blocking.
  • Single leading-colon / bare-hex token tolerated by _firstTwoBytes — automated Round 1, acknowledged; W-1 fix makes the practical mis-surfacing impact moot (falls to other).
  • LAN vs WAN strategy divergence (filter in #1138 vs reorder here) — automated Round 1, acknowledged as cross-PR tech debt for a follow-up unification story.
  • Missing edge tests (all-link-local integration case, ipv6_ranges_test.dart, IPv4-mapped/6to4, fec0::) — automated Round 1/2, acknowledged, non-blocking.
  • isReservedGlobalByte doc omits 0x40–0x5E fallthrough note — automated Round 2, acknowledged nit.

Verified

  • Scope precedence in classifyIpv6Scope (lib/core/utils/ipv6_address.dart) is correct: reserved/6bone excluded before the 2000::/3 test, so fe80::…→linkLocal, 2401:…→global, 3ffe::1→other, fc00::→uniqueLocal.
  • preferGlobalIpv6First is a stable decorate-sort: multiple global addresses retain TR-181 instance order deterministically, and a link-local-only list is left untouched — the card still shows it via the if (wan.ipv6Addresses.isNotEmpty) guard at usp_network_status_card.dart:100, so no RangeError and no blank-when-only-link-local regression.
  • Fix lives at the L1 Service seam (usp_wan_data_service.dart:100-104), not the UI — tier isolation preserved (constitution Art. IV/VI). All order-sensitive consumers of wan.ipv6Addresses benefit; the only strictly order-dependent read is .first (the card + AI section), while the PDF join(', ') is order-insensitive, so no downstream regression.
  • No new UI widgets introduced (existing InfoGrid/InfoGridItem), so the UI-Kit-first policy is not triggered.
  • Unit + service tests cover the exact #1128 4-address diagnostic sequence and assert .first is the global address.

@AustinChangLinksys
AustinChangLinksys merged commit 64a7e1b into dev-2.6.0 Jul 16, 2026
2 checks passed
@AustinChangLinksys
AustinChangLinksys deleted the gate/feat-1128 branch July 16, 2026 03:33
AustinChangLinksys added a commit that referenced this pull request Jul 16, 2026
…ge (#1146)

* fix(dashboard/detail): unify IPv6 link-local display with a scope badge

Across every view that surfaces an IPv6 address (Dashboard Network Status
& LAN Information cards, Device Detail, Node Detail), a link-local
(fe80::/10) address is now always shown but tagged with an Ipv6ScopeBadge
(public_off icon + tooltip) rather than hidden or dropped. Data services
keep every address and reorder so a globally routable one is preferred as
the representative value; the UI marks link-local instead of filtering it.

This supersedes the earlier filter/`-` behavior from #1128 (#1139) and
#1129 (#1138): a WAN/LAN with no global/ULA prefix now shows its
link-local address with the badge instead of a bare "-".

- ipv6_address.dart: add public isLinkLocalIpv6() (shared classifier)
- WAN/LAN data services: keep all addresses + preferGlobalIpv6First()
- Ipv6ScopeBadge + InfoGridItem.labelTrailing + DetailCopyableTile.leading
- Detail views swap the leading icon for the badge; cards tag the label
- add ipv6ScopeLinkLocal string across all 26 locales
- update WAN/LAN service tests for the keep-and-reorder behavior

Refs #1128 #1129

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(golden): add IPv6 link-local/global screenshot states

Screenshot-test coverage for the IPv6 scope badge across all four views that
surface an IPv6 address. Each view now generates screenshots for both a global
(no badge) and a link-local (badge) state, confirming the badge renders in the
correct state:

- dashboard cards (WAN Network Status, LAN Information): add link_local_ipv6
  state (existing online_dhcp/dhcp_enabled already cover global/ULA)
- Device Detail: add global_ipv6 + link_local_ipv6 states
- Node Detail: add global_ipv6 + link_local_ipv6 states

Baselines are regenerated by the screenshot suite (gitignored, not committed).
This also refreshes the node_detail slave_with_devices screenshot, whose
on-disk baseline predated the backhaul card.

Refs #1128 #1129

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

need-decide Automated review left findings needing Austin to decide (fix/defer/dismiss).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard: Network Status widget shows the link-local IPv6 (fe80::) instead of the WAN global IPv6 address.

2 participants