Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,343 changes: 1,343 additions & 0 deletions apps/server/swarm_widget_api.py

Large diffs are not rendered by default.

25 changes: 17 additions & 8 deletions apps/site/public/mcp/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,37 @@ <h2>Install in Claude Desktop</h2>
<pre>{
"mcpServers": {
"impt": {
"command": "npx",
"args": ["-y", "@impt/mcp-server"],
"env": { "IMPT_PARTNER_KEY": "YOUR_KEY" }
"url": "https://swarm.impt.io/api/mcp/http"
}
}
}</pre>
</div>

<div class="box">
<h2>Install in Claude Code</h2>
<pre>claude mcp add impt npx -y @impt/mcp-server -e IMPT_PARTNER_KEY=YOUR_KEY</pre>
<pre>claude mcp add impt --transport http --url https://swarm.impt.io/api/mcp/http</pre>
</div>

<div class="box">
<h2>HTTP transport (custom clients)</h2>
<p style="color:var(--mute);font-size:14px;margin-bottom:10px">SSE: <code>https://swarm.impt.io/api/mcp/sse</code> · Streamable HTTP: <code>https://swarm.impt.io/api/mcp/http</code></p>
<p style="font-size:13px"><a href="/openapi.json">OpenAPI spec →</a></p>
<h2>Endpoints (live now)</h2>
<p style="color:var(--mute);font-size:14px;margin-bottom:10px">Streamable HTTP transport — <a href="/api/mcp/http"><code>swarm.impt.io/api/mcp/http</code></a></p>
<p style="color:var(--mute);font-size:14px;margin-bottom:10px">Server info (paste-ready config) — <a href="/api/mcp/info"><code>swarm.impt.io/api/mcp/info</code></a></p>
<p style="color:var(--mute);font-size:14px"><a href="/api/gpt/openapi.json">OpenAPI Action manifest →</a> (use for ChatGPT GPTs and generic agents)</p>
</div>

<div class="box">
<h2>Tools available</h2>
<ul style="color:var(--mute);font-size:14px;line-height:1.9;padding-left:20px">
<li><code>impt_search_hotels(city, adults?, rooms?, limit?)</code> — JSON list of hotels</li>
<li><code>impt_create_intent(destination, partner_key?)</code> — booking intent + canonical deeplink</li>
<li><code>impt_get_quote(intent_id)</code> — look up an intent by id</li>
<li><code>impt_get_deeplink(destination, partner_key?)</code> — synthesize a deeplink without persisting</li>
</ul>
</div>

<div class="box">
<h2>Status</h2>
<p style="color:var(--mute);font-size:14px">P1 — npm package <code>@impt/mcp-server</code> publishing this week. Until then this page is the install brief; the SSE/HTTP transports come up alongside.</p>
<p style="color:var(--mute);font-size:14px">✅ <strong>LIVE</strong> 2026-05-09 — Streamable HTTP transport active. Tool calls are stateless JSON-RPC 2.0; intents persist 90 days.</p>
</div>

<footer>
Expand Down
317 changes: 317 additions & 0 deletions apps/site/public/partners/dashboard/index.html

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions apps/site/public/partners/login/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=/partners/dashboard">
<title>Partner sign-in — IMPT Swarm</title>
<link rel="canonical" href="/partners/dashboard">
<script>location.replace('/partners/dashboard');</script>
</head>
<body>
<p>Redirecting to <a href="/partners/dashboard">/partners/dashboard</a>…</p>
</body>
</html>
572 changes: 572 additions & 0 deletions apps/site/public/tools/index.html

Large diffs are not rendered by default.

617 changes: 617 additions & 0 deletions apps/site/public/widget/index.html

Large diffs are not rendered by default.

46 changes: 45 additions & 1 deletion ops/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,51 @@

---

## 2026-05-09 (latest) — PR #3 opened: bots + big homepage refresh
## 2026-05-09 (LIVE) — production deploy applied directly to mgmt-server

### Discovery that flipped the architecture
swarm.impt.io is **NOT** Cloudflare Pages. DNS resolves to `35.214.111.96` (this mgmt-server). nginx serves static + reverse-proxies `/api/widget/*` to a Python FastAPI on `localhost:2027` (`impt-swarm-widget.service`, systemd-managed).

