# VJ Page — Streaming Site Selector

## Overview

Currently [`vj-stream.mjs`](../www/js/vj-stream.mjs:8) hardcodes all streaming infrastructure URLs to `laantungir.net`. This plan adds a **streaming site selector** — a dropdown of remembered site configurations — so users can stream from any compatible server, not just laantungir.net.

## Problem

The following are hardcoded in [`vj-stream.mjs`](../www/js/vj-stream.mjs:1):

| Constant/Location | Hardcoded Value |
|---|---|
| [`STREAM_BASE_URL`](../www/js/vj-stream.mjs:8) | `https://laantungir.net` |
| [`deriveShowUrls().obsServer`](../www/js/vj-stream.mjs:50) | `rtmp://laantungir.net:1935/publish` |
| [`deriveShowUrls().obsKeyTemplate`](../www/js/vj-stream.mjs:51) | `{slug}/src/{SECRET_KEY}` |
| [`deriveShowUrls().masterPlaylist`](../www/js/vj-stream.mjs:47) | `https://laantungir.net/stream/{slug}/stream.m3u8` |
| [`deriveShowUrls().viewerPage`](../www/js/vj-stream.mjs:48) | `https://laantungir.net/stream/{slug}` |
| [`deriveShowUrls().stats`](../www/js/vj-stream.mjs:49) | `https://laantungir.net/api/stream/stats?show={slug}` |
| [OBS Server display in vj.html](../www/vj.html:1485) | `rtmp://laantungir.net:1935/publish` |

The monochrome/music URL (`https://monochrome.laantungir.net`) stays separate in the auto-announce section — it is **not** part of this feature.

## Streaming Site Data Model

A streaming site configuration bundles three pieces of infrastructure:

```js
{
  name: 'laantungir.net',           // Human-readable label for the dropdown
  streamBaseUrl: 'https://laantungir.net',  // Base URL for HLS, viewer page, stats API
  rtmpServer: 'rtmp://laantungir.net:1935/publish',  // OBS RTMP server URL
  obsKeyTemplate: '{slug}/src/{SECRET_KEY}',  // OBS stream key pattern
}
```

### URL Derivation from Site Config

Given a site config and a show slug, all URLs are derived:

| URL | Pattern |
|---|---|
| Master playlist | `{streamBaseUrl}/stream/{slug}/stream.m3u8` |
| Viewer page | `{streamBaseUrl}/stream/{slug}` |
| Stats API | `{streamBaseUrl}/api/stream/stats?show={slug}` |
| OBS Server | `{rtmpServer}` (verbatim) |
| OBS Key | `{obsKeyTemplate}` with `{slug}` replaced |

### Default Site

The laantungir.net configuration ships as a built-in default:

```js
const DEFAULT_STREAMING_SITE = {
  name: 'laantungir.net',
  streamBaseUrl: 'https://laantungir.net',
  rtmpServer: 'rtmp://laantungir.net:1935/publish',
  obsKeyTemplate: '{slug}/src/{SECRET_KEY}',
};
```

## Persistence

Saved sites are stored in the existing Nostr `kind:30078` user settings event under the `vjPage` namespace, alongside existing settings like `streamDraft`, `autoAnnounce`, and `columns`:

```json
{
  "vjPage": {
    "streamingSites": [
      {
        "name": "laantungir.net",
        "streamBaseUrl": "https://laantungir.net",
        "rtmpServer": "rtmp://laantungir.net:1935/publish",
        "obsKeyTemplate": "{slug}/src/{SECRET_KEY}"
      },
      {
        "name": "my-server.example.com",
        "streamBaseUrl": "https://my-server.example.com",
        "rtmpServer": "rtmp://my-server.example.com:1935/live",
        "obsKeyTemplate": "{slug}/{SECRET_KEY}"
      }
    ],
    "selectedSiteName": "laantungir.net",
    "streamDraft": { ... },
    "autoAnnounce": { ... },
    "columns": { ... }
  }
}
```

This syncs across devices via Nostr relays using the existing [`patchUserSettings()`](../www/vj.html:2106) / [`getUserSettings()`](../www/vj.html:2522) / [`onUserSettings()`](../www/vj.html:2531) infrastructure.

## Architecture

```mermaid
graph TD
    subgraph Settings - kind:30078
        S1[vjPage.streamingSites - array of site configs]
        S2[vjPage.selectedSiteName - last used site]
    end

    subgraph UI - vj.html Column 1
        U1[Site Selector Dropdown]
        U2[Add/Edit Site Form]
        U3[Delete Site Button]
    end

    subgraph vj-stream.mjs
        V1[currentSiteConfig - active site object]
        V2[deriveShowUrls - slug + site -> all URLs]
        V3[OBS hints display]
        V4[Stats polling]
        V5[HLS player source]
        V6[Stream event publishing]
    end

    S1 -->|load on init| U1
    S2 -->|restore selection| U1
    U1 -->|on change| V1
    U2 -->|save| S1
    V1 --> V2
    V2 --> V3
    V2 --> V4
    V2 --> V5
    V2 --> V6
```

