# PRD: Web Session Manager

**Module:** `web-sm/` | **Namespace:** `ajet.chat.web.*`
**Status:** v1 | **Last updated:** 2026-02-17

---

## 1. Overview

The Web Session Manager serves the browser-based chat UI. It renders HTML via Hiccup, uses Datastar for SSE-driven reactivity (server pushes HTML fragments to update the page), subscribes to NATS for real-time events, and calls the API for data reads/writes. **No direct PostgreSQL access.**

## 2. Architecture

```
Browser ←─ SSE ──→ Web SM ──→ NATS (subscribe events)
       ←─ HTML ──→        ──→ API (HTTP, data reads/writes)
```

**Key principle:** Full Datastar hypermedia. The server renders all HTML. Client-side JS is minimal — only what Datastar requires plus small helpers for image paste and textarea auto-resize.

## 3. Page Layout (Discord-Style)

```
┌─────┬───────────────┬─────────────────────────────────────────┬──────────────┐
│     │               │  # general                          ☰  │              │
│  C  │  CATEGORIES   │  ┌── channel topic text ──────────┐    │   THREAD     │
│  O  │               │  │                                │    │   PANEL      │
│  M  │  ▼ General    │  │  alice  10:30 AM               │    │  (when open) │
│  M  │    # general  │  │  hello everyone!                │    │              │
│  U  │    # random   │  │                                │    │  Thread in   │
│  N  │               │  │  bob  10:31 AM                  │    │  # general   │
│  I  │  ▼ Dev        │  │  hey! check this out            │    │              │
│  T  │    # backend  │  │  [image preview]                │    │  alice:      │
│  Y  │    # frontend │  │                                │    │  hello!      │
│     │               │  │  ── New Messages ──             │    │              │
│  I  │  ─────────    │  │                                │    │  bob:        │
│  C  │  DIRECT MSGS  │  │  carol  10:45 AM               │    │  reply here  │
│  O  │    alice      │  │  @bob nice work! 👍             │    │              │
│  N  │    bob ●      │  │                                │    │              │
│  S  │    group (3)  │  │  ── Load older messages ──     │    │              │
│     │               │  │                                │    │              │
│  +  │               │  ├────────────────────────────────┤    │              │
│     │  🔍 Search    │  │  @mention autocomplete ↕       │    │              │
│     │               │  │  [message input area      ] 📎│    │              │
│     │               │  │  bob is typing...              │    │              │
│     │               │  └────────────────────────────────┘    │              │
└─────┴───────────────┴─────────────────────────────────────────┴──────────────┘
```

### 3.1 Layout Regions

| Region | ID | Description |
|--------|----|-------------|
| Community Strip | `#community-strip` | Vertical icon strip (far left). Community avatars/initials. Click to switch. `+` button to create/join. |
| Sidebar | `#sidebar` | Channel list with collapsible categories. DM section below separator. Search at bottom. |
| Channel Header | `#channel-header` | Channel name, topic, settings icon. |
| Message List | `#message-list` | Paginated message display. "Load older" button at top. New message divider. |
| Message Input | `#message-input` | Auto-expanding textarea, @mention autocomplete, image paste, typing indicator display. |
| Thread Panel | `#thread-panel` | Slide-in panel on right when viewing a thread. Shows root message + replies. |
| Member List | `#member-list` | Optional right panel showing channel members + presence. Toggle via header icon. |

### 3.2 Community Strip

```
┌─────┐
│ MT  │  ← "My Team" (initials, or community avatar)
├─────┤
│ OS  │  ← "Open Source"
├─────┤
│     │
│ ... │
├─────┤
│ DM  │  ← DMs section (always present, not community-scoped)
├─────┤
│  +  │  ← Create/join community
└─────┘
```

- Active community: highlighted border/background
- Unread indicator: dot badge on community icon
- Mention indicator: red badge with count
- DM icon always at top (or bottom — separate from communities)
- Each community remembers the last-viewed channel

