# Configuration & Deployment | NiceGUI

[Button: icon:menu]

[](/)

[Installation](/#installation)

[Features](/#features)

[Demos](/#demos)

[Documentation](/documentation)

[Examples](/examples)

[Why?](/#why)

[Button]

[Button]

[](https://github.com/zauberzeug/nicegui/)

[Button: icon:more_vert]

# Configuration & Deployment

## URLs

You can access the list of all URLs on which the NiceGUI app is available via `app.urls`.
The URLs are not available in `app.on_startup` because the server is not yet running.
Instead, you can access them in a page function or register a callback with `app.urls.on_change`.

main.py

[Button]

````python
from nicegui import app, ui

@ui.page('/')
def index():
    for url in app.urls:
        ui.link(url, target=url)

ui.run()
````

## [ui.run](/documentation/run)

You can call `ui.run()` with optional arguments.
Most of them only apply after stopping and fully restarting the app and do not apply with auto-reloading.

:root: root page function (*added in version 3.0.0*)
:host: start server with this host (defaults to `'127.0.0.1` in native mode, otherwise `'0.0.0.0'`)
:port: use this port (default: 8080 in normal mode, and an automatically determined open port in native mode)
:title: page title (default: `'NiceGUI'`, can be overwritten per page)
:viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
:favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers).
    In Windows native mode, a local `.ico` file path is also applied as the native window icon.
:dark: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
:language: language for Quasar elements (default: `'en-US'`)
:binding_refresh_interval: interval for updating active links (default: 0.1 seconds, bigger is more CPU friendly, *since version 3.4.0*: can be ``None`` to disable update loop)
:reconnect_timeout: maximum time the server waits for the browser to reconnect (default: 3.0 seconds)
:message_history_length: maximum number of messages that will be stored and resent after a connection interruption (default: 1000, use 0 to disable, *added in version 2.9.0*)
:cache_control_directives: cache control directives for internal static files (default: `'public, max-age=31536000, immutable, stale-while-revalidate=31536000'`)
:gzip_middleware_factory: GZipMiddleware factory function (e.g. ``lambda app: GZipMiddleware(app, minimum_size=500, compresslevel=9)``, defaults to Starlette's ``GZipMiddleware``, can be ``None`` to disable, *added in version 3.5.0*)
:fastapi_docs: enable FastAPI's automatic documentation with Swagger UI, ReDoc, and OpenAPI JSON (bool or dictionary as described `here <https://fastapi.tiangolo.com/tutorial/metadata/>`_, default: `False`, *updated in version 2.9.0*)
:show: automatically open the UI in a browser tab (default: `True`, opens "/", *since version 3.6.0*: you can pass a specific path like "/path/to/page")
:on_air: tech preview: `allows temporary remote access <https://nicegui.io/documentation/section_configuration_deployment#nicegui_on_air>`_ if set to `True` (default: disabled)
:native: open the UI in a native window of size 800x600 (default: `False`, deactivates `show`, automatically finds an open port)
:window_size: open the UI in a native window with the provided size (e.g. `(1024, 786)`, default: `None`, also activates `native`)
:fullscreen: open the UI in a fullscreen window (default: `False`, also activates `native`)
:frameless: open the UI in a frameless window (default: `False`, also activates `native`)
:reload: automatically reload the UI on file changes (default: `True`)
:uvicorn_logging_level: logging level for uvicorn server (default: `'warning'`)
:uvicorn_reload_dirs: string with comma-separated list for directories to be monitored (default is current working directory only)
:uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'*.py'`)
:uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
:tailwind: whether to use Tailwind CSS (experimental, default: `True`)
:unocss: use UnoCSS with the specified preset instead of Tailwind CSS (default: ``None``, options: "mini", "wind3", "wind4", *added in version 3.7.0*)
:prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
:endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
:storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.user and ui.storage.browser)
:session_middleware_kwargs: additional keyword arguments passed to SessionMiddleware that creates the session cookies used for browser-based storage
:show_welcome_message: whether to show the welcome message (default: `True`)
:markdown: whether to serve a Markdown representation when a client sends ``Accept: text/markdown``
    (experimental, default: `False`, can be overwritten per page, *added in version 3.11.0*)
:kwargs: additional keyword arguments are passed to `uvicorn.run`

main.py

[Button]

````python
from nicegui import ui

ui.label('page with custom title')

ui.run(title='My App')
````

[See more →](/documentation/run)

## Native Mode

You can enable native mode for NiceGUI by specifying `native=True` in the `ui.run` function.
To customize the initial window size and display mode, use the `window_size` and `fullscreen` parameters respectively.
Additionally, you can provide extra keyword arguments via `app.native.window_args` and `app.native.start_args`.
Pick any parameter as it is defined by the internally used [pywebview module](https://pywebview.flowrl.com/api)
for the `webview.create_window` and `webview.start` functions.
Note that these keyword arguments will take precedence over the parameters defined in `ui.run`.

Additionally, you can change `webview.settings` via `app.native.settings`.

In native mode the `app.native.main_window` object allows you to access the underlying window.
It is an async version of [`Window` from pywebview](https://pywebview.flowrl.com/api/#webview-window).

Native mode requires a browser engine with ES module and import map support (Chrome 89+).
On Linux, ensure you have a modern browser engine — e.g. an up-to-date WebKitGTK or Qt-based backend.

On Windows, native mode requires the .NET Framework to be installed,
as pywebview uses it for the EdgeChromium backend.
This is typically pre-installed on standard Windows installations,
but may be missing on minimal or freshly installed systems.

On Windows, a file-path `favicon` is also used as the native window icon (taskbar, title bar).
The `.ico` format is required.

main.py

[Button]

````python
from nicegui import app, ui

app.native.window_args['resizable'] = False
app.native.start_args['debug'] = True
app.native.settings['ALLOW_DOWNLOADS'] = True

ui.label('app running in native mode')
ui.button('enlarge', on_click=lambda: app.native.main_window.resize(1000, 700))

ui.run(native=True, window_size=(400, 300), fullscreen=False)
````

## Native Window Events

In native mode you can react to window lifecycle events using `app.native.on`.
Handlers can be sync or async and optionally accept a `NativeEventArguments` parameter.
Supported events: "shown", "loaded", "minimized", "maximized", "restored", "resized", "moved", "closed", "drop".
The "resized" event provides `width` and `height` in `e.args`, "moved" provides `x` and `y`,
and "drop" provides `files` with a list of filesystem paths.

main.py

[Button]

````python
from nicegui import app, ui

ui.label('Try this demo in native mode to see the events in action!')

app.native.on('minimized', lambda: print('Window minimized'))
app.native.on('resized', lambda e: print(f'{e.args["width"]}x{e.args["height"]}'))
app.native.on('drop', lambda e: print(f'Dropped files: {e.args["files"]}'))

ui.run(native=True)
````


    Note that the native app is run in a separate
    [process](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process).
    Therefore any configuration changes from code run under a
    [main guard](https://docs.python.org/3/library/__main__.html#idiomatic-usage) is ignored by the native app.
    The following examples show the difference between a working and a non-working configuration.

    For packaged apps (nicegui-pack, PyInstaller, etc.), also see the "Packaging with Native Mode" section below
    regarding the correct placement of `freeze_support()`.


good_example.py

[Button]

````python
from nicegui import app, ui

app.native.window_args['resizable'] = False  # works

if __name__ == '__main__':
    ui.run(native=True, reload=False)

````

bad_example.py

[Button]

````python
from nicegui import app, ui

if __name__ == '__main__':
    app.native.window_args['resizable'] = False  # ignored

    ui.run(native=True, reload=False)

````


    If webview has trouble finding required libraries, you may get an error relating to "WebView2Loader.dll".
    To work around this issue, try moving the DLL file up a directory, e.g.:

    * from `.venv/Lib/site-packages/webview/lib/x64/WebView2Loader.dll`
    * to `.venv/Lib/site-packages/webview/lib/WebView2Loader.dll`


## Environment Variables

You can set the following environment variables to configure NiceGUI:

- `MATPLOTLIB` (default: true) can be set to `false` to avoid the potentially costly import of Matplotlib.
    This will make `ui.pyplot` and `ui.line_plot` unavailable.
- `NICEGUI_STORAGE_PATH` (default: local ".nicegui") can be set to change the location of the storage files.
- `MARKDOWN_CONTENT_CACHE_SIZE` (default: 1000): The maximum number of Markdown content snippets that are cached in memory.
- `RST_CONTENT_CACHE_SIZE` (default: 1000): The maximum number of ReStructuredText content snippets that are cached in memory.
- `NICEGUI_REDIS_URL` (default: None, means local file storage): The URL of the Redis server to use for shared persistent storage.
- `NICEGUI_REDIS_KEY_PREFIX` (default: "nicegui:"): The prefix for Redis keys.

main.py

[Button]

````python
from nicegui import ui
from nicegui.elements import markdown

ui.label(f'Markdown content cache size is {markdown.prepare_content.cache_info().maxsize}')

ui.run()
````

## Background Tasks

`background_tasks.create()` allows you to run an async function in the background and return a task object.
By default the task will be automatically cancelled during shutdown.
You can prevent this by using the `@background_tasks.await_on_shutdown` decorator (added in version 2.16.0).
This is useful for tasks that need to be completed even when the app is shutting down.

main.py

[Button]

````python
import aiofiles
import asyncio
from nicegui import background_tasks, ui

results = {'answer': '?'}

async def compute() -> None:
    await asyncio.sleep(1)
    results['answer'] = 42

@background_tasks.await_on_shutdown
async def backup() -> None:
    await asyncio.sleep(1)
    async with aiofiles.open('backup.json', 'w') as f:
        await f.write(f'{results["answer"]}')
    print('backup.json written', flush=True)

ui.label().bind_text_from(results, 'answer', lambda x: f'answer: {x}')
ui.button('Compute', on_click=lambda: background_tasks.create(compute()))
ui.button('Backup', on_click=lambda: background_tasks.create(backup()))

ui.run()
````

## Custom Vue Components


    You can create custom components by subclassing `ui.element` and implementing a corresponding Vue component.
    The ["Custom Vue components" example](https://github.com/zauberzeug/nicegui/tree/main/examples/custom_vue_component)
    demonstrates how to create a custom counter component which emits events and receives updates from the server.

    The ["Signature pad" example](https://github.com/zauberzeug/nicegui/blob/main/examples/signature_pad) and
    the ["Node module integration" example](https://github.com/zauberzeug/nicegui/blob/main/examples/node_module_integration)
    demonstrate how to bundle a custom Vue component with its dependencies defined in a `package.json` file.
    In Python we can use the `esm` parameter when subclassing `ui.element`
    to specify the ESM module name and the path to the bundled component.
    This adds the ESM module to the import map of the page and makes it available in the Vue component.


## Server Hosting


    To deploy your NiceGUI app on a server, you will need to execute your `main.py` (or whichever file contains your `ui.run(...)`) on your cloud infrastructure.
    You can, for example, just install the [NiceGUI python package via pip](https://pypi.org/project/nicegui/) and use systemd or similar service to start the main script.
    In most cases, you will set the port to 80 (or 443 if you want to use HTTPS) with the `ui.run` command to make it easily accessible from the outside.

    A convenient alternative is the use of our [pre-built multi-arch Docker image](https://hub.docker.com/r/zauberzeug/nicegui) which contains all necessary dependencies.
    With this command you can launch the script `main.py` in the current directory on the public port 80:


bash

[Button]

````console
docker run -it --restart always \
    -p 80:8080 \
    -e PUID=$(id -u) \
    -e PGID=$(id -g) \
    -v $(pwd)/:/app/ \
    zauberzeug/nicegui:latest

````


    The demo assumes `main.py` uses the port 8080 in the `ui.run` command (which is the default).
    The `-d` tells docker to run in background and `--restart always` makes sure the container is restarted if the app crashes or the server reboots.
    Of course this can also be written in a Docker compose file:


docker-compose.yml

[Button]

````yaml
app:
    image: zauberzeug/nicegui:latest
    restart: always
    ports:
        - 80:8080
    environment:
        - PUID=1000 # change this to your user id
        - PGID=1000 # change this to your group id
    volumes:
        - ./:/app/

````


    There are other handy features in the Docker image like non-root user execution and signal pass-through.
    For more details we recommend to have a look at our [Docker example](https://github.com/zauberzeug/nicegui/tree/main/examples/docker_image).

    To serve your application with [HTTPS](https://fastapi.tiangolo.com/deployment/https/) encryption, you can provide SSL certificates in multiple ways.
    For instance, you can directly provide your certificates to [Uvicorn](https://www.uvicorn.org/), which NiceGUI is based on, by passing the
    relevant [options](https://www.uvicorn.org/#command-line-options) to `ui.run()`.
    If both a certificate and key file are provided, the application will automatically be served over HTTPS:


main.py

[Button]

````python
from nicegui import ui

ui.run(
    port=443,
    ssl_certfile="<path_to_certfile>",
    ssl_keyfile="<path_to_keyfile>",
)

````


    In production we also like using reverse proxies like [Traefik](https://doc.traefik.io/traefik/) or [NGINX](https://www.nginx.com/) to handle these details for us.
    See our development [docker-compose.yml](https://github.com/zauberzeug/nicegui/blob/main/docker-compose.yml) as an example based on traefik or
    [this example nginx.conf file](https://github.com/zauberzeug/nicegui/blob/main/examples/nginx_https/nginx.conf) showing how NGINX can be used to handle the SSL certificates and
    reverse proxy to your NiceGUI app.

    You may also have a look at [our demo for using a custom FastAPI app](https://github.com/zauberzeug/nicegui/tree/main/examples/fastapi).
    This will allow you to do very flexible deployments as described in the [FastAPI documentation](https://fastapi.tiangolo.com/deployment/).
    Note that there are additional steps required to allow multiple workers.


## Package for Installation


    NiceGUI apps can also be bundled into an executable with `nicegui-pack` which is based on [PyInstaller](https://www.pyinstaller.org/).
    This allows you to distribute your app as a single file that can be executed on any computer.

    Just make sure

    - to call `ui.run` with `reload=False` in your main script to disable the auto-reload feature, and
    - to pass a `root` page function to `ui.run` or define at least one decorated `@page` function.

    Running the `nicegui-pack` command below will create an executable `myapp` in the `dist` folder:


main.py

[Button]

````python
from nicegui import native, ui

def root():
    ui.label('Hello from PyInstaller')

ui.run(root, reload=False, port=native.find_open_port())

````

bash

[Button]

````console
nicegui-pack --onefile --name "myapp" main.py

````


    **Packaging Tips:**

    - When building a PyInstaller app, your main script can use a native window (rather than a browser window) by
    using `ui.run(reload=False, native=True)`.
    The `native` parameter can be `True` or `False` depending on whether you want a native window or to launch a
    page in the user's browser - either will work in the PyInstaller generated app.

    - Specifying `--windowed` to `nicegui-pack` will prevent a terminal console from appearing.
    However you should only use this option if you have also specified `native=True` in your `ui.run` command.
    Without a terminal console the user won't be able to exit the app by pressing Ctrl-C.
    With the `native=True` option, the app will automatically close when the window is closed, as expected.

    - Specifying `--windowed` to `nicegui-pack` will create an `.app` file on Mac which may be more convenient to distribute.
    When you double-click the app to run it, it will not show any console output.
    You can also run the app from the command line with `./myapp.app/Contents/MacOS/myapp` to see the console output.

    - Specifying `--onefile` to `nicegui-pack` will create a single executable file.
    Whilst convenient for distribution, it will be slower to start up.
    This is not NiceGUI's fault but just the way Pyinstaller zips things into a single file, then unzips everything
    into a temporary directory before running.
    You can mitigate this by removing `--onefile` from the `nicegui-pack` command,
    and zip up the generated `dist` directory yourself, distribute it,
    and your end users can unzip once and be good to go,
    without the constant expansion of files due to the `--onefile` flag.

    - Specifying `--onedir` to `nicegui-pack` will create an executable with all supporting files in a directory.
    This starts faster than "--onefile" because it skips the unpacking step.
    For distribution, package the directory into an archive file (e.g., .zip or .7z).

    - Specifying `--clean` to `nicegui-pack` will clean the PyInstaller cache (in `./build` folder) and remove temporary files before building.

    - Specifying `--noconfirm` to `nicegui-pack` will replace the output directory (`./dist/SPECNAME`) without asking for confirmation.

    - Summary of user experience for different options:

        | `nicegui-pack`           | `ui.run(...)`  | Explanation |
        | :---                     | :---           | :---        |
        | `onefile`                | `native=False` | Single executable generated in `dist/`, runs in browser |
        | `onefile`                | `native=True`  | Single executable generated in `dist/`, runs in popup window |
        | `onefile` and `windowed` | `native=True`  | Single executable generated in `dist/` (on Mac a proper `dist/myapp.app` generated incl. icon), runs in popup window, no console appears |
        | `onefile` and `windowed` | `native=False` | Avoid (no way to exit the app) |
        | Specify neither          |                | A `dist/myapp` directory created which can be zipped manually and distributed; run with `dist/myapp/myapp` |

    - If you are using a Python virtual environment, ensure you `pip install pyinstaller` within your virtual environment
    so that the correct PyInstaller is used, or you may get broken apps due to the wrong version of PyInstaller being picked up.
    That is why the `nicegui-pack` invokes PyInstaller using `python -m PyInstaller` rather than just `pyinstaller`.


bash

[Button]

````console
python -m venv venv
source venv/bin/activate
pip install nicegui
pip install pyinstaller

````


    Note:
    If you're getting an error "TypeError: a bytes-like object is required, not 'str'", try adding the following lines to the top of your `main.py` file:
    ```py
    import sys
    sys.stdout = open('logs.txt', 'w')
    ```
    See <https://github.com/zauberzeug/nicegui/issues/681> for more information.


## Packaging with Nuitka


    NiceGUI apps can also be bundled with [Nuitka](https://nuitka.net/), which compiles Python to C.
    Compared to PyInstaller, builds take longer but the resulting binaries are harder to decompile.

    Two flags are required because NiceGUI uses [PEP 562](https://peps.python.org/pep-0562/) lazy imports
    that Nuitka's static analyzer cannot follow on its own:

    - `--include-package=nicegui` bundles every submodule reachable through `from nicegui import ui`.
    - `--include-package-data=nicegui` bundles data files (templates, libraries, ESM bundles for elements).

    The same `ui.run` rules as for PyInstaller apply:
    call it with `reload=False` and provide a `root` page or at least one `@page` function.


main.py

[Button]

````python
from nicegui import native, ui

def root():
    ui.label('Hello from Nuitka')

ui.run(root, reload=False, port=native.find_open_port())

````

bash

[Button]

````console
python -m nuitka \
    --onefile \
    --include-package=nicegui \
    --include-package-data=nicegui \
    main.py

````


    **Tips:**

    - Use `--standalone` for a `main.dist/` directory that starts faster than `--onefile`,
    which unpacks itself into a temporary directory on every launch.

    - For optional packages your app uses (e.g. `pyecharts` for `ui.echart.from_pyecharts`,
    or any other third-party package shipping templates or data files),
    add matching `--include-package=<name>` and `--include-package-data=<name>` flags.

    - Native mode (`ui.run(reload=False, native=True)`) works the same as with PyInstaller.
    Platform-specific flags include
    `--macos-create-app-bundle` (Mac),
    `--windows-disable-console` (Windows), and
    `--linux-onefile-icon=<path>` (Linux).

    - First builds are slow because Nuitka compiles the entire dependency graph; subsequent builds reuse Nuitka's cache.
    Add `--show-progress` to monitor long builds.



    **Packaging with Native Mode**

    When packaging your app (using nicegui-pack, PyInstaller, py2exe, etc.),
    you need to call `freeze_support()` to prevent new processes from being spawned in an endless loop.
    It should be called as the first statement inside the main guard.

    If you use `app.native` settings, they must be defined **outside** the main guard
    so they are applied before `freeze_support()` intercepts the subprocess:

    ```python
    from multiprocessing import freeze_support
    from nicegui import app, ui

    app.native.window_args['transparent'] = True  # outside main guard

    # any other code (page functions, etc.)

    if __name__ == '__main__':
        freeze_support()  # first statement in main guard
        ui.run(native=True, reload=False)
    ```



## Documentation Index


    NiceGUI serves its entire documentation as machine-readable JSON endpoints.
    Each index is a JSON array of objects with these fields:

    | Field     | Type    | Description                                                                                 |
    | --------- | ------- | ------------------------------------------------------------------------------------------- |
    | `title`   | string  | Section heading, e.g. "Button: Click Handler" or "Example: Chat App"                        |
    | `content` | string  | Description or search text (Markdown or reStructuredText)                                   |
    | `format`  | string  | Content format: "md" or "rst"                                                              |
    | `url`     | string  | Doc page path or GitHub example link                                                        |
    | `demo`    | string? | Complete Python demo code, or "" if none (sitewide index only; key absent in other indices) |



        **Available indices:**

        | Endpoint | Entries | Tokens | Includes code | Use case |
        | -------- | ------- | ------ | ------------- | -------- |
        | [`/static/sitewide_index.json`](https://nicegui.io/static/sitewide_index.json) | 779 | ~152k | Yes | RAG, AI tooling, full context                                                    |
        | [`/static/search_index.json`](https://nicegui.io/static/search_index.json)     | 838   | ~105k   | No  | Powers the on-site doc search; includes GitHub examples                          |
        | [`/static/examples_index.json`](https://nicegui.io/static/examples_index.json) | 59 | ~3k | No  | [GitHub examples](https://github.com/zauberzeug/nicegui/tree/main/examples) only |
    


    We welcome contributions for MCP servers, AI agent skills, and enhanced RAG implementations —
    see the [contributing guide](https://github.com/zauberzeug/nicegui/blob/main/CONTRIBUTING.md).


## NiceGUI On Air


    By using `ui.run(on_air=True)` you can share your local app with others over the internet 🧞.

    When accessing the on-air URL, all libraries (like Vue, Quasar, ...) are loaded from our CDN.
    Thereby only the raw content and events need to be transmitted by your local app.
    This makes it blazing fast even if your app only has a poor internet connection (e.g. a mobile robot in the field).

    By setting `on_air=True` you will get a random URL which is valid for 1 hour.
    If you sign-up at <https://on-air.nicegui.io>, you can setup an organization and device name to get a fixed URL:
    `https://on-air.nicegui.io/<my-org>/<my_device_name>`.
    The device is then identified by a unique, private token which you can use instead of a boolean flag: `ui.run(on_air='<your token>')`.
    If you [sponsor us](https://github.com/sponsors/zauberzeug),
    we will enable multi-device management and provide built-in passphrase protection for each device.

    Currently On Air is available as a tech preview and can be used free of charge.
    We will gradually improve stability and extend the service with usage statistics, remote terminal access and more.
    Please let us know your feedback on [GitHub](https://github.com/zauberzeug/nicegui/discussions),
    [Reddit](https://www.reddit.com/r/nicegui/), or [Discord](https://discord.gg/TEpFeAaF4f).

    **Data Privacy:**
    We take your privacy very serious.
    NiceGUI On Air does not log or store any content of the relayed data.


**Nice**GUI

The Python UI framework that shows up in your browser.

[](https://github.com/zauberzeug/nicegui/)

[](https://discord.gg/TEpFeAaF4f)

[](https://www.reddit.com/r/nicegui/)

Resources

[Documentation](/documentation)

[Examples](/examples)

[LLM reference](/llms.txt)

[GitHub](https://github.com/zauberzeug/nicegui/)

[PyPI](https://pypi.org/project/nicegui/)

Community

[Discussions](https://github.com/zauberzeug/nicegui/discussions)

[Discord](https://discord.gg/TEpFeAaF4f)

[Reddit](https://www.reddit.com/r/nicegui/)

[Contributing](https://github.com/zauberzeug/nicegui/blob/main/CONTRIBUTING.md)

[Sponsors](https://github.com/sponsors/zauberzeug)

Legal

[Imprint](/imprint_privacy#imprint)

[Privacy](/imprint_privacy#privacy)

Made with NiceGUI by [Zauberzeug](https://zauberzeug.com)

© 2026 [Zauberzeug GmbH](https://zauberzeug.com)