"""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 `[letter]_.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()