Portal server mode for Dokploy: Dockerfile, PORTAL_URLS, PORTAL_SERVER
The results Portal in a Linux container (python 3.13-slim + ODBC Driver 18). PORTAL_SERVER=1: no Start / Stop (the bot runs on the Windows PC). PORTAL_URLS: allowed hosts / origins, Secure cookie when all https. RESULTS_DB_CONN (a SQL login) goes in Dokploy's environment, never here. 71 test files, 1222 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce9bdbd974
commit
c049cece23
9 files changed
+163
-13
No files matched your search
+30
-8
@@ -58,7 +58,7 @@ from .users import (Accounts, CAN_ADMIN, CAN_RUN, # noqa: E402
|
||||
IDLE_MINUTES, LOCK_AFTER, LOCK_MINUTES, MANAGES,
|
||||
Sessions, SqlUserStore, UserError)
|
||||
from .bot_runner import (AlreadyRunning, BotRunner, # noqa: E402
|
||||
NotRunning, StopFailed)
|
||||
NoRunner, NotRunning, StopFailed)
|
||||
|
||||
CATEGORIES = ("Pass", "Fail", "Review")
|
||||
DB_DOWN = "Results database not reachable"
|
||||
@@ -261,13 +261,32 @@ def _reasons(rows, top=6) -> list:
|
||||
for (cat, text), n in ordered[:top]]
|
||||
|
||||
|
||||
def portal_settings(urls: str, server: bool) -> dict:
|
||||
"""create_app's host / origin / cookie / Start settings from
|
||||
PORTAL_URLS (comma separated, as people type them in the browser) and
|
||||
PORTAL_SERVER. No URLs: this PC only. A server Portal never starts the
|
||||
bot; an all-https Portal sends the cookie only over https."""
|
||||
parts = [urlsplit(u.strip()) for u in (urls or "").split(",")
|
||||
if u.strip()]
|
||||
if not parts:
|
||||
return {"allowed_hosts": list(ALLOWED_HOSTS),
|
||||
"allowed_origins": list(ALLOWED_ORIGINS),
|
||||
"secure_cookie": False, "can_start": not server}
|
||||
return {"allowed_hosts": [u.hostname for u in parts],
|
||||
"allowed_origins": [f"{u.scheme}://{u.netloc}" for u in parts],
|
||||
"secure_cookie": all(u.scheme == "https" for u in parts),
|
||||
"can_start": not server}
|
||||
|
||||
|
||||
def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None,
|
||||
allowed_origins=ALLOWED_ORIGINS, logs_dir=None,
|
||||
screenshots_root=None, users_store=None,
|
||||
sessions=None) -> FastAPI:
|
||||
sessions=None, can_start=True,
|
||||
secure_cookie=False) -> FastAPI:
|
||||
app = FastAPI(title="eCW Charge Validation — Results",
|
||||
docs_url=None, redoc_url=None, openapi_url=None)
|
||||
runner = runner if runner is not None else BotRunner()
|
||||
if runner is None:
|
||||
runner = BotRunner() if can_start else NoRunner()
|
||||
logs_dir = logs_dir if logs_dir is not None else cfg.LOGS
|
||||
screenshots_root = (screenshots_root if screenshots_root is not None
|
||||
else cfg.ROOT / "Screenshots")
|
||||
@@ -379,7 +398,8 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None,
|
||||
"mode": "LIVE-WRITE" if cfg.SAVE_CORRECTIONS else "VERIFY",
|
||||
"run_token": app.state.run_token, "crumbs": crumbs,
|
||||
"categories": CATEGORIES, "user": user,
|
||||
"can_run": bool(user and user["role"] in CAN_RUN),
|
||||
"can_run": bool(can_start and user
|
||||
and user["role"] in CAN_RUN),
|
||||
"is_admin": bool(user and user["role"] in CAN_ADMIN),
|
||||
**context})
|
||||
|
||||
@@ -521,7 +541,7 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None,
|
||||
@app.post("/api/bot/start")
|
||||
def bot_start(request: Request):
|
||||
_check_writer(request)
|
||||
user = _need(request, CAN_RUN)
|
||||
user = _need(request, CAN_RUN if can_start else ())
|
||||
try:
|
||||
return run_status.summarize(
|
||||
runner.start(started_by=user["username"]))
|
||||
@@ -531,7 +551,7 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None,
|
||||
@app.post("/api/bot/stop")
|
||||
def bot_stop(request: Request):
|
||||
_check_writer(request)
|
||||
user = _need(request, CAN_RUN)
|
||||
user = _need(request, CAN_RUN if can_start else ())
|
||||
try:
|
||||
return run_status.summarize(
|
||||
runner.stop(stopped_by=user["username"]))
|
||||
@@ -576,7 +596,8 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None,
|
||||
else _safe_next(next))
|
||||
response = RedirectResponse(target, status_code=303)
|
||||
response.set_cookie(COOKIE, sessions.new(user), httponly=True,
|
||||
samesite="strict", path="/")
|
||||
samesite="strict", path="/",
|
||||
secure=secure_cookie)
|
||||
return response
|
||||
|
||||
@app.post("/signout")
|
||||
@@ -709,4 +730,5 @@ def _safe_next(target) -> str:
|
||||
|
||||
|
||||
app = create_app(SqlResultsRepo(cfg.RESULTS_DB_CONN),
|
||||
users_store=SqlUserStore(cfg.RESULTS_DB_CONN))
|
||||
users_store=SqlUserStore(cfg.RESULTS_DB_CONN),
|
||||
**portal_settings(cfg.PORTAL_URLS, cfg.PORTAL_SERVER))
|
||||
@@ -50,6 +50,23 @@ class StopFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoRunner:
|
||||
"""The Portal on a server (Dokploy, 2026-10-27): the bot runs only on
|
||||
the Windows PC, so nothing here ever starts, stops or looks for it."""
|
||||
|
||||
def start(self, started_by: str = "") -> dict:
|
||||
raise NotRunning("Runs start on the bot PC, not from this server")
|
||||
|
||||
def stop(self, stopped_by: str = "") -> dict:
|
||||
raise NotRunning("Runs start on the bot PC, not from this server")
|
||||
|
||||
def status(self) -> dict:
|
||||
return {"running": False, "starting": False, "pid": None,
|
||||
"started_at": None, "ended_at": None, "exit_code": None,
|
||||
"stopped_by_operator": False, "source": None,
|
||||
"started_by": None, "log_path": None, "out_path": None}
|
||||
|
||||
|
||||
class BotRunner:
|
||||
# How long stop() waits for a killed process to be gone.
|
||||
stop_wait = 10.0
|
||||
|
||||
Reference in new issue
Block a user