@growthub/cli 0.9.1 → 0.9.3
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 +2295 -2949
- package/package.json +2 -2
package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/.env.example
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
# Core adapter selectors
|
|
2
|
+
AGENCY_PORTAL_DEPLOY_TARGET=vercel
|
|
3
|
+
AGENCY_PORTAL_DATA_ADAPTER=provider-managed
|
|
4
|
+
AGENCY_PORTAL_AUTH_ADAPTER=provider-managed
|
|
5
|
+
AGENCY_PORTAL_PAYMENT_ADAPTER=none
|
|
5
6
|
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
7
|
+
# Integration mode
|
|
8
|
+
# - growthub-bridge: hosted Growthub account authority
|
|
9
|
+
# - byo-api-key: workspace-owned connection metadata
|
|
10
|
+
# - static: local starter catalog only
|
|
11
|
+
AGENCY_PORTAL_INTEGRATION_ADAPTER=growthub-bridge
|
|
9
12
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
# Hosted bridge authority
|
|
14
|
+
GROWTHUB_BRIDGE_BASE_URL=https://www.growthub.ai
|
|
15
|
+
GROWTHUB_BRIDGE_INTEGRATIONS_PATH=/api/mcp/accounts
|
|
16
|
+
GROWTHUB_BRIDGE_ACCESS_TOKEN=
|
|
17
|
+
GROWTHUB_BRIDGE_USER_ID=
|
|
13
18
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
19
|
+
# Optional Windsor reporting lane.
|
|
20
|
+
# Hybrid first boot: keep growthub-bridge authority and set WINDSOR_API_KEY
|
|
21
|
+
# to mark Windsor AI + Google Sheets blended data connected locally.
|
|
22
|
+
AGENCY_PORTAL_REPORTING_ADAPTER=windsor
|
|
23
|
+
WINDSOR_API_KEY=
|
|
18
24
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
PORTAL_USER_ID=your-supabase-user-uuid-here
|
|
25
|
+
# Optional BYO connection metadata
|
|
26
|
+
AGENCY_PORTAL_BYO_CONNECTIONS_JSON=
|
|
22
27
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
# Optional payment/auth/database env selected by adapter values above
|
|
29
|
+
DATABASE_URL=
|
|
30
|
+
QSTASH_KV_REST_URL=
|
|
31
|
+
QSTASH_KV_REST_TOKEN=
|
|
32
|
+
AUTH_SECRET=
|
|
33
|
+
AUTH_ISSUER=
|
|
34
|
+
AUTH_CLIENT_ID=
|
|
35
|
+
AUTH_CLIENT_SECRET=
|
|
36
|
+
PAYMENT_SECRET_KEY=
|
|
37
|
+
PAYMENT_WEBHOOK_SECRET=
|
|
38
|
+
|
|
39
|
+
# Optional app settings
|
|
40
|
+
CRON_SECRET=
|
|
41
|
+
PORTAL_USER_ID=
|
|
@@ -23,6 +23,8 @@ Settings exposes two integration lanes:
|
|
|
23
23
|
|
|
24
24
|
Use `AGENCY_PORTAL_INTEGRATION_ADAPTER=growthub-bridge` when the deployed app should read connection state from the Growthub GH app MCP bridge. The reusable primitive is `lib/adapters/integrations/growthub-connection-normalizer.js`; it accepts SDK/profile-style `integrations[]` payloads and GH app MCP `accounts[]` payloads, then emits the same normalized object shape used by `byo-api-key`. Keep provider tokens in the hosted authority layer or named env vars; this app consumes normalized connection metadata only.
|
|
25
25
|
|
|
26
|
+
For first boot, the bundled app also supports a hybrid path: keep `AGENCY_PORTAL_INTEGRATION_ADAPTER=growthub-bridge` and set `WINDSOR_API_KEY` locally. That overlays connected state for Windsor AI and Google Sheets blended data without moving the rest of the portal off the hosted bridge authority path.
|
|
27
|
+
|
|
26
28
|
## Run
|
|
27
29
|
|
|
28
30
|
```bash
|
|
@@ -5,21 +5,25 @@ import { describeIntegrationAdapter, listAgencyPortalIntegrations } from "@/lib/
|
|
|
5
5
|
import { describePaymentAdapter } from "@/lib/adapters/payments";
|
|
6
6
|
import { describePersistenceAdapter } from "@/lib/adapters/persistence";
|
|
7
7
|
import { groupIntegrationsByLane } from "@/lib/domain/integrations";
|
|
8
|
-
import { portalCapabilities } from "@/lib/domain/portal";
|
|
8
|
+
import { buildPortalWorkspace, portalCapabilities } from "@/lib/domain/portal";
|
|
9
9
|
async function GET() {
|
|
10
10
|
const integrations = await listAgencyPortalIntegrations();
|
|
11
|
+
const config = readAdapterConfig();
|
|
12
|
+
const adapters = {
|
|
13
|
+
persistence: describePersistenceAdapter(),
|
|
14
|
+
auth: describeAuthAdapter(),
|
|
15
|
+
payments: describePaymentAdapter(),
|
|
16
|
+
integrations: describeIntegrationAdapter()
|
|
17
|
+
};
|
|
18
|
+
const settings = {
|
|
19
|
+
integrations: groupIntegrationsByLane(integrations)
|
|
20
|
+
};
|
|
11
21
|
return NextResponse.json({
|
|
12
|
-
config
|
|
13
|
-
adapters
|
|
14
|
-
persistence: describePersistenceAdapter(),
|
|
15
|
-
auth: describeAuthAdapter(),
|
|
16
|
-
payments: describePaymentAdapter(),
|
|
17
|
-
integrations: describeIntegrationAdapter()
|
|
18
|
-
},
|
|
22
|
+
config,
|
|
23
|
+
adapters,
|
|
19
24
|
capabilities: portalCapabilities,
|
|
20
|
-
settings
|
|
21
|
-
|
|
22
|
-
}
|
|
25
|
+
settings,
|
|
26
|
+
workspace: buildPortalWorkspace({ config, adapters, integrations: settings.integrations })
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
29
|
export {
|
package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/app/globals.css
CHANGED
|
@@ -119,6 +119,135 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
|
|
|
119
119
|
}
|
|
120
120
|
.page-heading p { max-width: 780px; margin: 0 0 12px; }
|
|
121
121
|
.eyebrow { color: var(--accent); font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.12em; }
|
|
122
|
+
.workspace-header { margin-bottom: 18px; }
|
|
123
|
+
.primitive-grid {
|
|
124
|
+
display: grid;
|
|
125
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
126
|
+
gap: 14px;
|
|
127
|
+
margin-bottom: 16px;
|
|
128
|
+
}
|
|
129
|
+
.primitive-grid.adapter { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
|
130
|
+
.primitive-card, .capability-primitive, .contract-panel a {
|
|
131
|
+
border: 1px solid var(--line);
|
|
132
|
+
border-radius: 10px;
|
|
133
|
+
background: rgba(17, 24, 39, 0.9);
|
|
134
|
+
}
|
|
135
|
+
.primitive-card { min-height: 158px; padding: 16px; }
|
|
136
|
+
.primitive-card-top, .capability-heading {
|
|
137
|
+
display: flex;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
align-items: flex-start;
|
|
140
|
+
gap: 12px;
|
|
141
|
+
}
|
|
142
|
+
.primitive-card strong {
|
|
143
|
+
display: block;
|
|
144
|
+
margin-top: 16px;
|
|
145
|
+
font-size: clamp(20px, 3vw, 34px);
|
|
146
|
+
line-height: 1;
|
|
147
|
+
word-break: break-word;
|
|
148
|
+
}
|
|
149
|
+
.primitive-meta, .integration-bindings {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-wrap: wrap;
|
|
152
|
+
gap: 7px;
|
|
153
|
+
margin-top: 16px;
|
|
154
|
+
}
|
|
155
|
+
.primitive-meta span, .primitive-meta code, .integration-bindings span, .binding-row code, .contract-panel code, .quick-actions code {
|
|
156
|
+
border: 1px solid var(--line);
|
|
157
|
+
border-radius: 999px;
|
|
158
|
+
padding: 4px 7px;
|
|
159
|
+
color: var(--muted);
|
|
160
|
+
background: #0d1420;
|
|
161
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
162
|
+
font-size: 11px;
|
|
163
|
+
}
|
|
164
|
+
.capability-board { display: grid; gap: 16px; }
|
|
165
|
+
.capability-primitive {
|
|
166
|
+
min-height: 252px;
|
|
167
|
+
padding: 18px;
|
|
168
|
+
scroll-margin-top: 20px;
|
|
169
|
+
}
|
|
170
|
+
.capability-primitive h2 { margin: 4px 0 0; font-size: 28px; }
|
|
171
|
+
.binding-list { display: grid; gap: 8px; margin-top: 16px; }
|
|
172
|
+
.binding-row {
|
|
173
|
+
display: grid;
|
|
174
|
+
grid-template-columns: minmax(90px, 0.8fr) minmax(0, 1fr) auto;
|
|
175
|
+
gap: 10px;
|
|
176
|
+
align-items: center;
|
|
177
|
+
border: 1px solid var(--line);
|
|
178
|
+
border-radius: 8px;
|
|
179
|
+
padding: 9px 10px;
|
|
180
|
+
background: #0d1420;
|
|
181
|
+
}
|
|
182
|
+
.binding-row span { color: var(--muted); font-size: 12px; }
|
|
183
|
+
.binding-row strong { font-size: 13px; word-break: break-word; }
|
|
184
|
+
.primitive-columns {
|
|
185
|
+
display: grid;
|
|
186
|
+
grid-template-columns: 1fr 0.7fr 1fr;
|
|
187
|
+
gap: 12px;
|
|
188
|
+
margin-top: 14px;
|
|
189
|
+
}
|
|
190
|
+
.primitive-stack {
|
|
191
|
+
display: grid;
|
|
192
|
+
align-content: start;
|
|
193
|
+
gap: 7px;
|
|
194
|
+
border: 1px solid var(--line);
|
|
195
|
+
border-radius: 8px;
|
|
196
|
+
padding: 10px;
|
|
197
|
+
background: rgba(13, 20, 32, 0.82);
|
|
198
|
+
}
|
|
199
|
+
.primitive-stack > span {
|
|
200
|
+
color: var(--accent);
|
|
201
|
+
font-size: 11px;
|
|
202
|
+
font-weight: 800;
|
|
203
|
+
text-transform: uppercase;
|
|
204
|
+
letter-spacing: 0.12em;
|
|
205
|
+
}
|
|
206
|
+
.primitive-stack div {
|
|
207
|
+
display: flex;
|
|
208
|
+
justify-content: space-between;
|
|
209
|
+
gap: 8px;
|
|
210
|
+
align-items: center;
|
|
211
|
+
border-top: 1px solid rgba(34, 48, 71, 0.65);
|
|
212
|
+
padding-top: 7px;
|
|
213
|
+
}
|
|
214
|
+
.primitive-stack strong {
|
|
215
|
+
min-width: 0;
|
|
216
|
+
font-size: 12px;
|
|
217
|
+
word-break: break-word;
|
|
218
|
+
}
|
|
219
|
+
.field-cloud {
|
|
220
|
+
display: flex;
|
|
221
|
+
flex-wrap: wrap;
|
|
222
|
+
gap: 6px;
|
|
223
|
+
margin-top: 12px;
|
|
224
|
+
}
|
|
225
|
+
.field-cloud code {
|
|
226
|
+
border: 1px solid rgba(56, 189, 248, 0.28);
|
|
227
|
+
border-radius: 999px;
|
|
228
|
+
padding: 4px 7px;
|
|
229
|
+
color: #d7e8ff;
|
|
230
|
+
background: rgba(56, 189, 248, 0.08);
|
|
231
|
+
font-size: 11px;
|
|
232
|
+
}
|
|
233
|
+
.integration-bindings span {
|
|
234
|
+
display: inline-flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
gap: 6px;
|
|
237
|
+
}
|
|
238
|
+
.contract-panel {
|
|
239
|
+
display: grid;
|
|
240
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
241
|
+
gap: 12px;
|
|
242
|
+
margin-top: 16px;
|
|
243
|
+
}
|
|
244
|
+
.contract-panel a {
|
|
245
|
+
display: flex;
|
|
246
|
+
justify-content: space-between;
|
|
247
|
+
gap: 12px;
|
|
248
|
+
padding: 14px;
|
|
249
|
+
text-decoration: none;
|
|
250
|
+
}
|
|
122
251
|
.hero-grid {
|
|
123
252
|
display: grid;
|
|
124
253
|
grid-template-columns: 1.2fr 0.9fr 0.9fr;
|
|
@@ -170,7 +299,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
|
|
|
170
299
|
gap: 8px;
|
|
171
300
|
width: 190px;
|
|
172
301
|
}
|
|
173
|
-
.quick-actions button {
|
|
302
|
+
.quick-actions button, .quick-actions a {
|
|
174
303
|
border: 1px solid var(--line);
|
|
175
304
|
border-radius: 8px;
|
|
176
305
|
padding: 10px 12px;
|
|
@@ -178,6 +307,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
|
|
|
178
307
|
color: var(--ink);
|
|
179
308
|
text-align: left;
|
|
180
309
|
font: inherit;
|
|
310
|
+
text-decoration: none;
|
|
181
311
|
}
|
|
182
312
|
.integration-board { display: grid; gap: 14px; }
|
|
183
313
|
.integration-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 14px 16px; }
|
|
@@ -211,6 +341,8 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
|
|
|
211
341
|
}
|
|
212
342
|
.status.connected { color: var(--good); border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.1); }
|
|
213
343
|
.status.needs-connection { color: var(--warn); border-color: rgba(245, 158, 11, 0.5); background: rgba(245, 158, 11, 0.1); }
|
|
344
|
+
.status.runtime-ready, .status.runtime-derived, .status.configured-by-env { color: var(--good); border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.1); }
|
|
345
|
+
.status.needs-runtime-config { color: var(--warn); border-color: rgba(245, 158, 11, 0.5); background: rgba(245, 158, 11, 0.1); }
|
|
214
346
|
.integration-card-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
|
|
215
347
|
.integration-card-meta span {
|
|
216
348
|
border: 1px solid var(--line);
|
|
@@ -224,7 +356,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
|
|
|
224
356
|
@media (max-width: 1020px) {
|
|
225
357
|
.shell { grid-template-columns: 1fr; }
|
|
226
358
|
.sidebar { position: static; height: auto; }
|
|
227
|
-
.hero-grid, .grid, .adapter, .ops-strip, .setup-grid, .results-panel, .results-metrics { grid-template-columns: 1fr; }
|
|
359
|
+
.primitive-grid, .primitive-grid.adapter, .capability-board, .primitive-columns, .contract-panel, .hero-grid, .grid, .adapter, .ops-strip, .setup-grid, .results-panel, .results-metrics { grid-template-columns: 1fr; }
|
|
228
360
|
.quick-actions { position: static; width: auto; margin: 20px 30px; }
|
|
229
361
|
}
|
|
230
362
|
|
package/assets/worker-kits/growthub-agency-portal-starter-v1/apps/agency-portal/app/page.jsx
CHANGED
|
@@ -1,165 +1,159 @@
|
|
|
1
1
|
import { describeAuthAdapter } from "@/lib/adapters/auth";
|
|
2
2
|
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
3
|
+
import { describeIntegrationAdapter, listAgencyPortalIntegrations } from "@/lib/adapters/integrations";
|
|
3
4
|
import { describePaymentAdapter } from "@/lib/adapters/payments";
|
|
4
5
|
import { describePersistenceAdapter } from "@/lib/adapters/persistence";
|
|
5
|
-
import {
|
|
6
|
+
import { groupIntegrationsByLane } from "@/lib/domain/integrations";
|
|
7
|
+
import { buildPortalWorkspace } from "@/lib/domain/portal";
|
|
6
8
|
import Link from "next/link";
|
|
7
|
-
|
|
8
|
-
...portalCapabilities.map((item) => ({ href: `#${item.id}`, label: item.label })),
|
|
9
|
-
{ href: "/settings/integrations", label: "Integrations" }
|
|
10
|
-
];
|
|
11
|
-
const quickActions = [
|
|
12
|
-
"Client onboarding",
|
|
13
|
-
"Publish report",
|
|
14
|
-
"Sync Windsor data",
|
|
15
|
-
"Review open tasks"
|
|
16
|
-
];
|
|
17
|
-
function Home() {
|
|
9
|
+
async function Home() {
|
|
18
10
|
const config = readAdapterConfig();
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
11
|
+
const integrations = groupIntegrationsByLane(await listAgencyPortalIntegrations());
|
|
12
|
+
const workspace = buildPortalWorkspace({
|
|
13
|
+
config,
|
|
14
|
+
integrations,
|
|
15
|
+
adapters: {
|
|
16
|
+
persistence: describePersistenceAdapter(),
|
|
17
|
+
auth: describeAuthAdapter(),
|
|
18
|
+
payments: describePaymentAdapter(),
|
|
19
|
+
integrations: describeIntegrationAdapter()
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
22
|
return <main className="shell">
|
|
23
|
-
<
|
|
24
|
-
<div className="brand">
|
|
25
|
-
<span className="brand-mark">GH</span>
|
|
26
|
-
<span>Agency Portal</span>
|
|
27
|
-
</div>
|
|
28
|
-
<nav className="nav">
|
|
29
|
-
{nav.map((item, index) => <Link className={index === 0 ? "active" : ""} href={item.href} key={item.href}>{item.label}</Link>)}
|
|
30
|
-
</nav>
|
|
31
|
-
<div className="sidebar-footer">
|
|
32
|
-
<span className="status-dot" />
|
|
33
|
-
Governed worker kit
|
|
34
|
-
</div>
|
|
35
|
-
</aside>
|
|
36
|
-
|
|
23
|
+
<PortalSidebar workspace={workspace} />
|
|
37
24
|
<section className="main">
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<div className="utility-actions">
|
|
44
|
-
<Link href="/settings/integrations">Integrations</Link>
|
|
45
|
-
<span className="pill">v1 kit</span>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
<div className="page-heading">
|
|
50
|
-
<div>
|
|
51
|
-
<p className="eyebrow">Growthub Local + Vercel</p>
|
|
52
|
-
<h1>Composable Agency Workspace</h1>
|
|
53
|
-
<p>
|
|
54
|
-
A production starter for agency portals with local-first Vite operation, Vercel
|
|
55
|
-
deployment, thin adapter contracts, Windsor data pipelines, and Growthub MCP
|
|
56
|
-
connection authority.
|
|
57
|
-
</p>
|
|
58
|
-
</div>
|
|
59
|
-
<span className="badge">deploy: {config.deployTarget}</span>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<section className="hero-grid" id="dashboard">
|
|
63
|
-
<article className="hero-card wide">
|
|
64
|
-
<p className="card-label">Monthly Revenue</p>
|
|
65
|
-
<strong>$0</strong>
|
|
66
|
-
<div className="progress"><span style={{ width: "0%" }} /></div>
|
|
67
|
-
<p className="muted">Connect persistence to populate invoices, retainers, and margin reporting.</p>
|
|
68
|
-
</article>
|
|
69
|
-
<article className="hero-card">
|
|
70
|
-
<p className="card-label">MCP Connections</p>
|
|
71
|
-
<strong>{config.integrationAdapter}</strong>
|
|
72
|
-
<p className="muted">Growthub bridge and BYO API key paths normalize into one object model.</p>
|
|
73
|
-
</article>
|
|
74
|
-
<article className="hero-card">
|
|
75
|
-
<p className="card-label">Client Results</p>
|
|
76
|
-
<strong>Windsor</strong>
|
|
77
|
-
<p className="muted">First-class Windsor AI and Google Sheets blended data pipeline support.</p>
|
|
78
|
-
</article>
|
|
79
|
-
</section>
|
|
80
|
-
|
|
81
|
-
<section className="ops-strip" aria-label="Setup paths">
|
|
82
|
-
<article>
|
|
83
|
-
<span>01</span>
|
|
84
|
-
<strong>Local Vite shell</strong>
|
|
85
|
-
<p>Use the same portal frame for agent-led local customization.</p>
|
|
86
|
-
</article>
|
|
87
|
-
<article>
|
|
88
|
-
<span>02</span>
|
|
89
|
-
<strong>Vercel app</strong>
|
|
90
|
-
<p>Deploy the Next app without binding persistence to a single provider.</p>
|
|
91
|
-
</article>
|
|
92
|
-
<article>
|
|
93
|
-
<span>03</span>
|
|
94
|
-
<strong>Growthub bridge</strong>
|
|
95
|
-
<p>Resolve hosted MCP accounts when the user connects Growthub authority.</p>
|
|
96
|
-
</article>
|
|
97
|
-
<article>
|
|
98
|
-
<span>04</span>
|
|
99
|
-
<strong>BYO keys</strong>
|
|
100
|
-
<p>Support Windsor and external provider keys through the same object contract.</p>
|
|
101
|
-
</article>
|
|
102
|
-
</section>
|
|
103
|
-
|
|
104
|
-
<section className="grid compact-grid">
|
|
105
|
-
{portalCapabilities.map((capability) => <article className="card" id={capability.id} key={capability.id}>
|
|
106
|
-
<h3>{capability.label}</h3>
|
|
107
|
-
<div className="metric">{capability.metric}</div>
|
|
108
|
-
<p>{capability.description}</p>
|
|
109
|
-
</article>)}
|
|
110
|
-
</section>
|
|
111
|
-
|
|
112
|
-
<section className="adapter" aria-label="Adapter contracts">
|
|
113
|
-
<article className="card">
|
|
114
|
-
<h3>Persistence</h3>
|
|
115
|
-
<p><strong>{persistence.label}</strong></p>
|
|
116
|
-
<p>Postgres, Qstash KV, or provider-managed.</p>
|
|
117
|
-
</article>
|
|
118
|
-
<article className="card">
|
|
119
|
-
<h3>Auth</h3>
|
|
120
|
-
<p><strong>{auth.id}</strong></p>
|
|
121
|
-
<p>{auth.requiredEnv.length ? auth.requiredEnv.join(", ") : "provider-defined"}</p>
|
|
122
|
-
</article>
|
|
123
|
-
<article className="card">
|
|
124
|
-
<h3>Payments</h3>
|
|
125
|
-
<p><strong>{payments.id}</strong></p>
|
|
126
|
-
<p>{payments.enabled ? "enabled" : "disabled"}</p>
|
|
127
|
-
</article>
|
|
128
|
-
<article className="card">
|
|
129
|
-
<h3>Integrations</h3>
|
|
130
|
-
<p><strong>{config.integrationAdapter}</strong></p>
|
|
131
|
-
<p><Link href="/settings/integrations">Open setup surface</Link></p>
|
|
132
|
-
</article>
|
|
133
|
-
<article className="card">
|
|
134
|
-
<h3>Worker API</h3>
|
|
135
|
-
<p><code>GET /api/workspace</code></p>
|
|
136
|
-
<p>Adapter and capability metadata for agents.</p>
|
|
137
|
-
</article>
|
|
138
|
-
</section>
|
|
139
|
-
|
|
140
|
-
<section className="results-panel" id="client-results">
|
|
141
|
-
<div>
|
|
142
|
-
<p className="eyebrow">Client Results</p>
|
|
143
|
-
<h2>Windsor AI + blended data ready</h2>
|
|
144
|
-
<p>
|
|
145
|
-
Windsor is a data pipeline object, not a database choice. Google Sheets blended
|
|
146
|
-
exports, GA4, Shopify, and Meta data stay composable through the integrations surface.
|
|
147
|
-
</p>
|
|
148
|
-
</div>
|
|
149
|
-
<div className="results-metrics">
|
|
150
|
-
<span>Meta</span>
|
|
151
|
-
<span>Shopify</span>
|
|
152
|
-
<span>GA4</span>
|
|
153
|
-
<span>Sheets</span>
|
|
154
|
-
</div>
|
|
155
|
-
</section>
|
|
25
|
+
<WorkspaceHeader workspace={workspace} />
|
|
26
|
+
<PrimitiveGrid id="dashboard" items={workspace.summary} variant="summary" />
|
|
27
|
+
<PrimitiveGrid items={workspace.adapters} variant="adapter" />
|
|
28
|
+
<CapabilityBoard capabilities={workspace.capabilities} />
|
|
29
|
+
<ContractPanel api={workspace.api} />
|
|
156
30
|
</section>
|
|
157
|
-
|
|
158
31
|
<aside className="quick-actions" aria-label="Quick actions">
|
|
159
|
-
{
|
|
32
|
+
{workspace.actions.map((action) => <Link href={action.href} key={action.href}>
|
|
33
|
+
<span>{action.label}</span>
|
|
34
|
+
<code>{action.objectType}</code>
|
|
35
|
+
</Link>)}
|
|
160
36
|
</aside>
|
|
161
37
|
</main>;
|
|
162
38
|
}
|
|
39
|
+
function PortalSidebar({ workspace }) {
|
|
40
|
+
return <aside className="sidebar">
|
|
41
|
+
<div className="brand">
|
|
42
|
+
<span className="brand-mark">{workspace.identity.mark}</span>
|
|
43
|
+
<span>{workspace.identity.label}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<nav className="nav">
|
|
46
|
+
{workspace.navigation.map((item, index) => <Link className={index === 0 ? "active" : ""} href={item.href} key={item.href}>{item.label}</Link>)}
|
|
47
|
+
</nav>
|
|
48
|
+
<div className="sidebar-footer">
|
|
49
|
+
<span className="status-dot" />
|
|
50
|
+
{workspace.identity.mode}
|
|
51
|
+
</div>
|
|
52
|
+
</aside>;
|
|
53
|
+
}
|
|
54
|
+
function WorkspaceHeader({ workspace }) {
|
|
55
|
+
return <header className="workspace-header">
|
|
56
|
+
<div className="utility-bar">
|
|
57
|
+
<div>
|
|
58
|
+
<strong>{workspace.identity.mode}</strong>
|
|
59
|
+
<span>{workspace.capabilities.length} capability primitives: {workspace.identity.primitiveContract}.</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="utility-actions">
|
|
62
|
+
{workspace.api.map((item) => <Link href={item.href} key={item.href}>{item.method} {item.href}</Link>)}
|
|
63
|
+
<span className="pill">deploy: {workspace.identity.deployTarget}</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="page-heading">
|
|
67
|
+
<p className="eyebrow">{workspace.identity.mode}</p>
|
|
68
|
+
<h1>{workspace.identity.label}</h1>
|
|
69
|
+
<p>{workspace.identity.primitiveContract}</p>
|
|
70
|
+
</div>
|
|
71
|
+
</header>;
|
|
72
|
+
}
|
|
73
|
+
function PrimitiveGrid({ id, items, variant }) {
|
|
74
|
+
return <section className={`primitive-grid ${variant}`} id={id} aria-label={variant}>
|
|
75
|
+
{items.map((item) => <PrimitiveCard item={item} key={item.id} />)}
|
|
76
|
+
</section>;
|
|
77
|
+
}
|
|
78
|
+
function PrimitiveCard({ item }) {
|
|
79
|
+
return <article className="primitive-card">
|
|
80
|
+
<div className="primitive-card-top">
|
|
81
|
+
<p className="card-label">{item.label}</p>
|
|
82
|
+
<span className={`status ${item.status}`}>{item.status}</span>
|
|
83
|
+
</div>
|
|
84
|
+
<strong>{item.value}</strong>
|
|
85
|
+
<div className="primitive-meta">
|
|
86
|
+
<span>{item.source}</span>
|
|
87
|
+
{item.env.map((key) => <code key={key}>{key}</code>)}
|
|
88
|
+
</div>
|
|
89
|
+
</article>;
|
|
90
|
+
}
|
|
91
|
+
function CapabilityBoard({ capabilities }) {
|
|
92
|
+
return <section className="capability-board" aria-label="Capability primitives">
|
|
93
|
+
{capabilities.map((capability) => <CapabilityPrimitive capability={capability} key={capability.id} />)}
|
|
94
|
+
</section>;
|
|
95
|
+
}
|
|
96
|
+
function CapabilityPrimitive({ capability }) {
|
|
97
|
+
return <article className="capability-primitive" id={capability.id}>
|
|
98
|
+
<div className="capability-heading">
|
|
99
|
+
<div>
|
|
100
|
+
<p className="eyebrow">{capability.objectType}</p>
|
|
101
|
+
<h2>{capability.label}</h2>
|
|
102
|
+
</div>
|
|
103
|
+
<span className={`status ${capability.status}`}>{capability.status}</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="binding-list">
|
|
106
|
+
{capability.bindings.map((binding) => <div className="binding-row" key={binding.id}>
|
|
107
|
+
<span>{binding.label}</span>
|
|
108
|
+
<strong>{binding.value}</strong>
|
|
109
|
+
<code>{binding.source}</code>
|
|
110
|
+
</div>)}
|
|
111
|
+
</div>
|
|
112
|
+
<div className="primitive-columns">
|
|
113
|
+
<PrimitiveStack label="Objects" items={capability.objects.map((item) => ({
|
|
114
|
+
id: item.id,
|
|
115
|
+
label: item.label,
|
|
116
|
+
meta: `${item.fields.length} fields`
|
|
117
|
+
}))} />
|
|
118
|
+
<PrimitiveStack label="Views" items={capability.views.map((view) => ({
|
|
119
|
+
id: view,
|
|
120
|
+
label: view,
|
|
121
|
+
meta: "view"
|
|
122
|
+
}))} />
|
|
123
|
+
<PrimitiveStack label="Widgets" items={capability.widgets.map((widget) => ({
|
|
124
|
+
id: widget.id,
|
|
125
|
+
label: widget.id,
|
|
126
|
+
meta: `${widget.chart} / ${widget.sourceObject}`
|
|
127
|
+
}))} />
|
|
128
|
+
</div>
|
|
129
|
+
<div className="field-cloud">
|
|
130
|
+
{capability.objects.flatMap((item) => item.fields.map((field) => <code key={`${item.id}-${field.name}`}>{field.name}:{field.type}</code>))}
|
|
131
|
+
</div>
|
|
132
|
+
{capability.integrations.length ? <div className="integration-bindings">
|
|
133
|
+
{capability.integrations.map((item) => <span key={item.id}>
|
|
134
|
+
{item.label}
|
|
135
|
+
<code>{item.status}</code>
|
|
136
|
+
</span>)}
|
|
137
|
+
</div> : null}
|
|
138
|
+
</article>;
|
|
139
|
+
}
|
|
140
|
+
function PrimitiveStack({ label, items }) {
|
|
141
|
+
return <section className="primitive-stack">
|
|
142
|
+
<span>{label}</span>
|
|
143
|
+
{items.map((item) => <div key={item.id}>
|
|
144
|
+
<strong>{item.label}</strong>
|
|
145
|
+
<code>{item.meta}</code>
|
|
146
|
+
</div>)}
|
|
147
|
+
</section>;
|
|
148
|
+
}
|
|
149
|
+
function ContractPanel({ api }) {
|
|
150
|
+
return <section className="contract-panel" id="operations">
|
|
151
|
+
{api.map((item) => <Link href={item.href} key={item.href}>
|
|
152
|
+
<span>{item.label}</span>
|
|
153
|
+
<code>{item.method} {item.href}</code>
|
|
154
|
+
</Link>)}
|
|
155
|
+
</section>;
|
|
156
|
+
}
|
|
163
157
|
export {
|
|
164
158
|
Home as default
|
|
165
159
|
};
|