Add README, CLAUDE.md, INSTRUCTIONS.md, and CI workflow

- README.md: feature list, stack, project structure, deferred items
- CLAUDE.md: architecture, conventions, route map, CSS variable system,
  EventSub subscription table, data model, custom event names
- INSTRUCTIONS.md: step-by-step initial deploy guide (Twitch app
  registration through OBS setup and custom CSS/JS), plus git workflow,
  first push, GitHub Actions, branching, and update procedure
- .github/workflows/ci.yml: syntax check and import check on every push

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bashy 2026-04-22 23:58:23 +03:00
parent 6ad661c220
commit c46f2f2fc3
4 changed files with 532 additions and 0 deletions

130
CLAUDE.md Normal file
View file

@ -0,0 +1,130 @@
# CLAUDE.md
## Running the app
```bash
source .venv/bin/activate
uvicorn app.main:app --reload
```
Requires `.env` — copy from `.env.example`. Database is created automatically on startup (`SQLModel.metadata.create_all`).
## Architecture
```
Twitch ──EventSub WebSocket──► app/eventsub.py ──► dispatch.py ──► ws.py ──► OBS overlay
Twitch ──IRC (tmi.js)────────────────────────────────────────────────────► OBS overlay
```
- `eventsub.py` runs as an asyncio background task started in `main.py` lifespan
- When it receives a notification, it calls `dispatch.dispatch_event()`
- `dispatch_event()` opens its own DB session (not request-scoped), resolves matching `EventAction` rows, checks cooldowns, logs to `ActivityLog`, and calls `ws.manager.broadcast()`
- `ws.manager` holds a list of active WebSocket connections from `/ws/overlay`
- The overlay JS receives the broadcast and plays the action
## Key conventions
**No passwords anywhere.** Auth is Twitch OAuth only. Session is a signed cookie via `SessionMiddleware`. `request.session["user_id"]` stores the DB user ID after login.
**Singleton tables.** `Settings` and `OverlayLayout` each have one row. `database.get_settings()` and `database.get_layout()` create the row on first call. Both return detached objects (safe to use after session close — no lazy relationships).
**Background DB sessions.** `eventsub.py` and `dispatch.py` use `with Session(engine) as session` directly — not FastAPI's `Depends(get_session)` which is request-scoped. Never pass a request-scoped session to a background task.
**Cooldowns are in-memory.** `app/cooldowns.py` tracks `{action_id: (triggered_at, duration)}`. Restarting the server resets all cooldowns. This is intentional — keeping them in the DB is not worth the complexity for this use case.
**Multiple actions per event.** `dispatch_event()` loops through ALL matching `EventAction` rows for the event type and fires each one (subject to its own cooldown). There is no "first match wins" logic.
**Event matching rules:**
- `event_type="command"`: `event_detail` must match the command name (case-insensitive, required)
- `event_type="bits"`: if `event_detail` is set, it's treated as a minimum bits threshold (`bits_amount >= int(event_detail)`)
- `event_type="channel_points"`: `event_detail` is the reward ID; None matches any reward
- `event_type="sub"/"gift_sub"`: `event_detail` is the tier ("1000", "2000", "3000"); None matches any tier
- `event_type="follow"/"raid"`: `event_detail` not used for matching
## Route map
| Method | Path | File | Notes |
|---|---|---|---|
| GET | `/` | routers/dashboard.py | Activity log, EventSub status |
| GET | `/auth/login` | routers/auth.py | Login page |
| GET | `/auth/twitch` | routers/auth.py | Redirect to Twitch OAuth |
| GET | `/auth/callback` | routers/auth.py | Exchange code, save user, start EventSub |
| GET | `/auth/logout` | routers/auth.py | Clear session |
| GET | `/overlay` | routers/overlay.py | Live OBS browser source |
| GET | `/overlay/preview` | routers/overlay.py | Mock preview for layout editor |
| WS | `/ws/overlay` | routers/overlay.py | Bidirectional: commands up, actions down |
| GET/POST | `/actions` | routers/actions.py | List and create actions (HTMX) |
| POST | `/actions/{id}/test` | routers/actions.py | Fire action immediately, skip cooldown |
| POST | `/actions/{id}/toggle` | routers/actions.py | Enable/disable (HTMX) |
| DELETE | `/actions/{id}` | routers/actions.py | Delete (HTMX) |
| GET/POST | `/media` | routers/media.py | Upload list (HTMX) |
| DELETE | `/media/{id}` | routers/media.py | Delete file from disk and DB |
| GET/POST | `/layout` | routers/layout.py | Style form |
| POST | `/layout/positions` | routers/layout.py | JSON endpoint called by drag JS |
| GET/POST | `/settings` | routers/settings.py | Channel, gap audio, custom CSS/JS |
| POST | `/settings/reconnect` | routers/settings.py | Restart EventSub task |
## Overlay CSS variable system
All overlay positioning and styling is driven by CSS custom properties injected into `overlay.html` and `overlay_preview.html` from `OverlayLayout`:
| Variable | Controls |
|---|---|
| `--chat-left`, `--chat-top` | Chat box position (%) |
| `--chat-width`, `--chat-height` | Chat box size (%) |
| `--chat-font-size` | Chat font size (px) |
| `--chat-bg-opacity` | Chat message background opacity |
| `--chat-text-color` | Chat message text color |
| `--alert-left`, `--alert-top` | Alert position (%, centered via transform) |
| `--alert-font-size` | Alert font size (px) |
| `--alert-bg-opacity` | Alert background opacity |
| `--alert-text-color` | Alert text color |
The layout editor (`/layout`) has drag handles in the parent document overlaid on an iframe pointing to `/overlay/preview`. Since both are same-origin, the editor updates the iframe's CSS variables directly via `iframe.contentDocument.documentElement.style.setProperty(...)` for live preview. Positions are persisted via `POST /layout/positions` (JSON) on drag end.
## Custom events dispatched to overlay JS
When the overlay WebSocket receives an `execute` message, it dispatches a `CustomEvent` on `document` before playing the alert:
```javascript
document.dispatchEvent(new CustomEvent(`bashyoverlay:${msg.event_type}`, { detail: msg }));
```
Available event types: `sub`, `gift_sub`, `bits`, `channel_points`, `command`, `follow`, `raid`.
`msg.detail` contains `{ event_type, action_type, media_url, alert_text }`.
## Data model
```
User ──────────────────────────────── Twitch OAuth user (one per login)
Settings ──────────────────────────── Singleton: channel, gap audio, custom CSS/JS
OverlayLayout ─────────────────────── Singleton: widget positions and styles
MediaFile ──────────────────────────── Uploaded sound or video file
EventAction ──── media_file (FK) ───── Event → action mapping
ActivityLog ────────────────────────── Append-only event log
```
`Claim` unique constraint on `(item_id, attendee_id)` prevents double-claiming — wait, that's IllBring. BashyOverlay has no Claim model. `EventAction` has no unique constraints; multiple rows with the same `event_type` are allowed and all fire.
## EventSub subscriptions
| Twitch event | Condition | Scope required |
|---|---|---|
| `channel.subscribe` v1 | `broadcaster_user_id` | `channel:read:subscriptions` |
| `channel.subscription.message` v1 | `broadcaster_user_id` | `channel:read:subscriptions` |
| `channel.subscription.gift` v1 | `broadcaster_user_id` | `channel:read:subscriptions` |
| `channel.cheer` v1 | `broadcaster_user_id` | `bits:read` |
| `channel.channel_points_custom_reward_redemption.add` v1 | `broadcaster_user_id` | `channel:read:redemptions` |
| `channel.follow` v2 | `broadcaster_user_id` + `moderator_user_id` | `moderator:read:followers` |
| `channel.raid` v1 | `to_broadcaster_user_id` | none |
Token refresh is handled in `eventsub._refresh_token()` — triggered on 401 during subscription.
## Not yet implemented
- Alert variable interpolation (`{username}` in alert text)
- Mod access (currently streamer-only login)
- Blerp / HudFX integrations
- Crypto payment notifications
- Morning-of reminder emails (IllBring feature, not applicable here)