PyMieSimX 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/.coveragerc +1 -0
  2. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/.github/workflows/deploy_PyPi.yml +5 -7
  3. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/.github/workflows/deploy_anaconda.yml +8 -9
  4. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/.github/workflows/deploy_coverage.yml +5 -2
  5. pymiesimx-0.1.3/.github/workflows/quality.yml +47 -0
  6. pymiesimx-0.1.3/.github/workflows/tests.yml +22 -0
  7. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/.gitignore +7 -0
  8. pymiesimx-0.1.3/CONTRIBUTING.md +7 -0
  9. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/Dockerfile +2 -1
  10. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PKG-INFO +26 -6
  11. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/__main__.py +3 -0
  12. pymiesimx-0.1.3/PyMieSimX/gui/admin.py +122 -0
  13. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/dashboard.css +477 -44
  14. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/default-settings.json +3 -3
  15. pymiesimx-0.1.3/PyMieSimX/gui/assets/home-spf-radial.gif +0 -0
  16. pymiesimx-0.1.3/PyMieSimX/gui/assets/home-spf-radial.png +0 -0
  17. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/callback_helpers.py +20 -3
  18. pymiesimx-0.1.1/PyMieSimX/gui/interface.py → pymiesimx-0.1.3/PyMieSimX/gui/callbacks.py +180 -167
  19. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/components/cards.py +0 -1
  20. pymiesimx-0.1.1/PyMieSimX/gui/services.py → pymiesimx-0.1.3/PyMieSimX/gui/computation.py +142 -18
  21. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/defaults.py +0 -1
  22. pymiesimx-0.1.3/PyMieSimX/gui/experiment_service.py +23 -0
  23. pymiesimx-0.1.3/PyMieSimX/gui/interface.py +105 -0
  24. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/jobs.py +3 -3
  25. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/layout.py +100 -12
  26. pymiesimx-0.1.3/PyMieSimX/gui/material_catalog.py +127 -0
  27. pymiesimx-0.1.3/PyMieSimX/gui/models.py +64 -0
  28. pymiesimx-0.1.3/PyMieSimX/gui/pages/experiment/page.py +62 -0
  29. pymiesimx-0.1.3/PyMieSimX/gui/pages/experiment/sections/configuration.py +47 -0
  30. pymiesimx-0.1.3/PyMieSimX/gui/pages/home.py +175 -0
  31. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/settings.py +0 -1
  32. pymiesimx-0.1.3/PyMieSimX/gui/pages/single/page.py +57 -0
  33. pymiesimx-0.1.3/PyMieSimX/gui/pages/single/sections/representations.py +37 -0
  34. pymiesimx-0.1.3/PyMieSimX/gui/pages/single/sections/setup.py +31 -0
  35. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/parsing.py +3 -1
  36. pymiesimx-0.1.3/PyMieSimX/gui/plotting.py +19 -0
  37. pymiesimx-0.1.3/PyMieSimX/gui/services.py +17 -0
  38. pymiesimx-0.1.3/PyMieSimX/gui/single_service.py +14 -0
  39. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/usage_metrics.py +18 -70
  40. pymiesimx-0.1.3/PyMieSimX/gui/validation.py +18 -0
  41. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX.egg-info/PKG-INFO +26 -6
  42. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX.egg-info/SOURCES.txt +20 -2
  43. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX.egg-info/requires.txt +5 -1
  44. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX.egg-info/scm_file_list.json +61 -43
  45. pymiesimx-0.1.3/PyMieSimX.egg-info/scm_version.json +8 -0
  46. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/README.rst +20 -3
  47. {pymiesimx-0.1.1 → pymiesimx-0.1.3/conda.recipe}/meta.yaml +3 -2
  48. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/makefile +14 -2
  49. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/pyproject.toml +9 -3
  50. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/pytest.ini +2 -0
  51. pymiesimx-0.1.3/tests/browser/test_dashboard.py +28 -0
  52. pymiesimx-0.1.3/tests/gui/cli.py +57 -0
  53. pymiesimx-0.1.3/tests/gui/test_admin.py +77 -0
  54. pymiesimx-0.1.3/tests/gui/test_dash_callbacks.py +161 -0
  55. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/tests/gui/test_services.py +21 -2
  56. pymiesimx-0.1.3/tests/gui/test_usage_metrics.py +31 -0
  57. pymiesimx-0.1.3/tools/generate_home_spf.py +100 -0
  58. pymiesimx-0.1.3/tools/next_release_version.py +24 -0
  59. pymiesimx-0.1.3/tools/release_tag.py +27 -0
  60. pymiesimx-0.1.1/.github/workflows/quality.yml +0 -30
  61. pymiesimx-0.1.1/PyMieSimX/gui/material_catalog.py +0 -90
  62. pymiesimx-0.1.1/PyMieSimX/gui/pages/experiment/page.py +0 -49
  63. pymiesimx-0.1.1/PyMieSimX/gui/pages/experiment/sections/configuration.py +0 -42
  64. pymiesimx-0.1.1/PyMieSimX/gui/pages/home.py +0 -152
  65. pymiesimx-0.1.1/PyMieSimX/gui/pages/single/page.py +0 -28
  66. pymiesimx-0.1.1/PyMieSimX/gui/pages/single/sections/representations.py +0 -38
  67. pymiesimx-0.1.1/PyMieSimX/gui/pages/single/sections/setup.py +0 -27
  68. pymiesimx-0.1.1/PyMieSimX.egg-info/scm_version.json +0 -8
  69. pymiesimx-0.1.1/tests/gui/cli.py +0 -31
  70. pymiesimx-0.1.1/tests/gui/test_usage_metrics.py +0 -30
  71. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/.flake8 +0 -0
  72. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/.github/dependabot.yml +0 -0
  73. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/.pre-commit-config.yaml +0 -0
  74. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/LICENSE +0 -0
  75. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/__init__.py +0 -0
  76. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/api.py +0 -0
  77. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/application/__init__.py +0 -0
  78. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/application/wsgi.py +0 -0
  79. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/__init__.py +0 -0
  80. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/card-state.js +0 -0
  81. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/logo.png +0 -0
  82. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/materials-stock.json +0 -0
  83. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/pymiesim-favicon.svg +0 -0
  84. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/pymiesim-logo-dark.svg +0 -0
  85. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/pymiesim-logo.svg +0 -0
  86. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/components/__init__.py +0 -0
  87. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/__init__.py +0 -0
  88. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/citation.py +0 -0
  89. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/documentation.py +0 -0
  90. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/experiment/__init__.py +0 -0
  91. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/experiment/sections/__init__.py +0 -0
  92. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/field_syntax.py +0 -0
  93. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/install_local.py +0 -0
  94. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/sellmeier.py +0 -0
  95. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/single/__init__.py +0 -0
  96. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/single/sections/__init__.py +0 -0
  97. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX/gui/schemas.py +0 -0
  98. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX.egg-info/dependency_links.txt +0 -0
  99. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX.egg-info/entry_points.txt +0 -0
  100. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/PyMieSimX.egg-info/top_level.txt +0 -0
  101. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/setup.cfg +0 -0
  102. {pymiesimx-0.1.1 → pymiesimx-0.1.3}/tests/gui/test_api.py +0 -0
