init prds
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
# 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 ] │
|
||||
│ │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 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:
|
||||
|
||||
```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. 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 | `||spoiler||` renders hidden, click to reveal |
|
||||
Reference in New Issue
Block a user