aggregate_api 1.0.0__py3-none-any.whl

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 (63) hide show
  1. aggregate_api/__init__.py +41 -0
  2. aggregate_api/__main__.py +154 -0
  3. aggregate_api/app.py +206 -0
  4. aggregate_api/audit.py +395 -0
  5. aggregate_api/bounds.py +331 -0
  6. aggregate_api/cache.py +319 -0
  7. aggregate_api/capability.py +823 -0
  8. aggregate_api/completion.py +219 -0
  9. aggregate_api/config.py +363 -0
  10. aggregate_api/cors.py +61 -0
  11. aggregate_api/examples.py +620 -0
  12. aggregate_api/layer_pricing.py +840 -0
  13. aggregate_api/library.py +94 -0
  14. aggregate_api/library_notes.py +96 -0
  15. aggregate_api/models.py +1407 -0
  16. aggregate_api/net.py +281 -0
  17. aggregate_api/pnl.py +101 -0
  18. aggregate_api/pricing.py +778 -0
  19. aggregate_api/resources.py +257 -0
  20. aggregate_api/routes/__init__.py +8 -0
  21. aggregate_api/routes/decl.py +327 -0
  22. aggregate_api/routes/examples.py +82 -0
  23. aggregate_api/routes/meta.py +282 -0
  24. aggregate_api/routes/objects.py +4119 -0
  25. aggregate_api/routes/status.py +466 -0
  26. aggregate_api/serializers.py +565 -0
  27. aggregate_api/sessions.py +353 -0
  28. aggregate_api/static/aggregate-api-logo-512.png +0 -0
  29. aggregate_api/static/aggregate-api-logo.png +0 -0
  30. aggregate_api/static/aggregate-api-trim.png +0 -0
  31. aggregate_api/static/android-chrome-192x192.png +0 -0
  32. aggregate_api/static/android-chrome-512x512.png +0 -0
  33. aggregate_api/static/apple-touch-icon.png +0 -0
  34. aggregate_api/static/assets/bootstrap-icons-BeopsB42.woff +0 -0
  35. aggregate_api/static/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  36. aggregate_api/static/assets/bootstrap-ohb1VZ53.js +5 -0
  37. aggregate_api/static/assets/codemirror-h62DHGGa.js +14 -0
  38. aggregate_api/static/assets/csv-grid.worker-DKzHGXac.js +4 -0
  39. aggregate_api/static/assets/echarts-B7o9sc00.js +40 -0
  40. aggregate_api/static/assets/echarts-gl-DG1Uf6wE.js +4282 -0
  41. aggregate_api/static/assets/lite-CUlcD8p4.css +1 -0
  42. aggregate_api/static/assets/lite-Dd2TnT4M.js +1 -0
  43. aggregate_api/static/assets/main-Bxhxa55v.css +9 -0
  44. aggregate_api/static/assets/main-CmoEiPit.js +9 -0
  45. aggregate_api/static/assets/tables-BHCF7qIF.js +8 -0
  46. aggregate_api/static/assets/tables-CxvajLr7.css +1 -0
  47. aggregate_api/static/favicon-16x16.png +0 -0
  48. aggregate_api/static/favicon-32x32.png +0 -0
  49. aggregate_api/static/favicon.ico +0 -0
  50. aggregate_api/static/index.html +912 -0
  51. aggregate_api/static/lite.html +83 -0
  52. aggregate_api/static/logo.png +0 -0
  53. aggregate_api/static/site.webmanifest +14 -0
  54. aggregate_api/static/sw.js +78 -0
  55. aggregate_api/status.py +536 -0
  56. aggregate_api/status_page.html +546 -0
  57. aggregate_api/tables.py +316 -0
  58. aggregate_api-1.0.0.dist-info/METADATA +187 -0
  59. aggregate_api-1.0.0.dist-info/RECORD +63 -0
  60. aggregate_api-1.0.0.dist-info/WHEEL +5 -0
  61. aggregate_api-1.0.0.dist-info/entry_points.txt +2 -0
  62. aggregate_api-1.0.0.dist-info/licenses/LICENSE +28 -0
  63. aggregate_api-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,466 @@
