Commit Graph

1820 Commits

Author SHA1 Message Date
Jeff Emmett 06b84bf2d7 fix(rsocials): list-and-match fallback to recover postizPostId after create
Postiz's POST /public/v1/posts response shape doesn't reliably include the
new post's id (varies by version). When parsing fails, query listPosts in a
narrow window right after create and match by integration+content to recover
the real id. Without this, reconcile can't find queued posts back in Postiz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:08:47 -04:00
Jeff Emmett 6a335e4dba fix(rsocials): include Postiz platform-specific settings defaults
Postiz's POST /public/v1/posts validates required per-platform fields on
settings (e.g. X needs who_can_reply_post). Added defaultSettingsFor()
covering x, linkedin, instagram, youtube.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:03:21 -04:00
Jeff Emmett b2fbe7ded4 fix(rsocials): postiz sweep skips nodes with any postizStatus set
Earlier version only skipped when postizPostId was set. If a send attempt
failed (no postizPostId but postizStatus='failed'), the sweep retried every
60s and hammered Postiz's throttler. Retries now require the user to clear
postizStatus — same gate as a fresh node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:56:23 -04:00
Jeff Emmett 1cde95f3fc fix(rsocials): Postiz createPost/createThread use nested posts[] shape
Postiz's /public/v1/posts expects a nested envelope:
  { type, date, shortLink, tags, posts: [{ integration:{id}, value:[{content}], settings:{__type} }] }