### 3.3 Sidebar — Channel List

```
▼ GENERAL                    ← category (collapsible)
  # general              3   ← unread count badge
  # random
  # announcements

▼ DEVELOPMENT
  # backend              ●   ← mention indicator
  # frontend
  # devops

────────────────────────────  ← separator

DIRECT MESSAGES
  alice                  ●   ← online indicator (green dot)
  bob
  group chat (3)         2   ← unread count
```

- Categories collapsible (click header to toggle)
- Channels: bold name = has unread messages
- Unread count badge (number) for messages
- Red dot for unread @mentions
- DM section: shows online status dot, unread count
- Right-click channel → context menu (mute, leave, mark read)

### 3.4 Message Display

Each message renders as:

```
┌─ avatar ─┬─────────────────────────────────────────────┐
│  [img]   │  alice                        10:30 AM      │
│          │  hello everyone! check out #backend          │
│          │                                              │
│          │  [inline image preview ─────────────]       │
│          │                                              │
│          │  👍 3  ❤️ 1  │  + Add Reaction  │  ⋮ More  │
│          │  💬 2 replies (last reply 5 min ago)         │
└──────────┴──────────────────────────────────────────────┘
```

**Message elements:**
- Avatar (from user profile or OAuth)
- Username (display name, colored if admin/owner)
- Timestamp (relative for today, absolute for older)
- Body (rendered Discord markdown → HTML)
- Mentions: `@alice` highlighted, `@here` highlighted differently
- Channel links: `#backend` clickable
- Inline image preview (click for full size)
- Reaction bar: emoji + count, click to toggle your reaction, `+` to add new
- Thread indicator: reply count + last reply time, click to open thread panel
- Hover actions: reply (thread), react, more (edit, delete, copy link)
- Edited indicator: "(edited)" text after timestamp if `edited_at` is set

**Message grouping:** Consecutive messages from the same user within 5 minutes collapse (no repeated avatar/name).

### 3.5 Message Input Area

```
┌─────────────────────────────────────────────────────────┐
│  @bob ▼                              ← autocomplete    │
│  @bobby                              dropdown            │
│  @bob_builder                                           │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  [auto-expanding textarea                          ] 📎│
│                                                         │
│  alice is typing...                                     │
└─────────────────────────────────────────────────────────┘
```

- Auto-expanding textarea (grows with content, max ~10 lines then scroll)
- Enter to send, Shift+Enter for newline
- `@` triggers user mention autocomplete (search by username/display name)
- `#` triggers channel autocomplete
- `/` at start triggers slash command autocomplete
- `📎` button for image upload (opens file picker, images only)
- Ctrl+V / Cmd+V pastes clipboard images directly
- Typing indicator: "alice is typing..." / "alice and bob are typing..." / "several people are typing..."

### 3.6 Thread Panel

```
┌──────────────────────────────────┐
│  Thread in #general          ✕  │
│─────────────────────────────────│
│  alice  10:30 AM                │
│  hello everyone!                │
│─────────── 3 replies ──────────│
│  bob  10:31 AM                  │
│  hey there!                     │
│                                 │
│  carol  10:32 AM                │
│  hi all!                        │
│                                 │
│  [reply input                ]  │
│  ☐ Also send to #general       │
└──────────────────────────────────┘
```

- Opens as a right panel (does not replace message list)
- Shows root message at top
- Thread replies below
- Reply input at bottom
- Optional "Also send to #general" checkbox (broadcasts reply to channel)
- Close button returns to normal view

### 3.7 Search Modal

