feat(rflows+rtasks): BCRG demo flow with live rTasks integration
Replace TBFF preset with 19-node BCRG Community Flow (2 sources →
central funnel → 5 person funnels → 11 outcomes). Seed matching
BCRG Outcomes board in rTasks (4 DONE, 5 IN_PROGRESS, 2 TODO).
Add SyncServer.registerWatcher() for cross-module doc change hooks.
When an rFlows outcome is marked "completed", auto-create a DONE task
in rTasks with ref:rflows:outcome:{id} deduplication.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d668ca287c
commit
9bb00a8bab
|
|
@ -2,7 +2,7 @@
|
||||||
* <folk-flows-app> — main rFlows application component.
|
* <folk-flows-app> — main rFlows application component.
|
||||||
*
|
*
|
||||||
* Views:
|
* Views:
|
||||||
* "landing" — TBFF info hero + flow list cards
|
* "landing" — BCRG info hero + flow list cards
|
||||||
* "detail" — Flow detail with tabs: Table | River | Transactions
|
* "detail" — Flow detail with tabs: Table | River | Transactions
|
||||||
*
|
*
|
||||||
* Attributes:
|
* Attributes:
|
||||||
|
|
@ -225,7 +225,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
}
|
}
|
||||||
// Fallback: demoNodes
|
// Fallback: demoNodes
|
||||||
this.currentFlowId = 'demo';
|
this.currentFlowId = 'demo';
|
||||||
this.flowName = "TBFF Demo Flow";
|
this.flowName = "BCRG Demo Flow";
|
||||||
this.nodes = demoNodes.map((n) => ({ ...n, data: { ...n.data } }));
|
this.nodes = demoNodes.map((n) => ({ ...n, data: { ...n.data } }));
|
||||||
localStorage.setItem('rflows:local:active', 'demo');
|
localStorage.setItem('rflows:local:active', 'demo');
|
||||||
this.render();
|
this.render();
|
||||||
|
|
@ -395,7 +395,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
} catch { return; }
|
} catch { return; }
|
||||||
} else if (flowId === 'demo') {
|
} else if (flowId === 'demo') {
|
||||||
this.currentFlowId = 'demo';
|
this.currentFlowId = 'demo';
|
||||||
this.flowName = 'TBFF Demo Flow';
|
this.flowName = 'BCRG Demo Flow';
|
||||||
this.nodes = demoNodes.map((n) => ({ ...n, data: { ...n.data } }));
|
this.nodes = demoNodes.map((n) => ({ ...n, data: { ...n.data } }));
|
||||||
} else { return; }
|
} else { return; }
|
||||||
localStorage.setItem('rflows:local:active', flowId);
|
localStorage.setItem('rflows:local:active', flowId);
|
||||||
|
|
@ -449,7 +449,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
// Always include demo if not already tracked
|
// Always include demo if not already tracked
|
||||||
if (!list.includes('demo')) list.unshift('demo');
|
if (!list.includes('demo')) list.unshift('demo');
|
||||||
return list.map(id => {
|
return list.map(id => {
|
||||||
if (id === 'demo') return { id: 'demo', name: 'TBFF Demo Flow', nodeCount: demoNodes.length, updatedAt: 0 };
|
if (id === 'demo') return { id: 'demo', name: 'BCRG Demo Flow', nodeCount: demoNodes.length, updatedAt: 0 };
|
||||||
const raw = localStorage.getItem(`rflows:local:${id}`);
|
const raw = localStorage.getItem(`rflows:local:${id}`);
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -661,7 +661,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flows-about">
|
<div class="flows-about">
|
||||||
<h2 class="flows-about__heading">How TBFF Works</h2>
|
<h2 class="flows-about__heading">How BCRG Works</h2>
|
||||||
<div class="flows-about__steps">
|
<div class="flows-about__steps">
|
||||||
<div class="flows-about__step">
|
<div class="flows-about__step">
|
||||||
<div class="flows-about__step-num">1</div>
|
<div class="flows-about__step-num">1</div>
|
||||||
|
|
@ -4829,7 +4829,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
const raw = localStorage.getItem(`rflows:local:${flowId}`);
|
const raw = localStorage.getItem(`rflows:local:${flowId}`);
|
||||||
if (raw) flow = JSON.parse(raw);
|
if (raw) flow = JSON.parse(raw);
|
||||||
else if (flowId === 'demo') {
|
else if (flowId === 'demo') {
|
||||||
flow = { id: 'demo', name: 'TBFF Demo Flow', nodes: demoNodes, createdAt: 0, updatedAt: 0, createdBy: null };
|
flow = { id: 'demo', name: 'BCRG Demo Flow', nodes: demoNodes, createdAt: 0, updatedAt: 0, createdBy: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!flow) return;
|
if (!flow) return;
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,299 @@
|
||||||
/**
|
/**
|
||||||
* Demo presets — ported from rflows-online/lib/presets.ts.
|
* Demo presets — BCRG Community Flow.
|
||||||
|
*
|
||||||
|
* 2 sources → BCRG central funnel → 5 person funnels (Alice–Eve) → 11 outcomes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FlowNode, FunnelNodeData, OutcomeNodeData, OutcomePhase, SourceNodeData } from "./types";
|
import type { FlowNode, FunnelNodeData, OutcomeNodeData, SourceNodeData } from "./types";
|
||||||
|
|
||||||
export const SPENDING_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#06b6d4", "#10b981", "#6366f1"];
|
export const SPENDING_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#06b6d4", "#10b981", "#6366f1"];
|
||||||
export const OVERFLOW_COLORS = ["#f59e0b", "#ef4444", "#f97316", "#eab308", "#dc2626", "#ea580c"];
|
export const OVERFLOW_COLORS = ["#f59e0b", "#ef4444", "#f97316", "#eab308", "#dc2626", "#ea580c"];
|
||||||
|
|
||||||
export const demoNodes: FlowNode[] = [
|
export const demoNodes: FlowNode[] = [
|
||||||
|
// ── Sources (Y=-300) ──
|
||||||
{
|
{
|
||||||
id: "revenue", type: "source", position: { x: 660, y: -200 },
|
id: "source-a", type: "source", position: { x: 440, y: -300 },
|
||||||
data: {
|
data: {
|
||||||
label: "Revenue Stream", flowRate: 5000, sourceType: "card",
|
label: "Grants & Donations", flowRate: 7500, sourceType: "card",
|
||||||
targetAllocations: [{ targetId: "treasury", percentage: 100, color: "#10b981" }],
|
targetAllocations: [{ targetId: "bcrg", percentage: 100, color: "#10b981" }],
|
||||||
} as SourceNodeData,
|
} as SourceNodeData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "treasury", type: "funnel", position: { x: 630, y: 0 },
|
id: "source-b", type: "source", position: { x: 880, y: -300 },
|
||||||
data: {
|
data: {
|
||||||
label: "Treasury", currentValue: 85000, desiredOutflow: 10000,
|
label: "Membership Fees", flowRate: 7500, sourceType: "card",
|
||||||
minThreshold: 10000, sufficientThreshold: 40000, maxThreshold: 60000,
|
targetAllocations: [{ targetId: "bcrg", percentage: 100, color: "#10b981" }],
|
||||||
maxCapacity: 90000, inflowRate: 1000, dynamicOverflow: true,
|
} as SourceNodeData,
|
||||||
overflowAllocations: [
|
|
||||||
{ targetId: "public-goods", percentage: 40, color: OVERFLOW_COLORS[0] },
|
|
||||||
{ targetId: "research", percentage: 35, color: OVERFLOW_COLORS[1] },
|
|
||||||
{ targetId: "emergency", percentage: 25, color: OVERFLOW_COLORS[2] },
|
|
||||||
],
|
|
||||||
spendingAllocations: [
|
|
||||||
{ targetId: "treasury-ops", percentage: 100, color: SPENDING_COLORS[0] },
|
|
||||||
],
|
|
||||||
} as FunnelNodeData,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── BCRG central funnel (Y=0) ──
|
||||||
{
|
{
|
||||||
id: "public-goods", type: "funnel", position: { x: 170, y: 450 },
|
id: "bcrg", type: "funnel", position: { x: 630, y: 0 },
|
||||||
data: {
|
data: {
|
||||||
label: "Public Goods", currentValue: 45000, desiredOutflow: 7000,
|
label: "BCRG", currentValue: 95000, desiredOutflow: 25000,
|
||||||
minThreshold: 7000, sufficientThreshold: 28000, maxThreshold: 42000,
|
minThreshold: 25000, sufficientThreshold: 100000, maxThreshold: 150000,
|
||||||
maxCapacity: 63000, inflowRate: 400,
|
maxCapacity: 225000, inflowRate: 15000, dynamicOverflow: true,
|
||||||
overflowAllocations: [],
|
overflowAllocations: [],
|
||||||
spendingAllocations: [
|
spendingAllocations: [
|
||||||
{ targetId: "pg-infra", percentage: 50, color: SPENDING_COLORS[0] },
|
{ targetId: "alice", percentage: 20, color: SPENDING_COLORS[0] },
|
||||||
{ targetId: "pg-education", percentage: 30, color: SPENDING_COLORS[1] },
|
{ targetId: "bob", percentage: 20, color: SPENDING_COLORS[1] },
|
||||||
{ targetId: "pg-tooling", percentage: 20, color: SPENDING_COLORS[2] },
|
{ targetId: "carol", percentage: 20, color: SPENDING_COLORS[2] },
|
||||||
|
{ targetId: "dave", percentage: 20, color: SPENDING_COLORS[3] },
|
||||||
|
{ targetId: "eve", percentage: 20, color: SPENDING_COLORS[4] },
|
||||||
],
|
],
|
||||||
} as FunnelNodeData,
|
} as FunnelNodeData,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Person funnels (Y=400) ──
|
||||||
{
|
{
|
||||||
id: "research", type: "funnel", position: { x: 975, y: 450 },
|
id: "alice", type: "funnel", position: { x: 100, y: 400 },
|
||||||
data: {
|
data: {
|
||||||
label: "Research", currentValue: 28000, desiredOutflow: 5000,
|
label: "Alice", currentValue: 18000, desiredOutflow: 5000,
|
||||||
minThreshold: 5000, sufficientThreshold: 20000, maxThreshold: 30000,
|
minThreshold: 5000, sufficientThreshold: 20000, maxThreshold: 30000,
|
||||||
maxCapacity: 45000, inflowRate: 350,
|
maxCapacity: 45000, inflowRate: 3000,
|
||||||
overflowAllocations: [],
|
overflowAllocations: [],
|
||||||
spendingAllocations: [
|
spendingAllocations: [
|
||||||
{ targetId: "research-grants", percentage: 70, color: SPENDING_COLORS[0] },
|
{ targetId: "alice-comms", percentage: 50, color: SPENDING_COLORS[0] },
|
||||||
{ targetId: "research-papers", percentage: 30, color: SPENDING_COLORS[1] },
|
{ targetId: "alice-events", percentage: 50, color: SPENDING_COLORS[1] },
|
||||||
],
|
],
|
||||||
} as FunnelNodeData,
|
} as FunnelNodeData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "emergency", type: "funnel", position: { x: 1320, y: 450 },
|
id: "bob", type: "funnel", position: { x: 380, y: 400 },
|
||||||
data: {
|
data: {
|
||||||
label: "Emergency", currentValue: 12000, desiredOutflow: 8000,
|
label: "Bob", currentValue: 14000, desiredOutflow: 4000,
|
||||||
minThreshold: 8000, sufficientThreshold: 32000, maxThreshold: 48000,
|
minThreshold: 4000, sufficientThreshold: 16000, maxThreshold: 24000,
|
||||||
maxCapacity: 72000, inflowRate: 250,
|
maxCapacity: 36000, inflowRate: 3000,
|
||||||
overflowAllocations: [],
|
overflowAllocations: [],
|
||||||
spendingAllocations: [
|
spendingAllocations: [
|
||||||
{ targetId: "emergency-response", percentage: 100, color: SPENDING_COLORS[0] },
|
{ targetId: "bob-research", percentage: 60, color: SPENDING_COLORS[0] },
|
||||||
|
{ targetId: "bob-writing", percentage: 40, color: SPENDING_COLORS[1] },
|
||||||
],
|
],
|
||||||
} as FunnelNodeData,
|
} as FunnelNodeData,
|
||||||
},
|
},
|
||||||
{ id: "pg-infra", type: "outcome", position: { x: -50, y: 900 },
|
{
|
||||||
|
id: "carol", type: "funnel", position: { x: 660, y: 400 },
|
||||||
data: {
|
data: {
|
||||||
label: "Infrastructure", description: "Core infrastructure development",
|
label: "Carol", currentValue: 22000, desiredOutflow: 6000,
|
||||||
fundingReceived: 22000, fundingTarget: 30000, status: "in-progress",
|
minThreshold: 6000, sufficientThreshold: 24000, maxThreshold: 36000,
|
||||||
|
maxCapacity: 54000, inflowRate: 3000,
|
||||||
|
overflowAllocations: [],
|
||||||
|
spendingAllocations: [
|
||||||
|
{ targetId: "carol-ops", percentage: 50, color: SPENDING_COLORS[0] },
|
||||||
|
{ targetId: "carol-infra", percentage: 50, color: SPENDING_COLORS[1] },
|
||||||
|
],
|
||||||
|
} as FunnelNodeData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dave", type: "funnel", position: { x: 940, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: "Dave", currentValue: 10000, desiredOutflow: 5000,
|
||||||
|
minThreshold: 5000, sufficientThreshold: 20000, maxThreshold: 30000,
|
||||||
|
maxCapacity: 45000, inflowRate: 3000,
|
||||||
|
overflowAllocations: [],
|
||||||
|
spendingAllocations: [
|
||||||
|
{ targetId: "dave-design", percentage: 45, color: SPENDING_COLORS[0] },
|
||||||
|
{ targetId: "dave-prototypes", percentage: 55, color: SPENDING_COLORS[1] },
|
||||||
|
],
|
||||||
|
} as FunnelNodeData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "eve", type: "funnel", position: { x: 1220, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: "Eve", currentValue: 16000, desiredOutflow: 5000,
|
||||||
|
minThreshold: 5000, sufficientThreshold: 20000, maxThreshold: 30000,
|
||||||
|
maxCapacity: 45000, inflowRate: 3000,
|
||||||
|
overflowAllocations: [],
|
||||||
|
spendingAllocations: [
|
||||||
|
{ targetId: "eve-legal", percentage: 40, color: SPENDING_COLORS[0] },
|
||||||
|
{ targetId: "eve-compliance", percentage: 30, color: SPENDING_COLORS[1] },
|
||||||
|
{ targetId: "eve-governance", percentage: 30, color: SPENDING_COLORS[2] },
|
||||||
|
],
|
||||||
|
} as FunnelNodeData,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Outcome nodes (Y=850) — 11 total ──
|
||||||
|
|
||||||
|
// Alice's outcomes
|
||||||
|
{ id: "alice-comms", type: "outcome", position: { x: -10, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Comms Strategy", description: "Community communications and outreach",
|
||||||
|
fundingReceived: 12000, fundingTarget: 12000, status: "completed",
|
||||||
phases: [
|
phases: [
|
||||||
{ name: "Foundation", fundingThreshold: 10000, tasks: [
|
{ name: "Planning", fundingThreshold: 4000, tasks: [
|
||||||
{ label: "Server provisioning", completed: true },
|
{ label: "Stakeholder mapping", completed: true },
|
||||||
{ label: "CI/CD pipeline", completed: true },
|
{ label: "Channel audit", completed: true },
|
||||||
] },
|
] },
|
||||||
{ name: "Scaling", fundingThreshold: 20000, tasks: [
|
{ name: "Execution", fundingThreshold: 8000, tasks: [
|
||||||
{ label: "Load balancer setup", completed: true },
|
{ label: "Newsletter launch", completed: true },
|
||||||
{ label: "Database replication", completed: false },
|
{ label: "Social media calendar", completed: true },
|
||||||
] },
|
] },
|
||||||
{ name: "Hardening", fundingThreshold: 30000, tasks: [
|
{ name: "Review", fundingThreshold: 12000, tasks: [
|
||||||
{ label: "Security audit", completed: false },
|
{ label: "Impact metrics report", completed: true },
|
||||||
{ label: "Disaster recovery", completed: false },
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
{ id: "alice-events", type: "outcome", position: { x: 210, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Event Series", description: "Quarterly community gatherings",
|
||||||
|
fundingReceived: 6000, fundingTarget: 15000, status: "in-progress",
|
||||||
|
phases: [
|
||||||
|
{ name: "Venue & Logistics", fundingThreshold: 5000, tasks: [
|
||||||
|
{ label: "Venue scouting", completed: true },
|
||||||
|
{ label: "Catering contracts", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Programming", fundingThreshold: 10000, tasks: [
|
||||||
|
{ label: "Speaker invitations", completed: false },
|
||||||
|
{ label: "Workshop facilitation", completed: false },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
|
||||||
|
// Bob's outcomes
|
||||||
|
{ id: "bob-research", type: "outcome", position: { x: 320, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Field Research", description: "Participatory action research in partner communities",
|
||||||
|
fundingReceived: 8000, fundingTarget: 20000, status: "in-progress",
|
||||||
|
phases: [
|
||||||
|
{ name: "Literature Review", fundingThreshold: 5000, tasks: [
|
||||||
|
{ label: "Systematic review", completed: true },
|
||||||
|
{ label: "Gap analysis", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Field Work", fundingThreshold: 15000, tasks: [
|
||||||
|
{ label: "Site visits", completed: false },
|
||||||
|
{ label: "Interviews & surveys", completed: false },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
{ id: "bob-writing", type: "outcome", position: { x: 490, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Publications", description: "Research papers and policy briefs",
|
||||||
|
fundingReceived: 2000, fundingTarget: 10000, status: "not-started",
|
||||||
|
phases: [
|
||||||
|
{ name: "Drafting", fundingThreshold: 5000, tasks: [
|
||||||
|
{ label: "Working paper draft", completed: false },
|
||||||
|
] },
|
||||||
|
{ name: "Peer Review", fundingThreshold: 10000, tasks: [
|
||||||
|
{ label: "Submit to journal", completed: false },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
|
||||||
|
// Carol's outcomes
|
||||||
|
{ id: "carol-ops", type: "outcome", position: { x: 600, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Operations", description: "Day-to-day operational management",
|
||||||
|
fundingReceived: 18000, fundingTarget: 18000, status: "completed",
|
||||||
|
phases: [
|
||||||
|
{ name: "Setup", fundingThreshold: 6000, tasks: [
|
||||||
|
{ label: "Process documentation", completed: true },
|
||||||
|
{ label: "Tool selection", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Execution", fundingThreshold: 12000, tasks: [
|
||||||
|
{ label: "Monthly reporting", completed: true },
|
||||||
|
{ label: "Budget tracking", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Optimization", fundingThreshold: 18000, tasks: [
|
||||||
|
{ label: "Workflow automation", completed: true },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
{ id: "carol-infra", type: "outcome", position: { x: 760, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Infrastructure", description: "Shared infrastructure and hosting",
|
||||||
|
fundingReceived: 10000, fundingTarget: 20000, status: "in-progress",
|
||||||
|
phases: [
|
||||||
|
{ name: "Provisioning", fundingThreshold: 8000, tasks: [
|
||||||
|
{ label: "Server setup", completed: true },
|
||||||
|
{ label: "CI/CD pipeline", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Hardening", fundingThreshold: 20000, tasks: [
|
||||||
|
{ label: "Security audit", completed: false },
|
||||||
|
{ label: "Disaster recovery plan", completed: false },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
|
||||||
|
// Dave's outcomes
|
||||||
|
{ id: "dave-design", type: "outcome", position: { x: 880, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Design System", description: "Shared UI/UX design system",
|
||||||
|
fundingReceived: 15000, fundingTarget: 15000, status: "completed",
|
||||||
|
phases: [
|
||||||
|
{ name: "Foundations", fundingThreshold: 5000, tasks: [
|
||||||
|
{ label: "Color & type system", completed: true },
|
||||||
|
{ label: "Component library", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Documentation", fundingThreshold: 10000, tasks: [
|
||||||
|
{ label: "Storybook setup", completed: true },
|
||||||
|
{ label: "Usage guidelines", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Rollout", fundingThreshold: 15000, tasks: [
|
||||||
|
{ label: "Team training", completed: true },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
{ id: "dave-prototypes", type: "outcome", position: { x: 1050, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Prototypes", description: "Rapid prototyping of new tools",
|
||||||
|
fundingReceived: 3000, fundingTarget: 12000, status: "in-progress",
|
||||||
|
phases: [
|
||||||
|
{ name: "Discovery", fundingThreshold: 4000, tasks: [
|
||||||
|
{ label: "User interviews", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Build", fundingThreshold: 8000, tasks: [
|
||||||
|
{ label: "MVP development", completed: false },
|
||||||
|
] },
|
||||||
|
{ name: "Test", fundingThreshold: 12000, tasks: [
|
||||||
|
{ label: "User testing rounds", completed: false },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
|
||||||
|
// Eve's outcomes
|
||||||
|
{ id: "eve-legal", type: "outcome", position: { x: 1140, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Legal Framework", description: "Legal structure and agreements",
|
||||||
|
fundingReceived: 10000, fundingTarget: 10000, status: "completed",
|
||||||
|
phases: [
|
||||||
|
{ name: "Research", fundingThreshold: 4000, tasks: [
|
||||||
|
{ label: "Jurisdiction analysis", completed: true },
|
||||||
|
{ label: "Entity comparison", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Formation", fundingThreshold: 10000, tasks: [
|
||||||
|
{ label: "Articles of incorporation", completed: true },
|
||||||
|
{ label: "Operating agreement", completed: true },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
{ id: "eve-compliance", type: "outcome", position: { x: 1280, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Compliance", description: "Regulatory compliance and reporting",
|
||||||
|
fundingReceived: 4000, fundingTarget: 12000, status: "in-progress",
|
||||||
|
phases: [
|
||||||
|
{ name: "Assessment", fundingThreshold: 4000, tasks: [
|
||||||
|
{ label: "Compliance gap analysis", completed: true },
|
||||||
|
] },
|
||||||
|
{ name: "Implementation", fundingThreshold: 8000, tasks: [
|
||||||
|
{ label: "KYC/AML procedures", completed: false },
|
||||||
|
] },
|
||||||
|
{ name: "Audit", fundingThreshold: 12000, tasks: [
|
||||||
|
{ label: "External audit", completed: false },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
} as OutcomeNodeData },
|
||||||
|
{ id: "eve-governance", type: "outcome", position: { x: 1430, y: 850 },
|
||||||
|
data: {
|
||||||
|
label: "Governance Model", description: "Governance framework and voting mechanisms",
|
||||||
|
fundingReceived: 1000, fundingTarget: 8000, status: "not-started",
|
||||||
|
phases: [
|
||||||
|
{ name: "Design", fundingThreshold: 3000, tasks: [
|
||||||
|
{ label: "Governance charter draft", completed: false },
|
||||||
|
] },
|
||||||
|
{ name: "Implementation", fundingThreshold: 8000, tasks: [
|
||||||
|
{ label: "Voting mechanism setup", completed: false },
|
||||||
|
{ label: "Dispute resolution process", completed: false },
|
||||||
] },
|
] },
|
||||||
],
|
],
|
||||||
} as OutcomeNodeData },
|
} as OutcomeNodeData },
|
||||||
{ id: "pg-education", type: "outcome", position: { x: 180, y: 900 },
|
|
||||||
data: { label: "Education", description: "Developer education programs", fundingReceived: 12000, fundingTarget: 20000, status: "in-progress" } as OutcomeNodeData },
|
|
||||||
{ id: "pg-tooling", type: "outcome", position: { x: 410, y: 900 },
|
|
||||||
data: { label: "Dev Tooling", description: "Open-source developer tools", fundingReceived: 5000, fundingTarget: 15000, status: "not-started" } as OutcomeNodeData },
|
|
||||||
{ id: "treasury-ops", type: "outcome", position: { x: 640, y: 900 },
|
|
||||||
data: { label: "Treasury Ops", description: "Day-to-day treasury management", fundingReceived: 15000, fundingTarget: 25000, status: "in-progress" } as OutcomeNodeData },
|
|
||||||
{ id: "research-grants", type: "outcome", position: { x: 870, y: 900 },
|
|
||||||
data: { label: "Grants", description: "Academic research grants", fundingReceived: 18000, fundingTarget: 25000, status: "in-progress" } as OutcomeNodeData },
|
|
||||||
{ id: "research-papers", type: "outcome", position: { x: 1100, y: 900 },
|
|
||||||
data: { label: "Papers", description: "Peer-reviewed publications", fundingReceived: 8000, fundingTarget: 10000, status: "in-progress" } as OutcomeNodeData },
|
|
||||||
{ id: "emergency-response", type: "outcome", position: { x: 1330, y: 900 },
|
|
||||||
data: { label: "Response Fund", description: "Rapid response for critical issues", fundingReceived: 5000, fundingTarget: 50000, status: "not-started" } as OutcomeNodeData },
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,13 @@ import type { SyncServer } from '../../server/local-first/sync-server';
|
||||||
import { flowsSchema, flowsDocId, type FlowsDoc, type SpaceFlow, type CanvasFlow } from './schemas';
|
import { flowsSchema, flowsDocId, type FlowsDoc, type SpaceFlow, type CanvasFlow } from './schemas';
|
||||||
import { demoNodes } from './lib/presets';
|
import { demoNodes } from './lib/presets';
|
||||||
import { OpenfortProvider } from './lib/openfort';
|
import { OpenfortProvider } from './lib/openfort';
|
||||||
|
import { boardDocId, createTaskItem } from '../rtasks/schemas';
|
||||||
|
import type { BoardDoc } from '../rtasks/schemas';
|
||||||
|
import type { OutcomeNodeData } from './lib/types';
|
||||||
|
|
||||||
let _syncServer: SyncServer | null = null;
|
let _syncServer: SyncServer | null = null;
|
||||||
let _openfort: OpenfortProvider | null = null;
|
let _openfort: OpenfortProvider | null = null;
|
||||||
|
const _completedOutcomes = new Set<string>(); // space:outcomeId — dedup for watcher
|
||||||
|
|
||||||
const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010";
|
const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010";
|
||||||
|
|
||||||
|
|
@ -129,6 +133,39 @@ function ensureDoc(space: string): FlowsDoc {
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a DONE task in rTasks when an rFlows outcome is completed.
|
||||||
|
* Deduplicates by checking for existing `ref:rflows:outcome:{id}` in the board doc.
|
||||||
|
*/
|
||||||
|
function createTaskForOutcome(space: string, outcomeId: string, label: string) {
|
||||||
|
if (!_syncServer) return;
|
||||||
|
const boardId = `${space}-bcrg`;
|
||||||
|
const docId = boardDocId(space, boardId);
|
||||||
|
|
||||||
|
// Ensure the board doc exists
|
||||||
|
let doc = _syncServer.getDoc<BoardDoc>(docId);
|
||||||
|
if (!doc) return; // BCRG board not seeded yet
|
||||||
|
|
||||||
|
// Check for duplicate — look for ref:rflows:outcome:{outcomeId}
|
||||||
|
const refTag = `ref:rflows:outcome:${outcomeId}`;
|
||||||
|
for (const t of Object.values(doc.tasks)) {
|
||||||
|
if (t.description?.includes(refTag)) return; // already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
_syncServer.changeDoc<BoardDoc>(docId, `Auto-create task for outcome ${outcomeId}`, (d) => {
|
||||||
|
d.tasks[taskId] = createTaskItem(taskId, space, label, {
|
||||||
|
status: 'DONE',
|
||||||
|
priority: 'MEDIUM',
|
||||||
|
description: `${refTag} — Auto-created from rFlows outcome completion`,
|
||||||
|
labels: ['rflows', 'bcrg'],
|
||||||
|
createdBy: 'did:system:rflows-watcher',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[rflows] Auto-created DONE task for outcome "${outcomeId}" in space "${space}"`);
|
||||||
|
}
|
||||||
|
|
||||||
const routes = new Hono();
|
const routes = new Hono();
|
||||||
|
|
||||||
// ─── Flow Service API proxy ─────────────────────────────
|
// ─── Flow Service API proxy ─────────────────────────────
|
||||||
|
|
@ -517,7 +554,7 @@ function seedTemplateFlows(space: string) {
|
||||||
|
|
||||||
const seedFlow: CanvasFlow = {
|
const seedFlow: CanvasFlow = {
|
||||||
id: canvasFlowId,
|
id: canvasFlowId,
|
||||||
name: 'TBFF Demo Flow',
|
name: 'BCRG Community Flow',
|
||||||
nodes: demoNodes.map((n) => ({ ...n, data: { ...n.data } })),
|
nodes: demoNodes.map((n) => ({ ...n, data: { ...n.data } })),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
|
|
@ -555,6 +592,48 @@ export const flowsModule: RSpaceModule = {
|
||||||
});
|
});
|
||||||
console.log('[rflows] Openfort provider initialized');
|
console.log('[rflows] Openfort provider initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch for completed outcomes in flow docs → auto-create DONE tasks
|
||||||
|
_syncServer.registerWatcher(':flows:data', (docId, doc) => {
|
||||||
|
try {
|
||||||
|
const flowsDoc = doc as FlowsDoc;
|
||||||
|
if (!flowsDoc.canvasFlows) return;
|
||||||
|
// Extract space slug from docId (format: {space}:flows:data)
|
||||||
|
const space = docId.split(':flows:data')[0];
|
||||||
|
if (!space) return;
|
||||||
|
|
||||||
|
for (const flow of Object.values(flowsDoc.canvasFlows)) {
|
||||||
|
if (!flow.nodes) continue;
|
||||||
|
for (const node of flow.nodes) {
|
||||||
|
if (node.type !== 'outcome') continue;
|
||||||
|
const data = node.data as OutcomeNodeData;
|
||||||
|
if (data.status !== 'completed') continue;
|
||||||
|
|
||||||
|
const key = `${space}:${node.id}`;
|
||||||
|
if (_completedOutcomes.has(key)) continue;
|
||||||
|
_completedOutcomes.add(key);
|
||||||
|
|
||||||
|
createTaskForOutcome(space, node.id, data.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pre-populate _completedOutcomes from existing docs to avoid duplicates on restart
|
||||||
|
for (const id of _syncServer.getDocIds()) {
|
||||||
|
if (!id.includes(':flows:data')) continue;
|
||||||
|
const doc = _syncServer.getDoc<FlowsDoc>(id);
|
||||||
|
if (!doc?.canvasFlows) continue;
|
||||||
|
const space = id.split(':flows:data')[0];
|
||||||
|
for (const flow of Object.values(doc.canvasFlows)) {
|
||||||
|
if (!flow.nodes) continue;
|
||||||
|
for (const node of flow.nodes) {
|
||||||
|
if (node.type === 'outcome' && (node.data as OutcomeNodeData).status === 'completed') {
|
||||||
|
_completedOutcomes.add(`${space}:${node.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
standaloneDomain: "rflows.online",
|
standaloneDomain: "rflows.online",
|
||||||
feeds: [
|
feeds: [
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,68 @@ function seedDemoIfEmpty(space: string = 'rspace-dev') {
|
||||||
console.log(`[Tasks] Demo data seeded for "${space}": 1 board, 11 tasks`);
|
console.log(`[Tasks] Demo data seeded for "${space}": 1 board, 11 tasks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed a "BCRG Outcomes" board with 11 tasks matching the BCRG flow outcomes.
|
||||||
|
* Called for demo space on startup and when new spaces are created via seedTemplate.
|
||||||
|
*/
|
||||||
|
function seedBCRGTasksIfEmpty(space: string = 'demo') {
|
||||||
|
if (!_syncServer) return;
|
||||||
|
const boardId = `${space}-bcrg`;
|
||||||
|
const docId = boardDocId(space, boardId);
|
||||||
|
const existing = _syncServer.getDoc<BoardDoc>(docId);
|
||||||
|
if (existing) return; // already seeded
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const doc = Automerge.change(Automerge.init<BoardDoc>(), 'seed BCRG outcomes board', (d) => {
|
||||||
|
d.meta = { module: 'tasks', collection: 'boards', version: 1, spaceSlug: space, createdAt: now };
|
||||||
|
d.board = {
|
||||||
|
id: boardId,
|
||||||
|
name: 'BCRG Outcomes',
|
||||||
|
slug: boardId,
|
||||||
|
description: 'Tasks tracking BCRG community flow outcomes',
|
||||||
|
icon: null,
|
||||||
|
ownerDid: 'did:demo:seed',
|
||||||
|
statuses: ['TODO', 'IN_PROGRESS', 'REVIEW', 'DONE'],
|
||||||
|
labels: ['rflows', 'bcrg'],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
d.tasks = {};
|
||||||
|
|
||||||
|
const bcrgTasks: Array<{ id: string; title: string; status: string; priority: string; description: string; sort: number }> = [
|
||||||
|
// 4 DONE (completed outcomes)
|
||||||
|
{ id: 'alice-comms', title: 'Comms Strategy', status: 'DONE', priority: 'MEDIUM', description: 'ref:rflows:outcome:alice-comms — Community communications and outreach', sort: 0 },
|
||||||
|
{ id: 'carol-ops', title: 'Operations', status: 'DONE', priority: 'HIGH', description: 'ref:rflows:outcome:carol-ops — Day-to-day operational management', sort: 1 },
|
||||||
|
{ id: 'dave-design', title: 'Design System', status: 'DONE', priority: 'HIGH', description: 'ref:rflows:outcome:dave-design — Shared UI/UX design system', sort: 2 },
|
||||||
|
{ id: 'eve-legal', title: 'Legal Framework', status: 'DONE', priority: 'MEDIUM', description: 'ref:rflows:outcome:eve-legal — Legal structure and agreements', sort: 3 },
|
||||||
|
// 5 IN_PROGRESS (partially-funded outcomes)
|
||||||
|
{ id: 'alice-events', title: 'Event Series', status: 'IN_PROGRESS', priority: 'MEDIUM', description: 'ref:rflows:outcome:alice-events — Quarterly community gatherings', sort: 0 },
|
||||||
|
{ id: 'bob-research', title: 'Field Research', status: 'IN_PROGRESS', priority: 'HIGH', description: 'ref:rflows:outcome:bob-research — Participatory action research', sort: 1 },
|
||||||
|
{ id: 'carol-infra', title: 'Infrastructure', status: 'IN_PROGRESS', priority: 'HIGH', description: 'ref:rflows:outcome:carol-infra — Shared infrastructure and hosting', sort: 2 },
|
||||||
|
{ id: 'dave-prototypes', title: 'Prototypes', status: 'IN_PROGRESS', priority: 'MEDIUM', description: 'ref:rflows:outcome:dave-prototypes — Rapid prototyping of new tools', sort: 3 },
|
||||||
|
{ id: 'eve-compliance', title: 'Compliance', status: 'IN_PROGRESS', priority: 'HIGH', description: 'ref:rflows:outcome:eve-compliance — Regulatory compliance and reporting', sort: 4 },
|
||||||
|
// 2 TODO (not-started outcomes)
|
||||||
|
{ id: 'bob-writing', title: 'Publications', status: 'TODO', priority: 'LOW', description: 'ref:rflows:outcome:bob-writing — Research papers and policy briefs', sort: 0 },
|
||||||
|
{ id: 'eve-governance', title: 'Governance Model', status: 'TODO', priority: 'MEDIUM', description: 'ref:rflows:outcome:eve-governance — Governance framework and voting mechanisms', sort: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of bcrgTasks) {
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
d.tasks[taskId] = createTaskItem(taskId, space, t.title, {
|
||||||
|
status: t.status,
|
||||||
|
priority: t.priority,
|
||||||
|
description: t.description,
|
||||||
|
labels: ['rflows', 'bcrg'],
|
||||||
|
sortOrder: t.sort,
|
||||||
|
createdBy: 'did:demo:seed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_syncServer.setDoc(docId, doc);
|
||||||
|
console.log(`[Tasks] BCRG outcomes board seeded for "${space}": 11 tasks`);
|
||||||
|
}
|
||||||
|
|
||||||
// ── API: Spaces (Boards) ──
|
// ── API: Spaces (Boards) ──
|
||||||
|
|
||||||
// GET /api/spaces — list workspaces (boards)
|
// GET /api/spaces — list workspaces (boards)
|
||||||
|
|
@ -416,10 +478,14 @@ export const tasksModule: RSpaceModule = {
|
||||||
routes,
|
routes,
|
||||||
standaloneDomain: "rtasks.online",
|
standaloneDomain: "rtasks.online",
|
||||||
landingPage: renderLanding,
|
landingPage: renderLanding,
|
||||||
seedTemplate: seedDemoIfEmpty,
|
seedTemplate(space: string) {
|
||||||
|
seedDemoIfEmpty(space);
|
||||||
|
seedBCRGTasksIfEmpty(space);
|
||||||
|
},
|
||||||
async onInit(ctx) {
|
async onInit(ctx) {
|
||||||
_syncServer = ctx.syncServer;
|
_syncServer = ctx.syncServer;
|
||||||
seedDemoIfEmpty();
|
seedDemoIfEmpty();
|
||||||
|
seedBCRGTasksIfEmpty('demo');
|
||||||
},
|
},
|
||||||
async onSpaceCreate(ctx: SpaceLifecycleContext) {
|
async onSpaceCreate(ctx: SpaceLifecycleContext) {
|
||||||
if (!_syncServer) return;
|
if (!_syncServer) return;
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ export class SyncServer {
|
||||||
#docSubscribers = new Map<string, Set<string>>(); // docId → Set<peerId>
|
#docSubscribers = new Map<string, Set<string>>(); // docId → Set<peerId>
|
||||||
#participantMode: boolean;
|
#participantMode: boolean;
|
||||||
#relayOnlyDocs = new Set<string>(); // docIds forced to relay mode (encrypted spaces)
|
#relayOnlyDocs = new Set<string>(); // docIds forced to relay mode (encrypted spaces)
|
||||||
|
#watchers: Array<{ prefix: string; cb: (docId: string, doc: Automerge.Doc<any>) => void }> = [];
|
||||||
#onDocChange?: (docId: string, doc: Automerge.Doc<any>) => void;
|
#onDocChange?: (docId: string, doc: Automerge.Doc<any>) => void;
|
||||||
#onRelayBackup?: (docId: string, blob: Uint8Array) => void;
|
#onRelayBackup?: (docId: string, blob: Uint8Array) => void;
|
||||||
#onRelayLoad?: (docId: string) => Promise<Uint8Array | null>;
|
#onRelayLoad?: (docId: string) => Promise<Uint8Array | null>;
|
||||||
|
|
@ -262,6 +263,7 @@ export class SyncServer {
|
||||||
if (this.#onDocChange) {
|
if (this.#onDocChange) {
|
||||||
this.#onDocChange(docId, doc);
|
this.#onDocChange(docId, doc);
|
||||||
}
|
}
|
||||||
|
for (const w of this.#watchers) if (docId.includes(w.prefix)) w.cb(docId, doc);
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +285,13 @@ export class SyncServer {
|
||||||
return Array.from(this.#docs.keys());
|
return Array.from(this.#docs.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a callback to fire when any doc whose ID contains `prefix` changes.
|
||||||
|
*/
|
||||||
|
registerWatcher(prefix: string, cb: (docId: string, doc: Automerge.Doc<any>) => void): void {
|
||||||
|
this.#watchers.push({ prefix, cb });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of connected peer IDs.
|
* Get list of connected peer IDs.
|
||||||
*/
|
*/
|
||||||
|
|
@ -381,6 +390,7 @@ export class SyncServer {
|
||||||
if (this.#onDocChange) {
|
if (this.#onDocChange) {
|
||||||
this.#onDocChange(docId, newDoc);
|
this.#onDocChange(docId, newDoc);
|
||||||
}
|
}
|
||||||
|
for (const w of this.#watchers) if (docId.includes(w.prefix)) w.cb(docId, newDoc);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Relay mode: forward sync message to all other subscribers
|
// Relay mode: forward sync message to all other subscribers
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue