@growthub/cli 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/.env.example +36 -20
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/README.md +2 -0
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/app/api/workspace/route.js +15 -11
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/app/globals.css +134 -2
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/app/page.jsx +143 -149
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/app/settings/integrations/page.jsx +67 -96
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/jsconfig.json +8 -0
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/lib/adapters/integrations/index.js +21 -1
- package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/lib/domain/portal.js +146 -12
- package/assets/worker-kits/growthub-agency-portal-starter-v1/docs/adapter-contracts.md +7 -0
- package/assets/worker-kits/growthub-agency-portal-starter-v1/studio/src/App.jsx +34 -37
- package/assets/worker-kits/growthub-agency-portal-starter-v1/studio/src/app.css +2 -1
- package/dist/index.js +1212 -2352
- package/package.json +1 -1
|
@@ -1,134 +1,105 @@
|
|
|
1
|
+
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
1
2
|
import { describeIntegrationAdapter, listAgencyPortalIntegrations } from "@/lib/adapters/integrations";
|
|
2
|
-
import { portalCapabilities } from "@/lib/domain/portal";
|
|
3
3
|
import { groupIntegrationsByLane } from "@/lib/domain/integrations";
|
|
4
|
+
import { buildPortalWorkspace } from "@/lib/domain/portal";
|
|
5
|
+
import { describeAuthAdapter } from "@/lib/adapters/auth";
|
|
6
|
+
import { describePaymentAdapter } from "@/lib/adapters/payments";
|
|
7
|
+
import { describePersistenceAdapter } from "@/lib/adapters/persistence";
|
|
4
8
|
import Link from "next/link";
|
|
5
|
-
const nav = [
|
|
6
|
-
...portalCapabilities.map((item) => ({ href: `/#${item.id}`, label: item.label })),
|
|
7
|
-
{ href: "/settings/integrations", label: "Integrations" }
|
|
8
|
-
];
|
|
9
9
|
async function IntegrationsSettingsPage() {
|
|
10
|
+
const config = readAdapterConfig();
|
|
10
11
|
const adapter = describeIntegrationAdapter();
|
|
11
12
|
const grouped = groupIntegrationsByLane(await listAgencyPortalIntegrations());
|
|
12
|
-
const
|
|
13
|
-
|
|
13
|
+
const workspace = buildPortalWorkspace({
|
|
14
|
+
config,
|
|
15
|
+
integrations: grouped,
|
|
16
|
+
adapters: {
|
|
17
|
+
persistence: describePersistenceAdapter(),
|
|
18
|
+
auth: describeAuthAdapter(),
|
|
19
|
+
payments: describePaymentAdapter(),
|
|
20
|
+
integrations: adapter
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const rows = [
|
|
24
|
+
...grouped.dataSources.map((item) => ({ ...item, primitiveGroup: "data-source" })),
|
|
25
|
+
...grouped.workspaceIntegrations.map((item) => ({ ...item, primitiveGroup: "workspace-integration" }))
|
|
26
|
+
];
|
|
14
27
|
return <main className="shell">
|
|
15
28
|
<aside className="sidebar">
|
|
16
29
|
<div className="brand">
|
|
17
|
-
<span className="brand-mark">
|
|
18
|
-
<span>
|
|
30
|
+
<span className="brand-mark">{workspace.identity.mark}</span>
|
|
31
|
+
<span>{workspace.identity.label}</span>
|
|
19
32
|
</div>
|
|
20
33
|
<nav className="nav">
|
|
21
|
-
{
|
|
34
|
+
{workspace.navigation.map((item) => <Link className={item.href === "/settings/integrations" ? "active" : ""} key={item.href} href={item.href.startsWith("#") ? `/${item.href}` : item.href}>
|
|
22
35
|
{item.label}
|
|
23
36
|
</Link>)}
|
|
24
37
|
</nav>
|
|
25
38
|
<div className="sidebar-footer">
|
|
26
39
|
<span className="status-dot" />
|
|
27
|
-
{
|
|
40
|
+
{adapter.authority}
|
|
28
41
|
</div>
|
|
29
42
|
</aside>
|
|
30
|
-
|
|
31
43
|
<section className="main">
|
|
32
44
|
<div className="utility-bar">
|
|
33
45
|
<div>
|
|
34
|
-
<strong>
|
|
35
|
-
<span>
|
|
46
|
+
<strong>{adapter.label}</strong>
|
|
47
|
+
<span>{workspace.identity.primitiveContract}</span>
|
|
36
48
|
</div>
|
|
37
49
|
<div className="utility-actions">
|
|
50
|
+
<span className="pill">{adapter.id}</span>
|
|
38
51
|
<span className="pill">{adapter.authority}</span>
|
|
39
|
-
<span className="pill">{adapter.source}</span>
|
|
40
52
|
</div>
|
|
41
53
|
</div>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<section className="setup-grid" aria-label="Integration setup paths">
|
|
54
|
-
<article className="setup-card">
|
|
55
|
-
<span>01</span>
|
|
56
|
-
<strong>Growthub Bridge</strong>
|
|
57
|
-
<p>Uses the authenticated Growthub account to resolve already-connected MCP accounts for this portal.</p>
|
|
58
|
-
<code>GROWTHUB_BRIDGE_ACCESS_TOKEN</code>
|
|
54
|
+
<section className="primitive-grid summary" aria-label="Integration adapter primitives">
|
|
55
|
+
<article className="primitive-card">
|
|
56
|
+
<div className="primitive-card-top">
|
|
57
|
+
<p className="card-label">Authority</p>
|
|
58
|
+
<span className="status runtime-derived">{adapter.authority}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<strong>{adapter.id}</strong>
|
|
61
|
+
<div className="primitive-meta">
|
|
62
|
+
{adapter.requiredEnv.map((key) => <code key={key}>{key}</code>)}
|
|
63
|
+
</div>
|
|
59
64
|
</article>
|
|
60
|
-
<article className="
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
<article className="primitive-card">
|
|
66
|
+
<div className="primitive-card-top">
|
|
67
|
+
<p className="card-label">Data-source primitives</p>
|
|
68
|
+
<span className="status runtime-derived">{grouped.dataSources.length}</span>
|
|
69
|
+
</div>
|
|
70
|
+
<strong>{grouped.dataSources.filter((item) => item.isConnected).length}/{grouped.dataSources.length}</strong>
|
|
71
|
+
<div className="primitive-meta"><span>{config.reportingAdapter || "reporting-adapter"}</span></div>
|
|
65
72
|
</article>
|
|
66
|
-
<article className="
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
<article className="primitive-card">
|
|
74
|
+
<div className="primitive-card-top">
|
|
75
|
+
<p className="card-label">Workspace primitives</p>
|
|
76
|
+
<span className="status runtime-derived">{grouped.workspaceIntegrations.length}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<strong>{grouped.workspaceIntegrations.filter((item) => item.isConnected).length}/{grouped.workspaceIntegrations.length}</strong>
|
|
79
|
+
<div className="primitive-meta"><span>{config.integrationAdapter}</span></div>
|
|
71
80
|
</article>
|
|
72
81
|
</section>
|
|
73
|
-
|
|
74
82
|
<section className="integration-board">
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
items={grouped.workspaceIntegrations}
|
|
92
|
-
/>
|
|
83
|
+
{rows.map((item) => <article className="integration-card" key={item.id}>
|
|
84
|
+
<div className="integration-card-top">
|
|
85
|
+
<div className="provider-mark">{item.icon || item.label.slice(0, 1)}</div>
|
|
86
|
+
<div>
|
|
87
|
+
<strong>{item.label}</strong>
|
|
88
|
+
<p>{item.provider} / {item.objectType} / {item.primitiveGroup}</p>
|
|
89
|
+
</div>
|
|
90
|
+
<span className={`status ${item.status}`}>{item.status}</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="integration-card-meta">
|
|
93
|
+
<span>{item.authPath}</span>
|
|
94
|
+
<span>{item.setupMode}</span>
|
|
95
|
+
<span>{item.authType}</span>
|
|
96
|
+
{item.secretEnvName ? <span>{item.secretEnvName}</span> : null}
|
|
97
|
+
</div>
|
|
98
|
+
</article>)}
|
|
93
99
|
</section>
|
|
94
100
|
</section>
|
|
95
101
|
</main>;
|
|
96
102
|
}
|
|
97
|
-
function IntegrationPanel({
|
|
98
|
-
title,
|
|
99
|
-
intro,
|
|
100
|
-
items
|
|
101
|
-
}) {
|
|
102
|
-
const connected = items.filter((item) => item.isConnected).length;
|
|
103
|
-
return <article className="integration-section">
|
|
104
|
-
<div className="section-heading">
|
|
105
|
-
<div>
|
|
106
|
-
<h2>{title}</h2>
|
|
107
|
-
<p className="panel-copy">{intro}</p>
|
|
108
|
-
</div>
|
|
109
|
-
<span className="badge">{connected}/{items.length} connected</span>
|
|
110
|
-
</div>
|
|
111
|
-
<div className="integration-list">
|
|
112
|
-
{items.map((item) => <article className="integration-card" key={item.id}>
|
|
113
|
-
<div className="integration-card-top">
|
|
114
|
-
<div className="provider-mark">{item.label.slice(0, 1)}</div>
|
|
115
|
-
<div>
|
|
116
|
-
<strong>{item.label}</strong>
|
|
117
|
-
<p>{item.description}</p>
|
|
118
|
-
</div>
|
|
119
|
-
<span className={`status ${item.status}`}>{item.status}</span>
|
|
120
|
-
</div>
|
|
121
|
-
<div className="integration-card-meta">
|
|
122
|
-
<span>{item.provider}</span>
|
|
123
|
-
<span>{item.objectType}</span>
|
|
124
|
-
<span>{item.authPath}</span>
|
|
125
|
-
<span>{item.setupMode}</span>
|
|
126
|
-
{item.secretEnvName ? <span>{item.secretEnvName}</span> : null}
|
|
127
|
-
</div>
|
|
128
|
-
</article>)}
|
|
129
|
-
</div>
|
|
130
|
-
</article>;
|
|
131
|
-
}
|
|
132
103
|
export {
|
|
133
104
|
IntegrationsSettingsPage as default
|
|
134
105
|
};
|
|
@@ -57,7 +57,27 @@ async function listAgencyPortalIntegrations() {
|
|
|
57
57
|
return agencyPortalIntegrationCatalog;
|
|
58
58
|
}
|
|
59
59
|
const payload = await response.json();
|
|
60
|
-
|
|
60
|
+
const merged = mergeBridgeRows(normalizeGrowthubBridgePayload(payload));
|
|
61
|
+
return applyApiKeyOverlays(merged, config);
|
|
62
|
+
}
|
|
63
|
+
function applyApiKeyOverlays(integrations, config) {
|
|
64
|
+
if (!config.dataSources.hasWindsorApiKey) return integrations;
|
|
65
|
+
const windsorOverlay = {
|
|
66
|
+
status: "connected",
|
|
67
|
+
isConnected: true,
|
|
68
|
+
isActive: true,
|
|
69
|
+
authPath: "byo-api-key",
|
|
70
|
+
setupMode: "bring-your-own-key",
|
|
71
|
+
authType: "api_token",
|
|
72
|
+
category: "api_key",
|
|
73
|
+
secretEnvName: "WINDSOR_API_KEY",
|
|
74
|
+
connectionMetadata: { source: "workspace-env", secretEnvName: "WINDSOR_API_KEY" }
|
|
75
|
+
};
|
|
76
|
+
return integrations.map((item) => {
|
|
77
|
+
if (item.provider === "windsor-ai") return { ...item, ...windsorOverlay };
|
|
78
|
+
if (item.provider === "google-sheets") return { ...item, ...windsorOverlay, secretEnvName: undefined, connectionMetadata: { source: "windsor-blended-data" } };
|
|
79
|
+
return item;
|
|
80
|
+
});
|
|
61
81
|
}
|
|
62
82
|
function readBringYourOwnRows() {
|
|
63
83
|
const raw = process.env.AGENCY_PORTAL_BYO_CONNECTIONS_JSON;
|
package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/lib/domain/portal.js
CHANGED
|
@@ -1,16 +1,150 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
const fieldTypes = [
|
|
2
|
+
"text",
|
|
3
|
+
"long_text",
|
|
4
|
+
"number",
|
|
5
|
+
"currency",
|
|
6
|
+
"percentage",
|
|
7
|
+
"date",
|
|
8
|
+
"datetime",
|
|
9
|
+
"select",
|
|
10
|
+
"multi_select",
|
|
11
|
+
"boolean",
|
|
12
|
+
"relation",
|
|
13
|
+
"multi_relation",
|
|
14
|
+
"formula",
|
|
15
|
+
"rollup",
|
|
16
|
+
"url",
|
|
17
|
+
"email",
|
|
18
|
+
"phone",
|
|
19
|
+
"json",
|
|
20
|
+
"file",
|
|
21
|
+
"rating",
|
|
22
|
+
"user"
|
|
13
23
|
];
|
|
24
|
+
const capabilitySpecs = [
|
|
25
|
+
tab("dashboard", "Dashboard", "workspace-summary", ["agencyAccount", "client", "task"], ["pipelineValue", "clientHealth", "openTasks", "reportingReadiness"], ["data", "auth", "integrations"]),
|
|
26
|
+
tab("clients", "Clients", "client-records", ["client", "contact", "clientKpi"], ["activeClients", "retainerValue", "healthScore", "renewalWindow"], ["data", "auth"]),
|
|
27
|
+
tab("pipeline", "Pipeline", "opportunity-flow", ["opportunity", "client", "contact"], ["stageValue", "winRate", "nextFollowUp", "ownerLoad"], ["data", "integrations"]),
|
|
28
|
+
tab("content", "Content", "publishing-workflow", ["contentPlan", "client", "task"], ["scheduledPosts", "channelMix", "approvalQueue", "overdueAssets"], ["data", "integrations"]),
|
|
29
|
+
tab("tasks", "Tasks", "execution-queue", ["task", "client", "workflowRun"], ["openTasks", "slaRisk", "ownerLoad", "recurringTemplates"], ["data", "integrations"]),
|
|
30
|
+
tab("finance", "Finance", "billing-control", ["invoice", "expense", "client"], ["mrr", "outstandingBalance", "margin", "paymentState"], ["data", "payments"]),
|
|
31
|
+
tab("reports", "Reports", "reporting-adapter", ["report", "clientKpi", "dataConnector"], ["reportQueue", "connectedSources", "periodDelta", "deliveryState"], ["reporting", "integrations"]),
|
|
32
|
+
tab("metrics", "Metrics", "agency-health", ["agencyMetric", "clientKpi", "opportunity"], ["mrrTrend", "churnRisk", "pipelineCoverage", "capacity"], ["data", "reporting"]),
|
|
33
|
+
tab("client-results", "Client Results", "client-performance", ["clientKpi", "report", "dataConnector"], ["roas", "cac", "revenue", "sourceFreshness"], ["reporting", "data-sources"]),
|
|
34
|
+
tab("operations", "Operations", "process-memory", ["sop", "workflowRun", "task"], ["sopCoverage", "runHistory", "handoffHealth", "blockedWork"], ["data", "auth"]),
|
|
35
|
+
tab("settings", "Settings", "workspace-control", ["workspaceSetting", "dataConnector", "userPreference"], ["adapterState", "permissionCoverage", "auditTrail", "deploymentState"], ["auth", "payments", "integrations"])
|
|
36
|
+
];
|
|
37
|
+
const objectDefinitions = {
|
|
38
|
+
agencyAccount: object("agencyAccount", "Agency account", [["name", "text"], ["domain", "url"], ["owner", "user"], ["settings", "json"], ["createdAt", "datetime"]]),
|
|
39
|
+
client: object("client", "Client", [["name", "text"], ["retainer", "currency"], ["healthScore", "rating"], ["stage", "select"], ["primaryContact", "relation"], ["renewalDate", "date"], ["monthlyRevenue", "currency"], ["notes", "long_text"]]),
|
|
40
|
+
contact: object("contact", "Contact", [["fullName", "text"], ["email", "email"], ["phone", "phone"], ["client", "relation"], ["role", "select"], ["lastTouchAt", "datetime"]]),
|
|
41
|
+
clientKpi: object("clientKpi", "Client KPI", [["client", "relation"], ["source", "select"], ["period", "date"], ["metric", "text"], ["value", "number"], ["delta", "percentage"], ["formulaValue", "formula"]]),
|
|
42
|
+
opportunity: object("opportunity", "Opportunity", [["client", "relation"], ["name", "text"], ["stage", "select"], ["value", "currency"], ["probability", "percentage"], ["owner", "user"], ["nextFollowUp", "datetime"]]),
|
|
43
|
+
contentPlan: object("contentPlan", "Content plan", [["client", "relation"], ["channel", "multi_select"], ["publishAt", "datetime"], ["asset", "file"], ["status", "select"], ["approvers", "multi_relation"]]),
|
|
44
|
+
task: object("task", "Task", [["title", "text"], ["client", "relation"], ["owner", "user"], ["priority", "select"], ["dueAt", "datetime"], ["done", "boolean"], ["sourcePayload", "json"]]),
|
|
45
|
+
workflowRun: object("workflowRun", "Workflow run", [["name", "text"], ["status", "select"], ["startedAt", "datetime"], ["completedAt", "datetime"], ["relatedTasks", "multi_relation"], ["trace", "json"]]),
|
|
46
|
+
invoice: object("invoice", "Invoice", [["client", "relation"], ["amount", "currency"], ["status", "select"], ["dueDate", "date"], ["paidAt", "datetime"], ["lineItems", "json"]]),
|
|
47
|
+
expense: object("expense", "Expense", [["client", "relation"], ["amount", "currency"], ["category", "select"], ["receipt", "file"], ["billable", "boolean"]]),
|
|
48
|
+
report: object("report", "Report", [["client", "relation"], ["period", "date"], ["status", "select"], ["sourceConnectors", "multi_relation"], ["snapshot", "json"], ["publishedUrl", "url"]]),
|
|
49
|
+
dataConnector: object("dataConnector", "Data connector", [["provider", "text"], ["status", "select"], ["authPath", "text"], ["lastSyncedAt", "datetime"], ["metadata", "json"]]),
|
|
50
|
+
agencyMetric: object("agencyMetric", "Agency metric", [["period", "date"], ["mrr", "currency"], ["churn", "percentage"], ["utilization", "percentage"], ["pipeline", "currency"], ["score", "formula"]]),
|
|
51
|
+
sop: object("sop", "SOP", [["title", "text"], ["area", "select"], ["owner", "user"], ["body", "long_text"], ["relatedObjects", "multi_relation"], ["version", "number"]]),
|
|
52
|
+
workspaceSetting: object("workspaceSetting", "Workspace setting", [["key", "text"], ["value", "json"], ["adapter", "select"], ["permission", "select"], ["updatedAt", "datetime"]]),
|
|
53
|
+
userPreference: object("userPreference", "User preference", [["user", "user"], ["theme", "select"], ["defaultView", "text"], ["notifications", "json"]])
|
|
54
|
+
};
|
|
55
|
+
const portalCapabilities = capabilitySpecs.map(({ id, label, objectType, bindings }) => ({ id, label, objectType, bindings }));
|
|
56
|
+
function tab(id, label, objectType, objects, widgets, bindings) {
|
|
57
|
+
return { id, label, objectType, objects, widgets, bindings };
|
|
58
|
+
}
|
|
59
|
+
function object(id, label, fields) {
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
label,
|
|
63
|
+
fields: fields.map(([name, type]) => ({ name, type })),
|
|
64
|
+
views: ["table", "kanban", "record", "dashboard"],
|
|
65
|
+
contract: "twenty-sdk/define"
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function buildPortalWorkspace({ config, adapters, integrations }) {
|
|
69
|
+
const integrationRows = [...integrations.dataSources, ...integrations.workspaceIntegrations];
|
|
70
|
+
const connectedRows = integrationRows.filter((item) => item.isConnected);
|
|
71
|
+
const connectedDataSources = integrations.dataSources.filter((item) => item.isConnected);
|
|
72
|
+
const adapterRows = [
|
|
73
|
+
primitive("data", "Data", config.dataAdapter, adapters.persistence.mode, adapters.persistence.requiredEnv),
|
|
74
|
+
primitive("auth", "Auth", config.authAdapter, adapters.auth.id, adapters.auth.requiredEnv),
|
|
75
|
+
primitive("payments", "Payments", config.paymentAdapter, adapters.payments.enabled ? "enabled" : "disabled", adapters.payments.requiredEnv),
|
|
76
|
+
primitive("integrations", "Integrations", config.integrationAdapter, adapters.integrations.authority, adapters.integrations.requiredEnv),
|
|
77
|
+
primitive("reporting", "Reporting", config.reportingAdapter || "not-configured", config.dataSources.hasWindsorApiKey ? "windsor-key-present" : "adapter-selected", []),
|
|
78
|
+
primitive("data-sources", "Data sources", `${connectedDataSources.length}/${integrations.dataSources.length}`, config.dataSources.hasWindsorApiKey ? "windsor-overlay" : "integration-state", [])
|
|
79
|
+
];
|
|
80
|
+
return {
|
|
81
|
+
identity: {
|
|
82
|
+
label: "Agency Portal",
|
|
83
|
+
mark: "GH",
|
|
84
|
+
mode: "governed-worker-kit",
|
|
85
|
+
deployTarget: config.deployTarget,
|
|
86
|
+
primitiveContract: "twenty-sdk/define objects, fields, views, dashboards, widgets, permissions, CRUD, API, webhooks, and audit logs",
|
|
87
|
+
fieldTypes
|
|
88
|
+
},
|
|
89
|
+
navigation: [
|
|
90
|
+
...portalCapabilities.map((item) => ({ href: `#${item.id}`, label: item.label })),
|
|
91
|
+
{ href: "/settings/integrations", label: "Integrations" }
|
|
92
|
+
],
|
|
93
|
+
summary: [
|
|
94
|
+
primitive("object-schema", "Object schema", `${Object.keys(objectDefinitions).length} objects`, "twenty-sdk/define", fieldTypes),
|
|
95
|
+
primitive("dashboard-widgets", "Dashboard widgets", `${capabilitySpecs.reduce((total, item) => total + item.widgets.length, 0)} widgets`, "capability dashboards", ["bar", "line", "pie", "number"]),
|
|
96
|
+
primitive("connection-state", "Connection state", `${connectedRows.length}/${integrationRows.length}`, config.integrationAdapter, adapters.integrations.requiredEnv)
|
|
97
|
+
],
|
|
98
|
+
adapters: adapterRows,
|
|
99
|
+
capabilities: capabilitySpecs.map((item) => buildCapabilityPrimitive(item, adapterRows, integrations)),
|
|
100
|
+
actions: capabilitySpecs.slice(0, 5).map((item) => ({ href: `#${item.id}`, label: item.label, objectType: item.objectType })),
|
|
101
|
+
api: [
|
|
102
|
+
{ label: "Workspace contract", href: "/api/workspace", method: "GET" },
|
|
103
|
+
{ label: "Integration contract", href: "/api/settings/integrations", method: "GET" }
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function primitive(id, label, value, source, env) {
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
label,
|
|
111
|
+
value,
|
|
112
|
+
source,
|
|
113
|
+
env,
|
|
114
|
+
status: env.length ? "configured-by-env" : "runtime-derived"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function buildCapabilityPrimitive(capability, adapterRows, integrations) {
|
|
118
|
+
const bindings = adapterRows.filter((item) => capability.bindings.includes(item.id));
|
|
119
|
+
const relatedObjects = capability.objects.map((id) => objectDefinitions[id]);
|
|
120
|
+
const fieldCount = relatedObjects.reduce((count, item) => count + item.fields.length, 0);
|
|
121
|
+
const dataSources = integrations.dataSources.filter((item) => capability.bindings.includes("data-sources") || capability.bindings.includes("reporting"));
|
|
122
|
+
const workspaceIntegrations = integrations.workspaceIntegrations.filter((item) => capability.bindings.includes("integrations"));
|
|
123
|
+
return {
|
|
124
|
+
...capability,
|
|
125
|
+
status: bindings.some((item) => item.value === "not-configured") ? "needs-runtime-config" : "runtime-ready",
|
|
126
|
+
fields: fieldCount,
|
|
127
|
+
objects: relatedObjects,
|
|
128
|
+
views: Array.from(new Set(relatedObjects.flatMap((item) => item.views))),
|
|
129
|
+
widgets: capability.widgets.map((id, index) => ({
|
|
130
|
+
id,
|
|
131
|
+
chart: ["number", "bar", "line", "pie"][index % 4],
|
|
132
|
+
sourceObject: relatedObjects[index % relatedObjects.length].id,
|
|
133
|
+
filters: ["client", "period", "owner", "status"].slice(0, 2 + index % 3),
|
|
134
|
+
realtime: true
|
|
135
|
+
})),
|
|
136
|
+
bindings,
|
|
137
|
+
integrations: [...dataSources, ...workspaceIntegrations].map((item) => ({
|
|
138
|
+
id: item.id,
|
|
139
|
+
label: item.label,
|
|
140
|
+
provider: item.provider,
|
|
141
|
+
objectType: item.objectType,
|
|
142
|
+
status: item.status,
|
|
143
|
+
source: item.authPath
|
|
144
|
+
}))
|
|
145
|
+
};
|
|
146
|
+
}
|
|
14
147
|
export {
|
|
148
|
+
buildPortalWorkspace,
|
|
15
149
|
portalCapabilities
|
|
16
150
|
};
|
|
@@ -77,3 +77,10 @@ It also accepts the GH app MCP accounts shape returned by `/api/mcp/accounts`:
|
|
|
77
77
|
Both shapes normalize into the same `AgencyPortalIntegration` object used by the BYO path. Unknown connected providers are preserved as discovered workspace integrations instead of being dropped.
|
|
78
78
|
|
|
79
79
|
The BYO path uses the same normalized object shape, but expects the workspace operator to provide the connection metadata and secret env names explicitly. Windsor is first-class: when `WINDSOR_API_KEY` is set with `AGENCY_PORTAL_INTEGRATION_ADAPTER=byo-api-key`, the app marks the Windsor AI data pipeline object as connected without requiring a larger JSON payload. The hosted Growthub bridge remains the lower-friction first-party path for user-owned MCP connections.
|
|
80
|
+
|
|
81
|
+
The starter also supports a safe hybrid mode for new users: keep `AGENCY_PORTAL_INTEGRATION_ADAPTER=growthub-bridge` for hosted account authority, and set `WINDSOR_API_KEY` locally when Windsor reporting is being bootstrapped before the hosted bridge has returned a Windsor account row. In that mode the app overlays a connected Windsor state onto:
|
|
82
|
+
|
|
83
|
+
- `windsor-ai`
|
|
84
|
+
- `google-sheets` blended data
|
|
85
|
+
|
|
86
|
+
This preserves the bridge as the source of truth for the rest of the integration catalog while letting the portal surface Windsor-backed reporting setup end to end from first boot.
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { agencyPortalIntegrationCatalog, groupIntegrationsByLane } from "../../apps/agency-portal/lib/domain/integrations.js";
|
|
3
|
+
import { portalCapabilities } from "../../apps/agency-portal/lib/domain/portal.js";
|
|
2
4
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
["Clients", "Profiles and onboarding", "Client records, notes, KPIs, lifecycle state, and contacts."],
|
|
6
|
-
["Pipeline", "Opportunities", "Lead stages, potential value, won/lost state, and follow-up ownership."],
|
|
7
|
-
["Reports", "Performance reviews", "Ad and campaign reporting through pluggable reporting adapters."],
|
|
8
|
-
["Client Results", "Windsor reporting", "Blended Meta, Shopify, GA4, and Google Sheets-backed results."],
|
|
9
|
-
["Settings", "Workspace control", "Branding, adapter selections, deployment metadata, and user preferences."],
|
|
10
|
-
];
|
|
11
|
-
|
|
5
|
+
const grouped = groupIntegrationsByLane(agencyPortalIntegrationCatalog);
|
|
6
|
+
const integrations = [...grouped.dataSources, ...grouped.workspaceIntegrations];
|
|
12
7
|
const adapters = [
|
|
13
|
-
["Persistence", "
|
|
14
|
-
["Auth", "
|
|
15
|
-
["Payments", "
|
|
16
|
-
["Integrations", "Growthub MCP bridge, BYO API tokens, and Windsor data pipelines."],
|
|
8
|
+
["Persistence", "Provider-managed", "Postgres, Qstash KV, or provider-managed adapter boundary."],
|
|
9
|
+
["Auth", "Provider-managed", "OIDC, Clerk, Auth.js, or provider-managed auth selector."],
|
|
10
|
+
["Payments", "None", "Stripe, Polar, or disabled payment adapter selector."],
|
|
11
|
+
["Integrations", `${integrations.filter((item) => item.isConnected).length}/${integrations.length} connected`, "Growthub MCP bridge, BYO API tokens, and Windsor data pipelines."],
|
|
12
|
+
];
|
|
13
|
+
const quickActions = [
|
|
14
|
+
...portalCapabilities.slice(0, 4).map((item) => ({ href: `#${item.id}`, label: item.label })),
|
|
15
|
+
{ href: "#integrations", label: "Integrations" },
|
|
17
16
|
];
|
|
18
|
-
|
|
19
|
-
const quickActions = ["Client onboarding", "Publish report", "Sync Windsor data", "Review open tasks"];
|
|
20
17
|
|
|
21
18
|
export default function App() {
|
|
22
19
|
return (
|
|
@@ -27,8 +24,8 @@ export default function App() {
|
|
|
27
24
|
<span>Agency Portal</span>
|
|
28
25
|
</div>
|
|
29
26
|
<nav className="nav">
|
|
30
|
-
{
|
|
31
|
-
<a key={
|
|
27
|
+
{portalCapabilities.map((capability) => (
|
|
28
|
+
<a key={capability.id} href={`#${capability.id}`}>{capability.label}</a>
|
|
32
29
|
))}
|
|
33
30
|
<a href="#integrations">Integrations</a>
|
|
34
31
|
</nav>
|
|
@@ -46,7 +43,7 @@ export default function App() {
|
|
|
46
43
|
</div>
|
|
47
44
|
<div className="utility-actions">
|
|
48
45
|
<a href="#integrations">Open integrations</a>
|
|
49
|
-
<span className="pill">
|
|
46
|
+
<span className="pill">deploy: local-preview</span>
|
|
50
47
|
</div>
|
|
51
48
|
</div>
|
|
52
49
|
|
|
@@ -54,26 +51,25 @@ export default function App() {
|
|
|
54
51
|
<span className="eyebrow">Agency operating system</span>
|
|
55
52
|
<h1>Client work, reporting, and integrations in one governed shell.</h1>
|
|
56
53
|
<p>
|
|
57
|
-
|
|
58
|
-
composable adapter model, local Vite workflow, and clean Vercel deployment path.
|
|
54
|
+
The local shell previews the same capability and integration objects that the deployable portal exposes.
|
|
59
55
|
</p>
|
|
60
56
|
</header>
|
|
61
57
|
|
|
62
58
|
<section className="hero-grid">
|
|
63
59
|
<article className="hero-card primary">
|
|
64
|
-
<span>
|
|
65
|
-
<strong
|
|
66
|
-
<p>
|
|
60
|
+
<span>Runtime adapters</span>
|
|
61
|
+
<strong>provider-managed</strong>
|
|
62
|
+
<p>Persistence, auth, and payment selectors stay replaceable at runtime.</p>
|
|
67
63
|
</article>
|
|
68
64
|
<article className="hero-card">
|
|
69
65
|
<span>MCP connections</span>
|
|
70
|
-
<strong>
|
|
71
|
-
<p>
|
|
66
|
+
<strong>{integrations.filter((item) => item.isConnected).length}/{integrations.length}</strong>
|
|
67
|
+
<p>Bridge, BYO, and catalog rows normalize into one integration object model.</p>
|
|
72
68
|
</article>
|
|
73
69
|
<article className="hero-card">
|
|
74
70
|
<span>Windsor data</span>
|
|
75
|
-
<strong>
|
|
76
|
-
<p>
|
|
71
|
+
<strong>{grouped.dataSources.filter((item) => item.isConnected).length}/{grouped.dataSources.length}</strong>
|
|
72
|
+
<p>Data pipeline readiness comes from integration state, not seeded reporting metrics.</p>
|
|
77
73
|
</article>
|
|
78
74
|
</section>
|
|
79
75
|
|
|
@@ -101,19 +97,20 @@ export default function App() {
|
|
|
101
97
|
</section>
|
|
102
98
|
|
|
103
99
|
<section className="grid">
|
|
104
|
-
{
|
|
105
|
-
<article className="card" id={
|
|
106
|
-
<span>{metric}</span>
|
|
107
|
-
<h3>{label}</h3>
|
|
108
|
-
<p>{description}</p>
|
|
100
|
+
{portalCapabilities.map((capability) => (
|
|
101
|
+
<article className="card" id={capability.id} key={capability.id}>
|
|
102
|
+
<span>{capability.metric}</span>
|
|
103
|
+
<h3>{capability.label}</h3>
|
|
104
|
+
<p>{capability.description}</p>
|
|
109
105
|
</article>
|
|
110
106
|
))}
|
|
111
107
|
</section>
|
|
112
108
|
|
|
113
109
|
<section className="adapter">
|
|
114
|
-
{adapters.map(([title, body]) => (
|
|
110
|
+
{adapters.map(([title, value, body]) => (
|
|
115
111
|
<article className="card" key={title}>
|
|
116
112
|
<h3>{title}</h3>
|
|
113
|
+
<p><strong>{value}</strong></p>
|
|
117
114
|
<p>{body}</p>
|
|
118
115
|
</article>
|
|
119
116
|
))}
|
|
@@ -147,15 +144,15 @@ export default function App() {
|
|
|
147
144
|
</p>
|
|
148
145
|
</div>
|
|
149
146
|
<div className="results-metrics">
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
{grouped.dataSources.map((source) => (
|
|
148
|
+
<div key={source.id}><strong>{source.label}</strong><span>{source.status}</span></div>
|
|
149
|
+
))}
|
|
153
150
|
</div>
|
|
154
151
|
</section>
|
|
155
152
|
</section>
|
|
156
153
|
|
|
157
154
|
<div className="quick-actions" aria-label="Quick actions">
|
|
158
|
-
{quickActions.map((action) => <
|
|
155
|
+
{quickActions.map((action) => <a key={action.href} href={action.href}>{action.label}</a>)}
|
|
159
156
|
</div>
|
|
160
157
|
</main>
|
|
161
158
|
);
|
|
@@ -114,7 +114,7 @@ a { color: inherit; }
|
|
|
114
114
|
.results-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
|
|
115
115
|
.results-metrics span { display: block; color: var(--muted); font-size: 12px; }
|
|
116
116
|
.quick-actions { position: fixed; right: 20px; bottom: 20px; display: grid; gap: 8px; width: 190px; }
|
|
117
|
-
.quick-actions button {
|
|
117
|
+
.quick-actions button, .quick-actions a {
|
|
118
118
|
border: 1px solid var(--line);
|
|
119
119
|
border-radius: 8px;
|
|
120
120
|
padding: 10px 12px;
|
|
@@ -122,6 +122,7 @@ a { color: inherit; }
|
|
|
122
122
|
color: var(--ink);
|
|
123
123
|
text-align: left;
|
|
124
124
|
font: inherit;
|
|
125
|
+
text-decoration: none;
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
@media (max-width: 1020px) {
|