Address review feedback for #5784 (heartbeat)#122
Closed
evnchn wants to merge 88 commits intoheartbeat-worker-keep-alivefrom
Closed
Address review feedback for #5784 (heartbeat)#122evnchn wants to merge 88 commits intoheartbeat-worker-keep-alivefrom
evnchn wants to merge 88 commits intoheartbeat-worker-keep-alivefrom
Conversation
…berzeug#5770) ### Motivation Add a `copilot-setup-steps.yml` workflow so that the [GitHub Copilot coding agent](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/customize-the-agent-environment#preinstalling-tools-or-dependencies-in-copilots-environment) has Python, uv, project dependencies, and pre-commit hooks pre-installed in its ephemeral environment. This reduces exploration time and avoids build failures when Copilot works on issues or PRs. ### Implementation The workflow follows the canonical schema from the official GitHub documentation: - **Job name**: `copilot-setup-steps` (required for Copilot to recognize it) - **Triggers**: `workflow_dispatch`, `push`, and `pull_request` (path-filtered to itself for validation) - **Permissions**: Minimal `contents: read` - **Setup steps**: Mirrors the existing CI setup in `_check.yml` — checkout, setup-uv with caching, install Python 3.10, `uv sync`, and `uv run pre-commit install` ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
…ith() (zauberzeug#5768) ### Motivation Fixes zauberzeug#5480 When UI elements (e.g. `ui.button()`) are created at module level before `ui.run_with()`, NiceGUI's `Context.slot_stack` creates a pseudo "script client" with `_request=None`. This client is designed for `ui.run()` script mode, which properly handles it. However, `run_with()` never handled this case — leaving an orphaned request-less client in `Client.instances`. When `prune_user_storage()` runs every 10 seconds, it crashes accessing `client.request.session['id']` on this client. Supersedes zauberzeug#5742, which was closed because it only silenced the crash without addressing the root cause. ### Implementation Detect `core.script_mode` at the start of `run_with()`. If UI elements were created outside a page context: 1. Log a clear, actionable warning telling the user what happened and how to fix it 2. Delete the orphaned script client (removing it from `Client.instances`) 3. Reset `core.script_mode` to `False` This mirrors how `ui.run()` handles `core.script_mode` in `ui_run.py`, adapted for `run_with()` where module-level UI creation is not a valid pattern. ### Reproduction From zauberzeug#5480 (comment): ```python # myplugin.py from nicegui import ui class MyPlugin: def __init__(self) -> None: self.my_button = ui.button() ``` ```python # main.py import importlib from fastapi import FastAPI from nicegui import ui fastapi_app = FastAPI() module = importlib.import_module('myplugin') plugin_instance = module.MyPlugin() ui.run_with(fastapi_app, storage_secret='secret') ``` **Before**: `RuntimeError: Request is not set` every 10 seconds. **After**: Clear warning at startup, no crash. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
### Motivation Adds an example demonstrating how to control a single device using events and proper UI bindings. Addresses zauberzeug#5201 (reply in thread) This example shows users how to: - Wire hardware/IoT devices to NiceGUI - Use event-driven architecture for reactive UIs - Implement device logging and autonomous behavior - Use Pythonic property setters for device control ### Implementation The example features a `Lightbulb` class that: - Encapsulates device state (power, brightness) - Emits events for state changes (`power_changed`, `brightness_changed`, `log_message`) - Uses property setters for clean, Pythonic API (`lightbulb.power = True`) - Includes autonomous heartbeat logging when powered on using `app.timer()` - Logs all actions with timestamps to `ui.log` The UI subscribes to device events for reactive updates, demonstrating clean separation between device logic and presentation. This pattern is ideal for connecting real hardware devices, IoT equipment, and industrial controllers to NiceGUI. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). ### Human note: - First-gen UI was awful. I had it abide to "less is more" principle. - Forced it to do getter and setter. - Originally had like 5 properties. Cut it down to just 1. --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
### Motivation See discussion zauberzeug#5771: Calling `user.find(ui.number).type('42')` in user simulation tests raises an `AssertionError` because `type()` only supports `ui.input`, `ui.editor`, and `ui.codemirror`. There's no reason `ui.number` shouldn't work too. ### Implementation - Added `ui.number` as a supported element in `UserInteraction.type()` - For `ui.number`, the typed text is converted to `float` and set directly (no string concatenation, since that doesn't apply to numeric values) - Replaced the bare `assert` with an explicit `TypeError` for unsupported element types, giving a clear error message - Added `test_type_number` to verify typing and clearing on `ui.number` elements ### Limitation ~~Unlike text inputs, `type()` on `ui.number` does not simulate character-by-character typing. It converts the full text to a float in one step. Simulating intermediate keystroke states (e.g., `type('7.')` → NaN → `type('0')` → 7.0) would require modeling browser-level input behavior, which is out of scope for this change.~~ Each `type()` call must produce a valid float. Intermediate states that aren't valid floats (like `"."` or `"1e"`) can't be represented, and trailing dots get lost (e.g., `type("7.")` + `type("5")` yields `75.0` instead of `7.5` because `"7."` is parsed as `7`). ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] This is not a security issue. - [x] Pytests have been added. - [x] Documentation is not necessary.
### Motivation NiceGUI users need clear guidance on security best practices, especially regarding: - Which components can safely handle user input - When and how to validate/sanitize input - Common vulnerability patterns (XSS, path traversal, URL injection, CSS injection) - Secure coding practices specific to NiceGUI applications This documentation helps developers understand the shared responsibility model where NiceGUI provides secure defaults but developers must write secure code. ### Implementation Added a new "Security Best Practices" section to the documentation that covers: - **Security Model**: Explains the shared responsibility between framework and developers - **Common Sense Demo**: Shows safe practices like using `ast.literal_eval()` instead of `eval()` - **Component Selection**: Categorizes components by security risk level (secure by default vs. requires validation) - **URL Validation**: Demonstrates preventing `javascript:` URL injection attacks - **CSS Injection**: Shows how to validate CSS values to prevent data exfiltration - **Additional Resources**: Links to OWASP, security advisories, and key security principles The section includes interactive demos with both secure and vulnerable code examples, making it easy for developers to understand what to avoid. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the security advisory process. - [x] Pytests are not necessary. - [x] Documentation has been added. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ### Human notes **I made massive tweaks** to the output to be concise. I don't want the section to sound like a broken record! --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com> Co-authored-by: Falko Schindler <mail@falkoschindler.de>
…5780) ### Motivation Fixes zauberzeug#5750. When running NiceGUI from a local fork/clone with `reload=True` (the default), the first startup can trigger spurious reloads because uvicorn's watchfiles detects changes inside `.venv/Lib/site-packages/` (e.g. from lazy bytecode compilation or package extraction). The root cause is that uvicorn's `FileFilter` applies exclude patterns via `pathlib.Path.match()`, which only matches the **filename** (last path component). So the default `.*` exclude pattern matches files *named* with a leading dot (e.g. `.hidden.py`), but does **not** exclude files *inside* dot-prefixed directories like `.venv/`. Files like `.venv/Lib/site-packages/IPython/core/display.py` pass right through because `display.py` doesn't match `.*`. ### Implementation Appends `sys.prefix` to uvicorn's `reload_excludes` list. Uvicorn's `FileFilter` checks each exclude entry with `Path(e).is_dir()` — when it resolves to an actual directory, it adds it to an `exclude_dirs` list and skips all files within that directory tree. Using `sys.prefix` (rather than the `VIRTUAL_ENV` env var) works regardless of how the virtualenv was entered — activated, `uv run`, direct interpreter path, poetry, etc. It also covers the edge case of someone running `main.py` from within a bare-metal Python installation directory. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…5778) ### Motivation Quick win: the original `border-radius: 6px` inline style on the GitHub Sponsors iframe is broken, as the original content is not rounded. This replaces the inline styles with Tailwind utility classes and adds proper dark mode support. ### Implementation - Replaced `style="border: 0; border-radius: 6px;"` with Tailwind classes - `border-0` to suppress the default iframe border - `outline-[1px]` with `outline-offset-[-1px]` for a subtle border that matches GitHub's Primer design tokens (`#d1d9e0` light / `#3d444d` dark) - `rounded` for border radius ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
### Motivation When reopening the search dialog on nicegui.io, the previously entered text remains, requiring users to manually delete it before starting a new search. As suggested in zauberzeug#5744 (reply in thread), selecting the existing text (instead of clearing it) provides the best of both worlds: users can immediately type to replace the old query, or use arrow keys to navigate and edit it. ### Implementation - Store a reference to the search `ui.input` element as `self.input` - Add an `open_dialog()` method that selects the input text via `ui.run_javascript` before opening the dialog - Route both the keyboard shortcut handler and the search button through `open_dialog()` instead of `self.dialog.open()` directly ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
### Motivation
Closing a fullscreen `ui.table` triggers a scroll animation if `html {
scroll-behavior: smooth; }` is set. This is the same class of issue
fixed for `ui.dialog` and `ui.select` in PR zauberzeug#5050.
MRE:
```py
ui.add_css('html { scroll-behavior: smooth }')
ui.link('Go to bottom', '#bottom')
ui.link_target('bottom').classes('mt-[2000px]')
table = ui.table(rows=[{'name': 'Alice'}])
with table.add_slot('bottom'):
ui.button('Toggle fullscreen', on_click=table.toggle_fullscreen).props('flat')
```
### Implementation
Applies the same pattern as PR zauberzeug#5050: temporarily override
`scroll-behavior` to `auto` on the `<html>` element while the table is
in fullscreen mode.
One difference from the dialog/select fix: Quasar's fullscreen mixin
uses `setTimeout(() => el.scrollIntoView())` to restore the scroll
position after exiting fullscreen. Because the `@fullscreen` event fires
before that macrotask, we also use `setTimeout` to defer removing the
class, ensuring `scroll-behavior: auto` is still in effect when
`scrollIntoView()` executes.
### Progress
- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] This is not a security issue.
- [x] Pytests have been added.
- [x] Documentation is not necessary.
### Motivation Addresses zauberzeug#4758 (comment) Adds a new example demonstrating integration with the [reaktiv](https://github.com/buiapp/reaktiv) reactive state management library. This showcases an alternative approach to state management in NiceGUI using fine-grained reactive signals. ### Implementation The example is an order calculator with: - **Signals** for user inputs (price, quantity, tax rate) - **Computed** values for derived state (subtotal, tax, total) forming a diamond dependency graph - **Effects** to automatically update NiceGUI labels when any dependency changes Requires reaktiv >= 0.21.1 which fixes a diamond dependency graph bug ([buiapp/reaktiv#27](buiapp/reaktiv#27)). ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
### Motivation Relates to zauberzeug#5749. Supersedes zauberzeug#5757. `renderRecursively` creates new vnode objects and slot function closures on every render pass, which makes Vue treat every component as dirty — even when its data hasn't changed. This has always been the case, but became visible in 3.7 when `ui.html` gained an `updated()` hook that re-assigns `innerHTML`: any unrelated server-side update now triggers that hook and destroys client-side DOM modifications. PR zauberzeug#5757 addresses the symptom at the component level by skipping redundant `innerHTML` writes in `html.js`. This PR fixes the underlying inefficiency so that unchanged components are never re-rendered in the first place. ### Implementation Add a vnode cache (`Map<elementId, {vnode, propsContext}>`) to `renderRecursively`. When a server update arrives, the cache is invalidated for the changed elements **and their ancestors** (since ancestor vnodes embed child vnodes via slot closures). Unchanged elements return the same vnode reference, which Vue recognizes as identical and skips entirely — no prop diffing, no slot evaluation, no lifecycle hooks. This benefits all components, not just `ui.html`: any component with side effects in lifecycle hooks is protected from spurious re-renders, and the overall rendering workload is reduced for every update. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] This is not a security issue. - [x] New pytests are not necessary. - [x] Existing Pytests are passing. - [x] Add a pytest ensuring `$stable` is still there and `ui.html` isn't re-rendered unexpectedly. - [x] Documentation is not necessary. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Evan Chan <37951241+evnchn@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
) ### Motivation Live test screenshot of https://nicegui.io looks like: <img height="300" alt="image" src="https://github.com/user-attachments/assets/2c182de6-8c90-40e4-8188-8d4fa288fdad" /> while the rest of the pages are all normal. ### Implementation The implementation details of `Google Inspection Tool smartphone` remains intentionally sparse by Google, but it is likely that it simulates a really long screen to facilitate taking a long screenshot without scrolling-and-stitching. Assuming this is true, I set a `max-h-[200vw]` to limit the first chunk in our documentation to a 1:2 aspect ratio. As no mainstream phone broke the 2:1 aspect ratio bound, 99% users should not notice a difference. For the 1% (Foldable phone users on external screen), you will have a blue bar at the bottom, but I'd argue the visual balance is better after this fix anyways 🤷 <img height="300" alt="image" src="https://github.com/user-attachments/assets/2c2ec512-9fe6-41cd-b8fe-b97d747fa9c1" /> ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary).
### Motivation The devcontainer currently lacks GitHub CLI (`gh`) integration and pre-commit hook setup, making it harder for contributors to interact with GitHub (e.g., creating PRs, reviewing issues) and to catch linting/formatting issues before pushing. ### Implementation - **Dockerfile**: Added `gh` (GitHub CLI) to the installed packages. - **devcontainer.json**: - Added a persistent named volume (`gh-config`) mounted at `~/.config/gh` so that `gh auth` tokens survive container rebuilds. - Extended `postCreateCommand` to fix volume ownership, run `pre-commit install`, and prompt for `gh auth login` if not already authenticated. - **CONTRIBUTING.md**: Updated the devcontainer setup instructions to note the GitHub authentication prompt on first launch. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
…ield (zauberzeug#5732) ### Motivation Fixes race condition error introduced by zauberzeug@f1f7533 on https://nicegui.io where `emitEvent` is called before app initialization completes: ``` Uncaught TypeError: Cannot read properties of undefined (reading '$refs') at getElement (nicegui.js:111:22) at emitEvent (nicegui.js:155:3) at (index):308:51 ``` https://github.com/zauberzeug/nicegui/blob/2941a963ed4600690d45664d6eb19009a1e1324f/main.py#L74-L80 This occurs when `window.addEventListener('load', ...)` fires before `createApp` finishes, caused by async DOMPurify import delaying app initialization. ### Implementation - Store browser feature detection result in cookie on first load - Use cookie to conditionally render DOMPurify import server-side - Browsers with native `setHTML` skip import entirely → synchronous, immediate app initialization - Self-healing: if cookie assumption is wrong, clear and reload once ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the security advisory process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
### Motivation Supersedes zauberzeug#5793 with the simplified implementation suggested in [review](zauberzeug#5793 (review)). <img height="300" alt="image" src="https://github.com/user-attachments/assets/5aa42b6e-f45d-47e3-ac1e-47f761d37799" /> While may not have been relevant before the AI boom, I highly suspect Google uses VLM on the screenshot for SEO purposes. Otherwise the screenshot preview wouldn't be necessary in the first place. As such, having the demo show a spinner when the screenshot happens is a dealbreaker. ### Implementation Instead of introducing new abstractions (as in zauberzeug#5793), this PR takes the minimal approach suggested by @falkoschindler: 1. Track `first_demo_seen = False` in `render_content`. 2. Override `lazy` with `part.demo.lazy and first_demo_seen` — the first demo always gets `lazy=False`, subsequent demos respect their configured `lazy` value. This is a 3-line change in `website/documentation/rendering.py` with no new types, constants, or API surface. > PR opened by Claude Code on behalf of @evnchn, using the code originally written by GitHub Copilot in #84. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…erzeug#5775) ### Motivation When calling `set_value` on `ui.codemirror`, the entire document was replaced, which reset the cursor position and any active selection. This made it impossible to programmatically update part of the editor content (e.g. via a timer or external event) without disrupting the user who is actively typing. > **Note:** This does not enable true multi-user collaborative editing. Simultaneous edits from multiple clients still follow last-writer-wins semantics, which can cause lost keystrokes. Real-time collaboration would require CRDT or OT, which is out of scope here. ### Implementation **Minimal diff in `setEditorValue`** ([codemirror.js](nicegui/elements/codemirror/codemirror.js)): - Instead of replacing the full document, `setEditorValue` now finds the common prefix and suffix between the old and new text, then dispatches only the changed region to CodeMirror. - Cursors and selections outside the edited region are preserved automatically by CodeMirror's transaction system. - O(n) character scan, no new dependencies. Behavior is unchanged for equal documents (early return) and full replacements. **New "Preserving Cursor Position" documentation demo** ([codemirror_documentation.py](website/documentation/content/codemirror_documentation.py)): - A timer updates the first line every second while the user edits freely below, demonstrating that the cursor stays in place. **Pytest** ([test_codemirror.py](tests/test_codemirror.py)): - `test_set_value_preserves_cursor`: places the cursor after "Hello" in "Hello World", calls `set_value("Hello Earth")`, then types a comma — asserts the result is "Hello, Earth". ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary).
…auberzeug#5809) ### Motivation Website imports trigger demo code that registers a custom exception handler via `app.on_page_exception`. Without resetting it in `App.reset()`, this handler leaks between tests, blocking reliable pytesting of documentation features. Split out from zauberzeug#5767 per review feedback to isolate concerns. ### Implementation - Add `self._page_exception_handler = None` to `App.reset()` ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
### Motivation `Outbox.stop()` sets `_should_stop` but doesn't wake the loop when it is sleeping in `Event.wait(timeout=1.0)`. The loop only checks `_should_stop` at the top of the while loop, so after `stop()` is called the outbox loop lingers for up to 1 second until the `Event.wait` times out. This is a resource hygiene issue: deleted clients' outbox loops should stop promptly rather than lingering. The linger is concurrent (nothing awaits the outbox task after `stop()`), so it does not block the caller. But it leaves unnecessary background tasks in the event loop and extends the cleanup window that test harnesses or diagnostic tooling must account for. Closes zauberzeug#5804 ### Implementation `Outbox.stop()` now calls `self._set_enqueue_event()` after setting `_should_stop = True`. This is the same helper already used by `enqueue_update()`, `enqueue_delete()`, `enqueue_message()`, and `try_rewind()` to wake the loop — so the fix follows the existing pattern rather than introducing anything new. The loop wakes, sees `_should_stop` is true, and exits immediately. ### Progress - [X] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [X] The implementation is complete. - [X] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [X] Pytests have been added (or are not necessary). - [X] Documentation has been added (or is not necessary). --------- Co-authored-by: Brian Ballsun-Stanton <denubisx@noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com> Co-authored-by: Brian Ballsun-Stanton <denubis@noreply.github.com>
…collection (zauberzeug#5812) ### Motivation Effects in reaktiv must be assigned to variables, otherwise they get garbage collected and stop working. The previous example only worked due to a bug in reaktiv that has since been fixed. Updated the example to properly store Effect instances. This API behavior is documented in the reaktiv docs. References: zauberzeug#5783, zauberzeug#4758 ### Implementation We need just to assign Effects to variables. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary).
### Motivation The test `test_warning_if_response_takes_too_long` is flaky in CI. When the page's `response_timeout` fires and the client is deleted, the resulting incomplete page response causes Selenium's `get()` to hang until its internal script timeout (~30s), throwing a `TimeoutException` before the test's log assertion is ever reached. This has been observed failing on Python 3.10 and 3.14 in CI ([example run](https://github.com/zauberzeug/nicegui/actions/runs/22177922207/job/64131335858)). ### Implementation Replace `screen.open('/')` with a plain `httpx.get()` request. The test only verifies that a server-side warning is logged — it doesn't need a fully loaded browser page. Using `httpx` avoids Selenium entirely, eliminating the script timeout issue while keeping the same assertion. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] This is not a security issue. - [x] Pytests have been updated. - [x] Documentation is not necessary. --------- Co-authored-by: Evan Chan <37951241+evnchn@users.noreply.github.com>
…erzeug#5822) ### Motivation Fixes zauberzeug#5819. Updating echart data (e.g. appending points via a timer) always resets interactive state like dataZoom slider positions. This is because `this.chart.options` is not a valid ECharts API — it's always `undefined` — so the `notMerge` flag evaluates to `true` on every update, causing a full replacement of chart state instead of a merge. MRE: ```py chart = ui.echart({ 'xAxis': {}, 'yAxis': {}, 'dataZoom': [{'type': 'slider', 'yAxisIndex': 0}], 'series': [{'type': 'line', 'data': []}], }) ui.timer(1, lambda: chart.options['series'][0]['data'].append(len(chart.options['series'][0]['data']))) ``` ### Implementation Replace `this.chart.options?.series?.length` with `this.chart.getOption()?.series?.length` in `echart.js`. [`getOption()`](https://echarts.apache.org/en/api.html#echartsInstance.getOption) is the correct ECharts API to retrieve the current chart configuration. This way `notMerge` is only `true` when the number of series actually changes (requiring a clean replacement), and `false` for normal data updates (preserving zoom, slider positions, and other interactive state). ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] This is not a security issue. - [x] Pytests are not necessary (would be pretty hard). - [x] Documentation is not necessary.
### Motivation Fixes zauberzeug#5816. The vnode cache introduced in zauberzeug#5761 prevents Vue 3.5's dependency tracking from capturing all elements during partial re-renders. After a small update triggers a cached re-render, the render effect's dep list shrinks from hundreds of elements to ~40 (only those whose vnodes were invalidated). Subsequent changes to elements outside that set — such as `ui.sub_pages` content during client-side navigation — go undetected by Vue, leaving the page blank. Originally reported in zauberzeug#5794 (comment). ### Implementation Add a reactive `renderToggle` boolean to the Vue app's data. The render function subscribes to it (`void this.renderToggle`), and the update handler flips it after every update. This guarantees Vue always re-enters the render function when elements change, while the vnode cache still prevents unnecessary DOM work for unchanged elements. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
### Motivation Reverts the vnode cache from zauberzeug#5761, which broke `ui.log` auto-scroll (zauberzeug#5823) and potentially other elements relying on Vue's `updated()` lifecycle hook. Instead applies the component-level approach from zauberzeug#5757: guards `renderContent()` in `html.js`, `markdown.js`, and `interactive_image.js` to skip redundant `innerHTML` writes, preserving client-side DOM modifications (zauberzeug#5749). The vnode cache is the right long-term fix, but needs more work to handle elements that legitimately depend on `updated()` firing when slot content changes (like `ui.log`). For 3.8, the component-level guards are the safer path. ### Implementation **`nicegui.js`**: Reverted `vNodeCache`, `parentOf`, `invalidateVnodeCache()`, cache lookup/storage in `renderRecursively`, and the `$forceUpdate()` from zauberzeug#5821. **`html.js`, `markdown.js`, `interactive_image.js`**: Added a `previous*` guard in `renderContent()` that compares the current content against the previously rendered value and returns early if unchanged. These are the only three elements that gained destructive `innerHTML` side effects in `updated()` via f1f7533 (the sanitize feature). All other elements with `updated()` hooks are either idempotent or already have their own guards. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] This is not a security issue. - [x] New pytests are not necessary. - [x] Documentation is not necessary. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…erzeug#5806) (Ala Claude, the investigation logs are in the issue. This investigation was spurred when I kicked off a bunch of playwright e2e tests and they started failing randomly, inconsistently.) ### Motivation `page.py`'s `decorated()` creates two competing tasks via `asyncio.wait(return_when=FIRST_COMPLETED)`: the page coroutine (`task`) and a connection wait (`task_wait_for_connection`). The cancellation guard only fires on timeout — when the page coroutine completes first (the normal case for pages that don't call `await client.connected()`), `task_wait_for_connection` is never cancelled. The leaked task wraps `client._waiting_for_connection.wait()`. Since `_waiting_for_connection` is only `.set()` inside `connected()` (client.py:211), and `handle_handshake()` calls `.clear()` not `.set()` (client.py:298), the event is never set for pages that don't call `connected()`. The task persists in `background_tasks.running_tasks` until server shutdown. Each async page load that doesn't call `connected()` adds one such task. This is a task count leak, not a memory leak — each `Event.wait` coroutine frame is small and doesn't pin the `Client` (the Event doesn't back-reference its owner). But leaked tasks accumulate in `background_tasks.running_tasks` and `asyncio.all_tasks()` without bound over the lifetime of the server. Closes zauberzeug#5803 ### Implementation `asyncio.wait(return_when=FIRST_COMPLETED)` has four possible outcomes after returning: | Outcome | task.done() | task_wait.done() | Action | |---------|-------------|------------------|--------| | Page completes first | True | False | Cancel task_wait **(the fix)** | | Client connects first | False | True | Let task finish via callback | | Timeout | False | False | Cancel both, warn, delete | | Both complete | True | True | No cleanup needed | The existing `if not task_wait.done() and not task.done()` correctly handles timeout (outcome 3). That `and` is load-bearing — it distinguishes "client connected, page still loading" (outcome 2) from "timeout" (outcome 3). The fix adds `elif not task_wait_for_connection.done(): task_wait_for_connection.cancel()` to handle outcome 1. The `elif` makes the branches mutually exclusive: timeout block handles outcome 3, the elif handles outcome 1, outcomes 2 and 4 need no action. Cancelling `task_wait_for_connection` is safe: it wraps `asyncio.Event.wait()`, `CancelledError` is caught by `background_tasks._handle_exceptions`, and the done callback removes it from `running_tasks`. The underlying `_waiting_for_connection` event is unaffected — `handle_handshake()` can still clear/set it independently. ### Progress - [X] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [X] The implementation is complete. - [X] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [X] Pytests have been added (or are not necessary). - [X] Documentation has been added (or is not necessary). --------- Co-authored-by: Brian Ballsun-Stanton <denubisx@noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: evnchn <evanchan040511@gmail.com> Co-authored-by: Evan Chan <37951241+evnchn@users.noreply.github.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
…ug#5831) ### Motivation Fixes zauberzeug#5828. Applying Tailwind `bg-*` classes to `ui.log` results in "dirty" colors (e.g. `bg-white` shows as #F8FAFC instead of pure white) because a semi-transparent `background-color: rgba(127, 159, 191, 0.05)` on the inner `.q-scrollarea__container` blends on top of the user-specified background. ### Implementation Move the default `background-color` from `.nicegui-log .q-scrollarea__container` to `.nicegui-log` itself. This way, user-applied `bg-*` classes on the log element win via CSS specificity, cleanly overriding the default tint instead of being layered underneath it. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] This is not a security issue. - [x] Pytests are not necessary. - [x] Documentation is not necessary.
### Motivation <img width="2752" height="848" alt="image" src="https://github.com/user-attachments/assets/c0c42943-ca62-4ffd-8a13-d21727266b4f" /> NiceGUI's SPA is totally not known to Plausible, as such the analytics is quite messed up. ### Implementation This pull request updates the Plausible analytics script to a newer version that automatically handles Single-Page Application (SPA) tracking. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary).
* use json.dumps instead of string interpolation * add pytests * remove eval() fallback from runMethod() to prevent XSS * empty commit * wrap long line * add a link target for backward compatibility * mention arbitrary JavaScript conditionally
### Motivation Clicking a `ui.tab` element in the user simulation testing framework had no effect — the parent `ui.tabs` value was never updated, so `on_change` callbacks were not triggered. This made it impossible to test tab switching with the `User` fixture. Fixes zauberzeug#5885. ### Implementation Added a `ui.tab` case to `UserInteraction.click()`, following the same pattern used for `ui.radio` and `ui.select`: when a tab is clicked, its parent `ui.tabs` element's value is set to the tab's name, which triggers the `on_change` callback. A corresponding test (`test_switching_tabs`) verifies the fix. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] This is not a security issue. - [x] Pytest has been added. - [x] Documentation is not necessary. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clamp nicegui_chunk_size to prevent DoS via media streaming routes User-controlled nicegui_chunk_size query parameter was passed to read() without validation. A value of -1 causes read(-1) which reads entire files into memory, enabling memory exhaustion DoS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * remove blank line * handle malformed range headers --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
… wait-for-healthy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…berzeug#5893) ### Motivation Developers adopting Claude Code and Playwright MCP tooling generate local runtime and configuration files that should not be committed. Adding them to `.gitignore` keeps the repository clean for all contributors without requiring each person to configure `.git/info/exclude` individually. ### Implementation Add three entries to `.gitignore`: - `.playwright-mcp/` — runtime artifacts created by the Playwright MCP server - `.claude/settings.local.json` — user-specific Claude Code permission and preference settings (the project-level `.claude/` config may be tracked separately in the future) - `CLAUDE.local.md` — private per-user project instructions that override the shared `CLAUDE.md` ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### Motivation The code review guidelines in AGENTS.md already flag missing PR template usage as a BLOCKER (item #7), but there was no corresponding authoring instruction telling AI agents to actually use the template when creating PRs. This meant the first AI-generated PR would always fail that review check (as @falkoschindler flagged rightly in zauberzeug#5893 (comment)). ### Implementation Add a "Creating Pull Requests" section before the Code Review Guidelines that instructs AI agents to use the repository's `.github/PULL_REQUEST_TEMPLATE.md` with its **Motivation**, **Implementation**, and **Progress** sections. ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### Motivation <!-- What problem does this PR solve? Which new feature or improvement does it implement? --> <!-- Please provide relevant links to corresponding issues and feature requests. --> Several checklist items in the PR template have been confusing for contributors (both humans and AI agents). The "sentence completion" instruction for titles, the ambiguous "or are not necessary" phrasing, and the security checkbox were all sources of confusion. ### Implementation <!-- What is the concept behind the implementation? How does it work? --> <!-- Include any important technical decisions or trade-offs made. --> - **Title guidance**: Replaced the abstract "completes the sentence" instruction with concrete verb examples (Add, Fix, Update, Remove). - **Security checkbox**: Inverted to "This PR does not address a security issue" so the happy path is just checking the box, with guidance for the exception case. - **Pytests/Docs**: Changed to "added/updated or are not necessary" with a hidden comment asking the author to remove the option that does not apply — so reviewers can see which case it is. - **Breaking changes**: Added a new checkbox for API stability awareness. - **Draft PR hint**: Added a note to use draft PRs for incomplete implementations. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation is not necessary. - [x] No breaking changes to the public API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### Motivation <!-- What problem does this PR solve? Which new feature or improvement does it implement? --> <!-- Please provide relevant links to corresponding issues and feature requests. --> The sad face SVG on error pages (404, 500) used a hardcoded `stroke:#000` (black), making it barely visible on dark backgrounds. With `dark=True`, users could hardly see the illustration. MRE: ```py from nicegui import ui ui.run(dark=True) ``` → Black face on dark background when visiting "/". ### Implementation <!-- What is the concept behind the implementation? How does it work? --> <!-- Include any important technical decisions or trade-offs made. --> Changed `stroke:#000` to `stroke:currentColor` in `sad_face.svg` so the SVG inherits the page's text color — white on dark backgrounds, black on light backgrounds. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation is not necessary. - [x] No breaking changes to the public API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…5898) ### Motivation Calling `await element.initialized()` multiple times on `ui.leaflet`, `ui.scene`, or `ui.scene_view` causes a "Event listeners changed after initial definition. Re-rendering affected elements." warning and unnecessarily re-mounts the entire component. ```python @ui.page('/') async def page(): m = ui.leaflet() await m.initialized() await m.initialized() # warning + full component re-mount ``` ### Implementation Each call to `initialized()` created a new `asyncio.Event` and registered a new `'init'` event listener via `self.on()`. These listeners accumulated and triggered a client-side re-render path that deletes and re-creates the Vue component. Replace this with a shared `_initialized_event` (`asyncio.Event`) created once in `__init__` and set in `_handle_init()`. The `initialized()` method now simply awaits this shared event -- no new listeners per call, and it returns immediately if already initialized. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytest has been added. - [x] Documentation is not necessary. - [x] No breaking changes to the public API. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### Motivation <!-- What problem does this PR solve? Which new feature or improvement does it implement? --> <!-- Please provide relevant links to corresponding issues and feature requests. --> Snyk flagged security vulnerabilities in the `python:3.12-slim` base image used for the NiceGUI Docker image. Rather than just rebuilding (which only helps if upstream has patched the issues), we upgrade to Python 3.14 — the newest version supported by NiceGUI. ### Implementation <!-- What is the concept behind the implementation? How does it work? --> <!-- Include any important technical decisions or trade-offs made. --> - Changed the base image in `release.dockerfile` from `python:3.12-slim` to `python:3.14-slim`. - NiceGUI already declares `requires-python = ">=3.10,<4"` and has dependency markers for 3.14 in `pyproject.toml`, so no other changes are needed. **Trade-offs considered:** This changes the Python version that all users of the NiceGUI Docker image get. It could affect users who: - Install packages with C extensions compiled against 3.12 (would need rebuild for 3.14) - Reference `python3.12`-specific paths inside the container - Depend on stdlib behavior changed in 3.13/3.14 We believe these are rare edge cases. Users who specifically need 3.12 can build their own image. Shipping a Docker image with known vulnerabilities for months is a worse trade-off. An alternative would be to wait for NiceGUI 4.0 / Python 3.15 in November, but that means 8 more months with a flagged base image. **Reviewers: please veto if you think this bump should wait for a major release.** ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR addresses Snyk-flagged CVEs in the Debian base image, not in NiceGUI itself — no GHSA needed. - [x] Pytests are not necessary. - [x] Documentation is not necessary. - [x] No breaking changes to the public Python API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rzeug#5903) ### Motivation Setting `.error` on a `ValidationElement` (e.g. `ui.input`) was silently ignored when no `validation` function was configured. This is counter-intuitive — Quasar supports setting `error` and `error-message` independently of `rules`, and users expect `.error = "message"` to always show the error. Fixes zauberzeug#5895. ### Implementation In the `error` setter, the `error` prop was unconditionally set to `None` when `self.validation is None`, preventing the error from ever showing. The fix adds an additional check: the prop is only set to `None` when _both_ the error message and validation are `None`. When an error message is explicitly provided, the prop is set to `True` regardless of whether validation is configured. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests have been added. - [x] Documentation is not necessary. - [x] No breaking changes to the public API or migration steps are described above. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…zeug#5905) ### Motivation Fixes zauberzeug#5808 The "Initializer" subsection under "Reference" on documentation pages truncates multi-line `:param` descriptions. For example, `ui.markdown`'s `sanitize` parameter only shows "sanitization mode:" instead of the full description with all options. The demo section at the top of the page is not affected. ### Implementation The root cause is in `website/documentation/reference.py` line 25: a list comprehension filters lines by `:param`, discarding indented continuation lines that belong to the same parameter. The fix replaces the one-liner with a loop that appends indented continuation lines to the preceding `:param` entry: ```python lines: list[str] = [] for line in description.splitlines(): if ':param' in line: lines.append(line.replace(':param ', ':')) elif lines and line and line[0].isspace(): lines[-1] += '\n' + line ``` This affects every element with multi-line `:param` descriptions (e.g. `ui.markdown`, `ui.interactive_image`, `ui.chat_message`). ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation is not necessary. - [x] No breaking changes to the public API. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rs (zauberzeug#5902) ### Motivation Fixes zauberzeug#5885 (comment) Wrapping `ui.tab` elements in a `ui.row()` inside `ui.tabs()` is the recommended Quasar pattern for allowing tabs to wrap. However, `ui.tab` only checked its immediate parent for the `ui.tabs` reference, so intermediate containers like `ui.row` caused `tab.tabs` to point to the wrong element. This made `UserInteraction.click()` silently fail. ### Implementation - Use `self.ancestors()` to walk up the element tree and find the nearest `ui.tabs` ancestor, instead of only checking the immediate parent via `context.slot.parent`. This follows the same pattern used by `ui.menu_item` and `ui.fab_action`. - Emit a deprecation warning via `helpers.warn_once` if no `ui.tabs` ancestor is found, to prepare for enforcing this in NiceGUI 4.0. - Simplify the guard in `user_interaction.py` since `tab.tabs` is now either a `Tabs` instance or `None`. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytest has been added. - [x] Documentation is not necessary. - [x] No breaking changes to the public API or migration steps are described above. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### Motivation Fixes zauberzeug#5868 Several built-in elements use Tailwind utility classes for styling, which breaks when running with `tailwind=False`. This PR replaces all Tailwind classes in built-in elements with inline CSS or `nicegui.css` rules so they work regardless of the Tailwind setting. ### Implementation | Element | Tailwind classes | Replacement | | ---------------------------- | ---------------------------------------------------- | -------------------------------------------- | | `ui.code` markdown | `overflow-auto h-full` | `style('overflow: auto; height: 100%')` | | `ui.code` copy button | `absolute right-2 top-2 opacity-20 hover:opacity-80` | `.nicegui-code-copy` class in `nicegui.css` | | `ui.input` password toggle | `cursor-pointer` | `style('cursor: pointer')` | | `ui.date_input` button | `cursor-pointer` | `style('cursor: pointer')` | | `ui.time_input` button | `cursor-pointer` | `style('cursor: pointer')` | | `ui.color_input` button | `cursor-pointer` | `style('cursor: pointer')` | | `ui.linear_progress` label | `text-sm` | `style('font-size: 0.875rem')` | | `ui.circular_progress` label | `text-xs` | `style('font-size: 0.75rem')` | | `ui.table` header | `[&>*]:inline-block` | `.nicegui-table-header > *` in `nicegui.css` | For simple one-off properties like `cursor: pointer` and `font-size`, inline `.style()` is used directly. For the code copy button (which needs a `:hover` rule) and the table header (which uses a child selector), dedicated CSS classes were added to `nicegui.css`. Note: `absolute-center` and `text-white` on the progress labels are Quasar classes, not Tailwind — they remain unchanged. Test script covering all affected elements: ```python ui.code('from nicegui import ui\n\nui.run()') ui.input('Password', password_toggle_button=True) ui.date_input('Date') ui.time_input('Time') ui.color_input('Color') ui.linear_progress(0.5) ui.circular_progress(0.5) with ui.table(rows=[{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]).add_slot('header-cell-age'): with ui.table.header('age'): ui.label().props(':innerHTML=props.col.label') ``` ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation is not necessary. - [x] No breaking changes to the public API. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### Motivation Fixes zauberzeug#4786 `client.ip` always returns `127.0.0.1` when accessed via On Air because the relay forwards requests locally via ASGI transport. The same issue affects any deployment behind a reverse proxy (nginx, Caddy, Cloudflare, etc.) — the proxy's IP is reported instead of the real client IP. ### Implementation `client.ip` now checks the `X-Forwarded-For` header before falling back to `request.client.host`. This is the standard header set by reverse proxies to pass through the original client IP. When multiple proxies are chained, the leftmost entry (the original client) is returned. The corresponding On Air relay change (setting `X-Forwarded-For` on forwarded requests) will be deployed separately. **Re IP spoofing concerns (raised in zauberzeug#4786):** Unlike the [Next.js CVE-2025-29927](https://nextjs.org/blog/cve-2025-29927) where a client-set header was trusted for _authorization_, `client.ip` is purely informational — NiceGUI does not use it for access control. Additionally, in the On Air case the relay _overwrites_ (not appends to) any client-sent `X-Forwarded-For`, so a browser cannot inject a fake IP. For non-On Air deployments behind a reverse proxy, the proxy similarly controls the header. This is the same approach taken by Django, Flask, and Express. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation is not necessary. - [x] No breaking changes to the public API. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#5911) ### Motivation The "pip install nicegui" button on the hero section shows a "Copied!" notification but doesn't actually copy the text to the clipboard. The `navigator.clipboard.writeText()` logic was lost during the website redesign in zauberzeug#5910. Spotted during code review: zauberzeug#5910 (comment) ### Implementation Add `ui.clipboard.write('pip install nicegui')` alongside the existing `ui.notify()` call, matching the pattern already used in `code_window()` in `website/documentation/windows.py`. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation is not necessary. - [x] No breaking changes to the public API. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
### Motivation
Calling `sub_pages_router.refresh()` from within a parent element (e.g.
a `ui.card()`) raises `RuntimeError('The parent element this slot
belongs to has been deleted.')`.
Fixes zauberzeug#5912.
### Implementation
Two issues caused the error:
1. `_handle_open()` in `SubPagesRouter` accessed `context.client` after
`_show()` had already cleared elements -- including the parent element
on the slot stack. Fixed by saving a `client` reference before clearing,
matching the existing pattern in `_handle_navigate()`.
2. `_scroll_to_top()` and `_scroll_to_fragment()` in `SubPages` used the
module-level `run_javascript()`, which resolves the client via
`context.client` and the slot stack. When the outer `SubPages._show()`
deletes nested elements and a nested `SubPages` then tries to scroll,
the slot parent is gone. Fixed by using `self.client.run_javascript()`
instead, which accesses the client directly on the element.
### Progress
- [x] The PR title is a short phrase starting with a verb like "Add
...", "Fix ...", "Update ...", "Remove ...", etc.
- [x] The implementation is complete.
- [x] This PR does not address a security issue.
- [x] Pytest has been extended.
- [x] Documentation is not necessary.
- [x] No breaking changes to the public API.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### Motivation The website search dialog only supports mouse interaction. Users who prefer keyboard navigation (common for developer tools) have to reach for the mouse to browse and select results after typing a query. ### Implementation Add keyboard navigation to the search dialog in `website/search.py`: - **Arrow Down / Arrow Up** on the search input moves the selection highlight through the results list, with `prevent` to avoid cursor movement in the input field. - **Enter** navigates to the currently highlighted result via `ui.navigate.to()` and closes the dialog. - The **first result is selected by default** so pressing Enter immediately navigates to the top match. - A subtle `bg-gray-500/7` highlight (semi-transparent, works in both light and dark mode) indicates the active result, with `scrollIntoView` keeping it visible. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation is not necessary. - [x] No breaking changes to the public API. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: evnchn <evanchan040511@gmail.com>
) ### Motivation `nicegui/helpers.py` had grown into a grab-bag of unrelated utilities (async introspection, network checks, file hashing, string conversion, element validation, logging). This makes it a frequent source of import cycles — notably blocking PR zauberzeug#5879 from cleanly importing `AwaitableResponse` in `should_await()`. Splitting it into focused submodules allows other modules to import only what they need, breaking cycle-prone dependency chains. ### Implementation Converted `helpers.py` into a `helpers/` package with submodules: | Module | Contents | | -------------- | ------------------------------------------------- | | `__init__.py` | Re-exports everything for backward compatibility | | `functions.py` | `is_coroutine_function`, `expects_arguments` | | `network.py` | `is_port_open`, `schedule_browser` | | `files.py` | `is_file`, `hash_file_path` | | `strings.py` | `kebab_to_camel_case`, `event_type_to_camel_case` | | `elements.py` | `require_top_level_layout` | | `warnings.py` | `warn_once` | All existing `from nicegui.helpers import ...` and `from .. import helpers` imports continue to work unchanged via re-exports in `__init__.py`. `is_pytest()` and `is_user_simulation()` remain in `helpers/__init__.py` as before. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary (pure refactor, no behavior change). - [x] Documentation is not necessary. - [x] No breaking changes to the public API. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ort (zauberzeug#5909) ### Motivation Since zauberzeug#5351 switched to ES modules, native mode no longer works with Qt5's WebEngine (Chromium ~87), which lacks import map support. Users see a blank window with cryptic JS errors and no indication of what went wrong. Closes zauberzeug#5907 ### Implementation - Added a runtime check in `native_mode.py` that hooks into the pywebview `loaded` event and inspects the Chrome version from the user agent. If the version is below 89 (the minimum for import map support), a clear error is logged telling users to upgrade to Qt6. - Added a note to the native mode documentation about the ES module / Chrome 89+ requirement. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation has been updated. - [x] No breaking changes to the public API. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: evnchn <evanchan040511@gmail.com>
56fb16e to
f61c128
Compare
…elative URL, raise interval floor
- Heartbeat reschedules delete tasks instead of permanently canceling (prevents client leak)
- Add pagehide listener to stop/terminate worker on navigation
- Use relative heartbeat URL (works behind reverse proxies)
- Raise minimum heartbeat interval from 0.5s to 2s
- Replace silent .catch(() => {}) with console.debug
- Reduce test sleep times (15s -> 5s)
- Replace counter.__setitem__ hack with simple list+helper
- Add comment explaining why heartbeatWorker is on window (pagehide needs it)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
f61c128 to
5896a9f
Compare
Owner
Author
|
Fix cherry-picked to heartbeat-worker-keep-alive branch (upstream PR zauberzeug#5784) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pagehidelistener to stop/terminate worker on navigation.catch(() => {})withconsole.debugcounter.__setitem__hack with simple list+helperheartbeatWorkeris onwindowNote: On Air compatibility (#3 from review) not verified — requires manual testing.
🤖 Generated with Claude Code