Branch
[FIX] web_responsive: make the mobile apps menu one-handed - On phones the navbar shows a visible, labelled apps grid icon (it was an invisible tap area). - The apps sidebar slides in from the left, next to the icon that opens it, and swipe-left closes it. - "All Apps" in the sidebar opens the full-screen app menu with its search box, like on desktop. - Tapping the icon while the app menu covers a screen closes it; when the app menu is the only screen (redirect-to-home users) it opens the sidebar instead. - The redundant sidebar button in the user menu is removed. - The app title now shortens before the user menu is pushed off narrow (360px) screens. - Vietnamese translation added; template refreshed. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_mrp: remove Odoo wording from Manufacturing help texts Manufacturing shows Odoo in the help of the automatic printing options of the manufacturing operation type, in the help of Operation Dependencies and of the manual consumption of components, and in a note that sends the user to the Odoo IoT App and Odoo's documentation. Say 'the system' in the help texts. Replace the note with a plain statement that a PDF preview opens by default: the Viindoo IoT box does not route these reports to a printer, so the advice does not apply. The work center control panel digest tip is already rewritten by this module. Bump the module version and update the .pot/.po. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_pos: remove Odoo wording from POS user-facing messages The POS 'Connection to the printer failed' dialog told cashiers to follow Odoo's documentation. Replace it with the same advice in Viindoo words and no documentation link; this also covers the Epson ePOS path, which reuses the base printer message. Same kind of user-facing Odoo mentions covered in this module: - the generic 'Odoo Server Error' dialog title is now 'Server Error'; - the customer display 'Powered by' badge shows the Viindoo logo instead of the Odoo one; - the IoT customer display failure notice no longer talks about an Odoo database subscription. It is swapped on the notification the core service raises, and the module's terms are now sent to that public page so the Vietnamese wording is applied there. Bump the module version and update the .pot/.po. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_pos: remove Odoo wording from POS user-facing messages The POS 'Connection to the printer failed' dialog told cashiers to follow Odoo's documentation. Replace it with the same advice in Viindoo words and no documentation link; this also covers the Epson ePOS path, which reuses the base printer message. Same kind of user-facing Odoo mentions covered in this module: - the generic 'Odoo Server Error' dialog title is now 'Server Error'; - the customer display 'Powered by' badge shows the Viindoo logo instead of the Odoo one; - the IoT customer display failure notice no longer talks about an Odoo database subscription. It is swapped on the notification the core service raises, and the module's terms are now sent to that public page so the Vietnamese wording is applied there. Bump the module version and update the .pot/.po. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_pos: remove Odoo wording from printer error dialog The POS 'Connection to the printer failed' dialog told cashiers to follow Odoo's documentation. Replace it with the same advice in Viindoo words and no documentation link; this also covers the Epson ePOS path, which reuses the base printer message. Same kind of user-facing Odoo mentions covered in this module: - the generic 'Odoo Server Error' dialog title is now 'Server Error'; - the customer display 'Powered by' badge shows the Viindoo logo instead of the Odoo one. Bump the module version and update the .pot/.po. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[I18N] *: align Vietnamese terminology with the corrected core catalogs Odoo 18 core's Vietnamese catalogs were corrected against a glossary researched per business domain (Viindoo/odoo#1299). This brings this repository's catalogs in line, so a Vietnamese user sees one term for one concept across core and Viindoo modules: - Records shared with core (model names, re-declared fields, selection values) now carry core's corrected wording exactly; before, the module loaded last decided which label users saw. - Records shared between Viindoo modules read the same in every module that declares them, following the module that defines them. - Key terms applied in context, e.g. Đơn bán / Đơn mua, Phiếu kho, Loại nghiệp vụ, Hoàn thành, Duyệt / Đã duyệt / Chờ duyệt, Thiết lập, Nháp, Ghi sổ, Phát sinh kế toán, Tài khoản quản trị, Chế độ thuế, Hóa đơn điều chỉnh giảm, Trung tâm sản xuất, Lệnh công đoạn, Làm thêm giờ, Bảng giờ công, Đối chiếu, Danh sách chặn, Công cụ (wizards), plus the creator/updater boilerplate. - Definite errors fixed: reversed or wrong meanings, broken placeholders and markup, garbled text and typos. - Old-style tone marks throughout (hóa, hủy, khóa). Only msgstr content changes; untranslated entries are left as they are.
[FIX] viin_brand_crm: depend on viin_brand_digest to fix install-order crash Installing modules in some environments could abort partway through with: ParseError: .../viin_brand_crm/data/digest_data.xml:5 ValueError: Invalid field 'active' on model 'digest.tip' digest_data.xml writes digest.tip.active, but that field is defined by the sibling module viin_brand_digest, not by viin_brand_crm or by crm. Both modules are auto_install: True with no dependency edge between them, so Odoo's loader has no ordering guarantee and can process viin_brand_crm before viin_brand_digest has added the field - observed on a real build aborting at module 72 of 126. Add viin_brand_digest to viin_brand_crm's depends so the loader is forced to install it first. This does not widen who gets auto-installed: core crm already depends on digest, so whenever crm is installed, digest is installed too, which means viin_brand_digest (depends ['digest'], auto_install) was already an install candidate in exactly the same situations. The new edge only constrains order, which is precisely what was missing. digest_data.xml itself is untouched - the data is correct; it was only running too early.
[IMP] viin_brand: own the module-icon and favicon branding mechanism Moved from to_base (tvtmaaddons), where it ran for every database served by a process that had viin_brand on its addons path and forced to_base to be loaded server wide. The functions, hooks, models (company favicon, settings-page icon RPC, menu web_icon rewrite, session marker), data, views, assets and the QUnit mock server come over unchanged apart from their imports; the uninstall hook now actually restores the core functions it replaced. viin_brand is the module to load server wide from now on (--load=base,web,viin_brand); it warns when it is not.
[FIX] web_responsive: dismiss the apps menu when a search result is chosen Searching the apps menu and choosing a result appeared to do nothing: the menus filtered, but clicking one or pressing Enter left the user where they were. Navigation was in fact succeeding. The controller changed and the URL moved to the target, with no JS error and every RPC returning 200 - but the full-screen overlay stayed mounted over the destination and, being position:fixed with pointer-events:auto, swallowed clicks on the view underneath. The overlay must be dismissed explicitly. When the apps menu became an overlay-service render independent of the action stack, dismiss() was added before selectMenu() on the app-icon path but not on the search paths, and core's selectMenu() only mutates the action stack. Three call sites needed it, not one: the canonical search bar (whose handler the Enter key also reaches, so click and Enter are one path), the fuse variant that inherits it, and the command-palette variant, which fails through a different mechanism because openMainPalette was passed no onClose. The command-palette path dismisses only when a menu was actually selected. onClose fires on every palette close, Escape included, so dismissing unconditionally would tear the apps menu down when a user merely backs out of the palette - trading one surprise for another. It also registers that callback once per open palette session: core's command service ignores onClose on every call after the first while its palette is open, so re-passing it per keystroke orphaned a patch layer on the session-lifetime menu service each time. The app-tile route, which every core tour uses to reach an app, was already correct and is untouched. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_common: withdraw the square-corners brand decision The product owner withdrew the "square corners are part of the Viindoo brand identity" decision. This module forced Bootstrap's global $border-radius to 0 in bootstrap_overridden.scss; drop that override so Odoo's own product-wide border-radius default applies again across buttons, inputs, cards, badges, dropdowns and modals. No sibling radius variable ($border-radius-sm/lg/xl/xxl) is touched - those are independent Bootstrap literals this module never overrode. viin_brand_square_corners_tour and its Python HttpCase driver (test_real_backend_buttons_keep_the_brand_square_corners) existed solely to guard the withdrawn contract, so both are retired together with it. Every other assertion in the same tour file and test class (brand primary colour, theme secondary colour, base font-size, statusbar outline/fill, discuss sidebar, settings tab) is untouched. Verified: before this change the retired assertion measured .btn border-top-left-radius = 0px (passing, as designed). After this change the full --test-tags /viin_brand_common suite is green (0 failed, 0 error, 0 skipped, 20/20), and a fresh computed-style probe measures .btn border-top-left-radius = 4px - Odoo core's own $o-border-radius default - confirming the product-wide change took effect. Bump manifest version 0.3.4 -> 0.3.5. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] web_responsive: stop leaking a patch layer per command-palette keystroke A prior fix on this branch (05b40f6) made the command-palette search variant dismiss the apps-menu overlay only when a menu was actually selected, using a temporary patch() on the menu service to detect the selection. That patch was re-applied on every keystroke, but core's command service silently drops the onClose callback on every call after the first while its palette stays open - so only the first keystroke's patch layer was ever unwound, and each keystroke after that permanently leaked one more wrapper layer onto the menu service for the rest of the browser session. Track whether an open-palette session is already being watched and only patch the menu service + register the dismiss callback once per session; later keystrokes in the same session now only update the palette's search value. Adds a Hoot test covering the character-by-character search-typing path (the one the prior tests bypassed) and trims two test comments per the repo's comment convention. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] web_responsive: correct a false idempotency comment + bind the session_info test Three review findings from the forward-port batch. The migration's comment claimed the is_redirect_home filter makes a re-run match nothing, so an opted-out user is never silently re-opted-in. The second half was false: an opted-out user holds False, which is exactly what the domain selects. Odoo compiles a boolean "= False" to (col IS NULL OR col = FALSE), so the domain provably cannot tell "never backfilled" from "deliberately opted out" - the manifest version gate is what makes this run once. The comment now says that. The domain and the logic are unchanged and correct: that same collapse is what lets the backfill match the NULL rows every pre-existing user actually has. test_session_info only ever exercised the preference at True, so a hardcoded True in the session_info transport would have passed it. It now also flips the preference to False and re-asserts, binding the assertion to the user's real stored value on both sides. The new JS test file carried an OCA sibling's copyright header; it is Viindoo's own 2026 file. The LGPL-3 license line stays - the module is OCA.
[IMP] viin_brand_website_sale: drop unverifiable "#1" claim from promo The eCommerce "Powered by" footer stated "The #1 Open Source eCommerce Software" (vi_VN: "... Mã nguồn mở số 1"), an unverifiable superiority ranking. Vietnam's advertising law forbids "#1"/"best" style claims without supporting evidence and penalises them heavily, so remove the ranking wording from both the template source and the Vietnamese translation. - views/templates.xml: drop the "#1" from the promo label and the standalone "The" text node so the label reads "Open Source eCommerce Software"; add a comment at the label noting the VN advertising-law constraint. - i18n/vi_VN.po and .pot: realign msgid to the new source string and drop the now-orphaned "The" term; msgstr "... Mã nguồn mở số 1" -> "... Mã nguồn mở". - bump module version 0.1 -> 0.2 so existing databases pick up the updated view and translation on upgrade.
[FIX] viin_backend_theme: keep apps menu open while app reloads Opening the apps menu from the navbar while an app was still reloading (for example clicking the app brand and then the apps button before the screen finished loading) closed the menu before its tiles appeared, so the user saw nothing to pick an app from. It broke the runbot hr_expense tour create_expense_no_employee_access_tour (step 13/13, .o_app tile not found, 4 failures out of 5 runs). Cause: NavBar.setup() closed the menu overlay on every ACTION_MANAGER:UI-UPDATED. The reload started by the brand click finished while the overlay's ViinHomeMenu was still in onWillStart (its mail.activity search_count and res.users read RPCs), so the overlay entry was removed before any tile was mounted. The UI-UPDATED listener is removed. The menu is still closed by a route change, outside click, Escape and when the navbar is destroyed; the mobile "All Apps" guard that refuses to open a menu before any screen exists is unchanged. Tests: a Hoot desktop test holds the list reload and the menu's own RPCs and requires the overlay and its app tiles to be on screen once both finish; it fails with the listener and passes without it. The mobile test that faked a screen to assert the removed close is dropped; the real guard stays covered by "All Apps tapped while the boot app menu is still loading opens no overlay and the toggle keeps working". Signed-off-by: David Tran <david.tran@tvtmarine.com>
[I18N] *: align Vietnamese terminology with the corrected core catalogs Odoo 19 core's Vietnamese catalogs were corrected against the glossary researched per business domain (Viindoo/odoo#1302). This brings this repository's catalogs in line, so a Vietnamese user sees one term for one concept across core and Viindoo modules. The 18.0 alignment came over with the forward-port; this covers the strings new in 19.0 and the terms decided for 19.0: - Records shared with core (model names, re-declared fields, selection values) now carry core's 19.0 wording exactly; before, the module loaded last decided which label users saw. - 19.0 terms: weekdays in word form (Thứ Hai ... Chủ Nhật). - The approved glossary applied in context to strings new in 19.0 (Phiếu kho, Loại nghiệp vụ, Cổng thanh toán, Đóng kiện, ...), and debranded help texts say "hệ thống" where the source does, not "Odoo". - Definite errors fixed: reversed or wrong meanings, broken placeholders and markup, English left in translations, garbled text and typos. - Old-style tone marks throughout (hóa, hủy, khóa), ASCII hyphens. Only msgstr content changes; untranslated entries are left as they are.
Merge remote-tracking branch 'origin/18.0' into 19.0 Range 4a5885d (1 commit). Conflict resolution: - i18n/vi_VN.po: kept the 19.0 catalog shape and entries; where 18.0 changed a msgstr and the same msgid exists on 19.0, the 18.0 wording wins. - viin_brand_common (renamed viin_brand_base_setup on 19.0): its 18.0 msgstr change applied to viin_brand_base_setup/i18n/vi_VN.po. - viin_brand_membership (removed on 19.0): catalog stays deleted.
[FIX] viin_brand_web: scope the navbar hover/active hook guard to the regular navbar test_navbar_menu_entry_interactive_states_use_deep_chrome_rung asserted that core never DECLARES --NavBar-entry-backgroundColor--hover/--active, because the var() fallback those hooks read is what carries the brand's deep chrome rung. It checked that with a substring scan over the whole compiled web.assets_backend, so any declaration anywhere tripped it. viin_customizer declares both hooks to theme its own dark top bar: `.customizer_container .customizer_navbar .o_main_navbar { ... }`. That rule never applies to the regular web client navbar, so the brand rung there is untouched, yet the scan reported it as core re-declaring the hook and the test went red as soon as viin_customizer was installed. The guard now collects only the declarations whose selector can reach the regular navbar entries - the entry itself or any ancestor on the NAVBAR_ANCESTORS chain, including class-less subjects such as :root. Custom properties inherit, so an ancestor-level declaration counts; a declaration under a context the navbar is not in does not. The check cannot be left to the cascade resolution further down: that walks only the entry, so a `.o_main_navbar` declaration inherited from above would stay invisible to it. Verified on 19.0 with viin_brand_web and viin_customizer installed: the old guard fails with "'--NavBar-entry-backgroundColor--hover:' unexpectedly found", the new one passes, and temporarily adding `.o_main_navbar { --NavBar-entry-backgroundColor--hover: #00bbce }` to the brand bundle still makes it fail. Also reword the docstring of viin_backend_theme's test_navbar_apps_icon_computes_cores_navbar_entry_colour. It said nothing in the addons path declares --NavBar-entry-color, which stops being true once viin_customizer is installed. It now says no rule that reaches the regular navbar declares it, and that viin_customizer's declaration lives only under its own .customizer_navbar scope, which the cascade resolver excludes. Docstring only; that test class still passes with viin_customizer installed. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_web: scope the navbar hover/active hook guard to the regular navbar test_navbar_menu_entry_interactive_states_use_deep_chrome_rung asserted that core never DECLARES --NavBar-entry-backgroundColor--hover/--active, because the var() fallback those hooks read is what carries the brand's deep chrome rung. It checked that with a substring scan over the whole compiled web.assets_backend, so any declaration anywhere tripped it. viin_customizer declares both hooks to theme its own dark top bar: `.customizer_container .customizer_navbar .o_main_navbar { ... }`. That rule never applies to the regular web client navbar, so the brand rung there is untouched, yet the scan reported it as core re-declaring the hook and the test went red as soon as viin_customizer was installed. The guard now collects only the declarations whose selector can reach the regular navbar entries - the entry itself or any ancestor on the NAVBAR_ANCESTORS chain, including class-less subjects such as :root. Custom properties inherit, so an ancestor-level declaration counts; a declaration under a context the navbar is not in does not. The check cannot be left to the cascade resolution further down: that walks only the entry, so a `.o_main_navbar` declaration inherited from above would stay invisible to it. Verified on 19.0 with viin_brand_web and viin_customizer installed: the old guard fails with "'--NavBar-entry-backgroundColor--hover:' unexpectedly found", the new one passes, and temporarily adding `.o_main_navbar { --NavBar-entry-backgroundColor--hover: #00bbce }` to the brand bundle still makes it fail. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[IMP] viin_brand_website: one click from the site into the backend Clicking the apps grid on the website used to open a dropdown listing the internal user's apps, so reaching the backend cost an extra click every time. The button now links straight to /odoo and no longer toggles anything. /odoo can take a noticeable moment on an install with many modules, so the icon swaps to a spinner on click: the user can see the click registered. A Ctrl/Cmd/Shift or middle click opens the backend in a new tab and leaves the current page's icon alone, and a Back-button restore from the browser's cache puts the icon back rather than leaving it spinning for good. That spinner is wired by a delegated listener on the document, not by a public Interaction. It was written as an Interaction first and it never ran: the interaction service scans from #wrapwrap, and this button renders outside it, under body > .o_frontend_to_backend_nav > .o_frontend_to_backend_buttons. Core drives this same bar from a plain frontend module for the same reason. Both click and auxclick are bound, because a non-primary click fires the latter and core treats the two as a pair. The spinner is a dedicated child element rather than classes on the anchor, and that distinction is the whole of the difference. Core renders that anchor as both the icon and a styled flex box, so a spin class on it rotates the box - background, padding and all - which is what it did at first. Every pending indicator in Odoo 19 puts fa fa-circle-o-notch fa-spin on an element whose only job is to be the spinner, and this now does the same. Measured with the animation frozen at one angle: the old anchor's bounding box grew from 48x54 to 72x72 as the box turned; the anchor now holds 48x54, identical to an untouched button, and only the glyph inside it moves. The tour asserts the anchor carries no spin class, so putting one back there fails loudly instead of passing. Core's own dropdown markup beside the button is left exactly as core renders it, including the ir.ui.menu lookup it performs on every frontend page. Taking it out looked like a free win and was tried twice; core's website_backend_menus_redirect tour opens that dropdown and clicks an entry inside it, and the entry only exists when the lookup runs. The de-branded title on the button is unchanged. Tests: the click, modifier and cache-restore behaviour are covered on a real DOM rather than by matching the text of an inline handler, which an implementation with the transitions swapped passed just as happily. A tour drives the button on a real rendered page and asserts both that the icon swaps and that the button is NOT inside #wrapwrap, so if a future Odoo moves it the guard fails loudly instead of passing for the wrong reason - a unit test cannot make that assertion, because the test helper fabricates a #wrapwrap of its own, which is precisely why nine green unit tests hid a feature that did not work. Bundle guards assert the module really reaches the browser. Also here, and separable from the behaviour: comments in this module's tests cited source by line number. Each now names the symbol instead. One had already rotted badly, naming a method while pointing fifteen lines above its real definition.
[IMP] viin_brand_website: one click from the site into the backend Clicking the apps grid on the website used to open a dropdown listing the internal user's apps, so reaching the backend cost an extra click every time. The button now links straight to /odoo and no longer toggles anything. /odoo can take a noticeable moment on an install with many modules, so the icon swaps to a spinner on click: the user can see the click registered. A Ctrl/Cmd/Shift or middle click opens the backend in a new tab and leaves the current page's icon alone, and a Back-button restore from the browser's cache puts the icon back rather than leaving it spinning for good. That spinner is wired by a delegated listener on the document, not by a public Interaction. It was written as an Interaction first and it never ran: the interaction service scans from #wrapwrap, and this button renders outside it, under body > .o_frontend_to_backend_nav > .o_frontend_to_backend_buttons. Core drives this same bar from a plain frontend module for the same reason. Both click and auxclick are bound, because a non-primary click fires the latter and core treats the two as a pair. Core's own dropdown markup beside the button is left exactly as core renders it, including the ir.ui.menu lookup it performs on every frontend page. Taking it out looked like a free win and was tried twice; core's website_backend_menus_redirect tour opens that dropdown and clicks an entry inside it, and the entry only exists when the lookup runs. The de-branded title on the button is unchanged. Tests: the click, modifier and cache-restore behaviour are covered on a real DOM rather than by matching the text of an inline handler, which an implementation with the transitions swapped passed just as happily. A tour drives the button on a real rendered page and asserts both that the icon swaps and that the button is NOT inside #wrapwrap, so if a future Odoo moves it the guard fails loudly instead of passing for the wrong reason - a unit test cannot make that assertion, because the test helper fabricates a #wrapwrap of its own, which is precisely why nine green unit tests hid a feature that did not work. Bundle guards assert the module really reaches the browser. Also here, and separable from the behaviour: comments in this module's tests cited source by line number. Each now names the symbol instead. One had already rotted badly, naming a method while pointing fifteen lines above its real definition.
[FIX] viin_brand_website: wire the apps button off the DOM it is in The apps-button spinner never ran on a real page. It was written as a public Interaction, and the interaction service only ever scans from #wrapwrap, but the button sits outside it - it is a `body > .o_frontend_to_backend_nav > .o_frontend_to_backend_buttons > a`, a #wrapwrap SIBLING, never a descendant. So the interaction never started, the icon never swapped on click, and a Back-button restore never reset it. Replace it with a delegated click/auxclick listener bound on the document at load time, which does not care where the button lives. The three functions that decide what happens on click are unchanged. The nine unit tests over the old Interaction had all been passing for the wrong reason: the Hoot fixture helper wraps bare markup in a #wrapwrap of its own, so the suite was exercising the button in a position it never occupies on the real page. Those tests now mount the fixture directly against the new listener, and a tour drives the real rendered homepage - asserting both that the click swaps the icon and that the button is NOT inside #wrapwrap, so a future core move that reintroduces the mismatch fails loudly instead of passing for the wrong reason again.
[MISC] viin_brand_spreadsheet_dashboard: guard tours on web_tour Tests that boot the webclient via start_tour required the tour engine unconditionally, so they errored instead of skipping under the acceptance CLI's --skip-auto-install flag, which excludes the web_tour/bus auto-install closure. They now probe res.users for the tour_enabled field in setUp and skip cleanly with a stated reason when web_tour is not installed, so a per-module acceptance run stays readable instead of burying a genuine failure under environment-driven errors. A bundle-compile test is added, compiling this module's real asset bundles and asserting on the compiled output. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[MISC] pyproject: pin the repo's first ruff lint configuration Ruff was running fully unconfigured against this repository. A repo-root pyproject.toml now establishes an explicit configuration, pinning today's implicit rule set so a future ruff upgrade cannot silently change what is enforced. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_web: resolve dark mode from the user, not the browser Two reported symptoms, one root cause: the code treated the `color_scheme` cookie as if it were a preference, when it is a cache of a resolution. A preference belongs to a USER; this cookie belongs to a BROWSER and outlives the session that wrote it. Symptom 1 - another person's cookie decided your scheme. color_scheme() ranked the cookie above the stored preference, so on a shared browser the last user's choice served the next user the wrong compiled bundle. Measured before the change: preference 'light' plus a `color_scheme=dark` cookie rendered data-bs-theme="dark", with the dark bundle referenced. Symptom 2 - "System" always came up light. setScheme('auto') writes the matchMedia-resolved scheme to the cookie, then awaits an ORM write whose own handler expired that very cookie. Measured: the write response carried `color_scheme=; Max-Age=0; Path=/`, the host-only cookie left the jar, and the reload that follows then found no cookie, fell through to core's `return "light"`, and served light whatever the device said. The client re-resolved afterwards, so the user saw light, a half-flip, and dark only on the next reload. The resolution order is now the user's own explicit light/dark preference, then the cookie, then super(). The cookie keeps its place above super() because 'auto' is the one case the server genuinely cannot decide: it cannot read the device preference, so the client resolves it and caches the effective light|dark there. For the same reason writing 'auto' no longer expires the cookie - that would throw away the only copy of the answer the imminent reload needs. That cookie is also core's own client-side source of truth for dark mode: core reads it for graph colours, the colour picker, the ace editor and the pdf.js viewer. Expiring it did not merely mis-pick a bundle, it left every one of those drawing light-mode colours inside a dark UI. Two tests pinned the old behaviour and are replaced, not loosened. One asserted the cookie "overrides everything"; it now asserts an explicit preference survives a contrary cookie, with a sibling covering the 'auto' case where the cookie is authoritative. The other asserted the switch to 'auto' expires the cookie, citing the old precedence as its reason; it now asserts the cached resolution is left alone. Both were protecting a defect, so changing them is a corrected contract, not an expectation bent to match output. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[MISC] pyproject: pin the repo's first ruff lint configuration Ruff was running fully unconfigured against this repository. A repo-root pyproject.toml now establishes an explicit configuration, pinning today's implicit rule set so a future ruff upgrade cannot silently change what is enforced. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_backend_theme: dedupe the active marker in the Appearance menu The Appearance systray menu's active Theme row and active Density row each drew two selected-markers: core's own `.dropdown-item.active::before` FontAwesome checkmark painted on top of this component's own right-aligned check icon, so the active choice showed a double mark instead of one. Core's marker is suppressed on both DropdownItem loops with core's own opt-out class `dropdown-item_active_noarrow`, leaving the component's own check as the single indicator. mail's action list already reuses that class for the identical situation, so this reuses an existing core lever instead of inventing new CSS. A tour-driven HttpCase asserts the rendered result - the active row's computed `::before` is 'none' and exactly one visible `.fa-check` remains inside it - in both the light and the dark colour scheme, so the contract is pinned on render output rather than on markup. Signed-off-by: David Tran <david.tran@tvtmarine.com>