/**
* 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 += ``;
}
const statusColor = t.ready ? '#22c55e' : t.partial ? '#f59e0b' : '#64748b';
html += ``;
}
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];