92 lines
3.5 KiB
Python
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()
|