rDocs: Suggestion metadata (author, type, text, status) is now persisted
to Automerge alongside comments, surviving page reloads and syncing across
clients. Accept/reject actions record final status (accepted/rejected).
EncryptID: The /join page now has "I'm new" / "I have an account" tabs so
existing users can sign in with their passkey to accept space invites,
instead of being forced to create a duplicate account.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New modules/rlending/ with moved folk-mortgage-simulator, mortgage-engine,
mortgage-types; page route at / renders the simulator
- /api/mortgage/rates + /api/mortgage/positions (GET+POST) moved to rLending;
Aave v3 live rate lookup unchanged
- FlowsDoc schema stays in rFlows (mortgagePositions + reinvestmentPositions fields);
rLending reads/writes via imported ensureFlowsDoc + flowsDocId + FlowsDoc types
- rFlows: export ensureFlowsDoc (renamed from ensureDoc); drop mortgage API routes,
/mortgage page route, mortgageScripts, seed block, subPageInfos mortgage entry
- folk-flows-app: mortgage fetches now target /rlending/api/mortgage/* via a derived
lendingBase (keeps the rPool dashboard in rFlows working unchanged)
- Display: rLending (🏦) added to module-display, rstack-app-switcher (Commerce),
rstack-tab-bar (Funding & Commerce), e2e/fixtures/module-list
- Vite: folk-mortgage-simulator build repointed to modules/rlending/ with aliases
resolving to modules/rlending/lib/mortgage-{types,engine}
New Grid / Timeline toggle on the rSocials gallery. Timeline lays out
scheduled posts on a horizontal time axis with one lane per platform
(X / LinkedIn / Instagram / Threads / Bluesky / YouTube / Newsletter).
Zoomable runway:
- Base 30 px/hr, zoom range 0.15×–4× (≈ month overview to single-hour
detail)
- Ctrl/Cmd+wheel or trackpad pinch zooms at the cursor anchor; plain
scroll-wheel pans horizontally. +/- buttons step 25%, ⊡ Fit auto-
sizes to all visible posts
- Adaptive tick density: day-only when <15 px/hr, every 12h, 6h, 3h,
or 1h as zoom increases
- Day markers always shown; hour ticks appear past the density gate
- Sticky time-axis header and sticky left gutter with platform labels
- Today marker rendered as a red vertical line + "Now" chip when the
current time is in range; 📍 Now button scrolls to it
Cards are absolute-positioned by exact scheduledAt, show platform
icon, formatted time, thread count badge, and a 2-line preview; click
still opens the same post detail overlay.
State persistence: _viewMode, _timelineZoom, and _timelineScrollLeft
all survive re-renders + reloads via localStorage. Cache bump
folk-thread-gallery to v=6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- EncryptID SMTP override in docker-compose.encryptid.yml now defaults to
internal postfix container (no auth), matching the main rspace pattern.
The external mail.rmail.online + rspace.online creds have been rejecting
auth for weeks, leaving EncryptID in [NO SMTP] mode and silently dropping
invite emails.
- New POST /api/spaces/:slug/invites/:id/revoke on rspace: admin check via
Automerge space doc, proxies to new internal EncryptID endpoint. Fixes
invitation revoke that previously 404'd.
- /api/users/search switched from prefix to substring ILIKE %q%, min query
length 1, limit up to 20, ranked exact > prefix > substring.
- Dark-mode fix: color-scheme: dark + explicit option styling on role,
scope, and input selects in the edit-space modal so dropdown text is
readable on dark backgrounds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
/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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.
- 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
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>
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>
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>
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>
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>
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.
- 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.