@@ -8,6 +8,7 @@ omit =
8
8
  PyMieSimX/**/__main__.py
9
9
 
10
10
  [report]
11
+ fail_under = 52
11
12
  skip_covered = True
12
13
  show_missing = True
13
14
  precision = 2
@@ -1,19 +1,17 @@
1
- name: Deploy to PyPI
1
+ name: Publish to PyPI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: ["master"]
6
- tags: ["*"]
7
- pull_request:
8
- branches: ["master"]
5
+ tags: ["v*"]
9
6
 
10
7
  permissions:
11
8
  contents: read
12
9
 
13
10
  jobs:
14
- PyPI:
15
- uses: MartinPdeS/MPSActions/.github/workflows/publish_pure_package_to_PyPi.yml@master
11
+ publish:
12
+ uses: MartinPdeS/MPSActions/.github/workflows/publish_pure_package_to_PyPi.yml@v5
16
13
  with:
17
14
  python-version: "3.11"
15
+ test-command: python -c 'import PyMieSimX'
18
16
  secrets:
19
17
  PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
@@ -1,21 +1,20 @@
1
- name: Deploy Conda Packages
1
+ name: Publish Conda package
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [ "master" ]
6
- tags: '*'
7
- pull_request:
8
- branches: [ "master" ]
5
+ tags: ["v*"]
9
6
 
