Enrich results DB, add process log, and build a live web dashboard

Requested during deployment: make the DB maximally detailed, add an
experiment process log, and a web UI to watch runs live.

- optim/objective.py build_detail(): each stored run now records
  inter-coil distances, absolute coil positions along the tube, every
  component part number/spec, winding geometry, computed physics
  (resistance, air inductance, peak current, peak field), and per-stage
  outcome (entry/exit velocity, sensor timing, energy breakdown)
- optim/progress_log.py: append-only <db>.log with progress %, feasible
  rate, throughput, ETA, and a line on each new best efficiency; wired
  into both sweep and evolve
- src/gausse/web/: stdlib-only (http.server) dashboard `gausse serve` --
  self-contained HTML polling /api/overview every 3s: counters, KPD
  histogram, top-configs table with per-run drill-down, failure reasons,
  live log tail. No new dependencies.
- docker-compose.yml: `web` service on port 8000
- 77 tests pass locally and in Docker

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-07 00:27:50 +05:00
parent d487021c2f
commit 70b732d170
12 changed files with 636 additions and 5 deletions

77
tests/test_web.py Normal file
View File

@@ -0,0 +1,77 @@
import json
import threading
import urllib.request
from http.server import ThreadingHTTPServer
from gausse.optim.sweep import run_sweep
from gausse.web.page import PAGE_HTML
from gausse.web.stats import overview, run_detail
def _make_db(tmp_path):
db = tmp_path / "runs.sqlite3"
run_sweep(db, n_runs=60, n_workers=2, seed=11)
return db
def test_overview_reports_counts_and_top(tmp_path):
db = _make_db(tmp_path)
o = overview(db, db.with_suffix(".sqlite3.log"))
assert o["total"] == 60
assert o["feasible"] + o["infeasible"] == 60
assert len(o["efficiency_histogram"]) == 10
# лог процесса должен существовать и попасть в хвост
assert len(o["log_tail"]) > 0
if o["top"]:
# топ отсортирован по КПД по убыванию
effs = [t["efficiency"] for t in o["top"]]
assert effs == sorted(effs, reverse=True)
def test_run_detail_has_full_physics_and_distances(tmp_path):
db = _make_db(tmp_path)
o = overview(db, None)
assert o["top"], "нужен хотя бы один реализуемый прогон"
d = run_detail(db, o["top"][0]["run_id"])
assert d is not None
detail = d["detail"]
# ключевое, что просил пользователь: расстояния между катушками и позиции
assert "inter_stage_gaps_m" in detail
assert "coil_center_positions_m" in detail
stage0 = detail["stages"][0]
assert "electrical" in stage0 and "wire_resistance_ohm" in stage0["electrical"]
assert "winding" in stage0 and "total_turns" in stage0["winding"]
assert "sensor_to_coil_distance_m" in stage0
def test_run_detail_missing_id_returns_none(tmp_path):
db = _make_db(tmp_path)
assert run_detail(db, "нет-такого-id") is None
def test_http_server_serves_page_and_api(tmp_path):
from functools import partial
from gausse.web.server import _Handler
db = _make_db(tmp_path)
handler = partial(_Handler, db_path=db, log_path=db.with_suffix(".sqlite3.log"))
httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
port = httpd.server_address[1]
t = threading.Thread(target=httpd.serve_forever, daemon=True)
t.start()
try:
base = f"http://127.0.0.1:{port}"
page = urllib.request.urlopen(base + "/", timeout=5).read().decode("utf-8")
assert "<!doctype html>" in page.lower()
api = json.loads(urllib.request.urlopen(base + "/api/overview", timeout=5).read())
assert api["total"] == 60
finally:
httpd.shutdown()
httpd.server_close()
def test_page_is_self_contained():
# никаких внешних ресурсов (CSP-безопасно): ни http-ссылок в src/href, ни CDN
assert "src=\"http" not in PAGE_HTML
assert "href=\"http" not in PAGE_HTML