Commit Graph

2072 Commits

Author SHA1 Message Date
Jeff Emmett 29332a5023 feat(rsocials): threadable drafts — vertical tweet chain + [+] in overlay
Post detail overlay now renders content as a tweet chain:
- Each tweet is a spine-connected row (dot + connecting line) with
  tweet number, per-platform char counter, and inline textarea in
  edit mode
- [+ Add tweet] button at the bottom appends a new empty tweet to
  the chain; × per tweet removes it (hidden when only one tweet)
- Save writes threadPosts[] when len > 1, else plain content (and
  clears threadPosts), so single drafts stay flat
- View mode shows tweets stacked vertically reading top-to-bottom,
  edit mode keeps the same layout with editable boxes
- Per-platform limits (x/twitter 280, bluesky 300, threads 500, li
  3000, ig 2200, yt 5000) drive live char counters; over-limit
  goes red bold
- Grid cards gain a 🧵 N badge when threadPosts.length > 1
- Fix FLIP animation replaying on every re-render (now guarded with
  an instance flag, plays once per open)

Bump folk-thread-gallery cache to v=5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:15:04 -04:00
Jeff Emmett a3f3e67cdb merge(dev): rsocials post detail overlay
CI/CD / deploy (push) Successful in 2m40s Details
2026-04-18 12:24:36 -04:00
Jeff Emmett 729570c48b feat(rsocials): progressive-zoom post detail overlay with inline edit
Clicking a post card expands it into a FLIP-animated detail overlay
instead of navigating away. Overlay has two modes:

View mode — full post content, schedule, hashtags, campaign, char
count. Actions: Open in Campaign (external link), Edit.

Edit mode — textarea for content, status picker (Draft/Scheduled/
Published), datetime-local for scheduled date, space-separated
hashtag input. Saves via offline runtime changeDoc so the post list
updates live everywhere else the doc is subscribed. Hashtag strings
are auto-prefixed with # if missing. ESC cancels edit; ESC again
(or backdrop click) closes the overlay. Backdrop blur + smooth
translate+scale entry from the clicked card's rect.

Threads still use the dedicated thread-editor since they have
multi-tweet rich editing. Card anchors become buttons with
data-post-id; cursor/focus-visible styling preserved. Cache bump
folk-thread-gallery to v=4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:24:33 -04:00
Jeff Emmett c3ee221cbd merge(dev): rsocials centered status toggles
CI/CD / deploy (push) Successful in 3m0s Details
2026-04-18 11:46:10 -04:00
Jeff Emmett 076f4f170f feat(rsocials): drop sub-nav, center Drafts/Scheduled/Published toggles
Remove the top-right Campaigns / Campaign Canvas / Newsletter sub-nav
from /rsocials landing. Gallery filter chips become the primary toggle:
Drafts / Scheduled / Published only (no "All"), centered above the grid.
Default filter is drafts. Threads render alongside published posts.
Bump folk-thread-gallery cache to v=3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:46:07 -04:00
Jeff Emmett 35f122b86e merge(dev): rsocials landing gallery with status filter
CI/CD / deploy (push) Successful in 3m19s Details
2026-04-17 21:16:51 -04:00
Jeff Emmett 91837fd1d4 feat(rsocials): landing shows posts gallery with draft/scheduled/published filter
/rsocials now lands on the content gallery instead of the nav hub.
Gallery surfaces all campaign posts (not just drafts/scheduled) and
adds filter chips for All / Drafts / Scheduled / Published with live
count badges. Last-selected filter persists in localStorage. Cards use
per-status left-border accent (amber/blue/green) and a published badge.

Top-right sub-nav keeps Campaigns / Campaign Canvas / Newsletter one
click away. Bump folk-thread-gallery cache to v=2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:16:47 -04:00
Jeff Emmett 4eba2cb163 merge(dev): Phase A.5 canvas interaction parity
CI/CD / deploy (push) Successful in 2m56s Details
2026-04-17 18:19:20 -04:00
Jeff Emmett e34eec649f feat(canvas): unified rApp canvas interaction — keyboard + space-to-grab
Phase A.5: bring rApp mini-canvases to parity with the main rSpace canvas
on keyboard shortcuts and Space-to-grab behavior, plus ship reusable
primitives for fit-view and zoom chrome.

