Mbonea Cursor commited on
Commit
cc4e53a
·
1 Parent(s): fb29daa

Raise schedule max blocks to 20 and add Dar-day plan push script.

Browse files
Files changed (2) hide show
  1. app/config.py +1 -1
  2. scripts/push_day_plan.py +341 -0
app/config.py CHANGED
@@ -49,7 +49,7 @@ class Settings(BaseSettings):
49
  schedule_day_start: str = "06:00"
50
  schedule_day_end: str = "23:00"
51
  schedule_timezone: str = "Africa/Dar_es_Salaam"
52
- schedule_max_blocks: int = 7
53
 
54
  @property
55
  def is_prod(self) -> bool:
 
49
  schedule_day_start: str = "06:00"
50
  schedule_day_end: str = "23:00"
51
  schedule_timezone: str = "Africa/Dar_es_Salaam"
52
+ schedule_max_blocks: int = 20
53
 
54
  @property
55
  def is_prod(self) -> bool:
scripts/push_day_plan.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Push tomorrow's ChatPlan to Habit Journal (Dar es Salaam date).
3
+
4
+ Auth from env / .env: AGENT_TOKEN preferred, else APP_PASSWORD session.
5
+ Never prints secrets. EMBASSY_DAY=1 switches to embassy variant (not default).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ import uuid
14
+ from datetime import datetime, timedelta
15
+ from pathlib import Path
16
+ from typing import Any
17
+ from urllib.error import HTTPError, URLError
18
+ from urllib.request import Request, build_opener, HTTPCookieProcessor
19
+ from http.cookiejar import CookieJar
20
+
21
+ try:
22
+ from zoneinfo import ZoneInfo
23
+ except ImportError: # pragma: no cover
24
+ ZoneInfo = None # type: ignore
25
+
26
+
27
+ ROOT = Path(__file__).resolve().parents[1]
28
+
29
+
30
+ def load_dotenv() -> None:
31
+ path = ROOT / ".env"
32
+ if not path.is_file():
33
+ return
34
+ for line in path.read_text(encoding="utf-8").splitlines():
35
+ line = line.strip()
36
+ if not line or line.startswith("#") or "=" not in line:
37
+ continue
38
+ key, val = line.split("=", 1)
39
+ key = key.strip()
40
+ val = val.strip().strip('"').strip("'")
41
+ os.environ.setdefault(key, val)
42
+
43
+
44
+ def dar_tomorrow() -> str:
45
+ if ZoneInfo is not None:
46
+ try:
47
+ tz = ZoneInfo("Africa/Dar_es_Salaam")
48
+ return (datetime.now(tz).date() + timedelta(days=1)).isoformat()
49
+ except Exception: # noqa: BLE001
50
+ pass
51
+ # Fallback: UTC+3 approx (EAT)
52
+ return (datetime.utcnow().date() + timedelta(days=1)).isoformat()
53
+
54
+
55
+ def minutes_between(start: str, end: str) -> int:
56
+ sh, sm = map(int, start.split(":"))
57
+ eh, em = map(int, end.split(":"))
58
+ return max(1, (eh * 60 + em) - (sh * 60 + sm))
59
+
60
+
61
+ def pack_chat_plan(day: str) -> dict[str, Any]:
62
+ blocks_spec = [
63
+ ("07:30", "08:00", "Wake - water - leave bed", "P0", "body_care", "duty", False, "No shame scroll. Phone after standing."),
64
+ ("08:00", "08:25", "Interrupt walk (no headphones)", "P0", "body_care", "measure", False, "Different path if possible. Eyes outside. Wallet home. Preview ≠ proof."),
65
+ ("08:25", "08:50", "Breakfast + hygiene", "P0", "food_out", "duty", False, "Eat before admin spiral."),
66
+ ("08:50", "09:05", "App Daily open + movement fields", "P1", "stabilize", "measure", False, "Mark left_home / interrupt minutes. Court closed."),
67
+ ("09:05", "10:35", "P0 Fix AXA insurance PDF", "P0", "admin_spain", "duty", True, "BLOCKER. Residence=Tanzania. Coverage 26 Aug 2026–5 Sep 2026. Schengen min €30k. Replace SCHBE000291367 if still wrong country. Save PDF to embassy folder."),
68
+ ("10:35", "10:50", "Break - water - no status war", "P2", "stabilize", "restore_fun", False, "No her thread. No mockers."),
69
+ ("10:50", "12:20", "P0 Refundable stay proof MAD (+ BCN)", "P0", "admin_spain", "duty", True, "Flexible/refundable only while visa pending. Budget ~€110/night MAD, ~€130 BCN, ~€960 total band. Save confirmations PDF. Do not over-research."),
70
+ ("12:20", "13:15", "Lunch + rest (not bed freeze)", "P0", "food_out", "duty", False, "Leave bedroom if freeze risk."),
71
+ ("13:15", "14:00", "P0 Qatar hold PDF + print pack list", "P0", "admin_spain", "duty", True, "PNR 9QBP66 hold PDF only — DO NOT PAY. Cover letter + itinerary print-ready. Checklist for next walk-in (form, photo, fee, residence, AXA, stays, funds)."),
72
+ ("14:00", "14:20", "Buffer / compound walk", "P2", "body_care", "measure", False, "Still no cinema headphones if FSE high."),
73
+ ("14:20", "15:20", "Earn: offer + 3 payment asks", "P1", "earn_ship", "duty", False, "One sentence offer + price. Send ≥3 asks. Price once then silence. Pay-first or 50% deposit. Metric=TZS not likes. Solo no partners."),
74
+ ("15:20", "15:40", "Break", "P2", "stabilize", "restore_fun", False, ""),
75
+ ("15:40", "16:15", "Embassy folder: bank + bonds proof", "P1", "admin_spain", "duty", False, "One physical/digital folder. Statements + investment/bonds as discussed. No UTT drama if bonds path chosen."),
76
+ ("16:15", "16:40", "Restore — music max 25m AFTER proof only", "P2", "restore_fun", "restore_fun", False, "Only if P0 AXA or stays moved. Else skip music; short silent walk. Leisure not medicine."),
77
+ ("16:40", "17:05", "Seal: Log + plan checkoffs + day_review notes", "P1", "stabilize", "measure", False, "Log AXA/stays/asks. FSE in feedback notes if spiked. List gaps for next embassy day."),
78
+ ("22:00", "22:30", "Sleep wind-down", "P0", "sleep_window", "duty", False, "No multi-escape spiral. Phone out of bed if possible."),
79
+ ]
80
+ empty_review = {
81
+ "did": None,
82
+ "minutes": None,
83
+ "comment": "",
84
+ "emotions": [],
85
+ "fse_event": "",
86
+ "intensity": None,
87
+ }
88
+ blocks = []
89
+ for start, end, title, priority, kind, intent, locked, notes in blocks_spec:
90
+ blocks.append(
91
+ {
92
+ "id": str(uuid.uuid4()),
93
+ "start": start,
94
+ "end": end,
95
+ "title": title,
96
+ "priority": priority,
97
+ "kind": kind,
98
+ "intent": intent,
99
+ "notes": notes,
100
+ "locked": locked,
101
+ "status": "planned",
102
+ "review": dict(empty_review),
103
+ }
104
+ )
105
+ return {
106
+ "schema_version": 1,
107
+ "date": day,
108
+ "title": "Spain pack + first asks",
109
+ "intention": (
110
+ "Close AXA + stay proof + hold PDF. Three customer asks. "
111
+ "No embassy unless pack 100% and day is Mon/Wed/Fri. No flight pay."
112
+ ),
113
+ "constraints": [
114
+ "do not pay Qatar until visa stamped",
115
+ "AXA residence must be Tanzania not Spain/Belgium",
116
+ "interrupt walk: no headphones first 20 min",
117
+ "Weeknd/music only after P0 proof",
118
+ "wallet home on morning walk",
119
+ "no WhatsApp Spain flex",
120
+ "solo earn - no partners",
121
+ ],
122
+ "blocks": blocks,
123
+ "day_review": {
124
+ "comment": "",
125
+ "emotions": [],
126
+ "fse_events": "",
127
+ "what_moved": "",
128
+ "what_avoided": "",
129
+ "tomorrow_change": "",
130
+ },
131
+ }
132
+
133
+
134
+ def to_day_plan_put(chat: dict[str, Any], day: str) -> dict[str, Any]:
135
+ notes = (
136
+ "INTENTION: Close AXA + stay proof + hold PDF. Three customer asks. "
137
+ "No flight pay. Embassy only if Mon/Wed/Fri AND pack complete.\n"
138
+ "CONSTRAINTS: no pay Qatar pre-stamp; AXA residence Tanzania; walk no HP 20m; "
139
+ "Weeknd after P0; wallet home morning; no Spain status flex; solo earn\n"
140
+ "AXA: fix/replace if residence Spain/Belgium; dates 26 Aug–5 Sep 2026; PNR hold 9QBP66 PDF only"
141
+ )
142
+ blocks = []
143
+ for raw in chat["blocks"]:
144
+ start, end = raw["start"], raw["end"]
145
+ blocks.append(
146
+ {
147
+ "id": raw.get("id") or str(uuid.uuid4()),
148
+ "date": day,
149
+ "start": start,
150
+ "end": end,
151
+ "title": raw["title"],
152
+ "kind": raw["kind"],
153
+ "intent": raw["intent"],
154
+ "priority": raw["priority"],
155
+ "planned_min": minutes_between(start, end),
156
+ "status": "planned",
157
+ "source": "cursor",
158
+ "locked": bool(raw.get("locked")),
159
+ "notes": raw.get("notes") or "",
160
+ "version_added": 1,
161
+ }
162
+ )
163
+ return {
164
+ "source": "cursor",
165
+ "capacity_hint": 0.75,
166
+ "notes": notes,
167
+ "title": chat.get("title") or "",
168
+ "intention": chat.get("intention") or "",
169
+ "constraints": chat.get("constraints") or [],
170
+ "day_review": chat.get("day_review") or {},
171
+ "force_p0_move": True,
172
+ "blocks": blocks,
173
+ }
174
+
175
+
176
+ class Client:
177
+ def __init__(self, base: str) -> None:
178
+ self.base = base.rstrip("/")
179
+ self.jar = CookieJar()
180
+ self.opener = build_opener(HTTPCookieProcessor(self.jar))
181
+ self.endpoint = ""
182
+
183
+ def request(
184
+ self,
185
+ method: str,
186
+ path: str,
187
+ *,
188
+ body: dict | None = None,
189
+ bearer: str | None = None,
190
+ raw: bool = False,
191
+ ) -> Any:
192
+ data = None if body is None else json.dumps(body).encode("utf-8")
193
+ headers = {"Accept": "application/json"}
194
+ if body is not None:
195
+ headers["Content-Type"] = "application/json"
196
+ if bearer:
197
+ headers["Authorization"] = f"Bearer {bearer}"
198
+ req = Request(f"{self.base}{path}", data=data, headers=headers, method=method)
199
+ try:
200
+ with self.opener.open(req, timeout=60) as resp:
201
+ text = resp.read().decode("utf-8")
202
+ if raw:
203
+ return text
204
+ return json.loads(text) if text else {}
205
+ except HTTPError as exc:
206
+ err_body = exc.read().decode("utf-8", errors="replace")
207
+ raise RuntimeError(f"HTTP {exc.code} {path}: {err_body[:800]}") from exc
208
+ except URLError as exc:
209
+ raise RuntimeError(f"Network error {path}: {exc}") from exc
210
+
211
+
212
+ def push(day: str) -> int:
213
+ load_dotenv()
214
+ base = os.environ.get("HF_SPACE_URL", "https://Mbonea-Fastwhisper.hf.space").rstrip("/")
215
+ agent = (os.environ.get("AGENT_TOKEN") or "").strip()
216
+ password = (os.environ.get("APP_PASSWORD") or "").strip()
217
+ client = Client(base)
218
+ chat = pack_chat_plan(day)
219
+ put = to_day_plan_put(chat, day)
220
+
221
+ # Peek existing
222
+ prev_count = None
223
+ try:
224
+ if agent:
225
+ prev = client.request("GET", f"/api/agent/context?day={day}", bearer=agent)
226
+ if prev.get("ok") and isinstance(prev.get("data"), dict):
227
+ plan = prev["data"].get("current_plan") or {}
228
+ prev_count = len(plan.get("blocks") or [])
229
+ except Exception: # noqa: BLE001
230
+ pass
231
+
232
+ last_err = None
233
+ # 1) Import ChatPlan (session or agent session login)
234
+ # Prefer agent PUT DayPlanPut
235
+ if agent:
236
+ try:
237
+ env = client.request(
238
+ "PUT",
239
+ f"/api/agent/plan/{day}",
240
+ body=put,
241
+ bearer=agent,
242
+ )
243
+ if env.get("ok"):
244
+ client.endpoint = f"PUT /api/agent/plan/{day} (bearer)"
245
+ else:
246
+ raise RuntimeError(str(env.get("error")))
247
+ except Exception as exc: # noqa: BLE001
248
+ last_err = exc
249
+ agent = agent # keep for later GET
250
+ # fall through
251
+ env = None
252
+ else:
253
+ return report(client, day, env, prev_count, agent_token=agent)
254
+
255
+ if password:
256
+ login = client.request("POST", "/api/auth/login", body={"password": password})
257
+ if not login.get("ok"):
258
+ print(f"LOGIN_FAIL: {login.get('error')}", file=sys.stderr)
259
+ return 1
260
+ # Try import first (ChatPlan native)
261
+ try:
262
+ env = client.request(
263
+ "POST",
264
+ f"/api/plan/{day}/import",
265
+ body={"plan": chat, "mode": "replace"},
266
+ )
267
+ if env.get("ok"):
268
+ client.endpoint = f"POST /api/plan/{day}/import (session)"
269
+ return report(client, day, env, prev_count)
270
+ last_err = RuntimeError(str(env.get("error")))
271
+ except Exception as exc: # noqa: BLE001
272
+ last_err = exc
273
+ try:
274
+ env = client.request("PUT", f"/api/plan/{day}", body=put)
275
+ if env.get("ok"):
276
+ client.endpoint = f"PUT /api/plan/{day} (session)"
277
+ return report(client, day, env, prev_count)
278
+ last_err = RuntimeError(str(env.get("error")))
279
+ except Exception as exc: # noqa: BLE001
280
+ last_err = exc
281
+
282
+ print(f"PUSH_FAILED: {last_err}", file=sys.stderr)
283
+ print("Set AGENT_TOKEN or APP_PASSWORD in env/.env (not git).", file=sys.stderr)
284
+ return 1
285
+
286
+
287
+ def report(
288
+ client: Client,
289
+ day: str,
290
+ env: dict[str, Any],
291
+ prev_count: int | None,
292
+ agent_token: str | None = None,
293
+ ) -> int:
294
+ # Prefer GET plan with session; agent context as fallback
295
+ data = env.get("data") if isinstance(env.get("data"), dict) else {}
296
+ blocks = data.get("blocks")
297
+ if not isinstance(blocks, list):
298
+ try:
299
+ got = client.request("GET", f"/api/plan/{day}")
300
+ if got.get("ok"):
301
+ data = got["data"]
302
+ blocks = data.get("blocks") or []
303
+ except Exception: # noqa: BLE001
304
+ if agent_token:
305
+ got = client.request(
306
+ "GET", f"/api/agent/context?day={day}", bearer=agent_token
307
+ )
308
+ plan = (got.get("data") or {}).get("current_plan") or {}
309
+ blocks = plan.get("blocks") or []
310
+ data = plan
311
+ else:
312
+ blocks = []
313
+
314
+ n = len(blocks)
315
+ titles = [str(b.get("title") or "") for b in blocks]
316
+ print(f"DAY={day}")
317
+ print(f"ENDPOINT={client.endpoint}")
318
+ if prev_count is not None:
319
+ print(f"PREV_BLOCKS={prev_count}")
320
+ print(f"BLOCKS={n}")
321
+ print("FIRST_THREE=" + " | ".join(titles[:3]))
322
+ if data.get("title"):
323
+ print(f"TITLE={data.get('title')}")
324
+ if n < 14:
325
+ print(f"VERIFY_FAIL: expected >=14 blocks, got {n}", file=sys.stderr)
326
+ return 1
327
+ print("OK")
328
+ return 0
329
+
330
+
331
+ def main() -> int:
332
+ load_dotenv()
333
+ day = os.environ.get("DAY") or dar_tomorrow()
334
+ if os.environ.get("EMBASSY_DAY", "0") == "1":
335
+ print("EMBASSY_DAY=1 not implemented in this script; use pack day.", file=sys.stderr)
336
+ return 2
337
+ return push(day)
338
+
339
+
340
+ if __name__ == "__main__":
341
+ raise SystemExit(main())