Commit Graph
308 Commits
Author SHA1 Message Date
LeoVasanko ba2d6ec60c contact messages: editor preview renders no button when messaging disabled
The preview registered the {contact:} directive unconditionally, so with
no configured label it showed the button with the 'Contact us'
placeholder while the served page renders nothing. Mirror the page
logic in the preview render path.
2026-09-28 17:06:38 +00:00
LeoVasanko 023783d189 nitro theme: slanted parallelogram article buttons, hover scale lift
In-article buttons (the {contact:} trigger and raw <button>s, not the
editor chrome) get a / slanted parallelogram clip-path in the nitro
theme: nav-tab-style bezier sweeps at the obtuse corners, small arcs at
the sharp ones, fixed 1.7em height so every button shares one shape.
Base stylesheet: the contact envelope's line-height no longer inflates
the button, and article buttons get a slight scale(1.01) hover lift.
2026-09-28 16:57:35 +00:00
LeoVasanko ed2d123276 contact messages: document the Brand <email> mailto recipient 2026-09-28 15:46:15 +00:00
LeoVasanko c568af8285 contact messages: mailto recipient is Brand <email>
The contact-email frame now carries the RFC-encoded recipient (John
Smith <john@example.com> → John%20Smith%20%3Cjohn@example.com%3E),
encoded server-side where the current brand lives; the link text stays
the bare address and the localized subject rides the query as before.
2026-09-28 15:45:14 +00:00
LeoVasanko 8d577dc56e contact messages: label is the enable switch; single-line settings row
Data.contact_enabled is gone: an empty contact label disables messaging
(no buttons, no visitor icon, sends rejected). The settings row is one
line — flag selector, label, email, subject as small boxes with tooltip
instructions — and saves re-render the page in place so button/dialog
strings update without a reload; the open dialog also re-reads its
strings on page swaps. The built-in Contact us default survives only as
the inert preview placeholder.
2026-09-28 15:43:01 +00:00
LeoVasanko 59fe426e5e contact messages: messaging header = flag selector + fused enable toggle
The site settings' messaging block now opens with the article editor's
LangSelect (the flag dropdown on the shared shell-wide editorLang, same
option construction) instead of a plain select, and the enable checkbox
is fused into the "messaging" section header — one line for the whole
header. The separate enabled field row is gone.
2026-09-28 15:28:03 +00:00
LeoVasanko 719546a0ee contact messages: document the PoW quota, email reveal and trail marker
docs/messages.md matches the quota protocol (proof frames, quota acks,
contact-email, upload 429 re-auth); docs/analytics.md notes the
server-originated contact trail marker; AGENTS.md's tracking/messages
bullets match.
2026-09-28 14:42:47 +00:00
LeoVasanko 2843469e04 contact messages: client proof flow, upload re-auth, trail ✉️
The contact bundle submits each solved challenge immediately and treats
{quota} frames as the grant that releases queued sends/uploads; an
upload answered 429 "pow" re-registers for a fresh challenge and
retries once it lands (Composer's reauthorize prop). The contact email
frame fills the dialog's mailto link only after a solve. Visit trails
render the ✉️ marker on the step where the panel was opened and the
proof solved.
2026-09-28 14:39:49 +00:00
LeoVasanko 7fd73da933 contact messages: PoW quota, post-solve email reveal, analytics trail marker
The client now submits each solved challenge as soon as it is ready
({token, pow}); a validated proof grants a quota of 10 messages/uploads
(RAM, keyed by token hash so the HTTP upload route shares it) and a
fresh challenge is pushed only when the quota runs out. The site
contact email leaves the page asset config and is revealed over the
socket only after a solve. A validated proof also records a
server-originated contact marker in the analytics raw log, which
display() lands on the visit's trail step where it first happened.
2026-09-28 14:33:17 +00:00
LeoVasanko 106712a359 contact messages: document the settings, localization and toggle
docs/messages.md gains the site-settings/localization section (enabled
toggle, mailto link, chunk-keyed string translations and their manual
override UI); docs/localization.md notes the site strings riding the
title/nav pipeline; AGENTS.md's i18n/translate bullets match.
2026-09-28 06:35:55 +00:00
LeoVasanko dd82005f6c contact messages: settings UI, localized dialog title, mailto link
The site editor gains a messaging block: enabled toggle, contact email,
and label/subject fields with a language selector that switches them to
editing per-language overrides (the manual-override UI for the strings
auto translators fill). The visitor dialog titles itself with the
localized label and shows a mailto link (localized subject prefilled)
when an email is configured; pagerite.js hides the corner icon from
visitors when messaging is disabled.
2026-09-28 06:33:14 +00:00
LeoVasanko f5fbe8ddc5 contact messages: contact settings in /_api/settings, ?lang overrides
GET reports contact_enabled/label/email/subject; with a translation
?lang= it also reports the per-language label/subject overrides (*_tr).
PUT accepts the contact fields; with a translation ?lang= only the
label/subject apply, written as overrides into the chunk-keyed trans
store (i18n.set_string_translation) — empty or the original's text
drops the override.
2026-09-28 06:27:45 +00:00
LeoVasanko f68071cb51 contact messages: translator service picks up the site contact strings
pending_items emits the effective contact label/subject as title-kind
fragments (chunk-keyed trans storage, like node titles), and nav-mode
jobs carry them as extra top-level list items, so Seed-X and LLM
translators localize the contact chrome with the menu. Manual overrides
land in the same store via i18n.set_string_translation.
2026-09-28 06:26:13 +00:00
LeoVasanko 2adafd215b contact messages: site settings fields, localized strings, enabled toggle
Data gains contact_enabled/contact_label/contact_email/contact_subject.
The label and subject localize via the chunk-keyed trans store
(i18n.site_string), ride the page asset config as pagerite:contact-*
(absent when disabled), and default the {contact:} button label; a
per-button {contact: Label} arg still wins. Disabled messaging renders
no buttons and rejects visitor sends; admins keep the inbox.
2026-09-28 06:24:44 +00:00
LeoVasanko 1796942892 contact messages: drop the accent focus outline on the trigger 2026-09-28 02:59:51 +00:00
LeoVasanko f81e5f593b contact messages: center trigger icon and label (inline-flex) 2026-09-28 02:48:24 +00:00
LeoVasanko a68c07da53 contact messages: enlarge the trigger button's ✉️ (1.38em span) 2026-09-28 02:46:44 +00:00
LeoVasanko cc14303179 contact messages: trigger button uses the navbar font and weight
Nav family/weight become --font-nav/--nav-weight (base defaults body/400; nitro heading/700, corporate 600), referenced by both #nav and .contact-trigger. The button keeps the article's size and line-height.
2026-09-28 02:16:18 +00:00
LeoVasanko e555714c2e contact messages: 1.38em corner icon, ✉️ prefix on the trigger button 2026-09-28 02:13:28 +00:00
LeoVasanko 2e08f4861f contact messages: fix icon size specificity, scroll to end on open
.editor-pens button's font: inherit out-specified .contact-icon; scope the size bump to .editor-pens .contact-icon. The visitor panel only ever existed while open (the overlay is v-if'd), so its state.open watch never fired: scroll to the newest message on mount instead, with the message-count watch covering sent/received while open.
2026-09-28 02:07:48 +00:00
LeoVasanko 3ab28d0d7d contact messages: bump corner ✉️ icon size to match its neighbors 2026-09-28 02:01:52 +00:00
LeoVasanko d6088b90ff contact messages: unread nudge animation instead of dot/pill badges
Unread state (new reply for visitors, unanswered threads for admins) now animates the trigger button and the corner icon themselves: a scale-up shake every 4.5 s, gated on prefers-reduced-motion, stopping when read (the has-unread class goes away). The static accent dot and the admin count pill are gone.
2026-09-28 01:46:07 +00:00
LeoVasanko afa6b967e6 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.
2026-09-28 01:37:37 +00:00
LeoVasanko 9a2435f03a contact messages: centered panel on paskia's shared blur backdrop
The panel was a right-docked sidebar with its own translucent backdrop. It is main content: now centered at the paskia dialog's dimensions (min(36rem,100%) x min(42rem,92vh)), full-screen below 36rem, holding the refcounted global blur backdrop (z 1099) while open with the panel's click-catch overlay at 1100 (below the auth iframe at 9999). Escape closes too.
2026-09-28 01:04:33 +00:00
LeoVasanko f3c288c76c contact messages: trigger hover follows the navbar tab idiom
The filled button now hovers like nitro's accent nav tab: text --ink (falling back to --bg on themes without it) switching to the bright --tab-hover with the same text-shadow glow, instead of the invented fill shift.
2026-09-28 00:58:23 +00:00
LeoVasanko 78b1fbc16e contact messages: filled accent trigger button, subtle hover
The {contact:} button was a hollow outline that inverted wholesale on hover; now it is filled with the accent (--bg text, the pairing accents are chosen to contrast with), hover only nudges the fill toward the page text color, and keyboard focus gets an outline. The button also gains margins so it separates from surrounding text both standalone and inline.
2026-09-28 00:45:00 +00:00
LeoVasanko 4ad4e3bdff contact messages: standalone /_m admin inbox, site-name reply label
The inbox moves out of the analytics tab to its own page at /_m (own pens-row icon, own entry and socket lifecycle; the admin-gated /_api/ws/analytics socket keeps serving both, with typed messaging frames ignored by AnalyticsView). Admin replies are labeled with the currently configured site name in the wire payload (the SSO name stays stored for audit), and the thread view is capped to roughly the visitor panel's width.
2026-09-27 18:59:22 +00:00
LeoVasanko f4d1f0ac58 contact messages: optional unvalidated email, plain relative timestamps
Email is no longer required and its format is not checked anywhere
(placeholder notes it's optional); only the name gates the first
message. Timestamps are just the analytics relative format — no full
date on hover.
2026-09-27 18:31:21 +00:00
LeoVasanko 67c19230b4 contact messages: visitor identity, relative timestamps, alternating headers
- The first message of a thread must carry name + email introducing the
  visitor; the panel shows the two fields until the thread has an
  identity (send stays disabled while they look empty, so the draft is
  never lost), prefilled from and saved to localStorage. The identity
  is stored on ContactThread (msgspec defaults — old databases decode
  unchanged) and fixed afterwards; it labels the visitor's messages
  instead of 'you', and the admin inbox lists threads by name with the
  email as a mailto in the thread header.
- Timestamps use the analytics relative formatter (formatWhen), with
  the full date on hover.
- Message headers alternate alignment (own messages right, theirs
  left) as the visual side cue; the sent-from page link is dropped
  from the display (kept as stored metadata).
- Composer gains a sendDisabled prop for the intro gate.
2026-09-27 18:19:03 +00:00
LeoVasanko e810c5f664 contact messages: plain thread view instead of bubbles
Drop the bubble styling (muted/accent backgrounds, rounded boxes,
left/right alignment) from both the visitor panel and the admin inbox:
messages now stack full-width as a flat thread with hairline
separators, a small muted meta line (author + time) above the body,
and 'you'/'visitor' fallback labels carrying the side distinction
instead of color.
2026-09-27 17:41:59 +00:00
LeoVasanko e3080d70c0 contact messages: hold the PoW challenge as a WS-handler local
Move the PoW state from a module-level dict keyed by WebSocket in
messages.py to a plain per-connection local in tracking.activity_ws,
making PoW a WS-level concern instead of being buried in the message
handler:

- activity_ws validates and burns the solution (raw.pop('pow')) before
  forwarding a message frame, and issues a fresh challenge after every
  registration and every vetted message frame; a failed validation is
  answered with a 'required' challenge and the frame never reaches
  messages.py.
- messages.py loses the _pow dict, _issue_pow and the disconnect
  cleanup for it; visitor_frame needs no return value since a vetted
  frame always triggers a fresh challenge (a too-long message simply
  comes with contact-error and required=false, so the client does not
  retry it).

The wire protocol and the frontend are unchanged.
2026-09-27 16:35:14 +00:00
LeoVasanko 0adcd0bf90 contact messages: browser-side PoW solving in the contact panel
The bridge routes {type: pow} frames to the panel. contact-main solves
each fresh challenge in the background with the backend-served easypow
solver module (imported by URL from a variable so Rollup leaves it
dynamic), aborting stale solves on replacement challenges. Sending
awaits the current solution and burns it with the message; a
'required' rejection re-solves and retries the message once.

Composer and ContactPanel need no changes: uploads stay ungated, and
the send path just awaits the in-flight solve.
2026-09-27 06:14:04 +00:00
LeoVasanko ec35bc71e8 contact messages: easypow proof-of-work for sends, pending-upload cleanup
New dependency: easypow (PBKDF2-SHA512 challenges, bundled JS solver).

Sends are PoW-gated: registration/resync and every accepted message
come with a fresh single-use challenge (~5 s solve), held in per-socket
RAM state only — nothing in the database. A message burns one
solution; a missing, stale or spent solution is dropped and answered
with a fresh 'required' challenge. The bundled browser solver is
served at /_contact/easypow.js straight from the installed package, so
client and validator versions always match.

Uploads are deliberately not PoW-gated (the transfer rate is the real
cost). Instead, uploads are tracked as pending per session and a quota
caps pending files at 10 — answered 429, not 403, which could trip
auth flows. A sent message strikes the files it references off the
pending set; when the session's last socket closes, the remaining
pending files are deleted from the store.
2026-09-27 06:13:53 +00:00
LeoVasanko 6b34719397 contact messages: inert .file attachment blobs, universal forced download
Rework attachment storage and serving per the refined separation
scheme:

- Non-media uploads are stored as {hash}.file blobs (store_attachment)
  — the original extension never lands on the filesystem, so the store
  directory holds nothing double-click-executable. The original name
  lives only in the URL: /_f/{hash}/{original-name}. The .file suffix
  itself is rejected in URLs.
- Serving is strictly separated: /_f/{name} serves media only (hash
  with or without extension, .orig. still blocked) and 404s .file
  names; /_f/{name}/{filename} serves only {name}.file blobs, never
  media, never by hash alone.
- Every /_f/ response now carries bare 'content-disposition:
 attachment' (the URL's last segment already names the download) and
  x-content-type-options: nosniff — media included. Subresource
  fetches (<img>, <video>, favicons, the zoom viewer) ignore the
  disposition, so inline media is unaffected, while a direct
  navigation can never render or sniff a store file into something
  executable in the site's origin. The mime-based script-carrier
  forcing and the header filename are gone (subsumed).