shared/canvas-interaction.ts — extend CanvasInteractionController:
  • enableSpaceToGrab — Space sets grab cursor and flips isSpaceHeld()
  • enableKeyboardShortcuts — 0 fit, +/- zoom, arrows pan, Ctrl+Z/Y
    undo/redo, Delete/Backspace delete (with shadow-DOM-aware text
    input detection so inline editors still work)
  • zoomByFactor() helper for chrome +/- buttons

shared/canvas-viewport.ts (new):
  • fitViewToRects / fitViewToNodes — standard fit algorithm matching
    the main canvas (40px padding, 0.1–1.5 clamp default)
  • persistViewport / restoreViewport — localStorage under
    rspace_viewport:<key> with finite-value validation

shared/components/rspace-canvas-chrome.ts (new):
  • <rspace-canvas-chrome> web component — zoom out/in/fit buttons +
    percent indicator + optional grid toggle. Emits canvas-zoom-*
    events. Available as drop-in for new/future canvases.

Migrate 8 rApp canvases to opt into space-to-grab + keyboard fit:
rsocials planner (keeps existing isEnabled gate), rsocials workflow,
rminders automation, rflows, rgov circuit, rnetwork CRM graph,
applet-circuit-canvas. rTime skips enableSpaceToGrab because it has
its own Space tracking integrated with node-drag state.

Bump asset cache versions so browsers pick up the new JS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 18:18:49 -04:00
Jeff Emmett 969c363fd2 Merge branch 'dev': board view + rsocials planner fixes
CI/CD / deploy (push) Successful in 3m1s Details
2026-04-17 17:53:35 -04:00
Jeff Emmett cd8878d925 feat(rsocials): board view + planner UX fixes
- Fix inline post editor: pointerdown + wheel on SVG now bail when
  target is inside .cp-inline-config so textarea/select focus and
  native scrolling work. Grow post panel to 300×460 with 380px body
  so the Done/Delete toolbar isn't clipped.
- Restyle channel palette chips as vertical tldraw-style cards (big
  tinted icon, centered label, constraint tags). 2-column grid.
- Add Board view (Drafts / Scheduled / Published) as a 4th switcher
  option. Each column toggleable (min one must stay on). Drag a post
  card between columns: Drafts→Scheduled opens a date/time picker
  modal, Scheduled→Drafts reverts status (blocked if already queued
  on Postiz). Published is terminal (no drops). Card shows platform
  badge, 3-line preview, char bar + limit, scheduled/published
  timestamp, Postiz error badge, release link, Edit button that
  jumps back into the canvas editor.

Bump planner assets: css?v=5, js?v=6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:53:28 -04:00
Jeff Emmett 82663be934 docs: rApp → applet mapping for canvas shape parity (Phase B)
Catalog of all rApp mini-canvas flow-node types proposed for promotion
to AppletDefinition so the main rSpace canvas can render them. 53
proposed applets across rsocials (planner + workflow), rminders,
rflows, rgov, rtime, rnetwork, rchoices. Review doc — no code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:17:41 -04:00
Jeff Emmett d3e65a0522 feat(canvas): shared CanvasInteractionController for rApp mini-canvases
CI/CD / deploy (push) Successful in 3m38s Details
Extract the rSpace main canvas wheel + touch interaction model into a
single shared controller (shared/canvas-interaction.ts). Wheel defaults
to pan, Ctrl/Cmd+wheel or trackpad pinch (ctrlKey) zooms at cursor,
two-finger touch pans + pinch-zooms at gesture center.

