Skip to content

M1 unbundled + deploy pipeline + M2 PR A (foundation) - #1

Open
ArchDLayer wants to merge 3 commits into
mainfrom
claude/repo-exploration-do94hs
Open

M1 unbundled + deploy pipeline + M2 PR A (foundation)#1
ArchDLayer wants to merge 3 commits into
mainfrom
claude/repo-exploration-do94hs

Conversation

@ArchDLayer

@ArchDLayer ArchDLayer commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Status (updated as work continues — read this section first)

This PR is being built across multiple sessions/conversations. This section
is the continuity log so a fresh session (or you, on your phone) can pick up
without re-reading the whole chat history. Commit messages also carry
per-commit rationale.

Done and pushed:

  1. Unbundled challenge_keeper.py (M1) into the real module layout, rebuilt
    tools/build_single_file.py, verified byte-identical regeneration.
  2. Deployment pipeline: systemd unit, cloud-init, GitHub Actions deploy,
    phone click-path README.
  3. Fixed a pre-existing bug in M1: models.create_challenge() defaulted
    start_date to date.today(), violating CLAUDE.md's own rule. Now a
    required argument; callers resolve "today" via resolve_local_date().
  4. M2 PR A (foundation): /newchallenge guided setup, post+pin the text
    board, Join button → enrollment → DM deep-link → timezone picker
    (quick-pick zones + free-text "Other"). See bot.py, config.py,
    strings.py, challenge_setup.py, handlers/.

Decisions made along the way, and why:

  • Freeze tokens: skipped. The plan flags that reset_progress (one
    missed day wipes all 100 days) likely causes dropout around week six.
    Decided to surface longest_streak prominently instead of implementing
    freeze tokens — reset_progress stays fully strict, freezes_left stays
    unused (models.enrol() still defaults it to 0).
  • Grace window: kept at 3 hours (the existing default, unchanged).
  • PR size: fewer, larger PRs rather than one per plan step. M2 is split
    into four: A (this one, foundation)B (core loop: check-in,
    repeat-tap handling, full board / my stats buttons — the plan's own M2
    "done when" checkpoint) → C (image board: avatars, Pillow rendering)
    D (refresh job).
  • Testing: unit tests only for now. This sandbox's network policy blocks
    both api.telegram.org and PyPI installs, so python-telegram-bot can't
    be installed or imported here. Everything not touching telegram is
    tested directly (98 tests passing: the original 76 + challenge_setup.py
    validation + a call-sequence integration test mirroring what the handlers
    do). The bot.py/handlers/ Telegram wiring itself is unverified by
    execution — first real test happens once this is running somewhere with
    real network access (the VPS, or a Termux stopgap).
  • Quick-pick timezones: Europe/Vilnius, Warsaw, London, Lisbon, Athens,
    Madrid — matches the zones already used in the M1 demo data.

Telegram-side state (as of the session that built PR A): a throwaway dev
bot (@Challange2536_bot) exists with privacy mode enabled, added as admin
to a throwaway group ("Challange group"). Its token was shared directly in
that chat session, not stored anywhere — if it's needed again and lost,
regenerate via BotFather → /mybots → the bot → API Token. This is all
throwaway/dev-only; the real bot and group get created separately later.

Not started: PR B onward. M2's Telegram-facing wiring has not been
tested against live Telegram at all yet.


Original summary (M1 unbundle + deploy pipeline)

challenge_keeper.py was a single-file transport bundle for M1 (data layer
and streak engine). This PR expands it into the real project, per
HANDOFF.md, and builds the deployment pipeline ahead of there being a bot
to deploy.