1
+ """The operator's view: ``GET /v1/status`` and the page that renders it.
2
+
3
+ Two routes and one gate. ``/v1/status`` is the contract, a JSON document that
4
+ is scriptable, curl'able from the VPS, and the thing a future monitor would
5
+ poll. ``/v1/status/page`` is one self-contained HTML file that fetches it and
6
+ re-renders on a timer. Both are read only, both are private, and neither
7
+ appears in the OpenAPI schema.
8
+
9
+ Private by construction, three layers
10
+ -------------------------------------
11
+
12
+ None of the three is trusted alone, and the first needs no trust in this file
13
+ at all.
14
+
15
+ 1. **The public Caddy block answers ``/v1/status*`` with 404**, beside the
16
+ ``/docs`` and ``/openapi.json`` matcher already there. The app is never
17
+ reached, so nothing here can expose the page. One prefix covers both routes
18
+ and every future one, which is why they live under ``/v1`` together.
19
+ 2. **This module's** :func:`require_private`, for the case where a Caddy block
20
+ is edited, reloaded wrong, or a second front door appears. Deny by default:
21
+ see ``net.py`` for why the peer address cannot be the test and why the
22
+ forwarded chain is read from the last element.
23
+ 3. **An explicit zone header**, ``X-Aggapi-Zone: private``, set by the VPN Caddy
24
+ block and stripped by the public one. Off by default (author ruling,
25
+ 2026-08-18); ``AGGAPI_STATUS_REQUIRE_ZONE_HEADER=true`` turns it on. It is
26
+ the only layer that survives a mistake in the CIDR list, so it ships built
27
+ and documented rather than unwritten.
28
+
29
+ Refusal is 404 and not 403, so the page's existence is not advertised, and it is
30
+ logged at WARNING: a refusal on a correctly configured box means something
31
+ changed.
32
+
33
+ Read only, always
34
+ -----------------
35
+
36
+ **No route under ``/v1/status`` may build, evict, clear, or mutate anything.**
37
+ Every number is read off a live structure or queried from the audit log. A
38
+ "clear the cache" button would be a separate plan with its own gate, and it is
39
+ not this one. ``tests/test_status.py`` asserts the invariant by snapshotting the
40
+ cache and the counters around a request.
41
+
42
+ What it costs
43
+ -------------
44
+
45
+ Tens of milliseconds, and flat in the age of the deployment. The audit queries
46
+ are windowed, indexed, limited, and share one connection; the cache sizes are
47
+ arithmetic on ``log2`` rather than a walk into numpy
48
+ (:func:`aggregate_api.status.estimated_bytes` says why); the resource block
49
+ never blocks on a CPU interval. Measured on the Windows development box at 25 to
50
+ 40 ms, of which ``psutil`` is about 17: reading process memory and handle counts
51
+ is markedly more expensive on Windows than the ``/proc`` reads it does on the
52
+ Linux VPS, so the deployed number is smaller than the development one. Against a
53
+ ten second refresh either is free.
54
+
55
+ ``generated_in_ms`` rides in the payload so the page reports its own cost and a
56
+ regression is self evident rather than inferred.
57
+ """
58
+
59
+ from __future__ import annotations
60
+
61
+ import logging
62
+ import platform
63
+ import sys
64
+ import time
65
+ from collections import Counter
66
+ from datetime import datetime, timezone
67
+ from importlib.metadata import version as _pkg_version
68
+ from importlib.resources import files
69
+ from pathlib import Path
70
+
71
+ from fastapi import APIRouter, Depends, HTTPException, Request, Response
72
+ from fastapi.responses import HTMLResponse
73
+
74
+ from .. import models
75
+ from .. import resources as resource_block
76
+ from .. import status as status_state
77
+ from ..audit import AuditLog
78
+ from ..cache import ObjectCache
79
+ from ..config import Settings, get_settings
80
+ from ..library import get_underwriter
81
+ from ..net import is_private_request
82
+ from ..sessions import SessionRegistry
83
+ from . import objects as objects_routes
84
+
85
+ log = logging.getLogger(__name__)
86
+
87
+ router = APIRouter()
88
+
89
+ #: Layer three's header and the value it must carry. The VPN Caddy block sets
90
+ #: it; the public block deletes any inbound one, so it cannot be forged.
91
+ ZONE_HEADER = "X-Aggapi-Zone"
92
+ ZONE_PRIVATE = "private"
93
+
94
+ #: Windows the build panel reports over. An hour answers "what is happening
95
+ #: now" and a day answers "what has this box been doing", and two numbers side
96
+ #: by side say more than either: a busy hour inside a quiet day is a session,
97
+ #: and a quiet hour inside a busy day is one that just ended.
98
+ BUILD_WINDOWS = (("hour", 3600.0), ("day", 86400.0))
99
+
100
+ #: What the oversight charter's state snapshot records, so the page can say when
101
+ #: the library has moved. Not a check and not a floor: a mismatch is news, not
102
+ #: an error, and the direction it moves in is the useful part. See
103
+ #: ``T:/worktrees/CLAUDE.md``, "State snapshot".
104
+ CHARTER_EXHIBITS = 12
105
+ CHARTER_CHARTS = 8
106
+
107
+ #: How much of a cached program the payload carries. Same limit the key-scope
108
+ #: buffer uses, so one pasted portfolio cannot push everything else off the
109
+ #: page, and the page offers an expand control for the rest.
110
+ PROGRAM_CHARS = status_state.PROGRAM_CHARS
111
+
112
+
113
+ def require_private(request: Request,
114
+ settings: Settings = Depends(get_settings)) -> str:
115
+ """Admit a request only if it demonstrably came from a private origin.
116
+
117
+ Parameters
118
+ ----------
119
+ request : starlette.requests.Request
120
+ settings : Settings
121
+ For ``private_cidrs`` and ``status_require_zone_header``.
122
+
123
+ Returns
124
+ -------
125
+ str
126
+ The address the request was admitted on, so a route can report it.
127
+
128
+ Raises
129
+ ------
130
+ HTTPException
131
+ 404 for anything not admitted.
132
+
133
+ Notes
134
+ -----
135
+ **Deny by default.** The verdict comes from
136
+ :func:`aggregate_api.net.is_private_request`, which answers False for an
137
+ address it could not establish as readily as for one outside the list, so
138
+ there is no path where an unexpected shape falls through to allowed.
139
+
140
+ **404 rather than 403.** A 403 confirms the route exists, which is
141
+ information this page should not give away; a 404 is what an unmounted
142
+ route would say. The refusal is logged and buffered instead, where the
143
+ operator who is entitled to know can see it.
144
+
145
+ **The single-hop assumption is load bearing.** Exactly one trusted proxy
146
+ sits in front of this app, so the last forwarded element is the address
147
+ Caddy observed. Add a second proxy and that index is wrong and the gate
148
+ opens. ``net.py``'s module docstring and ``human-hints.md`` both carry this;
149
+ it is repeated here because this is the function it would break.
150
+ """
151
+ allowed, address = is_private_request(request, settings.private_cidrs)
152
+ reason = None if allowed else "address not in AGGAPI_PRIVATE_CIDRS"
153
+ if allowed and settings.status_require_zone_header:
154
+ zone = (request.headers.get(ZONE_HEADER) or "").strip().lower()
155
+ if zone != ZONE_PRIVATE:
156
+ allowed, reason = False, f"missing or wrong {ZONE_HEADER}"
157
+ if not allowed:
158
+ status_state.record_refusal(address, request.url.path)
159
+ log.warning("status route refused: address=%s path=%s reason=%s",
160
+ address, request.url.path, reason)
161
+ raise HTTPException(status_code=404, detail="Not Found")
162
+ return address
163
+
164
+
165
+ def _recipe_count(uw) -> int | None:
166
+ """How many recipes a base holds, or ``None`` if it will not say.
167
+
168
+ Notes
169
+ -----
170
+ ``len(uw._recipes)`` is a private attribute of a stable-tier library class,
171
+ taken deliberately and recorded in the oversight charter's tolerated list
172
+ (author ruling, 2026-08-18). The public route is ``Underwriter.recipes``,
173
+ which builds a pandas frame per call, and this page calls this once for the
174
+ process base and once per live session on every refresh. The read is
175
+ guarded, so a rename upstream costs one field reading ``None`` rather than a
176
+ broken route.
177
+ """
178
+ if uw is None:
179
+ return None
180
+ try:
181
+ return len(uw._recipes)
182
+ except (AttributeError, TypeError):
183
+ return None
184
+
185
+
186
+ def _loaded_underwriter():
187
+ """The process recipe base, but only if something has already loaded it.
188
+
189
+ Returns
190
+ -------
191
+ aggregate.underwriter.Underwriter or None
192
+ ``None`` when nothing in this process has resolved the library yet.
193
+
194
+ Notes
195
+ -----
196
+ **Reporting on the library must not cause the library to be read.**
197
+ :func:`aggregate_api.library.get_underwriter` loads on first call, which
198
+ takes about two seconds for a custom ``.agg``, so a status route that simply
199
+ called it would make the first page load on a cold process pay for the read,
200
+ and would report a "boot library load time" that its own request had caused.
201
+ A page that changes what it measures is not an instrument.
202
+
203
+ ``lru_cache`` publishes ``cache_info()``, so "has anything loaded this yet"
204
+ is answerable without touching it. Before the first build the page says the
205
+ library is not loaded, which is a true and useful thing to say.
206
+ """
207
+ if get_underwriter.cache_info().currsize == 0:
208
+ return None
209
+ return get_underwriter()
210
+
211
+
212
+ def _identity(settings: Settings) -> dict:
213
+ """Versions, interpreter, process and library, the version-skew block.
214
+
215
+ Notes
216
+ -----
217
+ Both package versions come from ``importlib.metadata``, which reports what
218
+ was recorded when the editable install was built rather than what
219
+ ``pyproject.toml`` says now. That is the standing trap in both repos'
220
+ CLAUDE.md: a library bump without ``uv sync --extra dev`` leaves the server
221
+ reporting stale versions indefinitely. Showing both beside the process start
222
+ time is what turns that from an hour of confusion into a glance.
223
+ """
224
+ facts = status_state.process_facts()
225
+ uw = _loaded_underwriter()
226
+ recipes = _recipe_count(uw)
227
+ return {
228
+ "version": _pkg_version("aggregate_api"),
229
+ "aggregate_version": _pkg_version("aggregate"),
230
+ "tables_version": _pkg_version("greater-tables"),
231
+ "python": sys.version.split()[0],
232
+ "platform": platform.platform(terse=True),
233
+ "host": settings.host,
234
+ "port": settings.port,
235
+ "library": settings.library or "aggregate bundled library.agg",
236
+ "library_loaded": uw is not None,
237
+ "library_recipes": recipes,
238
+ **facts,
239
+ }
240
+
241
+
242
+ def _settings_block(settings: Settings) -> dict:
243
+ """The live config knobs, so a deploy's actual settings are readable.
244
+
245
+ Notes
246
+ -----
247
+ Every field here is a knob an operator sets, and none of them is a secret:
248
+ the api has no credentials to leak. ``private_cidrs`` is echoed as text
249
+ rather than as parsed networks so what is shown is what was configured,
250
+ which is the form a typo is visible in.
251
+ """
252
+ return {
253
+ "log2_cap": settings.log2_cap,
254
+ "log2_default": settings.log2_default,
255
+ "build_timeout_s": settings.build_timeout_s,
256
+ "cache_max": settings.cache_max,
257
+ "session_max": settings.session_max,
258
+ "session_ttl_s": settings.session_ttl_s,
259
+ "max_chart_detail": settings.max_chart_detail,
260
+ "cors_origins": settings.cors_origins,
261
+ "audit_db": settings.audit_db,
262
+ "serve_spa": settings.serve_spa,
263
+ "private_cidrs": settings.private_cidrs_raw,
264
+ "status_require_zone_header": settings.status_require_zone_header,
265
+ "status_refresh_s": settings.status_refresh_s,
266
+ }
267
+
268
+
269
+ def _cache_block(cache: ObjectCache) -> dict:
270
+ """Counters plus a row per entry, LRU first so the next eviction reads first."""
271
+ block = cache.stats()
272
+ rows = []
273
+ for oid, entry in cache.items():
274
+ program, truncated = status_state.truncate(entry.decl, PROGRAM_CHARS)
275
+ rows.append({
276
+ "id": oid,
277
+ "kind": entry.kind,
278
+ "name": entry.name,
279
+ "log2": entry.log2,
280
+ "bs": entry.bs,
281
+ "created_at": entry.created_at.isoformat(timespec="seconds"),
282
+ "notes": len(entry.notes),
283
+ "estimated_bytes": status_state.estimated_bytes(entry.obj, entry.log2),
284
+ "program": program,
285
+ "truncated": truncated,
286
+ })
287
+ block["rows"] = rows
288
+ return block
289
+
290
+
291
+ def _watch_block(settings: Settings, cache: ObjectCache, audit: AuditLog) -> dict:
292
+ """The small things that earn their place, section 4.7 of the plan.
293
+
294
+ Notes
295
+ -----
296
+ Four items, each answering a question nothing else in this process surfaces.
297
+
298
+ **Notes volume.** How many cached objects carry a library warning, and which
299
+ warning is most common. A spike means the library started saying something
300
+ new about ordinary programs, which is exactly the ripple the oversight
301
+ charter asks each side to check for and which no route reports.
302
+
303
+ **Capability drift.** The live exhibit and chart registry counts against the
304
+ numbers the charter's state snapshot records. A library bump that registers
305
+ a new exhibit announces itself here rather than being noticed when somebody
306
+ goes looking for a tab.
307
+
308
+ **Static build freshness.** Whether the SPA bundle is mounted and how old
309
+ its ``index.html`` is. The two-stage build is a documented trap, and "the
310
+ deploy did not rebuild the web app" is otherwise diagnosed by confusion.
311
+
312
+ **Dead config.** A standing slot for settings that are read by nothing. It
313
+ is empty today, and the slot is kept so the next one is noticed.
314
+ """
315
+ entries = [entry for _, entry in cache.items()]
316
+ notes = Counter(note for entry in entries for note in entry.notes)
317
+ try:
318
+ from aggregate.charts import CHARTS
319
+ from aggregate.exhibits import EXHIBITS
320
+
321
+ registries = {
322
+ "exhibits": len(EXHIBITS),
323
+ "charts": len(CHARTS),
324
+ "charter_exhibits": CHARTER_EXHIBITS,
325
+ "charter_charts": CHARTER_CHARTS,
326
+ "drifted": len(EXHIBITS) != CHARTER_EXHIBITS or len(CHARTS) != CHARTER_CHARTS,
327
+ }
328
+ except Exception as exc: # noqa: BLE001 (a provisional module, by charter)
329
+ registries = {"unavailable": str(exc)}
330
+ return {
331
+ "notes": {
332
+ "entries_with_notes": sum(1 for entry in entries if entry.notes),
333
+ "distinct": len(notes),
334
+ "most_common": [{"note": text, "count": count}
335
+ for text, count in notes.most_common(5)],
336
+ },
337
+ "registries": registries,
338
+ "static": _static_freshness(settings),
339
+ "audit_db_bytes": audit.size_bytes(),
340
+ "unused_settings": [],
341
+ }
342
+
343
+
344
+ def _static_freshness(settings: Settings) -> dict:
345
+ """Whether the SPA bundle is mounted, and when its ``index.html`` was written.
346
+
347
+ Notes
348
+ -----
349
+ The import of ``_resolve_static_dir`` is function-local because ``app.py``
350
+ imports this module to mount it, so a module-level import would be a cycle.
351
+ Reimplementing the three lines instead would be a second copy of a
352
+ precedence order that has already changed once, which is the worse of the
353
+ two.
354
+ """
355
+ from ..app import _resolve_static_dir
356
+
357
+ directory = _resolve_static_dir(settings)
358
+ if directory is None:
359
+ return {"mounted": False, "reason": "serve_spa is off or no directory set"}
360
+ index = Path(directory) / "index.html"
361
+ try:
362
+ stat = index.stat()
363
+ except OSError:
364
+ return {"mounted": False, "directory": str(directory),
365
+ "reason": "no index.html; the web build has not run"}
366
+ built = datetime.fromtimestamp(stat.st_mtime, timezone.utc)
367
+ return {
368
+ "mounted": True,
369
+ "directory": str(directory),
370
+ "index_built_at": built.isoformat(timespec="seconds"),
371
+ "index_age_s": round(time.time() - stat.st_mtime, 1),
372
+ }
373
+
374
+
375
+ @router.get("/status", response_model=models.StatusResponse, include_in_schema=False)
376
+ def status(response: Response,
377
+ address: str = Depends(require_private),
378
+ settings: Settings = Depends(get_settings),
379
+ cache: ObjectCache = Depends(objects_routes._get_cache),
380
+ audit: AuditLog = Depends(objects_routes._get_audit),
381
+ sessions: SessionRegistry = Depends(objects_routes._get_sessions)) -> dict:
382
+ """Everything the operator's page shows, as JSON.
383
+
384
+ Notes
385
+ -----
386
+ ``no-store`` rather than a short max-age. The whole document is a snapshot
387
+ of a moving process, and a cached copy of it is worse than no copy: an
388
+ operator reading a thirty second old queue depth would draw the wrong
389
+ conclusion and have no way to tell.
390
+
391
+ The session baseline is the reference underwriter's recipe count, passed to
392
+ :meth:`aggregate_api.sessions.SessionRegistry.rows` so each session's column
393
+ reads what that session declared rather than the library plus what it
394
+ declared.
395
+ """
396
+ started = time.monotonic()
397
+ response.headers["Cache-Control"] = "no-store"
398
+ baseline = _recipe_count(_loaded_underwriter()) or 0
399
+ session_block = sessions.stats()
400
+ session_block["rows"] = [
401
+ {**row, "session_id": status_state.short_session_id(row["session_id"])}
402
+ for row in sessions.rows(baseline=baseline)
403
+ ]
404
+ builds = {"live": status_state.build_state()}
405
+ builds.update(audit.window_summaries(BUILD_WINDOWS))
406
+ # The slowest-build rows come from the audit log, which stores full session
407
+ # ids on purpose: it is the permanent record. This payload is not, so they
408
+ # are shortened on the way out, and the shortening happens in exactly the
409
+ # three places a session id can reach the wire.
410
+ for label, _ in BUILD_WINDOWS:
411
+ for row in builds[label]["slowest"]:
412
+ row["session_id"] = status_state.short_session_id(row["session_id"])
413
+ payload = {
414
+ "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
415
+ "generated_in_ms": 0.0,
416
+ "identity": _identity(settings),
417
+ "settings": _settings_block(settings),
418
+ "sessions": session_block,
419
+ "cache": _cache_block(cache),
420
+ "chart_cache": status_state.cache_state(
421
+ "chart",
422
+ len(objects_routes._chart_cache), objects_routes._CHART_CACHE_MAX),
423
+ "exhibit_cache": status_state.cache_state(
424
+ "exhibit",
425
+ len(objects_routes._exhibit_cache),
426
+ objects_routes._EXHIBIT_CACHE_MAX),
427
+ "builds": builds,
428
+ "key_scope": status_state.key_scope_state(),
429
+ "resources": resource_block.snapshot(settings.audit_db),
430
+ "gate": {**status_state.gate_state(), "admitted_on": address},
431
+ "watch": _watch_block(settings, cache, audit),
432
+ }
433
+ payload["generated_in_ms"] = round((time.monotonic() - started) * 1000, 2)
434
+ return payload
435
+
436
+
437
+ @router.get("/status/page", include_in_schema=False)
438
+ def status_page(_: str = Depends(require_private),
439
+ settings: Settings = Depends(get_settings)) -> Response:
440
+ """The operator's page: one self-contained file, no build step.
441
+
442
+ Notes
443
+ -----
444
+ **It must not go through Vite and must not live in** ``static/``. The web
445
+ build wipes that directory on every run, so anything placed there is deleted
446
+ by the next deploy, and a status page whose delivery depends on the pipeline
447
+ it exists to report on cannot report on that pipeline failing. So it is one
448
+ HTML file with inline CSS and inline JS, shipped inside the package and
449
+ served by this route.
450
+
451
+ The precedent is ``routes/meta.py``'s ``_ASSETS`` allow-list, whose comment
452
+ reads "Deliberately not a StaticFiles mount". Same argument, one file, and
453
+ no path from the request reaches the filesystem at all.
454
+
455
+ The refresh interval is substituted into the page rather than fetched,
456
+ because the page's first job on a slow box is to render, and one fewer round
457
+ trip before it can is worth a string replace.
458
+ """
459
+ body = _page_html().replace("__REFRESH_MS__",
460
+ str(int(settings.status_refresh_s * 1000)))
461
+ return HTMLResponse(content=body, headers={"Cache-Control": "no-store"})
462
+
463
+
464
+ def _page_html() -> str:
465
+ """Read the packaged page. Not cached: it is small, and a reload should show edits."""
466
+ return (files("aggregate_api") / "status_page.html").read_text(encoding="utf-8")