165 lines
5.9 KiB
TypeScript
165 lines
5.9 KiB
TypeScript
/**
|
|
* 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 `
|
|
<div style="text-align:center">
|
|
<div style="font-size:13px;font-weight:600;margin-bottom:6px">${label}</div>
|
|
<div style="font-size:24px;font-weight:700;color:#e2e8f0">${pct}%</div>
|
|
<div style="font-size:10px;color:#94a3b8;margin:4px 0 8px">${committed}/${capacity} hrs</div>
|
|
<div style="background:#334155;border-radius:3px;height:6px;overflow:hidden">
|
|
<div style="background:#7c3aed;width:${pct}%;height:100%;border-radius:3px;transition:width 0.3s"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
onInputReceived(portName, value, ctx) {
|
|
if (portName === "pool-in" && value && typeof value === "object") {
|
|
const pool = value as Record<string, unknown>;
|
|
ctx.emitOutput("committed-out", Number(pool.committed) || 0);
|
|
}
|
|
},
|
|
};
|
|
|
|
// ── Weaving Coverage ──
|
|
|
|
interface WeavePlacedTask {
|
|
id: string;
|
|
title: string;
|
|
status: string;
|
|
needs: Record<string, number>;
|
|
}
|
|
|
|
interface WeaveData {
|
|
placedTasks?: WeavePlacedTask[];
|
|
connections?: Array<{ commitmentId: string; taskId: string; skill: string; hours: number }>;
|
|
}
|
|
|
|
const SKILL_COLORS: Record<string, string> = {
|
|
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<Record<string, unknown>> {
|
|
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<string, number>; fulfilled: Record<string, number> }>) || [];
|
|
const summary = (snapshot.summary as { total: number; ready: number; partial: number }) || { total: 0, ready: 0, partial: 0 };
|
|
|
|
if (tasks.length === 0) {
|
|
return `<div style="text-align:center;color:#64748b;font-style:italic;padding:16px 0">No tasks on weaving canvas</div>`;
|
|
}
|
|
|
|
let html = `<div style="font-size:10px;color:#94a3b8;margin-bottom:6px">${summary.ready}/${summary.total} tasks resourced</div>`;
|
|
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 += `<div style="display:flex;align-items:center;gap:4px;margin:1px 0">
|
|
<span style="width:50px;font-size:8px;color:${color};text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${sk}</span>
|
|
<div style="flex:1;background:#1e293b;border-radius:2px;height:4px;overflow:hidden">
|
|
<div style="background:${color};width:${pct}%;height:100%;border-radius:2px"></div>
|
|
</div>
|
|
<span style="font-size:8px;color:#64748b;width:24px">${got}/${need}</span>
|
|
</div>`;
|
|
}
|
|
const statusColor = t.ready ? '#22c55e' : t.partial ? '#f59e0b' : '#64748b';
|
|
html += `<div style="margin-bottom:6px;padding:4px 6px;background:#0f172a;border-radius:4px;border-left:2px solid ${statusColor}">
|
|
<div style="font-size:10px;font-weight:600;color:#e2e8f0;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${t.title}</div>
|
|
${bars}
|
|
</div>`;
|
|
}
|
|
if (tasks.length > 5) {
|
|
html += `<div style="font-size:9px;color:#64748b;text-align:center">+${tasks.length - 5} more</div>`;
|
|
}
|
|
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<string, unknown> {
|
|
const placed = data.placedTasks || [];
|
|
const connections = data.connections || [];
|
|
|
|
// Build fulfillment map: taskId → skill → hours fulfilled
|
|
const fulfillment = new Map<string, Record<string, number>>();
|
|
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];
|