Unbundled:

  • streaks.py, db.py, models.py, views.py, render_text.py,
    tools/demo.py, tests/*, migrations/001_init.sql — extracted
    programmatically from the bundle, not retyped
  • tools/build_single_file.py reconstructed — this is what generated the
    bundle originally. Running it regenerates challenge_keeper.py
    byte-for-byte identical to the file you uploaded. challenge_keeper.py is
    a build artifact now — edit the modules and rerun the build script, per
    CLAUDE.md.
  • requirements.txt, .env.example, .gitignore, README.md, CLAUDE.md

Deployment pipeline (deploy/, .github/workflows/deploy.yml):

  • deploy/challengebot.service — systemd unit, non-root user, Restart=always
  • deploy/cloud-init.yaml — first-boot setup targeting Oracle Cloud ARM free
    tier (Ubuntu/aarch64), with Hetzner fallback notes
  • .github/workflows/deploy.yml — on push to main: SSH in, git reset --hard
    to the new commit, reinstall requirements only if requirements.txt
    changed, restart the service
  • deploy/README.md — the phone click-path: generating the two SSH keypairs
    involved, which GitHub secrets to add and where, what to paste into the
    cloud provider's instance-creation form, how to confirm it worked

Until bot.py exists, challengebot.service crash-loops harmlessly under
Restart=always. Now that PR A adds bot.py, that's no longer true once
this merges and deploys — worth re-checking service status after that.

What you need to do by hand

  1. Review this PR (still growing — check back before merging, or ask for it
    to be merged now and continue on a follow-up PR/branch).
  2. Create the real VPS when ready — deploy/README.md has the click-path.
  3. Add the three GitHub Actions secrets (VPS_HOST, VPS_USER,
    VPS_SSH_KEY).
  4. When ready to test for real: either wait for the VPS, or use Termux as a
    stopgap (flagged in the plan) to run the bot directly on your phone.

One open decision, flagged not resolved (from the original M1 PR)

The freeze-token questionresolved above: skipped in favor of
surfacing longest_streak.

claude added 3 commits August 8, 2026 16:48
…ent pipeline

M1 (data layer and streak engine) was shipped as a single-file bundle for
transport onto a phone-only workflow. This expands it back into the proper
repo layout described in HANDOFF.md:

- streaks.py, db.py, models.py, views.py, render_text.py, tools/demo.py,
  tests/*, migrations/001_init.sql, extracted programmatically from the
  bundle's _SOURCES/_TESTS/_SCHEMA_SQL dicts
- tools/build_single_file.py reconstructed as the script that produces
  challenge_keeper.py; regenerating it reproduces the original bundle
  byte-for-byte
- requirements.txt, .env.example, .gitignore, README.md
- CLAUDE.md with standing instructions for future sessions

Also builds the deployment pipeline ahead of there being a bot to deploy:

- deploy/challengebot.service — systemd unit, non-root user, Restart=always
- deploy/cloud-init.yaml — first-boot setup for Oracle ARM free tier
  (Ubuntu/aarch64), notes on Hetzner differences
- .github/workflows/deploy.yml — SSH deploy on push to main, restarts the
  service, reinstalls requirements only if they changed
- deploy/README.md — phone click-path: which GitHub secrets to add, what to
  paste into the provider's form, how to check the service afterwards

Verified: 76 tests pass, python3 -m tools.demo and python3 challenge_keeper.py
both print the same sample board.

challengebot.service will crash-loop harmlessly until M2 adds bot.py — the
goal here is a proven pipeline, not a running bot. M2 is not started.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTxQxm8bFaQDEFVdJEafnt
…default

Pre-existing in the M1 bundle: create_challenge() fell back to date.today()
when start_date wasn't given. CLAUDE.md's own rule is that nothing in this
codebase calls date.today() directly -- every instant becomes a day through
streaks.resolve_local_date() so the user's timezone and grace window are
always applied. This was never exercised because every existing caller
(tools/demo.py, tests/test_models.py) already passed start_date explicitly,
but the upcoming /newchallenge handler is the first real caller that could
have hit it.

start_date is now a required keyword argument. The three test call sites
that relied on the default now pass one explicitly. Bundle regenerated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTxQxm8bFaQDEFVdJEafnt
Foundation milestone from challenge-bot-plan.md section 5, M2, steps 1-4:

- config.py: TELEGRAM_BOT_TOKEN / DATABASE_PATH from the environment
- strings.py: every user-facing string, keyed
- challenge_setup.py: name/target/timezone validation and the quick-pick
  zone list (Vilnius/Warsaw/London/Lisbon/Athens/Madrid). No Telegram
  import, same isolation principle as streaks.py/views.py -- this is the
  only new module that's actually testable in an environment without a
  live `telegram` install.
- handlers/admin.py: is_group_admin(), checked live via getChatMember,
  never a hardcoded username list
- handlers/commands.py: the /newchallenge conversation (name, target,
  unit, mode, miss policy, confirm), ending by creating the challenge and
  posting + pinning the text board with a Join button
- handlers/callbacks.py: the Join button (enrols, then deep-links to DM)
  and the timezone conversation (/start tz, /settz, quick-pick + free-text
  "Other" validated against zoneinfo)
- bot.py: wires everything into a polling Application

Product decisions from discussion: freeze tokens are not wired up this
milestone (freezes=0, matching models.enrol()'s default) -- longest_streak
stays the mitigation for reset_progress dropout instead. Grace window
stays at the 3-hour default. The admin who runs /newchallenge is not
auto-enrolled.

Testing note: this environment cannot install python-telegram-bot (no
outbound PyPI access), so nothing importing `telegram` could be executed
here. What's covered by tests/test_challenge_setup.py (16 tests) and
tests/test_m2_setup_flow.py (6 tests) is every non-Telegram call the
handlers make -- validation and the exact models.py/views.py sequence
each flow uses -- run directly rather than through Telegram objects. The
bot.py/handlers wiring itself is unverified by execution; first real test
is the VPS. 98 tests total, all passing. The M1 bundle is untouched --
challenge_keeper.py stays standard-library-only by design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTxQxm8bFaQDEFVdJEafnt
@ArchDLayer ArchDLayer changed the title Unbundle M1 into a real repo layout, add deployment pipeline M1 unbundled + deploy pipeline + M2 PR A (foundation) Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants