From 05d2280a2bbbc7bfa96418905b37000338a0061c Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 22 Feb 2026 22:53:11 +0000 Subject: [PATCH] =?UTF-8?q?Remove=20rnotes.online=20routing=20=E2=80=94=20?= =?UTF-8?q?re-deployed=20standalone=20with=20Memory=20Card=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 5 +-- modules/cal/mod.ts | 103 ++++++++++++++++++++++++++++++++++++++++++- modules/notes/mod.ts | 94 ++++++++++++++++++++++++++++++++++++++- modules/vote/mod.ts | 60 ++++++++++++++++++++++++- modules/work/mod.ts | 44 +++++++++++++++++- 5 files changed, 298 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index fe1a42b..c5457a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,10 +83,7 @@ services: - "traefik.http.routers.rspace-rvote.entrypoints=web" - "traefik.http.routers.rspace-rvote.priority=120" - "traefik.http.routers.rspace-rvote.service=rspace-online" - - "traefik.http.routers.rspace-rnotes.rule=Host(`rnotes.online`)" - - "traefik.http.routers.rspace-rnotes.entrypoints=web" - - "traefik.http.routers.rspace-rnotes.priority=120" - - "traefik.http.routers.rspace-rnotes.service=rspace-online" + # rnotes.online — re-deployed standalone with Memory Card spec (Feb 22) - "traefik.http.routers.rspace-rwork.rule=Host(`rwork.online`)" - "traefik.http.routers.rspace-rwork.entrypoints=web" - "traefik.http.routers.rspace-rwork.priority=120" diff --git a/modules/cal/mod.ts b/modules/cal/mod.ts index 1071a5b..888d6c3 100644 --- a/modules/cal/mod.ts +++ b/modules/cal/mod.ts @@ -28,7 +28,108 @@ async function initDB() { } } -initDB(); +async function seedDemoIfEmpty() { + try { + const count = await sql.unsafe("SELECT count(*)::int as cnt FROM rcal.events"); + if (parseInt(count[0].cnt) > 0) return; + + // Create calendar sources + const community = await sql.unsafe( + `INSERT INTO rcal.calendar_sources (name, source_type, color, is_active, is_visible) + VALUES ('Community Events', 'MANUAL', '#6366f1', true, true) RETURNING id` + ); + const sprints = await sql.unsafe( + `INSERT INTO rcal.calendar_sources (name, source_type, color, is_active, is_visible) + VALUES ('Development Sprints', 'MANUAL', '#f59e0b', true, true) RETURNING id` + ); + const communityId = community[0].id; + const sprintsId = sprints[0].id; + + // Create location hierarchy + const world = await sql.unsafe( + `INSERT INTO rcal.locations (name, granularity) VALUES ('Earth', 1) RETURNING id` + ); + const europe = await sql.unsafe( + `INSERT INTO rcal.locations (name, granularity, parent_id, lat, lng) VALUES ('Europe', 2, $1, 48.8566, 2.3522) RETURNING id`, + [world[0].id] + ); + const berlin = await sql.unsafe( + `INSERT INTO rcal.locations (name, granularity, parent_id, lat, lng) VALUES ('Berlin', 4, $1, 52.52, 13.405) RETURNING id`, + [europe[0].id] + ); + + // Seed events — past, current week, and future + const now = new Date(); + const events = [ + { + title: "rSpace Launch Party", + desc: "Celebrating the launch of the unified rSpace platform with all 22 modules live.", + start: daysFromNow(-21, 18, 0), end: daysFromNow(-21, 22, 0), + sourceId: communityId, locationName: "Radiant Hall, Pittsburgh", + }, + { + title: "Provider Onboarding Workshop", + desc: "Hands-on session for print providers joining the cosmolocal network.", + start: daysFromNow(-12, 14, 0), end: daysFromNow(-12, 17, 0), + sourceId: communityId, virtual: true, virtualUrl: "https://meet.jit.si/rspace-providers", virtualPlatform: "Jitsi", + }, + { + title: "Weekly Community Standup", + desc: "Open standup — share what you're working on, ask for help, coordinate.", + start: daysFromNow(0, 16, 0), end: daysFromNow(0, 16, 45), + sourceId: communityId, virtual: true, virtualUrl: "https://meet.jit.si/rspace-standup", virtualPlatform: "Jitsi", + }, + { + title: "Sprint: Module Seeding & Polish", + desc: "Focus sprint on populating demo data and improving UX across all modules.", + start: daysFromNow(0, 9, 0), end: daysFromNow(5, 18, 0), + sourceId: sprintsId, allDay: true, + }, + { + title: "rFunds Budget Review", + desc: "Quarterly review of treasury flows, enoughness thresholds, and overflow routing.", + start: daysFromNow(6, 15, 0), end: daysFromNow(6, 17, 0), + sourceId: communityId, virtual: true, virtualUrl: "https://meet.jit.si/rfunds-review", virtualPlatform: "Jitsi", + }, + { + title: "Cosmolocal Design Sprint", + desc: "Two-day design sprint on the next generation of cosmolocal tooling.", + start: daysFromNow(11, 9, 0), end: daysFromNow(12, 18, 0), + sourceId: sprintsId, locationId: berlin[0].id, locationName: "Druckwerkstatt Berlin", + }, + { + title: "Q1 Retrospective", + desc: "Looking back at what we built, what worked, and what to improve.", + start: daysFromNow(21, 16, 0), end: daysFromNow(21, 18, 0), + sourceId: communityId, virtual: true, virtualUrl: "https://meet.jit.si/rspace-retro", virtualPlatform: "Jitsi", + }, + ]; + + for (const e of events) { + await sql.unsafe( + `INSERT INTO rcal.events (title, description, start_time, end_time, all_day, source_id, + location_id, location_name, is_virtual, virtual_url, virtual_platform) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, + [e.title, e.desc, e.start.toISOString(), e.end.toISOString(), e.allDay || false, + e.sourceId, e.locationId || null, e.locationName || null, + e.virtual || false, e.virtualUrl || null, e.virtualPlatform || null] + ); + } + + console.log("[Cal] Demo data seeded: 2 sources, 3 locations, 7 events"); + } catch (e) { + console.error("[Cal] Seed error:", e); + } +} + +function daysFromNow(days: number, hours: number, minutes: number): Date { + const d = new Date(); + d.setDate(d.getDate() + days); + d.setHours(hours, minutes, 0, 0); + return d; +} + +initDB().then(seedDemoIfEmpty); // ── API: Events ── diff --git a/modules/notes/mod.ts b/modules/notes/mod.ts index 5da8c9d..3208c5c 100644 --- a/modules/notes/mod.ts +++ b/modules/notes/mod.ts @@ -28,7 +28,99 @@ async function initDB() { } } -initDB(); +async function seedDemoIfEmpty() { + try { + const count = await sql.unsafe("SELECT count(*)::int as cnt FROM rnotes.notebooks"); + if (parseInt(count[0].cnt) > 0) return; + + // Notebook 1: Project Ideas + const nb1 = await sql.unsafe( + `INSERT INTO rnotes.notebooks (title, description, cover_color, is_public) + VALUES ('Project Ideas', 'Brainstorms and design notes for the r* ecosystem', '#6366f1', true) RETURNING id` + ); + // Notebook 2: Meeting Notes + const nb2 = await sql.unsafe( + `INSERT INTO rnotes.notebooks (title, description, cover_color, is_public) + VALUES ('Meeting Notes', 'Weekly standups, design reviews, and retrospectives', '#f59e0b', true) RETURNING id` + ); + // Notebook 3: How-To Guides + const nb3 = await sql.unsafe( + `INSERT INTO rnotes.notebooks (title, description, cover_color, is_public) + VALUES ('How-To Guides', 'Tutorials and onboarding guides for contributors', '#10b981', true) RETURNING id` + ); + + // Create tags + const tagIds: Record = {}; + for (const name of ["design", "architecture", "cosmolocal", "governance", "onboarding", "review", "standup"]) { + const row = await sql.unsafe( + `INSERT INTO rnotes.tags (name) VALUES ($1) ON CONFLICT (name) DO UPDATE SET name = $1 RETURNING id`, + [name] + ); + tagIds[name] = row[0].id; + } + + // Seed notes + const notes = [ + { + nbId: nb1[0].id, title: "Cosmolocal Manufacturing Network", + content: "## Vision\n\nDesign global, manufacture local. Every creative work should be producible by the nearest capable provider.\n\n## Key Components\n\n- **Artifact Spec**: Standardized envelope describing what to produce\n- **Provider Registry**: Directory of local makers with capabilities + pricing\n- **rCart**: Marketplace connecting creators to providers\n- **Revenue Splits**: 50% provider, 35% creator, 15% community\n\n## Open Questions\n\n- How do we handle quality assurance across distributed providers?\n- Should providers be able to set custom margins?\n- What's the minimum viable set of capabilities for launch?", + tags: ["cosmolocal", "architecture"], pinned: true, + }, + { + nbId: nb1[0].id, title: "Revenue Sharing Model", + content: "## Current Split\n\n| Recipient | Share | Rationale |\n|-----------|-------|-----------|\n| Provider | 50% | Covers materials, labor, shipping |\n| Creator | 35% | Design and creative work |\n| Community | 15% | Platform maintenance, commons fund |\n\n## Enoughness Thresholds\n\nOnce a funnel reaches its sufficient threshold, surplus flows to the next highest-need funnel. This prevents accumulation and keeps resources flowing.\n\n## Implementation\n\nrFunds Flow Service handles deposits from rCart. Each order total is routed through the configured flow → funnel → overflow splits.", + tags: ["cosmolocal", "governance"], + }, + { + nbId: nb1[0].id, title: "FUN Model: Forget, Update, New", + content: "## Replacing CRUD\n\nNothing is permanently destroyed in rSpace.\n\n- **Forget** replaces Delete — soft-delete with `forgotten: true`. Shapes stay in document, hidden from canvas. Memory panel lets you browse + Remember.\n- **Update** stays the same — public `sync.updateShape()` for programmatic updates\n- **New** replaces Create — language shift: toolbar says \"New X\", events are `new-shape`\n\n## Why?\n\nData sovereignty means users should always be able to recover their work. The Memory panel makes forgotten shapes discoverable, like a digital archive.", + tags: ["design", "architecture"], + }, + { + nbId: nb2[0].id, title: "Weekly Standup — Feb 15, 2026", + content: "## Attendees\n\nAlice, Bob, Carol\n\n## Updates\n\n**Alice**: Finished EncryptID guardian recovery flow. 2-of-3 guardian approval working. Next: device linking via QR code.\n\n**Bob**: Provider registry now has 6 printers globally. Working on proximity search with earthdistance extension.\n\n**Carol**: rFunds river visualization deployed. Enoughness layer showing golden glow on sufficient funnels.\n\n## Action Items\n\n- [ ] Alice: Document guardian recovery API endpoints\n- [ ] Bob: Add turnaround time estimates to provider matching\n- [ ] Carol: Add demo mode to river view with mock data", + tags: ["standup"], + }, + { + nbId: nb2[0].id, title: "Design Review — rBooks Flipbook Reader", + content: "## What We Reviewed\n\nThe react-pageflip integration for PDF reading in rBooks.\n\n## Feedback\n\n1. **Page turn animation** — smooth, feels good on desktop. On mobile, swipe gesture needs larger hit area.\n2. **PDF rendering** — react-pdf handles most PDFs well. Large files (>50MB) cause browser memory issues.\n3. **Read Locally mode** — IndexedDB storage works. Need to show storage usage somewhere.\n\n## Decisions\n\n- Ship current version, iterate on mobile\n- Add a 50MB soft warning on upload\n- Explore PDF.js worker for background rendering", + tags: ["review", "design"], + }, + { + nbId: nb3[0].id, title: "Getting Started with rSpace Development", + content: "## Prerequisites\n\n- Bun runtime (v1.3+)\n- Docker + Docker Compose\n- Git access to Gitea\n\n## Local Setup\n\n```bash\ngit clone ssh://git@gitea.jeffemmett.com:223/jeffemmett/rspace-online.git\ncd rspace-online\nbun install\nbun run dev\n```\n\n## Module Structure\n\nEach module lives in `modules/{name}/` and exports an `RSpaceModule` interface:\n\n```typescript\nexport interface RSpaceModule {\n id: string;\n name: string;\n icon: string;\n description: string;\n routes: Hono;\n}\n```\n\n## Adding a New Module\n\n1. Create `modules/{name}/mod.ts`\n2. Create `modules/{name}/components/` for web components\n3. Add build step in `vite.config.ts`\n4. Register in `server/index.ts`", + tags: ["onboarding"], + }, + { + nbId: nb3[0].id, title: "How to Add a Cosmolocal Provider", + content: "## Overview\n\nProviders are local print shops, makerspaces, or studios that can fulfill rCart orders.\n\n## Steps\n\n1. Visit `providers.mycofi.earth`\n2. Sign in with your rStack passkey\n3. Click \"Register Provider\"\n4. Fill in:\n - Name, location (address + coordinates)\n - Capabilities (laser-print, risograph, screen-print, etc.)\n - Substrates (paper types, fabric, vinyl)\n - Turnaround time and pricing\n5. Submit for review\n\n## Matching Algorithm\n\nWhen an order comes in, rCart matches based on:\n- Required capabilities vs. provider capabilities\n- Geographic distance (earthdistance extension)\n- Turnaround time\n- Price", + tags: ["cosmolocal", "onboarding"], + }, + ]; + + for (const n of notes) { + const row = await sql.unsafe( + `INSERT INTO rnotes.notes (notebook_id, title, content, content_plain, type, is_pinned) + VALUES ($1, $2, $3, $4, 'NOTE', $5) RETURNING id`, + [n.nbId, n.title, n.content, n.content.replace(/<[^>]*>/g, " ").replace(/[#*|`\-\[\]]/g, " ").replace(/\s+/g, " ").trim(), n.pinned || false] + ); + for (const tagName of n.tags) { + if (tagIds[tagName]) { + await sql.unsafe( + "INSERT INTO rnotes.note_tags (note_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING", + [row[0].id, tagIds[tagName]] + ); + } + } + } + + console.log("[Notes] Demo data seeded: 3 notebooks, 7 notes, 7 tags"); + } catch (e) { + console.error("[Notes] Seed error:", e); + } +} + +initDB().then(seedDemoIfEmpty); // ── Helper: get or create user ── async function getOrCreateUser(did: string, username?: string) { diff --git a/modules/vote/mod.ts b/modules/vote/mod.ts index c2f5814..b42ccfa 100644 --- a/modules/vote/mod.ts +++ b/modules/vote/mod.ts @@ -28,7 +28,65 @@ async function initDB() { } } -initDB(); +async function seedDemoIfEmpty() { + try { + const count = await sql.unsafe("SELECT count(*)::int as cnt FROM rvote.spaces"); + if (parseInt(count[0].cnt) > 0) return; + + // Create demo user + const user = await sql.unsafe( + `INSERT INTO rvote.users (did, username) VALUES ('did:demo:seed', 'demo') + ON CONFLICT (did) DO UPDATE SET username = 'demo' RETURNING id` + ); + const userId = user[0].id; + + // Create voting space + await sql.unsafe( + `INSERT INTO rvote.spaces (slug, name, description, owner_did, visibility, promotion_threshold) + VALUES ('community', 'Community Governance', 'Proposals for the rSpace ecosystem', 'did:demo:seed', 'public', 100)` + ); + + // Seed proposals in various states + const proposals = [ + { title: "Add dark mode across all r* modules", desc: "Implement a consistent dark theme with a toggle in shell.css. Use CSS custom properties for theming so each module inherits automatically.", status: "RANKING", score: 45 }, + { title: "Implement real-time collaboration in rNotes", desc: "Use Automerge CRDTs (already in the stack) to enable simultaneous editing of notes, similar to how rSpace canvas works.", status: "RANKING", score: 72 }, + { title: "Adopt cosmolocal print-on-demand for all merch", desc: "Route all merchandise orders through the provider registry to find the closest printer. Reduces shipping emissions and supports local economies.", status: "VOTING", score: 105 }, + { title: "Use EncryptID passkeys for all authentication", desc: "Standardize on WebAuthn passkeys via EncryptID across the entire r* ecosystem. One passkey, all apps.", status: "PASSED", score: 150 }, + { title: "Switch from PostgreSQL to SQLite for simpler deployment", desc: "Evaluate replacing PostgreSQL with SQLite for modules that don't need concurrent writes.", status: "FAILED", score: 30 }, + ]; + + for (const p of proposals) { + const row = await sql.unsafe( + `INSERT INTO rvote.proposals (space_slug, author_id, title, description, status, score) + VALUES ('community', $1, $2, $3, $4, $5) RETURNING id`, + [userId, p.title, p.desc, p.status, p.score] + ); + + if (p.status === "VOTING") { + await sql.unsafe( + `UPDATE rvote.proposals SET voting_ends_at = NOW() + INTERVAL '5 days', final_yes = 5, final_no = 2 WHERE id = $1`, + [row[0].id] + ); + } else if (p.status === "PASSED") { + await sql.unsafe( + `UPDATE rvote.proposals SET final_yes = 12, final_no = 3, final_abstain = 2 WHERE id = $1`, + [row[0].id] + ); + } else if (p.status === "FAILED") { + await sql.unsafe( + `UPDATE rvote.proposals SET final_yes = 2, final_no = 8, final_abstain = 1 WHERE id = $1`, + [row[0].id] + ); + } + } + + console.log("[Vote] Demo data seeded: 1 space, 5 proposals"); + } catch (e) { + console.error("[Vote] Seed error:", e); + } +} + +initDB().then(seedDemoIfEmpty); // ── Helper: get or create user by DID ── async function getOrCreateUser(did: string, username?: string) { diff --git a/modules/work/mod.ts b/modules/work/mod.ts index d1aadb3..db44f60 100644 --- a/modules/work/mod.ts +++ b/modules/work/mod.ts @@ -28,7 +28,49 @@ async function initDB() { } } -initDB(); +async function seedDemoIfEmpty() { + try { + const count = await sql.unsafe("SELECT count(*)::int as cnt FROM rwork.spaces"); + if (parseInt(count[0].cnt) > 0) return; + + // Create workspace + const space = await sql.unsafe( + `INSERT INTO rwork.spaces (name, slug, description, icon, owner_did) + VALUES ('rSpace Development', 'rspace-dev', 'Building the cosmolocal r* ecosystem', '🚀', 'did:demo:seed') + RETURNING id` + ); + const spaceId = space[0].id; + + // Seed tasks across all kanban columns + const tasks = [ + { title: "Add dark mode toggle to settings page", status: "TODO", priority: "MEDIUM", labels: ["feature"], sort: 0 }, + { title: "Write API documentation for rPubs endpoints", status: "TODO", priority: "LOW", labels: ["docs"], sort: 1 }, + { title: "Investigate slow PDF generation on large documents", status: "TODO", priority: "HIGH", labels: ["bug"], sort: 2 }, + { title: "Implement file search and filtering in rFiles", status: "IN_PROGRESS", priority: "HIGH", labels: ["feature"], sort: 0 }, + { title: "Set up SMTP relay for transactional notifications", status: "IN_PROGRESS", priority: "MEDIUM", labels: ["chore"], sort: 1 }, + { title: "Add PDF export to rNotes notebooks", status: "REVIEW", priority: "MEDIUM", labels: ["feature"], sort: 0 }, + { title: "Fix conviction score decay calculation in rVote", status: "REVIEW", priority: "HIGH", labels: ["bug"], sort: 1 }, + { title: "Deploy EncryptID passkey authentication", status: "DONE", priority: "URGENT", labels: ["feature"], sort: 0 }, + { title: "Set up Cloudflare tunnel for all r* domains", status: "DONE", priority: "HIGH", labels: ["chore"], sort: 1 }, + { title: "Create cosmolocal provider directory with 6 printers", status: "DONE", priority: "MEDIUM", labels: ["feature"], sort: 2 }, + { title: "Migrate email from Resend to self-hosted Mailcow", status: "DONE", priority: "MEDIUM", labels: ["chore"], sort: 3 }, + ]; + + for (const t of tasks) { + await sql.unsafe( + `INSERT INTO rwork.tasks (space_id, title, status, priority, labels, sort_order) + VALUES ($1, $2, $3, $4, $5, $6)`, + [spaceId, t.title, t.status, t.priority, t.labels, t.sort] + ); + } + + console.log("[Work] Demo data seeded: 1 workspace, 11 tasks"); + } catch (e) { + console.error("[Work] Seed error:", e); + } +} + +initDB().then(seedDemoIfEmpty); // ── API: Spaces ──