Sync with canonical matcha/phast@69bbbab and run on Fly as phast-app
The 2026-10-14 port took tea-mcp/src/phast, an older copy (light design, no OAuth). This merges matcha/phast main with its history: the black/orange phast.app and my.phast.app pages, and the Teahub-OAuth MCP server. On top, for prod: - server.py serves phast.app / and my.phast.app / from this repo (no copies), and /healthz - Dockerfile + start.sh: volume at /data, app runs unprivileged - fly.toml: phast-app, volume phast_data, GITEA_URL=https://teahub.xyz
This commit is contained in:
commit
cbd4a28e75
13 files changed
+612
-280
No files matched your search
@@ -1,174 +1,425 @@
|
||||
"""Phast public landing and fail-closed owner control plane."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import uvicorn
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
||||
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Badge, Button, Card, Code, Column, Heading, Row, Separator, Text
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse, JSONResponse
|
||||
from prefab_ui.components import Badge, Card, Column, Heading, Row, Text
|
||||
from starlette.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
||||
|
||||
try:
|
||||
from .registry import AppRegistry, RegistryUnavailable, registry_from_runtime
|
||||
except ImportError: # Docker runs this file directly from /app.
|
||||
from registry import AppRegistry, RegistryUnavailable, registry_from_runtime
|
||||
DATA = Path(os.getenv("PHAST_DATA_DIR", "/data/phast"))
|
||||
# The public pages are served by this app too: one source, no copies to drift.
|
||||
HERE = Path(__file__).resolve().parent
|
||||
PAGES = {"phast.app": HERE / "index.html", "my.phast.app": HERE / "my" / "index.html"}
|
||||
DATA.mkdir(parents=True, exist_ok=True)
|
||||
GITEA = os.getenv("GITEA_URL", "https://teahub.xyz").rstrip("/")
|
||||
CID = os.environ["GITEA_OAUTH_CLIENT_ID"]
|
||||
SECRET = os.environ["GITEA_OAUTH_CLIENT_SECRET"]
|
||||
JWT = os.environ["JWT_SIGNING_KEY"]
|
||||
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "")
|
||||
USERS = {x.strip() for x in os.getenv("ALLOWED_GITEA_USERS", "matcha,chai").split(",") if x.strip()}
|
||||
RX = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
|
||||
|
||||
OWNER_HOST = "my.phast.app"
|
||||
AUTH_BLOCKER = "Owner authentication is not configured; app mutations are disabled."
|
||||
|
||||
try:
|
||||
registry: AppRegistry | None = registry_from_runtime()
|
||||
registry_blocker: str | None = None
|
||||
except RegistryUnavailable as error:
|
||||
registry = None
|
||||
registry_blocker = str(error)
|
||||
class V(IntrospectionTokenVerifier):
|
||||
async def verify_token(self, token):
|
||||
access = await super().verify_token(token)
|
||||
if not access:
|
||||
return None
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.get(
|
||||
GITEA + "/login/oauth/userinfo",
|
||||
headers={"Authorization": "Bearer " + token},
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
login = str(response.json().get("preferred_username") or "")
|
||||
if login not in USERS:
|
||||
return None
|
||||
access.claims = {**(access.claims or {}), "login": login}
|
||||
return access
|
||||
|
||||
mcp = FastMCP(
|
||||
"My Phast",
|
||||
instructions="Owner control plane for Phast app manifests. Mutations fail closed until owner auth is installed.",
|
||||
|
||||
def auth(base: str, resource: str | None = None) -> OAuthProxy:
|
||||
verifier = V(
|
||||
introspection_url=GITEA + "/login/oauth/introspect",
|
||||
client_id=CID,
|
||||
client_secret=SECRET,
|
||||
client_auth_method="client_secret_basic",
|
||||
required_scopes=["openid"],
|
||||
)
|
||||
return OAuthProxy(
|
||||
upstream_authorization_endpoint=GITEA + "/login/oauth/authorize",
|
||||
upstream_token_endpoint=GITEA + "/login/oauth/access_token",
|
||||
upstream_client_id=CID,
|
||||
upstream_client_secret=SECRET,
|
||||
token_verifier=verifier,
|
||||
base_url=base,
|
||||
resource_base_url=resource or base,
|
||||
valid_scopes=["openid", "profile", "email"],
|
||||
forward_pkce=True,
|
||||
forward_resource=False,
|
||||
token_endpoint_auth_method="client_secret_basic",
|
||||
jwt_signing_key=JWT,
|
||||
require_authorization_consent="remember",
|
||||
enable_cimd=True,
|
||||
)
|
||||
|
||||
|
||||
def actor() -> str:
|
||||
access = get_access_token()
|
||||
login = str(((access.claims if access else {}) or {}).get("login") or "")
|
||||
if login not in USERS:
|
||||
raise RuntimeError("actor not enabled")
|
||||
return login
|
||||
|
||||
|
||||
def slug(value: str) -> str:
|
||||
value = value.strip().lower()
|
||||
if not RX.fullmatch(value) or value in {"my", "www", "mcp"}:
|
||||
raise ValueError("invalid slug")
|
||||
return value
|
||||
|
||||
|
||||
def path(value: str) -> Path:
|
||||
return DATA / slug(value) / "phast.json"
|
||||
|
||||
|
||||
def read(value: str) -> dict[str, Any]:
|
||||
p = path(value)
|
||||
if not p.exists():
|
||||
raise KeyError("unknown Phast app")
|
||||
return json.loads(p.read_text())
|
||||
|
||||
|
||||
def write(value: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
p = path(value)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
p.parent.chmod(0o700)
|
||||
except OSError:
|
||||
pass
|
||||
tmp = p.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
|
||||
tmp.chmod(0o600)
|
||||
tmp.replace(p)
|
||||
try:
|
||||
p.chmod(0o600)
|
||||
except OSError:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
def mine(value: str) -> dict[str, Any]:
|
||||
data = read(value)
|
||||
if data.get("owner") != actor():
|
||||
raise PermissionError("not your Phast app")
|
||||
return data
|
||||
|
||||
|
||||
def create(value: str, name: str = "") -> dict[str, Any]:
|
||||
value = slug(value)
|
||||
if path(value).exists():
|
||||
raise ValueError("already exists")
|
||||
# Provision auth before durable state so a failed OAuth mutation cannot leave
|
||||
# a half-created personal MCP behind.
|
||||
return write(
|
||||
value,
|
||||
{
|
||||
"schema_version": 1,
|
||||
"kind": "phast",
|
||||
"slug": value,
|
||||
"name": name.strip() or value,
|
||||
"owner": actor(),
|
||||
"host": value + ".phast.app",
|
||||
"surfaces": {"human": "/", "mcp": "/mcp"},
|
||||
"tools": {"installed": []},
|
||||
"connections": [],
|
||||
"prefab": {"version": 1, "home": {"mode": "grid", "edit": True, "widgets": []}},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def prefab(data: dict[str, Any]) -> PrefabApp:
|
||||
widgets = data["prefab"]["home"]["widgets"]
|
||||
with PrefabApp(title=data["name"], css_class="max-w-4xl mx-auto p-6", mode="light") as app:
|
||||
with Column(gap=5):
|
||||
with Row(justify="between", align="center"):
|
||||
Heading(data["name"], level=1)
|
||||
Badge(str(len(data["tools"]["installed"])) + " tools", variant="secondary")
|
||||
Text("https://" + data["host"] + "/mcp", code=True)
|
||||
if not widgets:
|
||||
with Card(css_class="p-5"):
|
||||
Heading("Blank home", level=2)
|
||||
Text("Add a provider or widget from Chat or my.phast.app.")
|
||||
for widget in widgets:
|
||||
with Card(css_class="p-5"):
|
||||
Heading(str(widget.get("title") or widget["id"]), level=2)
|
||||
Text(str(widget.get("description", "")))
|
||||
return app
|
||||
|
||||
|
||||
def human(data: dict[str, Any]) -> str:
|
||||
widgets = data["prefab"]["home"]["widgets"]
|
||||
cards = "".join(
|
||||
"<article><h2>"
|
||||
+ str(widget.get("title") or widget["id"])
|
||||
+ "</h2><p>"
|
||||
+ str(widget.get("description", ""))
|
||||
+ "</p></article>"
|
||||
for widget in widgets
|
||||
) or "<article><h2>Blank home</h2><p>Add a widget from Chat or my.phast.app.</p></article>"
|
||||
return (
|
||||
'<!doctype html><meta name="viewport" content="width=device-width">'
|
||||
+ "<title>"
|
||||
+ data["name"]
|
||||
+ " - Phast app</title>"
|
||||
+ "<style>body{margin:0;background:#f5f3ee;color:#121211;font:14px system-ui}"
|
||||
+ "main{max-width:1000px;margin:auto;padding:32px}header{display:flex;justify-content:space-between}"
|
||||
+ "b{font-size:28px}em{color:#ff4d00}h1{font-size:72px;letter-spacing:-5px;margin:80px 0 40px}"
|
||||
+ "section{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}"
|
||||
+ "article{border:1px solid #d5d1c8;padding:18px;min-height:150px}p{color:#77746c}"
|
||||
+ "@media(max-width:700px){section{grid-template-columns:1fr}}</style>"
|
||||
+ "<main><header><b>Phast app<em>.</em></b><code>"
|
||||
+ data["host"]
|
||||
+ "/mcp</code></header><h1>"
|
||||
+ data["name"]
|
||||
+ "</h1><section>"
|
||||
+ cards
|
||||
+ "</section></main>"
|
||||
)
|
||||
|
||||
|
||||
m = FastMCP(
|
||||
"My Phast app",
|
||||
version="0.2.0",
|
||||
instructions="Create and manage portable Phast app MCP tool belts and Prefab homes.",
|
||||
auth=auth("https://my.phast.app"),
|
||||
mask_error_details=True,
|
||||
)
|
||||
|
||||
|
||||
def _require_registry() -> AppRegistry:
|
||||
if registry is None:
|
||||
raise RegistryUnavailable(registry_blocker or "durable registry is offline")
|
||||
return registry
|
||||
|
||||
|
||||
def _require_owner_auth() -> None:
|
||||
# Deliberately not an environment-secret pseudo-auth scheme. Replace this
|
||||
# only with an accepted reusable FastMCP owner identity provider.
|
||||
raise PermissionError(AUTH_BLOCKER)
|
||||
|
||||
|
||||
@mcp.tool(annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True})
|
||||
@m.tool()
|
||||
def phast_status() -> dict:
|
||||
"""Get public Phast endpoints and control-plane readiness."""
|
||||
return {"ok": True, "actor": actor(), "version": "0.2.0"}
|
||||
|
||||
|
||||
@m.tool()
|
||||
def phasts_list() -> dict:
|
||||
current = actor()
|
||||
rows = []
|
||||
for p in DATA.glob("*/phast.json"):
|
||||
try:
|
||||
data = json.loads(p.read_text())
|
||||
except Exception:
|
||||
continue
|
||||
if data.get("owner") == current:
|
||||
rows.append(data)
|
||||
return {"phasts": sorted(rows, key=lambda item: item["slug"])}
|
||||
|
||||
|
||||
@m.tool()
|
||||
def phasts_get(slug: str) -> dict:
|
||||
return mine(slug)
|
||||
|
||||
|
||||
@m.tool()
|
||||
def phasts_create(slug: str, name: str = "") -> dict:
|
||||
data = create(slug, name)
|
||||
return {
|
||||
"ok": True,
|
||||
"service": "phast",
|
||||
"url": "https://phast.app",
|
||||
"owner_url": "https://my.phast.app",
|
||||
"mcp": "https://my.phast.app/mcp",
|
||||
"registry_ready": registry is not None,
|
||||
"registry_blocker": registry_blocker,
|
||||
"mutations_ready": False,
|
||||
"auth_blocker": AUTH_BLOCKER,
|
||||
"phast": data,
|
||||
"human_url": "https://" + data["host"] + "/",
|
||||
"mcp_url": "https://" + data["host"] + "/mcp",
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool(name="apps_list", annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True})
|
||||
def apps_list() -> dict:
|
||||
"""List the owner's registered Phast app manifests."""
|
||||
return {"apps": _require_registry().list()}
|
||||
@m.tool()
|
||||
def phasts_update(slug: str, name: str) -> dict:
|
||||
data = mine(slug)
|
||||
clean = name.strip()
|
||||
if not clean:
|
||||
raise ValueError("name is required")
|
||||
data["name"] = clean
|
||||
return write(slug, data)
|
||||
|
||||
|
||||
@mcp.tool(name="apps_get", annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True})
|
||||
def apps_get(slug: str) -> dict:
|
||||
"""Get one registered Phast app manifest by slug."""
|
||||
result = _require_registry().get(slug)
|
||||
if result is None:
|
||||
raise KeyError(f"unknown app: {slug}")
|
||||
return result
|
||||
@m.tool()
|
||||
def providers_search(query: str) -> dict:
|
||||
return {"query": query, "source": "oracle", "status": "adapter-next"}
|
||||
|
||||
|
||||
@mcp.tool(name="apps_create", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": False})
|
||||
def apps_create(slug: str, name: str, base_url: str, human_route: str = "/", mcp_route: str = "/mcp") -> dict:
|
||||
"""Create a Phast app manifest; disabled until owner auth is installed."""
|
||||
_require_owner_auth()
|
||||
return _require_registry().create(
|
||||
slug=slug, name=name, base_url=base_url, human_route=human_route, mcp_route=mcp_route
|
||||
@m.tool()
|
||||
def widgets_list(slug: str) -> dict:
|
||||
return {"widgets": list(mine(slug)["prefab"]["home"]["widgets"])}
|
||||
|
||||
|
||||
@m.tool()
|
||||
def widgets_add(slug: str, widget_id: str, title: str, description: str = "") -> dict:
|
||||
data = mine(slug)
|
||||
widgets = data["prefab"]["home"]["widgets"]
|
||||
if any(widget["id"] == widget_id for widget in widgets):
|
||||
raise ValueError("widget exists")
|
||||
widget = {"id": widget_id, "title": title, "kind": "prefab", "description": description}
|
||||
widgets.append(widget)
|
||||
write(slug, data)
|
||||
return widget
|
||||
|
||||
|
||||
@m.tool()
|
||||
def widgets_update(slug: str, widget_id: str, title: str = "", description: str = "") -> dict:
|
||||
data = mine(slug)
|
||||
for widget in data["prefab"]["home"]["widgets"]:
|
||||
if widget["id"] == widget_id:
|
||||
if title.strip():
|
||||
widget["title"] = title.strip()
|
||||
if description:
|
||||
widget["description"] = description
|
||||
write(slug, data)
|
||||
return widget
|
||||
raise KeyError("unknown widget")
|
||||
|
||||
|
||||
@m.tool()
|
||||
def widgets_remove(slug: str, widget_id: str) -> dict:
|
||||
data = mine(slug)
|
||||
widgets = data["prefab"]["home"]["widgets"]
|
||||
kept = [widget for widget in widgets if widget["id"] != widget_id]
|
||||
if len(kept) == len(widgets):
|
||||
raise KeyError("unknown widget")
|
||||
data["prefab"]["home"]["widgets"] = kept
|
||||
write(slug, data)
|
||||
return {"removed": widget_id}
|
||||
|
||||
|
||||
@m.tool(app=True)
|
||||
def phast_preview(slug: str) -> PrefabApp:
|
||||
return prefab(mine(slug))
|
||||
|
||||
|
||||
mgmt = m.http_app(path="/mcp", transport="streamable-http", stateless_http=True)
|
||||
cache: dict[str, Any] = {}
|
||||
started: dict[str, Any] = {}
|
||||
locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
|
||||
def inst(value: str):
|
||||
value = slug(value)
|
||||
if value in cache:
|
||||
return cache[value]
|
||||
data = read(value)
|
||||
personal = FastMCP(
|
||||
data["name"],
|
||||
version="0.2.0",
|
||||
instructions="Portable personal Phast app MCP.",
|
||||
auth=auth("https://my.phast.app", "https://" + data["host"]),
|
||||
mask_error_details=True,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool(name="apps_update", annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True})
|
||||
def apps_update(slug: str, name: str | None = None, base_url: str | None = None,
|
||||
human_route: str | None = None, mcp_route: str | None = None) -> dict:
|
||||
"""Update a Phast app manifest; disabled until owner auth is installed."""
|
||||
_require_owner_auth()
|
||||
return _require_registry().update(
|
||||
slug, name=name, base_url=base_url, human_route=human_route, mcp_route=mcp_route
|
||||
)
|
||||
|
||||
|
||||
def control_app() -> PrefabApp:
|
||||
apps = registry.list() if registry is not None else []
|
||||
with PrefabApp(title="My Phast", css_class="max-w-4xl mx-auto p-6", mode="light") as result:
|
||||
with Column(gap=5):
|
||||
with Row(gap=3, justify="between", align="center"):
|
||||
with Column(gap=1):
|
||||
Text("PHAST · OWNER CONTROL PLANE", code=True)
|
||||
Heading("My Phast", level=1)
|
||||
Badge("read-only" if registry is not None else "setup required", variant="warning")
|
||||
Text("Your apps share one boring convention: a human surface at / and an agent surface at /mcp.")
|
||||
with Card(css_class="p-5"):
|
||||
with Column(gap=3):
|
||||
with Row(gap=2, justify="between", align="center"):
|
||||
Heading("Apps", level=2)
|
||||
Badge(str(len(apps)), variant="secondary")
|
||||
if apps:
|
||||
for item in apps:
|
||||
with Card(css_class="p-4"):
|
||||
with Row(gap=3, justify="between", align="center"):
|
||||
with Column(gap=1):
|
||||
Heading(item["name"], level=3)
|
||||
Text(item["base_url"], code=True)
|
||||
Badge(item["runtime"], variant="outline")
|
||||
Text(f"human {item['human']['route']} · mcp {item['mcp']['route']}")
|
||||
else:
|
||||
Text("No durable app registry is available yet.")
|
||||
with Card(css_class="p-5"):
|
||||
with Column(gap=3):
|
||||
Heading("Control-plane readiness", level=2)
|
||||
Text(registry_blocker or "Durable SQLite registry is mounted and readable.")
|
||||
Text(AUTH_BLOCKER)
|
||||
Button("Create app", disabled=True)
|
||||
Separator()
|
||||
Text("Agent interface", bold=True)
|
||||
Code("https://my.phast.app/mcp", language="text")
|
||||
Text("Mutations stay disabled until both a Fly volume and accepted owner authentication exist.")
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool(name="my_phast_app", app=True, annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True})
|
||||
def my_phast_app() -> PrefabApp:
|
||||
"""Open the read-only My Phast Prefab control surface."""
|
||||
return control_app()
|
||||
|
||||
|
||||
@mcp.custom_route("/", methods=["GET"])
|
||||
async def homepage(request: Request):
|
||||
host = request.headers.get("host", "").split(":", 1)[0].lower()
|
||||
if host == OWNER_HOST:
|
||||
return HTMLResponse(control_app().html(renderer_mode="bundled"))
|
||||
return HTMLResponse(Path(__file__).with_name("index.html").read_text())
|
||||
|
||||
|
||||
@mcp.custom_route("/healthz", methods=["GET"])
|
||||
async def health(_: Request):
|
||||
return JSONResponse(
|
||||
{
|
||||
@personal.tool()
|
||||
def phast_status() -> dict:
|
||||
owned = mine(value)
|
||||
return {
|
||||
"ok": True,
|
||||
"service": "phast",
|
||||
"source_sha": os.environ.get("SOURCE_SHA", "local"),
|
||||
"control_plane": {
|
||||
"registry_ready": registry is not None,
|
||||
"mutations_ready": False,
|
||||
"blockers": [value for value in (registry_blocker, AUTH_BLOCKER) if value],
|
||||
},
|
||||
"slug": value,
|
||||
"owner": owned["owner"],
|
||||
"tools": len(owned["tools"]["installed"]),
|
||||
"widgets": len(owned["prefab"]["home"]["widgets"]),
|
||||
}
|
||||
)
|
||||
|
||||
@personal.tool()
|
||||
def phast_manifest() -> dict:
|
||||
return mine(value)
|
||||
|
||||
@personal.tool(app=True)
|
||||
def home_app() -> PrefabApp:
|
||||
return prefab(mine(value))
|
||||
|
||||
cache[value] = personal.http_app(path="/mcp", transport="streamable-http", stateless_http=True)
|
||||
return cache[value]
|
||||
|
||||
|
||||
app = mcp.http_app(
|
||||
path="/mcp",
|
||||
transport="streamable-http",
|
||||
stateless_http=True,
|
||||
allowed_hosts=["phast.app", "www.phast.app", "my.phast.app", "phast-app.fly.dev", "localhost", "testserver"],
|
||||
)
|
||||
async def ready_inst(value: str):
|
||||
value = slug(value)
|
||||
app = inst(value)
|
||||
if value in started:
|
||||
return app
|
||||
lock = locks.setdefault(value, asyncio.Lock())
|
||||
async with lock:
|
||||
if value not in started:
|
||||
cm = app.router.lifespan_context(app)
|
||||
await cm.__aenter__()
|
||||
started[value] = cm
|
||||
return app
|
||||
|
||||
|
||||
class Router:
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope.get("type") == "lifespan":
|
||||
try:
|
||||
await mgmt(scope, receive, send)
|
||||
finally:
|
||||
for cm in reversed(list(started.values())):
|
||||
try:
|
||||
await cm.__aexit__(None, None, None)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
host = dict(scope.get("headers", [])).get(b"host", b"").decode().split(":")[0].lower()
|
||||
request_path = scope.get("path", "/")
|
||||
method = scope.get("method", "GET")
|
||||
|
||||
if request_path == "/healthz":
|
||||
await JSONResponse({"ok": True, "service": "phast", "source_sha": os.getenv("SOURCE_SHA", "local")})(scope, receive, send)
|
||||
return
|
||||
|
||||
page = PAGES.get(host)
|
||||
if page and request_path == "/" and method in {"GET", "HEAD"}:
|
||||
await FileResponse(page, media_type="text/html", headers={"Cache-Control": "public, max-age=60"})(scope, receive, send)
|
||||
return
|
||||
|
||||
if host == "phast.app":
|
||||
await JSONResponse({"error": "not found"}, status_code=404)(scope, receive, send)
|
||||
return
|
||||
|
||||
if host == "my.phast.app":
|
||||
await mgmt(scope, receive, send)
|
||||
return
|
||||
|
||||
if host.endswith(".phast.app"):
|
||||
value = host[:-10]
|
||||
try:
|
||||
data = read(value)
|
||||
except Exception:
|
||||
await JSONResponse({"error": "unknown Phast app"}, status_code=404)(scope, receive, send)
|
||||
return
|
||||
|
||||
if request_path == "/" and method in {"GET", "HEAD"}:
|
||||
body = human(data)
|
||||
if method == "HEAD":
|
||||
await Response(content=b"", media_type="text/html", status_code=200)(scope, receive, send)
|
||||
else:
|
||||
await HTMLResponse(body)(scope, receive, send)
|
||||
return
|
||||
|
||||
personal = await ready_inst(value)
|
||||
await personal(scope, receive, send)
|
||||
return
|
||||
|
||||
await JSONResponse({"error": "unknown host"}, status_code=404)(scope, receive, send)
|
||||
|
||||
|
||||
app = Router()
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8080")))
|
||||
uvicorn.run(app, host=os.getenv("PHAST_HOST", "::"), port=int(os.getenv("PORT", "8081")))
|
||||
Reference in new issue
Block a user