@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.
@@ -1,25 +1,41 @@
1
- # ── Supabase ──────────────────────────────────────────────────────────────────
2
- # Get these from: https://supabase.com/dashboard → your project → Settings → API
3
- NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
4
- NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
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
- # Service role key — used only in server-side API routes (keep secret, never commit)
7
- # Get from: Settings API → service_role key
8
- SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
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
- # ── Windsor AI (optional — only needed for the automated report engine) ────────
11
- # Get from: https://windsor.ai → Settings → API Key
12
- WINDSOR_API_KEY=your-windsor-api-key-here
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
- # ── Cron / Automation (optional) ─────────────────────────────────────────────
15
- # A random secret you generate used to authenticate the /api/reports/generate endpoint
16
- # Generate one: openssl rand -hex 32
17
- CRON_SECRET=your-random-secret-here
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
- # The Supabase user ID of the portal owner (used by the cron report job)
20
- # Find it in: Supabase Dashboard → Authentication → Users
21
- PORTAL_USER_ID=your-supabase-user-uuid-here
25
+ # Optional BYO connection metadata
26
+ AGENCY_PORTAL_BYO_CONNECTIONS_JSON=
22
27
 
23
- # ── Agency settings ───────────────────────────────────────────────────────────
24
- # Monthly revenue target shown on the dashboard (in your local currency)
25
- NEXT_PUBLIC_REVENUE_TARGET=10000
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: readAdapterConfig(),
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
- integrations: groupIntegrationsByLane(integrations)
22
- }
25
+ settings,
26
+ workspace: buildPortalWorkspace({ config, adapters, integrations: settings.integrations })
23
27
  });
24
28
  }
25
29
  export {
@@ -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
 
@@ -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 { portalCapabilities } from "@/lib/domain/portal";
6
+ import { groupIntegrationsByLane } from "@/lib/domain/integrations";
7
+ import { buildPortalWorkspace } from "@/lib/domain/portal";
6
8
  import Link from "next/link";
7
- const nav = [
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 persistence = describePersistenceAdapter();
20
- const auth = describeAuthAdapter();
21
- const payments = describePaymentAdapter();
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
- <aside className="sidebar">
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
- <div className="utility-bar">
39
- <div>
40
- <strong>Production workspace</strong>
41
- <span>Local customization, Vercel deployment, bridge-ready integrations.</span>
42
- </div>
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
- {quickActions.map((action) => <button type="button" key={action}>{action}</button>)}
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
  };