From 1635b087041c7740bb9f43ec31eb50c454543b8c Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 2 Mar 2026 22:49:21 -0800 Subject: [PATCH] feat: add seed template data for rCart, rChoices, rFiles, rForum, rFunds, rInbox, rSplat Each module now seeds starter content when a new space is created, giving users something to interact with immediately rather than an empty state. Co-Authored-By: Claude Opus 4.6 --- modules/rcart/mod.ts | 46 ++++++++++++++++++++++++++++ modules/rchoices/mod.ts | 60 +++++++++++++++++++++++++++++++++++- modules/rfiles/mod.ts | 39 ++++++++++++++++++++++++ modules/rforum/mod.ts | 26 ++++++++++++++++ modules/rfunds/mod.ts | 24 +++++++++++++++ modules/rinbox/mod.ts | 67 +++++++++++++++++++++++++++++++++++++++++ modules/rsplat/mod.ts | 43 ++++++++++++++++++++++++++ 7 files changed, 304 insertions(+), 1 deletion(-) diff --git a/modules/rcart/mod.ts b/modules/rcart/mod.ts index 4fe0f6f..027095d 100644 --- a/modules/rcart/mod.ts +++ b/modules/rcart/mod.ts @@ -620,6 +620,51 @@ routes.get("/", (c) => { })); }); +// ── Seed template data ── + +function seedTemplateCart(space: string) { + if (!_syncServer) return; + const doc = ensureCatalogDoc(space); + if (Object.keys(doc.items).length > 0) return; + + const docId = catalogDocId(space); + const now = Date.now(); + + const items: Array<{ title: string; type: string; caps: string[]; subs: string[]; tags: string[] }> = [ + { + title: 'Commons Community Sticker Pack', type: 'sticker', + caps: ['laser-print'], subs: ['vinyl-matte'], + tags: ['merch', 'stickers'], + }, + { + title: 'Cosmolocal Network Poster (A2)', type: 'poster', + caps: ['risograph'], subs: ['paper-heavyweight'], + tags: ['merch', 'poster', 'cosmolocal'], + }, + { + title: 'rSpace Contributor Tee', type: 'apparel', + caps: ['screen-print'], subs: ['cotton-organic'], + tags: ['merch', 'apparel'], + }, + ]; + + _syncServer.changeDoc(docId, 'seed template catalog', (d) => { + for (const item of items) { + const id = crypto.randomUUID(); + d.items[id] = { + id, artifactId: crypto.randomUUID(), artifact: null, + title: item.title, productType: item.type, + requiredCapabilities: item.caps, substrates: item.subs, + creatorId: 'did:demo:seed', sourceSpace: space, + tags: item.tags, status: 'active', + createdAt: now, updatedAt: now, + }; + } + }); + + console.log(`[Cart] Template seeded for "${space}": 3 catalog entries`); +} + export const cartModule: RSpaceModule = { id: "rcart", name: "rCart", @@ -633,6 +678,7 @@ export const cartModule: RSpaceModule = { routes, standaloneDomain: "rcart.online", landingPage: renderLanding, + seedTemplate: seedTemplateCart, async onInit(ctx) { _syncServer = ctx.syncServer; }, diff --git a/modules/rchoices/mod.ts b/modules/rchoices/mod.ts index acb4563..45c6a29 100644 --- a/modules/rchoices/mod.ts +++ b/modules/rchoices/mod.ts @@ -13,7 +13,7 @@ import { renderShell } from "../../server/shell"; import type { RSpaceModule } from "../../shared/module"; import { renderLanding } from "./landing"; import { getModuleInfoList } from "../../shared/module"; -import { getDocumentData } from "../../server/community-store"; +import { getDocumentData, addShapes } from "../../server/community-store"; const routes = new Hono(); @@ -61,6 +61,63 @@ routes.get("/", (c) => { })); }); +// ── Seed template data ── + +function seedTemplateChoices(space: string) { + // Check if space already has choice shapes + const docData = getDocumentData(space); + const choiceTypes = ["folk-choice-vote", "folk-choice-rank", "folk-choice-spider"]; + if (docData?.shapes) { + const existing = Object.values(docData.shapes as Record) + .filter((s: any) => !s.forgotten && choiceTypes.includes(s.type)); + if (existing.length > 0) return; + } + + const now = Date.now(); + const shapes: Record[] = [ + { + id: `tmpl-choice-vote-${now}`, type: 'folk-choice-vote', + x: 50, y: 1800, width: 420, height: 360, rotation: 0, + title: 'Governance Priority Vote', + mode: 'plurality', + options: [ + { id: crypto.randomUUID(), label: 'Infrastructure improvements', color: '#3b82f6' }, + { id: crypto.randomUUID(), label: 'Community education programs', color: '#8b5cf6' }, + { id: crypto.randomUUID(), label: 'Open-source tooling grants', color: '#10b981' }, + ], + votes: [], createdAt: now, + }, + { + id: `tmpl-choice-rank-${now}`, type: 'folk-choice-rank', + x: 520, y: 1800, width: 420, height: 360, rotation: 0, + title: 'Sprint Priority Ranking', + options: [ + { id: crypto.randomUUID(), label: 'Dark mode across all modules' }, + { id: crypto.randomUUID(), label: 'Mobile-responsive layouts' }, + { id: crypto.randomUUID(), label: 'Offline-first sync' }, + { id: crypto.randomUUID(), label: 'Notification system' }, + ], + rankings: [], createdAt: now, + }, + { + id: `tmpl-choice-spider-${now}`, type: 'folk-choice-spider', + x: 990, y: 1800, width: 420, height: 360, rotation: 0, + title: 'Team Skills Assessment', + options: [ + { id: crypto.randomUUID(), label: 'Frontend' }, + { id: crypto.randomUUID(), label: 'Backend' }, + { id: crypto.randomUUID(), label: 'Design' }, + { id: crypto.randomUUID(), label: 'DevOps' }, + { id: crypto.randomUUID(), label: 'Community' }, + ], + scores: [], createdAt: now, + }, + ]; + + addShapes(space, shapes); + console.log(`[Choices] Template seeded for "${space}": 3 choice shapes`); +} + export const choicesModule: RSpaceModule = { id: "rchoices", name: "rChoices", @@ -70,6 +127,7 @@ export const choicesModule: RSpaceModule = { routes, standaloneDomain: "rchoices.online", landingPage: renderLanding, + seedTemplate: seedTemplateChoices, feeds: [ { id: "poll-results", diff --git a/modules/rfiles/mod.ts b/modules/rfiles/mod.ts index bbd6086..92c2d17 100644 --- a/modules/rfiles/mod.ts +++ b/modules/rfiles/mod.ts @@ -630,6 +630,44 @@ routes.get("/", (c) => { })); }); +// ── Seed template data ── + +function seedTemplateFiles(space: string) { + if (!_syncServer) return; + const doc = ensureDoc(space, 'default'); + if (Object.keys(doc.files).length > 0 || Object.keys(doc.memoryCards).length > 0) return; + + const docId = filesDocId(space, 'default'); + const now = Date.now(); + + _syncServer.changeDoc(docId, 'seed template files', (d) => { + const files: Array<{ name: string; title: string; mime: string; size: number; tags: string[] }> = [ + { name: 'project-charter.pdf', title: 'Project Charter', mime: 'application/pdf', size: 245000, tags: ['governance', 'founding'] }, + { name: 'logo.svg', title: 'Community Logo', mime: 'image/svg+xml', size: 12400, tags: ['branding', 'design'] }, + { name: 'budget-2026.xlsx', title: 'Budget 2026', mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 89000, tags: ['finance', 'planning'] }, + ]; + for (const f of files) { + const id = crypto.randomUUID(); + d.files[id] = { + id, originalFilename: f.name, title: f.title, description: '', + mimeType: f.mime, fileSize: f.size, fileHash: null, storagePath: '', + tags: f.tags, isProcessed: true, processingError: null, + uploadedBy: 'did:demo:seed', sharedSpace: 'default', + createdAt: now, updatedAt: now, + }; + } + const mcId = crypto.randomUUID(); + d.memoryCards[mcId] = { + id: mcId, sharedSpace: 'default', title: 'Welcome', + body: 'This is your shared file space. Upload documents, images, and other files to collaborate with your community.', + cardType: 'note', tags: ['onboarding'], position: 0, + createdBy: 'did:demo:seed', createdAt: now, updatedAt: now, + }; + }); + + console.log(`[Files] Template seeded for "${space}": 3 files, 1 memory card`); +} + export const filesModule: RSpaceModule = { id: "rfiles", name: "rFiles", @@ -639,6 +677,7 @@ export const filesModule: RSpaceModule = { docSchemas: [{ pattern: '{space}:files:cards:{sharedSpace}', description: 'Files and memory cards', init: filesSchema.init }], routes, landingPage: renderLanding, + seedTemplate: seedTemplateFiles, async onInit(ctx) { _syncServer = ctx.syncServer; }, diff --git a/modules/rforum/mod.ts b/modules/rforum/mod.ts index 409a281..5533212 100644 --- a/modules/rforum/mod.ts +++ b/modules/rforum/mod.ts @@ -199,6 +199,31 @@ routes.get("/", (c) => { })); }); +// ── Seed template data ── + +function seedTemplateForum(_space: string) { + if (!_syncServer) return; + const doc = ensureDoc(); + if (Object.keys(doc.instances).length > 0) return; + + const now = Date.now(); + const instanceId = crypto.randomUUID(); + + _syncServer.changeDoc(FORUM_DOC_ID, 'seed template forum instance', (d) => { + d.instances[instanceId] = { + id: instanceId, userId: 'did:demo:seed', name: 'Commons Forum', + domain: 'commons.rforum.online', status: 'active', errorMessage: '', + discourseVersion: '3.2.0', provider: 'hetzner', vpsId: 'demo-vps', + vpsIp: '0.0.0.0', region: 'eu-central', size: 'cx21', + adminEmail: 'admin@commons.rforum.online', smtpConfig: {}, + dnsRecordId: '', sslProvisioned: true, + createdAt: now, updatedAt: now, provisionedAt: now, destroyedAt: 0, + }; + }); + + console.log(`[Forum] Template seeded: 1 demo instance`); +} + export const forumModule: RSpaceModule = { id: "rforum", name: "rForum", @@ -208,6 +233,7 @@ export const forumModule: RSpaceModule = { docSchemas: [{ pattern: 'global:forum:instances', description: 'Forum provisioning metadata', init: forumSchema.init }], routes, landingPage: renderLanding, + seedTemplate: seedTemplateForum, standaloneDomain: "rforum.online", externalApp: { url: "https://commons.rforum.online", name: "Discourse" }, feeds: [ diff --git a/modules/rfunds/mod.ts b/modules/rfunds/mod.ts index c019857..8d3622a 100644 --- a/modules/rfunds/mod.ts +++ b/modules/rfunds/mod.ts @@ -313,6 +313,29 @@ routes.get("/flow/:flowId", (c) => { })); }); +// ── Seed template data ── + +function seedTemplateFunds(space: string) { + if (!_syncServer) return; + const doc = ensureDoc(space); + if (Object.keys(doc.spaceFlows).length > 0) return; + + const docId = fundsDocId(space); + const now = Date.now(); + const flowId = crypto.randomUUID(); + + // Create a SpaceFlow entry pointing to "demo" — the frontend + // already renders demoNodes from presets.ts in demo mode. + _syncServer.changeDoc(docId, 'seed template flow', (d) => { + d.spaceFlows[flowId] = { + id: flowId, spaceSlug: space, flowId: 'demo', + addedBy: 'did:demo:seed', createdAt: now, + }; + }); + + console.log(`[Funds] Template seeded for "${space}": 1 demo flow association`); +} + export const fundsModule: RSpaceModule = { id: "rfunds", name: "rFunds", @@ -322,6 +345,7 @@ export const fundsModule: RSpaceModule = { docSchemas: [{ pattern: '{space}:funds:flows', description: 'Space flow associations', init: fundsSchema.init }], routes, landingPage: renderLanding, + seedTemplate: seedTemplateFunds, async onInit(ctx) { _syncServer = ctx.syncServer; }, diff --git a/modules/rinbox/mod.ts b/modules/rinbox/mod.ts index 88218af..71bf9d2 100644 --- a/modules/rinbox/mod.ts +++ b/modules/rinbox/mod.ts @@ -984,6 +984,72 @@ routes.get("/", (c) => { })); }); +// ── Seed template data ── + +function seedTemplateInbox(space: string) { + if (!_syncServer) return; + // Skip if space already has mailboxes + const prefix = `${space}:inbox:mailboxes:`; + const existing = _syncServer.listDocs().filter((id) => id.startsWith(prefix)); + if (existing.length > 0) return; + + const mbId = crypto.randomUUID(); + const docId = mailboxDocId(space, mbId); + const now = Date.now(); + const day = 86400000; + + const doc = Automerge.change(Automerge.init(), 'seed template mailbox', (d) => { + d.meta = { module: 'inbox', collection: 'mailboxes', version: 1, spaceSlug: space, createdAt: now }; + d.mailbox = { + id: mbId, workspaceId: null, slug: 'commons-team', name: 'Commons Team', + email: `commons-team@${space}.rspace.online`, + description: 'Shared mailbox for the commons coordination team.', + visibility: 'members', ownerDid: 'did:demo:seed', + safeAddress: null, safeChainId: null, approvalThreshold: 1, createdAt: now, + }; + d.members = []; + d.threads = {}; + d.approvals = {}; + + const threads: Array<{ subj: string; from: string; fromName: string; body: string; tags: string[]; age: number }> = [ + { + subj: 'Grant Application Update — Gitcoin GG22', + from: 'grants@gitcoin.co', fromName: 'Gitcoin Grants', + body: 'Your application for the Cosmolocal Commons project has been accepted into GG22 Climate Solutions round. Matching pool opens April 1.', + tags: ['grants', 'important'], age: 2, + }, + { + subj: 'Partnership Inquiry — Barcelona Maker Space', + from: 'hello@bcnmakers.cat', fromName: 'BCN Makers', + body: 'Hi! We saw your cosmolocal print network and would love to join as a provider. We have risograph, screen print, and laser cutting capabilities.', + tags: ['partnerships'], age: 5, + }, + { + subj: 'Weekly Digest — rSpace Dev Updates', + from: 'digest@rspace.online', fromName: 'rSpace Bot', + body: '## This Week\n\n- 3 new modules deployed (rChoices, rSplat, rInbox)\n- EncryptID guardian recovery shipped\n- 2 new providers joined the cosmolocal network', + tags: ['digest'], age: 1, + }, + ]; + + for (const t of threads) { + const tId = crypto.randomUUID(); + d.threads[tId] = { + id: tId, mailboxId: mbId, messageId: `<${crypto.randomUUID()}@demo>`, + subject: t.subj, fromAddress: t.from, fromName: t.fromName, + toAddresses: [`commons-team@${space}.rspace.online`], ccAddresses: [], + bodyText: t.body, bodyHtml: '', tags: t.tags, status: 'open', + isRead: t.age > 3, isStarred: t.tags.includes('important'), + assignedTo: null, hasAttachments: false, + receivedAt: now - t.age * day, createdAt: now - t.age * day, comments: [], + }; + } + }); + + _syncServer.setDoc(docId, doc); + console.log(`[Inbox] Template seeded for "${space}": 1 mailbox, 3 threads`); +} + export const inboxModule: RSpaceModule = { id: "rinbox", name: "rInbox", @@ -993,6 +1059,7 @@ export const inboxModule: RSpaceModule = { docSchemas: [{ pattern: '{space}:inbox:mailboxes:{mailboxId}', description: 'Mailbox with threads and approvals', init: mailboxSchema.init }], routes, landingPage: renderLanding, + seedTemplate: seedTemplateInbox, async onInit(ctx) { _syncServer = ctx.syncServer; console.log("[Inbox] Module initialized (Automerge-only, no PG)"); diff --git a/modules/rsplat/mod.ts b/modules/rsplat/mod.ts index fdc8d9f..23eb342 100644 --- a/modules/rsplat/mod.ts +++ b/modules/rsplat/mod.ts @@ -669,6 +669,48 @@ routes.get("/view/:id", async (c) => { return c.html(html); }); +// ── Seed template data ── + +function seedTemplateSplat(space: string) { + if (!_syncServer) return; + const doc = ensureDoc(space); + if (Object.keys(doc.items).length > 0) return; + + const docId = splatScenesDocId(space); + const now = Date.now(); + + const scenes: Array<{ title: string; slug: string; desc: string; tags: string[] }> = [ + { + title: 'Community Garden Scan', slug: 'community-garden-scan', + desc: 'A 3D Gaussian splat capture of the community garden space, captured with a phone camera walk-around.', + tags: ['outdoor', 'community', 'garden'], + }, + { + title: 'Workshop Space Scan', slug: 'workshop-space-scan', + desc: 'Interior scan of the maker workshop, showing CNC, 3D printer stations, and material shelving.', + tags: ['indoor', 'workshop', 'makerspace'], + }, + ]; + + _syncServer.changeDoc(docId, 'seed template splats', (d) => { + for (const s of scenes) { + const id = crypto.randomUUID(); + d.items[id] = { + id, slug: s.slug, title: s.title, description: s.desc, + filePath: '', fileFormat: 'splat', fileSizeBytes: 0, + tags: s.tags, spaceSlug: space, + contributorId: 'did:demo:seed', contributorName: 'Demo', + source: 'upload', status: 'published', viewCount: 0, + paymentTx: null, paymentNetwork: null, + createdAt: now, processingStatus: 'ready', processingError: null, + sourceFileCount: 0, sourceFiles: [], + }; + } + }); + + console.log(`[Splat] Template seeded for "${space}": 2 splat entries`); +} + // ── Module export ── export const splatModule: RSpaceModule = { @@ -680,6 +722,7 @@ export const splatModule: RSpaceModule = { docSchemas: [{ pattern: '{space}:splat:scenes', description: 'Splat scene metadata', init: splatScenesSchema.init }], routes, landingPage: renderLanding, + seedTemplate: seedTemplateSplat, standaloneDomain: "rsplat.online", hidden: true, outputPaths: [