Migrate 8 rApp mini-canvases to use it, replacing 8 slightly-different
hand-rolled wheel handlers:
- rsocials campaign-planner (the bug report — was zoom-only)
- rsocials campaign-workflow
- rminders automation canvas (was zoom-only)
- rtime timebank weave
- rflows canvas
- rgov circuit
- rnetwork CRM graph (was zoom-only)
- lib/applet-circuit-canvas (was zoom-only)

Fixes the "wheel = zoom" regression everywhere and gives each rApp
canvas two-finger touch pan + pinch for free. Pointer-based pan and
marquee selection remain per-rApp because they depend on per-rApp
hit-testing. Bump asset cache versions for all affected routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:07:32 -04:00
Jeff Emmett 02889ce3ec merge(dev): campaign canvas pan/zoom fix
CI/CD / deploy (push) Successful in 3m48s Details
2026-04-17 14:37:35 -04:00
Jeff Emmett 04e38088b7 fix(rsocials): campaign canvas wheel = pan, Ctrl/pinch = zoom
Align /rsocials/campaign canvas with main rspace canvas behavior:
two-finger scroll now pans; Ctrl/Cmd+wheel or trackpad pinch
(ctrlKey) zooms at cursor. Previously any wheel event zoomed.
Sibling folk-campaign-workflow.ts already used this pattern.
Bump asset cache to v=4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:37:31 -04:00
Jeff Emmett dc87eeb91f feat(rsocials): two-way planner↔Postiz sync with drift detection
Outbound: forward per-node platformSettings + title through
createPost/createThread via new PostizIntegrationRef.settings/title
merged on top of defaultSettingsFor. Record postizSyncedHash on the
node after each successful send.

Inbound: postizReconcile pulls remote content/publishDate onto queued
and draft nodes when the local hash matches postizSyncedHash, so
Postiz-side edits surface in the planner without clobbering pending
local edits. Published posts always take remote content as final.

Drift→Resync: inline editor shows a yellow "Local edits not yet on
Postiz" row when client SHA-1 (normalized content+title+schedule+tags+
platformSettings) drifts from postizSyncedHash. New
/resync-postiz route deletes the Postiz-side post (via new deletePost
helper, 404-tolerant) then re-creates with current content. Rejected
once published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:36:56 -04:00
Jeff Emmett 0be5e15c22 merge(dev): rsocials per-platform post fields + auth headers
CI/CD / deploy (push) Successful in 3m44s Details
2026-04-17 14:16:20 -04:00
Jeff Emmett 98f554888c feat(rsocials): per-platform post fields, media uploads, auth on campaigns dashboard
Extend campaign planner with platform-aware post composition: optional
title (YouTube/TikTok/Reddit/Pinterest), mediaUrls attachments, and
per-platform settings overrides merged into Postiz __type. New
platform-specs.ts library encodes platform constraints. Bump asset
cache version to v=3.

Campaigns dashboard now sends Authorization headers on flow fetch/create
and surfaces 401/403 with a clear message instead of silently failing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:16:12 -04:00
Jeff Emmett 83671d3e35 merge(dev): redesign space invitation email
CI/CD / deploy (push) Successful in 3m40s Details
2026-04-17 13:46:52 -04:00
Jeff Emmett 08326ea834 feat(invites): redesign space invitation email with prominent CTA
Unify space invite emails across three code paths (new-user identity
invite, existing-user invite, add-by-username, plus notification-service
fallback). Template mentions the space name + rSpace, lists shared
collaboration tools, and shows a large gradient CTA button. Pass
spaceName through from server/spaces.ts so the display uses the
friendly name (e.g. "Crypto Commons") instead of the slug.

