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:
maziggy
2026-05-01 08:01:19 +02:00
parent 889c8bd87f
commit 4aea4be2bd
18 changed files with 329 additions and 21 deletions
+2
View File
@@ -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
+33 -2
View File
@@ -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,
+140 -1
View File
@@ -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();
+3 -2
View File
@@ -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'),
+6 -2
View File
@@ -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) {
+1
View File
@@ -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}}',
+1
View File
@@ -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}}',
+1
View File
@@ -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}}',
+1
View File
@@ -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}}',
+1
View File
@@ -2067,6 +2067,7 @@ export default {
updateAvailableVersion: 'アップデート利用可能: v{{version}}',
releaseNotes: 'リリースノート',
updateViaDocker: 'Docker Composeでアップデート:',
updateViaHomeAssistant: 'アップデートはHome Assistant Supervisorによって管理されます。Home Assistantの設定→アドオン→Bambuddyを開いて新しいバージョンをインストールしてください。',
installUpdate: 'アップデートをインストール',
latestVersionRunning: '最新バージョンを使用しています',
failedToCheckUpdates: 'アップデートの確認に失敗しました: {{error}}',
+1
View File
@@ -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}}',
+1
View File
@@ -2062,6 +2062,7 @@ export default {
updateAvailableVersion: '可用更新:v{{version}}',
releaseNotes: '发布说明',
updateViaDocker: '通过 Docker Compose 更新:',
updateViaHomeAssistant: '更新由 Home Assistant Supervisor 管理。请在 Home Assistant 中打开 设置 → 加载项 → Bambuddy 以安装新版本。',
installUpdate: '安装更新',
latestVersionRunning: '您正在运行最新版本',
failedToCheckUpdates: '检查更新失败:{{error}}',
+1
View File
@@ -2062,6 +2062,7 @@ export default {
updateAvailableVersion: '可用更新:v{{version}}',
releaseNotes: '發布說明',
updateViaDocker: '透過 Docker Compose 更新:',
updateViaHomeAssistant: '更新由 Home Assistant Supervisor 管理。請在 Home Assistant 中開啟 設定 → 附加元件 → Bambuddy 以安裝新版本。',
installUpdate: '安裝更新',
latestVersionRunning: '您正在執行最新版本',
failedToCheckUpdates: '檢查更新失敗:{{error}}',
+7 -1
View File
@@ -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
View File
@@ -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>