diff --git a/app/cli.py b/app/cli.py index 47152fb..6e6f2a3 100644 --- a/app/cli.py +++ b/app/cli.py @@ -94,6 +94,57 @@ async def show_status(email: str) -> int: return 0 +async def send_test_digest(email: str, kind: str) -> int: + """Generate a digest and send it to the named user immediately, ignoring + opt-in state and idempotency. Useful for previewing copy in your own + inbox before a real run lands.""" + import httpx + + from app.jobs._market_context import ( + REFERENCE_LINE, + latest_quotes_by_group, + recent_headlines_by_bucket, + ) + from app.jobs.email_digest_job import _generate_variants, _send_one + from app.services.openrouter import llm_configured + + if kind not in ("daily", "weekly"): + print(f"error: kind must be 'daily' or 'weekly' (got {kind!r})", + file=sys.stderr) + return 2 + if not llm_configured(): + print("error: LLM provider not configured (set OPENROUTER_API_KEY)", + file=sys.stderr) + return 1 + + factory = get_session_factory() + async with factory() as session: + user = await _get_user_by_email(session, email) + if user is None: + print(f"error: no user with email {email!r}", file=sys.stderr) + return 1 + today = _utcnow() + quotes = await latest_quotes_by_group(session) + news = await recent_headlines_by_bucket( + session, hours=(168 if kind == "weekly" else 24), + ) + ctx = dict(today=today, quotes_by_group=quotes, + headlines_by_bucket=news, reference_line=REFERENCE_LINE) + async with httpx.AsyncClient(follow_redirects=True) as client: + variants = await _generate_variants(session, client, kind, ctx) + tone = (user.digest_tone or "INTERMEDIATE").upper() + content = (variants.get(tone) + or variants.get("INTERMEDIATE") + or next(iter(variants.values()), None)) + if content is None: + print("error: all LLM variants failed", file=sys.stderr) + return 1 + date_str = today.strftime("%Y-%m-%d") + await _send_one(user, kind, content, date_str, session) + print(f"sent {kind} digest to {email} (tone={tone})") + return 0 + + def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(prog="app.cli", description="Cassandra admin CLI") sub = p.add_subparsers(dest="cmd", required=True) @@ -108,6 +159,11 @@ def build_parser() -> argparse.ArgumentParser: s = sub.add_parser("show-status", help="Print paid-tier status for a user") s.add_argument("email") + t = sub.add_parser("send-test-digest", + help="Send one digest immediately (bypasses opt-in/idempotency)") + t.add_argument("email") + t.add_argument("kind", choices=("daily", "weekly")) + return p @@ -122,6 +178,8 @@ async def _dispatch(args) -> int: return await revoke_credit(args.email) if args.cmd == "show-status": return await show_status(args.email) + if args.cmd == "send-test-digest": + return await send_test_digest(args.email, args.kind) return 2 finally: await get_engine().dispose()