diff --git a/lyra/modes.py b/lyra/modes.py index 950e53c..3155cf9 100644 --- a/lyra/modes.py +++ b/lyra/modes.py @@ -44,7 +44,7 @@ _CASH_TOOLS = _BASE + _LOOKUPS + ( "start_session", "add_buyin", "log_stack", "log_hand", "record_hand", "add_read", "analyze_spot", "session_stats", "session_state", "end_session", "generate_recap", "scar_note", "confidence_bank", "alligator_blood", "reset_ritual", - "undo_last", + "undo_last", "update_session", ) # Talk mode also gets start_session as the *entry point*: opening a session from a diff --git a/lyra/poker.py b/lyra/poker.py index 13868e7..19f2e31 100644 --- a/lyra/poker.py +++ b/lyra/poker.py @@ -341,6 +341,50 @@ def _resolve(session_id: int | None) -> int | None: return live["id"] if live else None +def review_session_id() -> int | None: + """The session to attach reflective entries to: the live one if any, else the + most-recent real session (closed). Lets rituals/notes land while reviewing after + you've racked up. Excludes the standing 'Hand Reviews' bucket.""" + live = live_session() + if live: + return live["id"] + r = _c().execute( + "SELECT id FROM poker_sessions WHERE status != 'review' ORDER BY id DESC LIMIT 1" + ).fetchone() + return int(r["id"]) if r else None + + +_EDITABLE = ("venue", "stakes", "game", "format", "buy_in_total", "cash_out", + "mantra", "mood") + + +def update_session(session_id: int, **fields) -> dict | None: + """Edit session details (during or after play). Only known columns are touched; + net is recomputed when buy-in/cash-out change and both are known.""" + s = get_session(session_id) + if not s: + return None + sets, vals = [], [] + for k, v in fields.items(): + if k in _EDITABLE and v is not None: + sets.append(f"{k} = ?") + vals.append(float(v) if k in ("buy_in_total", "cash_out") else v) + if sets: + conn = _c() + with conn: + conn.execute(f"UPDATE poker_sessions SET {', '.join(sets)} WHERE id = ?", + (*vals, session_id)) + s = get_session(session_id) + # keep net consistent if the money fields changed and both are present + if s.get("cash_out") is not None and s.get("buy_in_total") is not None: + net = float(s["cash_out"]) - float(s["buy_in_total"]) + if net != s.get("net"): + with conn: + conn.execute("UPDATE poker_sessions SET net = ? WHERE id = ?", (net, session_id)) + s = get_session(session_id) + return s + + def add_buyin(amount: float, session_id: int | None = None) -> float: """Add a buy-in/rebuy to a session. Returns the new total in.""" sid = _resolve(session_id) @@ -1090,7 +1134,10 @@ def hud(session_id: int | None = None) -> dict | None: "id": sid, "venue": s.get("venue"), "stakes": s.get("stakes"), "game": s.get("game"), "format": s.get("format"), "status": s.get("status"), "started_at": s.get("started_at"), - "buy_in_total": s.get("buy_in_total"), + "ended_at": s.get("ended_at"), "hours": s.get("hours"), + "buy_in_total": s.get("buy_in_total"), "cash_out": s.get("cash_out"), + "net": s.get("net"), "mantra": s.get("mantra"), "mood": s.get("mood"), + "is_live": s.get("status") == "live", "has_recap": bool(s.get("recap_md")), }, "stack": { "current": state["current"], "buy_in": state["buy_in"], "net": state["net"], diff --git a/lyra/tools.py b/lyra/tools.py index b5111b5..d98c3ed 100644 --- a/lyra/tools.py +++ b/lyra/tools.py @@ -117,6 +117,21 @@ def _log_stack(args: dict, ctx: dict) -> str: return f"Stack ${amount:g} logged" + (f" (net {net:+.0f})." if net is not None else ".") +def _update_session(args: dict, ctx: dict) -> str: + sid = poker.review_session_id() + if sid is None: + return "No session to edit yet." + fields = {k: args.get(k) for k in ("venue", "stakes", "game", "format", + "buy_in_total", "cash_out", "mantra", "mood") if args.get(k) not in (None, "")} + if not fields: + return "Tell me what to change (venue, stakes, game, buy-in, etc.)." + s = poker.update_session(sid, **fields) + if not s: + return "Couldn't find that session." + changed = ", ".join(f"{k}={v}" for k, v in fields.items()) + return f"Session #{sid} updated — {changed}." + + def _undo_last(args: dict, ctx: dict) -> str: what = (args.get("what") or "").strip().lower() aliases = {"hands": "hand", "stacks": "stack", "reads": "read", @@ -143,11 +158,11 @@ def _scar_note(args: dict, ctx: dict) -> str: cls = (args.get("classification") or "").strip().lower() or None if cls and cls not in ("punt", "cooler", "standard"): cls = None - try: - poker.log_ritual("scar", content=content, classification=cls, - hand_id=args.get("hand_id")) - except ValueError: - return "No live session — start one and I'll keep the scar notes." + sid = poker.review_session_id() # live, or the most-recent session (post-game review) + if sid is None: + return "No session yet — start one and I'll keep the scar notes." + poker.log_ritual("scar", content=content, classification=cls, + hand_id=args.get("hand_id"), session_id=sid) return f"Scar note logged{f' ({cls})' if cls else ''}." @@ -155,10 +170,10 @@ def _confidence_bank(args: dict, ctx: dict) -> str: content = (args.get("content") or "").strip() if not content: return "Nothing to bank — tell me the good process." - try: - poker.log_ritual("confidence", content=content, hand_id=args.get("hand_id")) - except ValueError: - return "No live session — start one and I'll run the confidence bank." + sid = poker.review_session_id() + if sid is None: + return "No session yet — start one and I'll run the confidence bank." + poker.log_ritual("confidence", content=content, hand_id=args.get("hand_id"), session_id=sid) return "Banked. 💰" @@ -174,10 +189,10 @@ def _alligator_blood(args: dict, ctx: dict) -> str: def _reset_ritual(args: dict, ctx: dict) -> str: content = (args.get("content") or "").strip() or None - try: - poker.log_ritual("reset", content=content) - except ValueError: - return "No live session to reset." + sid = poker.review_session_id() + if sid is None: + return "No session to reset." + poker.log_ritual("reset", content=content, session_id=sid) return "Reset logged. Clean slate — this is a new session in your head." @@ -389,6 +404,20 @@ TOOLS.update({ "add_buyin": {"handler": _add_buyin, "spec": _f( "add_buyin", "Record a rebuy / additional buy-in in the live session.", {"amount": {**_N, "description": "Amount added"}}, ["amount"])}, + "update_session": {"handler": _update_session, "spec": _f( + "update_session", + "Edit details of the current/most-recent session — during or after play. Use " + "when Brian corrects something ('change the stakes to 2/5', 'venue was actually " + "Bellagio', 'I bought in for 600', 'cashed out 1240'). Only pass fields that change.", + {"venue": {**_S, "description": "Casino/room"}, + "stakes": {**_S, "description": "e.g. '1/3', '2/5'"}, + "game": {**_S, "description": "NLH, PLO, ..."}, + "format": {**_S, "description": "cash | tournament"}, + "buy_in_total": {**_N, "description": "Total bought in"}, + "cash_out": {**_N, "description": "Final cashout (recomputes net)"}, + "mantra": {**_S, "description": "Pre-session focus/anchor"}, + "mood": {**_S, "description": "Mental-game note"}}, + [])}, "undo_last": {"handler": _undo_last, "spec": _f( "undo_last", "Undo/delete the most recent logged entry in the live session when Brian says " diff --git a/lyra/web/server.py b/lyra/web/server.py index 9b3d5bf..79785c9 100644 --- a/lyra/web/server.py +++ b/lyra/web/server.py @@ -108,11 +108,19 @@ def create_app() -> FastAPI: return FileResponse(str(_STATIC / "session.html")) @app.get("/session/data") - async def session_hud_data() -> dict: - """The current live session's HUD bundle (or {session: None} if none open).""" - bundle = await asyncio.to_thread(poker.hud) + async def session_hud_data(id: int | None = None) -> dict: + """HUD bundle for the live session, or a specific past session via ?id=.""" + bundle = await asyncio.to_thread(poker.hud, id) return bundle or {"session": None} + @app.patch("/session/{session_id}") + async def session_update(session_id: int, request: Request) -> dict: + """Edit a session's details (venue/stakes/game/buy-in/cash-out/…).""" + body = await request.json() + s = await asyncio.to_thread(lambda: poker.update_session(session_id, **body)) + logbus.log("info", "session edited", id=session_id, fields=list(body)) + return {"ok": s is not None, "session": s} + @app.delete("/session/entry/{kind}/{entry_id}") async def delete_entry(kind: str, entry_id: int) -> dict: """Delete one HUD entry (hand | stack | read | ritual) by id.""" diff --git a/lyra/web/static/history.html b/lyra/web/static/history.html index 167cf0c..43224c1 100644 --- a/lyra/web/static/history.html +++ b/lyra/web/static/history.html @@ -85,7 +85,7 @@ const date=(s.started_at||'').slice(0,10); const meta=[date,s.venue,`${s.hands} hand${s.hands===1?'':'s'}`, s.hours?`${(+s.hours).toFixed(1)}h`:''].filter(Boolean).join(' · '); - const href=s.has_recap?`/recap/${s.id}`:`/session`; + const href=`/session?id=${s.id}`; // read-only HUD detail for any session const net=s.net!=null?money(s.net):(s.status==='live'?'live':'—'); return `