๐Ÿ“š BusinessExplain ๐Ÿ  Home ๐Ÿ“Š Analytics
โ† Back to Skills

Whatsapp Chat Viewer

โš™๏ธ DevOps & Infrastructure 20 min read v1.0.0
๐Ÿ“‹ Contents

WhatsApp Chat Viewer

Web dashboard for browsing Hermes WhatsApp session transcripts in a WhatsApp Web-like dark theme. Reads session JSONL files directly from ~/.hermes/sessions/, resolves group names from the WhatsApp Bridge API, and serves them via Flask + nginx with SSL and auth.

Live at: https://wa.hkus2.s4s.host/ (munaf:munaf basic auth)

Architecture

Browser โ†’ nginx (SSL + basic auth) โ†’ Flask (port 9376) โ†’ session JSONL files
                                          โ†“
                                   WhatsApp Bridge (port 3000)
                                     GET /chat/{jid} โ†’ group metadata
  • Flask app at /var/www/whatsapp-chat/server.py โ€” reads sessions, fetches group names from bridge
  • Frontend at /var/www/whatsapp-chat/index.html โ€” WhatsApp Web-like dark UI, localStorage read tracking
  • Systemd service whatsapp-chat.service โ€” runs Flask on port 9376
  • Nginx โ€” SSL termination + basic auth, /api/ routes exempt from auth (JS fetch requirement)
  • Deployment

    Flask App

    # Install Flask + CORS
    /usr/local/lib/hermes-agent/venv/bin/pip install flask flask-cors
    

    Create app directory

    mkdir -p /var/www/whatsapp-chat

    Copy server.py and index.html there

    Systemd Service

    # /etc/systemd/system/whatsapp-chat.service
    [Unit]
    Description=WhatsApp Chat Viewer
    After=network.target
    

    [Service]
    Type=simple
    User=root
    WorkingDirectory=/var/www/whatsapp-chat
    ExecStart=/usr/local/lib/hermes-agent/venv/bin/python3 /var/www/whatsapp-chat/server.py
    Restart=always
    RestartSec=5

    [Install]
    WantedBy=multi-user.target

    systemctl daemon-reload
    systemctl enable --now whatsapp-chat.service
    

    Nginx + SSL + Auth

    # /etc/nginx/sites-available/whatsapp-chat
    server {
        listen 80;
        server_name wa.example.com;
        return 301 https://$host$request_uri;
    }
    

    server {
    listen 443 ssl;
    server_name wa.example.com;

    ssl_certificate /etc/letsencrypt/live/wa.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/wa.example.com/privkey.pem;

    # API endpoints โ€” NO auth (JS fetch needs this)
    location /api/ {
    proxy_pass http://127.0.0.1:9376;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Everything else โ€” basic auth
    location / {
    auth_basic "WhatsApp Chat";
    auth_basic_user_file /etc/nginx/.htpasswd-wa;

    proxy_pass http://127.0.0.1:9376;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    }
    }

    # Create auth file
    htpasswd -bc /etc/nginx/.htpasswd-wa  
    

    Enable site + SSL

    ln -sf /etc/nginx/sites-available/whatsapp-chat /etc/nginx/sites-enabled/ certbot --nginx -d wa.example.com nginx -t && systemctl reload nginx

    How It Works

    Session Key Format

    Hermes stores WhatsApp sessions in ~/.hermes/sessions/sessions.json with keys like:

    agent:main:whatsapp:dm:923332312410                           โ†’ DM
    agent:main:whatsapp:group:120363403860678579@g.us:923332312410 โ†’ Group
    

    The Flask backend parses these keys to determine chat type and extract the phone/JID.

    Group Name Resolution

    The WhatsApp Bridge (Baileys) runs on port 3000 and exposes several HTTP endpoints:

  • GET /chat/:jid โ€” Chat metadata (group names, participants). Used by the chat viewer.
  • GET /health โ€” Bridge status and uptime check
  • GET /messages โ€” Long-poll for incoming WhatsApp messages
  • POST /send-media โ€” Send images, videos, audio, documents as native WhatsApp attachments
  • def get_group_name(group_jid):
        """Fetch group name from WhatsApp bridge."""
        url = f"{BRIDGE_URL}/chat/{group_jid}"
        # group_jid format: "120363403860678579@g.us"
        # Response: {"id": "...", "name": "Khilji house", ...}
    

    Full bridge API docs: see hermes-agent skill, references/whatsapp-bridge-api.md

    The backend caches group names in-memory with a 5-minute TTL to avoid hammering the bridge.

    Read/Unread Tracking

    The frontend uses localStorage to track the last-read timestamp per chat. When chat.updated_at > readState[chat.id], the chat shows an unread badge. The "โœ“ Read All" button sets all chats to their current updated_at.

    Auto-Refresh

    The frontend polls /api/chats every 10 seconds and re-renders the sidebar. If a chat is currently open, it also refreshes messages. Scroll position is preserved on refresh.

    Pitfalls

    Systemd Service Management

    Gateway Service (hermes-gateway.service)

    # /etc/systemd/system/hermes-gateway.service
    [Unit]
    Description=Hermes Gateway + WhatsApp Bridge
    After=network.target
    

    [Service]
    Type=simple
    User=root
    WorkingDirectory=/root/.hermes
    ExecStart=/usr/local/bin/hermes gateway run --replace
    KillMode=control-group # CRITICAL: kills gateway + bridge.js child
    TimeoutStopSec=210
    Restart=always
    RestartSec=10

    [Install]
    WantedBy=multi-user.target

    systemctl daemon-reload
    systemctl enable --now hermes-gateway.service
    systemctl restart hermes-gateway.service  # Clean restart
    

    Why KillMode=control-group: Without it, killing the gateway process does NOT kill the child bridge.js process. The orphaned bridge continues forwarding messages to a dead gateway, causing WhatsApp to silently stop responding. Check with:

    systemctl status hermes-gateway.service  # Shows gateway + bridge in CGroup
    pkill -f "bridge.js" && systemctl restart hermes-gateway # Force clean state

    ๐Ÿ”ด Nginx Auth + JS fetch Conflict

    When nginx uses auth_basic and the browser navigates via https://user:pass@domain/, JavaScript fetch() calls to the same origin fail with:

    Window.fetch: /api/chats is an url with embedded credentials
    

    This is a browser security restriction โ€” embedded credentials in the URL are stripped from sub-requests. credentials: 'include' in fetch options does NOT fix this when the URL itself contains user:pass@.

    Fix: Exempt /api/ routes from auth_basic in nginx config. The page itself is still protected (browser shows auth dialog), but API calls from the authenticated page work without re-prompting.

    location /api/ {
        # NO auth_basic here!
        proxy_pass http://127.0.0.1:9376;
    }
    

    ๐Ÿ”ด Group JID Parsing โ€” Don't Split on :

    The session key format agent:main:whatsapp:group:GROUPID@g.us:PHONE contains colons inside the group JID (e.g., @g.us). Naive key.split(":") breaks the JID.

    Correct parsing:

    if ":group:" in key:
    after_group = key.split(":group:")[1] # "GROUPID@g.us:PHONE"
    group_jid = after_group.rsplit(":", 1)[0] # "GROUPID@g.us"

    Wrong:

    parts = key.split(":")
    group_jid = parts[5] # Gets "120363403860678579@g.us" but breaks if format changes

    Or worse: iterating parts after "group" label which may not align


    ๐Ÿ”ด Duplicate const in JS Kills Entire Script

    If you declare const msgArea twice in the same