init prds

This commit is contained in:
2026-02-17 01:08:02 -05:00
parent 79b6a5e225
commit a3b28549b4
8 changed files with 2329 additions and 0 deletions
+441
View File
@@ -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 |