contact messages: unified overlay, corner ✉️ icon for everyone

The admin inbox leaves the short-lived /_m page and joins the visitor panel as one overlay (ContactOverlay shell over paskia's shared blur backdrop, centered dialog, full-screen on mobile): the corner ✉️ icon opens the visitor thread for visitors and the wider sidebar inbox for admins, while {contact:} trigger buttons always open the visitor side so admins can test both. The icon highlights unread state without extra connections: visitors via localStorage seen/last-admin timestamps, admins via the contact_unread count added to the /_api/settings probe, both kept live by the bundle once loaded. MessagesView owns its admin socket only while the overlay is open.
This commit is contained in:
LeoVasanko committed 2026-09-28 01:37:37 +00:00
1 parent 9a2435f03a
commit afa6b967e6
16 files changed
+451 -427

No files matched your search

+38 -24
View File
@@ -1,25 +1,30 @@
# Contact messages
Visitor→admin messaging built into the site: a `{contact: Label}` directive
in article Markdown renders a button that opens a chat-style panel; messages
are stored in the kanta database and answered from the admin inbox on `/_m`.
Visitors are anonymous — a localStorage session token identifies their
thread — and everything realtime rides the two existing WebSockets; there is
no REST polling anywhere.
in article Markdown renders a button that opens the messaging overlay, and
the corner ✉️ icon (top right, for everyone) opens it too — the visitor's
own thread for visitors, the admin inbox for editors. Messages are stored
in the kanta database. Visitors are anonymous — a localStorage session
token identifies their thread — and everything realtime rides the two
existing WebSockets; there is no REST polling anywhere.
- `pagerite/messages.py` — the feature's core: the strict message renderer,
thread store ops, wire payloads, both socket protocols, the `/_m` inbox
page route and the `/_contact/files` upload route.
thread store ops, wire payloads, both socket protocols, the admin unread
count and the `/_contact/files` upload route.
- `pagerite/data.py` — `Data.contact` (`ContactThread`/`ContactMessage`).
- `pagerite/tracking.py` — the sockets the protocol rides (see below).
- `pagerite/views.py` — the `{contact:}` directive (`_contact_tag`), the
`/_m` page render and the on-demand bundles' asset config.
- `frontend/src/contact-main.js` + `ContactPanel.vue` — the visitor panel,
imported on demand by `pagerite.js` on trigger clicks.
- `pagerite/api.py` — `/_api/settings` carries `contact_unread` (the admin
icon badge seed — the probe runs on every page load, no polling).
- `pagerite/views.py` — the `{contact:}` directive (`_contact_tag`) and the
on-demand bundle's asset config.
- `frontend/src/contact-main.js` + `ContactOverlay.vue` — the overlay entry
and shell (paskia's shared blur backdrop, centered dialog, full-screen on
mobile), imported on demand by `pagerite.js` on icon/trigger clicks.
- `frontend/src/ContactPanel.vue` — the visitor thread content.
- `frontend/src/MessagesView.vue` — the admin inbox content (thread list,
replies, read marking); connects its admin-gated socket only while open.
- `frontend/src/Composer.vue` + `richtext.js` — the shared composer and its
DOM→Markdown serializer.
- `frontend/src/messages-main.js` + `MessagesView.vue` — the admin inbox
page at `/_m` (own pens-row icon, own socket lifecycle).
## Trigger
@@ -38,8 +43,11 @@ The token is `crypto.randomUUID()` in localStorage
(`pagerite-contact-token`), generated on first use; the server stores only
its blake3 hash as the thread key. Pages are shared/cached renders, so the
thread UI is entirely client-side. A per-browser "seen" timestamp
(`pagerite-contact-seen`) drives the unread dot on trigger buttons when an
admin reply arrives while the panel is closed.
(`pagerite-contact-seen`) and the newest admin reply's timestamp
(`pagerite-contact-lastadmin`) drive the unread dot on trigger buttons and
the corner ✉️ icon when an admin reply arrives while the panel is closed —
including on a fresh page load before any socket activity (pagerite.js
reads the pair directly; the bundle keeps it live once loaded).
The panel rides the public `/_ws` activity socket via a bridge in
`pagerite.js` (`register`/`send`, sharing its queue and reconnect pacing —
@@ -78,18 +86,24 @@ solves and retries once. Challenges are a per-connection local in the
## Admin side
Admins watch the inbox on its own page, **`/_m`** (✉️ icon in the pens
row), served by the existing `/_api/ws/analytics` socket (admin-gated like
all of `/_api`). Analytics payloads keep their type-less snapshot shape;
messaging frames always carry a `type` field, and that is how each client
picks out its own frames (the analytics viewer ignores them). On connect
the server sends `{type: "threads", threads: [...]}` (100 most
recently active); new messages and replies push single-thread updates.
Admin→server frames: `{type: "reply", id, text}` and `{type: "read", id}`.
Editors open the inbox from the same ✉️ corner icon (the trigger buttons
always open the visitor side, so both can be tested side by side): the
same overlay, wider to fit the thread list, served by the existing
`/_api/ws/analytics` socket (admin-gated like all of `/_api`) which
`MessagesView` connects only while the overlay is open. Analytics payloads
keep their type-less snapshot shape; messaging frames always carry a
`type` field, and that is how each client picks out its own frames (the
analytics viewer ignores them). On connect the server sends
`{type: "threads", threads: [...]}` (100 most recently active); new
messages and replies push single-thread updates. Admin→server frames:
`{type: "reply", id, text}` and `{type: "read", id}`.
Replies store the SSO `remote-user` name for audit but are labeled with
the currently configured **site name** in the wire payload (substituted at
read time) and mark the thread read; `unread` on a thread means its last
message is the visitor's and newer than the admin's last read.
message is the visitor's and newer than the admin's last read. The icon's
badge (a count of unanswered threads) is seeded by the `/_api/settings`
probe's `contact_unread` on every page load and kept live by the inbox
while it is open.
## Message format and the XSS model