diff --git a/CHANGELOG.md b/CHANGELOG.md index 006473210..05d0625dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.5b2] - Unreleased ### Fixed +- **Light theme: low-contrast washed-out text on status/warning banners and colored badges (#1909, reporter @AntonPalmqvist)** — The app was built dark-first: hundreds of hardcoded Tailwind semantic-color text utilities at light shades (`text-amber-400`, `text-blue-300`, `text-green-200`, etc.) had no `dark:` variant, so with `darkMode: 'class'` they applied in the light theme too — light-amber/blue/green text on pale `bg-*-500/10` tints or white cards, barely readable. The three reporter-cited spots were all this bug: the AMS **Drying** status banner (Printers page), the **Archives** no-3MF-thumbnail warning, and the **System/debug-logging** banner. Fixed app-wide by giving every affected semantic text/icon/tint a theme-aware pair — a readable darker shade in light theme (e.g. `text-amber-700`/`-800`/`-900`) with the original light shade pinned to `dark:` so the dark theme is unchanged — across ~100 files (banners, status pills, permission/role badges, method chips, validation errors, stat-card icons, delete-action hovers, and more). The custom CSS-variable `bambu-*` palette (which already self-corrects per theme) and the SpoolBuddy kiosk (dark-theme-only) were deliberately left untouched. - **Sponsor toast ignored its 14-day cooldown and re-fired on every fresh session (#2477, reporter @pchulpjoost)** — The in-app supporter toast ("You've completed X prints with Bambuddy…") reappeared on essentially every visit instead of respecting the documented 14-day cross-family cooldown. **Root cause:** the backend owns the cooldown, but it only persists the cooldown anchor (`last_shown_at`) and the seen-milestone record inside `POST /sponsor-prompt/dismiss` — and the frontend hook only called `dismiss` from the "View supporters" CTA's `onClick`. A user who *saw* the toast but never clicked the CTA persisted no state at all. The per-tab `sessionStorage` guard hid the re-fire within a single browser session, but every fresh session (new tab, reopened browser, or another device — the reporter confirmed "Other OS/Browser has the same problem") re-ran the check against still-empty backend state and showed the same milestone again. To a user opening Bambuddy to check on a finished print, that looked like "every print." **Fix:** the hook now records the toast as shown the moment it renders — it POSTs `/dismiss` (whose documented job is exactly "anchor the 14-day cooldown and record the milestone as shown") immediately after `showPersistentToast`, instead of waiting for a CTA click. Being *displayed* is what arms the cooldown; clicking the CTA stays optional and just navigates to the sponsors page. `frontend/src/hooks/useSponsorPrompt.ts` only; backend cooldown logic unchanged. - **Windows installer: fresh install fails to start — "connection refused", nothing listening on port 8000 (#2474, reporter @fangme)** — On a clean Windows 10 machine, installing the `.exe` left the Bambuddy service showing "running" in services.msc but the dashboard refused every connection (localhost, IP, and hostname), and `netstat -ano` showed nothing on :8000. **Root cause — bottom of a 100k-line `service-stderr.log` traceback:** `init_db()` → SQLAlchemy async engine → `greenlet_spawn` → `ValueError: the greenlet library is required to use this function. DLL load failed while importing _greenlet: The specified module could not be found.` The 100k repeated `merged_lifespan` frames above it are just FastAPI's nested-lifespan stack unwinding — noise. The real failure: greenlet's `_greenlet.pyd` couldn't load, SQLAlchemy's async engine couldn't start, the FastAPI lifespan raised, and uvicorn **never bound the port** — so the supervised process stays up (NSSM sees it running) while the app itself has crashed on startup. **Why greenlet specifically:** the Win32 error 126 ("The specified module could not be found") on a `.pyd` that pip installed successfully means the module is present but a *dependency DLL* is missing. greenlet's extension is **C++** and needs `vcruntime140_1.dll` (table-based exception handling); the python.org **embeddable** distribution the installer bundles ships `vcruntime140.dll` but **not** `vcruntime140_1.dll`. `python313.dll` is pure C and only needs the former, which is exactly why python.exe starts and runs all the way to `init_db` before greenlet is the first thing to need the missing C++ runtime. On machines that already have the VC++ 2015-2022 redistributable installed (the maintainer's box, CI runners) `vcruntime140_1.dll` is in System32 and everything works — masking the bug until a truly fresh Win10 box hit it. **Fix:** the installer build now ships the C++ runtime app-locally — `vcruntime140_1.dll` and `msvcp140.dll` are staged next to `python.exe` (where `vcruntime140.dll` already lives), sourced from a vendored copy if present or the build runner's System32 otherwise, failing the build loudly if neither has them. `installers/windows/build.py` only; the Inno Setup `[Files]` step already copies `staging\python\*` recursively so the new DLLs are packaged automatically. **Workaround for the current build:** install the "Microsoft Visual C++ 2015-2022 Redistributable (x64)" from Microsoft, then restart the Bambuddy service — that puts `vcruntime140_1.dll` in System32 where the embedded Python finds it. - **Virtual Printer "bind interface" dropdown is empty on macOS** — Adding a Virtual Printer on macOS showed no interfaces to bind to. `get_network_interfaces()` only routed Windows to the cross-platform psutil path; macOS fell into the Linux branch, which uses the Linux-only `SIOCGIFADDR`/`SIOCGIFNETMASK` ioctls (`0x8915`/`0x891B`). macOS/BSD have `fcntl` but different ioctl numbers and sockaddr layout, so every per-interface ioctl raised `OSError` and the function silently returned an empty list (and `get_all_interface_ips()`, which has no `ip` binary to fall back to on macOS, inherited the empty result). Interface enumeration now routes **all** non-Linux platforms (macOS, BSD, Windows) through psutil, which returns each interface's name + IPv4 + netmask and filters loopback/link-local/down adapters while keeping real LAN and VPN (utun/Tailscale) interfaces bindable. Linux keeps its existing ioctl path unchanged. diff --git a/frontend/src/components/AMSHistoryModal.tsx b/frontend/src/components/AMSHistoryModal.tsx index 96ca8fa41..a9af37519 100644 --- a/frontend/src/components/AMSHistoryModal.tsx +++ b/frontend/src/components/AMSHistoryModal.tsx @@ -136,8 +136,8 @@ export function AMSHistoryModal({ const tempTrend = getTrend(chartData.map(d => d.temperature)); const TrendIcon = ({ trend }: { trend: string }) => { - if (trend === 'up') return ; - if (trend === 'down') return ; + if (trend === 'up') return ; + if (trend === 'down') return ; return ; }; diff --git a/frontend/src/components/APIBrowser.tsx b/frontend/src/components/APIBrowser.tsx index c49c40bd9..31c935cde 100644 --- a/frontend/src/components/APIBrowser.tsx +++ b/frontend/src/components/APIBrowser.tsx @@ -70,11 +70,11 @@ interface APIResponse { } const METHOD_COLORS: Record = { - get: 'bg-blue-500/20 text-blue-400 border-blue-500/30', - post: 'bg-green-500/20 text-green-400 border-green-500/30', - put: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', - patch: 'bg-orange-500/20 text-orange-400 border-orange-500/30', - delete: 'bg-red-500/20 text-red-400 border-red-500/30', + get: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-500/30', + post: 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400 border-green-300 dark:border-green-500/30', + put: 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-500/30', + patch: 'bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400 border-orange-300 dark:border-orange-500/30', + delete: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 border-red-300 dark:border-red-500/30', }; function resolveRef(schema: OpenAPISchema, ref: string): SchemaSpec { @@ -340,7 +340,7 @@ function EndpointItem({ path, method, spec, schema, apiKey }: EndpointItemProps)
{param.schema?.enum ? ( {preset.name}
{preset.source === 'local' && ( - + {t('profiles.localProfiles.badge')} )} {preset.source === 'orca_cloud' && ( - + {t('configureAmsSlot.orcaCloud')} )} @@ -1210,7 +1210,7 @@ export function ConfigureAmsSlotModal({ )} {preset.source === 'builtin' && ( - + {t('configureAmsSlot.builtin')} )} @@ -1258,7 +1258,7 @@ export function ConfigureAmsSlotModal({ {t('configureAmsSlot.noMatchingKProfiles')}

) : ( - + {t('configureAmsSlot.selectFilamentFirst')} )} @@ -1402,7 +1402,7 @@ export function ConfigureAmsSlotModal({ {/* Filament Profile Select */}
{preset.name}
{preset.source === 'local' && ( - + {t('profiles.localProfiles.badge')} )} {preset.source === 'orca_cloud' && ( - + {t('configureAmsSlot.orcaCloud')} )} @@ -1450,7 +1450,7 @@ export function ConfigureAmsSlotModal({ )} {preset.source === 'builtin' && ( - + {t('configureAmsSlot.builtin')} )} @@ -1497,7 +1497,7 @@ export function ConfigureAmsSlotModal({ {t('configureAmsSlot.noMatchingKProfiles')}

) : ( - + {t('configureAmsSlot.selectFilamentFirst')} )} @@ -1653,7 +1653,7 @@ export function ConfigureAmsSlotModal({ variant="secondary" onClick={() => resetMutation.mutate()} disabled={resetMutation.isPending || configureMutation.isPending} - className="text-red-400 hover:text-red-300 hover:bg-red-500/10" + className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-500/10" > {resetMutation.isPending ? ( <> @@ -1693,7 +1693,7 @@ export function ConfigureAmsSlotModal({ {/* Error */} {(configureMutation.isError || resetMutation.isError) && ( -
+
{(configureMutation.error as Error)?.message || (resetMutation.error as Error)?.message}
)} diff --git a/frontend/src/components/ConfirmModal.tsx b/frontend/src/components/ConfirmModal.tsx index d4fef4645..f42dd9ef1 100644 --- a/frontend/src/components/ConfirmModal.tsx +++ b/frontend/src/components/ConfirmModal.tsx @@ -63,11 +63,11 @@ export function ConfirmModal({ const variantStyles = { danger: { - icon: 'text-red-400', + icon: 'text-red-600 dark:text-red-400', button: 'bg-red-500 hover:bg-red-600', }, warning: { - icon: 'text-yellow-400', + icon: 'text-yellow-600 dark:text-yellow-400', button: 'bg-yellow-500 hover:bg-yellow-600 text-black', }, default: { diff --git a/frontend/src/components/ConnectionDiagnostic.tsx b/frontend/src/components/ConnectionDiagnostic.tsx index 89f82f73f..7bddbe9db 100644 --- a/frontend/src/components/ConnectionDiagnostic.tsx +++ b/frontend/src/components/ConnectionDiagnostic.tsx @@ -19,8 +19,8 @@ import { function StatusIcon({ status }: { status: DiagnosticStatus }) { if (status === 'pass') return ; - if (status === 'fail') return ; - if (status === 'warn') return ; + if (status === 'fail') return ; + if (status === 'warn') return ; return ; } @@ -36,8 +36,8 @@ export function DiagnosticChecklist({ result }: { result: PrinterDiagnosticResul result.overall === 'ok' ? 'bg-bambu-green/10 border-bambu-green/30 text-bambu-green' : result.overall === 'warnings' - ? 'bg-amber-500/10 border-amber-500/30 text-amber-300' - : 'bg-red-500/10 border-red-500/30 text-red-300'; + ? 'bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-300' + : 'bg-red-50 dark:bg-red-500/10 border-red-300 dark:border-red-500/30 text-red-700 dark:text-red-300'; const renderCheck = (check: DiagnosticCheck) => { const params = @@ -189,7 +189,7 @@ export function ConnectionDiagnosticModal(props: ConnectionDiagnosticModalProps) )} {diagnose.isError && ( -
+
{t('diagnostic.runFailed', { error: (diagnose.error as Error).message })}
)} diff --git a/frontend/src/components/ContextMenu.tsx b/frontend/src/components/ContextMenu.tsx index 54c6d3077..9e4d1f99b 100644 --- a/frontend/src/components/ContextMenu.tsx +++ b/frontend/src/components/ContextMenu.tsx @@ -99,7 +99,7 @@ function SubmenuPanel({ subItem.disabled ? 'text-bambu-gray cursor-not-allowed' : subItem.danger - ? 'text-red-400 hover:bg-red-400/10' + ? 'text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-400/10' : 'text-white hover:bg-bambu-dark-tertiary' }`} > @@ -269,7 +269,7 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) { item.disabled ? 'text-bambu-gray cursor-not-allowed' : item.danger - ? 'text-red-400 hover:bg-red-400/10' + ? 'text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-400/10' : 'text-white hover:bg-bambu-dark-tertiary' } ${hasSubmenu && activeSubmenu === index ? 'bg-bambu-dark-tertiary' : ''}`} > diff --git a/frontend/src/components/CreateUserAdvancedAuthModal.tsx b/frontend/src/components/CreateUserAdvancedAuthModal.tsx index 30364d726..55dd0e9f8 100644 --- a/frontend/src/components/CreateUserAdvancedAuthModal.tsx +++ b/frontend/src/components/CreateUserAdvancedAuthModal.tsx @@ -137,7 +137,7 @@ export function CreateUserAdvancedAuthModal({ {/* Username Field */}
{group.name} {group.is_system && ( - ({t('users.system')}) + ({t('users.system')}) )} ))} diff --git a/frontend/src/components/EmailSettings.tsx b/frontend/src/components/EmailSettings.tsx index 8d8ef498a..c87877208 100644 --- a/frontend/src/components/EmailSettings.tsx +++ b/frontend/src/components/EmailSettings.tsx @@ -207,14 +207,14 @@ export function EmailSettings() {
{advancedEnabled ? ( -
+
- +

{t('settings.email.advancedAuthEnabled') || 'Advanced Authentication is enabled'}

-
    +
    • {t('settings.email.feature1') || 'Passwords are auto-generated and emailed to new users'}
    • {t('settings.email.feature2') || 'Users can login with username or email'}
    • {t('settings.email.feature3') || 'Forgot password feature is available'}
    • @@ -224,14 +224,14 @@ export function EmailSettings() {
) : ( -
+
- +

{t('settings.email.advancedAuthDisabled') || 'Advanced Authentication is disabled'}

-

+

{t('settings.email.advancedAuthDisabledDesc') || 'Enable advanced authentication to activate email-based features for user management.'}

diff --git a/frontend/src/components/EmbeddedCameraViewer.tsx b/frontend/src/components/EmbeddedCameraViewer.tsx index 31a15022f..6f8ff3ef0 100644 --- a/frontend/src/components/EmbeddedCameraViewer.tsx +++ b/frontend/src/components/EmbeddedCameraViewer.tsx @@ -644,7 +644,7 @@ export function EmbeddedCameraViewer({ printerId, printerName, viewerIndex = 0, className="p-1 hover:bg-red-500/20 rounded" title="Close" > - +
diff --git a/frontend/src/components/ExternalLinksSettings.tsx b/frontend/src/components/ExternalLinksSettings.tsx index 2991f7676..2451c75da 100644 --- a/frontend/src/components/ExternalLinksSettings.tsx +++ b/frontend/src/components/ExternalLinksSettings.tsx @@ -337,7 +337,7 @@ export function ExternalLinksSettings() { diff --git a/frontend/src/components/FileManagerModal.tsx b/frontend/src/components/FileManagerModal.tsx index c33560a12..625c827c8 100644 --- a/frontend/src/components/FileManagerModal.tsx +++ b/frontend/src/components/FileManagerModal.tsx @@ -702,7 +702,7 @@ export function FileManagerModal({ printerId, printerName, onClose }: FileManage variant="secondary" disabled={selectedFiles.size === 0 || deleteMutation.isPending} onClick={handleDelete} - className="text-red-400 hover:text-red-300" + className="text-red-700 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300" > {deleteMutation.isPending ? ( diff --git a/frontend/src/components/FileUploadModal.tsx b/frontend/src/components/FileUploadModal.tsx index c20710b25..7b80acfc5 100644 --- a/frontend/src/components/FileUploadModal.tsx +++ b/frontend/src/components/FileUploadModal.tsx @@ -215,12 +215,12 @@ export function FileUploadModal({ folderId, onClose, onUploadComplete, onFileUpl {/* ZIP Options */} {hasZipFiles && ( -
+
- +
-

{t('fileManager.zipFilesDetected')}

-

+

{t('fileManager.zipFilesDetected')}

+

{t('fileManager.zipExtractOptions')}