Also fix a bug in notification-service where actionUrl starting with
http would be double-prefixed with https://{slug}.rspace.online.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:46:34 -04:00
Jeff Emmett 8c99e640b8 Merge branch 'dev'
CI/CD / deploy (push) Successful in 3m45s Details
2026-04-17 13:16:36 -04:00
Jeff Emmett 7b70865a81 fix(rsplat): inline resized image as data URI for Hunyuan3D
fal.ai was returning "invalid images found in the input, failed to
read/process" because it couldn't reliably fetch the staged image URL
through Cloudflare/Traefik. Encode the resized JPEG inline so the fetch
stays inside the fal.ai request and doesn't depend on our origin being
reachable from their crawler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:16:29 -04:00
Jeff Emmett 4f987d8875 Merge branch 'dev'
CI/CD / deploy (push) Successful in 3m23s Details
2026-04-17 13:11:39 -04:00
Jeff Emmett 8b8041da9f fix(rsplat): stop double fileInput.click on mobile generate upload
Tapping inline "browse" fired both the browse handler and the bubbled
drop-area handler, causing mobile Safari to cancel and reopen the file
picker so the first selection was silently dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:11:34 -04:00
Jeff Emmett be762ba2e8 Merge branch 'dev': cross-subdomain passkey hint
CI/CD / deploy (push) Successful in 3m3s Details
2026-04-17 12:05:36 -04:00
Jeff Emmett 2ecee5b2e7 feat(auth): cross-subdomain username hint for passkey SSO
Adds a non-HttpOnly `rspace_hint` cookie scoped to .rspace.online carrying
only the public username (+ optional displayName). No JWT, session, or
credential crosses origins — WebAuthn RP ID rspace.online still drives
the actual ceremony, so moving between space subdomains requires a single
passkey tap instead of re-registering an account.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:05:07 -04:00
Jeff Emmett ca4a0b9503 Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m49s Details
2026-04-17 11:35:56 -04:00
Jeff Emmett c15462a37b harden(mailcow): detect silent API failures on write endpoints
Mailcow's JSON API returns HTTP 200 with a generic "Task completed" message
even when a write silently no-ops — e.g. wrong payload shape (array-wrapped
instead of {items, attr}), missing authsource, or any per-item ACL/validation
failure. The previous client only checked res.ok, so those failures looked
like successes and reconciler loops kept running without noticing.

parseMailcowResponse now:
- requires HTTP 2xx (unchanged)
- reads the body (array or object) and throws on type=danger/error
- requires a specific per-item success marker (alias_added, mailbox_modified,
  etc.) — the generic "Task completed" fallback indicates every item was
  silently skipped and raises a "likely silent no-op" error.

Applied to createAlias / updateAlias / deleteAlias / createMailbox /
deleteMailbox. Adds updateMailbox for password+attribute edits with the
correct {items, attr} shape and authsource gate pre-filled so new callers
can't hit the silent-swallow footgun.
2026-04-17 11:35:53 -04:00
Jeff Emmett 5528474dde Merge branch 'dev'
CI/CD / deploy (push) Successful in 3m40s Details
2026-04-17 11:24:20 -04:00
Jeff Emmett 6f8d938e1f fix(presence): keep peers online across rApp navigation in same space
- Drop beforeunload leave broadcast — was flashing users offline during
  intra-space page nav between rApps
- Heartbeat 10s → 5s; GC stale threshold 15s → 12s
- Presence payload now carries userId; overlay keys peer map by userId
  so reconnects with a new WS peerId map back to the same row
- uniquePeers() prefers entries with module/context metadata so the panel
  shows which rApp a peer is in, even alongside cursor-only awareness
2026-04-17 11:23:50 -04:00
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 97a61d6bec Merge branch 'dev'
CI/CD / deploy (push) Successful in 3m15s Details
2026-04-17 10:27:58 -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 609bae45f5 Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m59s Details
2026-04-17 10:18:01 -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 03d2a6594d Merge branch 'dev'
CI/CD / deploy (push) Has been cancelled Details
2026-04-17 10:14:57 -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 837ecf4ba0 Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m31s Details
2026-04-17 10:09: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 a99c2eb56e Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m42s Details
2026-04-17 10:05:55 -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 48c86c6c1f Merge branch 'dev'
CI/CD / deploy (push) Failing after 2m40s Details
2026-04-17 09:53:59 -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 c6ae38d0d7 Merge branch 'dev'
CI/CD / deploy (push) Failing after 2m51s Details
2026-04-17 09:37:15 -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