igneum/tools/fleet/page.py

63 lines
4.5 KiB
Python
Executable file

#!/usr/bin/env python3
"""Builds ~/Desktop/fleet/fleet.json (the fleet page's single source) from the registry (boxes.json), the ledger, the
result rows (results.json: {card_key: {...}}), the night rows (night.json: [..]), the phases (phases.json) and the log
(log.jsonl), then publishes it with the master checkout's tools/fleet/publish-fleet.sh (at most once a minute).
page.py log "<text>" append a log line (and rebuild, no publish)
page.py phase p1 state=running done=3 note="..."
page.py build rebuild only
page.py publish rebuild and publish (skipped when the last publish was under 60 s ago unless --force)
"""
import json, os, sys, time, datetime, subprocess
ROOT = os.path.expanduser("~/Desktop/fleet"); OUT = os.path.join(ROOT, "fleet.json")
PUBLISH = "/Users/joshm/Projects/igneum/tools/fleet/publish-fleet.sh"
def now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def jl(name, default):
p = os.path.join(ROOT, name)
if not os.path.exists(p): return default
if name.endswith(".jsonl"): return [json.loads(l) for l in open(p) if l.strip()]
return json.load(open(p))
def hours(a, b=None):
t0 = datetime.datetime.strptime(a, "%Y-%m-%dT%H:%M:%SZ")
t1 = datetime.datetime.strptime(b, "%Y-%m-%dT%H:%M:%SZ") if b else datetime.datetime.utcnow()
return max((t1 - t0).total_seconds(), 0) / 3600
def build():
reg = jl("boxes.json", {}); res = jl("results.json", {}); night = jl("night.json", []); log = jl("log.jsonl", [])
phases = jl("phases.json", [
{"order": 1, "name": "Memory matrix on real cards", "state": "planned", "planned": 11, "done": 0, "note": ""},
{"order": 2, "name": "Prover fleet on the devnet", "state": "planned", "planned": 25, "done": 0, "note": ""},
{"order": 3, "name": "The 8x rigs", "state": "planned", "planned": 2, "done": 0, "note": ""},
{"order": 4, "name": "AMD mining", "state": "planned", "planned": 3, "done": 0, "note": ""}])
boxes = []; vast = 0.0; runpod = 0.0
for iid, b in reg.items():
h = b.get("hours") if b.get("state") == "destroyed" else hours(b["rented_at"])
cost = round(h * b["dph"], 3)
if b.get("provider", "vast") == "vast": vast += cost
else: runpod += cost
boxes.append({"id": iid, "label": b["label"], "card": b["card"], "vram_gb": round((b.get("vram_mb") or 0) / 1024), "provider": b.get("provider", "vast"),
"rate_usd_h": b["dph"], "phase": b.get("phase", "1"), "state": "done" if b.get("state") == "destroyed" else b.get("state", "renting"),
"started_at": b["rented_at"], "ended_at": b.get("destroyed_at"), "hours": round(h, 2), "cost_usd": cost, "doing": (("done, destroyed: " + (b.get("doing") or "its measurement is in")) if b.get("state") == "destroyed" else b.get("doing", ""))})
running = [b for b in boxes if b["state"] not in ("done", "failed")]
page = {"spend": {"vast_usd": round(vast, 2), "runpod_usd": round(runpod, 2), "cap_vast": 1000, "cap_runpod": 500, "updated_at": now(), "running": len(running), "running_note": open(os.path.join(ROOT, "why.txt")).read().strip() if os.path.exists(os.path.join(ROOT, "why.txt")) else ""},
"phases": phases, "boxes": boxes, "results": list(res.values()), "night": night, "log": log[-200:],
"devnet2": jl("devnet2-status.json", {"state": "building", "boxes": 0, "height": None, "last_gate": "none yet", "last_gate_at": None})}
json.dump(page, open(OUT, "w"), indent=1); return page
def publish(force=False):
stamp = os.path.join(ROOT, ".last-publish")
if not force and os.path.exists(stamp) and time.time() - os.path.getmtime(stamp) < 60: print("published under a minute ago; skipped"); return
build(); open(stamp, "w").write(now())
r = subprocess.run([PUBLISH, OUT], capture_output=True, text=True, timeout=300); print((r.stdout + r.stderr).strip()[-300:])
if __name__ == "__main__":
c = sys.argv[1] if len(sys.argv) > 1 else "build"
if c == "log":
with open(os.path.join(ROOT, "log.jsonl"), "a") as f: f.write(json.dumps({"at": now(), "text": sys.argv[2]}) + "\n")
build()
elif c == "phase":
ph = jl("phases.json", None) or build()["phases"]
for p in ph:
if f"p{p['order']}" == sys.argv[2]:
for kv in sys.argv[3:]:
k, v = kv.split("=", 1); p[k] = int(v) if v.isdigit() else v
json.dump(ph, open(os.path.join(ROOT, "phases.json"), "w")); build()
elif c == "publish": publish("--force" in sys.argv)
else: build(); print(OUT)