Merge branch 'dev'
CI/CD / deploy (push) Failing after 2m8s Details

This commit is contained in:
Jeff Emmett 2026-04-15 12:09:46 -04:00
commit 2e9dfef39f
3 changed files with 120 additions and 14 deletions

View File

@ -83,14 +83,14 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="apple-touch-icon" href="/logo.png">
<title>rSpace Own Your Community Infrastructure</title>
<meta name="description" content="rSpace is a local-first platform where communities own their tools, data, and governance. ${modules.length} composable apps — from voting to budgets to maps — encrypted, interoperable, and yours.">
<title>rSpace Reclaim (you)rSpace on the Internet</title>
<meta name="description" content="rSpace is a local-first platform where groups coordinate around what they care about — without stitching together a dozen corporate apps. ${modules.length} composable tools, encrypted and yours.">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://rspace.online">
<meta property="og:title" content="rSpace — Own Your Community Infrastructure">
<meta property="og:description" content="rSpace is a local-first platform where communities own their tools, data, and governance. ${modules.length} composable apps — from voting to budgets to maps — encrypted, interoperable, and yours.">
<meta property="og:title" content="rSpace — Reclaim (you)rSpace on the Internet">
<meta property="og:description" content="rSpace is a local-first platform where groups coordinate around what they care about — without stitching together a dozen corporate apps. ${modules.length} composable tools, encrypted and yours.">
<meta property="og:image" content="https://rspace.online/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
@ -98,8 +98,8 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="rSpace — Own Your Community Infrastructure">
<meta name="twitter:description" content="Local-first community platform. ${modules.length} composable apps — voting, budgets, maps, payments, identity — encrypted and self-sovereign.">
<meta name="twitter:title" content="rSpace — Reclaim (you)rSpace on the Internet">
<meta name="twitter:description" content="One place for your group to plan, decide, fund, and build together. ${modules.length} composable tools — encrypted, local-first, yours.">
<meta name="twitter:image" content="https://rspace.online/og-image.png">
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
@ -131,10 +131,10 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<div class="lp-hero__orb lp-hero__orb--indigo" aria-hidden="true"></div>
<div class="lp-hero__grid" aria-hidden="true"></div>
<div class="lp-hero__content">
<span class="rl-tagline">Local-first community platform</span>
<span class="rl-tagline">Reclaim (you)<span style="color:#f97316">r</span><span style="color:#14b8a6">Space</span> on the internet</span>
<h1 class="lp-wordmark"><span class="lp-wordmark__r">r</span><span class="lp-wordmark__space">Space</span></h1>
<p class="lp-hero__tagline">
One platform. ${modules.length} apps. All your community&rsquo;s tools talking to each other.
Coordinate around what you care about &mdash; without stitching together a dozen corporate apps.
</p>
<div class="lp-hero__ctas">
<a href="${demoUrl}" class="lp-btn lp-btn--primary" id="ml-primary">Start your Space &rarr;</a>
@ -161,9 +161,9 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<!-- 3. Flow Stories -->
<section class="rl-section">
<div class="rl-container">
<h2 class="lp-section-heading">One Platform. Every Tool Connected.</h2>
<h2 class="lp-section-heading">Your Group. One Shared Workspace.</h2>
<p class="rl-subtext" style="text-align:center">
rApps share one sync layer. Data flows automatically &mdash; no import/export rituals.
Everything your group needs lives in one place. Data flows between tools automatically &mdash; no copy-pasting between apps.
</p>
<div class="lp-flows">
<div class="lp-flow">
@ -293,10 +293,10 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<!-- 7. Final CTA -->
<section class="lp-final-cta">
<div class="rl-container" style="max-width:720px;text-align:center">
<h2 class="lp-final-cta__heading">Your community. Your rules. Your data.</h2>
<h2 class="lp-final-cta__heading">Reclaim (you)<span style="color:#f97316;-webkit-text-fill-color:#f97316">r</span><span style="color:#14b8a6;-webkit-text-fill-color:#14b8a6">Space</span>.</h2>
<p class="rl-subtext" style="font-size:1.15rem;line-height:1.7;text-align:center">
No algorithms deciding what you see. No ads. No data harvesting.
Just tools that work for you, run by you, owned by you.
Just one place for your group to plan, decide, fund, and build together.
</p>
<div class="lp-hero__ctas">
<a href="${demoUrl}" class="lp-btn lp-btn--primary">Start your Space &rarr;</a>
@ -609,8 +609,8 @@ body {
}
.lp-wordmark__r {
font-weight: 400;
color: var(--rs-text-primary);
-webkit-text-fill-color: unset;
color: #f97316;
-webkit-text-fill-color: #f97316;
}
.lp-wordmark__space {
background: var(--rs-gradient-brand);

99
server/welcome-email.ts Normal file
View File

@ -0,0 +1,99 @@
/**
* Welcome Email sent once when a user first connects their email address.
*/
import { getSmtpTransport } from "./notification-service";
export async function sendWelcomeEmail(email: string, username: string): Promise<void> {
const transport = await getSmtpTransport();
if (!transport) {
console.warn("[welcome-email] No SMTP transport available");
return;
}
const displayName = username || "there";
const demoUrl = "https://demo.rspace.online";
const createUrl = "https://rspace.online/create";
const html = `
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 520px; margin: 0 auto; padding: 24px;">
<div style="background: #1e293b; border-radius: 12px; padding: 28px; color: #e2e8f0;">
<h1 style="margin: 0 0 4px; font-size: 22px; color: #f1f5f9;">
Welcome to <span style="color: #f97316;">r</span><span style="color: #14b8a6;">Space</span>, ${escapeHtml(displayName)}!
</h1>
<p style="margin: 0 0 24px; font-size: 15px; color: #94a3b8;">
Reclaim (you)<span style="color: #f97316;">r</span><span style="color: #14b8a6;">Space</span> on the internet &mdash; one place for your group to coordinate around what you care about.
</p>
<div style="background: #0f172a; border-radius: 8px; padding: 16px; margin-bottom: 20px;">
<p style="margin: 0 0 12px; font-size: 14px; color: #e2e8f0; line-height: 1.6;">
Instead of scattering your group across Slack, Google Docs, Trello, Zoom, Splitwise, and a dozen other apps &mdash;
<strong style="color: #14b8a6;">(you)rSpace puts it all in one shared workspace</strong> that your group actually owns.
</p>
<p style="margin: 0; font-size: 14px; color: #94a3b8; line-height: 1.6;">
Plan together. Decide together. Fund together. Build together. No corporate middlemen.
</p>
</div>
<p style="margin: 0 0 12px; font-size: 13px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">How groups use rSpace</p>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr>
<td style="padding: 8px; font-size: 14px; color: #e2e8f0; line-height: 1.5; border-bottom: 1px solid #334155;">
<strong style="color: #f1f5f9;">Plan &amp; Decide</strong><br>
<span style="color: #94a3b8; font-size: 13px;">Schedule, vote, set priorities &mdash; decisions flow into tasks automatically</span>
</td>
</tr>
<tr>
<td style="padding: 8px; font-size: 14px; color: #e2e8f0; line-height: 1.5; border-bottom: 1px solid #334155;">
<strong style="color: #f1f5f9;">Create &amp; Share</strong><br>
<span style="color: #94a3b8; font-size: 13px;">Docs, maps, data &mdash; all synced, all encrypted, all yours</span>
</td>
</tr>
<tr>
<td style="padding: 8px; font-size: 14px; color: #e2e8f0; line-height: 1.5; border-bottom: 1px solid #334155;">
<strong style="color: #f1f5f9;">Fund &amp; Sustain</strong><br>
<span style="color: #94a3b8; font-size: 13px;">Shared wallets, resource flows, transparent budgets</span>
</td>
</tr>
<tr>
<td style="padding: 8px; font-size: 14px; color: #e2e8f0; line-height: 1.5;">
<strong style="color: #f1f5f9;">Stay Connected</strong><br>
<span style="color: #94a3b8; font-size: 13px;">Chat, meet, coordinate &mdash; without giving your conversations to ad companies</span>
</td>
</tr>
</table>
<div style="background: #0f172a; border-radius: 8px; padding: 12px 16px; margin-bottom: 20px;">
<p style="margin: 0; font-size: 13px; color: #94a3b8; line-height: 1.5;">
&#x1f510; <strong style="color: #e2e8f0;">Your identity is yours.</strong>
One passkey &mdash; no passwords, no seeds, no email loops. Works everywhere, owned by nobody but you.
</p>
</div>
<div style="text-align: center;">
<a href="${demoUrl}" style="display: inline-block; padding: 10px 22px; background: linear-gradient(135deg, #14b8a6, #0d9488); color: white; text-decoration: none; border-radius: 6px; font-size: 14px; font-weight: 600; margin-right: 8px;">Explore the Demo Space</a>
<a href="${createUrl}" style="display: inline-block; padding: 10px 22px; background: transparent; color: #14b8a6; text-decoration: none; border-radius: 6px; font-size: 14px; font-weight: 600; border: 1px solid #14b8a6;">Create Your Space</a>
</div>
</div>
<p style="margin: 14px 0 0; font-size: 11px; color: #64748b; text-align: center;">
You can manage your email in profile settings at any time.
</p>
</div>`;
try {
await transport.sendMail({
from: "rSpace <hello@rspace.online>",
to: email,
subject: `Welcome to rSpace, ${displayName} — your group's new home`,
html,
replyTo: "hello@rspace.online",
});
console.log(`[welcome-email] Sent to ${email}`);
} catch (err: any) {
console.error(`[welcome-email] Failed to send to ${email}:`, err.message);
}
}
function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

View File

@ -160,6 +160,7 @@ import {
aliasExists,
} from './mailcow.js';
import { notify } from '../../server/notification-service';
import { sendWelcomeEmail } from '../../server/welcome-email';
import { startTrustEngine } from './trust-engine.js';
import { provisionSpaceAlias, syncSpaceAlias, deprovisionSpaceAlias, provisionAgentMailbox, deprovisionAgentMailbox } from './space-alias-service.js';
@ -1231,9 +1232,15 @@ app.put('/api/user/profile', async (c) => {
if (body.profileEmailIsRecovery !== undefined) updates.profileEmailIsRecovery = body.profileEmailIsRecovery;
if (body.walletAddress !== undefined) updates.walletAddress = body.walletAddress;
const oldProfile = await getUserProfile(claims.sub as string);
const profile = await updateUserProfile(claims.sub as string, updates);
if (!profile) return c.json({ error: 'User not found' }, 404);
// Send welcome email on first email connect
if (updates.profileEmail && !oldProfile?.profileEmail) {
sendWelcomeEmail(updates.profileEmail, profile.username).catch(() => {});
}
// If profile email changed and forwarding is active, update/disable the alias
if (updates.profileEmail !== undefined && isMailcowConfigured()) {
try {