There is no Messages page. Conversations live inside each project, alongside the files, site visits, certifications, and invoices they refer to. A red dot on a project row means someone is talking. Open the project to read and reply.
Every conversation in the portal is linked to a project. No exceptions. General questions that don't belong to a project belong on a phone call or a text message — not in the system. This constraint keeps the conversation log useful: when you open a project six months from now, you see every discussion that happened about it, in the same place as the certs, photos, and invoices.
Open any project. Scroll to the Correspondence section. That's where the conversations are — inline, not behind a link.
| Surface | What You See |
|---|---|
| Project Detail | Full conversation thread with compose area, Friday access, file upload |
| Projects table | Red dot on the project ID when there are unread messages |
| Calendar | Red dot on the inspection event when the linked project has unread messages |
| Morning Briefing | Unread counts in the Messages stat row — click through to Projects |
| Notification Bell | Unread message count in the bell dropdown — click through to Projects |
| Sidebar Search | Conversations appear in quick-search results — click to open the linked project |
The Correspondence section on a project detail page shows one thread per conversation. If a project has multiple conversations (a general thread and an RFI, for instance), tabs appear across the top.
| Element | Treatment |
|---|---|
| Sender + timestamp | Monospace, uppercase, light — metadata recedes |
| Message body | Serif, normal weight — the prose stands forward |
| Friday responses | Cream background, gold left border, ···friday··· dot-matrix attribution |
| Date breaks | 1px dashed rule with centered date — no heavy dividers |
| Actions | Lowercase monospace text links in a bottom bar — no buttons |
| RFI conversations | Red left border instead of the default cream/neutral |
If a project has no conversation yet, the Correspondence section shows a compose area. Type a message and send — a conversation is auto-created and linked to the project.
You can also start a conversation by clicking + new message in the action bar, which opens a compose area with a type selector (general, RFI, or review).
| Type | When to Use | Visual Cue |
|---|---|---|
| Project | General discussion about the job | Neutral left border |
| RFI | Request for information — something is unclear or missing | Red left border, RFI badge |
| Review | Cert or invoice review discussion | Neutral, linked to review queue |
project_id: null. This is by design: if it doesn't belong to a project, it doesn't belong in the portal.
| Action | How |
|---|---|
| Send a message | Enter or click the send arrow |
| New line | Shift + Enter |
| Ask Friday | Click ask friday in the action bar |
| Attach a file | Click attach in the action bar |
| Flag as RFI | Click flag rfi — converts the conversation type |
Messages are sent via REST and then broadcast to all viewers via WebSocket. You see your message immediately (optimistic render) while the server confirms delivery.
A persistent WebSocket connection keeps threads live. When someone else sends a message to the conversation you're viewing, it appears instantly.
| Feature | Behavior |
|---|---|
| Typing indicators | "Jacob is typing..." appears below the thread. Auto-clears after 4 seconds. |
| Viewing presence | Thread header shows who else is viewing the same conversation. |
| Read receipts | Opening a conversation marks all messages as read. The red dot on the project row clears. |
| Live delivery | New messages appear for all viewers without refresh. |
| Reconnection | Auto-reconnects with 3-second backoff if the connection drops. |
The ask friday action in the compose bar invokes Friday AI directly inside the conversation thread. Friday reads the entire message history, understands the linked project, and responds as a visible participant.
Friday's response is saved permanently and broadcast via WebSocket. Friday messages are visually distinct: cream background, gold left border, dot-matrix attribution (···friday···).
| Ask Friday (in thread) | Friday Bubble (floating icon) | |
|---|---|---|
| Where | Inside a project's Correspondence section | Floating eye icon, available on every page |
| Audience | Team-visible — everyone sees the reply | Private — only you see it |
| Storage | Permanent (SQLite, part of the project record) | Session only (lost on tab close) |
| Protocol | WebSocket (live broadcast) | REST (request/response) |
| Context | Reads full conversation history + project data | Reads current page context |
Same AI, different audiences. Friday in the thread is a team participant. The bubble is a private assistant.
The system tracks read state per user per conversation. Unread messages surface as red dots across multiple views, so you don't need to check each project individually.
| Where | Indicator | Clears When |
|---|---|---|
| Projects table | 6px red dot left of project ID | You open the project and view the thread |
| Calendar event | 5px red dot on the inspection | You open the linked project |
| Notification bell | Gold badge with total unread count | Messages are read |
| Morning Briefing | Open/unanswered/aging stat counters | Conversations are addressed |
The project's Activity section is a chronological feed of all events: status changes, file uploads, cert deliveries, invoice payments, and human messages. Conversations are woven into the timeline alongside system events.
| Entry Type | Appearance |
|---|---|
| System event | Compact one-line: icon + description + timestamp. Grey text. |
| Human message | Expandable snippet with sender name, conversation title, and preview. Neutral left border. |
| RFI message | Same as human, but with an orange left border and RFI badge. |
Filter buttons at the top: All, Messages, Events, RFIs, Files. A "Load more" button appears when the timeline exceeds 50 entries.
When a message is sent, the notification router decides how to alert other participants:
| Channel | When |
|---|---|
| Portal | Always — in-app notification + red dot |
| If the recipient is offline. Delayed 2 minutes (cancellable if they come online). | |
| Field team members (Darius) — critical or time-sensitive only. | |
| SMS | If enabled in notification preferences — critical notifications only. |
DND hours are respected. Consolidation batches multiple notifications within a 2-minute window into a single alert. Preferences are configurable per user via the gear icon in the notification panel.
| What | Where |
|---|---|
| Conversations & messages | /data/portal-auth/chat.db |
| WebSocket endpoint | /portal/chat/ws?token={jwt} |
| REST base | /portal/chat/* |
| Unread by project | /portal/chat/unread-by-project |
| Friday context | /portal/friday/chat-context |
| Project timeline | /portal/chat/project-timeline/{id} |
All conversations and messages are stored permanently in SQLite. The WebSocket connection requires a JWT token re-issued via /portal/chat/ws-token (workaround for httpOnly cookie constraints). Messages are sent via REST for reliability, then broadcast via WebSocket for immediacy.