10
7
  permissions:
11
- contents: write
8
+ contents: read
12
9
 
13
10
  jobs:
14
- linux:
15
- uses: MartinPdeS/MPSActions/.github/workflows/publish_compiled_package_to_anaconda.yml@master
11
+ publish:
12
+ uses: MartinPdeS/MPSActions/.github/workflows/publish_compiled_package_to_anaconda.yml@v5
16
13
  with:
17
14
  python_versions: '["3.11", "3.12", "3.13"]'
18
- apt_package: ""
19
15
  os_list: '["ubuntu-latest", "macos-latest", "windows-latest"]'
16
+ package_name: pymiesimx
17
+ recipe_path: conda.recipe
18
+ test_import: PyMieSimX
20
19
  secrets:
21
20
  ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }}
@@ -16,11 +16,11 @@ jobs:
16
16
  name: Run GUI tests and publish coverage
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
20
20
  with:
21
21
  fetch-depth: 0
22
22
 
23
- - uses: actions/setup-python@v5
23
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
24
24
  with:
25
25
  python-version: "3.11"
26
26
 
@@ -29,6 +29,9 @@ jobs:
29
29
  python -m pip install --upgrade pip wheel build
30
30
  python -m pip install -e ".[testing]"
31
31
 
32
+ - name: Initialize material catalog
33
+ run: python -m PyOptik setup --no-progress
34
+
32
35
  - name: Run GUI tests with coverage
33
36
  run: |
34
37
  coverage erase
