An automated system that monitors Fuhuihua (馥薈華) on Tock for reservation releases and books a table in seconds — before they sell out.
Playwright with stealth patches. Handles Cloudflare Turnstile, maintains session cookies, supports headed/headless modes.
src/browser.pyMulti-selector fallback system tries 6 different DOM selectors. Clicks calendar days regardless of CSS class, extracts time from parent text.
src/checker.pyTwo-phase sniper: Phase 1 (first 60s) is a lightweight no-op skip — slots haven't dropped yet. Phase 2 fires concurrent calendar scans with abort-on-first-slot. Page reuse across polls. Session pre-warm 15 min before window.
src/monitor.pyFires asyncio tasks for each available slot simultaneously. First successful booking sets an Event flag — all others abort instantly.
src/booker.pyColor-coded webhook embeds: green (confirmed), yellow (slots found), orange (sniper active), red (errors), blue (dry-run).
src/notifier.pyLogs every detected slot to JSON + CSV with timestamp, date, time, and day of week. Used to reverse-engineer the release schedule.
src/tracker.py| Severity | Bug | Root Cause | Status |
|---|---|---|---|
| Critical | Booker can't click days | booker.py used available_day_button (requires is-available class), Fuhuihua never has this class | ✓ Fixed |
| Critical | Checker finds 0 slots on live site | Only tried Consumer-resultsListItem.is-available selector. Real UI uses hashed CSS classes with "Book" text buttons | ✓ Fixed |
| Critical | Sniper fires 2 min late | int() truncation: 0.5s delta → 0 → hold check bypassed → non-sniper sequential poll ran through the window | ✓ Fixed |
| High | 3+ min poll during sniper | Unreleased dates triggered 60s pagination timeouts. Each of 3 unreleased dates = 180s wasted | ✓ Fixed |
| High | Notifier shows wrong slot count | self._poll_count (cumulative poll #) passed as slots_found to sniper_mode_ended() | ✓ Fixed |
| Medium | PM2 Chromium crash loop | MachPortRendezvousServer failure. PM2 daemon corrupted after 30 rapid restart cycles | ✓ Fixed |
| Medium | playwright-stealth v2 API break | stealth_async() renamed to Stealth().apply_stealth_async() in v2 | ✓ Fixed |
| Medium | Discord token in plaintext | Hardcoded in start-claude.sh, tracked by git | ✓ Fixed |
| High | Debug PNGs + .claude/ config unignored | Authenticated page captures and machine-local Claude settings could be accidentally staged and pushed | ✓ Fixed |
| High | Generic "Book" button clicks wrong slot | _click_time_slot() clicked the first visible Book button without verifying the parent element contained the target time text | ✓ Fixed |
| High | _wait_for_checkout() times out early | Waited on a single DOM selector for checkout — timing mismatch caused premature timeout before checkout page rendered | ✓ Fixed |
| Medium | Phase 1 still doing full calendar scans | test_sniper_phases never set monitor._sniper_active, so keep_pages=False in every poll — the Phase 1 early-return never fired | ✓ Fixed |
| Medium | Saved card detection false negative | saved_payment_card selector didn't match Tock's live UI; _has_saved_card() returned False despite card being on file | ✓ Fixed |
Validates DOM selectors against the live Tock site. First line of defense when Tock updates their frontend.
Full flow without booking. Logs what it would have booked. Safe for production testing.
Navigates to checkout on a test restaurant (Benu). Verifies saved card, CVC field, confirm button. Screenshots the state.
Sends all 4 Discord notification types. Verifies webhook URL is correct and messages render properly.
Validates session pre-warm, page reuse with reload, and confirm retry logic. Tests all sniper components in isolation.
Single poll cycle then exit. Fast validation that the full scan works without committing to the monitoring loop.
Drives N polls straddling the sniper window start. Validates Phase 1 (pre-release no-op, no calendar scans) transitions correctly to Phase 2 (aggressive concurrent scan).
Unit tests with mocked DOM passed 39/39 while the live site used completely different CSS classes. Always validate against real pages with --test-booking-flow.
Fixing detection without fixing booking means you can see slots but can't grab them. Every selector change must be applied to both code paths.
A 2-minute timing drift (int truncation) and 3-minute poll cycles (pagination timeouts) meant missing slots that sold out in under 60 seconds.
The screenshot showing "New reservations will be released on March 27 at 8:00 PM PDT" confirmed the exact release schedule and revealed the modal calendar structure.
Every successful feature started with research → plan → implement → test. Skipping the plan led to assumptions that broke on the live site.
PM2 daemon corruption, Chrome/Playwright port conflicts, conda vs system Python paths — production environment issues cost as much time as code bugs.
All critical fixes deployed and validated. Two-phase sniper, concurrent abort, correct saved-card detection, and checkout polling all confirmed. Waiting for next Friday 8PM release window.
Add date validation after clicking a calendar day — confirm the UI actually transitioned to the target date before attempting to book.
Match AvailableSlot.slot_time to the specific button clicked, so the bot books the preferred time slot rather than just the first available.
Move from Mac Mini to a cloud VM for true 24/7 uptime without Chrome conflicts or PM2 daemon issues.