From 4c5ba24a1d2a5b681979e193103dc44c74f2a6fd Mon Sep 17 00:00:00 2001 From: emfurst Date: Sat, 17 Oct 2026 12:17:54 -0400 Subject: [PATCH] Add WSGI entry point and gunicorn; default debug off Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X9jG4CUFcH6n4vFvgtwndY --- README.md | 8 ++++++++ pyproject.toml | 1 + src/admin_analytics/cli.py | 2 +- src/admin_analytics/dashboard/wsgi.py | 8 ++++++++ uv.lock | 11 +++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/admin_analytics/dashboard/wsgi.py diff --git a/README.md b/README.md index a9b9e5e..b2b6cc2 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/pyproject.toml b/pyproject.toml index f97ec25..23b13a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "pandas>=2.0", "dash>=2.17", "plotly[express]>=5.22", + "gunicorn>=26.2.0", ] [project.scripts] diff --git a/src/admin_analytics/cli.py b/src/admin_analytics/cli.py index f8080c8..fc17796 100644 --- a/src/admin_analytics/cli.py +++ b/src/admin_analytics/cli.py @@ -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 diff --git a/src/admin_analytics/dashboard/wsgi.py b/src/admin_analytics/dashboard/wsgi.py new file mode 100644 index 0000000..29bef09 --- /dev/null +++ b/src/admin_analytics/dashboard/wsgi.py @@ -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 diff --git a/uv.lock b/uv.lock index e368aa5..d56a4b8 100644 --- a/uv.lock +++ b/uv.lock @@ -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"