mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
feat(updates): detect HA Supervisor addon and defer update UI to it (#1167)
Bambuddy already supports running as a Home Assistant addon (HA_URL/HA_TOKEN env-var integration since #283, community addon at hobbypunk90/homeassistant-addon-bambuddy), but the update UI was oblivious to it: HA addon users saw the in-app "Update available" banner and, on Settings, the docker-compose snippet — neither of which they can act on, since the HA Supervisor owns the addon lifecycle. Detection uses the SUPERVISOR_TOKEN env var that HA Supervisor injects into every addon container; no other environment sets it, so the check has zero false-positive surface. Backend: - new _is_ha_addon() helper in routes/updates.py - /updates/check now returns is_ha_addon: bool and extends update_method to 'git' | 'docker' | 'ha_addon' - /updates/apply checks HA before Docker (HA addons ARE Docker containers, so checking docker first would mis-classify) and returns an HA-specific message that points to Settings → Add-ons → Bambuddy in HA - response keeps is_docker: true alongside is_ha_addon: true so older frontend bundles still hit a managed-deployment branch instead of rendering an Install button that can't work Frontend: - SettingsPage update card branches on is_ha_addon BEFORE is_docker; HA users get a Supervisor-targeted message instead of the docker-compose snippet - Layout update banner is suppressed for HA addons — HA Supervisor surfaces its own update notification natively, so Bambuddy's banner would be duplicate noise linking to a page that just says "update via HA" - Plain Docker deployments are unaffected i18n: settings.updateViaHomeAssistant added to all 8 locales with full native translations. Tests: 3 backend unit tests for _is_ha_addon (present, absent, empty-string treated as unset), 3 backend integration tests (HA-precedes-Docker rejection on apply; HA branch on check; plain Docker branch on check), 2 SettingsPage tests pinning the mutually-exclusive UI rendering, 2 Layout tests pinning banner suppression for HA and retention for plain Docker.
This commit is contained in:
@@ -5,6 +5,8 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [0.2.4b2] - Unreleased
|
||||
|
||||
### Added
|
||||
- **Home Assistant addon detection — Settings → Updates and the in-app update banner now defer to the HA Supervisor** ([#1167](https://github.com/maziggy/bambuddy/issues/1167), reported by @Spegeli) — Bambuddy already shipped `HA_URL`/`HA_TOKEN` env-var support specifically labelled "for HA Add-on deployments" ([#283](https://github.com/maziggy/bambuddy/issues/283)) and a community-maintained HA addon (`hobbypunk90/homeassistant-addon-bambuddy`) exists upstream, so an HA-supervised installation is a real first-class deployment shape. Until now though, the update UI didn't know about it: HA addon users got the same "Update available!" banner as everyone else and, if they clicked through to Settings, saw the docker-compose snippet ("`docker compose pull && docker compose up -d`") which they cannot run from inside an HA addon container — that's the Supervisor's job. Detection uses the canonical signal: HA Supervisor injects `SUPERVISOR_TOKEN` into every addon container, and that variable is not set in any other environment. A new `_is_ha_addon()` helper in `backend/app/api/routes/updates.py` flips a request-level boolean which `/updates/check` surfaces as `is_ha_addon: bool` + an extended `update_method: 'git' | 'docker' | 'ha_addon'` enum. The check is checked **before** Docker on `/updates/apply` because HA addons *are* Docker containers — checking docker first would mis-classify them and serve the wrong message; the response also keeps `is_docker: true` alongside `is_ha_addon: true` so older frontend bundles still hit a managed-deployment branch (degrading to the Docker UX) instead of rendering an in-app Install button that can't work. Frontend branches identically: `SettingsPage.tsx`'s update card checks `is_ha_addon` first and renders "Updates are managed by the Home Assistant Supervisor. Open Settings → Add-ons → Bambuddy in Home Assistant to install the new version." in place of the docker-compose hint; `Layout.tsx`'s update banner is suppressed entirely for HA addons since the HA Supervisor's own update notification already surfaces the new version natively in the HA UI and a duplicate Bambuddy banner would just be noise that links to a page that says "go to HA". Plain Docker deployments are unaffected — the existing docker-compose hint and the in-app banner still render the same way they did. Localised across all 8 UI languages (en/de/fr/it/ja/pt-BR/zh-CN/zh-TW) with full translations of the new `settings.updateViaHomeAssistant` string. 6 new tests pin the contract: 3 backend unit tests for `_is_ha_addon()` (env var present → true, absent → false, empty string treated as unset to guard against shells that export it empty), 1 backend integration test for the HA-precedes-Docker rejection on `/updates/apply` (asserts the message says "Home Assistant" and not "Docker Compose"), 2 backend integration tests for `/updates/check` covering the HA-addon branch (`update_method == "ha_addon"`, both flags true) and the plain-Docker branch (`is_ha_addon: false`, `update_method == "docker"`); 2 frontend SettingsPage tests pin the mutually-exclusive UI rendering (HA branch shows the HA copy and not the docker-compose snippet; Docker branch shows the snippet and not the HA copy, neither shows the Install button); 2 frontend Layout tests pin the banner suppression for HA and its retention for plain Docker.
|
||||
|
||||
- **Filament Track Switch (FTS) support — print modal filament dropdown is no longer empty when an X2D / H2D has the FTS accessory installed** ([#1162](https://github.com/maziggy/bambuddy/issues/1162), reported by @mkavalecz) — When the FTS accessory is installed the printer's MQTT changes one nibble of the per-AMS `info` bitmask: bits 8-11 flip from a fixed extruder ID (0x0 / 0x1) to `0xE` ("uninitialized"), because the AMS is no longer wired to a single nozzle — the FTS dynamically routes any slot to either extruder. Bambuddy's MQTT parser already skipped 0xE entries when building `ams_extruder_map` (matching BambuStudio's reading for boot-time transient state), so with the FTS installed the map ended up empty and the print modal's filament dropdown — which filters by `extruderId === nozzle_id` to prevent cross-nozzle assignment ("position of left hotend is abnormal" failures) — filtered out *every* loaded slot. Net effect: empty Filament Mapping dropdown on every dual-nozzle print with the FTS, even when the AMS was fully loaded with the right material. Detection comes from a new MQTT field — `print.device.fila_switch` — which is non-null only when the accessory is installed; it carries the routing topology as two arrays: `in[track] = currently fed slot (-1 = empty)` and `out[track] = extruder this track terminates at`. The fix surfaces this through a new `FilaSwitchState` dataclass on `PrinterState` (`installed`, `in_slots`, `out_extruders`, `stat`, `info`) and the equivalent `FilaSwitchResponse` Pydantic schema on the `GET /printers/{id}/status` route. Frontend (`useFilamentMapping.ts` + `FilamentMapping.tsx`) skips the per-extruder filter when `printerStatus.fila_switch?.installed === true` so any compatible AMS slot can satisfy any nozzle's filament requirement, since the FTS handles the routing. Slots currently fed into a track also get a routing badge in the dropdown — `[L]` or `[R]` — so the user can tell at a glance which slot the FTS is currently routing where (idle slots get no badge: they can be routed to either extruder on demand). The hard "no cross-nozzle assignment" filter on real dual-nozzle printers without the FTS stays untouched (still trips the same way it always has — `fila_switch == null` keeps the existing behaviour). 4 backend tests in `test_bambu_mqtt.py::TestFilamentTrackSwitchDetection` (default-not-installed, detect-from-MQTT-using-the-reporter's-bundle, no-fila_switch-field-stays-not-installed, missing-in-out-arrays-don't-crash) and 2 frontend tests in `useFilamentMapping.test.ts` (FTS-active drops the nozzle filter; explicit `fila_switch: null` keeps the filter applied). Upstream fila_switch payloads with anything other than the documented shape are tolerated — `installed` flips on the *presence* of the field, the routing arrays default to empty lists if missing, and the dropdown skips the badge for slots not currently in `in_slots`.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -54,6 +54,16 @@ def _is_docker_environment() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _is_ha_addon() -> bool:
|
||||
"""Detect if running as a Home Assistant Supervisor addon.
|
||||
|
||||
HA Supervisor injects ``SUPERVISOR_TOKEN`` into every addon container;
|
||||
the variable is not set in any other environment, so a single env-var
|
||||
check is sufficient with no false-positive surface.
|
||||
"""
|
||||
return bool(os.environ.get("SUPERVISOR_TOKEN"))
|
||||
|
||||
|
||||
def _find_executable(name: str) -> str | None:
|
||||
"""Find an executable in PATH or common locations."""
|
||||
# Try standard PATH first
|
||||
@@ -355,6 +365,13 @@ async def check_for_updates(
|
||||
}
|
||||
|
||||
is_docker = _is_docker_environment()
|
||||
is_ha_addon = _is_ha_addon()
|
||||
if is_ha_addon:
|
||||
update_method = "ha_addon"
|
||||
elif is_docker:
|
||||
update_method = "docker"
|
||||
else:
|
||||
update_method = "git"
|
||||
return {
|
||||
"update_available": update_available,
|
||||
"current_version": APP_VERSION,
|
||||
@@ -364,7 +381,8 @@ async def check_for_updates(
|
||||
"release_url": release_url,
|
||||
"published_at": published_at,
|
||||
"is_docker": is_docker,
|
||||
"update_method": "docker" if is_docker else "git",
|
||||
"is_ha_addon": is_ha_addon,
|
||||
"update_method": update_method,
|
||||
}
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
@@ -669,7 +687,20 @@ async def apply_update(
|
||||
"status": _update_status,
|
||||
}
|
||||
|
||||
# Check if running in Docker
|
||||
# Check for managed deployment shapes that own the update lifecycle.
|
||||
# HA addons are also Docker, so check HA first to surface the more
|
||||
# specific message.
|
||||
if _is_ha_addon():
|
||||
return {
|
||||
"success": False,
|
||||
"is_ha_addon": True,
|
||||
"is_docker": True,
|
||||
"message": (
|
||||
"Bambuddy is running as a Home Assistant addon. "
|
||||
"Updates are managed by the Home Assistant Supervisor "
|
||||
"(Settings → Add-ons → Bambuddy → Update)."
|
||||
),
|
||||
}
|
||||
if _is_docker_environment():
|
||||
return {
|
||||
"success": False,
|
||||
|
||||
@@ -15,11 +15,34 @@ class TestUpdatesAPI:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_update_docker_rejection(self, async_client: AsyncClient):
|
||||
with patch("backend.app.api.routes.updates._is_docker_environment", return_value=True):
|
||||
with (
|
||||
patch("backend.app.api.routes.updates._is_ha_addon", return_value=False),
|
||||
patch("backend.app.api.routes.updates._is_docker_environment", return_value=True),
|
||||
):
|
||||
response = await async_client.post("/api/v1/updates/apply")
|
||||
result = response.json()
|
||||
assert result["success"] is False
|
||||
assert result["is_docker"] is True
|
||||
assert result.get("is_ha_addon") is not True
|
||||
# Docker message tells the user to docker compose, not HA.
|
||||
assert "Docker Compose" in result["message"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_update_ha_addon_rejection(self, async_client: AsyncClient):
|
||||
"""HA addons are also Docker, so the route must check HA first and
|
||||
return the HA-specific message — otherwise users see "run docker
|
||||
compose" advice they can't follow."""
|
||||
with (
|
||||
patch("backend.app.api.routes.updates._is_ha_addon", return_value=True),
|
||||
patch("backend.app.api.routes.updates._is_docker_environment", return_value=True),
|
||||
):
|
||||
response = await async_client.post("/api/v1/updates/apply")
|
||||
result = response.json()
|
||||
assert result["success"] is False
|
||||
assert result["is_ha_addon"] is True
|
||||
assert result["is_docker"] is True
|
||||
assert "Home Assistant" in result["message"]
|
||||
assert "Docker Compose" not in result["message"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_update_non_docker(self, async_client: AsyncClient):
|
||||
@@ -27,6 +50,7 @@ class TestUpdatesAPI:
|
||||
to prevent side effects (network call to GitHub releases API + actual
|
||||
git/pip subprocesses)."""
|
||||
with (
|
||||
patch("backend.app.api.routes.updates._is_ha_addon", return_value=False),
|
||||
patch("backend.app.api.routes.updates._is_docker_environment", return_value=False),
|
||||
patch(
|
||||
"backend.app.api.routes.updates._discover_target_release",
|
||||
@@ -44,6 +68,119 @@ class TestUpdatesAPI:
|
||||
with patch("os.path.exists", return_value=True):
|
||||
assert _is_docker_environment() is True
|
||||
|
||||
def test_is_ha_addon_detects_supervisor_token(self):
|
||||
"""HA Supervisor sets SUPERVISOR_TOKEN on every addon container.
|
||||
That env-var alone is the canonical HA-addon signal."""
|
||||
from backend.app.api.routes.updates import _is_ha_addon
|
||||
|
||||
with patch.dict("os.environ", {"SUPERVISOR_TOKEN": "abc123"}, clear=False):
|
||||
assert _is_ha_addon() is True
|
||||
|
||||
def test_is_ha_addon_false_outside_supervisor(self):
|
||||
from backend.app.api.routes.updates import _is_ha_addon
|
||||
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
assert _is_ha_addon() is False
|
||||
|
||||
def test_is_ha_addon_empty_token_treated_as_unset(self):
|
||||
"""An empty string is not a real token — guard against shells that
|
||||
export the variable empty."""
|
||||
from backend.app.api.routes.updates import _is_ha_addon
|
||||
|
||||
with patch.dict("os.environ", {"SUPERVISOR_TOKEN": ""}, clear=False):
|
||||
assert _is_ha_addon() is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_returns_ha_addon_flag_and_method(self, async_client: AsyncClient):
|
||||
"""`/updates/check` must surface the deployment shape so the frontend
|
||||
can pick the right CTA. HA must take precedence over Docker because
|
||||
HA addons run *inside* a Docker container — checking docker first
|
||||
would mis-classify them."""
|
||||
import httpx as _httpx
|
||||
|
||||
fake_release = {
|
||||
"tag_name": "v999.9.9",
|
||||
"name": "Far Future Release",
|
||||
"body": "",
|
||||
"html_url": "https://example.invalid/r",
|
||||
"published_at": "2099-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
class _Resp:
|
||||
status_code = 200
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return [fake_release]
|
||||
|
||||
class _FakeClient:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
return None
|
||||
|
||||
async def get(self, *_, **__):
|
||||
return _Resp()
|
||||
|
||||
with (
|
||||
patch.object(_httpx, "AsyncClient", _FakeClient),
|
||||
patch("backend.app.api.routes.updates._is_ha_addon", return_value=True),
|
||||
patch("backend.app.api.routes.updates._is_docker_environment", return_value=True),
|
||||
):
|
||||
response = await async_client.get("/api/v1/updates/check")
|
||||
body = response.json()
|
||||
assert body["is_ha_addon"] is True
|
||||
assert body["update_method"] == "ha_addon"
|
||||
# is_docker is preserved alongside so older frontend bundles still
|
||||
# hit a managed-deployment branch (degrades to Docker UX) instead of
|
||||
# rendering the in-app Install button.
|
||||
assert body["is_docker"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_docker_only_returns_docker_method(self, async_client: AsyncClient):
|
||||
import httpx as _httpx
|
||||
|
||||
fake_release = {
|
||||
"tag_name": "v999.9.9",
|
||||
"name": "Far Future Release",
|
||||
"body": "",
|
||||
"html_url": "https://example.invalid/r",
|
||||
"published_at": "2099-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
class _Resp:
|
||||
status_code = 200
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return [fake_release]
|
||||
|
||||
class _FakeClient:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
return None
|
||||
|
||||
async def get(self, *_, **__):
|
||||
return _Resp()
|
||||
|
||||
with (
|
||||
patch.object(_httpx, "AsyncClient", _FakeClient),
|
||||
patch("backend.app.api.routes.updates._is_ha_addon", return_value=False),
|
||||
patch("backend.app.api.routes.updates._is_docker_environment", return_value=True),
|
||||
):
|
||||
response = await async_client.get("/api/v1/updates/check")
|
||||
body = response.json()
|
||||
assert body["is_ha_addon"] is False
|
||||
assert body["is_docker"] is True
|
||||
assert body["update_method"] == "docker"
|
||||
|
||||
def test_parse_version(self):
|
||||
from backend.app.api.routes.updates import parse_version
|
||||
|
||||
@@ -261,6 +398,7 @@ class TestUpdatesAPI:
|
||||
return "v0.2.4b1"
|
||||
|
||||
with (
|
||||
patch.object(updates_module, "_is_ha_addon", return_value=False),
|
||||
patch.object(updates_module, "_is_docker_environment", return_value=False),
|
||||
patch.object(updates_module, "_perform_update", side_effect=fake_perform_update),
|
||||
patch.object(updates_module, "_discover_target_release", side_effect=fake_discover),
|
||||
@@ -289,6 +427,7 @@ class TestUpdatesAPI:
|
||||
updates_module._update_status = {"status": "idle", "progress": 0, "message": "", "error": None}
|
||||
|
||||
with (
|
||||
patch.object(updates_module, "_is_ha_addon", return_value=False),
|
||||
patch.object(updates_module, "_is_docker_environment", return_value=False),
|
||||
patch.object(updates_module, "_discover_target_release", side_effect=fake_discover),
|
||||
):
|
||||
|
||||
@@ -255,4 +255,54 @@ describe('Layout', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update banner suppression for HA addon', () => {
|
||||
// HA Supervisor surfaces its own update notification natively in the HA
|
||||
// UI, so the in-app banner would be duplicate noise that links to a page
|
||||
// that just says "update via HA". Suppress it for HA addon deployments.
|
||||
it('hides the update-available banner when running as an HA addon', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/updates/check', () => {
|
||||
return HttpResponse.json({
|
||||
update_available: true,
|
||||
current_version: '0.2.4',
|
||||
latest_version: '0.2.5',
|
||||
is_docker: true,
|
||||
is_ha_addon: true,
|
||||
update_method: 'ha_addon',
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<Layout />);
|
||||
|
||||
await waitFor(() => {
|
||||
const sidebar = document.querySelector('aside');
|
||||
expect(sidebar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(document.body.textContent).not.toContain('Update available');
|
||||
});
|
||||
|
||||
it('still shows the update-available banner for plain Docker deployments', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/updates/check', () => {
|
||||
return HttpResponse.json({
|
||||
update_available: true,
|
||||
current_version: '0.2.4',
|
||||
latest_version: '0.2.5',
|
||||
is_docker: true,
|
||||
is_ha_addon: false,
|
||||
update_method: 'docker',
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<Layout />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body.textContent).toContain('0.2.5');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,6 +174,73 @@ describe('SettingsPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('update CTA per deployment shape', () => {
|
||||
// The update card branches on the deployment shape returned by
|
||||
// /updates/check. Each branch is mutually exclusive — verify the right
|
||||
// one wins so HA addon users never see the docker-compose snippet
|
||||
// (which they can't run from inside an HA addon container) and Docker
|
||||
// users never see the in-app Install button (which would no-op).
|
||||
const renderWithUpdateCheck = async (
|
||||
checkBody: Record<string, unknown>,
|
||||
) => {
|
||||
server.use(
|
||||
http.get('/api/v1/settings/', () =>
|
||||
HttpResponse.json({ ...mockSettings, check_updates: true }),
|
||||
),
|
||||
http.get('/api/v1/updates/check', () => HttpResponse.json(checkBody)),
|
||||
);
|
||||
render(<SettingsPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Updates')).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
it('shows the HA Supervisor message when running as an HA addon', async () => {
|
||||
await renderWithUpdateCheck({
|
||||
update_available: true,
|
||||
current_version: '0.2.4',
|
||||
latest_version: '0.2.5',
|
||||
release_name: '0.2.5',
|
||||
release_notes: '',
|
||||
release_url: 'https://example.invalid/r',
|
||||
published_at: '2099-01-01T00:00:00Z',
|
||||
is_docker: true,
|
||||
is_ha_addon: true,
|
||||
update_method: 'ha_addon',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Home Assistant Supervisor/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
// Docker hint must NOT render — HA branch wins.
|
||||
expect(screen.queryByText('docker compose pull && docker compose up -d')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /install update/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the docker-compose snippet for Docker (non-HA) deployments', async () => {
|
||||
await renderWithUpdateCheck({
|
||||
update_available: true,
|
||||
current_version: '0.2.4',
|
||||
latest_version: '0.2.5',
|
||||
release_name: '0.2.5',
|
||||
release_notes: '',
|
||||
release_url: 'https://example.invalid/r',
|
||||
published_at: '2099-01-01T00:00:00Z',
|
||||
is_docker: true,
|
||||
is_ha_addon: false,
|
||||
update_method: 'docker',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('docker compose pull && docker compose up -d')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText(/Home Assistant Supervisor/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /install update/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tabs navigation', () => {
|
||||
it('can switch to Network tab', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -2354,7 +2354,8 @@ export interface UpdateCheckResult {
|
||||
error?: string;
|
||||
message?: string;
|
||||
is_docker?: boolean;
|
||||
update_method?: 'docker' | 'git';
|
||||
is_ha_addon?: boolean;
|
||||
update_method?: 'docker' | 'git' | 'ha_addon';
|
||||
}
|
||||
|
||||
export interface UpdateStatus {
|
||||
@@ -4417,7 +4418,7 @@ export const api = {
|
||||
getVersion: () => request<VersionInfo>('/updates/version'),
|
||||
checkForUpdates: () => request<UpdateCheckResult>('/updates/check'),
|
||||
applyUpdate: () =>
|
||||
request<{ success: boolean; message: string; status?: UpdateStatus; is_docker?: boolean }>('/updates/apply', {
|
||||
request<{ success: boolean; message: string; status?: UpdateStatus; is_docker?: boolean; is_ha_addon?: boolean }>('/updates/apply', {
|
||||
method: 'POST',
|
||||
}),
|
||||
getUpdateStatus: () => request<UpdateStatus>('/updates/status'),
|
||||
|
||||
@@ -377,10 +377,14 @@ export function Layout() {
|
||||
setDragOverId(null);
|
||||
};
|
||||
|
||||
// Show update banner if update available and not dismissed for this version
|
||||
// Show update banner if update available and not dismissed for this version.
|
||||
// Suppressed when running as a Home Assistant addon — HA Supervisor surfaces
|
||||
// its own update notification in the HA UI, so the in-app banner is duplicate
|
||||
// noise that links to a page that just says "update via HA."
|
||||
const showUpdateBanner = updateCheck?.update_available &&
|
||||
updateCheck.latest_version &&
|
||||
updateCheck.latest_version !== dismissedUpdateVersion;
|
||||
updateCheck.latest_version !== dismissedUpdateVersion &&
|
||||
!updateCheck.is_ha_addon;
|
||||
|
||||
const dismissUpdateBanner = () => {
|
||||
if (updateCheck?.latest_version) {
|
||||
|
||||
@@ -2068,6 +2068,7 @@ export default {
|
||||
updateAvailableVersion: 'Update verfügbar: v{{version}}',
|
||||
releaseNotes: 'Versionshinweise',
|
||||
updateViaDocker: 'Update über Docker Compose:',
|
||||
updateViaHomeAssistant: 'Updates werden vom Home Assistant Supervisor verwaltet. Öffne Einstellungen → Add-ons → Bambuddy in Home Assistant, um die neue Version zu installieren.',
|
||||
installUpdate: 'Update installieren',
|
||||
latestVersionRunning: 'Sie verwenden die neueste Version',
|
||||
failedToCheckUpdates: 'Update-Prüfung fehlgeschlagen: {{error}}',
|
||||
|
||||
@@ -2071,6 +2071,7 @@ export default {
|
||||
updateAvailableVersion: 'Update available: v{{version}}',
|
||||
releaseNotes: 'Release Notes',
|
||||
updateViaDocker: 'Update via Docker Compose:',
|
||||
updateViaHomeAssistant: 'Updates are managed by the Home Assistant Supervisor. Open Settings → Add-ons → Bambuddy in Home Assistant to install the new version.',
|
||||
installUpdate: 'Install Update',
|
||||
latestVersionRunning: "You're running the latest version",
|
||||
failedToCheckUpdates: 'Failed to check for updates: {{error}}',
|
||||
|
||||
@@ -2019,6 +2019,7 @@ export default {
|
||||
updateAvailableVersion: 'Mise à jour disponible : v{{version}}',
|
||||
releaseNotes: 'Notes de version',
|
||||
updateViaDocker: 'Mettre à jour via Docker Compose :',
|
||||
updateViaHomeAssistant: 'Les mises à jour sont gérées par le superviseur Home Assistant. Ouvrez Paramètres → Modules complémentaires → Bambuddy dans Home Assistant pour installer la nouvelle version.',
|
||||
installUpdate: 'Installer la mise à jour',
|
||||
latestVersionRunning: 'Vous utilisez la dernière version',
|
||||
failedToCheckUpdates: 'Échec de la vérification des mises à jour : {{error}}',
|
||||
|
||||
@@ -2018,6 +2018,7 @@ export default {
|
||||
updateAvailableVersion: 'Aggiornamento disponibile: v{{version}}',
|
||||
releaseNotes: 'Note di rilascio',
|
||||
updateViaDocker: 'Aggiorna tramite Docker Compose:',
|
||||
updateViaHomeAssistant: 'Gli aggiornamenti sono gestiti dal Supervisor di Home Assistant. Apri Impostazioni → Add-on → Bambuddy in Home Assistant per installare la nuova versione.',
|
||||
installUpdate: 'Installa aggiornamento',
|
||||
latestVersionRunning: 'Stai usando l\'ultima versione',
|
||||
failedToCheckUpdates: 'Controllo aggiornamenti fallito: {{error}}',
|
||||
|
||||
@@ -2067,6 +2067,7 @@ export default {
|
||||
updateAvailableVersion: 'アップデート利用可能: v{{version}}',
|
||||
releaseNotes: 'リリースノート',
|
||||
updateViaDocker: 'Docker Composeでアップデート:',
|
||||
updateViaHomeAssistant: 'アップデートはHome Assistant Supervisorによって管理されます。Home Assistantの設定→アドオン→Bambuddyを開いて新しいバージョンをインストールしてください。',
|
||||
installUpdate: 'アップデートをインストール',
|
||||
latestVersionRunning: '最新バージョンを使用しています',
|
||||
failedToCheckUpdates: 'アップデートの確認に失敗しました: {{error}}',
|
||||
|
||||
@@ -2018,6 +2018,7 @@ export default {
|
||||
updateAvailableVersion: 'Atualização disponível: v{{version}}',
|
||||
releaseNotes: 'Notas da versão',
|
||||
updateViaDocker: 'Atualizar via Docker Compose:',
|
||||
updateViaHomeAssistant: 'As atualizações são gerenciadas pelo Supervisor do Home Assistant. Abra Configurações → Complementos → Bambuddy no Home Assistant para instalar a nova versão.',
|
||||
installUpdate: 'Instalar atualização',
|
||||
latestVersionRunning: 'Você está usando a versão mais recente',
|
||||
failedToCheckUpdates: 'Falha ao verificar atualizações: {{error}}',
|
||||
|
||||
@@ -2062,6 +2062,7 @@ export default {
|
||||
updateAvailableVersion: '可用更新:v{{version}}',
|
||||
releaseNotes: '发布说明',
|
||||
updateViaDocker: '通过 Docker Compose 更新:',
|
||||
updateViaHomeAssistant: '更新由 Home Assistant Supervisor 管理。请在 Home Assistant 中打开 设置 → 加载项 → Bambuddy 以安装新版本。',
|
||||
installUpdate: '安装更新',
|
||||
latestVersionRunning: '您正在运行最新版本',
|
||||
failedToCheckUpdates: '检查更新失败:{{error}}',
|
||||
|
||||
@@ -2062,6 +2062,7 @@ export default {
|
||||
updateAvailableVersion: '可用更新:v{{version}}',
|
||||
releaseNotes: '發布說明',
|
||||
updateViaDocker: '透過 Docker Compose 更新:',
|
||||
updateViaHomeAssistant: '更新由 Home Assistant Supervisor 管理。請在 Home Assistant 中開啟 設定 → 附加元件 → Bambuddy 以安裝新版本。',
|
||||
installUpdate: '安裝更新',
|
||||
latestVersionRunning: '您正在執行最新版本',
|
||||
failedToCheckUpdates: '檢查更新失敗:{{error}}',
|
||||
|
||||
@@ -769,7 +769,7 @@ export function SettingsPage() {
|
||||
const applyUpdateMutation = useMutation({
|
||||
mutationFn: api.applyUpdate,
|
||||
onSuccess: (data) => {
|
||||
if (data.is_docker) {
|
||||
if (data.is_ha_addon || data.is_docker) {
|
||||
showToast(data.message, 'error');
|
||||
} else {
|
||||
refetchUpdateStatus();
|
||||
@@ -2271,6 +2271,12 @@ export function SettingsPage() {
|
||||
<div className="mt-3 p-2 bg-red-500/20 rounded text-sm text-red-400">
|
||||
{updateStatus.error || updateStatus.message}
|
||||
</div>
|
||||
) : updateCheck?.is_ha_addon ? (
|
||||
<div className="mt-3 p-3 bg-bambu-dark-tertiary rounded-lg">
|
||||
<p className="text-sm text-bambu-gray">
|
||||
{t('settings.updateViaHomeAssistant')}
|
||||
</p>
|
||||
</div>
|
||||
) : updateCheck?.is_docker ? (
|
||||
<div className="mt-3 p-3 bg-bambu-dark-tertiary rounded-lg">
|
||||
<p className="text-sm text-bambu-gray mb-2">
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-CTxVw43p.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D4GSGeyj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-7GmlJb0k.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user