meadows-web 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meadows_web-0.1.0/.coverage +0 -0
- meadows_web-0.1.0/.env.example +13 -0
- meadows_web-0.1.0/.gitignore +19 -0
- meadows_web-0.1.0/Dockerfile +9 -0
- meadows_web-0.1.0/PKG-INFO +88 -0
- meadows_web-0.1.0/README.md +70 -0
- meadows_web-0.1.0/captain-hooks/backup_files.sh +4 -0
- meadows_web-0.1.0/captain-hooks/restore_files.sh +4 -0
- meadows_web-0.1.0/docker-compose.yml +35 -0
- meadows_web-0.1.0/pyproject.toml +82 -0
- meadows_web-0.1.0/shared_keys/jwt.key +1 -0
- meadows_web-0.1.0/src/meadows/web/__about__.py +3 -0
- meadows_web-0.1.0/src/meadows/web/__init__.py +18 -0
- meadows_web-0.1.0/src/meadows/web/__main__.py +28 -0
- meadows_web-0.1.0/src/meadows/web/app.py +120 -0
- meadows_web-0.1.0/src/meadows/web/build.py +112 -0
- meadows_web-0.1.0/src/meadows/web/static/link-popup.js +629 -0
- meadows_web-0.1.0/src/meadows/web/static/socket.io/socket.io.js +7 -0
- meadows_web-0.1.0/src/meadows/web/templates/index.html +5084 -0
- meadows_web-0.1.0/start.sh +2 -0
- meadows_web-0.1.0/tasks.py +70 -0
- meadows_web-0.1.0/tests/conftest.py +32 -0
- meadows_web-0.1.0/tests/test_app.py +50 -0
- meadows_web-0.1.0/tests/test_build.py +87 -0
- meadows_web-0.1.0/uv.lock +997 -0
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Meadows web host: bind address and port (Traefik terminates TLS in front of this)
|
|
2
|
+
MEADOWS_WEB_HOST=0.0.0.0
|
|
3
|
+
MEADOWS_WEB_PORT=8081
|
|
4
|
+
|
|
5
|
+
# URL of meadows-server the browser Socket.IO client connects to (injected into index.html)
|
|
6
|
+
MEADOWS_SERVER_URL=http://localhost:8080
|
|
7
|
+
|
|
8
|
+
# Display name shown in the chat UI
|
|
9
|
+
MEADOWS_SYSTEM_NAME=MEADOWS Chat
|
|
10
|
+
|
|
11
|
+
# Traefik routing (see docker-compose.yml labels)
|
|
12
|
+
PROJECT=meadows
|
|
13
|
+
HOSTINGDOMAIN=localhost
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: meadows-web
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MEADOWS web host: serves index.html and static assets. Dumb HTTP host, no domain logic.
|
|
5
|
+
Author: MEADOWS
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: meadows-protocol
|
|
8
|
+
Requires-Dist: starlette>=0.37.0
|
|
9
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
12
|
+
Requires-Dist: httpx>=0.27.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff==0.14.9; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# meadows-web
|
|
20
|
+
|
|
21
|
+
> MEADOWS web host: a dumb HTTP host that serves `index.html` and static assets.
|
|
22
|
+
> No Socket.IO, no auth, no domain logic. The browser is the client; the
|
|
23
|
+
> Socket.IO connection runs browser→meadows-server, NOT via this Python webserver.
|
|
24
|
+
> See `MEADOWS-migration-intent.md` section 2 line 40 and section 4 line 115.
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
```mermaid
|
|
29
|
+
graph LR
|
|
30
|
+
B[Browser<br>JS] -->|Socket.IO| S[meadows-server<br>:8080]
|
|
31
|
+
B -->|HTTP| W[meadows-web<br>:8081]
|
|
32
|
+
W -.->|static files only| B
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The browser connects directly to `meadows-server` via Socket.IO. This Python server is just a file host.
|
|
36
|
+
|
|
37
|
+
## What this package contains
|
|
38
|
+
|
|
39
|
+
- `app.py` — the Starlette ASGI app. Serves `/` → `dist/index.html` and `/static/*` → assets. Nothing else.
|
|
40
|
+
- `build.py` — template injection: reads `templates/index.html`, injects protocol constants + env config, writes `dist/index.html`.
|
|
41
|
+
- `templates/index.html` — minimal webchat page (Socket.IO client in the browser).
|
|
42
|
+
- `__main__.py` — `python -m meadows.web` entrypoint (uvicorn).
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv pip install -e ".[dev]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This pulls in `meadows-protocol` (editable, via the sibling path) — the **only** MEADOWS dependency. The web host touches `meadows.protocol` solely to inject `EventName` constants into the template. It does **not** import `Message`, `JWTClaims`, or any domain model.
|
|
51
|
+
|
|
52
|
+
## Build the template
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv run python -m meadows.web.build
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Run
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv run python -m meadows.web
|
|
62
|
+
# or
|
|
63
|
+
uv run uvicorn meadows.web.app:app --host 0.0.0.0 --port 8081
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Test
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv run pytest -q
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Architecture invariants
|
|
73
|
+
|
|
74
|
+
1. **Dumb host.** No Socket.IO, no auth, no JWT, no message parsing. It serves files. Period.
|
|
75
|
+
2. **TLS is not a concern.** Traefik terminates TLS (section 4 line 115). No cert logic here.
|
|
76
|
+
3. **Protocol constants only.** The only import from `meadows.protocol` is `EventName` (for template injection).
|
|
77
|
+
4. **PEP 420 namespace.** `src/meadows/web/__init__.py` exists; there is NO `src/meadows/__init__.py`.
|
|
78
|
+
|
|
79
|
+
## Configuration (env vars, managed via `check_env` in `tasks.py:setup`)
|
|
80
|
+
|
|
81
|
+
| variable | default | purpose |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `MEADOWS_WEB_HOST` | `0.0.0.0` | bind address for uvicorn |
|
|
84
|
+
| `MEADOWS_WEB_PORT` | `8081` | bind port for uvicorn |
|
|
85
|
+
| `MEADOWS_SERVER_URL` | `http://localhost:8080` | server URL injected into the page (browser Socket.IO target) |
|
|
86
|
+
| `MEADOWS_SYSTEM_NAME` | `MEADOWS Chat` | display name injected into the page |
|
|
87
|
+
| `PROJECT` | `meadows` | Traefik router prefix |
|
|
88
|
+
| `HOSTINGDOMAIN` | `localhost` | Traefik host domain |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# meadows-web
|
|
2
|
+
|
|
3
|
+
> MEADOWS web host: a dumb HTTP host that serves `index.html` and static assets.
|
|
4
|
+
> No Socket.IO, no auth, no domain logic. The browser is the client; the
|
|
5
|
+
> Socket.IO connection runs browser→meadows-server, NOT via this Python webserver.
|
|
6
|
+
> See `MEADOWS-migration-intent.md` section 2 line 40 and section 4 line 115.
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
|
|
10
|
+
```mermaid
|
|
11
|
+
graph LR
|
|
12
|
+
B[Browser<br>JS] -->|Socket.IO| S[meadows-server<br>:8080]
|
|
13
|
+
B -->|HTTP| W[meadows-web<br>:8081]
|
|
14
|
+
W -.->|static files only| B
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The browser connects directly to `meadows-server` via Socket.IO. This Python server is just a file host.
|
|
18
|
+
|
|
19
|
+
## What this package contains
|
|
20
|
+
|
|
21
|
+
- `app.py` — the Starlette ASGI app. Serves `/` → `dist/index.html` and `/static/*` → assets. Nothing else.
|
|
22
|
+
- `build.py` — template injection: reads `templates/index.html`, injects protocol constants + env config, writes `dist/index.html`.
|
|
23
|
+
- `templates/index.html` — minimal webchat page (Socket.IO client in the browser).
|
|
24
|
+
- `__main__.py` — `python -m meadows.web` entrypoint (uvicorn).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv pip install -e ".[dev]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This pulls in `meadows-protocol` (editable, via the sibling path) — the **only** MEADOWS dependency. The web host touches `meadows.protocol` solely to inject `EventName` constants into the template. It does **not** import `Message`, `JWTClaims`, or any domain model.
|
|
33
|
+
|
|
34
|
+
## Build the template
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv run python -m meadows.web.build
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Run
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv run python -m meadows.web
|
|
44
|
+
# or
|
|
45
|
+
uv run uvicorn meadows.web.app:app --host 0.0.0.0 --port 8081
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Test
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv run pytest -q
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Architecture invariants
|
|
55
|
+
|
|
56
|
+
1. **Dumb host.** No Socket.IO, no auth, no JWT, no message parsing. It serves files. Period.
|
|
57
|
+
2. **TLS is not a concern.** Traefik terminates TLS (section 4 line 115). No cert logic here.
|
|
58
|
+
3. **Protocol constants only.** The only import from `meadows.protocol` is `EventName` (for template injection).
|
|
59
|
+
4. **PEP 420 namespace.** `src/meadows/web/__init__.py` exists; there is NO `src/meadows/__init__.py`.
|
|
60
|
+
|
|
61
|
+
## Configuration (env vars, managed via `check_env` in `tasks.py:setup`)
|
|
62
|
+
|
|
63
|
+
| variable | default | purpose |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `MEADOWS_WEB_HOST` | `0.0.0.0` | bind address for uvicorn |
|
|
66
|
+
| `MEADOWS_WEB_PORT` | `8081` | bind port for uvicorn |
|
|
67
|
+
| `MEADOWS_SERVER_URL` | `http://localhost:8080` | server URL injected into the page (browser Socket.IO target) |
|
|
68
|
+
| `MEADOWS_SYSTEM_NAME` | `MEADOWS Chat` | display name injected into the page |
|
|
69
|
+
| `PROJECT` | `meadows` | Traefik router prefix |
|
|
70
|
+
| `HOSTINGDOMAIN` | `localhost` | Traefik host domain |
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
networks:
|
|
2
|
+
meadows:
|
|
3
|
+
driver: bridge
|
|
4
|
+
broker:
|
|
5
|
+
external: true
|
|
6
|
+
name: broker
|
|
7
|
+
|
|
8
|
+
services:
|
|
9
|
+
meadows-web:
|
|
10
|
+
build: .
|
|
11
|
+
image: meadows-web:latest
|
|
12
|
+
restart: unless-stopped
|
|
13
|
+
environment:
|
|
14
|
+
- MEADOWS_WEB_HOST=${MEADOWS_WEB_HOST:-0.0.0.0}
|
|
15
|
+
- MEADOWS_WEB_PORT=${MEADOWS_WEB_PORT:-8081}
|
|
16
|
+
- MEADOWS_SERVER_URL=${MEADOWS_SERVER_URL:-http://localhost:8080}
|
|
17
|
+
- MEADOWS_SYSTEM_NAME=${MEADOWS_SYSTEM_NAME:-MEADOWS Chat}
|
|
18
|
+
- PROJECT=${PROJECT:-meadows}
|
|
19
|
+
- HOSTINGDOMAIN=${HOSTINGDOMAIN:-localhost}
|
|
20
|
+
networks:
|
|
21
|
+
- meadows
|
|
22
|
+
- broker
|
|
23
|
+
logging:
|
|
24
|
+
driver: "json-file"
|
|
25
|
+
options:
|
|
26
|
+
max-size: "100m"
|
|
27
|
+
max-file: "5"
|
|
28
|
+
labels:
|
|
29
|
+
- "traefik.enable=true"
|
|
30
|
+
- "traefik.http.routers.${PROJECT:-meadows}-meadows-web-secure.rule=Host(`${ROUTER_HOST:-chat.${HOSTINGDOMAIN:-localhost}}`)"
|
|
31
|
+
- "traefik.http.routers.${PROJECT:-meadows}-meadows-web-secure.tls=true"
|
|
32
|
+
- "traefik.http.routers.${PROJECT:-meadows}-meadows-web-secure.entrypoints=web-secured"
|
|
33
|
+
- "traefik.http.routers.${PROJECT:-meadows}-meadows-web-secure.tls.certresolver=letsencrypt"
|
|
34
|
+
- "traefik.http.services.${PROJECT:-meadows}-meadows-web.loadbalancer.server.port=${MEADOWS_WEB_PORT:-8081}"
|
|
35
|
+
- "traefik.docker.network=broker"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "meadows-web"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MEADOWS web host: serves index.html and static assets. Dumb HTTP host, no domain logic."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license-expression = "MIT"
|
|
12
|
+
authors = [{ name = "MEADOWS" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"meadows-protocol",
|
|
15
|
+
"uvicorn>=0.30.0",
|
|
16
|
+
"starlette>=0.37.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = ["hatch", "pytest>=8.0.0", "pytest-asyncio>=0.23.0", "pytest-cov>=5.0.0", "httpx>=0.27.0", "ruff==0.14.9"]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/meadows"]
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources]
|
|
26
|
+
meadows-protocol = { path = "../meadows-protocol", editable = true }
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
target-version = "py312"
|
|
30
|
+
line-length = 120
|
|
31
|
+
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
select = ["F", "E", "W", "Q", "A", "SIM", "ARG", "PTH", "RUF", "C90", "N", "YTT"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
asyncio_mode = "auto"
|
|
38
|
+
|
|
39
|
+
[tool.vommit]
|
|
40
|
+
allow_breaking_bang = true
|
|
41
|
+
allow_breaking_footer = true
|
|
42
|
+
prerelease_token = "rc"
|
|
43
|
+
confirm = true
|
|
44
|
+
|
|
45
|
+
[tool.vommit.git]
|
|
46
|
+
enabled = true
|
|
47
|
+
origin = "origin"
|
|
48
|
+
branch = "main"
|
|
49
|
+
on_wrong_branch = "error"
|
|
50
|
+
tag_format = "v{version}"
|
|
51
|
+
commit_format = "{version}"
|
|
52
|
+
|
|
53
|
+
[tool.vommit.changelog]
|
|
54
|
+
enabled = true
|
|
55
|
+
file = "CHANGELOG.md"
|
|
56
|
+
include_prereleases = false
|
|
57
|
+
placeholder = "<!-- next-version-placeholder -->"
|
|
58
|
+
placeholder_regex = "<auto>"
|
|
59
|
+
entry_title_format = "## v{version} ({date:%Y-%m-%d})"
|
|
60
|
+
|
|
61
|
+
[tool.vommit.changelog.levels]
|
|
62
|
+
break = "Breaking Change{s}"
|
|
63
|
+
feat = "Feature{s}"
|
|
64
|
+
fix = "Fix{es}"
|
|
65
|
+
perf = "Performance"
|
|
66
|
+
docs = "Documentation"
|
|
67
|
+
|
|
68
|
+
[tool.vommit.pypi]
|
|
69
|
+
enabled = true
|
|
70
|
+
use_keyring = true
|
|
71
|
+
|
|
72
|
+
[tool.vommit.commands]
|
|
73
|
+
clean = "rm -rf ./dist"
|
|
74
|
+
build = "uv build"
|
|
75
|
+
publish = "../pypi-publish"
|
|
76
|
+
post_publish = ""
|
|
77
|
+
|
|
78
|
+
[tool.vommit.version_bump_map]
|
|
79
|
+
break = "major"
|
|
80
|
+
feat = "minor"
|
|
81
|
+
fix = "patch"
|
|
82
|
+
perf = "patch"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
f66dee38fa244ab248937238aef06918868c485c8f086a5bf022a0220a8c5464
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""MEADOWS web host — a dumb HTTP host (MEADOWS §2 line 40).
|
|
2
|
+
|
|
3
|
+
This package serves index.html and static assets. It is deliberately the
|
|
4
|
+
simplest of the five MEADOWS distributions: no Socket.IO, no auth, no domain
|
|
5
|
+
logic. The browser is the Socket.IO client; the connection runs browser→
|
|
6
|
+
meadows-server, NOT via this Python webserver. TLS is terminated by Traefik
|
|
7
|
+
(section 4 line 115).
|
|
8
|
+
|
|
9
|
+
The only touch of meadows.protocol is in build.py, and only for EventName
|
|
10
|
+
(architecture invariant #3: protocol constants only).
|
|
11
|
+
|
|
12
|
+
PEP 420 (section 3.1): there is intentionally NO `src/meadows/__init__.py`.
|
|
13
|
+
Only this leaf package's __init__ exists.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from meadows.web.__about__ import __version__
|
|
17
|
+
|
|
18
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""BUSINESS RULE (MEADOWS §2 line 40): `python -m meadows.web` runs the dumb
|
|
2
|
+
HTTP host via uvicorn. Plain HTTP — TLS is terminated by Traefik (§4 line 115),
|
|
3
|
+
so no cert logic here (invariant #2). Defaults match tasks.py:check_env so the
|
|
4
|
+
container and local dev behave identically.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import uvicorn
|
|
12
|
+
|
|
13
|
+
from meadows.web.app import create_app
|
|
14
|
+
|
|
15
|
+
app = create_app()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
"""BUSINESS RULE: serve files on the configured host/port. Nothing else —
|
|
20
|
+
the browser opens its own Socket.IO connection to meadows-server using the
|
|
21
|
+
URL injected into index.html by build.py."""
|
|
22
|
+
host = os.environ.get("MEADOWS_WEB_HOST", "0.0.0.0")
|
|
23
|
+
port = int(os.environ.get("MEADOWS_WEB_PORT", "8081"))
|
|
24
|
+
uvicorn.run(app, host=host, port=port)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BUSINESS RULE (MEADOWS §2 line 40 + §4 line 115): meadows-web is a dumb HTTP
|
|
3
|
+
host. It serves index.html and static assets, injects protocol constants into
|
|
4
|
+
the template, and does nothing else. The browser is the Socket.IO client; the
|
|
5
|
+
connection runs browser→meadows-server, NOT via this Python webserver.
|
|
6
|
+
|
|
7
|
+
This module deliberately has no socketio, no auth, no domain logic, no JWT, no
|
|
8
|
+
message parsing (architecture invariant #1). TLS is terminated by Traefik
|
|
9
|
+
(section 4 line 115), so this host speaks plain HTTP (invariant #2).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from starlette.applications import Starlette
|
|
17
|
+
from starlette.responses import FileResponse, JSONResponse, Response
|
|
18
|
+
from starlette.routing import Mount
|
|
19
|
+
from starlette.staticfiles import StaticFiles
|
|
20
|
+
|
|
21
|
+
from meadows.web.build import build
|
|
22
|
+
|
|
23
|
+
# Resolved relative to this file so the app works regardless of CWD (container
|
|
24
|
+
# WORKDIR is /app; the package lives at /app/src/meadows/web).
|
|
25
|
+
_PACKAGE_DIR = Path(__file__).resolve().parent
|
|
26
|
+
_DEFAULT_INDEX = _PACKAGE_DIR / "dist" / "index.html"
|
|
27
|
+
_DEFAULT_STATIC = _PACKAGE_DIR / "static"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WebHost:
|
|
31
|
+
"""
|
|
32
|
+
BUSINESS RULE (MEADOWS §2 line 40): meadows-web is a dumb HTTP host.
|
|
33
|
+
It serves files. The browser is the Socket.IO client, connecting directly
|
|
34
|
+
to meadows-server. This Python webserver never touches Socket.IO. TLS is
|
|
35
|
+
terminated by Traefik (MEADOWS §4 line 115).
|
|
36
|
+
|
|
37
|
+
This class wires three routes and nothing more:
|
|
38
|
+
- `GET /` -> the built dist/index.html (template-injected by build.py)
|
|
39
|
+
- `/static/*` -> static assets, if any (404 when absent)
|
|
40
|
+
- anything else -> 404
|
|
41
|
+
|
|
42
|
+
No auth, no sockets, no domain logic. If dist/index.html is missing on
|
|
43
|
+
startup, the template is built once so the host is usable without a manual
|
|
44
|
+
build step — this is static-asset preparation, not domain logic.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, index_path: Path = _DEFAULT_INDEX, static_dir: Path = _DEFAULT_STATIC) -> None:
|
|
48
|
+
self.index_path = index_path
|
|
49
|
+
self.static_dir = static_dir
|
|
50
|
+
|
|
51
|
+
def _ensure_built(self) -> None:
|
|
52
|
+
"""BUSINESS RULE: guarantee the served index.html exists before serving.
|
|
53
|
+
|
|
54
|
+
build.py is the single place that injects protocol constants (invariant
|
|
55
|
+
#3). If the dist file is absent (fresh checkout / container start without
|
|
56
|
+
a prior build), render it once from the template + env. This keeps the
|
|
57
|
+
host a pure file server at request time while still being runnable
|
|
58
|
+
standalone.
|
|
59
|
+
"""
|
|
60
|
+
if not self.index_path.exists():
|
|
61
|
+
build()
|
|
62
|
+
|
|
63
|
+
def serve_index(self, request) -> Response:
|
|
64
|
+
"""BUSINESS RULE: serve the pre-built index.html at `/`. Dumb file host —
|
|
65
|
+
no templating per request, no auth, no session. The page already carries
|
|
66
|
+
the injected protocol constants + server URL from build.py."""
|
|
67
|
+
del request # request is unused: this route is a fixed file response (dumb host).
|
|
68
|
+
self._ensure_built()
|
|
69
|
+
if not self.index_path.exists():
|
|
70
|
+
return Response("index.html not found", status_code=404)
|
|
71
|
+
return FileResponse(self.index_path, media_type="text/html")
|
|
72
|
+
|
|
73
|
+
def serve_status(self, request) -> Response:
|
|
74
|
+
"""BUSINESS RULE (MEADOWS §2 line 40): meadows-web has no auth surface.
|
|
75
|
+
The monolith's checkAuthStatus() fetches '/status' to detect Auth0
|
|
76
|
+
sessions; in MEADOWS, auth is JWT-only via the socket handshake. We
|
|
77
|
+
return a fixed `{"logged_in": false}` so the client JS falls through
|
|
78
|
+
to the manual-JWT auth overlay without a console 404 error. This is
|
|
79
|
+
a compatibility shim for the copied template, not domain logic."""
|
|
80
|
+
del request # unused: fixed response, no per-request logic.
|
|
81
|
+
return JSONResponse({"logged_in": False})
|
|
82
|
+
|
|
83
|
+
def not_found(self, request) -> Response:
|
|
84
|
+
"""BUSINESS RULE: anything that isn't `/` or `/static/*` is a 404. The
|
|
85
|
+
host serves only the chat page and static assets — no API surface."""
|
|
86
|
+
del request # unused: a dumb host has no per-request routing logic here.
|
|
87
|
+
return Response("Not found", status_code=404)
|
|
88
|
+
|
|
89
|
+
def create_app(self) -> Starlette:
|
|
90
|
+
"""BUSINESS RULE: assemble the ASGI app. Two real routes + a 404 catch-all.
|
|
91
|
+
StaticFiles is mounted (directory created empty if missing so missing
|
|
92
|
+
assets 404 instead of crashing the mount)."""
|
|
93
|
+
self.static_dir.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
self._ensure_built()
|
|
95
|
+
|
|
96
|
+
routes = [
|
|
97
|
+
Mount(
|
|
98
|
+
"/static",
|
|
99
|
+
app=StaticFiles(directory=str(self.static_dir), html=False),
|
|
100
|
+
name="static",
|
|
101
|
+
),
|
|
102
|
+
]
|
|
103
|
+
app = Starlette(routes=routes)
|
|
104
|
+
app.router.add_route("/", self.serve_index, methods=["GET"])
|
|
105
|
+
app.router.add_route("/status", self.serve_status, methods=["GET"])
|
|
106
|
+
app.router.add_route("/{path:path}", self.not_found, methods=["GET"])
|
|
107
|
+
return app
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def create_app() -> Starlette:
|
|
111
|
+
"""Module-level factory for `uvicorn meadows.web.app:app`."""
|
|
112
|
+
return WebHost().create_app()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# BUSINESS RULE: a module-level `app` so `uvicorn meadows.web.app:app` works
|
|
116
|
+
# without the caller invoking create_app() explicitly (matches the original
|
|
117
|
+
# monolith's `webchat` ASGI exposure pattern).
|
|
118
|
+
app = create_app()
|
|
119
|
+
|
|
120
|
+
__all__ = ["WebHost", "app", "create_app"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BUSINESS RULE (MEADOWS §2 line 40): meadows-web injects protocol constants
|
|
3
|
+
into the template. The browser needs to know event names (e.g. "message",
|
|
4
|
+
"authenticate", "user_typing") to talk to meadows-server. Rather than
|
|
5
|
+
hardcoding string literals in JS (which is what the monolith did — see
|
|
6
|
+
MEADOWS-migration-intent.md §1 line 11, where the protocol "zat verstopt
|
|
7
|
+
in de handlers en in conventies"), we inject them from the single source of
|
|
8
|
+
truth: meadows.protocol.EventName.
|
|
9
|
+
|
|
10
|
+
This module is the ONLY place meadows-web touches meadows.protocol, and it
|
|
11
|
+
touches it ONLY for EventName. Per architecture invariant #3, it does not
|
|
12
|
+
import Message, JWTClaims, or anything else — the web host doesn't need them.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from meadows.protocol import EventName
|
|
22
|
+
|
|
23
|
+
# BUSINESS RULE (MEADOWS §2 line 40): the template is the one artifact this host
|
|
24
|
+
# shapes before serving. Paths are relative to this module so the build works
|
|
25
|
+
# whether invoked from the repo root or from `python -m meadows.web.build`.
|
|
26
|
+
_TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
|
27
|
+
TEMPLATE_PATH = _TEMPLATES_DIR / "index.html"
|
|
28
|
+
OUTPUT_PATH = Path(__file__).resolve().parent / "dist" / "index.html"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_protocol_constants() -> dict[str, str]:
|
|
32
|
+
"""BUSINESS RULE (MEADOWS §2 line 40 + §3.1): expose the closed set of event
|
|
33
|
+
names to the browser as a JSON blob.
|
|
34
|
+
|
|
35
|
+
The protocol package declares EventName as the single source of truth for the
|
|
36
|
+
Socket.IO events the system contracts (meadows-protocol/src/meadows/protocol/
|
|
37
|
+
events.py). The browser JS must emit and listen for these exact names. We
|
|
38
|
+
serialize `{MEMBER_NAME: string_value}` so JS can reference
|
|
39
|
+
`window.MEADOWS_PROTOCOL.MESSAGE` etc. without hardcoding literals.
|
|
40
|
+
|
|
41
|
+
This is a pure declaration pull — no behavior is imported, keeping invariant
|
|
42
|
+
#3 (protocol constants only).
|
|
43
|
+
"""
|
|
44
|
+
return {member.name: member.value for member in EventName}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compute_hash(content: str) -> str:
|
|
48
|
+
"""BUSINESS RULE: produce a cache-busting token for the served page.
|
|
49
|
+
|
|
50
|
+
The original monolith build.py (chat.openit.chat/webchat/build.py) computed a
|
|
51
|
+
SHA-256 prefix for the same purpose. We keep the 16-char hex prefix so CDN /
|
|
52
|
+
browser caches invalidate whenever the rendered content (and thus the injected
|
|
53
|
+
protocol constants or config) changes.
|
|
54
|
+
"""
|
|
55
|
+
return hashlib.sha256(content.encode()).hexdigest()[:16]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build(
|
|
59
|
+
*,
|
|
60
|
+
server_url: str | None = None,
|
|
61
|
+
system_name: str | None = None,
|
|
62
|
+
template_path: Path | None = None,
|
|
63
|
+
output_path: Path | None = None,
|
|
64
|
+
) -> Path:
|
|
65
|
+
"""BUSINESS RULE (MEADOWS §2 line 40): render templates/index.html into
|
|
66
|
+
dist/index.html with env config + protocol constants injected.
|
|
67
|
+
|
|
68
|
+
This is the meadows-web equivalent of the monolith's build.py — but the only
|
|
69
|
+
protocol knowledge it carries is EventName (invariant #3). The rendered file
|
|
70
|
+
is what app.py serves at `/`. TLS, auth and sockets are deliberately absent
|
|
71
|
+
(invariants #1 and #2); the browser uses the injected MEADOWS_SERVER_URL to
|
|
72
|
+
open its own Socket.IO connection straight to meadows-server.
|
|
73
|
+
|
|
74
|
+
Defaults read from the environment so `python -m meadows.web.build` works in
|
|
75
|
+
the container (env vars come from docker-compose / check_env in tasks.py).
|
|
76
|
+
Returns the path to the built file so callers (tests, CLI) can locate it.
|
|
77
|
+
"""
|
|
78
|
+
import os
|
|
79
|
+
|
|
80
|
+
server_url = server_url if server_url is not None else os.environ.get("MEADOWS_SERVER_URL", "http://localhost:8080")
|
|
81
|
+
system_name = system_name if system_name is not None else os.environ.get("MEADOWS_SYSTEM_NAME", "MEADOWS Chat")
|
|
82
|
+
|
|
83
|
+
template_path = template_path if template_path is not None else TEMPLATE_PATH
|
|
84
|
+
output_path = output_path if output_path is not None else OUTPUT_PATH
|
|
85
|
+
|
|
86
|
+
if not template_path.exists():
|
|
87
|
+
raise FileNotFoundError(f"Template not found: {template_path}")
|
|
88
|
+
|
|
89
|
+
content = template_path.read_text(encoding="utf-8")
|
|
90
|
+
|
|
91
|
+
# Inject config + protocol constants (single source of truth = EventName).
|
|
92
|
+
protocol_json = json.dumps(get_protocol_constants(), indent=2, sort_keys=True)
|
|
93
|
+
content = content.replace("{{ MEADOWS_SERVER_URL }}", server_url)
|
|
94
|
+
content = content.replace("{{ MEADOWS_SYSTEM_NAME }}", system_name)
|
|
95
|
+
content = content.replace("{{ MEADOWS_PROTOCOL }}", protocol_json)
|
|
96
|
+
|
|
97
|
+
# Cache-busting hash: computed over the rendered content, then injected
|
|
98
|
+
# as a meta tag before </head> (matching the monolith's build.py pattern).
|
|
99
|
+
client_hash = compute_hash(content)
|
|
100
|
+
content = content.replace(
|
|
101
|
+
"</head>",
|
|
102
|
+
f'<meta name="meadows-hash" content="{client_hash}"></head>',
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
output_path.write_text(content, encoding="utf-8")
|
|
107
|
+
print(f"Built {output_path} (hash={client_hash})")
|
|
108
|
+
return output_path
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
build()
|