/** * rTime applet definitions — Commitment Meter + Weaving Coverage. */ import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; import { getModuleApiBase } from "../../shared/url-helpers"; const commitmentMeter: AppletDefinition = { id: "commitment-meter", label: "Commitment Meter", icon: "⏳", accentColor: "#7c3aed", ports: [ { name: "pool-in", type: "json", direction: "input" }, { name: "committed-out", type: "number", direction: "output" }, ], renderCompact(data: AppletLiveData): string { const { snapshot } = data; const committed = (snapshot.committed as number) || 0; const capacity = (snapshot.capacity as number) || 1; const pct = Math.min(100, Math.round((committed / capacity) * 100)); const label = (snapshot.poolName as string) || "Pool"; return `
${label}
${pct}%
${committed}/${capacity} hrs
`; }, onInputReceived(portName, value, ctx) { if (portName === "pool-in" && value && typeof value === "object") { const pool = value as Record; ctx.emitOutput("committed-out", Number(pool.committed) || 0); } }, }; // ── Weaving Coverage ── interface WeavePlacedTask { id: string; title: string; status: string; needs: Record; } interface WeaveData { placedTasks?: WeavePlacedTask[]; connections?: Array<{ commitmentId: string; taskId: string; skill: string; hours: number }>; } const SKILL_COLORS: Record = { facilitation: '#8b5cf6', design: '#ec4899', tech: '#3b82f6', outreach: '#10b981', logistics: '#f59e0b', }; const weavingCoverage: AppletDefinition = { id: "weaving-coverage", label: "Weaving Coverage", icon: "🧶", accentColor: "#7c3aed", ports: [ { name: "pool-in", type: "json", direction: "input" }, { name: "coverage-out", type: "json", direction: "output" }, ], async fetchLiveData(space: string): Promise> { try { const resp = await fetch(`${getModuleApiBase("rtime")}/api/weave`); if (!resp.ok) return {}; const data: WeaveData = await resp.json(); return buildCoverageSnapshot(data); } catch { return {}; } }, renderCompact(data: AppletLiveData): string { const { snapshot } = data; const tasks = (snapshot.tasks as Array<{ id: string; title: string; ready: boolean; partial: boolean; needs: Record; fulfilled: Record }>) || []; const summary = (snapshot.summary as { total: number; ready: number; partial: number }) || { total: 0, ready: 0, partial: 0 }; if (tasks.length === 0) { return `
No tasks on weaving canvas
`; } let html = `
${summary.ready}/${summary.total} tasks resourced
`; for (const t of tasks.slice(0, 5)) { const skills = Object.keys(t.needs); let bars = ''; for (const sk of skills) { const need = t.needs[sk] || 1; const got = (t.fulfilled?.[sk]) || 0; const pct = Math.min(100, Math.round((got / need) * 100)); const color = SKILL_COLORS[sk] || '#6b7280'; bars += `
${sk}
${got}/${need}
`; } const statusColor = t.ready ? '#22c55e' : t.partial ? '#f59e0b' : '#64748b'; html += `
${t.title}
${bars}
`; } if (tasks.length > 5) { html += `
+${tasks.length - 5} more
`; } return html; }, onInputReceived(portName, value, ctx) { if (portName === "pool-in") { // Pool data received — re-fetch would be ideal but for now just trigger output // The fetchLiveData polling handles refresh } }, }; function buildCoverageSnapshot(data: WeaveData): Record { const placed = data.placedTasks || []; const connections = data.connections || []; // Build fulfillment map: taskId → skill → hours fulfilled const fulfillment = new Map>(); for (const conn of connections) { if (!fulfillment.has(conn.taskId)) fulfillment.set(conn.taskId, {}); const tf = fulfillment.get(conn.taskId)!; tf[conn.skill] = (tf[conn.skill] || 0) + conn.hours; } const tasks = placed.map(t => { const needs = t.needs || {}; const fulfilled = fulfillment.get(t.id) || {}; const skills = Object.keys(needs); const allFilled = skills.length > 0 && skills.every(sk => (fulfilled[sk] || 0) >= needs[sk]); const someFilled = skills.some(sk => (fulfilled[sk] || 0) > 0); return { id: t.id, title: t.title, needs, fulfilled, ready: allFilled, partial: !allFilled && someFilled, }; }); const ready = tasks.filter(t => t.ready).length; const partial = tasks.filter(t => t.partial).length; return { tasks, summary: { total: tasks.length, ready, partial }, }; } export const timeApplets: AppletDefinition[] = [commitmentMeter, weavingCoverage];