/* ===========================================================================
launch-integrations.jsx — Two-way project-manager sync (ClickUp · Asana ·
Trello · Basecamp). Mounts inside Settings → Integrations and exposes a
per-task send control. Reads/writes the `pmIntegrations` fold seam.
Components → window: PMIntegrations, TaskSyncControl.
=========================================================================== */
const { Section: PMSection, act: pmAct, ENGINE_LABELS: PM_ENG, ENGINE_COLORS: PM_ENGC } = window;
const RECON_TYPES = {
conflict: { label: "Conflict", color: "#E0B638" },
inbound: { label: "New in tool", color: "#10B5C7" },
outbound: { label: "Not pushed", color: "#7A4FBF" },
deleted: { label: "Removed in tool", color: "#F25BA7" },
unmapped: { label: "Unmapped", color: "var(--pvt-text-dim)" },
};
const PM_DIRS = [ { id: "push", label: "Push" }, { id: "two-way", label: "Two-way" }, { id: "pull", label: "Pull" } ];
const PM_FREQ = { realtime: "Real-time", "15m": "Every 15 min", hourly: "Hourly", manual: "Manual" };
const PM_MAP_ROWS = [
{ key: "root", lcc: "Workspace root", hint: "The launch maps to one…" },
{ key: "engines", lcc: "Engines", hint: "Six operating engines" },
{ key: "tasks", lcc: "Tasks", hint: "This-week task board" },
{ key: "pipelines", lcc: "Pipelines", hint: "Inquiry → Advanced" },
{ key: "timeline", lcc: "Timeline", hint: "Production weeks" },
];
const PM_SCOPE_ROWS = [
{ key: "tasks", label: "Tasks & status", sub: "Two-way task + status sync" },
{ key: "engines", label: "Engines", sub: "Group tasks by engine" },
{ key: "pipelines", label: "Pipelines", sub: "Stage board cards" },
{ key: "timeline", label: "Timeline", sub: "Milestone schedule" },
];
/* Brand monogram tile (no external logos). */
function PMMark({ pm, size = 38 }) {
return (
{pm.name[0]}
);
}
function PMStatusChip({ pm }) {
const tone = pm.connected ? "#10B5C7" : "var(--pvt-text-dim)";
return (
{pm.connected ? "Connected" : "Not connected"}
);
}
/* Small segmented control. */
function PMSeg({ value, options, onChange }) {
return (
{options.map((o) => {
const on = o.id === value;
return (
onChange(o.id)} style={{ border: 0, cursor: "pointer", borderRadius: 999, padding: "6px 14px", fontSize: 12, fontWeight: 700, fontFamily: "var(--pvt-font-body)",
background: on ? "var(--role-accent)" : "transparent", color: on ? "#fff" : "var(--pvt-muted)" }}>{o.label}
);
})}
);
}
function PMToggle({ on, onClick }) {
return (
);
}
/* ---------------------------------------------------------------------------
Configuration slide-over for one connector.
--------------------------------------------------------------------------- */
function PMConfigDrawer({ pm, onClose, patch }) {
React.useEffect(() => {
const onKey = (e) => { if (e.key === "Escape") onClose(); };
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, []);
if (!pm) return null;
const conflictOpts = { lcc: "LCC wins", platform: pm.name + " wins", recent: "Most recent wins" };
const connect = () => patch(pm.id, pm.connected
? { connected: false, account: "", who: "", lastSync: "—" }
: { connected: true, account: "ARGT 2026", who: "kam@argt.com", lastSync: "just now", log: [{ at: "just now", dir: "out", text: "Connected " + pm.name + " · initial sync queued" }, ...(pm.log || [])] });
const syncNow = () => { patch(pm.id, { lastSync: "just now", log: [{ at: "just now", dir: "out", text: "Manual sync — pushed 8 tasks, pulled 2 updates" }, ...(pm.log || [])] }); if (window.argtPing) window.argtPing(pm.name + " synced just now"); };
const setMap = (k, v) => patch(pm.id, { mapping: { ...pm.mapping, [k]: v } });
const setScope = (k) => patch(pm.id, { scope: { ...pm.scope, [k]: !pm.scope[k] } });
return (
e.stopPropagation()} style={{ width: "min(500px, 96vw)", height: "100%", background: "var(--pvt-surface-raised)", borderLeft: "1px solid var(--pvt-border-light)", boxShadow: "-32px 0 80px rgba(0,0,0,0.5)", display: "flex", flexDirection: "column", animation: "lccDrawerIn 220ms ease" }}>
{/* header */}
{pm.connected ? "Disconnect" : "Connect"}
{pm.connected &&
{pm.account} · {pm.who} · last sync {pm.lastSync}
}
{/* body */}
{/* direction */}
Sync direction
patch(pm.id, { direction: v })} />
{pm.direction === "two-way" ? "Changes flow both ways. Edits here push out; edits in " + pm.name + " pull back in." : pm.direction === "push" ? "LCC is the source of truth — changes push out to " + pm.name + " only." : pm.name + " is the source of truth — changes pull into LCC only."}
{/* what syncs */}
What syncs
{PM_SCOPE_ROWS.map((r) => (
))}
{/* mapping — group LCC surfaces onto the platform's native hierarchy */}
Mapping · how LCC groups into {pm.name}
{PM_MAP_ROWS.map((row) => (
→
setMap(row.key, e.target.value)} style={{ background: "var(--pvt-surface)", border: "1px solid var(--pvt-border-light)", borderRadius: 8, color: "var(--pvt-text)", padding: "7px 9px", fontSize: 12, fontFamily: "var(--pvt-font-body)", outline: "none", width: "100%" }}>
{pm.containers.map((c) => {c} )}
))}
{/* cadence + conflict */}
Frequency
patch(pm.id, { frequency: e.target.value })} style={pmSelStyle()}>
{Object.entries(PM_FREQ).map(([k, v]) => {v} )}
On conflict
patch(pm.id, { conflict: e.target.value })} style={pmSelStyle()}>
{Object.entries(conflictOpts).map(([k, v]) => {v} )}
{/* activity */}
{(pm.log || []).map((l, i) => (
{l.dir === "in" ? "↓" : "↑"}
))}
{(!pm.log || pm.log.length === 0) &&
No sync activity yet.
}
);
}
function pmSelStyle() { return { background: "var(--pvt-surface-mid)", border: "1px solid var(--pvt-border-light)", borderRadius: "var(--r-md)", color: "var(--pvt-text)", padding: "10px 12px", fontSize: 13, fontFamily: "var(--pvt-font-body)", outline: "none", width: "100%" }; }
/* ---------------------------------------------------------------------------
Manager — grid of connectors, mounted in Settings → Integrations.
--------------------------------------------------------------------------- */
function PMIntegrations() {
const [list, setList] = window.useDB("pmIntegrations");
const [recon] = window.useDB("pmReconcile");
const [open, setOpen] = React.useState(null);
const patch = (id, fields) => setList((xs) => xs.map((p) => p.id === id ? { ...p, ...fields } : p));
const reconBy = (k) => recon.filter((r) => r.platform === k).length;
const connectedCount = list.filter((p) => p.connected).length;
const syncAll = () => { setList((xs) => xs.map((p) => p.connected ? { ...p, lastSync: "just now" } : p)); if (window.argtPing) window.argtPing("Synced all connected tools"); };
const active = open ? list.find((p) => p.id === open) : null;
return (
{connectedCount}/{list.length} CONNECTED · 2-WAY Sync all now }>
{list.map((pm) => (
setOpen(pm.id)} title={"Configure " + pm.name}
style={{ background: "var(--pvt-surface-mid)", border: "1px solid var(--pvt-border-light)", borderRadius: "var(--r-md)", padding: 16, display: "flex", flexDirection: "column", gap: 12, cursor: "pointer", transition: "border-color 140ms, transform 140ms" }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = pm.color; e.currentTarget.style.transform = "translateY(-2px)"; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--pvt-border-light)"; e.currentTarget.style.transform = "none"; }}>
{reconBy(pm.key) > 0 &&
{reconBy(pm.key)} to reconcile}
{pm.connected ? "Synced " + pm.lastSync : "Set up sync"}
{pm.connected ? "Configure" : "Connect"}
))}
setOpen(null)} patch={patch} />
);
}
/* ---------------------------------------------------------------------------
Per-task send/sync control — drops into the task edit modal.
--------------------------------------------------------------------------- */
function TaskSyncControl({ task, setTask }) {
const [list] = window.useDB("pmIntegrations");
const connected = list.filter((p) => p.connected);
const sync = task && task.sync;
const sendTo = (pm) => { setTask((t) => ({ ...t, sync: { key: pm.key, name: pm.name, color: pm.color, mapped: pm.mapping.tasks, at: "just now" } })); if (window.argtPing) window.argtPing("Task sent to " + pm.name + " as a " + pm.mapping.tasks); };
const openExt = () => { if (window.argtPing) window.argtPing("Opening task in " + sync.name); };
const unlink = () => setTask((t) => { const n = { ...t }; delete n.sync; return n; });
return (
Project-manager sync
{connected.length === 0 ? (
No tools connected. Connect one in Settings → Integrations.
) : sync ? (
Synced to {sync.name} {sync.mapped ? · {sync.mapped} : null}
Open ↗
Unlink
) : (
Send to
{connected.map((pm) => (
sendTo(pm)} className="pill" style={{ display: "inline-flex", alignItems: "center", gap: 7 }}>
{pm.name}
))}
)}
);
}
/* ---------------------------------------------------------------------------
Reconciliation queue — divergences a two-way sync can't auto-resolve.
Lives on the integrations page, below the connector grid.
--------------------------------------------------------------------------- */
function ReconRow({ item, pm, onResolve }) {
const meta = RECON_TYPES[item.type] || RECON_TYPES.unmapped;
const [mapEngine, setMapEngine] = React.useState("content");
const engLabel = item.engine ? (PM_ENG[item.engine] || item.engine) : null;
const name = pm ? pm.name : item.platform;
const color = pm ? pm.color : "var(--pvt-text-dim)";
const actions = {
conflict: [ { l: "Keep LCC", k: "keep-lcc" }, { l: "Keep " + name, k: "keep-ext" } ],
inbound: [ { l: "Import to LCC", k: "import", primary: true }, { l: "Ignore", k: "ignore", dim: true } ],
outbound: [ { l: "Push to " + name, k: "push", primary: true }, { l: "Skip", k: "skip", dim: true } ],
deleted: [ { l: "Delete in LCC", k: "delete", danger: true }, { l: "Restore in " + name, k: "restore" } ],
unmapped: [ { l: "Map + import", k: "map", primary: true }, { l: "Ignore", k: "ignore", dim: true } ],
}[item.type] || [];
return (
{item.taskTitle}
{meta.label}
{engLabel && {engLabel} }
{name} · {item.detected}
{item.type === "conflict" && item.fields ? (
Field
LCC
{name}
{item.fields.map((f, i) => (
{f.label}
{f.lcc}
{f.ext}
))}
) : (
{item.summary}
)}
{item.type === "unmapped" && (
setMapEngine(e.target.value)} style={{ background: "var(--pvt-surface)", border: "1px solid var(--pvt-border-light)", borderRadius: 8, color: "var(--pvt-text)", padding: "7px 9px", fontSize: 12, fontFamily: "var(--pvt-font-body)", outline: "none" }}>
{Object.keys(PM_ENG).map((k) => {PM_ENG[k]} )}
)}
{actions.map((a) => (
onResolve(item, a.k, mapEngine)}
style={a.danger ? { color: "#F25BA7", borderColor: "rgba(216,40,132,0.4)" } : a.dim ? { color: "var(--pvt-text-dim)" } : {}}>{a.l}
))}
);
}
function reconCell() { return { background: "var(--pvt-surface)", padding: "8px 12px", fontSize: 12 }; }
function PMReconcile() {
const [recon, setRecon] = window.useDB("pmReconcile");
const [list, setList] = window.useDB("pmIntegrations");
const [filter, setFilter] = React.useState(null);
const pmFor = (k) => list.find((p) => p.key === k);
const shown = filter ? recon.filter((r) => r.platform === filter) : recon;
const connected = list.filter((p) => p.connected);
const resolve = (item, choice, mapEngine) => {
const pm = pmFor(item.platform);
const name = pm ? pm.name : item.platform;
const msg = {
"keep-lcc": "Kept LCC version of '" + item.taskTitle + "' · pushed to " + name,
"keep-ext": "Accepted " + name + " version of '" + item.taskTitle + "'",
"import": "Imported '" + item.taskTitle + "' from " + name,
"ignore": "Ignored '" + item.taskTitle + "'",
"push": "Pushed '" + item.taskTitle + "' to " + name,
"skip": "Skipped '" + item.taskTitle + "'",
"delete": "Deleted '" + item.taskTitle + "' in LCC",
"restore": "Restored '" + item.taskTitle + "' in " + name,
"map": "Mapped '" + item.taskTitle + "' to " + (PM_ENG[mapEngine] || mapEngine) + " and imported",
}[choice] || "Resolved";
setRecon((xs) => xs.filter((r) => r.id !== item.id));
if (pm) {
const dir = (choice === "keep-lcc" || choice === "push" || choice === "restore") ? "out" : "in";
setList((xs) => xs.map((p) => p.id === pm.id ? { ...p, log: [{ at: "just now", dir, text: msg }, ...(p.log || [])] } : p));
}
if (window.argtPing) window.argtPing(msg);
};
return (
0
? {recon.length} NEED REVIEW
: ALL IN SYNC }>
{recon.length > 0 && (
setFilter(null)}>All {recon.length}
{connected.map((pm) => {
const n = recon.filter((r) => r.platform === pm.key).length;
if (!n) return null;
return setFilter(pm.key)}> {pm.name} {n} ;
})}
)}
{shown.length > 0 ? (
{shown.map((item) => )}
) : (
✓
Everything is in sync
No conflicts or pending changes across your connected tools.
)}
);
}
Object.assign(window, { PMIntegrations, TaskSyncControl });