22 KiB
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:
@alicehighlighted,@herehighlighted differently - Channel links:
#backendclickable - 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_atis 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:
- Browser loads initial full page (server-rendered Hiccup)
- Datastar opens SSE connection to
/sse/events - Server subscribes to relevant NATS subjects for the user
- 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:
- Datastar auto-reconnects (built-in)
- Server uses NATS JetStream to replay missed events since last event ID
Last-Event-IDheader 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 ] │
│ │
└──────────────────────────────────────┘
- Shown after first-ever OAuth login
- 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:
{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:
channel_members.last_read_message_id— stored in API DB- When user views a channel: Web SM calls
POST /api/channels/:id/readwith the latest message ID - Unread count = messages in channel with
id > last_read_message_id - Mention count = mentions targeting user in those unread messages
- 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. Test Cases
9.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 |
9.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 |
9.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 |
9.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 |
9.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 |
9.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 | ` |