Files
ARR-2.0-0918/arr_web/preview.py

92 lines
3.5 KiB
Python

"""Loopback-only, empty-data UI preview. Never used by the production runtime."""
from __future__ import annotations
import argparse
from dataclasses import replace
from typing import Mapping
from urllib.parse import urlsplit
from arr_web.app import PortalApplication, SessionLedger
from arr_web.contracts import PortalError, Response, failure
from arr_web.repository import UnavailablePortalRepository
from arr_web.server import serve
class EmptyPreviewRepository(UnavailablePortalRepository):
def list_jobs(self, month_key, limit=50, offset=0):
return [], 0
def list_monthly_runs(self, month_key, limit=50, offset=0):
return [], 0
def list_history_month_counts(self):
return []
def list_months(self):
return []
class ReadOnlyPreview:
"""Render the actual portal with unavailable services and no external state."""
def __init__(self) -> None:
self._sessions = SessionLedger()
self._app = PortalApplication(
repository=EmptyPreviewRepository(), sessions=self._sessions
)
def handle(
self, method: str, target: str, headers: Mapping[str, str],
body: bytes = b"", client_id: str = "direct",
) -> Response:
if method != "GET":
return Response.json(405, failure(PortalError(
"PREVIEW_READ_ONLY", "本地预览仅供查看,不能提交业务操作。", 405
)), headers={"Allow": "GET"})
path = urlsplit(target).path
if path == "/assets/preview.css":
return Response(200, (
".local-preview-banner{margin:0 0 20px;padding:12px 16px;"
"border:1px solid var(--line);border-radius:12px;"
"background:var(--blue-soft);color:var(--ink);font-size:13px}"
).encode(), "text/css; charset=utf-8", {"Cache-Control": "no-store"})
# Internal, transient identity belongs only to this empty preview instance.
# No browser cookie, credentials, runtime configuration or real service is used.
session_id, _ = self._sessions.issue("")
request_headers = {k: v for k, v in headers.items() if k.lower() != "cookie"}
request_headers["Cookie"] = f"arr_session={session_id}"
try:
response = self._app.handle(method, target, request_headers, body, client_id)
finally:
self._sessions.revoke(session_id)
if path in {"/", "/index.html"} and response.status == 200:
html = response.body.decode("utf-8").replace(
"<title>ARR Report</title>", "<title>ARR Report · 本地只读预览</title>"
).replace(
"</head>", '<link rel="stylesheet" href="/assets/preview.css" /></head>'
).replace(
'<main class="shell page" id="main-content">',
'<main class="shell page" id="main-content">'
'<aside class="local-preview-banner" role="note">'
'<strong>本地只读预览</strong> · 仅展示页面,未连接业务数据;'
'上传和自动下载均未启用。</aside>',
)
response = replace(response, body=html.encode("utf-8"))
return response
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", type=int, default=8872)
args = parser.parse_args()
print(f"Read-only preview: http://127.0.0.1:{args.port}/", flush=True)
serve(ReadOnlyPreview(), "127.0.0.1", args.port)
if __name__ == "__main__":
main()