diff --git a/CHANGELOG.md b/CHANGELOG.md index dced64eee..1c8557bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.5b1] - Unreleased + +## [0.2.4.5] - 2026-06-03 + ### Added - **System theme detection — sidebar toggle and Settings selector follow OS dark/light preference (#1418, contributed by @TempleClause via PR #1501)** — `ThemeMode` gains a third value `'system'` alongside the existing `'dark'` / `'light'`. The provider listens to `window.matchMedia('(prefers-color-scheme: dark)')`, tracks the OS preference in real time, and exposes a new `resolvedMode: 'light' | 'dark'` to consumers — the actual rendered theme after resolving system → OS preference. Layout's sidebar toggle now cycles `dark → light → system → dark` with the icon hinting at the next stop (`Sun`→`Monitor`→`Moon`); the existing logo selection and the dark/light "active" panel highlight in Settings switched from `mode` to `resolvedMode` so they always reflect what's actually painted, regardless of whether the user chose explicitly or inherited from the OS. Settings → Appearance gained a 3-button Dark / Light / System selector (border-green-keys-off-`mode` so System actually highlights System even when it resolves to dark), with a "Settings saved" toast on click matching the adjacent Background/Accent/Style selects. Existing users' persisted `theme-mode` is untouched — anyone on `dark` or `light` stays there and simply gains an extra stop in the cycle; new installs default to `dark`. **Review-caught fixes shipped in the same PR**: (a) the project's `__tests__/setup.ts` mocked `window.matchMedia` with `vi.fn().mockImplementation(...)`, which `vi.restoreAllMocks()` in three test files reset to "return undefined" — pre-PR nothing called `matchMedia` at render time so the wipe went unnoticed, this PR was the first caller and broke 23 existing tests. Rewritten as a plain function (`Object.defineProperty(window, 'matchMedia', { writable: true, value: (query) => ({...}) })`) so `restoreAllMocks` can't touch it. (b) `themeToggleHint` had previously only been updated in `en.ts`; real translations now ship in all 8 non-English locales (de/es/fr/it/ja/pt-BR/zh-CN/zh-TW) describing the 3-state cycle without referencing the old sun/moon icon pair. (c) PR description reworded to honestly call out the sidebar cycle change as a behaviour change for every user of the toggle (`dark → light → system` now intercepts where users previously got `dark → light → dark`), with the persisted-preference-unchanged caveat made explicit. (d) New i18n key `nav.switchToSystem` with real translations across all 9 locales (`'Switch to system mode'` / `'Zum Systemmodus wechseln'` / `'システムモードに切替'` etc.). **Tests**: 11 new in `ThemeContext.test.tsx` (systemPreference inits from `matchMedia.matches`, change event updates state, resolvedMode follows explicit mode vs systemPreference per `mode` value, dark class applied based on resolved mode, `toggleMode` cycles dark→light→system→dark); 1 new in `Layout.test.tsx` (toggle button title attribute walks the cycle); 4 new in `SettingsPage.test.tsx` (all three buttons render, active green border keys off `mode`, click switches mode, click fires toast). 26 previously-broken tests in `AddNotificationModal.test.tsx` + `NotificationProviderCardStockAlerts.test.tsx` + `CameraTokensPage.test.tsx` pass again post-`setup.ts` fix. Frontend build clean (2682 modules); i18n parity green at 4995 keys × 9 locales (+1 from `switchToSystem`). Contributor handled the entire round-1 review (matchMedia mock, locale parity, PR honesty, full test coverage, toast parity, `.map()` refactor for the button group) in a single revision push, no follow-ups deferred. - **MQTT auth rate-limit on the virtual printer** — Bambuddy's VP exposes an 8-char access code via the slicer-facing MQTT server on port 8883. Without a rate limit the code is brute-forceable by anyone who can reach the VP's bind IP (LAN, Tailscale, or any other tunnel the user chose to expose). The new per-IP limiter records each failed CONNECT auth attempt and rejects further CONNECTs from that IP once 5 failures occur within a 60 s window. The window is sliding (not cumulative), recovers automatically after expiry — no manual unblock — and successful auth clears the IP's prior failure history so a user who fat-fingered their code 3 times then got it right isn't penalised on their next reconnect. Per-IP tracker uses `time.monotonic()` so wall-clock jumps can't extend or shorten the window unexpectedly. Constants `_AUTH_RATE_LIMIT_MAX_ATTEMPTS = 5` and `_AUTH_RATE_LIMIT_WINDOW_SECONDS = 60.0` are module-level for ops tunability. 5 unit tests in `test_vp_mqtt_server.py::TestAuthRateLimit` pin the under-limit/at-limit/window-recovery/multi-IP/success-clears semantics.