- Message uploads treat SVG as media (rasterized derivatives; an
  as-is-served .svg is script-inert inside <img>), and undecodable
  fake images fall through to the .file attachment path.
2026-09-26 03:35:08 +00:00
LeoVasanko 95554b4165 contact messages: allow all attachment types, enforce downloads
Drop the script-carrier blocklist: any file type (html, exe, svg, ...)
may be attached now, with safety from enforced downloads instead of
type filtering, verified twice:

- the attachment chip anchor carries a download attribute (already),
- the link is /_f/{name}/{original-filename}, a new file-store route
  that answers content-disposition: attachment naming the original
  file (ASCII-safe fallback + RFC 5987 UTF-8 form). The filename rides
  in the URL itself — no filename registry in the database.

Close the residual gap on the plain /_f/{name} route: a stored file
whose mime executes script in origin (html/xml/svg/js) is forced to
download there too, so stripping the filename segment off an
attachment URL cannot turn it into an in-origin page. All /_f/
responses now carry x-content-type-options: nosniff. Subresource
fetches (<img>, favicons) ignore Content-Disposition, so inline
images and icons are unaffected.

Undecodable fake images (store_image falls back to as-is storage) now
attach as files instead of being marked inline images.
2026-09-26 01:59:08 +00:00
LeoVasanko 49bdedd938 contact messages: fix inline formatting loss, attachment uploads
The serializer treated only DIV/P as blocks: a formatting element
directly under the editable root (a bolded segment of an undivided
line is a top-level <b>) became a separate PLAIN block, losing both
the formatting and the paragraph structure. Top-level inline runs now
accumulate into one paragraph, and style-based formatting (Firefox's
font-weight/font-style spans) maps back to markers.

