Skip to content

Commit 3fe9703

Browse files
fix: honor view_path scoping in ha_config_get_dashboard get mode (#2023)
* fix: honor view_path scoping in ha_config_get_dashboard get mode Get mode accepted view_path but returned the full multi-view config regardless (the parameter only selected the screenshot render view), so a single-view read on a large dashboard blew up the response. view_path now scopes the get-mode payload to the matched view: the response carries view + view_index instead of config (so the view object cannot be pushed back as a full-config replacement), while config_hash still covers the full config so python_transform optimistic locking works unchanged. The matcher is shared with screenshot view resolution, so semantics and errors (unknown/empty/strategy paths) stay identical, and scoping works without the screenshot beta feature. Also adds the standard fields= projection to ha_report_issue, whose full response repeats the captured logs in the raw keys and in each template. Closes #2010 * fix: address CI drift check and review findings - Register ha_report_issue in the fields-projection drift check (TOOL_SPECS) and enumerate all projectable keys in the fields= description ('Available keys: ...') — fixes the Unit Tests CI failure. - Warn when view_path is passed to mode='search' (cross-dashboard search-all), matching the ignored-parameter warning the other non-get modes already emit instead of silently dropping it. - Start the ha_report_issue docstring with an approved action verb (Codex review). - Broaden the _note_screenshot_ignored docstring to cover view_path and tighten two Field description wordings. - Add tests: non-dict config with view_path, screenshot-option view_path branch selection, CSV-string fields form, search-all ignored-view_path warning. --------- Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
1 parent cdf4266 commit 3fe9703

6 files changed

Lines changed: 587 additions & 80 deletions

File tree

src/ha_mcp/dashboard_screenshot/paths.py

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -347,42 +347,28 @@ async def fetch_dashboard_render_config(
347347
return cast(dict[str, Any], config)
348348

349349

350-
def resolve_dashboard_view(
350+
def match_dashboard_view(
351351
dashboard_url_path: str,
352352
config: dict[str, Any],
353-
view_path: str | None,
354-
) -> DashboardRenderTarget:
355-
"""Resolve one named Lovelace view to a canonical frontend route."""
356-
base_path = dashboard_frontend_path(dashboard_url_path)
357-
if view_path is None:
358-
views = config.get("views")
359-
has_static_views = isinstance(views, list) and bool(views)
360-
if "strategy" in config:
361-
warning = (
362-
"No view_path was supplied; this strategy dashboard generates "
363-
"its views at runtime, so only the base route is available."
364-
)
365-
elif has_static_views:
366-
warning = (
367-
"No view_path was supplied; the dashboard base route renders "
368-
"the first view visible to Puppet's Home Assistant user."
369-
)
370-
else:
371-
warning = (
372-
"No view_path was supplied and the dashboard has no static views; "
373-
"only the base route is available."
374-
)
375-
return DashboardRenderTarget(
376-
dashboard_url_path=base_path,
377-
view_path=None,
378-
render_path=base_path,
379-
view_index=None,
380-
stable=False,
381-
warnings=(warning,),
382-
)
383-
384-
cleaned_view_path = view_path
385-
if not cleaned_view_path.strip():
353+
view_path: str,
354+
*,
355+
strategy_suggestions: tuple[str, ...] = (
356+
"Use dashboard_path with the frontend route instead",
357+
),
358+
) -> tuple[int, dict[str, Any]]:
359+
"""Match one named Lovelace view by its configured ``views[].path``.
360+
361+
Shared by screenshot view resolution and get-mode view scoping so both
362+
honour identical semantics: exact match on the configured path, with
363+
structured errors for an empty path, a strategy dashboard (no static
364+
views to match), and a missing/ambiguous path (the not-found error lists
365+
the available paths). Returns ``(view_index, view)``.
366+
367+
``strategy_suggestions`` lets each caller phrase the strategy-dashboard
368+
remediation in its own vocabulary (the screenshot tools have a
369+
``dashboard_path`` route parameter; config scoping does not).
370+
"""
371+
if not view_path.strip():
386372
raise_tool_error(
387373
create_error_response(
388374
ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -397,9 +383,9 @@ def resolve_dashboard_view(
397383
"Strategy dashboards do not expose static named view paths.",
398384
context={
399385
"dashboard_url_path": dashboard_url_path,
400-
"view_path": cleaned_view_path,
386+
"view_path": view_path,
401387
},
402-
suggestions=["Use dashboard_path with the frontend route instead"],
388+
suggestions=list(strategy_suggestions),
403389
)
404390
)
405391
views = config.get("views")
@@ -408,7 +394,7 @@ def resolve_dashboard_view(
408394
matches = [
409395
(index, view)
410396
for index, view in enumerate(views)
411-
if _configured_view_path(view) == cleaned_view_path
397+
if _configured_view_path(view) == view_path
412398
]
413399
if len(matches) != 1:
414400
available = [
@@ -420,17 +406,57 @@ def resolve_dashboard_view(
420406
raise_tool_error(
421407
create_error_response(
422408
ErrorCode.RESOURCE_NOT_FOUND,
423-
f"Dashboard view path '{cleaned_view_path}' is {reason}.",
409+
f"Dashboard view path '{view_path}' is {reason}.",
424410
context={
425411
"dashboard_url_path": dashboard_url_path,
426-
"view_path": cleaned_view_path,
412+
"view_path": view_path,
427413
"available_view_paths": available,
428414
},
429415
suggestions=["Use a view_path returned by ha_config_get_dashboard"],
430416
)
431417
)
418+
return matches[0]
419+
432420

433-
view_index, _ = matches[0]
421+
def resolve_dashboard_view(
422+
dashboard_url_path: str,
423+
config: dict[str, Any],
424+
view_path: str | None,
425+
) -> DashboardRenderTarget:
426+
"""Resolve one named Lovelace view to a canonical frontend route."""
427+
base_path = dashboard_frontend_path(dashboard_url_path)
428+
if view_path is None:
429+
views = config.get("views")
430+
has_static_views = isinstance(views, list) and bool(views)
431+
if "strategy" in config:
432+
warning = (
433+
"No view_path was supplied; this strategy dashboard generates "
434+
"its views at runtime, so only the base route is available."
435+
)
436+
elif has_static_views:
437+
warning = (
438+
"No view_path was supplied; the dashboard base route renders "
439+
"the first view visible to Puppet's Home Assistant user."
440+
)
441+
else:
442+
warning = (
443+
"No view_path was supplied and the dashboard has no static views; "
444+
"only the base route is available."
445+
)
446+
return DashboardRenderTarget(
447+
dashboard_url_path=base_path,
448+
view_path=None,
449+
render_path=base_path,
450+
view_index=None,
451+
stable=False,
452+
warnings=(warning,),
453+
)
454+
455+
view_index, _ = match_dashboard_view(dashboard_url_path, config, view_path)
456+
cleaned_view_path = view_path
457+
views = config.get("views")
458+
if not isinstance(views, list):
459+
views = []
434460
render_path = _safe_named_render_path(base_path, cleaned_view_path)
435461
if render_path is None:
436462
return _fallback_view_target(

src/ha_mcp/tools/tools_bug_report.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
)
3535
from .component_api import get_component_caps
3636
from .helpers import log_tool_usage, register_tool_methods
37-
from .util_helpers import ANSI_ESCAPE_RE
37+
from .util_helpers import ANSI_ESCAPE_RE, JSON_STRING_COERCION, project_fields
3838

3939
logger = logging.getLogger(__name__)
4040

@@ -676,10 +676,38 @@ async def ha_report_issue(
676676
),
677677
),
678678
] = 10,
679+
fields: Annotated[
680+
str | list[str] | None,
681+
JSON_STRING_COERCION,
682+
Field(
683+
default=None,
684+
description=(
685+
"Return only the specified top-level response keys — the "
686+
"full response (both templates + logs + diagnostics, with "
687+
"log content repeated across the raw keys and templates) "
688+
"is very large. "
689+
"None = full response. Typical for a runtime bug: "
690+
"'runtime_bug_template,suggested_title,"
691+
"runtime_bug_submit_url,duplicate_check_urls,"
692+
"anonymization_guide,instructions'; for agent feedback "
693+
"swap in agent_behavior_template and "
694+
"agent_behavior_submit_url. The templates already embed "
695+
"the relevant logs, so the raw log keys are only needed "
696+
"for your own analysis. "
697+
"Available keys: diagnostic_info, recent_logs, "
698+
"startup_logs, addon_logs, core_error_log, log_count, "
699+
"startup_log_count, formatted_report, "
700+
"runtime_bug_template, agent_behavior_template, "
701+
"anonymization_guide, suggested_title, "
702+
"runtime_bug_submit_url, agent_behavior_submit_url, "
703+
"duplicate_check_urls, missing_tool_hint, instructions."
704+
),
705+
),
706+
] = None,
679707
ctx: Context | None = None,
680708
) -> dict[str, Any]:
681709
"""
682-
Collect diagnostic information for filing issue reports or feedback.
710+
Get diagnostic information and templates for filing issue reports or feedback.
683711
684712
This tool generates templates for TWO types of reports:
685713
1. **Runtime Bug Report** - For ha-mcp errors, failures, unexpected behavior
@@ -709,7 +737,10 @@ async def ha_report_issue(
709737
- "That was inefficient"
710738
711739
**OUTPUT:**
712-
Returns both templates plus diagnostic data. Key fields:
740+
Returns both templates plus diagnostic data. The full response is
741+
LARGE (the captured logs appear in the raw log keys AND inside each
742+
template) — pass fields=... to fetch only the keys you need once you
743+
know which template applies. Key fields:
713744
- `runtime_bug_template`, `agent_behavior_template` — pick based on context
714745
- `recent_logs`, `startup_logs` — captured ha-mcp tool/server log entries
715746
- `addon_logs` — addon container stdout/stderr (HA add-on installs only;
@@ -843,7 +874,7 @@ async def ha_report_issue(
843874
for keyword in search_keywords[:3] # Limit to top 3 keywords
844875
]
845876

846-
return {
877+
result: dict[str, Any] = {
847878
"success": True,
848879
"diagnostic_info": diagnostic_info,
849880
"recent_logs": recent_logs,
@@ -934,6 +965,7 @@ async def ha_report_issue(
934965
"CRITICAL: Always ANONYMIZE the report BEFORE presenting it in markdown code blocks!"
935966
),
936967
}
968+
return project_fields(result, fields)
937969

938970

939971
def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:

0 commit comments

Comments
 (0)