Remove rnotes.online routing — re-deployed standalone with Memory Card spec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-22 22:53:11 +00:00
parent 519f13045e
commit 05d2280a2b
5 changed files with 298 additions and 8 deletions

View File

@ -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"

View File

@ -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 ──

View File

@ -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<string, string> = {};
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) {

View File

@ -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) {

View File

@ -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 ──