```
┌──────────────────────────────────────────┐
│  🔍 Search...                            │
│  ┌────────────────────────────────────┐  │
│  │  [search input                  ]  │  │
│  │  Filter: ○ All ○ Messages          │  │
│  │          ○ Channels ○ Users        │  │
│  └────────────────────────────────────┘  │
│                                          │
│  Results:                                │
│  ┌────────────────────────────────────┐  │
│  │  #backend · alice · Feb 15         │  │
│  │  "check out the new **API** docs"  │  │
│  ├────────────────────────────────────┤  │
│  │  #general · bob · Feb 14           │  │
│  │  "the API is looking good"         │  │
│  └────────────────────────────────────┘  │
│  Load more results                       │
└──────────────────────────────────────────┘
```

- Triggered by Ctrl+K or clicking search in sidebar
- Search input with type filter tabs
- Results show context (channel, author, date) with highlighted matches
- Click result → navigate to message in channel (scroll to it)

## 4. Datastar SSE Integration

### 4.1 How It Works

Datastar uses SSE to push HTML fragment updates from server to client. The server decides what changes and sends targeted DOM updates.

**Connection flow:**
1. Browser loads initial full page (server-rendered Hiccup)
2. Datastar opens SSE connection to `/sse/events`
3. Server subscribes to relevant NATS subjects for the user
4. When events arrive (new message, reaction, typing, etc.), server:
   a. Renders the HTML fragment for the change
   b. Sends it over SSE with a Datastar merge directive
   c. Browser updates the DOM in-place

**SSE endpoint:** `GET /sse/events`
- Params: `?community_id=<uuid>` (which community is active)
- Server holds connection open, pushes fragments

### 4.2 Datastar Fragment Types

| Event | Fragment Target | Behavior |
|-------|----------------|----------|
| New message | `#message-list` | Append message HTML at bottom |
| Message edited | `#msg-{id}` | Replace message content |
| Message deleted | `#msg-{id}` | Remove element |
| Reaction added/removed | `#reactions-{msg-id}` | Replace reaction bar |
| Typing start | `#typing-indicator` | Update typing text |
| Typing stop | `#typing-indicator` | Update typing text |
| User online | `#member-{user-id}` | Update presence dot |
| User offline | `#member-{user-id}` | Update presence dot |
| Channel created | `#channel-list` | Append channel to sidebar |
| Unread count update | `#unread-{channel-id}` | Update badge number |
| Notification | `#notification-badge` | Update notification count |
| Thread reply | `#thread-{parent-id}` | Update reply count / append in thread panel |

### 4.3 SSE Reconnection

If SSE connection drops:
1. Datastar auto-reconnects (built-in)
2. Server uses NATS JetStream to replay missed events since last event ID
3. `Last-Event-ID` header used to resume from correct position

## 5. Client → Server Signals

All user actions are HTTP POSTs (Datastar form submissions):

| Action | Endpoint | Payload |
|--------|----------|---------|
| Send message | `POST /web/messages` | `{channel_id, body_md, parent_id?}` |
| Edit message | `POST /web/messages/:id/edit` | `{body_md}` |
| Delete message | `POST /web/messages/:id/delete` | — |
| Add reaction | `POST /web/reactions` | `{message_id, emoji}` |
| Remove reaction | `POST /web/reactions/remove` | `{message_id, emoji}` |
| Switch channel | `POST /web/navigate` | `{channel_id}` |
| Switch community | `POST /web/navigate` | `{community_id}` |
| Mark channel read | `POST /web/read` | `{channel_id, message_id}` |
| Upload image | `POST /web/upload` | Multipart (image file) |
| Typing indicator | `POST /web/typing` | `{channel_id}` |
| Search | `POST /web/search` | `{query, type?, community_id?}` |
| Slash command | `POST /web/command` | `{command, channel_id, community_id}` |
| Create community | `POST /web/communities` | `{name, slug}` |
| Create channel | `POST /web/channels` | `{name, type, visibility, category_id?}` |

All of these proxy to the API internally and return Datastar fragment responses.

## 6. Pages & Routes (Server-Rendered)