### What got deployed live (not just opened-as-PR)
- **`apps/server/swarm_widget_api.py`** mirrors the live `/srv/swarm/impt-swarm-oss-2026-05-05/backend/swarm_widget_api.py` after I appended ~750 lines:
- `POST /api/widget/intent` — JSON intent + canonical deeplink + embed + qr URL (KV-equivalent in SQLite)
- `GET /api/widget/quote/{iid}` — intent lookup
- `GET /api/widget/hotels` — proxy `platform.impt.io/api/hotels` (no cache yet — Python edge cache TBD)
- `GET /api/widget/qr/{key}.png` + `.svg` — real QR generator (`segno`)
- `GET /api/email/sig/{key}` — paste-ready Outlook-safe signature snippet
- `GET /api/gpt/openapi.json` — Custom GPT Action manifest
- `POST /api/mcp/http` — Streamable HTTP MCP server with 4 tools (`impt_search_hotels`, `impt_create_intent`, `impt_get_quote`, `impt_get_deeplink`)
- `GET /api/mcp/info` — paste-ready Claude Desktop / Claude Code config
- `POST /api/tg/webhook` — Telegram bot (LIVE, secret-token verified, smart heuristic for non-city text)
- `GET+POST /api/whatsapp/webhook` — Meta Cloud API (waiting on tokens)
- `GET+POST /api/fb/webhook` — Meta Messenger Platform (waiting on tokens)
- SQLite tables added: `intents`, `bot_events`
- Idempotent `swarm-` prefix in `build_deeplink` so partner keys never double-prefix
- **`ops/nginx-swarm.impt.io.conf`** mirrors the live nginx config (`/etc/nginx/sites-enabled/swarm.impt.io`) with my added location blocks for `/api/tg/webhook`, `/api/whatsapp/`, `/api/fb/`, `/api/(email|gpt|mcp)/`, `/api/widget/qr/.*\.(png|svg)$`
- **`apps/site/public/widget/index.html`** mirrors the new live homepage at `swarm.impt.io/widget`
- **`apps/site/public/mcp/index.html`** updated to show LIVE status and real install commands (HTTP transport)

### Live state (verified by curl)
- `https://swarm.impt.io/widget` 200 (22.6 KB cream-skin homepage)
- `https://swarm.impt.io/{tg,wa,fb,ig,...,partners}` all 200
- `t.me/Rambo_Marc2_bot` webhook = `https://swarm.impt.io/api/tg/webhook` (secret-token set, pending=0)
- `POST /api/widget/intent` — issues `iid_*` + canonical deeplink with `utm_source=swarm-<key>` (idempotent prefix)
- `POST /api/mcp/http` — JSON-RPC 2.0, returns 4 tools, executes `impt_get_deeplink` end-to-end
- `GET /api/widget/qr/<key>.png` — real QR PNG (1.2 KB, ink/cream palette)
- `GET /api/email/sig/<key>` — paste-ready HTML signature
- `GET /api/gpt/openapi.json` — Custom GPT Action manifest
- monitor.mjs cron `*/30 * * * *` running, all 40 probes green

### Live test events Mike has already done
The DB has 6+ real intents from chat_id `8103309746` (Mike) including "Rambo? 😂" (now correctly rejected by the heuristic) and "There are still a lot of bugs..." (also rejected).

### Backup files (rollback safe)
- `/etc/nginx/sites-enabled/swarm.impt.io.bak-pre-bots-20260509-103947`
- `/srv/swarm/impt-swarm-oss-2026-05-05/demo/index.html.bak-pre-omnichannel`

---

## 2026-05-09 (earlier) — PR #3 opened: bots + big homepage refresh

### Phase
P1 — Telegram + WhatsApp + Facebook webhooks; refreshed swarm.impt.io homepage.
Expand Down
269 changes: 269 additions & 0 deletions ops/nginx-swarm.impt.io.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# swarm.impt.io — IMPT AI Swarm Hotel & Travel AI
# Document root: /srv/swarm/impt-swarm/site
# Proxy: /api/chat/* → hotel-ai.impt.io agent-hub on localhost:2025

