Add WSGI entry point and gunicorn; default debug off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9jG4CUFcH6n4vFvgtwndY
This commit is contained in:
emfurst 2026-10-17 12:17:54 -04:00
commit 4c5ba24a1d
5 changed files with 29 additions and 1 deletions

View file

@ -69,6 +69,14 @@ uv run admin-analytics dashboard
Opens at [http://localhost:8050](http://localhost:8050). Use `--port` to change the port, or `--host 0.0.0.0` for network access (e.g. over Tailscale).
For a production server, run it under gunicorn instead of Dash's development server:
```bash
uv run gunicorn -w 2 -b 127.0.0.1:8050 admin_analytics.dashboard.wsgi:server
```
Each worker opens its own read-only DuckDB connection. Don't use `--preload`, which would share one connection across forked workers.
The dashboard must be restarted to pick up newly ingested data (DuckDB opens in read-only mode to avoid lock conflicts).
The dashboard has eight tabs:

View file

@ -18,6 +18,7 @@ dependencies = [
"pandas>=2.0",
"dash>=2.17",
"plotly[express]>=5.22",
"gunicorn>=26.2.0",
]
[project.scripts]

View file

@ -196,7 +196,7 @@ def scrape() -> None:
def dashboard(
host: Annotated[str, typer.Option(help="Host to bind to (0.0.0.0 for network access)")] = "127.0.0.1",
port: Annotated[int, typer.Option(help="Port to serve on")] = 8050,
debug: Annotated[bool, typer.Option(help="Enable Dash debug mode")] = True,
debug: Annotated[bool, typer.Option(help="Enable Dash debug mode")] = False,
) -> None:
"""Launch the analytics dashboard."""
from admin_analytics.dashboard.app import create_app

View file

@ -0,0 +1,8 @@
"""WSGI entry point for production servers, e.g.:
uv run gunicorn admin_analytics.dashboard.wsgi:server
"""
from admin_analytics.dashboard.app import create_app
server = create_app().server

11
uv.lock generated
View file

@ -18,6 +18,7 @@ dependencies = [
{ name = "beautifulsoup4" },
{ name = "dash" },
{ name = "duckdb" },
{ name = "gunicorn" },
{ name = "httpx" },
{ name = "lxml" },
{ name = "openpyxl" },
@ -41,6 +42,7 @@ requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.12" },
{ name = "dash", specifier = ">=2.17" },
{ name = "duckdb", specifier = ">=1.0" },
{ name = "gunicorn", specifier = ">=26.2.0" },
{ name = "httpx", specifier = ">=0.27" },
{ name = "lxml", specifier = ">=5.0" },
{ name = "openpyxl", specifier = ">=3.1" },
@ -356,6 +358,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" },
]
[[package]]
name = "gunicorn"
version = "26.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d9/8a/e4ef6ee11701b6cd64702848415ffb69eeff85cb388a3c6c7fe86f22f3f8/gunicorn-26.2.0.tar.gz", hash = "sha256:62b864895d9ebff0b2f9867ba04fe811c93121596540830c9c916d0769668447", size = 787921, upload-time = "2026-08-24T15:05:59.3Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/85/7522a52e5e2f42faf1a129113ab63e548c42e103e9af395b7bfe65e403e2/gunicorn-26.2.0-py3-none-any.whl", hash = "sha256:bd249d0b3f7972f7432f0a6b6ff3b3ee2d129f70cd1ff6c09a9dd9e29a2b88e3", size = 228389, upload-time = "2026-08-24T15:05:57.67Z" },
]
[[package]]
name = "h11"
version = "0.16.0"