| Route | Description |
|-------|-------------|
| `GET /` | Redirect to last community or setup wizard |
| `GET /app` | Main chat application (full page render) |
| `GET /app/channel/:id` | Direct link to a channel |
| `GET /app/dm/:id` | Direct link to a DM |
| `GET /setup` | Community creation wizard (first-time only) |

### 6.1 Setup Wizard Page

```
┌──────────────────────────────────────┐
│                                      │
│     Welcome to ajet chat!            │
│                                      │
│     Create your community            │
│                                      │
│     Name: [My Team              ]    │
│     Slug: [my-team              ]    │
│           chat.example.com/my-team   │
│                                      │
│     [  Create Community  ]           │
│                                      │
└──────────────────────────────────────┘
```

**Note:** The initial first-deployment setup wizard (configure OAuth providers, admin login, create first community) is handled entirely by Auth GW at `/setup/*` routes. The Web SM `/setup` page shown here is used for **subsequent** community creation by already-authenticated users (e.g., clicking `+` on the community strip).

- Slug auto-generated from name (lowercase, hyphenated)
- On submit: creates community + #general, redirects to /app

## 7. Connection Tracking

Web SM maintains per-user connection state:

```clojure
{user-id {:sse-connection   <http-kit-channel>
          :active-community uuid
          :active-channel   uuid
          :nats-subs        [sub-handles...]
          :last-seen        instant}}
```

- On SSE connect: subscribe to user's communities on NATS, track connection
- On SSE disconnect: unsubscribe from NATS, clean up
- On community switch: unsubscribe old community subjects, subscribe new ones
- Multiple tabs: each tab is a separate SSE connection with independent state

## 8. Unread Tracking

**How unread counts are calculated:**

1. `channel_members.last_read_message_id` — stored in API DB
2. When user views a channel: Web SM calls `POST /api/channels/:id/read` with the latest message ID
3. Unread count = messages in channel with `id > last_read_message_id`
4. Mention count = mentions targeting user in those unread messages
5. On SSE events: Web SM pushes updated badge fragments for affected channels

**Sidebar rendering:**
- Bold channel name if unread count > 0
- Number badge for unread message count
- Red dot if unread mentions exist
- Community icon badge: sum of unread mentions across all channels in that community

## 9. Styling & CSS

### 9.1 Approach

**Tailwind CSS** via CDN (play CDN for dev, self-hosted build for prod).

- All layout uses Tailwind utility classes in Hiccup
- Custom CSS limited to: Datastar transition animations, syntax highlighting theme, spoiler reveal animation
- Dark theme only for v1 (Discord-style dark background)
- No CSS preprocessor — Tailwind utilities are sufficient

### 9.2 Color Palette (Dark Theme)

```
Background:        #1e1e2e (base)
Sidebar bg:        #181825 (darker)
Message hover:     #2a2a3c
Text primary:      #cdd6f4
Text secondary:    #a6adc8
Text muted:        #6c7086
Accent:            #89b4fa (links, active channel)
Mention highlight: #f38ba8 (pink, @mention background)
Online dot:        #a6e3a1 (green)
Unread badge:      #f38ba8 (red)
Code bg:           #313244
Input bg:          #313244
Input border:      #45475a
```

### 9.3 Emoji Picker

**Implementation:** Lightweight emoji picker rendered server-side as a Hiccup grid.

```
┌──────────────────────────────────┐
│ 🔍 Search emoji...               │
├──────────────────────────────────┤
│ Frequently Used                  │
│ 👍 ❤️ 😂 🎉 😮 😢 👀 🔥      │
│                                  │
│ Smileys & People                 │
│ 😀 😃 😄 😁 😆 😅 🤣 😂      │
│ 😊 😇 🙂 🙃 😉 😌 😍 🥰      │
│ ...                              │
├──────────────────────────────────┤
│ 😀 👤 🐱 🍎 ⚽ 🚗 💡 🏳️       │  ← category tabs
└──────────────────────────────────┘
```

