7.7 KiB
Browser API (Kasm Chrome) – degelas stack
The degelas stack runs a Kasm Chrome container so you can keep a browser session open (e.g. logged into X or Instagram) and control it via the backend API.
Architecture
- chrome (kasmweb/chrome:1.18.0-rolling-daily): runs Chrome with noVNC (port 6901) and CDP (port 9222, internal).
- backend: FastAPI exposes
/browser/*endpoints; uses Playwright to connect to Chrome via CDP and drive the session.
No frontend or other backend code was changed; only the Chrome service and a new router were added.
Accessing the browser session (noVNC)
Subdomain (recommended): Open https://browser.degelas.be — requires DNS + SSL for browser.degelas.be (see "Subdomain setup" below).
Direct port: Open https://<your-server>:6901 in a browser (or use SSH port-forward if you don’t expose 6901).
- Log in: user
kasm_user, password from envCHROME_VNC_PW(defaultdegelas). - Use Chrome as usual: log into X, Instagram, etc. Leave this tab/window open.
Subdomain setup (browser.degelas.be)
- DNS — Add A or CNAME:
browser.degelas.be→ your server IP (same as degelas.be). - SSL — Request a certificate for this subdomain only (uses the fullstack_degelas certbot stack):
Cert is stored undercd /root/fullstack_degelas && ./scripts/request-cert-browser-subdomain.sh/etc/letsencrypt/live/browser.degelas.be/; the script reloads nginx. - Chrome on proxy — The degelas compose already attaches chrome to the
proxynetwork so nginx can reachdegelas-chrome:6901. - Nginx — Config is in
fullstack_degelas/nginx/conf.d/browser.degelas.be.conf. Reload nginx after DNS and cert are set.
API endpoints (via degelas backend)
Base path: /api/browser (nginx forwards /api/ to the backend).
| Method | Path | Description |
|---|---|---|
| GET | /api/browser/status |
Check if Chrome CDP is reachable and return current page URL. |
| POST | /api/browser/navigate |
Body: {"url": "https://..."} – navigate the current tab. |
| POST | /api/browser/type |
Body: {"selector": "css", "text": "..."} – type into an element. |
| POST | /api/browser/click |
Body: {"selector": "css"} – click an element. |
| POST | /api/browser/execute |
Body: {"expression": "document.title"} – run JS in page, get result. |
| POST | /api/browser/post |
Body: {"platform": "x"|"instagram", "text": "..."} – best-effort post (see below). |
Posting to X or Instagram
-
In the noVNC session, open X or Instagram and log in. For /post you can either:
- Leave the tab on the X/Instagram compose screen (e.g. home with compose box visible), then call POST /api/browser/post with
platform: "x"or"instagram"andtext: "Your post text". - Or drive the flow yourself: POST /api/browser/navigate to the compose URL, then /type and /click with selectors you maintain.
- Leave the tab on the X/Instagram compose screen (e.g. home with compose box visible), then call POST /api/browser/post with
-
The /post endpoint uses best-effort selectors (e.g.
data-testid="tweetTextarea_0"for X). These can break when the sites change; for production, prefer /navigate, /type, and /click with your own selectors.
Deployment notes (live)
- Chrome VNC password: set
CHROME_VNC_PWin the environment (or in a.envnext todocker-compose.yml) so the noVNC session is not the default password. - Port 6901: only published on the host; restrict access (firewall or nginx with auth) if the server is public.
- CDP (9222): not published to the host; only the backend on the
degelasnetwork uses it. - Chrome CDP binding: Recent Chromium (M113+) may ignore
--remote-debugging-address=0.0.0.0and bind to127.0.0.1. If GET /api/browser/status returnsconnected: falsewith a connection error, Chrome is not accepting external CDP connections. Then you need either a custom image that uses a socat forward (e.g. listen on 0.0.0.0:9222 → 127.0.0.1:9223 with Chrome on 9223), or run Chrome with an older build that still honors the flag. See e.g. Kasm: How to Open Chrome's Remote Debugging Port.
Optional: backend env
- BROWSER_CDP_URL: default
http://chrome:9222. Override if Chrome runs elsewhere or with a different port.
Scheduled posts
Posts are stored in the database; a job runs every minute and publishes any post whose scheduled_at time has passed. You can load many posts for given dates (bulk create) and list by date range and platform.
| Method | Path | Description |
|---|---|---|
| POST | /api/scheduled-posts |
Create one: {"platform": "x", "text": "...", "scheduled_at": "2025-03-12T14:00:00Z"}. |
| POST | /api/scheduled-posts/bulk |
Load many: {"posts": [{"platform": "x", "text": "...", "scheduled_at": "..."}, ...]}. |
| GET | /api/scheduled-posts |
List. Query: ?status=pending, ?platform=x, ?from_date=2025-03-12, ?to_date=2025-03-20, ?limit=100. |
| DELETE | /api/scheduled-posts/{id} |
Cancel a pending post. |
- scheduled_at: ISO datetime (UTC or offset), or date-only
YYYY-MM-DDfor list filters. Single create requires a future time; bulk can be any. - from_date / to_date: Filter list by
scheduled_at(inclusive). UseYYYY-MM-DDor full ISO datetime. - Status:
pending,posted,failed,cancelled. Onlypendingcan be cancelled. - Character limits and max posts per platform per day are enforced (see below). Use GET /api/scheduled-posts/limits to get current limits for the UI.
Examples:
# Load database with posts for a range of dates (one per day to X)
curl -X POST "https://degelas.be/api/scheduled-posts/bulk" \
-H "Content-Type: application/json" \
-d '{
"posts": [
{"platform": "x", "text": "Post for March 12.", "scheduled_at": "2025-03-12T09:00:00Z"},
{"platform": "x", "text": "Post for March 13.", "scheduled_at": "2025-03-13T09:00:00Z"}
]
}'
# List posts for given dates and platform
curl -s "https://degelas.be/api/scheduled-posts?from_date=2025-03-12&to_date=2025-03-20&platform=x"
# List pending only
curl -s "https://degelas.be/api/scheduled-posts?status=pending"
# Cancel one
curl -X DELETE "https://degelas.be/api/scheduled-posts/1"
Where limits and calendar are configured
| What | Where | Env override |
|---|---|---|
| Character limit (X) | app/config.py: X_MAX_POST_LENGTH (default 280) |
X_MAX_POST_LENGTH |
| Character limit (Instagram) | app/config.py: INSTAGRAM_MAX_CAPTION_LENGTH (default 2200) |
INSTAGRAM_MAX_CAPTION_LENGTH |
| Max posts per platform per day | app/config.py: MAX_POSTS_PER_PLATFORM_PER_DAY (default 5) |
MAX_POSTS_PER_PLATFORM_PER_DAY |
| Validation | app/scheduled_posts.py: validate_post_text(), count_posts_on_date(); used in create_scheduled_post and create_scheduled_posts_bulk |
— |
| Limits API | GET /api/scheduled-posts/limits returns the above for UI | — |
Campaign calendar
Campaigns group posts by a name and date range; you can use them as a "campaign calendar".
| Method | Path | Description |
|---|---|---|
| POST | /api/campaigns |
Create: {"name": "...", "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD", "max_posts_per_platform_per_day": null} |
| GET | /api/campaigns |
List campaigns |
| GET | /api/campaigns/{id}/calendar |
Calendar: posts for this campaign grouped by date |
When creating scheduled posts (single or bulk), set campaign_id to link them to a campaign. List posts with ?campaign_id=... to see only that campaign’s posts.
Quick test
# After stack is up (docker compose up -d)
curl -s https://degelas.be/api/browser/status
# With noVNC tab open and a page loaded, you should see "connected": true and "page_url": ...