🏗️ Event Booker — Architecture Documentation¶
Architecture Classification¶
This application is a monolithic, single-process, polling-based automation bot.
| Question | Answer |
|---|---|
| What is this application? | A monolithic scheduled automation script that polls a website on a cron schedule, performs browser automation, and sends email notifications |
| How is it deployed? | As a systemd-managed daemon on an AWS EC2 instance (Amazon Linux 2023) |
Diagram 1 — Software Component Architecture¶
Shows all internal modules, their import dependencies, third-party libraries, and external systems.
graph TB
subgraph "Python Package: app_event_booker"
MAIN_ENTRY["__main__.py<br/><i>Entry point</i><br/>python -m app_event_booker"]
MAIN["main.py<br/><i>CLI + Scheduler</i><br/>argparse · APScheduler<br/>run_once() · run_scheduler()"]
CONFIG["config.py<br/><i>Configuration loader</i><br/>python-dotenv<br/>Reads .env → module-level constants"]
CRAWLER["crawler.py<br/><i>Browser automation</i><br/>CventBooker class<br/>Playwright Chromium"]
DISCOVERY["discovery.py<br/><i>Page analysis tool</i><br/>Dumps form elements to JSON<br/>Playwright Chromium"]
NOTIFIER["notifier.py<br/><i>Email notifications</i><br/>smtplib (stdlib)<br/>HTML email via Gmail SMTP"]
end
subgraph "Third-Party Libraries"
LIB_PW["🎭 playwright ≥1.40<br/><i>Browser automation</i>"]
LIB_APS["⏰ apscheduler ≥3.10<br/><i>Cron scheduler</i>"]
LIB_DOT["📄 python-dotenv ≥1.0<br/><i>.env file loader</i>"]
end
subgraph "Python Standard Library"
STD_SMTP["smtplib<br/><i>SMTP client</i>"]
STD_URL["urllib.request<br/><i>HTTP redirect resolver</i>"]
STD_ARG["argparse<br/><i>CLI argument parser</i>"]
STD_LOG["logging<br/><i>Dual-output logger</i>"]
STD_PATH["pathlib<br/><i>File path handling</i>"]
end
subgraph "External Systems"
CVENT_SHORT["🔗 cvent.me/e008PL<br/><i>Short URL (HTTP HEAD)</i>"]
CVENT_WEB["🌐 web.cvent.com<br/><i>Registration page</i><br/>JavaScript SPA"]
GMAIL["📧 smtp.gmail.com<br/><i>Port 587 (STARTTLS)</i><br/><i>Port 465 (SSL)</i>"]
end
subgraph "Local Filesystem"
FS_ENV[".env<br/><i>Secrets & configuration</i>"]
FS_BOOKED[".booked<br/><i>State flag file</i>"]
FS_STOP[".stop<br/><i>Graceful shutdown flag</i>"]
FS_SHOTS["screenshots/<br/><i>PNG at every step</i>"]
FS_DISC["discovery_output/<br/><i>JSON reports + HTML</i>"]
FS_LOG["event_booker.log<br/><i>Append-only log file</i>"]
end
%% Import dependencies (module → module)
MAIN_ENTRY -->|"imports"| MAIN
MAIN -->|"imports"| CONFIG
MAIN -->|"imports"| CRAWLER
MAIN -->|"imports"| NOTIFIER
MAIN -.->|"lazy import<br/>(--discover only)"| DISCOVERY
CRAWLER -->|"imports"| CONFIG
NOTIFIER -->|"imports"| CONFIG
DISCOVERY -->|"imports"| CONFIG
%% Library usage
CRAWLER -.-|"uses"| LIB_PW
DISCOVERY -.-|"uses"| LIB_PW
MAIN -.-|"uses"| LIB_APS
CONFIG -.-|"uses"| LIB_DOT
NOTIFIER -.-|"uses"| STD_SMTP
CRAWLER -.-|"uses"| STD_URL
MAIN -.-|"uses"| STD_ARG
%% External system interactions
CRAWLER -->|"HTTP HEAD<br/>resolve redirect"| CVENT_SHORT
CVENT_SHORT -->|"302 redirect"| CVENT_WEB
CRAWLER -->|"Chromium browser<br/>navigate · fill · submit"| CVENT_WEB
DISCOVERY -->|"Chromium browser<br/>analyse page elements"| CVENT_WEB
NOTIFIER -->|"SMTP TLS<br/>send HTML email"| GMAIL
%% Filesystem interactions
CONFIG -->|"reads"| FS_ENV
MAIN -->|"reads/writes"| FS_BOOKED
MAIN -->|"reads/writes"| FS_STOP
CRAWLER -->|"writes PNGs"| FS_SHOTS
DISCOVERY -->|"writes JSON,<br/>HTML, PNGs"| FS_DISC
MAIN -->|"appends"| FS_LOG
%% Styling
classDef module fill:#4A90D9,stroke:#2C5F8A,color:#fff,stroke-width:2px
classDef lib fill:#F5A623,stroke:#C77D0A,color:#fff,stroke-width:2px
classDef stdlib fill:#D4A574,stroke:#A07840,color:#fff,stroke-width:2px
classDef external fill:#E74C3C,stroke:#C0392B,color:#fff,stroke-width:2px
classDef filesystem fill:#27AE60,stroke:#1E8449,color:#fff,stroke-width:2px
class MAIN_ENTRY,MAIN,CONFIG,CRAWLER,DISCOVERY,NOTIFIER module
class LIB_PW,LIB_APS,LIB_DOT lib
class STD_SMTP,STD_URL,STD_ARG,STD_LOG,STD_PATH stdlib
class CVENT_SHORT,CVENT_WEB,GMAIL external
class FS_ENV,FS_BOOKED,FS_STOP,FS_SHOTS,FS_DISC,FS_LOG filesystem
Diagram 2 — Booking Pipeline Sequence Diagram¶
Shows the complete function call chain during a successful booking attempt, including all external network calls and responses.
sequenceDiagram
autonumber
participant SCH as ⏰ APScheduler<br/>(main.py)
participant RO as run_once()<br/>(main.py)
participant CB as CventBooker<br/>(crawler.py)
participant PW as 🎭 Playwright<br/>Chromium Browser
participant CVENT_S as 🔗 cvent.me<br/>(Short URL)
participant CVENT_W as 🌐 web.cvent.com<br/>(Registration SPA)
participant NF as notifier.py
participant GMAIL as 📧 Gmail SMTP
participant FS as 📁 Filesystem
Note over SCH: Cron fires at configured<br/>minutes (e.g., :00 and :30)
SCH->>RO: _scheduled_job() → run_once()
RO->>FS: Check .booked flag
FS-->>RO: Not found (proceed)
RO->>CB: CventBooker()
RO->>CB: book()
rect rgb(230, 240, 255)
Note over CB,PW: Phase 1 — Browser Launch
CB->>PW: launch()
PW-->>CB: Chromium started (headless)
end
rect rgb(255, 245, 230)
Note over CB,CVENT_W: Phase 2 — Navigation
CB->>CVENT_S: _resolve_url() HTTP HEAD
CVENT_S-->>CB: 302 → full web.cvent.com URL
CB->>PW: page.goto(resolved_url)
PW->>CVENT_W: HTTPS GET (load SPA)
CVENT_W-->>PW: HTML + JavaScript bundle
Note over PW: Wait 15s for JS SPA<br/>to finish rendering
CB->>FS: screenshot("01_page_loaded")
end
rect rgb(230, 255, 230)
Note over CB,CVENT_W: Phase 3 — Slot Detection
CB->>PW: find_available_slots()
PW->>PW: querySelectorAll<br/>'button[data-cvent-id="select-button"]'
PW-->>CB: List of {aria_label, day} slots
CB->>CB: pick_best_slot(slots)
Note over CB: Priority: 1) Preferred day<br/>2) Any weekend<br/>3) Any slot
alt No slots found
CB-->>RO: {success: false, error: "No slots"}
RO->>FS: screenshot("no_slots")
else No Saturday slot (preferred)
CB-->>RO: {success: false, error: "No Saturday"}
RO->>FS: screenshot("no_saturday_slots")
else Saturday slot found ✅
Note over CB: Proceed to form fill
end
end
rect rgb(240, 230, 255)
Note over CB,CVENT_W: Phase 4 — Form Fill (13+ fields)
CB->>PW: fill_form()
PW->>CVENT_W: Fill: First name, Last name
PW->>CVENT_W: Click radio: Age range 7-70
PW->>CVENT_W: Click radio: Residency status
PW->>CVENT_W: Fill: Date of birth
PW->>CVENT_W: Select dropdown: Gender
PW->>CVENT_W: Fill: Email, Phone, Organisation
PW->>CVENT_W: Click radios: Medication, Allergies, Injuries
PW->>CVENT_W: Fill: Emergency contact (name, relationship, phone)
CB->>FS: screenshot("03_form_filled")
opt HAS_GUEST is true
CB->>PW: _add_and_fill_guest()
PW->>CVENT_W: Click "Add a guest" button
PW->>CVENT_W: Fill guest fields (same 13 fields, 2nd occurrence)
CB->>FS: screenshot("03b_guest_filled")
end
end
rect rgb(255, 230, 230)
Note over CB,CVENT_W: Phase 5 — Slot Selection & Submit
CB->>PW: select_slot(best)
PW->>CVENT_W: Click "Select" button on chosen slot
CB->>FS: screenshot("02_slot_selected")
CB->>PW: submit()
PW->>CVENT_W: Click button#forward ("Next")
CVENT_W-->>PW: Registration submitted
Note over PW: Wait 8s for confirmation
CB->>FS: screenshot("04_after_submit")
end
CB->>PW: close()
PW-->>CB: Browser closed
CB-->>RO: {success: true, slot_text: "Saturday, 21 Mar..."}
rect rgb(230, 255, 245)
Note over RO,GMAIL: Phase 6 — Post-Booking
RO->>FS: _mark_booked() → write .booked flag
RO->>NF: notify_booking_success(slot_text, screenshot)
NF->>GMAIL: SMTP STARTTLS → login → send HTML email
GMAIL-->>NF: 250 OK
NF-->>RO: True (email sent)
end
Note over SCH: Subsequent cron fires will<br/>see .booked flag and skip
Diagram 3 — Deployment & Infrastructure Architecture¶
Shows how the application is deployed onto AWS EC2, managed by systemd, and interacts with external services.
graph TB
subgraph "Developer Machine (Windows)"
DEV_IDE["💻 IDE / Terminal<br/>(PyCharm / VS Code)"]
DEV_GIT["📂 Local Git Repo<br/>portfolio_tracker/"]
DEV_ENV["🔐 .env file<br/>(secrets — not in Git)"]
end
subgraph "GitHub"
GH_REPO["📦 darren2pro/portfolio_tracker<br/>(master branch)"]
end
subgraph "AWS EC2 Instance (Amazon Linux 2023)"
subgraph "systemd Service Layer"
SYSTEMD["🔧 systemd<br/>event-booker.service"]
JOURNALD["📋 journalctl<br/>(stdout/stderr logs)"]
end
subgraph "Application Runtime"
VENV["🐍 Python 3.13 venv<br/>/home/ec2-user/portfolio_tracker/venv/"]
APP["📦 app_event_booker<br/>python -m app_event_booker"]
APP_LOG["📄 event_booker.log"]
APP_SCREENSHOTS["📸 screenshots/"]
APP_ENV["🔐 .env<br/>(chmod 600)"]
end
subgraph "OS-Level Dependencies"
CHROMIUM["🌐 Playwright Chromium<br/>(headless browser binary)"]
SYS_LIBS["📚 System Libraries<br/>nss, nspr, atk, gtk3,<br/>mesa-libgbm, pango, ..."]
end
DEPLOY_SH["🚀 deploy_event_booker.sh<br/>git pull → pip install →<br/>playwright install →<br/>systemctl restart"]
end
subgraph "External Services"
CVENT["🌐 Cvent<br/>web.cvent.com<br/>(HTTPS — outbound 443)"]
GMAIL["📧 Gmail SMTP<br/>smtp.gmail.com<br/>(outbound 587/465)"]
end
%% Developer → GitHub
DEV_IDE --> DEV_GIT
DEV_GIT -->|"git push origin master"| GH_REPO
%% Developer → EC2
DEV_IDE -->|"SSH (port 22)<br/>portfolio-tracker-key.pem"| DEPLOY_SH
DEV_ENV -->|"SCP upload<br/>(one-time setup)"| APP_ENV
%% GitHub → EC2
GH_REPO -->|"git pull origin master<br/>(inside deploy script)"| DEPLOY_SH
%% Deploy script → Application
DEPLOY_SH -->|"pip install -r requirements.txt<br/>playwright install chromium"| VENV
DEPLOY_SH -->|"systemctl daemon-reload<br/>systemctl restart event-booker"| SYSTEMD
%% systemd → Application
SYSTEMD -->|"ExecStart=<br/>venv/bin/python -m app_event_booker"| APP
SYSTEMD -->|"stdout/stderr"| JOURNALD
SYSTEMD -->|"Restart=on-failure<br/>RestartSec=30"| SYSTEMD
%% Application internals
APP --> APP_LOG
APP --> APP_SCREENSHOTS
APP -->|"reads config"| APP_ENV
APP -->|"uses"| CHROMIUM
CHROMIUM -->|"depends on"| SYS_LIBS
APP --> VENV
%% Application → External
APP -->|"HTTPS (Chromium)<br/>Browse, fill, submit"| CVENT
APP -->|"SMTP TLS<br/>Send notifications"| GMAIL
%% systemd behaviors
SYSTEMD -.->|"auto-start on boot<br/>(WantedBy=multi-user.target)"| APP
SYSTEMD -.->|"auto-restart on crash<br/>(Restart=on-failure)"| APP
%% Styling
classDef dev fill:#5B9BD5,stroke:#2E75B6,color:#fff,stroke-width:2px
classDef github fill:#333,stroke:#555,color:#fff,stroke-width:2px
classDef ec2 fill:#FF9900,stroke:#CC7A00,color:#fff,stroke-width:2px
classDef systemd fill:#8E44AD,stroke:#6C3483,color:#fff,stroke-width:2px
classDef app fill:#27AE60,stroke:#1E8449,color:#fff,stroke-width:2px
classDef ext fill:#E74C3C,stroke:#C0392B,color:#fff,stroke-width:2px
classDef os fill:#D4A574,stroke:#A07840,color:#fff,stroke-width:2px
class DEV_IDE,DEV_GIT,DEV_ENV dev
class GH_REPO github
class SYSTEMD,JOURNALD systemd
class VENV,APP,APP_LOG,APP_SCREENSHOTS,APP_ENV,DEPLOY_SH app
class CVENT,GMAIL ext
class CHROMIUM,SYS_LIBS os
systemd Service Behaviour Summary¶
| Scenario | What Happens |
|---|---|
| SSH session closed | ✅ Service keeps running (managed by OS, not terminal) |
| EC2 reboots | ✅ Auto-starts (systemctl enable + WantedBy=multi-user.target) |
| Process crashes | ✅ Auto-restarts after 30 seconds (Restart=on-failure, RestartSec=30) |
systemctl stop event-booker |
❌ Service stops (manual stop is respected) |
.stop flag created |
✅ Scheduler shuts down gracefully before next cycle |
Diagram 4 — Scheduler & State Machine Flowchart¶
Shows the decision logic on every scheduler tick, including flag file checks, booking outcome branching, and state transitions.
flowchart TD
START(["⏰ APScheduler fires<br/>(cron: minute = 0,30)"]) --> CHECK_STOP{".stop flag<br/>exists?"}
CHECK_STOP -->|"Yes"| SHUTDOWN["🛑 Clear .stop flag<br/>scheduler.shutdown()"]
SHUTDOWN --> END_STOP(["Process exits"])
CHECK_STOP -->|"No"| RUN_ONCE["run_once()"]
RUN_ONCE --> CHECK_BOOKED{".booked flag<br/>exists?"}
CHECK_BOOKED -->|"Yes"| SKIP_BOOKED["⏭️ Log: Already booked<br/>(skip this cycle)"]
SKIP_BOOKED --> WAIT
CHECK_BOOKED -->|"No"| CREATE_BOOKER["Create CventBooker()"]
CREATE_BOOKER --> LAUNCH["launch()<br/>Start Chromium (headless)"]
LAUNCH --> NAVIGATE["navigate()<br/>Resolve short URL → load page<br/>Wait 15s for JS SPA"]
NAVIGATE --> FIND_SLOTS["find_available_slots()<br/>Query all Select buttons"]
FIND_SLOTS --> HAS_SLOTS{"Slots<br/>found?"}
HAS_SLOTS -->|"No slots at all"| NO_SLOTS["📸 screenshot: no_slots<br/>Log: No slot buttons found"]
NO_SLOTS --> CLOSE_BROWSER
HAS_SLOTS -->|"Yes"| ENOUGH_SLOTS{"≥ REQUIRED_SLOTS<br/>available?"}
ENOUGH_SLOTS -->|"No"| NOT_ENOUGH["📸 screenshot: not_enough_slots<br/>Log: Need N, found M"]
NOT_ENOUGH --> CLOSE_BROWSER
ENOUGH_SLOTS -->|"Yes"| PICK_BEST["pick_best_slot()<br/>1) Preferred day<br/>2) Any weekend<br/>3) Any slot"]
PICK_BEST --> IS_SATURDAY{"Best slot is<br/>a preferred day<br/>(Saturday)?"}
IS_SATURDAY -->|"No (and Saturday<br/>is preferred)"| NO_SAT["📸 screenshot: no_saturday_slots<br/>Log: No Saturday slot yet"]
NO_SAT --> CLOSE_BROWSER
IS_SATURDAY -->|"Yes ✅"| FILL_FORM["fill_form()<br/>Fill 13+ registration fields"]
FILL_FORM --> HAS_GUEST{"HAS_GUEST<br/>= true?"}
HAS_GUEST -->|"Yes"| FILL_GUEST["_add_and_fill_guest()<br/>Click 'Add a guest'<br/>Fill guest's 13 fields"]
HAS_GUEST -->|"No"| SELECT_SLOT
FILL_GUEST --> SELECT_SLOT["select_slot(best)<br/>Click the Select button"]
SELECT_SLOT --> SUBMIT["submit()<br/>Click Next / Submit button"]
SUBMIT --> SUBMIT_OK{"Submit<br/>succeeded?"}
SUBMIT_OK -->|"Yes ✅"| SUCCESS["🎉 Booking SUCCESS!<br/>📸 screenshot: booking_complete"]
SUCCESS --> MARK_BOOKED["Write .booked flag file"]
MARK_BOOKED --> NOTIFY_SUCCESS["📧 notify_booking_success()<br/>Send email via Gmail SMTP"]
NOTIFY_SUCCESS --> CLOSE_BROWSER
SUBMIT_OK -->|"No"| SUBMIT_FAIL["❌ Submit failed<br/>📸 screenshot: submit_failed"]
SUBMIT_FAIL --> NOTIFY_ERROR["📧 notify_error()<br/>Send error email"]
NOTIFY_ERROR --> CLOSE_BROWSER
CLOSE_BROWSER["close()<br/>Shut down Chromium"] --> WAIT
WAIT(["💤 Wait for next cron tick<br/>(e.g., 30 min)"])
%% Styling
classDef success fill:#27AE60,stroke:#1E8449,color:#fff,stroke-width:2px
classDef fail fill:#E74C3C,stroke:#C0392B,color:#fff,stroke-width:2px
classDef wait fill:#F39C12,stroke:#D68910,color:#fff,stroke-width:2px
classDef decision fill:#3498DB,stroke:#2471A3,color:#fff,stroke-width:2px
classDef action fill:#5DADE2,stroke:#2E86C1,color:#fff,stroke-width:2px
classDef stop fill:#8E44AD,stroke:#6C3483,color:#fff,stroke-width:2px
class SUCCESS,MARK_BOOKED,NOTIFY_SUCCESS success
class NO_SLOTS,NOT_ENOUGH,NO_SAT,SUBMIT_FAIL,NOTIFY_ERROR fail
class WAIT,SKIP_BOOKED wait
class CHECK_STOP,CHECK_BOOKED,HAS_SLOTS,ENOUGH_SLOTS,IS_SATURDAY,HAS_GUEST,SUBMIT_OK decision
class SHUTDOWN,END_STOP stop
Technology Stack Summary¶
| Layer | Technology | Purpose |
|---|---|---|
| Language | Python 3.13 | Application runtime |
| Browser Automation | Playwright (Chromium) | Navigate, fill forms, click, screenshot |
| Scheduling | APScheduler (BlockingScheduler + CronTrigger) | In-process cron-style recurring execution |
| Configuration | python-dotenv + .env file |
Externalised secrets and settings |
| smtplib (stdlib) + Gmail App Password | SMTP TLS notifications | |
| URL Resolution | urllib.request (stdlib) | Follow short URL redirects |
| CLI | argparse (stdlib) | Command-line interface modes |
| Logging | logging (stdlib) | Dual-output: stdout + file |
| State Management | Flag files (.booked, .stop) |
Prevent duplicate bookings, graceful shutdown |
| Process Management | systemd (Linux) | Daemon lifecycle, auto-restart, boot persistence |
| Cloud Infrastructure | AWS EC2 (Amazon Linux 2023) | 24/7 server hosting |
| Source Control | Git + GitHub | Version control and deployment source |
| Deployment | deploy_event_booker.sh + SCP |
Pull, install, restart via shell script |
Screenshots¶
Coming soon
Screenshots of the Playwright browser automation in action will be added here.