Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-02 20:07:37 -08:00
commit 96b0a48436
3 changed files with 503 additions and 1 deletions

View File

@ -30,6 +30,7 @@ import {
} from "./community-store";
import type { NestPermissions, SpaceRefFilter } from "./community-store";
import { ensureDemoCommunity } from "./seed-demo";
import { seedTemplateShapes, ensureTemplateSeeding } from "./seed-template";
// Campaign demo moved to rsocials module — see modules/rsocials/campaign-data.ts
import type { SpaceVisibility } from "./community-store";
import {
@ -817,6 +818,27 @@ app.use("/:space/*", async (c, next) => {
);
});
// ── Template seeding route: /:space/:moduleId/template ──
// Triggers template seeding for empty spaces, then redirects to the module page.
app.get("/:space/:moduleId/template", async (c) => {
const space = c.req.param("space");
const moduleId = c.req.param("moduleId");
if (!space || space === "api" || space.includes(".")) return c.notFound();
try {
await loadCommunity(space);
const seeded = seedTemplateShapes(space);
if (seeded) {
broadcastAutomergeSync(space);
broadcastJsonSnapshot(space);
}
} catch (e) {
console.error(`[Template] On-demand seed failed for "${space}":`, e);
}
return c.redirect(`/${space}/${moduleId}`, 302);
});
// ── Mount module routes under /:space/:moduleId ──
// Enforce enabledModules: if a space has an explicit list, only those modules route.
// The 'rspace' (canvas) module is always allowed as the core module.
@ -1348,6 +1370,22 @@ const server = Bun.serve<WSData>({
return app.fetch(req);
}
// Template seeding: /{moduleId}/template → seed + redirect
if (pathSegments.length >= 2 && pathSegments[pathSegments.length - 1] === "template") {
try {
await loadCommunity(subdomain);
const seeded = seedTemplateShapes(subdomain);
if (seeded) {
broadcastAutomergeSync(subdomain);
broadcastJsonSnapshot(subdomain);
}
} catch (e) {
console.error(`[Template] On-demand seed failed for "${subdomain}":`, e);
}
const withoutTemplate = pathSegments.slice(0, -1).join("/");
return Response.redirect(`${url.protocol}//${url.host}/${withoutTemplate}`, 302);
}
// Normalize module ID to lowercase (rTrips → rtrips)
const normalizedPath = "/" + pathSegments.map((seg, i) =>
i === 0 ? seg.toLowerCase() : seg
@ -1698,7 +1736,9 @@ const server = Bun.serve<WSData>({
})();
ensureDemoCommunity().then(() => console.log("[Demo] Demo community ready")).catch((e) => console.error("[Demo] Failed:", e));
loadAllDocs(syncServer).catch((e) => console.error("[DocStore] Startup load failed:", e));
loadAllDocs(syncServer)
.then(() => ensureTemplateSeeding())
.catch((e) => console.error("[DocStore] Startup load failed:", e));
// Restore relay mode for encrypted spaces
(async () => {

454
server/seed-template.ts Normal file
View File

@ -0,0 +1,454 @@
/**
* Template seeder for new rSpace communities.
*
* Every non-demo space gets generic "Getting Started" content covering
* all 25+ modules so users immediately see what each rApp can do.
*
* Shape IDs use a `tmpl-` prefix so they never collide with `demo-` or
* user-created shapes. Seeding is idempotent only spaces with 0 shapes
* are touched, and the demo space is always skipped.
*/
import {
addShapes,
getDocumentData,
listCommunities,
loadCommunity,
} from "./community-store";
// ── Template Shapes ─────────────────────────────────────────────────
const TEMPLATE_SHAPES: Record<string, unknown>[] = [
// ─── rTrips: Itinerary ──────────────────────────────────────
{
id: "tmpl-itinerary",
type: "folk-itinerary",
x: 50, y: 50, width: 400, height: 300, rotation: 0,
tripTitle: "My Trip",
startDate: "",
endDate: "",
travelers: [],
items: [
{ date: "Day 1", activity: "Arrive & settle in", category: "travel" },
{ date: "Day 2", activity: "Explore the area", category: "adventure" },
{ date: "Day 3", activity: "Head home", category: "travel" },
],
},
// ─── rTrips: Destination ────────────────────────────────────
{
id: "tmpl-destination",
type: "folk-destination",
x: 500, y: 50, width: 300, height: 200, rotation: 0,
destName: "My Destination",
country: "",
lat: 0,
lng: 0,
arrivalDate: "",
departureDate: "",
notes: "Add your destination details here.",
},
// ─── rNotes: Notebook ───────────────────────────────────────
{
id: "tmpl-notebook",
type: "folk-notebook",
x: 850, y: 50, width: 300, height: 180, rotation: 0,
notebookTitle: "My Notebook",
description: "A shared notebook for your space. Add notes, ideas, and reference material.",
noteCount: 0,
collaborators: [],
},
// ─── rNotes: Welcome Note ───────────────────────────────────
{
id: "tmpl-note-welcome",
type: "folk-note",
x: 850, y: 260, width: 300, height: 250, rotation: 0,
noteTitle: "Welcome to rSpace",
content: "## Getting Started\n\nThis is your space! Here's what you can do:\n\n- **Explore** the canvas — drag, zoom, and click shapes\n- **Create** new items using the module sidebar\n- **Collaborate** in real-time with your team\n- **Customize** by rearranging or removing these starter shapes\n\nEach shape represents an rApp module. Click one to dive in!",
tags: ["getting-started"],
editor: "",
editedAt: "",
},
// ─── rNotes: Ideas Note ─────────────────────────────────────
{
id: "tmpl-note-ideas",
type: "folk-note",
x: 1200, y: 50, width: 300, height: 200, rotation: 0,
noteTitle: "Ideas & Brainstorms",
content: "## Ideas\n\nCapture your ideas here.\n\n- Idea 1\n- Idea 2\n- Idea 3\n\nUse tags to organize your notes across the space.",
tags: ["ideas"],
editor: "",
editedAt: "",
},
// ─── rVote: Poll ────────────────────────────────────────────
{
id: "tmpl-poll",
type: "demo-poll",
x: 50, y: 400, width: 350, height: 200, rotation: 0,
question: "What should we work on first?",
options: [
{ label: "Set up the space", votes: 0 },
{ label: "Invite team members", votes: 0 },
{ label: "Plan our first project", votes: 0 },
],
totalVoters: 0,
status: "active",
endsAt: "",
},
// ─── rCart: Item ────────────────────────────────────────────
{
id: "tmpl-cart-item",
type: "demo-cart-item",
x: 50, y: 630, width: 320, height: 60, rotation: 0,
name: "Example Item",
price: 0,
funded: 0,
status: "In Cart",
requestedBy: "",
store: "",
},
// ─── rFunds: Budget ─────────────────────────────────────────
{
id: "tmpl-budget",
type: "folk-budget",
x: 450, y: 400, width: 350, height: 250, rotation: 0,
budgetTitle: "Space Budget",
currency: "USD",
budgetTotal: 0,
spent: 0,
categories: [
{ name: "Operations", budget: 0, spent: 0 },
{ name: "Marketing", budget: 0, spent: 0 },
{ name: "Development", budget: 0, spent: 0 },
],
},
// ─── rFunds: Expense ────────────────────────────────────────
{
id: "tmpl-expense",
type: "demo-expense",
x: 450, y: 680, width: 320, height: 60, rotation: 0,
description: "Example expense",
amount: 0,
currency: "USD",
paidBy: "",
split: "equal",
category: "operations",
date: "",
},
// ─── rMaps: Location Marker ─────────────────────────────────
{
id: "tmpl-map-marker",
type: "demo-map-marker",
x: 500, y: 280, width: 40, height: 40, rotation: 0,
name: "Home Base",
lat: 0,
lng: 0,
emoji: "📍",
category: "home",
status: "",
},
// ─── rTokens: Mint ──────────────────────────────────────────
{
id: "tmpl-mint",
type: "folk-token-mint",
x: 1550, y: 50, width: 320, height: 280, rotation: 0,
tokenName: "Contribution Token",
tokenSymbol: "CONTRIB",
description: "Track and reward contributions to your space. Customize the name, symbol, and supply.",
totalSupply: 1000,
issuedSupply: 0,
tokenColor: "#6d28d9",
tokenIcon: "⭐",
createdBy: "",
createdAt: "",
},
// ─── rTokens: Ledger ────────────────────────────────────────
{
id: "tmpl-ledger",
type: "folk-token-ledger",
x: 1940, y: 50, width: 380, height: 400, rotation: 0,
mintId: "tmpl-mint",
entries: [],
},
// ─── rTokens: Arrow ─────────────────────────────────────────
{
id: "tmpl-arrow-tokens",
type: "folk-arrow",
x: 0, y: 0, width: 0, height: 0, rotation: 0,
sourceId: "tmpl-mint",
targetId: "tmpl-ledger",
color: "#6d28d9",
},
// ─── rFiles: File ───────────────────────────────────────────
{
id: "tmpl-file-readme",
type: "folk-file",
x: 1550, y: 500, width: 280, height: 80, rotation: 0,
fileName: "README.md",
fileSize: "0 KB",
mimeType: "text/markdown",
uploadedBy: "",
uploadedAt: "",
tags: ["getting-started"],
},
// ─── rForum: Thread ─────────────────────────────────────────
{
id: "tmpl-forum-intro",
type: "folk-forum-thread",
x: 1550, y: 620, width: 320, height: 160, rotation: 0,
threadTitle: "Introductions",
author: "",
createdAt: "",
replyCount: 0,
lastReply: "",
preview: "Introduce yourself to the space! Share who you are and what you're excited to work on.",
tags: ["introductions"],
},
// ─── rBooks: Book ───────────────────────────────────────────
{
id: "tmpl-book",
type: "folk-book",
x: 1940, y: 500, width: 280, height: 200, rotation: 0,
bookTitle: "Getting Started Guide",
author: "rSpace",
coverColor: "#3b82f6",
pageCount: 0,
currentPage: 0,
readers: [],
status: "want-to-read",
},
// ─── rPubs: Publication ─────────────────────────────────────
{
id: "tmpl-pub",
type: "folk-pub",
x: 50, y: 780, width: 300, height: 180, rotation: 0,
pubTitle: "My First Publication",
pubType: "zine",
creator: "",
format: "",
status: "draft",
copies: 0,
price: 0,
currency: "USD",
description: "A placeholder publication. Edit this to create your first zine, booklet, or print artifact.",
},
// ─── rSwag: Item ────────────────────────────────────────────
{
id: "tmpl-swag",
type: "folk-swag",
x: 400, y: 780, width: 280, height: 120, rotation: 0,
swagTitle: "Space Sticker",
swagType: "sticker",
designer: "",
sizes: [],
price: 0,
currency: "USD",
status: "draft",
orderCount: 0,
},
// ─── rProviders: Provider ───────────────────────────────────
{
id: "tmpl-provider",
type: "folk-provider",
x: 400, y: 930, width: 300, height: 160, rotation: 0,
providerName: "Local Print Shop",
location: "",
capabilities: ["print", "stickers"],
substrates: ["paper", "vinyl"],
turnaround: "",
rating: 0,
ordersFulfilled: 0,
},
// ─── rWork: Task Board ──────────────────────────────────────
{
id: "tmpl-work-board",
type: "folk-work-board",
x: 750, y: 780, width: 500, height: 280, rotation: 0,
boardTitle: "Getting Started Tasks",
columns: [
{
name: "To Do",
tasks: [
{ title: "Customize your space", assignee: "", priority: "high" },
{ title: "Invite collaborators", assignee: "", priority: "medium" },
],
},
{
name: "In Progress",
tasks: [
{ title: "Explore the modules", assignee: "", priority: "medium" },
],
},
{
name: "Done",
tasks: [],
},
],
},
// ─── rCal: Calendar ─────────────────────────────────────────
{
id: "tmpl-calendar",
type: "folk-calendar",
x: 50, y: 1000, width: 350, height: 250, rotation: 0,
calTitle: "My Calendar",
month: "",
events: [
{ date: "Today", title: "Space Created", color: "#22c55e" },
],
},
// ─── rNetwork: Graph ────────────────────────────────────────
{
id: "tmpl-network",
type: "folk-network",
x: 1300, y: 780, width: 400, height: 280, rotation: 0,
networkTitle: "My Network",
nodes: [
{ id: "me", label: "Me", role: "organizer" },
],
edges: [],
},
// ─── rTube: Video ───────────────────────────────────────────
{
id: "tmpl-video",
type: "folk-video",
x: 1750, y: 780, width: 300, height: 180, rotation: 0,
videoTitle: "Welcome Video",
duration: "",
creator: "",
uploadedAt: "",
views: 0,
thumbnail: "",
},
// ─── rInbox: Inbox ──────────────────────────────────────────
{
id: "tmpl-inbox",
type: "folk-inbox",
x: 1750, y: 990, width: 300, height: 160, rotation: 0,
inboxTitle: "Inbox",
messages: [
{ from: "rSpace", text: "Welcome to your new space! Start by exploring the canvas.", time: "" },
],
},
// ─── rData: Dashboard ───────────────────────────────────────
{
id: "tmpl-dashboard",
type: "folk-dashboard",
x: 50, y: 1280, width: 320, height: 220, rotation: 0,
dashTitle: "Space Dashboard",
metrics: [
{ label: "Members", value: "1", trend: "neutral" },
{ label: "Shapes", value: "0", trend: "neutral" },
{ label: "Tasks", value: "0/3", trend: "neutral" },
],
},
// ─── rChoices: Decision Matrix ──────────────────────────────
{
id: "tmpl-choices",
type: "folk-choice-matrix",
x: 420, y: 1280, width: 320, height: 200, rotation: 0,
choiceTitle: "Decision Matrix",
options: [
{ name: "Option A", score: 0, criteria: {} },
{ name: "Option B", score: 0, criteria: {} },
],
decidedBy: "",
status: "open",
winner: "",
},
// ─── rSplat: 3D Capture ─────────────────────────────────────
{
id: "tmpl-splat",
type: "folk-splat",
x: 790, y: 1280, width: 300, height: 160, rotation: 0,
splatTitle: "My 3D Capture",
pointCount: "0",
capturedBy: "",
capturedAt: "",
fileSize: "0 KB",
status: "pending",
},
// ─── rNotes: Packing List ───────────────────────────────────
{
id: "tmpl-packing",
type: "folk-packing-list",
x: 1140, y: 1280, width: 300, height: 300, rotation: 0,
listTitle: "My Packing List",
items: [
{ name: "Item 1", packed: false, category: "general" },
{ name: "Item 2", packed: false, category: "general" },
{ name: "Item 3", packed: false, category: "general" },
],
},
];
// ── Public API ──────────────────────────────────────────────────────
/**
* Seed template shapes into a space if it has 0 shapes.
* Skips the demo space. Returns true if shapes were added.
*/
export function seedTemplateShapes(slug: string): boolean {
if (slug === "demo") return false;
const data = getDocumentData(slug);
const shapeCount = data ? Object.keys(data.shapes || {}).length : 0;
if (shapeCount > 0) {
return false;
}
addShapes(slug, TEMPLATE_SHAPES);
console.log(`[Template] Seeded ${TEMPLATE_SHAPES.length} template shapes into "${slug}"`);
return true;
}
/**
* Iterate all existing communities on startup and seed any
* empty (0-shape) spaces with template content.
*/
export async function ensureTemplateSeeding(): Promise<void> {
const slugs = await listCommunities();
let seededCount = 0;
for (const slug of slugs) {
if (slug === "demo") continue;
try {
await loadCommunity(slug);
if (seedTemplateShapes(slug)) {
seededCount++;
}
} catch (e) {
console.error(`[Template] Failed to seed "${slug}":`, e);
}
}
if (seededCount > 0) {
console.log(`[Template] Retroactively seeded ${seededCount} space(s)`);
}
}

View File

@ -46,6 +46,7 @@ import type { EncryptIDClaims } from "@encryptid/sdk/server";
import { getAllModules, getModule } from "../shared/module";
import type { SpaceLifecycleContext } from "../shared/module";
import { syncServer } from "./sync-instance";
import { seedTemplateShapes } from "./seed-template";
// ── Unified space creation ──
@ -98,6 +99,13 @@ export async function createSpace(opts: CreateSpaceOpts): Promise<CreateSpaceRes
}
}
// Seed generic template content (non-fatal)
try {
seedTemplateShapes(slug);
} catch (e) {
console.error(`[createSpace:${source}] Template seeding failed for ${slug}:`, e);
}
console.log(`[createSpace:${source}] Created space: ${slug}`);
return { ok: true, slug, name, visibility, ownerDID };
}