Skip to content

🏗️ 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
Email 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.