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-01-22 23:47:28 +01:00
|
|
|
from fasthtml.common import 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
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
from akkudoktoreos.config.config import get_config
|
2025-10-28 02:50:31 +01:00
|
|
|
from akkudoktoreos.core.logabc import LOGGING_LEVELS
|
|
|
|
|
from akkudoktoreos.core.logging import track_logging_config
|
|
|
|
|
from akkudoktoreos.core.version import __version__
|
|
|
|
|
from akkudoktoreos.server.dash.about import About
|
2025-01-22 23:47:28 +01:00
|
|
|
|
|
|
|
|
# Pages
|
2025-03-27 21:53:01 +01:00
|
|
|
from akkudoktoreos.server.dash.admin import Admin
|
|
|
|
|
from akkudoktoreos.server.dash.bokeh import BokehJS
|
|
|
|
|
from akkudoktoreos.server.dash.components import Page
|
|
|
|
|
from akkudoktoreos.server.dash.configuration import ConfigKeyUpdate, Configuration
|
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-01-22 23:47:28 +01:00
|
|
|
from akkudoktoreos.server.server import 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
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
config_eos = get_config()
|
|
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
|
|
|
|
|
# ------------------------------------
|
|
|
|
|
# Logging configuration at import time
|
|
|
|
|
# ------------------------------------
|
|
|
|
|
|
|
|
|
|
logger.remove()
|
|
|
|
|
track_logging_config(config_eos, "logging", None, None)
|
|
|
|
|
config_eos.track_nested_value("/logging", track_logging_config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------
|
|
|
|
|
# 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)",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 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 = {}
|
|
|
|
|
|
|
|
|
|
# Setup EOS logging level - first to have the other logging messages logged
|
|
|
|
|
# - 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
|
|
|
|
|
# - triggers logging configuration by track_logging_config
|
|
|
|
|
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(
|
|
|
|
|
title="EOSdash",
|
|
|
|
|
hdrs=hdrs,
|
2025-01-19 13:00:53 +01:00
|
|
|
secret_key=os.getenv("EOS_SERVER__EOSDASH_SESSKEY"),
|
|
|
|
|
)
|
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-01-22 23:47:28 +01:00
|
|
|
@app.get("/favicon.ico")
|
|
|
|
|
def get_eosdash_favicon(): # type: ignore
|
|
|
|
|
"""Get favicon."""
|
|
|
|
|
return FileResponse(path=favicon_filepath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
def get_eosdash(): # type: ignore
|
|
|
|
|
"""Serves the main EOSdash page.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Page: The main dashboard page with navigation links and footer.
|
|
|
|
|
"""
|
|
|
|
|
return Page(
|
|
|
|
|
None,
|
|
|
|
|
{
|
2025-10-28 02:50:31 +01:00
|
|
|
"Plan": "/eosdash/plan",
|
|
|
|
|
"Prediction": "/eosdash/prediction",
|
2025-01-22 23:47:28 +01:00
|
|
|
"Config": "/eosdash/configuration",
|
2025-03-27 21:53:01 +01:00
|
|
|
"Admin": "/eosdash/admin",
|
2025-10-28 02:50:31 +01:00
|
|
|
"About": "/eosdash/about",
|
2025-01-22 23:47:28 +01:00
|
|
|
},
|
2025-10-28 02:50:31 +01:00
|
|
|
About(),
|
2025-01-22 23:47:28 +01:00
|
|
|
Footer(*eos_server()),
|
|
|
|
|
"/eosdash/footer",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/eosdash/footer")
|
|
|
|
|
def get_eosdash_footer(): # type: ignore
|
|
|
|
|
"""Serves the EOSdash Foooter information.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Footer: The Footer component.
|
|
|
|
|
"""
|
|
|
|
|
return Footer(*eos_server())
|
|
|
|
|
|
|
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
@app.get("/eosdash/about")
|
|
|
|
|
def get_eosdash_about(): # type: ignore
|
|
|
|
|
"""Serves the EOSdash About page.
|
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")
|
|
|
|
|
def get_eosdash_admin(): # type: ignore
|
|
|
|
|
"""Serves the EOSdash Admin page.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Admin: The Admin page component.
|
|
|
|
|
"""
|
|
|
|
|
return Admin(*eos_server())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/eosdash/admin")
|
|
|
|
|
def post_eosdash_admin(data: dict): # type: ignore
|
2025-10-28 02:50:31 +01:00
|
|
|
"""Provide control data to the Admin page.
|
|
|
|
|
|
|
|
|
|
This endpoint is called from within the Admin page on user actions.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Admin: The Admin page component.
|
|
|
|
|
"""
|
2025-03-27 21:53:01 +01:00
|
|
|
return Admin(*eos_server(), data)
|
|
|
|
|
|
|
|
|
|
|
2025-01-22 23:47:28 +01:00
|
|
|
@app.get("/eosdash/configuration")
|
|
|
|
|
def get_eosdash_configuration(): # type: ignore
|
|
|
|
|
"""Serves the EOSdash Configuration page.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Configuration: The Configuration page component.
|
|
|
|
|
"""
|
|
|
|
|
return Configuration(*eos_server())
|
|
|
|
|
|
|
|
|
|
|
2025-03-27 21:53:01 +01:00
|
|
|
@app.put("/eosdash/configuration")
|
|
|
|
|
def put_eosdash_configuration(data: dict): # type: ignore
|
|
|
|
|
return ConfigKeyUpdate(*eos_server(), data["key"], data["value"])
|
|
|
|
|
|
|
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
@app.get("/eosdash/plan")
|
|
|
|
|
def get_eosdash_plan(data: dict): # type: ignore
|
|
|
|
|
"""Serves the EOSdash Plan page.
|
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")
|
|
|
|
|
def post_eosdash_plan(data: dict): # type: ignore
|
|
|
|
|
"""Provide control data to the Plan page.
|
|
|
|
|
|
|
|
|
|
This endpoint is called from within the Plan page on user actions.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Plan: The Plan page component.
|
|
|
|
|
"""
|
|
|
|
|
return Plan(*eos_server(), data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/eosdash/prediction")
|
|
|
|
|
def get_eosdash_prediction(data: dict): # type: ignore
|
|
|
|
|
"""Serves the EOSdash Prediction page.
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
def get_eosdash_health(): # type: ignore
|
|
|
|
|
"""Health check endpoint to verify that the EOSdash server is alive."""
|
|
|
|
|
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-01-22 23:47:28 +01:00
|
|
|
@app.get("/eosdash/assets/{fname:path}.{ext:static}")
|
|
|
|
|
def get_eosdash_assets(fname: str, ext: str): # type: ignore
|
|
|
|
|
"""Get assets."""
|
|
|
|
|
asset_filepath = Path(__file__).parent.joinpath(f"dash/assets/{fname}.{ext}")
|
|
|
|
|
return FileResponse(path=asset_filepath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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-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
|
|
|
|
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"],
|
|
|
|
|
log_level=config_eosdash["log_level"].lower(),
|
|
|
|
|
access_log=config_eosdash["access_log"],
|
|
|
|
|
reload=config_eosdash["reload"],
|
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()
|