Skip to content

Address review feedback for #5784 (heartbeat)#122

Closed
evnchn wants to merge 88 commits intoheartbeat-worker-keep-alivefrom
fix/heartbeat-worker-keep-alive
Closed

Address review feedback for #5784 (heartbeat)#122
evnchn wants to merge 88 commits intoheartbeat-worker-keep-alivefrom
fix/heartbeat-worker-keep-alive

Conversation

@evnchn
Copy link
Copy Markdown
Owner

@evnchn evnchn commented Apr 3, 2026

Summary

  • 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

Note: On Air compatibility (#3 from review) not verified — requires manual testing.

🤖 Generated with Claude Code

evnchn and others added 30 commits February 12, 2026 14:03
…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
falkoschindler and others added 24 commits March 18, 2026 15:29
### 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>
@evnchn evnchn force-pushed the fix/heartbeat-worker-keep-alive branch from 56fb16e to f61c128 Compare April 3, 2026 14:15
…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>
@evnchn evnchn force-pushed the fix/heartbeat-worker-keep-alive branch from f61c128 to 5896a9f Compare April 3, 2026 14:36
@evnchn
Copy link
Copy Markdown
Owner Author

evnchn commented Apr 3, 2026

Fix cherry-picked to heartbeat-worker-keep-alive branch (upstream PR zauberzeug#5784)

@evnchn evnchn closed this Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.