# InterServer Signal — API guide

Shared realtime hub (ASP.NET Core SignalR) for every phvtech.com project.
**Base URL:** `https://signalir.phvtech.com` · CORS-open (any origin, including `file://`) · no auth.

Think of it as the realtime twin of the InterServer db handler: one server, many projects.
Each project picks a **project** name (its namespace) and inside it any number of **rooms**.
Anyone in a room receives every event sent to that room. There is nothing to create first:
a room exists as soon as someone joins it or retains a message in it.

| Concept | Rule |
|---|---|
| `project` | 1–64 chars, `A-Z a-z 0-9 - _ .` — e.g. `fo-idex-game` |
| `room` | same rule — e.g. `lobby`, `kiosk-1`, `operator` |
| `event` | same rule — e.g. `score`, `start`, `state` |
| `data` | any JSON value, up to 256 KB |
| `user` | optional free-text label (≤128 chars) shown in presence/members |

---

## 1. Browser / JS (recommended)

Both files are served by the hub. **For kiosk builds, copy them into the project** (no CDN rule):
`https://signalir.phvtech.com/signalr.min.js` and `https://signalir.phvtech.com/interserver-signal.js`.

```html
<script src="signalr.min.js"></script>
<script src="interserver-signal.js"></script>
<script>
  const sig = InterServerSignal.connect({
    project: 'fo-idex-game',      // required
    user: 'kiosk-1',              // optional label
    // url: 'https://signalir.phvtech.com'   (default)
  });

  sig.on('score', (data, msg) => console.log('score', data, 'from', msg.user));
  sig.on('*', (data, msg) => {});              // every event
  sig.onPresence(p => console.log(p.type, p.user, 'now', p.count));
  sig.onState(s => console.log(s));            // connecting | connected | reconnecting | disconnected

  const info = await sig.join('lobby');        // { id, members:[{id,user}], retained:[...] }
  await sig.send('lobby', 'score', { n: 5 });                   // others in room
  await sig.send('lobby', 'state', { phase: 'play' }, { retain: true }); // + remembered for late joiners
  await sig.send('lobby', 'ping', null, { echo: true });        // include myself
  await sig.send('lobby', 'dm', { hi: 1 }, { to: otherId });    // one connection in the room
  await sig.members('lobby');
  await sig.leave('lobby');
  sig.stop();
</script>
```

The wrapper reconnects forever, re-joins all rooms after a reconnect (SignalR assigns a new
connection id), replays retained messages on join, and queues `send` calls made while offline.

## 2. Raw SignalR (any language / official client)

Hub URL: `https://signalir.phvtech.com/hub`

**Client → server** (every method takes one object argument):

| Method | Argument | Returns |
|---|---|---|
| `Join` | `{ project, room, user? }` | `{ id, members:[{id,user}], retained:[message] }` |
| `Leave` | `{ project, room }` | — |
| `Send` | `{ project, room, event, data?, retain?, echo?, to? }` | `{ delivered, ts }` |
| `Members` | `{ project, room }` | `[{ id, user }]` |

**Server → client:**

| Method | Payload |
|---|---|
| `message` | `{ project, room, event, data, from, user, retained, ts }` — `from` is the sender's connection id (`"api"` for REST) · `ts` = Unix ms |
| `presence` | `{ project, room, type: "join" \| "leave", id, user, count }` |

Notes: `Send` excludes the sender unless `echo: true`. You may `Send` to a room without joining it.
Errors come back as a rejected invoke with a readable message (e.g. `room is required`).

C# example (`Microsoft.AspNetCore.SignalR.Client`):

```csharp
var conn = new HubConnectionBuilder().WithUrl("https://signalir.phvtech.com/hub").WithAutomaticReconnect().Build();
conn.On<JsonElement>("message", m => Console.WriteLine(m));
await conn.StartAsync();
await conn.InvokeAsync<JsonElement>("Join", new { project = "demo", room = "lobby", user = "svc" });
await conn.InvokeAsync<JsonElement>("Send", new { project = "demo", room = "lobby", @event = "hello", data = new { x = 1 } });
// after a reconnect, call Join again for each room
```

## 3. REST (push from servers, SQL jobs, Postman, curl)

| Method & path | Body / query | Result |
|---|---|---|
| `POST /api/signal/send` | `{ project, room, event, data?, retain?, to?, user? }` | `{ ok, delivered, ts }` |
| `GET /api/signal/rooms?project=` | | `[{ room, members, retained }]` |
| `GET /api/signal/members?project=&room=` | | `[{ id, user }]` |
| `GET /api/signal/retained?project=&room=` | | `[message]` |
| `DELETE /api/signal/retained?project=&room=&event=` | omit `event` to clear all | `{ ok, cleared }` |
| `GET /api/health` | | `{ ok, connections, rooms, startedUtc, uptimeSeconds }` |

Errors: HTTP 400 `{ ok:false, error }`.

```bash
curl -X POST https://signalir.phvtech.com/api/signal/send -H "Content-Type: application/json" \
  -d '{"project":"demo","room":"lobby","event":"hello","data":{"text":"from curl"}}'
```

```js
fetch('https://signalir.phvtech.com/api/signal/send', {
  method: 'POST', headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ project: 'demo', room: 'lobby', event: 'hello', data: { text: 'hi' } })
});
```

## 4. Retained messages ("last value")

Send with `retain: true` and the hub remembers the **latest message per event** in that room
(max 100 events per room). Everyone who joins later receives them first (flag `retained: true`).
Good for "current game state", "current settings", "what screen is showing".
Retained data is **in memory only** — it is lost when the IIS app pool recycles. Anything that must
survive goes to the InterServer db handler (`https://mas.phvtech.com/api/Master/sp`).

## 5. Patterns

- **Operator panel ↔ kiosks:** room `control`; operator sends `settings` with `retain:true`; kiosks apply it on join and on change.
- **Second screen / leaderboard:** game sends `score` to room `display`; the display page only listens.
- **Multi-kiosk:** one room per venue (`venue-colombo`), `user` = kiosk name, use `presence` to show who is online.
- **Backend push:** after writing to the db handler, `POST /api/signal/send` so screens refresh instantly.

## 6. Limits & behaviour

- No auth: anyone who knows a project/room name can join it. Use unguessable room names for anything sensitive; never send secrets.
- Single server instance, in-memory state. Transport: WebSockets, with automatic fallback to Server-Sent Events / long polling.
- Keep-alive 15 s, client timeout 40 s. Max message ≈ 272 KB.
- Test console: `https://signalir.phvtech.com/`

## 7. Deploying the hub (Plesk / IIS)

1. `dotnet publish src -c Release -o publish` (targets .NET 8; the server needs the ASP.NET Core 8 Hosting Bundle).
2. Plesk → create subdomain `signalir.phvtech.com` → upload the contents of `publish/` to its root (httpdocs).
3. Enable **WebSocket Protocol** on IIS if it is not already (otherwise SignalR falls back to SSE/long polling — it still works).
4. Recommended app-pool settings: *Idle Time-out = 0* and *Start Mode = AlwaysRunning* so retained state and connections are not dropped by idle recycling.
5. Add an SSL certificate (Let's Encrypt in Plesk) and check `https://signalir.phvtech.com/api/health`.