@@ -0,0 +1,47 @@
1
+ name: Quality and GUI tests
2
+
3
+ on:
4
+ push:
5
+ branches: ["master"]
6
+ pull_request:
7
+ branches: ["master"]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ python-quality:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ matrix:
17
+ python-version: ["3.11", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
20
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - name: Install package and quality tools
24
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[testing,dev]"
25
+ - name: Initialize material catalog
26
+ run: python -m PyOptik setup --no-progress
27
+ - name: Ruff
28
+ run: python -m ruff check PyMieSimX tests
29
+ - name: Mypy
30
+ run: python -m mypy PyMieSimX/gui/parsing.py PyMieSimX/gui/schemas.py PyMieSimX/gui/models.py PyMieSimX/gui/jobs.py PyMieSimX/gui/callback_helpers.py PyMieSimX/gui/admin.py
31
+ - name: GUI tests
32
+ run: python -m pytest
33
+
34
+ browser-tests:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
38
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
39
+ with:
40
+ python-version: "3.11"
41
+ - name: Install package and browser test tools
42
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[testing,browser]"
43
+ - name: Initialize material catalog
44
+ run: python -m PyOptik setup --no-progress
45
+ - name: Browser workflow tests
46
+ # The full GUI suite enforces the repository-wide coverage threshold.
47
+ run: python -m pytest tests/browser --config-file=pytest.ini --headless --cov=PyMieSimX --cov-report=term --cov-fail-under=0
@@ -0,0 +1,22 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ tests:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
17
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
18
+ with:
19
+ python-version: "3.11"
20
+ - run: python -m pip install --upgrade pip && python -m pip install -e ".[testing]"
21
+ - run: python -m PyOptik setup --no-progress
22
+ - run: python -m pytest -q
@@ -10,3 +10,10 @@ dist/
10
10
  .coverage
11
11
  htmlcov/
12
12
  .DS_Store
13
+ !conda.recipe/
14
+ !conda.recipe/meta.yaml
15
+ !CONTRIBUTING.md
16
+ !makefile
17
+ !Makefile
18
+ !tools/
19
+ !tools/*.py
@@ -0,0 +1,7 @@
1
+ # Contributing to PyMieSimX
2
+
3
+ Use Python 3.10 or newer. Install development dependencies with `python -m pip install -e ".[testing,dev]"`, then run `make quality` and `make test`.
4
+
5
+ Releases use semantic version tags. Run `make patch`, `make minor`, or `make major` from a clean `master` checkout; the command creates the release commit, tags it, and pushes both. Publishing workflows run only for `v*` tags.
6
+
7
+ The Conda recipe is at `conda.recipe/meta.yaml`. Do not commit build artefacts, virtual environments, or caches.
@@ -7,6 +7,7 @@ WORKDIR /app
7
7
  COPY . .
8
8
 
9
9
  RUN python -m pip install --upgrade pip \
10
- && python -m pip install --no-cache-dir .
10
+ && python -m pip install --no-cache-dir . \
11
+ && python -m PyOptik setup --no-progress
11
12
 
12
13
  CMD ["sh", "-c", "exec gunicorn PyMieSimX.gui.interface:app --bind 0.0.0.0:${PORT:-8050} --workers 1 --threads 2 --timeout 3600"]
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyMieSimX
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Graphical Dash interface for PyMieSim.
5
5
  Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Repository, https://github.com/MartinPdeS/PyMieSimX
8
- Requires-Python: >=3.10
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/x-rst
10
10
  License-File: LICENSE
11
- Requires-Dist: PyMieSim<6,>=5.1.2
11
+ Requires-Dist: PyMieSim<6,>=5.6.4
12
+ Requires-Dist: PyOptik<4,>=3.2.1
12
13
  Requires-Dist: dash<5,>=4.1.0
13
14
  Requires-Dist: gunicorn<24,>=23.0.0
14
15
  Requires-Dist: numpy<3,>=2.0
@@ -19,6 +20,8 @@ Provides-Extra: testing
19
20
  Requires-Dist: pytest>=8.0; extra == "testing"
20
21
  Requires-Dist: pytest-cov>=5.0; extra == "testing"
21
22
  Requires-Dist: coverage>=7.0; extra == "testing"
23
+ Provides-Extra: browser
24
+ Requires-Dist: dash[testing]<5,>=4.1; extra == "browser"
22
25
  Provides-Extra: dev
23
26
  Requires-Dist: build>=1.2; extra == "dev"
24
27
  Requires-Dist: mypy==1.15.0; extra == "dev"
@@ -46,10 +49,15 @@ solver.
46
49
  Installation
47
50
  ------------
48
51
 
49
- Install the GUI and its PyMieSim dependency with::
52
+ Install the GUI with PyMieSim and PyOptik with::
50
53
 
51
54
  pip install PyMieSimX
52
55
 
56
+ The first ``pymiesimx`` launch downloads PyOptik's complete
57
+ RefractiveIndex.INFO snapshot. It can also be initialized explicitly with::
58
+
59
+ python -m PyOptik setup --no-progress
60
+
53
61
  Launch the dashboard with::
54
62
 
55
63
  pymiesimx
@@ -129,5 +137,17 @@ the Render service with::
129
137
 
130
138
  PyMieSimX writes namespaced counters to the shared ``metrics_counters`` table:
131
139
  ``pymiesimx_home_page_visit_count``, ``pymiesimx_experiment_run_count``, and
132
- ``pymiesimx_single_run_count``. If PostgreSQL is unavailable, local development
133
- falls back to a JSON file under the platform's application-data directory.
140
+ ``pymiesimx_single_run_count``. Metrics are collected only when the PostgreSQL
141
+ backend is explicitly configured and available. Local installations do not
142
+ persist usage counters.
143
+
144
+ Administration page
145
+ -------------------
146
+
147
+ Usage counters are shown only on the hidden, token-protected administration
148
+ page. Configure a server-side token with::
149
+
150
+ PYMIESIMX_ADMIN_TOKEN=<a long random value>
151
+
152
+ Then open ``/admin?token=<the same value>``. The page is intentionally absent
153
+ from the public sidebar and refreshes its counters every 30 seconds.
@@ -5,6 +5,9 @@ import logging
5
5
  import sys
6
6
 
7
7
 
8
+ LOGGER = logging.getLogger(__name__)
9
+
10
+
8
11
  def _build_argument_parser() -> argparse.ArgumentParser:
9
12
  """Create the CLI parser for the dashboard launcher."""
10
13
  parser = argparse.ArgumentParser(description="Launch the PyMieSimX dashboard.")
@@ -0,0 +1,122 @@
1
+ """Token-gated administration dashboard for server-side usage counters."""
2
+
3
+ import hmac
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from dash import dcc, html
10
+
11
+ from PyMieSimX.gui import usage_metrics
12
+ from PyMieSimX.gui.components import Card
13
+
14
+
15
+ ADMIN_TOKEN_ENV_VAR = "PYMIESIMX_ADMIN_TOKEN"
16
+ REFRESH_INTERVAL_MILLISECONDS = 30_000
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class AdminDashboardData:
21
+ """Snapshot displayed by the administration dashboard."""
22
+
23
+ metrics: usage_metrics.UsageMetrics = field(default_factory=usage_metrics.UsageMetrics)
24
+ generated_at: str = ""
25
+
26
+
27
+ def is_admin_access_granted(provided_token: Any) -> bool:
28
+ """Return whether the provided token exactly matches server configuration."""
29
+ configured_token = os.getenv(ADMIN_TOKEN_ENV_VAR, "").strip()
30
+ candidate_token = str(provided_token or "").strip()
31
+ return bool(configured_token and candidate_token and hmac.compare_digest(configured_token, candidate_token))
32
+
33
+
34
+ def collect_admin_dashboard_data() -> AdminDashboardData:
35
+ """Load the current server-side usage counters."""
36
+ return AdminDashboardData(
37
+ metrics=usage_metrics.load_usage_metrics(),
38
+ generated_at=f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC",
39
+ )
40
+
41
+
42
+ def metric_text(value: object) -> str:
43
+ """Format a metric for display, including unavailable local values."""
44
+ if isinstance(value, float) and value != value:
45
+ return "NaN"
46
+ return str(value)
47
+
48
+
49
+ def dashboard_values(data: AdminDashboardData) -> tuple[str, str, str, str]:
50
+ """Return callback-ready metric values and update timestamp."""
51
+ metrics = data.metrics
52
+ return (
53
+ metric_text(metrics.home_page_visit_count),
54
+ metric_text(metrics.experiment_run_count),
55
+ metric_text(metrics.single_run_count),
56
+ f"Last updated: {data.generated_at}",
57
+ )
58
+
59
+
60
+ def build_admin_page(token: str | None) -> html.Div:
61
+ """Build the hidden dashboard or an intentionally generic denial page."""
62
+ if not is_admin_access_granted(token):
63
+ return html.Div(
64
+ html.Section(
65
+ className=Card.classes(color="blue", extra="admin-access-card"),
66
+ children=[
67
+ html.H2("Page not available"),
68
+ html.P("The requested page is not available on this server."),
69
+ ],
70
+ ),
71
+ className="page-content-stack admin-page",
72
+ )
73
+
74
+ values = dashboard_values(collect_admin_dashboard_data())
75
+ tiles = (
76
+ ("admin-home-page-visits", values[0], "Home page visits"),
77
+ ("admin-experiment-runs", values[1], "Parameter sweeps"),
78
+ ("admin-single-runs", values[2], "Particle explorations"),
79
+ )
80
+ return html.Div(
81
+ className="page-content-stack admin-page",
82
+ children=[
83
+ dcc.Interval(id="admin-refresh-interval", interval=REFRESH_INTERVAL_MILLISECONDS, n_intervals=0),
84
+ html.Section(
85
+ className="page-hero",
86
+ children=[
87
+ html.H1("Administration"),
88
+ html.P("Server-side PyMieSimX usage metrics.", className="hero-text"),
89
+ html.Div(
90
+ [
91
+ html.Span(values[3], id="admin-last-updated"),
92
+ html.Button("Refresh", id="admin-refresh-button", n_clicks=0, className="run-button"),
93
+ ],
94
+ className="admin-toolbar",
95
+ ),
96
+ ],
97
+ ),
98
+ html.Div(
99
+ [
100
+ html.Section(
101
+ className=Card.classes(color="blue", extra="admin-metric-card"),
102
+ children=[
103
+ html.Div(value, id=value_id, className="admin-metric-value"),
104
+ html.Div(label, className="admin-metric-label"),
105
+ ],
106
+ )
107
+ for value_id, value, label in tiles
108
+ ],
109
+ className="admin-metric-grid",
110
+ ),
111
+ ],
112
+ )
113
+
114
+
115
+ __all__ = [
116
+ "ADMIN_TOKEN_ENV_VAR",
117
+ "AdminDashboardData",
118
+ "build_admin_page",
119
+ "collect_admin_dashboard_data",
120
+ "dashboard_values",
121
+ "is_admin_access_granted",
122
+ ]