## UI Design

### Site Selector Dropdown

Placed at the very top of the STREAM section in Column 1, **above** the SHOW selector. This is the first thing the user configures — which server am I streaming to?

```
┌─────────────────────────────────┐
│ STREAM SITE                     │
│ ┌─────────────────────────────┐ │
│ │ laantungir.net          ▼  │ │  <-- dropdown of saved sites
│ └─────────────────────────────┘ │
│ [+ Add Site]  [✎ Edit]  [🗑]   │  <-- action buttons
│                                 │
│ SHOW                            │
│ ┌─────────────────────────────┐ │
│ │ — Select a show —       ▼  │ │
│ └─────────────────────────────┘ │
│ ...                             │
```

### Add/Edit Site Form

A collapsible panel that appears when clicking "+ Add Site" or "✎ Edit":

```
┌─────────────────────────────────┐
│ Site Name                       │
│ ┌─────────────────────────────┐ │
│ │ my-server.example.com       │ │
│ └─────────────────────────────┘ │
│ Stream Base URL                 │
│ ┌─────────────────────────────┐ │
│ │ https://my-server.example.. │ │
│ └─────────────────────────────┘ │
│ RTMP Server                     │
│ ┌─────────────────────────────┐ │
│ │ rtmp://my-server.example..  │ │
│ └─────────────────────────────┘ │
│ OBS Key Template                │
│ ┌─────────────────────────────┐ │
│ │ {slug}/src/{SECRET_KEY}     │ │
│ └─────────────────────────────┘ │
│ [Save Site]  [Cancel]           │
└─────────────────────────────────┘
```

## Implementation Steps

### Step 1 — Define site data model and refactor deriveShowUrls in vj-stream.mjs

**File:** [`www/js/vj-stream.mjs`](../www/js/vj-stream.mjs)

- Add `DEFAULT_STREAMING_SITE` constant with laantungir.net config
- Add `let currentSiteConfig = { ...DEFAULT_STREAMING_SITE }` to module state
- Refactor [`deriveShowUrls(slug)`](../www/js/vj-stream.mjs:34) to use `currentSiteConfig` instead of hardcoded `STREAM_BASE_URL`:
  ```js
  function deriveShowUrls(slug) {
    const site = currentSiteConfig;
    const base = site.streamBaseUrl.replace(/\/+$/, '');
    const safeSlug = normalizeShowSlug(slug);
    if (!safeSlug) {
      return {
        masterPlaylist: '',
        viewerPage: '',
        stats: '',
        obsServer: site.rtmpServer,
        obsKeyTemplate: site.obsKeyTemplate,
      };
    }
    return {
      masterPlaylist: `${base}/stream/${safeSlug}/stream.m3u8`,
      viewerPage: `${base}/stream/${safeSlug}`,
      stats: `${base}/api/stream/stats?show=${encodeURIComponent(safeSlug)}`,
      obsServer: site.rtmpServer,
      obsKeyTemplate: site.obsKeyTemplate.replace(/\{slug\}/g, safeSlug),
    };
  }
  ```
- Remove the module-level `STREAM_BASE_URL` constant (replaced by `currentSiteConfig.streamBaseUrl`)
- Add a `setCurrentSiteConfig(siteConfig)` function that updates `currentSiteConfig`, refreshes OBS hints, and re-derives URLs for the current show
- Export `setCurrentSiteConfig` and `getCurrentSiteConfig` from the panel return object

### Step 2 — Add streaming site selector HTML to vj.html

**File:** [`www/vj.html`](../www/vj.html)

Insert a new "STREAM SITE" section at the top of [`#vjStreamCard`](../www/vj.html:1421), before the existing STREAM section:

```html
<div class="streamSection">
  <div class="musicSidebarTitle">STREAM SITE</div>
  <select id="selectStreamingSite" class="streamInput" aria-label="Select streaming site">
    <option value="laantungir.net">laantungir.net</option>
  </select>
  <div style="display:flex; gap:4px; margin-top:4px;">
    <button id="btnAddStreamingSite" class="btn musicMiniBtn" type="button">+ Add</button>
    <button id="btnEditStreamingSite" class="btn musicMiniBtn" type="button">✎ Edit</button>
    <button id="btnDeleteStreamingSite" class="btn musicMiniBtn" type="button">🗑</button>
  </div>
  <div id="streamingSiteFormPanel" class="hidden" style="margin-top:6px;">
    <input id="inputSiteName" class="streamInput" placeholder="Site name (e.g. my-server.com)" />
    <input id="inputSiteStreamBaseUrl" class="streamInput" placeholder="Stream base URL (e.g. https://my-server.com)" />
    <input id="inputSiteRtmpServer" class="streamInput" placeholder="RTMP server (e.g. rtmp://my-server.com:1935/publish)" />
    <input id="inputSiteObsKeyTemplate" class="streamInput" placeholder="OBS key template (e.g. {slug}/src/{SECRET_KEY})" />
    <div style="display:flex; gap:4px; margin-top:4px;">
      <button id="btnSaveStreamingSite" class="btn" type="button">Save Site</button>
      <button id="btnCancelStreamingSite" class="btn" type="button">Cancel</button>
    </div>
  </div>
</div>
```

