#!/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 "" 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": b.get("doing", "")}) page = {"spend": {"vast_usd": round(vast, 2), "runpod_usd": round(runpod, 2), "cap_vast": 900, "cap_runpod": 450, "updated_at": now()}, "phases": phases, "boxes": boxes, "results": list(res.values()), "night": night, "log": log[-200:]} 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)