2025-01-21 19:20:50 +01:00
|
|
|
import argparse
|
2025-01-24 00:09:28 +01:00
|
|
|
import os
|
2025-01-18 14:26:34 +01:00
|
|
|
import sys
|
2025-01-22 23:47:28 +01:00
|
|
|
import traceback
|
|
|
|
|
from pathlib import Path
|
2025-01-21 19:20:50 +01:00
|
|
|
|
2025-02-12 21:35:51 +01:00
|
|
|
import psutil
|
2024-12-15 14:40:03 +01:00
|
|
|
import uvicorn
|
2025-12-30 22:08:21 +01:00
|
|
|
from fasthtml.common import Base, FileResponse, JSONResponse
|
2025-06-10 22:00:28 +02:00
|
|
|
from loguru import logger
|
2025-01-22 23:47:28 +01:00
|
|
|
from monsterui.core import FastHTML, Theme
|
2025-12-30 22:08:21 +01:00
|
|
|
from starlette.middleware import Middleware
|
|
|
|
|
from starlette.requests import Request
|
2024-12-15 14:40:03 +01:00
|
|
|
|
2026-02-22 14:12:42 +01:00
|
|
|
from akkudoktoreos.core.coreabc import get_config
|
2025-10-28 02:50:31 +01:00
|
|
|
from akkudoktoreos.core.logabc import LOGGING_LEVELS
|
2025-12-30 22:08:21 +01:00
|
|
|
from akkudoktoreos.core.logging import logging_track_config
|
2025-10-28 02:50:31 +01:00
|
|
|
from akkudoktoreos.core.version import __version__
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
# Pages
|
2025-12-30 22:08:21 +01:00
|
|
|
from akkudoktoreos.server.dash.about import About
|
2025-03-27 21:53:01 +01:00
|
|
|
from akkudoktoreos.server.dash.admin import Admin
|
2025-12-30 22:08:21 +01:00
|
|
|
|
|
|
|
|
# helpers
|
2025-03-27 21:53:01 +01:00
|
|
|
from akkudoktoreos.server.dash.bokeh import BokehJS
|
|
|
|
|
from akkudoktoreos.server.dash.components import Page
|
2026-08-23 02:26:16 +02:00
|
|
|
from akkudoktoreos.server.dash.configuration import Configuration, config_options
|
2025-12-30 22:08:21 +01:00
|
|
|
from akkudoktoreos.server.dash.context import (
|
|
|
|
|
IngressMiddleware,
|
|
|
|
|
safe_asset_path,
|
|
|
|
|
)
|
2025-01-22 23:47:28 +01:00
|
|
|
from akkudoktoreos.server.dash.footer import Footer
|
2025-10-28 02:50:31 +01:00
|
|
|
from akkudoktoreos.server.dash.plan import Plan
|
|
|
|
|
from akkudoktoreos.server.dash.prediction import Prediction
|
2025-12-30 22:08:21 +01:00
|
|
|
from akkudoktoreos.server.server import (
|
|
|
|
|
drop_root_privileges,
|
|
|
|
|
get_default_host,
|
|
|
|
|
wait_for_port_free,
|
|
|
|
|
)
|
2025-10-28 02:50:31 +01:00
|
|
|
from akkudoktoreos.utils.stringutil import str2bool
|
2025-01-22 23:47:28 +01:00
|
|
|
|
2026-07-23 15:11:18 +02:00
|
|
|
# Config is the EOS configuration
|
|
|
|
|
# Config uses sysv args for initialisation
|
|
|
|
|
# sysv host and port is set for EOSdash but expected to be set for EOS
|
2026-02-22 14:12:42 +01:00
|
|
|
config_eos = get_config(init=True)
|
2024-12-15 14:40:03 +01:00
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
|
|
|
|
|
# ------------------------------------
|
|
|
|
|
# Logging configuration at import time
|
|
|
|
|
# ------------------------------------
|
|
|
|
|
|
|
|
|
|
logger.remove()
|
2025-12-30 22:08:21 +01:00
|
|
|
logging_track_config(config_eos, "logging", None, None)
|
|
|
|
|
config_eos.track_nested_value("/logging", logging_track_config)
|
2025-10-28 02:50:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------
|
|
|
|
|
# Safe argparse at import time
|
|
|
|
|
# ----------------------------
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Start EOSdash server.")
|
|
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--host",
|
|
|
|
|
type=str,
|
|
|
|
|
help="Host for the EOSdash server (default: value from config)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--port",
|
|
|
|
|
type=int,
|
|
|
|
|
help="Port for the EOSdash server (default: value from config)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--eos-host",
|
|
|
|
|
type=str,
|
|
|
|
|
help="Host of the EOS server (default: value from config)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--eos-port",
|
|
|
|
|
type=int,
|
|
|
|
|
help="Port of the EOS server (default: value from config)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--log_level",
|
|
|
|
|
type=str,
|
|
|
|
|
default="INFO",
|
|
|
|
|
help='Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "INFO")',
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--access_log",
|
|
|
|
|
type=str2bool,
|
|
|
|
|
default=False,
|
|
|
|
|
help="Enable or disable access logging. Options: True or False (default: False)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--reload",
|
|
|
|
|
type=str2bool,
|
|
|
|
|
default=False,
|
|
|
|
|
help="Enable or disable auto-reload. Useful for development. Options: True or False (default: False)",
|
|
|
|
|
)
|
2025-12-30 22:08:21 +01:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--run_as_user",
|
|
|
|
|
type=str,
|
|
|
|
|
help="The unprivileged user account the EOSdash server shall run if started in root-level.",
|
|
|
|
|
)
|
|
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
|
|
|
|
|
# Command line arguments
|
|
|
|
|
args: argparse.Namespace
|
|
|
|
|
args_unknown: list[str]
|
|
|
|
|
args, args_unknown = parser.parse_known_args()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# -----------------------------
|
|
|
|
|
# Prepare config at import time
|
|
|
|
|
# -----------------------------
|
|
|
|
|
|
|
|
|
|
# Set EOS config to actual environment variable & config file content
|
|
|
|
|
config_eos.reset_settings()
|
|
|
|
|
|
|
|
|
|
# Setup parameters from args, config_eos and default
|
|
|
|
|
# Remember parameters in config
|
|
|
|
|
config_eosdash = {}
|
|
|
|
|
|
2026-02-28 11:31:51 +01:00
|
|
|
# Setup EOSdash logging level - first to have the other logging messages logged
|
2025-10-28 02:50:31 +01:00
|
|
|
# - log level
|
|
|
|
|
if args and args.log_level is not None:
|
|
|
|
|
config_eosdash["log_level"] = args.log_level.upper()
|
|
|
|
|
else:
|
|
|
|
|
config_eosdash["log_level"] = "info"
|
|
|
|
|
# Ensure log_level from command line is in config settings
|
|
|
|
|
if config_eosdash["log_level"] in LOGGING_LEVELS:
|
|
|
|
|
# Setup console logging level using nested value
|
2025-12-30 22:08:21 +01:00
|
|
|
# - triggers logging configuration by logging_track_config
|
2025-10-28 02:50:31 +01:00
|
|
|
config_eos.set_nested_value("logging/console_level", config_eosdash["log_level"])
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"logging/console_level configuration set by argument to {config_eosdash['log_level']}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Setup EOS server host
|
|
|
|
|
if args and args.eos_host:
|
|
|
|
|
config_eosdash["eos_host"] = args.eos_host
|
|
|
|
|
elif config_eos.server.host:
|
|
|
|
|
config_eosdash["eos_host"] = str(config_eos.server.host)
|
|
|
|
|
else:
|
|
|
|
|
config_eosdash["eos_host"] = get_default_host()
|
|
|
|
|
|
|
|
|
|
# Setup EOS server port
|
|
|
|
|
if args and args.eos_port:
|
|
|
|
|
config_eosdash["eos_port"] = args.eos_port
|
|
|
|
|
elif config_eos.server.port:
|
|
|
|
|
config_eosdash["eos_port"] = config_eos.server.port
|
|
|
|
|
else:
|
|
|
|
|
config_eosdash["eos_port"] = 8503
|
|
|
|
|
|
|
|
|
|
# - EOSdash host
|
|
|
|
|
if args and args.host:
|
|
|
|
|
config_eosdash["eosdash_host"] = args.host
|
|
|
|
|
elif config_eos.server.eosdash_host:
|
|
|
|
|
config_eosdash["eosdash_host"] = str(config_eos.server.eosdash_host)
|
|
|
|
|
else:
|
|
|
|
|
config_eosdash["eosdash_host"] = get_default_host()
|
|
|
|
|
|
|
|
|
|
# - EOS port
|
|
|
|
|
if args and args.port:
|
|
|
|
|
config_eosdash["eosdash_port"] = args.port
|
|
|
|
|
elif config_eos.server.eosdash_port:
|
|
|
|
|
config_eosdash["eosdash_port"] = config_eos.server.eosdash_port
|
|
|
|
|
else:
|
|
|
|
|
config_eosdash["eosdash_port"] = 8504
|
|
|
|
|
|
|
|
|
|
# - access log
|
|
|
|
|
if args and args.access_log:
|
|
|
|
|
config_eosdash["access_log"] = args.access_log
|
|
|
|
|
else:
|
|
|
|
|
config_eosdash["access_log"] = False
|
|
|
|
|
|
|
|
|
|
# - reload
|
|
|
|
|
if args is None and args.reload is None:
|
|
|
|
|
config_eosdash["reload"] = False
|
|
|
|
|
else:
|
|
|
|
|
config_eosdash["reload"] = args.reload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------
|
|
|
|
|
# Prepare FastHTML app
|
|
|
|
|
# ---------------------
|
|
|
|
|
|
2025-01-22 23:47:28 +01:00
|
|
|
# The favicon for EOSdash
|
|
|
|
|
favicon_filepath = Path(__file__).parent.joinpath("dash/assets/favicon/favicon.ico")
|
|
|
|
|
if not favicon_filepath.exists():
|
|
|
|
|
raise ValueError(f"Does not exist {favicon_filepath}")
|
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
|
2025-03-27 21:53:01 +01:00
|
|
|
# Add Bokeh headers
|
2025-10-28 02:50:31 +01:00
|
|
|
# Get frankenui and tailwind headers via CDN using Theme.green.headers()
|
2025-01-22 23:47:28 +01:00
|
|
|
hdrs = (
|
2025-10-28 02:50:31 +01:00
|
|
|
*BokehJS,
|
2025-01-22 23:47:28 +01:00
|
|
|
Theme.green.headers(highlightjs=True),
|
|
|
|
|
)
|
2025-01-18 14:07:08 +01:00
|
|
|
|
2025-01-22 23:47:28 +01:00
|
|
|
# The EOSdash application
|
|
|
|
|
app: FastHTML = FastHTML(
|
2025-12-30 22:08:21 +01:00
|
|
|
title="EOSdash", # Default page title
|
|
|
|
|
hdrs=hdrs, # Additional FT elements to add to <HEAD>
|
|
|
|
|
# htmx=True, # Include HTMX header?
|
|
|
|
|
middleware=[Middleware(IngressMiddleware)],
|
|
|
|
|
secret_key=os.getenv("EOS_SERVER__EOSDASH_SESSKEY"), # Signing key for sessions
|
2025-01-19 13:00:53 +01:00
|
|
|
)
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
|
2025-01-22 23:47:28 +01:00
|
|
|
def eos_server() -> tuple[str, int]:
|
|
|
|
|
"""Retrieves the EOS server host and port configuration.
|
|
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
Takes values from `config_eos.server` or default.
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
tuple[str, int]: A tuple containing:
|
|
|
|
|
- `eos_host` (str): The EOS server hostname or IP.
|
|
|
|
|
- `eos_port` (int): The EOS server port.
|
|
|
|
|
"""
|
2025-10-28 02:50:31 +01:00
|
|
|
return config_eosdash["eos_host"], config_eosdash["eos_port"]
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
|
2025-12-30 22:08:21 +01:00
|
|
|
# -------------------------------------------------------------------
|
|
|
|
|
# Routes
|
|
|
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
2025-01-22 23:47:28 +01:00
|
|
|
@app.get("/favicon.ico")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash_favicon(request: Request): # type: ignore
|
|
|
|
|
"""Get the EOSdash favicon.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
FileResponse: The favicon file.
|
|
|
|
|
"""
|
2025-01-22 23:47:28 +01:00
|
|
|
return FileResponse(path=favicon_filepath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash(request: Request): # type: ignore
|
|
|
|
|
"""Serve the main EOSdash page with navigation links.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Page: The main dashboard page with navigation links and footer.
|
|
|
|
|
"""
|
2025-12-30 22:08:21 +01:00
|
|
|
root_path: str = request.scope.get("root_path", "")
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
Base(href=f"{root_path}/") if root_path else None,
|
|
|
|
|
Page(
|
|
|
|
|
None,
|
|
|
|
|
{
|
|
|
|
|
"Plan": "/eosdash/plan",
|
|
|
|
|
"Prediction": "/eosdash/prediction",
|
|
|
|
|
"Config": "/eosdash/configuration",
|
|
|
|
|
"Admin": "/eosdash/admin",
|
|
|
|
|
"About": "/eosdash/about",
|
|
|
|
|
},
|
|
|
|
|
About(),
|
|
|
|
|
Footer(*eos_server()),
|
|
|
|
|
"/eosdash/footer",
|
|
|
|
|
),
|
2025-01-22 23:47:28 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/eosdash/footer")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash_footer(request: Request): # type: ignore
|
|
|
|
|
"""Serve the EOSdash Footer information.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Footer: The Footer component.
|
|
|
|
|
"""
|
|
|
|
|
return Footer(*eos_server())
|
|
|
|
|
|
|
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
@app.get("/eosdash/about")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash_about(request: Request): # type: ignore
|
|
|
|
|
"""Serve the EOSdash About page.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
Returns:
|
2025-10-28 02:50:31 +01:00
|
|
|
About: The About page component.
|
2025-01-22 23:47:28 +01:00
|
|
|
"""
|
2025-10-28 02:50:31 +01:00
|
|
|
return About()
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
|
2025-03-27 21:53:01 +01:00
|
|
|
@app.get("/eosdash/admin")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash_admin(request: Request): # type: ignore
|
|
|
|
|
"""Serve the EOSdash Admin page.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
2025-03-27 21:53:01 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Admin: The Admin page component.
|
|
|
|
|
"""
|
|
|
|
|
return Admin(*eos_server())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/eosdash/admin")
|
2025-12-30 22:08:21 +01:00
|
|
|
def post_eosdash_admin(request: Request, data: dict): # type: ignore
|
2025-10-28 02:50:31 +01:00
|
|
|
"""Provide control data to the Admin page.
|
|
|
|
|
|
2025-12-30 22:08:21 +01:00
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
data (dict): User-submitted data from the Admin page.
|
2025-10-28 02:50:31 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Admin: The Admin page component.
|
|
|
|
|
"""
|
2025-03-27 21:53:01 +01:00
|
|
|
return Admin(*eos_server(), data)
|
|
|
|
|
|
|
|
|
|
|
2026-08-23 02:26:16 +02:00
|
|
|
@app.get("/eosdash/configuration/options/{options_source:path}")
|
|
|
|
|
def get_eosdash_configuration_options( # type: ignore
|
|
|
|
|
request: Request,
|
|
|
|
|
options_source: str,
|
|
|
|
|
search: str = "",
|
|
|
|
|
select_id: str = "",
|
|
|
|
|
name: str = "",
|
|
|
|
|
current: str = "",
|
|
|
|
|
):
|
|
|
|
|
"""Serve a filtered <select> fragment for a select_lazy config field."""
|
|
|
|
|
return config_options(options_source, search, select_id, name, current)
|
|
|
|
|
|
|
|
|
|
|
2025-01-22 23:47:28 +01:00
|
|
|
@app.get("/eosdash/configuration")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash_configuration(request: Request): # type: ignore
|
|
|
|
|
"""Serve the EOSdash Configuration page.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Configuration: The Configuration page component.
|
|
|
|
|
"""
|
|
|
|
|
return Configuration(*eos_server())
|
|
|
|
|
|
|
|
|
|
|
2025-03-27 21:53:01 +01:00
|
|
|
@app.put("/eosdash/configuration")
|
2025-12-30 22:08:21 +01:00
|
|
|
def put_eosdash_configuration(request: Request, data: dict): # type: ignore
|
|
|
|
|
"""Update a configuration key/value pair.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
data (dict): Dictionary containing 'key' and 'value' to trigger configuration update.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Configuration: The Configuration page component with updated configuration.
|
|
|
|
|
"""
|
|
|
|
|
return Configuration(*eos_server(), data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/eosdash/configuration")
|
|
|
|
|
def post_eosdash_configuration(request: Request, data: dict): # type: ignore
|
|
|
|
|
"""Provide control data to the configuration page.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
data (dict): User-submitted data from the configuration page.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Configuration: The Configuration page component with updated configuration.
|
|
|
|
|
"""
|
|
|
|
|
return Configuration(*eos_server(), data)
|
2025-03-27 21:53:01 +01:00
|
|
|
|
|
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
@app.get("/eosdash/plan")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash_plan(request: Request, data: dict): # type: ignore
|
|
|
|
|
"""Serve the EOSdash Plan page.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
data (dict): Optional query data.
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
Returns:
|
2025-10-28 02:50:31 +01:00
|
|
|
Plan: The Plan page component.
|
2025-01-22 23:47:28 +01:00
|
|
|
"""
|
2025-10-28 02:50:31 +01:00
|
|
|
return Plan(*eos_server(), data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/eosdash/plan")
|
2025-12-30 22:08:21 +01:00
|
|
|
def post_eosdash_plan(request: Request, data: dict): # type: ignore
|
2025-10-28 02:50:31 +01:00
|
|
|
"""Provide control data to the Plan page.
|
|
|
|
|
|
2025-12-30 22:08:21 +01:00
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
data (dict): User-submitted data from the Plan page.
|
2025-10-28 02:50:31 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Plan: The Plan page component.
|
|
|
|
|
"""
|
|
|
|
|
return Plan(*eos_server(), data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/eosdash/prediction")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash_prediction(request: Request, data: dict): # type: ignore
|
|
|
|
|
"""Serve the EOSdash Prediction page.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
data (dict): Optional query data.
|
2025-10-28 02:50:31 +01:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Prediction: The Prediction page component.
|
|
|
|
|
"""
|
|
|
|
|
return Prediction(*eos_server(), data)
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
|
2025-02-12 21:35:51 +01:00
|
|
|
@app.get("/eosdash/health")
|
2025-12-30 22:08:21 +01:00
|
|
|
def get_eosdash_health(request: Request): # type: ignore
|
|
|
|
|
"""Health check endpoint to verify the EOSdash server is alive.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
JSONResponse: Server status including PID and version.
|
|
|
|
|
"""
|
2025-02-12 21:35:51 +01:00
|
|
|
return JSONResponse(
|
|
|
|
|
{
|
|
|
|
|
"status": "alive",
|
|
|
|
|
"pid": psutil.Process().pid,
|
2025-10-28 02:50:31 +01:00
|
|
|
"version": __version__,
|
2025-02-12 21:35:51 +01:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-12-30 22:08:21 +01:00
|
|
|
@app.get("/eosdash/assets/{filepath:path}")
|
|
|
|
|
def get_eosdash_assets(request: Request, filepath: str): # type: ignore
|
|
|
|
|
"""Serve static assets for EOSdash safely.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request (Request): The incoming FastHTML request.
|
|
|
|
|
filepath (str): Relative path of the asset under dash/assets/.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
FileResponse: The requested asset file if it exists.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
404: If the file does not exist.
|
|
|
|
|
403: If the file path is forbidden (directory traversal attempt).
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
asset_filepath = safe_asset_path(filepath)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return {"error": "Forbidden"}, 403
|
|
|
|
|
|
|
|
|
|
if not asset_filepath.exists() or not asset_filepath.is_file():
|
|
|
|
|
return {"error": "File not found"}, 404
|
|
|
|
|
|
2025-01-22 23:47:28 +01:00
|
|
|
return FileResponse(path=asset_filepath)
|
|
|
|
|
|
|
|
|
|
|
2025-12-30 22:08:21 +01:00
|
|
|
# ----------------------
|
|
|
|
|
# Run the EOSdash server
|
|
|
|
|
# ----------------------
|
|
|
|
|
|
|
|
|
|
|
2025-01-22 23:47:28 +01:00
|
|
|
def run_eosdash() -> None:
|
2025-01-21 19:20:50 +01:00
|
|
|
"""Run the EOSdash server with the specified configurations.
|
|
|
|
|
|
|
|
|
|
This function starts the EOSdash server using the Uvicorn ASGI server. It accepts
|
|
|
|
|
arguments for the host, port, log level, access log, and reload options. The
|
|
|
|
|
log level is converted to lowercase to ensure compatibility with Uvicorn's
|
|
|
|
|
expected log level format. If an error occurs while attempting to bind the
|
|
|
|
|
server to the specified host and port, an error message is logged and the
|
|
|
|
|
application exits.
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-02-12 21:35:51 +01:00
|
|
|
None
|
2025-01-21 19:20:50 +01:00
|
|
|
"""
|
2025-12-30 22:08:21 +01:00
|
|
|
if args:
|
|
|
|
|
run_as_user = args.run_as_user
|
|
|
|
|
else:
|
|
|
|
|
run_as_user = None
|
|
|
|
|
|
|
|
|
|
# Drop root privileges if running as root
|
|
|
|
|
drop_root_privileges(run_as_user=run_as_user)
|
|
|
|
|
|
2025-02-12 21:35:51 +01:00
|
|
|
# Wait for EOSdash port to be free - e.g. in case of restart
|
2025-10-28 02:50:31 +01:00
|
|
|
wait_for_port_free(config_eosdash["eosdash_port"], timeout=120, waiting_app_name="EOSdash")
|
2025-02-12 21:35:51 +01:00
|
|
|
|
2026-02-28 11:31:51 +01:00
|
|
|
# Normalize log_level to uvicorn log level
|
|
|
|
|
VALID_UVICORN_LEVELS = {"critical", "error", "warning", "info", "debug", "trace"}
|
|
|
|
|
uv_log_level = config_eosdash["log_level"].lower()
|
|
|
|
|
if uv_log_level == "none":
|
|
|
|
|
uv_log_level = "critical" # effectively disables logging
|
|
|
|
|
elif uv_log_level not in VALID_UVICORN_LEVELS:
|
|
|
|
|
uv_log_level = "info" # fallback
|
|
|
|
|
|
2026-03-11 17:18:45 +01:00
|
|
|
logger.info(
|
|
|
|
|
f"Starting EOSdash server on {config_eosdash['eosdash_host']}:{config_eosdash['eosdash_port']}"
|
|
|
|
|
)
|
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
try:
|
|
|
|
|
uvicorn.run(
|
2025-01-21 19:20:50 +01:00
|
|
|
"akkudoktoreos.server.eosdash:app",
|
2025-10-28 02:50:31 +01:00
|
|
|
host=config_eosdash["eosdash_host"],
|
|
|
|
|
port=config_eosdash["eosdash_port"],
|
2026-02-28 11:31:51 +01:00
|
|
|
log_level=uv_log_level,
|
2025-10-28 02:50:31 +01:00
|
|
|
access_log=config_eosdash["access_log"],
|
|
|
|
|
reload=config_eosdash["reload"],
|
2025-12-30 22:08:21 +01:00
|
|
|
proxy_headers=True,
|
|
|
|
|
forwarded_allow_ips="*",
|
2024-12-15 14:40:03 +01:00
|
|
|
)
|
|
|
|
|
except Exception as e:
|
2025-10-28 02:50:31 +01:00
|
|
|
logger.error(
|
|
|
|
|
f"Could not bind to host {config_eosdash['eosdash_host']}:{config_eosdash['eosdash_port']}. Error: {e}"
|
|
|
|
|
)
|
2025-01-21 19:20:50 +01:00
|
|
|
raise e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
"""Parse command-line arguments and start the EOSdash server with the specified options.
|
|
|
|
|
|
|
|
|
|
This function sets up the argument parser to accept command-line arguments for
|
|
|
|
|
host, port, log_level, access_log, and reload. It uses default values from the
|
2025-01-22 23:47:28 +01:00
|
|
|
config module if arguments are not provided. After parsing the arguments,
|
2025-01-21 19:20:50 +01:00
|
|
|
it starts the EOSdash server with the specified configurations.
|
|
|
|
|
|
|
|
|
|
Command-line Arguments:
|
2025-01-18 14:26:34 +01:00
|
|
|
--host (str): Host for the EOSdash server (default: value from config).
|
|
|
|
|
--port (int): Port for the EOSdash server (default: value from config).
|
|
|
|
|
--eos-host (str): Host for the EOS server (default: value from config).
|
|
|
|
|
--eos-port (int): Port for the EOS server (default: value from config).
|
2025-01-21 19:20:50 +01:00
|
|
|
--log_level (str): Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info").
|
|
|
|
|
--access_log (bool): Enable or disable access log. Options: True or False (default: False).
|
|
|
|
|
--reload (bool): Enable or disable auto-reload. Useful for development. Options: True or False (default: False).
|
|
|
|
|
"""
|
|
|
|
|
try:
|
2025-01-22 23:47:28 +01:00
|
|
|
run_eosdash()
|
2025-02-12 21:35:51 +01:00
|
|
|
except Exception as ex:
|
|
|
|
|
error_msg = f"Failed to run EOSdash: {ex}"
|
|
|
|
|
logger.error(error_msg)
|
2025-01-22 23:47:28 +01:00
|
|
|
traceback.print_exc()
|
2025-01-18 14:26:34 +01:00
|
|
|
sys.exit(1)
|
2025-01-21 19:20:50 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|