Skip to content

Commit b788ee3

Browse files
feat: show installation method in settings footer (#2231)
* feat: show installation method in settings footer * test: register shared unknown locale string --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ae447b2 commit b788ee3

4 files changed

Lines changed: 146 additions & 12 deletions

File tree

src/ha_mcp/settings_ui/locales/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
"theme.light": "Light",
1414
"theme.dark": "Dark",
1515
"status.loading": "Loading…",
16+
"footer.installation": "installation: {method}",
17+
"footer.deployment.addon": "app/add-on",
18+
"footer.deployment.docker": "container/docker",
19+
"footer.deployment.pyinstaller": "standalone binary",
20+
"footer.deployment.git": "source checkout",
21+
"footer.deployment.pypi": "python package",
22+
"footer.deployment.unknown": "unknown",
1623
"nav.skip": "Skip to content",
1724
"tabs.aria": "Settings sections",
1825
"tabs.tools": "Tools",

src/ha_mcp/settings_ui/settings.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -536,14 +536,36 @@ async function applyInfoChrome() {
536536
'⚠ Changes saved. Restart the ha-mcp process, then refresh the MCP tool list in your AI client.'
537537
);
538538
}
539-
// Version footer — show the running ha-mcp build at the bottom
540-
// of every page. ``info.version`` is whatever
541-
// ``HA_MCP_BUILD_VERSION`` the addon's Dockerfile set (e.g.
542-
// ``7.5.0`` on stable, ``7.5.0.dev355`` on dev), with a fallback
543-
// to package metadata in standalone deployments.
539+
// Footer — show the running build and the same deployment classification
540+
// used by ha_report_issue. The backend keeps embedded and sidecar distinct
541+
// because they need different restart behavior; preserve those concise
542+
// names here and clarify the less obvious packaging-oriented values.
544543
if (info.version) {
545544
const fEl = document.getElementById('versionFooterText');
546-
if (fEl) fEl.textContent = 'ha-mcp ' + info.version;
545+
const deploymentMode = typeof info.deployment_mode === 'string'
546+
? info.deployment_mode
547+
: '';
548+
const deploymentLabels = {
549+
embedded: 'embedded',
550+
sidecar: 'sidecar',
551+
addon: t('footer.deployment.addon', {}, 'app/add-on'),
552+
docker: t('footer.deployment.docker', {}, 'container/docker'),
553+
pyinstaller: t('footer.deployment.pyinstaller', {}, 'standalone binary'),
554+
git: t('footer.deployment.git', {}, 'source checkout'),
555+
pypi: t('footer.deployment.pypi', {}, 'python package'),
556+
unknown: t('footer.deployment.unknown', {}, 'unknown'),
557+
};
558+
const deploymentLabel = Object.prototype.hasOwnProperty.call(
559+
deploymentLabels, deploymentMode
560+
) ? deploymentLabels[deploymentMode] : deploymentMode;
561+
const installation = deploymentLabel
562+
? ' · ' + t(
563+
'footer.installation',
564+
{method: deploymentLabel},
565+
'installation: {method}'
566+
)
567+
: '';
568+
if (fEl) fEl.textContent = 'ha-mcp ' + info.version + installation;
547569
}
548570
} catch (e) {
549571
// A transient /api/settings/info failure must not leave the restart

tests/src/unit/test_locale_parity.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,8 +678,10 @@ def test_derived_catalogs_match_the_canonical_store() -> None:
678678
AUTHORED_SHARED_PLACES = {
679679
"src/ha_mcp/settings_ui/locales": (
680680
"messages.advanced.extra_yaml_write_keys.label",
681+
"messages.footer.deployment.unknown",
681682
),
682683
"custom_components/ha_mcp_tools/translations": (
684+
"common.version_unknown",
683685
"options.step.tools_info.data.extra_yaml_keys",
684686
),
685687
}

tests/src/unit/test_settings_ui_js_behavior.py

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -628,11 +628,19 @@ def _assert_clean_init(result: HarnessResult) -> None:
628628

629629

630630
class TestVersionFooter:
631-
"""The version footer at the bottom of the settings page reads from
632-
/api/settings/info on init and renders ``ha-mcp <version>`` so an
633-
operator can see the running build without leaving the UI.
631+
"""The footer at the bottom of the settings page reads from
632+
/api/settings/info on init and renders the running version and installation
633+
method so an operator can identify the active build without leaving the UI.
634634
"""
635635

636+
@staticmethod
637+
def _footer_text(result: HarnessResult) -> str:
638+
match = re.search(r'id="versionFooterText">([^<]*)</div>', result.dom)
639+
assert match is not None, (
640+
f"version footer missing; dom tail: {result.dom[-1500:]}"
641+
)
642+
return match.group(1)
643+
636644
def test_version_rendered_from_settings_info(self, settings_script: str) -> None:
637645
fetches = {
638646
**DEFAULT_FETCHES,
@@ -654,8 +662,103 @@ def test_version_rendered_from_settings_info(self, settings_script: str) -> None
654662
invoke="await new Promise(r => setTimeout(r, 200));",
655663
)
656664
_assert_clean_init(result)
657-
assert "ha-mcp 7.5.0.dev400" in result.dom, (
658-
f"version footer missing or wrong; dom tail: {result.dom[-1500:]}"
665+
assert self._footer_text(result) == "ha-mcp 7.5.0.dev400"
666+
667+
@pytest.mark.parametrize(
668+
("deployment_mode", "expected_label"),
669+
[
670+
("embedded", "embedded"),
671+
("sidecar", "sidecar"),
672+
("addon", "app/add-on"),
673+
("docker", "container/docker"),
674+
("pyinstaller", "standalone binary"),
675+
("git", "source checkout"),
676+
("pypi", "python package"),
677+
("unknown", "unknown"),
678+
("future-mode", "future-mode"),
679+
("constructor", "constructor"),
680+
],
681+
)
682+
def test_installation_method_rendered_from_settings_info(
683+
self,
684+
settings_script: str,
685+
deployment_mode: str,
686+
expected_label: str,
687+
) -> None:
688+
fetches = {
689+
**DEFAULT_FETCHES,
690+
"/api/settings/info": {
691+
"status": 200,
692+
"json": {
693+
"is_addon": deployment_mode == "addon",
694+
"is_sidecar": deployment_mode == "sidecar",
695+
"deployment_mode": deployment_mode,
696+
"instance_id": "test-id",
697+
"started_at": 0,
698+
"version": "8.2.0",
699+
},
700+
},
701+
}
702+
result = run_script(
703+
settings_script,
704+
initial_html=MIN_DOM,
705+
fetch_map=fetches,
706+
invoke="await new Promise(r => setTimeout(r, 200));",
707+
)
708+
_assert_clean_init(result)
709+
assert (
710+
self._footer_text(result)
711+
== f"ha-mcp 8.2.0 · installation: {expected_label}"
712+
)
713+
714+
@pytest.mark.parametrize(
715+
("deployment_mode", "translated_label"),
716+
[("embedded", "intégré"), ("sidecar", "accompagnement")],
717+
)
718+
def test_server_mode_labels_stay_raw_when_footer_phrase_is_localized(
719+
self,
720+
settings_script: str,
721+
deployment_mode: str,
722+
translated_label: str,
723+
) -> None:
724+
from ha_mcp.settings_ui._i18n import build_payload, serialize_payload
725+
726+
catalog = build_payload("fr")
727+
catalog["messages"].update(
728+
{
729+
"footer.installation": "mode d’installation : {method}",
730+
f"footer.deployment.{deployment_mode}": translated_label,
731+
}
732+
)
733+
payload = serialize_payload(catalog)
734+
localized_dom = MIN_DOM.replace(
735+
"</body>",
736+
f'<script id="ha-mcp-i18n" type="application/json">{payload}</script>'
737+
"</body>",
738+
)
739+
fetches = {
740+
**DEFAULT_FETCHES,
741+
"/api/settings/info": {
742+
"status": 200,
743+
"json": {
744+
"is_addon": False,
745+
"is_sidecar": deployment_mode == "sidecar",
746+
"deployment_mode": deployment_mode,
747+
"instance_id": "test-id",
748+
"started_at": 0,
749+
"version": "8.2.0",
750+
},
751+
},
752+
}
753+
result = run_script(
754+
settings_script,
755+
initial_html=localized_dom,
756+
fetch_map=fetches,
757+
invoke="await new Promise(r => setTimeout(r, 200));",
758+
)
759+
_assert_clean_init(result)
760+
assert self._footer_text(result) == (
761+
f"ha-mcp 8.2.0 · mode d’installation : {deployment_mode}"
659762
)
660763

661764
def test_version_omitted_when_info_response_lacks_version(
@@ -684,7 +787,7 @@ def test_version_omitted_when_info_response_lacks_version(
684787
invoke="await new Promise(r => setTimeout(r, 200));",
685788
)
686789
_assert_clean_init(result)
687-
assert "ha-mcp undefined" not in result.dom
790+
assert self._footer_text(result) == ""
688791

689792

690793
class TestXssGuard:

0 commit comments

Comments
 (0)