Metadata-Version: 2.4
Name: pagebar
Version: 0.1.4
Summary: A tiny prod-safe perf footer & toolbar for ASGI apps.
Author-email: Stéfane Fermigier <sf@fermigier.com>
License: MIT
License-File: LICENSE
Keywords: asgi,footer,monitoring,performance,sql
Classifier: Framework :: AsyncIO
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Provides-Extra: flask
Requires-Dist: flask>=2; extra == 'flask'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# pagebar

A tiny prod-safe perf footer for ASGI apps. One line at the bottom of every page, click to expand. No SQL text, no env vars, no stack traces — nothing that could leak.

```
v2026.6.19 · 22ms · 4 SQL · GET /editeurs · 200
```

![pagebar's footer pill](docs/images/pill.png)

Click it to expand the full panel:

![The expanded pagebar panel](docs/images/panel-expanded.png)

## Install

```sh
pip install pagebar              # core only
pip install pagebar[sqlalchemy]  # with SQL query counter
pip install pagebar[flask]       # Flask (WSGI) integration
```

## Use

### Starlette / FastAPI

```python
from pagebar import PagebarMiddleware, pagebar_html
from starlette.applications import Starlette
from starlette.middleware import Middleware

app = Starlette(
    middleware=[Middleware(PagebarMiddleware, package="my-app")],
    routes=[...],
)
```

### Litestar (and other `mw_cls(app)`-style frameworks)

Litestar instantiates middleware as `cls(app)` with no further kwargs, so config has to be baked in. Use `PagebarMiddleware.bound(...)`:

```python
from litestar import Litestar
from pagebar import PagebarMiddleware

app = Litestar(
    middleware=[
        PagebarMiddleware.bound(package="my-app", unsafe=False),
    ],
    route_handlers=[...],
)
```

`.bound(**kwargs)` returns a thin subclass with the kwargs baked into `__init__`. Same fields as the regular constructor.

### Flask (WSGI)

Flask isn't ASGI, so there's no middleware — the `pagebar[flask]` extra ships a small extension that wires the same data collection into Flask's request lifecycle:

```python
from flask import Flask
from pagebar.flask import Pagebar

app = Flask(__name__)
Pagebar(app, package="my-app", unsafe=app.debug)
```

Same knobs as `PagebarMiddleware`. `pagebar_html` is registered as a Jinja global, so the template usage below works unchanged. `enabled`/`unsafe` callables receive the Flask `request` (the ASGI versions get the connection scope).

### Template

In your base template, anywhere inside `<body>`:

```html
{{ pagebar_html() | safe }}
</body>
```

That's it. One name for the middleware, one for the helper.

## What's on screen

| Field   | Source                                                            |
|---------|-------------------------------------------------------------------|
| Version | `importlib.metadata.version(package)` — or explicit `version=`    |
| Time    | `time.perf_counter()` delta                                       |
| SQL     | SQLAlchemy `before_cursor_execute` listener (if installed)        |
| Request | method + path                                                     |
| Memory  | `resource.getrusage().ru_maxrss` — RSS in MiB                     |
| Uptime  | `time.monotonic()` since worker import                            |
| Threads | `threading.active_count()`                                        |
| GC      | `gc.get_count()` — gen0/gen1/gen2 pending                         |
| Python  | `sys.version_info`                                                |
| PID     | `os.getpid()`                                                     |

No SQL text. No params. No env. No traces. That's the whole surface.

## Knobs

```python
PagebarMiddleware(
    app,
    package="my-app",     # distribution name (PyPI / pyproject.toml)
    version="",           # escape hatch: bypass importlib.metadata lookup
    enabled=True,         # bool or callable(scope) -> bool
    unsafe=False,         # bool or callable(scope) -> bool — see below
    query_budget=20,      # >0 → log a WARNING when SQL count exceeds this
)
```

## Unsafe mode

In dev, you usually *want* to see the SQL text. Flip `unsafe=True` and the panel gains a SQL summary with a **SQL details** button. That opens a full-screen modal with two sections: a **grouped N+1 report** (each unique statement with its repeat count and total time, repeats flagged in red — the panel also shows how many queries were redundant), and the **full list** of every query in order with bound parameters and per-statement timing. The modal text is selectable, and a **Copy report** button puts a self-contained N+1 summary on the clipboard — request line, totals, and each unique statement with its call count — ready to paste into a bug report:

```
pagebar SQL report — GET /widgets
21 queries, 2 unique, 19 redundant (possible N+1)

20×  0.0ms  SELECT name FROM widget WHERE id = ?
 1×  0.1ms  SELECT id FROM widget
```

![The unsafe-mode SQL modal: grouped N+1 report above a collapsible full-query list](docs/images/sql-modal.png)

A red `UNSAFE` badge in the pill makes the mode obvious.

```python
import os
PagebarMiddleware(app, package="my-app", unsafe=bool(os.getenv("DEBUG")))
# or framework-driven
PagebarMiddleware(app, package="my-app", unsafe=app.debug)
```

`unsafe` defaults to `False` and **only** takes its value from constructor wiring — no URL parameter, no header, no cookie. The host's deploy config is the authority.

`enabled` lets you hide the bar from JSON endpoints, healthchecks, or admin routes:

```python
def show(scope):
    return not scope["path"].startswith(("/api/", "/health"))

Middleware(PagebarMiddleware, package="my-app", enabled=show)
```

Bots are skipped automatically (`User-Agent` matching `bot|crawler|spider|googlebot|bingbot`).

## CSP

`pagebar_html()` accepts a `nonce` keyword that's applied to the inline `<style>` and `<script>`:

```html
{{ pagebar_html(nonce=csp_nonce) | safe }}
```

Otherwise the host needs `'unsafe-inline'` for both directives. No external assets, no third-party requests.

## Why not the Django/Flask/Litestar debug toolbars?

They reveal SQL text, environment, settings, request bodies, stack traces — fine in dev, unacceptable in production. pagebar surfaces a fixed, public-safe set of fields. The shape is the security model.

## Non-goals

No panels system, no plugins, no per-framework adapters, no APM, no time series, no SQL EXPLAIN, no profiler. Pure ASGI middleware + one helper function in one file.

## License

MIT.
