Commit 8bbe987
* feat: add VisibilityConfig model for entity visibility filter
* feat: add atomic load/save for entity_visibility.json
* feat: add pure resolver for entity visibility hidden set
* feat: add fail-open off-loop loader for the visibility hidden set
* feat: apply entity visibility filter to ha_search exact + domain paths
* feat: apply entity visibility filter to smart_search area + overview paths
* test: add e2e + runnable Tier-B coverage for entity visibility filter
Add tests/src/e2e/test_entity_visibility.py: exercises the filter end to end
through the ha_search MCP tool against a live HA. An enabled denylist removes
the entity from search results (with post-filter count coherence), while a
targeted ha_get_state still returns it – the Tier-B contract. Marked
external_only because the HAOS-inaddon backend runs the server in a separate
addon container, where in-process redirection of the config dir cannot reach
it; the component path is backend-identical, so the container run covers it.
Add a runnable in-process Tier-B test to the visibility unit suite that drives
_get_single_entity_state directly, so the "targeted reads are never filtered"
contract is covered without Docker.
* docs: document the opt-in entity visibility filter
* refactor: dedupe entity_id lookups in domain-search filter
Bind entity_id once via walrus in the _search_domain_only filter
comprehension instead of three separate dict lookups, and unify the
membership checks on the same value. Behavior-identical: a missing
entity_id short-circuits at the startswith check in both forms.
* test: add runnable coverage for visibility filter in get_system_overview
Drive get_system_overview in-process with a fake client so the M4 overview
wiring has non-e2e coverage: asserts the diagnostic entity is dropped from the
universe and that total_entities / total_domains / domain_stats reflect the
post-filter set (coherence), plus a disabled-config no-op case.
Also correct a now-stale comment: total_entities reflects the
visibility-filtered universe, no longer the full system.
* test: add end-to-end integration test for _search_domain_only visibility filter
Covers the domain-listing search path in-process: an enabled category
exclude drops the diagnostic sensor and total_matches reflects the
post-filter set (coherence). Closes the last untested end-to-end filter
path that had only shared-helper coverage.
* style: apply ruff format to visibility test files
* fix+test: warn on unusable registry payload + end-to-end coverage for smart_search sites
Address pr-review-toolkit findings:
- resolver.hidden_entity_ids now logs a warning when the filter is enabled but
the registry payload is unusable (non-dict / success:false / non-list result),
matching spec §7 'degrade with a warning'. Still fails open. The disabled
default stays silent (no per-call warning).
- add end-to-end tests driving the full _fetch_search_entities and
get_entities_by_area methods (previously only their staticmethod helpers were
tested), asserting the diagnostic entity is excluded and area totals stay
coherent.
- add resolver tests for warn-and-fail-open on bad/exception/non-list payloads
and silence when disabled.
- reword two comments: results[1]/[2] is the unprojected registry (not literally
'all fields' — entity_registry/list omits aliases).
* docs: include managed version field in the visibility config example
* fix: correct e2e get_state assertion and harden area-search error paths
Follow-up fixes on the entity-visibility filter branch (#1728):
- e2e: ha_get_state returns a {data, metadata} envelope with no top-level
"success" key, so assert via assert_mcp_success instead of a raw
got.get("success") check. This was the sole cause of the red E2E
Validation and HAOS E2E lanes (all four failed only on this test).
- resolver: coerce a bare-string "labels" value to a single-element list
before set.intersection so it is not iterated character-by-character,
which could otherwise spuriously hide an entity; add a regression test.
- get_entities_by_area: re-raise a failed mandatory states fetch and
propagate a cancelled registry sub-task instead of degrading to an empty
area result with success=True, mirroring _fetch_search_entities; add
regression tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: propagate cancellation in get_system_overview for sibling parity
get_system_overview used isinstance(results[0], Exception), so a cancelled
mandatory states fetch was assigned to `entities` and then crashed downstream
iteration with a TypeError instead of propagating. Switch to BaseException and
re-raise a cancelled services/registry sub-task, matching get_entities_by_area
and _fetch_search_entities; add a regression test that fails without the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: apply entity visibility denylist to registry-less entities
deny_entity_ids only hid entities present in the entity registry, because
hidden_entity_ids built the set solely from the registry list. An entity in
get_states() with no registry entry (legacy YAML / template entities) was
silently not hidden despite an exact deny_entity_ids match.
Seed the hidden set from the denylist directly — deny is a literal entity_id
match that needs no registry metadata. The registry-derived dimensions
(category/area/label/hidden_by) still require a registry entry, and a failed
registry read still fails open.
Also correct the FAQ: version is reserved, not "managed automatically" — no
write path bumps it in production.
Add regression tests for the states-only and empty-registry deny cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visibility): skip non-iterable label payloads per-entry instead of failing whole filter
The label check already coerced a bare-string `labels` value to a single-element
list. Extend that guard so an unexpected non-iterable payload (int/dict/…) skips
only that entry's label intersection rather than raising a TypeError that would
fail-open-disable the visibility filter for every other entity. Regression test
asserts a malformed sibling is skipped while a well-formed entry is still hidden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visibility): honor deny_entity_ids on degraded-registry read
The two unusable-registry early returns dropped the deny seed, so a
transient registry read failure silently un-hid explicitly denied
entities. deny is a literal entity_id match and needs no registry data;
return it on both degraded paths instead of failing fully open. Only the
registry-derived dimensions degrade to open on a bad read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(search): raise instead of dead return None in get_system_overview
exception_to_structured_error is NoReturn, so the trailing return None was
dead. Re-raise like the sibling handlers get_entities_by_area and
_fetch_search_entities for a consistent, unambiguous exit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visibility): surface degradation and dropped categories via warnings
The resolver now returns (hidden, warnings): an enabled-but-degraded read
(registry unavailable) and dropped unknown exclude_categories are carried
back as caller-facing warnings instead of only being logged, and each
ha_search / ha_get_overview seam threads them into the top-level response
warnings list. Unknown categories are dropped (warn-and-drop), not
hard-rejected, so a future HA category can never fail the whole config.
Addresses review points 2 and 4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visibility): add settings-UI config GET/PUT handlers with optimistic lock
Serve entity_visibility.json over /api/visibility/config (GET/PUT) from the
settings UI, mirroring the tool-policy handlers: version-mismatch returns 409,
corrupt file returns 500, invalid payload 400. This gives save_visibility_config
and the version field their consumer (addressing review point 1) and makes the
config editable without hand-authoring the file on disk. Always registered
(pure file I/O, no approval-queue dependency).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visibility): tidy warnings threading after simplify pass
- Move merge_visibility_warnings to tools/util_helpers.py (generic response
helper) so visibility/resolver.py stays pure-resolver.
- Drop redundant set() copies on the two degraded-registry returns; denied is
already a set and callers treat it read-only.
- Use the compositional merge in the domain-only search return for consistency
with the other seams.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visibility): add Entity Visibility settings-UI tab
A form-based tab (enable toggle, category checkboxes, area/label text fields,
per-entity denylist textarea) that reads and writes entity_visibility.json via
the /api/visibility/config GET/PUT handlers, with the same optimistic-lock
reload-on-409 flow as the policy tab. Tab switching, roving tabindex and
arrow-key navigation come for free from activateTab (one button + one panel).
The "noise reduction, not access control" caveat is shown at the top of the
panel so add-on users do not mistake it for a security boundary. Live registry
pickers for areas/labels/devices are deferred to a follow-up; this ships the
denylist config the resolver already honors, editable without hand-writing the
file. Panel added to the tablist a11y guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visibility): cover shipped default + registry-derived label dimension
- Unit: enabled-default config fires the shipped ["diagnostic","config"] and
proves both categories hide (config was never exercised before).
- E2E: a label assigned to the probe helper is hidden via exclude_labels,
exercising the registry-join path, not just the registry-independent denylist.
- Drop the "not executed locally" authoring note from the e2e docstring; keep
the config-dir-seam and external_only rationale.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visibility): FAQ reads-only rationale, settings UI, warn-drop behavior
State that the filter is read-scoping only and does not gate control tools
(writes stay with the Tool Security Policies engine), point users at the new
Entity Visibility settings tab, and document the constrained categories,
warn-and-drop on unknown values, version optimistic-lock, and degraded-read
warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visibility): add allowlist + respect-Assist-exposure dimensions
Complete #1728 by folding the two remaining dimensions into the opt-in entity
visibility filter alongside the shipped deny/exclude dimensions.
- allow_entity_ids / allow_areas / allow_labels: when any is set the filter
inverts to restrict mode (only matched entities stay visible). The resolver
now takes the seam's states list so states-only (YAML/template) entities are
covered too, not just the registry.
- respect_assist_exposure: reconstructs HA-core async_should_expose for the
conversation assistant (explicit per-entity override wins; else, when the
instance exposes new entities, domain + device-class defaults decide). Two
websocket reads are fetched once per search when enabled and fail soft (the
dimension is skipped with a warning rather than hiding everything).
- Mirror HA's DEFAULT_EXPOSED_DOMAINS + binary_sensor/sensor device-class sets;
device_class comes from the registry override or the live state attribute.
- Settings-UI fields for both; FAQ documents the conjunction model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(e2e): bump haos_inaddon skip ceiling for visibility e2e tests
The entity-visibility filter adds @external_only e2e tests
(denylist/label/allowlist) that skip on the haos_inaddon lane, raising
the observed skip count to 60, over the prior ceiling of 58.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visibility): read explicit Assist should_expose from registry options
The respect_assist_exposure dimension reconstructed exposure from
homeassistant/expose_entity/list, which HA-core only ever returns exposed
(True) entities in — an explicit per-entity un-expose is omitted entirely, so
it could never be honored: an explicitly un-exposed default-domain entity
(light, switch) stayed visible while HA's async_should_expose returns False.
Read the explicit should_expose (True or False, incl. HA's persisted computed
defaults) from each registry entry's options via
config/entity_registry/get_entries (mirrors _fetch_area_entity_aliases),
replacing the expose_entity/list read. WebSocket-read count stays at 2.
States-only entities have no registry options and fall to the default branch —
documented in the FAQ as a residual limit.
Replaces the seam test's impossible {"conversation": False} list fixture with a
faithful get_entries fixture and adds the expose_new=True + explicit-unexpose
regression case.
* fix(visibility): forward area-path visibility warnings to the response
ha_search's three area builders (_search_area_with_query,
_search_area_only_populated, _search_area_only) rebuild a fresh response dict
and never carried area_result's warnings, so a visibility/registry degradation
on ha_search(area_filter=...) was silently invisible while the non-area path
surfaced it. Forward area_result["warnings"] once at the area dispatch (mirrors
the non-area merge_visibility_warnings).
Add the missing "warnings last mile" coverage that let this slip: direct tests
for merge_visibility_warnings and response-level assertions that a corrupt
config surfaces a warning on the ha_search and ha_get_overview responses. The
area path itself gets a response-level assertion via the e2e (corrupt config).
* test(visibility): cover allow + exclude dimensions active together
Every allowlist test zeroed exclude_categories, so the central invariant — hide
dimensions compose independently — was proven one dimension at a time. A
refactor that let restrict mode skip the exclude loop would pass the suite while
leaking an allowed-but-diagnostic entity. Add a resolver test and a seam test
(_exact_match_search) with an allow and an exclude dimension simultaneously
active, asserting exclude wins over allow and the allowlist still restricts.
* fix(visibility): explicit extra-ignore, empty-registry allowlist guard, partial alignment
Round-2 small items:
- Make VisibilityConfig's extra="ignore" explicit with the forward-compat
rationale (a typo'd key stays silently dropped; documented, not incidental).
- Fail-open guard: a success-but-empty entity registry plus an area/label
allowlist would hide every states-only candidate (the fail-closed blank the
design forbids). Degrade those registry-derived allow dimensions to open and
warn; allow_entity_ids still applies.
- ha_get_overview no longer marks itself partial for a mere visibility warning
(config typo / skipped Assist dimension) on otherwise-complete data — partial
now reflects genuinely incomplete data only, matching ha_search.
- FAQ: state that excludes (not only deny) win over an allow_entity_ids match.
* fix(visibility): settings-tab a11y + non-clobbering 409, with JS behaviour tests
- Give the visibility-load-error region role="alert" aria-live="assertive" so a
failed config load is announced (it had neither).
- Route visibilitySaveConfig's failure branches through setStatusAlert so the
save status flips to role="alert" on error (network, HTTP, 409) instead of
staying a polite status, matching every sibling save path.
- The 409 optimistic-lock branch no longer auto-reloads the config: that
overwrote the user's unsaved edits while telling them to "re-apply". Keep the
edits, announce the conflict, and let the user reload deliberately (mirrors the
policy tab).
- Add behavioural JS tests (load failure, save success/failure status role, and
the 409 keep-edits-no-reload flow) driving the real script through JSDOM.
* fix(visibility): forward area warnings inside the unwrapped data payload
The prior area-warning forward attached warnings to the top level of the area
builders' response, but those builders wrap their payload via
add_timezone_metadata into {"data": {...}, "metadata": {...}} and the ha_search
orchestrator unwraps ["data"] before merging metadata (_apply_search_outcome) —
so top-level warnings were dropped again and never reached the response.
Merge the warnings into ["data"] so they survive the unwrap. Add a unit test
driving the real ha_search orchestrator (fake smart-tools returning an area
result with a warning) that asserts the warning reaches the response — it fails
against the previous top-level forward.
* test(e2e): allowlist positive/deny-wins, overview, and assist-unexpose coverage
Round-2 e2e gaps, each through real MCP dispatch against live HA:
- Allowlist: assert the positive side (an allowlisted entity stays visible) plus
deny-wins-over-allow — the prior allowlist e2e only asserted the unlisted probe
was hidden, which a hide-everything regression would still pass.
- ha_get_overview: the second filtered collection tool had zero e2e; assert a
denied probe drops out of the overview entity total.
- respect_assist_exposure: explicitly expose then un-expose a probe and assert it
becomes hidden — the regression for the round-2 Assist finding (explicit
should_expose=False now read from the registry options via get_entries).
Bump the haos_inaddon skip ceiling 60 -> 63 for the 3 new @external_only tests.
* fix(visibility): read Assist exposure from registry payload, restore states-only expose
Round-2 read per-entity conversation exposure via
config/entity_registry/get_entries. That was redundant – the
config/entity_registry/list payload the resolver already holds carries each
entry's options (HA's as_partial_dict includes options) – and it dropped
homeassistant/expose_entity/list, the only read reaching HA's separate
exposed-entities store for states-only (non-registry) entities. A states-only
entity a user explicitly exposed to Assist was then silently hidden, violating
the module's fail-open contract.
Read each registry entry's explicit should_expose (True or False) directly from
the list payload options, and re-add expose_entity/list as a True-only source
for ids with no registry entry (it cannot express False; for a states-only
entity that direction stays fail-open). Net websocket reads stay at two. Delete
the get_entries fetch and its _registry_entity_ids plumbing.
Add a seam test for the states-only explicit-expose case and a services-failure
overview test pinning partial:true beside a coexisting visibility warning.
Correct the seam fixture, FAQ residual note, and docstrings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(visibility): match device-inherited area and labels
The area/label exclude and allow dimensions matched only an entity's own
area_id/labels, but most entities are device-bound (registry area_id null +
a device_id) and inherit their area from the device registry; device labels
apply to their entities. exclude_areas under-hid those entities and
allow_areas over-hid them (hiding entities in the allowed area, the
fail-closed blank the module forbids). Live-verified on a real instance.
Thread config/device_registry/list into the resolver and match effective
area (entity area_id else device) and effective labels (entity union device)
in both the exclude and allow loops. A degraded or absent device payload
leaves the maps empty, so the dimensions fall back to entity-level matching
(fail open). Wire the device registry through all five load_hidden_set call
sites: three (_fetch_search_entities and the two tools_search paths) had no
device fetch and gained config/device_registry/list in their gather; two
(get_entities_by_area, get_system_overview) already fetched it.
Also reword the deep-search progress line so its raw pre-filter state count
does not read as "the visibility filter is off".
Add device-bound resolver fixtures (entity area_id null, device carries the
area/label) covering both exclude and allow directions, entity-over-device
precedence, the entity-union-device label rule, and the no-device fallback,
plus an overview seam test proving the call-site wiring reaches the resolver.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(visibility): bump embedded/inaddon skip ceilings for the visibility e2e
The master merge brought the embedded and haos_embedded e2e lanes (#1741); this
branch's entity-visibility e2e (test_entity_visibility.py, @external_only, they
need a real external MCP server) skip on those backends and on the inaddon lane,
pushing each lane's skip-marker count past its ceiling. Set each ceiling to the
CI-observed count: haos_inaddon 62 -> 65, embedded 119 -> 125, haos_embedded
90 -> 94. The external lane still runs the tests (real coverage); the skips are
the same @external_only class the existing ceilings already account for, not a
broadened marker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 813b97f commit 8bbe987
28 files changed
Lines changed: 3306 additions & 59 deletions
File tree
- docs
- src/ha_mcp
- settings_ui
- tools
- smart_search
- visibility
- tests/src
- e2e
- basic
- unit
- visibility
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
314 | 401 | | |
315 | 402 | | |
316 | 403 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
796 | 796 | | |
797 | 797 | | |
798 | 798 | | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
799 | 854 | | |
800 | 855 | | |
801 | 856 | | |
| |||
3237 | 3292 | | |
3238 | 3293 | | |
3239 | 3294 | | |
| 3295 | + | |
| 3296 | + | |
3240 | 3297 | | |
3241 | 3298 | | |
3242 | 3299 | | |
| |||
3397 | 3454 | | |
3398 | 3455 | | |
3399 | 3456 | | |
| 3457 | + | |
| 3458 | + | |
| 3459 | + | |
3400 | 3460 | | |
3401 | 3461 | | |
3402 | 3462 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
953 | 953 | | |
954 | 954 | | |
955 | 955 | | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
| 161 | + | |
161 | 162 | | |
162 | 163 | | |
163 | 164 | | |
| |||
403 | 404 | | |
404 | 405 | | |
405 | 406 | | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
406 | 471 | | |
407 | 472 | | |
408 | 473 | | |
| |||
0 commit comments