Attachments: the upload allowlist rejected everything but a handful of
types with a console-only 415. The logic inverts — raster images get
derivatives, script carriers (svg/html/js/xml...) are refused, and any
other file stores as a download chip. Upload failures now show in the
composer, and toolbar buttons keep the contenteditable selection on
click (mousedown.prevent).
2026-09-26 01:31:45 +00:00
LeoVasanko 975ebfe5e7 contact messages: docs
docs/messages.md covers the trigger, both socket protocols, the storage
model, the XSS stance (grammar restriction via a strict renderer, never
HTML sanitization), uploads, and the MVP gaps (PoW seams, moderation,
orphan GC, i18n). AGENTS.md and frontend-runtime.md learn the new
module, bundles and asset keys.
2026-09-26 01:17:30 +00:00
LeoVasanko bbb11bf17e contact messages: admin inbox on /_a
The analytics viewer gains a messages tab over the same socket: frames
with a type field are inbox payloads (thread list on connect, live
thread upserts), the type-less snapshot stays analytics. MessagesView
shows the thread list with unread dots and the selected thread with the
shared composer for replies; opening a thread marks it read server-side.
The header badge counts unread threads.
2026-09-26 01:14:52 +00:00
LeoVasanko eea89032a2 contact messages: visitor panel and composer
The composer is a constrained contenteditable (plain-text paste,
toolbar formatting, upload chips for images/attachments) serialized to
a Markdown subset by richtext.js — the wire format is text and the
server re-renders it, so no user markup ever reaches a page. Uploads go
to /_contact/files with the session token (admins share the component
via /_api/files). ContactPanel shows the thread pushed live over the
activity socket: pagerite.js lazy-loads the bundle on trigger clicks,
routes socket pushes to it, re-registers the session token on every
reconnect (the resync), and dots trigger buttons on unread replies.
2026-09-26 01:11:14 +00:00
LeoVasanko 776f408743 contact messages: {contact: Label} article directive
A lone {contact: Label} line in article Markdown renders a
.contact-trigger button that opens the visitor panel (pagerite.js binds
the click and lazy-loads the contact bundle; inert in the editor
preview, whose resolver now matches the served page). The on-demand
bundle's URLs ride in the pagerite-assets config like langselect's, and
the build gets a contact entry point. Description/card mining renders
resolve the tag to nothing.
2026-09-26 01:05:38 +00:00
LeoVasanko 98c66440f6 contact messages: public upload endpoint
PUT /_contact/files/{name} stores panel uploads in the content-addressed
file store: raster images through the usual derivative pipeline, a small
allowlist of document types as-is. SVG/HTML are excluded (script
carriers). Gates for now: a session-shaped token plus size/type caps;
the proof-of-work stamp plugs in here later. The dev proxy learns the
/_contact path.
2026-09-26 01:02:28 +00:00
LeoVasanko 2fb0cc219b contact messages: wire the sockets
/_ws routes frames carrying a session token to pagerite.messages (and
now also pushes thread state back down the same socket); the visitor
registry is cleaned on disconnect. /_api/ws/analytics becomes
bidirectional: admins get the contact inbox on connect, visitor messages
are pushed live, and reply/read frames are handled. Analytics payloads
keep their type-less shape; messaging frames always carry a type field.
2026-09-26 01:00:59 +00:00
LeoVasanko 1d827da62a contact messages: data model, strict renderer, socket protocol core
Data.contact holds visitor threads keyed by blake3(session token).
Message bodies are a Markdown subset rendered with a strict MarkdownIt
(html=False): the stored html is always markdown-it output, never
sanitized user markup. pagerite/messages.py owns the store ops, wire
payloads and both socket protocols (visitor frames on /_ws, admin frames
on /_api/ws/analytics); the socket wiring follows.
2026-09-26 00:59:45 +00:00
LeoVasanko 3c9ef04ffd Transition map beads: connector-colored elongated pulses
Beads now take their connector's color — accent for internal links,
plain text color for external ones (matching the tconn-exit styling) —
and render as an elongated pulse along the direction of travel instead
of a plain dot: a rounded capsule with a gradient-faded tail, rotated
to the edge's angle (cached per flow). Flow objects carry an 'external'
tag from buildFlows; the gradients live in the svg defs with stop
colors resolved from CSS so theme variables apply.
2026-09-25 20:23:20 +00:00
LeoVasanko fb2914d853 Don't accrue reading time across sleep
The inactivity cutoff timer cannot fire while the computer is asleep, and
performance.now() keeps running through sleep on some platforms, so after
a wake the whole slept span could be credited as reading time (inflating
min/visit stats). A gap longer than the inactivity timeout now closes the
span at its intended cutoff, and the cutoff timer clamps its accumulation
to its scheduled fire time — covering both event-first and timer-first
wake orderings.
2026-09-25 18:22:43 +00:00
LeoVasanko d4add54f10 Editor sockets: auth-aware reconnect, shared with the analytics view
An expired session failed the /_api/ws/editor handshake exactly like a dead
network, so the page/banner editors retried forever — the WS API cannot
open the login dialog. reconnect.js gains authReconnect(): on close, probe
/_api/settings (dialog-less fetchJson); a 401/403 runs the paskia login
flow and reconnects the moment it completes, a declined login holds at a
'session expired' ConnNote, and only transient failures take the paced
backoff. A paskia auth-success message from anywhere in the page
reconnects a waiting editor at once. AnalyticsView moves from its inline
copy of the same flow onto the shared helper.
2026-09-25 18:19:05 +00:00
LeoVasanko 31b56c3e5e Serve each article's Markdown at .md URLs (and via Accept: text/markdown)
/pages.md (front page /.md) serves the page's source Markdown — per-language
hybrid assembly, title injected as '# title' when the content has no h1 —
with the same ETag/Last-Modified/no-cache scheme as the HTML, which now
carries vary: accept. Pages advertise the counterpart via
<link rel=alternate type=text/markdown> and llms.txt links point at the
.md URLs.
2026-09-25 18:04:47 +00:00
LeoVasanko 7780d190d0 Analytics viewer: auth-aware reconnect, wake recovery, sliding time window
- Expired session dropped the socket into an endless paced retry loop the
  WS handshake can never escape: probe /_api/settings on close, run the
  paskia login flow on 401/403 and reconnect the moment it resolves; an
  aborted login leaves for the front page (history replace).
