Files
PTSRaiAgent/dashboard/app.py
T
Pavel Eliseev c01bfc90df
CI / ruff · pytest (pull_request) Successful in 2m1s
feat(dashboard): replace home with approved minimal design
Refs #73
2026-10-04 14:54:14 +03:00

124 lines
4.7 KiB
Python

"""Doctor dashboard and demo console (Streamlit). Talks to the backend only via the API.
Entry point: `streamlit run dashboard/app.py`. Pages are discovered from `dashboard/pages/*.py` — adding a page
is adding a file, app.py never needs editing:
* file name `<N>[letter]_<Title>.py` — N (and the optional letter) sets the order, underscores in the title become
spaces (`2_Демо-консоль.py` → «Демо-консоль»);
* section: pages with N == 1 go to «Кабинет врача», the rest to «Демо»;
* optional module-level constants (read as text, the file is not imported):
`NAV_TITLE = "..."`, `NAV_ICON = ":material/terminal:"`, `NAV_SECTION = "..."`, `NAV_URL = "slug"`.
"""
from __future__ import annotations
import re
import sys
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
_HERE = str(Path(__file__).resolve().parent)
# `streamlit run` puts dashboard/ on sys.path, where this very file would shadow the backend `app` package
# (needed for the frozen schemas). Import everything from the repo root as `dashboard.*` / `app.*` instead.
sys.path[:] = [p for p in sys.path if p not in ("", _HERE) and Path(p or ".").resolve() != Path(_HERE)]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
PAGES_DIR = Path(__file__).resolve().parent / "pages"
DOCTOR_SECTION = "Кабинет врача"
DEMO_SECTION = "Демо"
DEFAULT_ICONS = {
"Пациенты": ":material/groups:",
"Подключить пациента": ":material/person_add:",
"Демо-консоль": ":material/smartphone:",
"Сплит-экран": ":material/splitscreen:",
}
_NAME_RE = re.compile(r"^(\d+)([a-z]?)_(.+)$")
_META_RE = re.compile(r"""^NAV_(TITLE|ICON|SECTION|URL)\s*(?::\s*str\s*)?=\s*["'](.+?)["']\s*$""", re.MULTILINE)
_TRANSLIT = dict(
zip(
"абвгдеёжзийклмнопрстуфхцчшщъыьэюя",
[*"abvgdee", "zh", *"zijklmnoprstufh", "ts", "ch", "sh", "sch", "_", "y", "_", "e", "yu", "ya"],
strict=True,
)
)
@dataclass(frozen=True)
class PageSpec:
path: Path
title: str
icon: str
section: str
url_path: str
order: tuple[int, str]
def slugify(title: str) -> str:
out = "".join(_TRANSLIT.get(ch, ch) for ch in title.lower())
out = out.replace("_", "")
return re.sub(r"[^a-z0-9]+", "-", out).strip("-") or "page"
def discover_pages(pages_dir: Path = PAGES_DIR) -> list[PageSpec]:
specs = []
for path in pages_dir.glob("*.py"):
m = _NAME_RE.match(path.stem)
if not m:
continue
number, letter, raw = int(m.group(1)), m.group(2), m.group(3)
meta = dict(_META_RE.findall(path.read_text(encoding="utf-8")))
title = meta.get("TITLE", raw.replace("_", " "))
specs.append(
PageSpec(
path=path,
title=title,
icon=meta.get("ICON", DEFAULT_ICONS.get(title, ":material/description:")),
section=meta.get("SECTION", DOCTOR_SECTION if number == 1 else DEMO_SECTION),
url_path=meta.get("URL", slugify(title)),
order=(number, letter),
)
)
return sorted(specs, key=lambda s: s.order)
def main() -> None:
import streamlit as st
from dashboard import theme
from dashboard.components import live_alerts, state, ui
st.set_page_config(page_title="Кабинет врача · MedITron", page_icon="🩺", layout="wide")
st.html(theme.CSS)
st.logo(str(ROOT / "dashboard" / "assets" / "logo.svg"), size="large")
specs = discover_pages()
nav: dict[str, list[st.Page]] = {}
section_of: dict[str, str] = {}
for i, s in enumerate(specs):
page = st.Page(str(s.path), title=s.title, icon=s.icon, url_path=s.url_path, default=i == 0)
nav.setdefault(s.section, []).append(page)
section_of[s.url_path] = s.section
current = st.navigation(nav, position="sidebar", expanded=True)
with st.sidebar:
st.html(
'<div class="mt-sidebar-note"><strong>Рядом между визитами</strong>'
"Наблюдение за состоянием пациента и поддержка назначенного лечения.</div>"
)
st.caption("Прототип на синтетических данных")
short, _ = state.doctor_names()
st.html(ui.workspace_header(short, current.title))
live_alerts.flush_pending_toasts()
# Full-app rerun on a new alert only on doctor pages: demo-console pages may be mid-scenario.
live_alerts.render(rerun_app_on_new=section_of.get(current.url_path) == DOCTOR_SECTION)
current.run()
ui.footer()
if __name__ == "__main__":
main()