- Triggered by clicking `+` on reaction bar or emoji button in input
- Server renders emoji grid as Datastar fragment
- Click emoji → POST to add reaction or insert into message input
- Search filters emoji list (server-side filter, returns fragment)
- Category tabs at bottom for quick navigation
- "Frequently Used" based on user's recent emoji (stored in session state)

### 9.4 Notification Toasts

New notifications (beyond badge counts) show as brief toast popups:

```
┌──────────────────────────────────────┐
│  @alice mentioned you in #backend    │  ← toast (top-right)
│  "hey @bob can you review this?"     │
│                                 ✕    │
└──────────────────────────────────────┘
```

- Toasts appear for: @mentions, DMs, thread replies
- Auto-dismiss after 5 seconds
- Click toast → navigate to the message
- Click ✕ → dismiss immediately
- Max 3 toasts visible simultaneously (queue overflow)
- Toasts rendered as Datastar fragments pushed via SSE

## 10. Service Configuration

### 10.1 Config Shape

```clojure
{:server {:host "0.0.0.0" :port 3002}
 :api    {:base-url "http://localhost:3001"}
 :nats   {:url "nats://localhost:4222"
          :stream-name "ajet-events"}
 :assets {:tailwind-cdn true            ;; false in prod (use built CSS)
          :datastar-cdn true}           ;; false in prod (vendored)
 :session {:max-connections 10000}      ;; max concurrent SSE connections
 :ui     {:messages-per-page 50
          :typing-timeout-sec 15
          :toast-duration-sec 5}}
```

### 10.2 Startup / Shutdown Sequence

**Startup:**
```
1. Load config
2. Connect to NATS
3. Initialize connection tracker (atom)
4. Start http-kit server
5. Log "Web SM started on port {port}"
```

**Shutdown (graceful):**
```
1. Stop accepting new SSE connections
2. Send close event to all SSE clients
3. Unsubscribe all NATS subscriptions
4. Close NATS connection
5. Stop http-kit server
6. Log "Web SM stopped"
```