- Follow paskia's auth-success/auth-logout window messages: logins anywhere
  in the page reconnect at once instead of waiting out the backoff; logout
  leaves /_a. pagerite.js re-probes its pens on the same events.
- Recycle the socket on visibilitychange/online/bfcache restore: a socket
  that survived sleep looks OPEN but is dead, so no updates ever landed.
- The stats range window was frozen at mount (no reactive time dependency);
  it now slides on a minute clock.
- Transition map beads: fast-forward emitter schedules after long gaps
  instead of spawning a bead per missed interval in one tick.
2026-09-25 18:01:05 +00:00
LeoVasanko 204c6ab608 Transition map: always show the site structure, omit content-less index pages
Node inclusion no longer depends on views in the window: every page
from /_api/pages is shown (the front page used to vanish when quiet).
Omission follows the navigation instead — index pages without content
of their own (category labels and empty pages, the nodes nav links
bypass to the first child) are omitted, children promoted, first
visible child taking the slot. Deleted pages named by traffic are
inserted as ghosts where they would have been, materializing missing
ancestors as content-less category lanes.

/_api/pages gains the "empty" flag (not node.chunks) for the nav
notion; has_content (chunks is not None) stays for the structure
editor's delete affordance.
2026-09-25 03:23:53 +00:00
LeoVasanko 0ff5c6b526 ruff v0.8.1 2026-09-23 07:32:57 +00:00