#!/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 time, 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": ""}]) dj = os.path.join(ROOT, "disk.json") if not os.path.exists(dj) or time.time() - os.path.getmtime(dj) > 300: try: import importlib.util; spec = importlib.util.spec_from_file_location("disk_sweep", os.path.join(os.path.dirname(os.path.abspath(__file__)), "disk-sweep.py")); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); m.sweep(alert=True) except Exception as e: print("disk sweep failed:", str(e)[:120]) disk = jl("disk.json", {}) 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"), "disk_pct": (disk.get(b["label"]) or {}).get("pct"), "disk_free_gb": (disk.get(b["label"]) or {}).get("avail_gb"), "disk_red": bool(((disk.get(b["label"]) or {}).get("pct") or 0) >= 85), "standing": bool(b.get("standing")), "role": b.get("role", ""), "standing_since": b.get("standing_since"), "uptime_h": round(hours(b.get("standing_since") or b["rented_at"]), 1) if b.get("standing") and b.get("state") != "destroyed" else None, "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")] stand = [b for b in running if b["standing"]] standing_block = {"count": len(stand), "usd_h": round(sum(b["rate_usd_h"] for b in stand), 2), "usd_day": round(24 * sum(b["rate_usd_h"] for b in stand)), "roles": {r: sum(1 for b in stand if b["role"] == r) for r in sorted(set(b["role"] for b in stand))}, "rule": "Standing boxes (the project lead, 6 October 2026) stay up and are never destroyed on a job's end: 16 on the live devnet (mining, proving, voting) and 6 on Devnet 2, about USD 8/h; a dead host is re-rented in the same shape, the node runs under a supervisor with the recovery recipe, the version follows the live manifest by the update job, and no standing box joins an experiment (experiments use wave boxes); each should carry a mapped p2p port where the provider allows, so the live devnet stops being a star."} 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, "standing": standing_block, "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)