The earlier shim's flat {content, integrationIds, type, scheduledAt} was
rejected with "All posts must have an integration id" (the field is named
integration, not integrationIds; it's a nested object, not an array of strings).

Also: Postiz treats PostItem.value[] as thread segments, so createThread now
sends ONE request with multiple value entries rather than N grouped posts.

Callers now pass full {id, identifier} tuples so settings.__type is set
correctly (otherwise Postiz can't route to the provider handler).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:49:18 -04:00
Jeff Emmett 878646a399 fix(rsocials): Postiz public API uses raw key + /api prefix, not Bearer
Postiz's Nest backend expects Authorization: <apiKey> (no "Bearer"
prefix) and the public API is reverse-proxied at
<hostname>/api/public/v1/*. Our client was hitting /public/v1/*
directly and using Bearer, which 401'd.

Tolerate both config.url shapes (with or without /api) so older
module settings that point at the root host keep working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:40:17 -04:00
Jeff Emmett 5c32119946 feat(rsocials): auto-push scheduled posts to Postiz + reconcile published state
Per-post "Schedule to Postiz" button on Post nodes (rSpace → Postiz), a 60s
server sweep that auto-pushes Scheduled posts within a 10min lead window,
and a Postiz → rSpace reconcile poll that flips postizStatus to
'published' (or 'failed') and records publishedAt + releaseURL.

- schemas: PostNodeData gains postizPostId, postizIntegrationId,
  postizStatus, postizError, postizSentAt, postizCheckedAt,
  postizReleaseURL, publishedAt.
- postiz-client: listPosts(startDate, endDate) for reconciliation.
- mod.ts: sendCampaignNodeToPostiz() helper, POST /api/campaign/flows/
  :flowId/nodes/:nodeId/send-postiz, postizSweep() + postizReconcile()
  wired into onInit via startPostizScheduler.
- folk-campaign-planner: button + status badge in Post inspector,
  timeline/table dot colors reflect postizStatus (queued=purple,
  published=green, failed=red). Timeline bucket stays by scheduledAt.
- campaign-planner.css: postiz state pill styles.
- Bump campaign-planner JS/CSS to ?v=2 to bust CF cache.

Mock-Postiz smoke against the client lib passes 20/20 assertions.
Live Postiz round-trip pending deploy to Netcup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:28:26 -04:00
Jeff Emmett 1471d1d578 fix(passkey): derive WebAuthn user.id deterministically to stop duplicates
Retries of /api/register/start previously generated a fresh random user.id
each time, so the authenticator (iCloud Keychain, Windows Hello, 1Password,
etc.) stored a brand-new passkey per attempt. Users who hit the failing
registration flow ended up with three or four orphan passkeys in their
password manager for every successful one.

WebAuthn spec: a create() ceremony with the same (rpId, user.id) overwrites
the existing passkey. Deriving user.id as SHA-256(salt + username) means
repeated start calls for the same username produce the same user.id and the
authenticator overwrites in place.

Salt chain: USER_ID_SALT → JWT_SECRET → fallback constant. No new env var
needed in prod — JWT_SECRET is already set.
2026-04-17 10:27:55 -04:00
Jeff Emmett 43d68fd521 fix(rsocials+spaces): merge Posts button; redact password settings from module GET
- rsocials: collapse dead "posts" outputPath into "threads" entry (💬 Posts)
  so the sub-nav shows one button linking to the thread gallery + builder
- spaces GET /:slug/modules: redact settingsSchema fields of type 'password'
  to '********' for non-owner callers; PATCH preserves existing password
  when the sentinel is sent back unchanged

Closes the leak where unauth'd GET returned Listmonk/Postiz credentials in plaintext.
2026-04-17 10:17:10 -04:00
Jeff Emmett 93a63cd42b fix(register): reject duplicate usernames before passkey prompt
Friend hit a 500 on /api/register/complete because the Postgres unique
constraint fired after the WebAuthn ceremony — they'd already burned a
passkey creation by the time the server refused. Pre-check the username in
/api/register/start so the join page shows "Username is already taken"
before the browser prompts. Also catch the 23505 duplicate-key error in
/api/register/complete as a race-condition safety net.
2026-04-17 10:14:54 -04:00
Jeff Emmett 5bb46afe6d fix(invite-claim): send publicKey + JSON error responses
The /join and OIDC accept pages were calling /api/register/complete without
the public key extracted from the WebAuthn attestation. storeCredential writes
a credentials row with a NOT NULL public_key column, so the request crashed
with an unhandled exception and Hono replied "Internal Server Error" as plain
text — the client JSON.parse choked on it.

- Extract credential.response.getPublicKey() on the client and include it in
  the completion payload (both /join and /oidc/accept flows).
- Add app.onError to return JSON 500s on /api/* routes so any future crashes
  produce parseable error bodies instead of cascading into confusing "Unexpected
  token 'I'" errors in the browser.
2026-04-17 10:09:52 -04:00
Jeff Emmett 0a896f5740 security(encryptid): gate /api/internal/* against public reach
The /api/internal/* routes (user lookups, space invites, fund claims, agent
mailbox creds) were publicly reachable via auth.rspace.online because Traefik
forwards everything under the encryptid host rule. They were designed for
service-to-service calls over the Docker network only.

Add a Hono middleware that rejects /api/internal/* with 404 when the request
arrives through the public edge (X-Forwarded-For / X-Real-IP set by Traefik)
unless it carries a valid X-Internal-Key matching INTERNAL_API_KEY. Direct
container-to-container fetches don't set forwarded headers so existing
callers keep working untouched.
2026-04-17 10:05:49 -04:00
Jeff Emmett cfe060dc61 fix: invite UX — hide disabled modules, resolve usernames, show invites list, require userId on claim
- rApp dropdown no longer shows a "Manage rApps" panel for disabled modules.
  Enable/disable lives in Edit Space → Modules only.
- /api/internal/user-email/:userId now resolves by id OR did (real did🔑z6Mk…
  or legacy synthetic did🔑<32-char id prefix>), so Edit Space members list
  shows display names instead of raw DIDs.
- /api/spaces/:slug/invites admin check now accepts ownerDID stored as either
  raw userId or did🔑. Routes the list through a new internal endpoint on
  encryptid so Automerge-owned spaces without a space_members row still work.
- /join page registration payload was missing userId, causing every invite
  claim to fail with "Missing required fields: userId, credential, username".
  Same fix applied to the OIDC accept page flow.
2026-04-17 09:53:53 -04:00
Jeff Emmett 12b5938248 fix(email): align SMTP envelope with From header for DMARC pass
Space-agent and mailbox emails set From to {slug}-agent@rspace.online but
the envelope MAIL FROM was noreply@rmail.online. Rspamd DKIM-signs with
the envelope domain (rmail.online), which fails DMARC alignment against
the rspace.online From header — Gmail was quarantining invites.

Set envelope.from to match the From header so rspamd signs with
rspace.online's DKIM key and SPF/DKIM align. Verified via test send:
DKIM_SIGNED{rspace.online:s=dkim}, FROM_EQ_ENVFROM, no DMARC:Quarantine.

Covers: space invite emails (identity + existing-user paths), agent
notifications, rinbox MI agent blasts, mailbox approval sends.
2026-04-17 09:36:36 -04:00
Jeff Emmett 97141560ef chore(backlog): follow-ups from 2026-04-16 deployment session
5 new backlog tasks covering loose ends from tonight's rTasks canvas +
Sablier sidecar migration + infra cleanup work:

- MEDIUM.8: create 5 sidecar containers on Netcup
- MEDIUM.9: wire ollama into sidecar lifecycle (currently no-op)
- MEDIUM.10: roll canvas-with-widgets UX to remaining rApps
- LOW.1: Netcup memory pressure audit
- LOW.2: version-control deploy of /opt/scripts/
- LOW.3: Sablier-for-encryptid (retargeted/deferred)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 19:18:35 -04:00
Jeff Emmett dcd6e1a261 chore(infra): drop /var/run/docker.sock mount from rspace container
No longer needed — sidecar lifecycle is delegated to Sablier via HTTP API
(see ee251fd). Removes an unnecessary Docker socket exposure from the
main app container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 19:08:33 -04:00
Jeff Emmett 03e96c215a chore(backlog): TASK-MEDIUM.7 — Sablier sidecar migration (Done)
Completion notes: rewritten sidecar-manager delegates to Sablier HTTP API,
sablier joined rspace-internal network, deployed image 000ee0da verified
on demo.rspace.online.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 19:00:29 -04:00
Jeff Emmett ee251fd621 refactor(sidecar): delegate lifecycle to Sablier instead of Docker socket
Replaces the custom Docker Engine API implementation in sidecar-manager.ts
with HTTP calls to Sablier's blocking strategy endpoint. Sablier owns the
Docker socket, handles start + readiness + session-TTL idle stop.

- Drops ~80 lines of Docker API plumbing and the idle-watcher interval
- Public API (ensureSidecar/markSidecarUsed/isSidecarRunning/startIdleWatcher)
  unchanged — callers in server/index.ts untouched
- SABLIER_URL defaults to http://sablier:10000 (reachable once sablier is
  attached to rspace-online_rspace-internal; dev-ops change separate)
- SIDECAR_SESSION_DURATION env (default 5m) matches previous idle timeout
- Graceful no-op when Sablier unreachable (local dev)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 18:49:52 -04:00
Jeff Emmett 7ada95b46a fix(infra): sablier-support must be no-op until traefik wiring is ready
Copying the sablier-encryptid.yml labels would immediately apply
traefik.enable=false to the live encryptid container, breaking auth
before Sablier has a middleware route in place. Reduced to empty
services:{} with a comment explaining what's needed to activate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 18:42:44 -04:00
Jeff Emmett 4f2aebe46e chore(infra): add sablier-support compose override referenced in .env
.env on Netcup sets COMPOSE_FILE=docker-compose.yml:docker-compose.sablier-support.yml
but the file was never created, causing docker compose commands to fail
without explicit -f flag. Adds the missing override with Sablier labels for
encryptid services (matching the existing sablier-encryptid.yml pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 18:42:09 -04:00
Jeff Emmett 7b6d1d60f9 chore(rschedule): bump JS cache version to v=2 after Phase B-H build
Phase B's full booking UI + admin dashboard + cancel page bundles are
now built. Bumping cache version forces Cloudflare + browser to fetch
the new code instead of the Phase A stub.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 18:31:33 -04:00
Jeff Emmett 69ce497aa8 feat(rschedule): complete native port of schedule-jeffemmett
Phases D-H of the rSchedule booking module:

- **Google Calendar sync** (`lib/gcal-sync.ts`): reuses rspace OAuth, syncs busy
  times into config doc, creates booking events, deletes on cancel.
- **Admin UI** (`components/folk-schedule-admin.ts`): 6-tab passkey-gated dashboard
  (overview, availability rules + overrides, bookings, invitations, gcal, settings).
  Timezone-shift banner when browser tz diverges from host tz.
- **Emails** (`lib/emails.ts`, `lib/calendar-links.ts`): confirmation with
  Google/Outlook/Yahoo add-to-calendar buttons + .ics attachment; cancellation
  with 3 suggested slots from availability engine; 24h reminder.
- **Cron** (`lib/cron.ts`): in-process 5-min reminder sweep + 10-min gcal sweep
  per connected space, started from onInit.
- **Invitations + timezone shift** (mod.ts, admin UI): PATCH
  /api/invitations/:id accept/decline; POST /api/admin/timezone/shift with
  optional booking relabel; invitations tab shows cross-space invites.

Full public booking flow (`components/folk-schedule-booking.ts`): month calendar,
date → slot drill, booking form, confirmation view, timezone picker.

EncryptID passkey gates admin routes via resolveCallerRole ≥ moderator.
Per-entity model: each space (and user-space) hosts its own bookable page;
bookings mirror into invitee spaces' :invitations docs so cross-space visibility
works without cross-space reads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 18:19:10 -04:00
Jeff Emmett 700e372260 fix(rschedule): add missing lib/availability + component updates
lib/availability.ts (getAvailableSlots) was imported by mod.ts but never
committed — caused module load to fail on server, leaving renderShell
callers with undefined 'modules' and producing 500s on every rschedule
route. Plus small updates to booking/admin/cancel components and mod.ts
wiring.
2026-04-16 17:35:05 -04:00
Jeff Emmett f45cb753e1 feat(rschedule,rtasks): wire Calendly-style rSchedule + rTasks canvas port
rSchedule (Calendly-style public booking module):
- modules/rschedule/mod.ts — routes, docSchemas (config/bookings/invitations),
  standaloneDomain rschedule.online, landing, feeds, outputPaths
- components/folk-schedule-booking.ts — public booking page
- components/folk-schedule-admin.ts — admin availability/bookings/gcal
- components/folk-schedule-cancel.ts — guest self-cancel page
- booking.css, landing.ts (marketing)
- Registered via registerModule(scheduleModule) in server/index.ts
- Component bundles declared in vite.config.ts

rTasks canvas port (uses new shared folk-app-canvas/folk-widget):
- components/folk-tasks-canvas.ts, folk-tasks-activity.ts, folk-tasks-backlog.ts
- modules/rtasks/mod.ts now renders <folk-app-canvas app-id=rtasks> shell

Plus minor rnetwork delegation-manager / power-indices tweaks.
2026-04-16 17:29:42 -04:00
Jeff Emmett 8042e86815 wip(rschedule): schemas + folk-app-canvas/folk-widget shared components
Schema for the new Calendly-style rSchedule port plus two new shared
canvas components (folk-app-canvas, folk-widget) that replace per-rApp
tab-based nav with a unified widget-on-canvas surface.
2026-04-16 17:26:45 -04:00
Jeff Emmett 3a614e2866 rename: finish rschedule → rminders migration
Complete the rename started in dda7760 (which removed rschedule/ but
left callers unmigrated and the rminders/ dir uncommitted). Updates
vite.config.ts build entries, API base fetches in folk-comment-pin,
folk-rapp widget map, module-display meta, calendar reminder-drop
route, docs comment-panel, e2e fixtures, shell/landing/mcp-server
references, and backlog/ONTOLOGY docs.

Fixes vite build failure: "Could not resolve entry module
modules/rschedule/components/folk-schedule-app.ts".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 17:18:17 -04:00
Jeff Emmett 312ea4b535 fix(build): commit rminders module dir (orphaned during rschedule rename)
Files existed on disk and were referenced by vite.config.ts entry
config, but were never git-added when the rschedule→rminders rename
happened in dda7760. Build on a fresh clone failed with "Could not
resolve entry module modules/rschedule/components/folk-schedule-app.ts"
because vite was picking up the stale pre-rename config on disk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 17:16:15 -04:00
Jeff Emmett df7d0b021f mobile: touch + pen compatibility pass across rApps
Global shell.css: @media (pointer:coarse) baseline — inputs ≥16px
(no iOS zoom), buttons/role-buttons ≥44×44, touch-action: manipulation,
transparent tap highlight. New utility classes: .rapp-hscroll and
.rapp-drawer for bottom-sheet patterns.

folk-drawfast + folk-makereal: branch on pointerType, honor real pen
pressure (constant 0.5 for mouse/touch), single-pointer capture, palm
rejection (touch ignored while pen is down), pointercancel cleanup.

rNotes: mobile drill-down layout below 768px — three panels collapse
to one full-width view per selection stage (vaults → files → preview)
with back buttons. 16px fonts, 44px touch targets.

rSheets: sticky row/col headers, min-width max-content table, visible
scroll thumb, 72px min cell width on coarse pointers.

rMaps: bottom-sheet handle touchstart/touchend → unified PointerEvents
so pen users get drag-to-expand. Pointer capture + horizontal-swipe
reject.

rPhotos lightbox: pinch-zoom (2-pointer), pan when zoomed, horizontal
swipe between photos, double-tap to toggle zoom, prev/next buttons on
desktop (swipe-only on mobile).

Bump cache versions: folk-notes-app v=10→11, folk-map-viewer v=7→8,
folk-photo-gallery v=3→4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 17:13:35 -04:00
Jeff Emmett eee89f9f32 fix(rpast): always https for non-loopback hosts
Traefik → container hop in the rSpace stack sets x-forwarded-proto: http
even when the real client request is https. That was leaking into the
"Open in <rApp>" links emitted in chronicle.mw. Simplify: any non-local
host gets https, period.
2026-04-16 17:06:25 -04:00
Jeff Emmett dda7760433 infra(traefik): scope rate limit per CF-Connecting-IP, raise to 600/150
Previous limits (avg 120/min, burst 30) had no sourceCriterion. Traefik
default groups by request Host, so ALL users of rspace.online shared a
single 120/min bucket — tripped almost immediately under normal load
(repeated 429s in rpast, api/mi/models, etc).

Scope per Cloudflare client IP (CF-Connecting-IP header) and raise to
600/min average with 150 burst — interactive use can spike above 120/min
from one client easily (module loads + polling + autosave).
2026-04-16 17:04:15 -04:00
Jeff Emmett 68648608a9 test(rsocials): Playwright smoke suite + planner reliability fixes
Adds e2e/tests/rsocials-campaign-flow.spec.ts — 13 tests covering the
unified campaign flow UX: dashboard → planner navigation, brief canvas
node (+ preview banner), markdown import modal, wizard handoff, and
API shape. 36 passed / 3 AI-skipped across chromium/firefox/mobile.

Bug fixes uncovered by the suite:
- markDownstreamStale only redraws when a node actually flips stale,
  so typing in an input node no longer destroys the open inline-edit
  overlay.
- executeSave wraps the local-first write in try/catch and nulls the
  client on failure, so a half-initialised client (WS down, IDB
  unavailable) falls through to localStorage instead of throwing
  "Document not open".
- init-failure path also nulls the client so the first save after a
  failed subscribe doesn't hit a doc that was never opened.

Test infra:
- server/security.ts + server/index.ts honour DISABLE_RATE_LIMIT=1
  (and NODE_ENV=test) to bypass HTTP rate limiter and anon WS-per-IP
  cap so the suite can run under 8 parallel workers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 16:59:59 -04:00
Jeff Emmett 317e7a5579 fix(deploy): track registry image + default rpast links to https
- docker-compose.yml: rspace service now uses
  localhost:3000/jeffemmett/rspace-online:${IMAGE_TAG:-latest} so
  CI's `docker pull ... && docker compose up -d --no-build` actually
  picks up the new image without a manual retag step.
- modules/rpast/mod.ts: base URL defaults to https for any non-loopback
  host, honoring x-forwarded-proto only when set to http|https. Fixes
  "Open in <rApp>" links previously emitted as http:// on CF/Traefik.
2026-04-16 16:58:01 -04:00
Jeff Emmett 595188b9d8 chore(sw): bump cache version to v10 to purge stale rpast 404 shells
Users on an older SW built before rpast existed saw a cached
/rpast shell with an outdated module list (rmail/rfunds/rwork/...).
Bumping the cache version forces activation-time cleanup to drop
the old entries on next visit.
2026-04-16 16:36:54 -04:00
Jeff Emmett 98d6d22e55 ui(space-settings): Invite label + breathing room under username search
- Rename 'Add' to 'Invite' in the By-Username add-member row to match
  the By-Email path and reflect that it goes through an invite flow.
- Add 10px top margin to .add-member-row so the role dropdown and
  button don't visually collide with the search input/selected-user
  area above.
2026-04-16 16:36:52 -04:00
Jeff Emmett 351565c934 fix(space-settings): refresh invitations tab after sending invite
Email-invite path previously only showed 'Invite sent' without
refreshing the list. Username-invite path reloaded members but not
invitations. Both now call #loadInvitations after a successful POST.
2026-04-16 16:34:05 -04:00
Jeff Emmett f388812927 fix(spaces): show username instead of truncated DID in members list
GET /api/spaces/:slug/members now enriches entries missing displayName
by looking up the username from EncryptID (/api/internal/user-email).
Old member records (particularly owners created before displayName was
passed to setMember) were showing as 'jAV6y4tg8UbKJEkN0npv...' in the
space settings Members tab.
2026-04-16 16:29:32 -04:00
Jeff Emmett 14ffee4101 fix(rpast): serve interactive viewer at space root
Adds `GET /` to rpast routes. Without it, /rpast on a space subdomain
404'd because the sub-app had no root handler. Now matches the rNotes
pattern: bare domain → marketing landing, in-space URL → app shell with
<rpast-viewer>.
2026-04-16 16:21:19 -04:00
Jeff Emmett d950e1a656 chore(backlog): add DAO power-indices follow-up tasks (144-150)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 16:13:29 -04:00
Jeff Emmett 5e5fcd7681 feat(rsocials): campaign planner polish
Expanded folk-campaign-planner with richer live state, styled dashboard
refresh, and schema + mod updates to match the new planner flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 16:13:20 -04:00
Jeff Emmett 3a69234819 feat(rpast): chronicle-of-self timeline + markwhen projection
- New rpast module renders a cross-rApp personal timeline
- shared/markwhen/ projection layer hydrates from syncServer docs
- rCal gets a Timeline applet wiring into the same markwhen view
- rstack-markwhen-view component for embedding elsewhere
- Smoke-test fixtures under output/ and scripts/smoke-rpast.ts
- Adds @markwhen/{parser,timeline,calendar,mw} deps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 16:13:11 -04:00
Jeff Emmett 3d11acd48b chore(sw): bump cache version to v9 to purge stale HTML fragments
Stale cached HTML fragments referenced old asset hashes (e.g. 404 on
canvas-PlYnCtxh.js while server has canvas-R4rXE5Sc.js) after frequent
rebuilds. Bumping CACHE_VERSION and the SW registration query string
forces the new SW to install and purge all old versioned caches on
activate.
2026-04-16 16:11:29 -04:00
Jeff Emmett e3f322e7ff fix(rmeets): use subdomain-aware URLs everywhere
All internal rMeets links were hardcoded to /{space}/rmeets, so clicking
Quick Meet on https://demo.rspace.online/rmeets landed on
https://demo.rspace.online/demo/rmeets/<id> — a banned path-with-subdomain
URL. Added rmeetsBase(c) that returns /rmeets on {space}.rspace.online and
/{space}/rmeets elsewhere; swept all five `base` sites plus the inline
meeting page's meetsBase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 15:55:27 -04:00
Jeff Emmett dd608f831c tweak(mi-voice): quiet feminine — Emma, volume 0.25, neutral pitch
User preference: quiet + feminine. Swap Andrew (male) for Emma
(calmer, softer female than Aria). Drop volume 0.3 -> 0.25, reset
pitch to natural (deeper shift made it sound male). Rate stays slow.
2026-04-16 15:51:31 -04:00
Jeff Emmett 07b4714f48 fix(rpubs): canonical subdomain URLs for published pages
Published hosted_url, pdf_url, epub_url, and all links in the reader
page now use {space}.rspace.online/rpubs/... (subdomain form) instead
of path-scoped rspace.online/{space}/rpubs/... — matching the site
convention that space slugs always appear as subdomains.

The server already rewrites subdomain → path-scope internally for
routing, so Hono route mounts stay the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 15:51:28 -04:00
Jeff Emmett d55b9f74c0 tweak(mi-voice): brief but well-formed, not telegraphic
Prior voice-mode prompt leaned too far into caveman/telegraphic speech
('drop articles'). User wants brevity with proper grammar — short
complete sentences, not neanderthal fragments.
2026-04-16 15:48:48 -04:00
Jeff Emmett 7cbf96de8b feat(rmeets): live captions + MI internal-key auth
- Add Web Speech API captions overlay in the default Jitsi meeting view,
  broadcasting final/interim transcripts over Jitsi's data channel so each
  participant sees everyone's speech in real time.
- Toggle via the MI FAB dropdown; degrades gracefully where SR is unsupported.
- miApiFetch + /api/mi-proxy now forward X-MI-Internal-Key so the Meeting
  Intelligence recordings/search/meetings lists resolve from the backend
  without per-user tokens.
- docker-compose exposes MEETING_INTELLIGENCE_API_URL, MI_INTERNAL_KEY,
  JITSI_URL to the rspace container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 15:46:30 -04:00
Jeff Emmett 39fbd99897 feat(mi-voice): quieter+smoother voice, caveman terseness in voice mode
- Default voice en-US-AvaMultilingualNeural -> en-US-AndrewMultilingualNeural
  (smoother, less feminine timbre).
- Volume 0.55 -> 0.3, rate -8% -> -10%, pitch -2Hz -> -6Hz.
- Browser fallback matches: pitch 0.85, volume 0.3.
- Client passes voiceMode flag to /api/mi/ask; server appends a VOICE
  MODE section to the system prompt demanding ≤1-2 short sentences,
  no lists/markdown/emoji/preamble — because listening to long replies
  is tedious.
2026-04-16 15:44:11 -04:00
Jeff Emmett 71782b1cf1 fix(mi-voice): await AudioContext.resume before source.start
Suspended contexts silently queued audio that never played. Await resume,
split connect() chain to avoid undefined-return on older browsers, and
re-check state after source setup in case first resume lost the gesture.
2026-04-16 15:30:16 -04:00
Jeff Emmett 858711e783 feat(rpubs): EPUB export + Publish-to-Space hosted URLs
- epub-gen.ts: reflowable (markdown → styled xhtml) and fixed-layout
  (Typst per-page PNGs wrapped as pre-paginated EPUB3). No new deps.
- typst-compile.ts: compileDocumentToPages() rasterizes Typst directly
  to PNG via the CLI (no poppler/mupdf needed).
- Persistent publications store at /data/rpubs-publications/{space}/{slug}
  with public reader page, PDF + EPUB downloads at /{space}/rpubs/publications/{slug}.
- Preview/Press step now has quick EPUB download buttons next to PDF.
- Publish panel: "Publish to {space}" is now the primary action, showing
  hosted URL + copy-link after publishing. EPUB variants remain in downloads.
- Dockerfile: new PUBS_DIR volume for persistence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 15:23:49 -04:00
Jeff Emmett 1f084fa674 fix(mi-voice): big-endian header parse, apply gain, softer voice defaults
- Parse WS frame header length as big-endian (server uses struct.pack('>I')).
  Previous little-endian read always failed, silently forcing the browser
  Web Speech fallback.
- Apply header volume via GainNode so quieter actually plays quieter.
- Default voice en-US-AriaNeural -> en-US-AvaMultilingualNeural, with
  rate -8% / pitch -2Hz / volume 0.55 for a calmer, less grating output.
  Browser fallback gets matching rate/pitch/volume.
2026-04-16 15:18:40 -04:00
Jeff Emmett 9daeb60895 fix(collab): dedup cursors by username + same-page-only visibility
- Deduplicate cursor rendering using #uniquePeers() (was showing
  multiple cursors per user from different tabs/sessions)
- Strict same-page filtering: cursors only visible when peers share
  the same effectiveViewId (viewId ?? moduleId)
- Users on different rApps no longer see each other's cursors
- Applied same fixes to focus rings and panel "different view" badge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:00:11 -04:00