server {
server_name swarm.impt.io www.swarm.impt.io;

# Let's Encrypt http-01 challenge — keep on :80 even after redirect.
location /.well-known/acme-challenge/ {
root /var/www/html;
}

# Until cert is issued, serve site over HTTP. Once certbot adds the 443
# block + redirect, this server{} stays for ACME + 301.
root /srv/swarm/impt-swarm/site;
index index.html;

# Same-origin proxy to swarm chat backend (Claude). :2026 is owned by mike;
# AJ's :2025 stays untouched but its OpenAI key is over-quota so we don't use it.
location /api/chat/ {
limit_req_status 429;
# Per-IP rate limit. 30 req/min sustained, burst 10 nodelay.
# Rationale: Anthropic costs scale with usage + fraud-cluster lessons 2026-04-24.
limit_req zone=swarm_chat burst=10 nodelay;

proxy_pass http://localhost:2026/chat/;
proxy_set_header Host swarm.impt.io;
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;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
chunked_transfer_encoding on;
}

# Health + threads listing for ops dashboards.
location = /api/threads {
proxy_pass http://localhost:2026/threads;
}
location = /api/health {
proxy_pass http://localhost:2026/health;
}

# ── IMPT Swarm Widget (open-source, 5% commission) — 2026-05-05 ──
# Public widget bundle — must be cross-origin embeddable by any partner site.
location = /widget.js {
alias /srv/swarm/impt-swarm-oss-2026-05-05/repo/src/widget.js;
default_type application/javascript;
add_header Access-Control-Allow-Origin "*" always;
add_header Cache-Control "public, max-age=300, s-maxage=300" always;
add_header X-Content-Type-Options "nosniff" always;
}

# Demo + signup landing page.
location = /widget {
alias /srv/swarm/impt-swarm-oss-2026-05-05/demo/index.html;
default_type text/html;
}
location = /widget/ {
alias /srv/swarm/impt-swarm-oss-2026-05-05/demo/index.html;
default_type text/html;
}

# ── Widget API — security wall layered before backend ──
# Bot UA filter `$widget_bot_block` lives in /etc/nginx/conf.d/widget-rate-limit.conf
# (map directive must be at http context, not server).

# Body-size cap for all widget endpoints (signup form is < 1KB; webhook < 4KB)
client_max_body_size 8k;

# Signup — 5/hr/IP, body cap 4KB. Email-verify gate is enforced backend-side.
location = /api/widget/partners/signup {
limit_req zone=widget_signup burst=2 nodelay;
limit_req_status 429;
client_max_body_size 4k;
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 15s;
add_header Access-Control-Allow-Origin "*" always;
}

# Click redirect — 60/min/IP + bot UA filter.
location = /api/widget/r {
if ($widget_bot_block) { return 403; }
limit_req zone=widget_redirect burst=30 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 15s;
}

# Track pixel — 300/min/IP.
location = /api/widget/track {
limit_req zone=widget_track burst=100 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 5s;
}

# Booking webhook — 60/min/IP. Only app.impt.io should reach here in practice.
location = /api/widget/booking {
limit_req zone=widget_booking burst=20 nodelay;
limit_req_status 429;
client_max_body_size 4k;
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 15s;
}

# Email verify — 30/hr/IP.
location = /api/widget/verify {
limit_req zone=widget_verify burst=5 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 15s;
}

# Catch-all for the rest of /api/widget/* (health, /partners/me, future).
location /api/widget/ {
proxy_pass http://127.0.0.1:2027/api/widget/;
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;
proxy_http_version 1.1;
proxy_read_timeout 30s;
add_header Access-Control-Allow-Origin "*" always;
}

# ── Bot webhooks (Telegram, WhatsApp, Facebook Messenger) — 2026-05-09 ──
location = /api/tg/webhook {
client_max_body_size 64k;
proxy_pass http://127.0.0.1:2027;
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;
proxy_set_header X-Telegram-Bot-Api-Secret-Token $http_x_telegram_bot_api_secret_token;
proxy_http_version 1.1;
proxy_read_timeout 30s;
}

location ~* ^/api/whatsapp/ {
client_max_body_size 64k;
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 30s;
}

location ~* ^/api/fb/ {
client_max_body_size 64k;
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 30s;
}

location ~* ^/api/(email|gpt|mcp)/ {
client_max_body_size 64k;
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 30s;
add_header Access-Control-Allow-Origin "*" always;
}

# QR file extensions need explicit handling so the .png/.svg suffix doesn't
# get gobbled by the asset-cache regex location below.
location ~* ^/api/widget/qr/.*\.(png|svg)$ {
proxy_pass http://127.0.0.1:2027;
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;
proxy_http_version 1.1;
proxy_read_timeout 10s;
add_header Access-Control-Allow-Origin "*" always;
}



# 2026-05-03: /for-travel-managers* → impt.io/b2b/ (canonical B2B sales page)
location ~ ^/for-travel-managers(\.html)?$ {
return 301 https://impt.io/b2b/?utm_source=swarm&utm_medium=for-travel-managers&utm_campaign=launch;
}

# Static site — try clean URLs (e.g. /how-it-works → how-it-works.html or how-it-works/index.html)
location / {
try_files $uri $uri/ $uri.html $uri/index.html =404;
}

# Long cache for assets
location ~* \.(?:css|js|svg|woff2?|ico|png|jpg|jpeg|webp|avif)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
try_files $uri =404;
}

error_page 404 /404.html;

access_log /var/log/nginx/swarm.impt.io.access.log;
error_log /var/log/nginx/swarm.impt.io.error.log;


# Security headers — added 2026-05-03 launch hardening
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/swarm.impt.io/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/swarm.impt.io/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}


server {
if ($host = swarm.impt.io) {
return 301 https://$host$request_uri;
} # managed by Certbot


listen 80;
listen [::]:80;
server_name swarm.impt.io www.swarm.impt.io;
return 404; # managed by Certbot


}