### 10.3 Health Check

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/web/health` | None | Service health |

```json
{"status": "ok", "connections": 142, "checks": {"api": "ok", "nats": "ok"}}
```

### 10.4 Error Pages

| Status | Page | Description |
|--------|------|-------------|
| 404 | Not Found | Unknown web route — shows "Page not found" with link to home |
| 500 | Server Error | Unhandled exception — shows "Something went wrong" with retry link |
| 502 | API Unavailable | API service unreachable — shows banner "Reconnecting..." with auto-retry |

---

## 11. Test Cases

### 11.1 Page Rendering

| ID | Test | Description |
|----|------|-------------|
| WEB-T1 | Initial page load | GET /app returns full HTML with sidebar, message list, input area |
| WEB-T2 | Channel list renders | Sidebar shows categories + channels for active community |
| WEB-T3 | DM list renders | DM section shows user's DM channels |
| WEB-T4 | Community strip renders | Icon strip shows all user's communities |
| WEB-T5 | Message list renders | Channel messages displayed with correct formatting |
| WEB-T6 | Markdown rendering | Discord markdown renders correctly as HTML |
| WEB-T7 | Mention rendering | `@<user:uuid>` renders as `@displayname` with highlight |
| WEB-T8 | Image inline preview | Image attachments render as inline previews |
| WEB-T9 | Thread indicator | Messages with replies show reply count and link |
| WEB-T10 | Setup wizard | GET /setup shows community creation form for first-time users |

### 11.2 SSE & Real-Time

| ID | Test | Description |
|----|------|-------------|
| WEB-T11 | SSE connection established | Browser opens SSE, server subscribes to NATS |
| WEB-T12 | New message appears | Message from another user appears in message list without refresh |
| WEB-T13 | Message edit updates | Edited message content updates in-place |
| WEB-T14 | Message delete removes | Deleted message disappears from list |
| WEB-T15 | Reaction update | Adding/removing reaction updates reaction bar in real-time |
| WEB-T16 | Typing indicator shows | Other user typing shows indicator below input |
| WEB-T17 | Typing indicator clears | Indicator disappears after 15 seconds of no typing |
| WEB-T18 | Presence update | User going online/offline updates sidebar dot |
| WEB-T19 | New channel appears | Channel created by admin appears in sidebar |
| WEB-T20 | Unread badge updates | New message in other channel updates unread count badge |
| WEB-T21 | SSE reconnect | After connection drop, reconnects and catches up on missed events |
| WEB-T22 | Community switch | Switching community updates sidebar and message list |
| WEB-T23 | Channel switch | Switching channel loads new messages, marks old as read |

### 11.3 User Actions

| ID | Test | Description |
|----|------|-------------|
| WEB-T24 | Send message | Typing and pressing Enter sends message, appears in list |
| WEB-T25 | Shift+Enter newline | Shift+Enter adds newline, does not send |
| WEB-T26 | @mention autocomplete | Typing @ shows user dropdown, selecting inserts mention |
| WEB-T27 | #channel autocomplete | Typing # shows channel dropdown |
| WEB-T28 | /slash command | Typing / shows command autocomplete |
| WEB-T29 | Edit message (hover) | Hover → edit icon → inline edit mode → save |
| WEB-T30 | Edit after 1 hour | Edit button not shown for messages > 1 hour old |
| WEB-T31 | Delete message | Hover → delete icon → confirmation → message removed |
| WEB-T32 | Add reaction | Click + on reaction bar → emoji picker → reaction added |
| WEB-T33 | Toggle reaction | Click existing reaction emoji to toggle on/off |
| WEB-T34 | Image paste | Ctrl+V with clipboard image → upload → preview in input → send |
| WEB-T35 | Image upload button | Click 📎 → file picker → select image → upload → send |
| WEB-T36 | Open thread | Click thread indicator → thread panel opens on right |
| WEB-T37 | Reply in thread | Type in thread input → reply appears in thread |
| WEB-T38 | Search | Ctrl+K → search modal → type query → results shown → click to navigate |
| WEB-T39 | Create channel | Admin clicks + → form → submits → channel appears in sidebar |
| WEB-T40 | Join public channel | Click channel → join prompt → joined → messages load |
| WEB-T41 | Leave channel | Right-click → leave → channel removed from sidebar |
| WEB-T42 | Collapse category | Click category header → channels hidden → click again → shown |
| WEB-T43 | Mark channel read | Opening channel marks it as read, badge clears |
| WEB-T44 | Paginated loading | Scroll to "Load older" → click → older messages prepended |
| WEB-T45 | Create community | Click + on community strip → wizard → community created |

### 11.4 Profile & Settings

| ID | Test | Description |
|----|------|-------------|
| WEB-T46 | User profile popover | Click username → popover with avatar, name, status, role |
| WEB-T47 | Set status | Click own avatar → status input → save |
| WEB-T48 | Set nickname | In community settings → nickname field → save |

### 11.5 Error Handling

| ID | Test | Description |
|----|------|-------------|
| WEB-T49 | API unreachable | Shows error banner, retries |
| WEB-T50 | Message send fails | Error shown inline below input, message not lost |
| WEB-T51 | Upload too large | Error shown, file not uploaded |
| WEB-T52 | Rate limited | Error shown with retry countdown |

### 11.6 Responsive / Layout

| ID | Test | Description |
|----|------|-------------|
| WEB-T53 | Desktop layout | Full 3-column layout renders correctly |
| WEB-T54 | Thread panel coexists | Thread panel + message list visible simultaneously |
| WEB-T55 | Long messages wrap | Long messages wrap correctly, no horizontal scroll |
| WEB-T56 | Code block rendering | Fenced code blocks render with syntax highlighting |
| WEB-T57 | Spoiler tags | `||spoiler||` renders hidden, click to reveal |