### Step 3 — Add site management elements to vj-stream.mjs els object

**File:** [`www/js/vj-stream.mjs`](../www/js/vj-stream.mjs)

Add to the [`els`](../www/js/vj-stream.mjs:229) object:
- `selectStreamingSite`
- `btnAddStreamingSite`, `btnEditStreamingSite`, `btnDeleteStreamingSite`
- `streamingSiteFormPanel`
- `inputSiteName`, `inputSiteStreamBaseUrl`, `inputSiteRtmpServer`, `inputSiteObsKeyTemplate`
- `btnSaveStreamingSite`, `btnCancelStreamingSite`

### Step 4 — Wire site selector logic in vj-stream.mjs

**File:** [`www/js/vj-stream.mjs`](../www/js/vj-stream.mjs)

Add to [`wireButtons()`](../www/js/vj-stream.mjs:1209):

- **selectStreamingSite change**: Update `currentSiteConfig` from the selected site, refresh all derived URLs, update OBS hints, re-set HLS player source if a show is selected
- **btnAddStreamingSite click**: Show the form panel in "add" mode (clear fields)
- **btnEditStreamingSite click**: Show the form panel in "edit" mode (pre-fill from current selection)
- **btnDeleteStreamingSite click**: Remove the selected site from the list (prevent deleting the last site), persist, select the first remaining site
- **btnSaveStreamingSite click**: Validate inputs, add/update the site in the list, persist via callback, select the new/updated site
- **btnCancelStreamingSite click**: Hide the form panel

Add new functions:
- `populateSiteDropdown(sites, selectedName)` — rebuilds the `<select>` options
- `getSelectedSiteConfig()` — returns the site config matching the dropdown selection
- `applySiteSelection(siteName)` — sets `currentSiteConfig`, refreshes URLs, updates OBS hints

### Step 5 — Wire persistence in vj.html

**File:** [`www/vj.html`](../www/vj.html)

In the `initVjStreamPanel()` call, pass two new callbacks:
- `onSitesChanged(sites, selectedName)` — called when the sites list or selection changes; triggers `queueVjSettingsPatch({ streamingSites: sites, selectedSiteName: selectedName })`
- `getSavedSites()` — returns `{ sites: [...], selectedName: '...' }` from `getVjPageSettingsNode(pageSettings)`

In [`applyVjPageSettingsFromState()`](../www/vj.html:2129):
- Read `vjNode.streamingSites` and `vjNode.selectedSiteName`
- If `streamingSites` is a non-empty array, pass it to the stream panel to populate the dropdown
- If empty/missing, use the default laantungir.net site
- Restore the `selectedSiteName` selection

### Step 6 — Update initialize() to accept initial site config

**File:** [`www/js/vj-stream.mjs`](../www/js/vj-stream.mjs)

In [`initialize()`](../www/js/vj-stream.mjs:1440):
- Accept an optional `{ sites, selectedSiteName }` parameter
- If provided, populate the dropdown and apply the selection before discovering shows
- If not provided, use the default laantungir.net site

Add to the return object:
- `setSites(sites, selectedSiteName)` — for live settings updates via `onUserSettings`

### Step 7 — Remove hardcoded OBS server text from vj.html

**File:** [`www/vj.html`](../www/vj.html)

Change the hardcoded OBS server display at [line 1485](../www/vj.html:1485):
```html
<!-- Before -->
<span id="spanObsServer" class="streamInfoValue">rtmp://laantungir.net:1935/publish</span>

<!-- After -->
<span id="spanObsServer" class="streamInfoValue">—</span>
```

The actual value will be set dynamically by `updateObsHints()` using `currentSiteConfig`.

## Files Changed

| File | Changes |
|---|---|
| [`www/js/vj-stream.mjs`](../www/js/vj-stream.mjs) | Refactor URL derivation, add site config state, add site selector wiring, add site management functions, update exports |
| [`www/vj.html`](../www/vj.html) | Add site selector HTML, wire persistence callbacks, restore site selection from settings |

## Key Technical Notes

1. **Backward compatibility**: If no `streamingSites` exist in settings, the default laantungir.net config is used — existing users see no change
2. **Site name as key**: The `name` field serves as the unique identifier for sites in the dropdown and settings
3. **No validation of server reachability**: We trust the user to enter correct URLs; the stats polling and HLS player will naturally show errors if the server is unreachable
4. **OBS key template**: The `{slug}` placeholder in the template is replaced with the actual show slug; `{SECRET_KEY}` remains as a literal placeholder the user replaces manually in OBS
5. **Monochrome URL is separate**: The auto-announce URL base (`inputAutoAnnounceUrlBase`) is not affected by site selection — it stays in its own section
