s3watch static · zero AWS infra · 100% client-sidestorage: checking…

Live bucket monitor in a single HTML file. Polls ListObjectsV2, diffs snapshots, and keeps the full event history in a persistent DuckDB database (browser OPFS) — close the tab, come back, keep tracking. Timeline, SQL console, and Parquet/CSV export included.

1 · Connect

In role mode the page fetches ./aws-credentials.json (same origin — see setup below). Nothing to paste.
idle

Needs only s3:ListBucket — works for non-owners with read access. Settings (not secrets) are remembered in localStorage.

EC2 / VPC setup: serve the page next to a credentials file (5 lines, no app server needed)

Browsers cannot call EC2 instance metadata (169.254.169.254) directly — it sends no CORS headers. Instead, refresh a same-origin JSON file server-side; the page picks it up with zero keys pasted:

# run on the EC2 instance (cron every 30 min — creds rotate, refresh early)
TOKEN=$(curl -s -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 3600')
ROLE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/)
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE \
 | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps({'AccessKeyId':d['AccessKeyId'],'SecretAccessKey':d['SecretAccessKey'],'Token':d['Token']}))" \
 > /var/www/html/aws-credentials.json

# serve this HTML from the same directory: python3 -m http.server 8000 -d /var/www/html
# then open http://<instance-ip>:8000/s3watch-static-draft.html

Same-origin fetch = no CORS involved. The instance role needs s3:ListBucket on the target bucket(s). The JSON file is only reachable inside your VPC.

Prerequisite: one CORS rule on the watched bucket (30-second, one-time)

The page calls the S3 API cross-origin from your browser, so the bucket must allow it. S3 console → bucket → Permissions → CORS:

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedOrigins": ["http://<your-ec2-ip>:8000"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

Use the exact origin serving this file (* works for testing but exposes LIST to every website). Without this the page cannot reach S3 — no static workaround exists.

Architecture & honest limits