@fayz-ai/plugin-marketing 0.9.0 → 0.10.0

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.
Files changed (100) hide show
  1. package/README.md +3 -3
  2. package/dist/{MarketingContext.d.ts → context.d.ts} +5 -3
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/data/mock.d.ts +1 -1
  5. package/dist/data/mock.d.ts.map +1 -1
  6. package/dist/{registries.d.ts → data/registries.d.ts} +1 -1
  7. package/dist/data/registries.d.ts.map +1 -0
  8. package/dist/index.d.ts +6 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +241 -188
  11. package/dist/index.js.map +1 -1
  12. package/dist/integrations/ranklayer/connectorDef.d.ts +1 -1
  13. package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -1
  14. package/dist/integrations/ranklayer/data/supabase.d.ts +24 -6
  15. package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -1
  16. package/dist/integrations/ranklayer/outbound.d.ts +16 -0
  17. package/dist/integrations/ranklayer/outbound.d.ts.map +1 -0
  18. package/dist/integrations/ranklayer/settings.d.ts +12 -0
  19. package/dist/integrations/ranklayer/settings.d.ts.map +1 -0
  20. package/dist/integrations/ranklayer/types.d.ts +0 -1
  21. package/dist/integrations/ranklayer/types.d.ts.map +1 -1
  22. package/dist/lib/format.d.ts.map +1 -0
  23. package/dist/{presets.d.ts → lib/presets.d.ts} +1 -1
  24. package/dist/lib/presets.d.ts.map +1 -0
  25. package/dist/migrations/index.d.ts +1 -1
  26. package/dist/migrations/index.d.ts.map +1 -1
  27. package/dist/{MarketingPage.d.ts → views/MarketingPage.d.ts} +5 -5
  28. package/dist/views/MarketingPage.d.ts.map +1 -0
  29. package/dist/views/blog/BlogAdminView.d.ts.map +1 -1
  30. package/dist/views/content/ContentPlannerContext.d.ts +1 -1
  31. package/dist/views/content/ContentPlannerContext.d.ts.map +1 -1
  32. package/dist/views/dashboardWidgets.d.ts +1 -1
  33. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  34. package/package.json +6 -9
  35. package/dist/MarketingContext.d.ts.map +0 -1
  36. package/dist/MarketingPage.d.ts.map +0 -1
  37. package/dist/format.d.ts.map +0 -1
  38. package/dist/presets.d.ts.map +0 -1
  39. package/dist/registries.d.ts.map +0 -1
  40. package/src/MarketingContext.tsx +0 -42
  41. package/src/MarketingPage.tsx +0 -139
  42. package/src/components/MarketingBits.tsx +0 -131
  43. package/src/components/icons.tsx +0 -36
  44. package/src/data/blogEntities.tsx +0 -91
  45. package/src/data/capability.test.ts +0 -92
  46. package/src/data/contentMedia.ts +0 -25
  47. package/src/data/contentMock.ts +0 -253
  48. package/src/data/contentSupabase.ts +0 -263
  49. package/src/data/contentTypes.ts +0 -138
  50. package/src/data/mock.ts +0 -308
  51. package/src/data/supabase.ts +0 -613
  52. package/src/data/tables.ts +0 -33
  53. package/src/data/types.ts +0 -97
  54. package/src/format.ts +0 -40
  55. package/src/index.ts +0 -457
  56. package/src/integrations/ranklayer/RANKLAYER.md +0 -40
  57. package/src/integrations/ranklayer/connectorDef.tsx +0 -102
  58. package/src/integrations/ranklayer/data/supabase.ts +0 -106
  59. package/src/integrations/ranklayer/types.ts +0 -16
  60. package/src/locales/en.ts +0 -192
  61. package/src/locales/index.ts +0 -7
  62. package/src/locales/pt-BR.ts +0 -192
  63. package/src/migrations/000_plg_rename.sql +0 -15
  64. package/src/migrations/001_content_planner.sql +0 -94
  65. package/src/migrations/002_multi_platform.sql +0 -25
  66. package/src/migrations/003_recording_ops.sql +0 -25
  67. package/src/migrations/004_ranklayer.sql +0 -50
  68. package/src/migrations/005_analytics_base.sql +0 -60
  69. package/src/migrations/006_analytics_views.sql +0 -68
  70. package/src/migrations/007_agent_rpcs.sql +0 -119
  71. package/src/migrations/008_landing_pages.sql +0 -112
  72. package/src/migrations/index.ts +0 -601
  73. package/src/presets.ts +0 -119
  74. package/src/registries.ts +0 -62
  75. package/src/schema/index.ts +0 -106
  76. package/src/store.ts +0 -102
  77. package/src/types.ts +0 -153
  78. package/src/views/CampaignComposer.tsx +0 -89
  79. package/src/views/CampaignsView.tsx +0 -59
  80. package/src/views/ChannelDetailView.tsx +0 -59
  81. package/src/views/ChannelsView.tsx +0 -46
  82. package/src/views/FunnelView.tsx +0 -56
  83. package/src/views/LandingPageComposer.tsx +0 -201
  84. package/src/views/LandingPagesView.tsx +0 -172
  85. package/src/views/OverviewView.tsx +0 -24
  86. package/src/views/SettingsView.tsx +0 -72
  87. package/src/views/blog/BlogAdminView.tsx +0 -99
  88. package/src/views/blog/BlogEntityForm.tsx +0 -101
  89. package/src/views/blog/useEntityCrud.ts +0 -50
  90. package/src/views/content/ContentPlannerContext.tsx +0 -13
  91. package/src/views/content/ContentView.tsx +0 -908
  92. package/src/views/content/PlanBriefView.tsx +0 -268
  93. package/src/views/content/PostPage.tsx +0 -370
  94. package/src/views/content/PostSocialPreview.tsx +0 -212
  95. package/src/views/content/RecordingChecklist.tsx +0 -145
  96. package/src/views/content/contentBits.tsx +0 -114
  97. package/src/views/content/contentStore.ts +0 -160
  98. package/src/views/content/templates.ts +0 -172
  99. package/src/views/dashboardWidgets.tsx +0 -173
  100. /package/dist/{format.d.ts → lib/format.d.ts} +0 -0
package/src/data/types.ts DELETED
@@ -1,97 +0,0 @@
1
- import type {
2
- Campaign,
3
- CampaignStatus,
4
- ChannelPerformance,
5
- DateRangeKey,
6
- FunnelStage,
7
- LandingPage,
8
- LandingPagePerf,
9
- LandingPageStatus,
10
- MarketingOverview,
11
- MarketingQuery,
12
- MarketingSite,
13
- } from '../types'
14
-
15
- // ---------------------------------------------------------------------------
16
- // Provider — everything the marketing surface reads/writes. The mock provider
17
- // computes analytics from a vertical-flavored seed; a Supabase-backed provider
18
- // lands later. Attribution + sites data can also come from sibling plugins via
19
- // the bridge seams below (defined now, injected later).
20
- // ---------------------------------------------------------------------------
21
-
22
- export interface SaveCampaignInput {
23
- id?: string
24
- name: string
25
- channelId: string
26
- status: CampaignStatus
27
- start: string
28
- end?: string
29
- spend: number
30
- }
31
-
32
- export interface SaveLandingPageInput {
33
- id?: string
34
- siteId: string
35
- name: string
36
- route: string
37
- leadTags?: string[]
38
- status?: LandingPageStatus
39
- prompt?: string
40
- isActive?: boolean
41
- }
42
-
43
- export interface MarketingDataProvider {
44
- overview(query: MarketingQuery): Promise<MarketingOverview>
45
- channelPerformance(query: MarketingQuery): Promise<ChannelPerformance[]>
46
- listCampaigns(query: MarketingQuery): Promise<Campaign[]>
47
- getCampaign(id: string): Promise<Campaign | null>
48
- saveCampaign(input: SaveCampaignInput): Promise<Campaign>
49
- deleteCampaign(id: string): Promise<void>
50
- funnel(query: MarketingQuery): Promise<FunnelStage[]>
51
- landingPages(query: MarketingQuery): Promise<LandingPagePerf[]>
52
- saveLandingPage(input: SaveLandingPageInput): Promise<LandingPage>
53
- deleteLandingPage(id: string): Promise<void>
54
- /** Sites the tenant owns — the "site" half of a landing page. */
55
- listSites(): Promise<MarketingSite[]>
56
- /** Lead-tag catalog offered in the landing-page form (read-only). */
57
- listLeadTags(): Promise<Array<{ slug: string; name: string }>>
58
- /** Drop any provider-side read cache (external writes, e.g. agent RPCs). */
59
- invalidateCache?(): void
60
- }
61
-
62
- // ---------------------------------------------------------------------------
63
- // Bridge seams (optional DI). Apps inject these to swap mock numbers for real
64
- // conversions read from plugin-crm / plugin-agenda / plugin-orders, and real
65
- // landing-page performance read from plugin-sites. Mirrors the agenda↔financial
66
- // bridge pattern (plugins compose via DI, never direct imports).
67
- // ---------------------------------------------------------------------------
68
-
69
- export interface AttributionBridge {
70
- conversionsByChannel(
71
- query: MarketingQuery,
72
- ): Promise<Array<{ channelId: string; conversions: number; value: number }>>
73
- funnel(query: MarketingQuery): Promise<FunnelStage[]>
74
- }
75
-
76
- /**
77
- * Visit analytics for landing pages. The pages themselves are rows the tenant
78
- * owns here; the VISITS live wherever the site is deployed, so this reads them
79
- * back by (app id, route, period) — the contract the hosting platform's
80
- * analytics endpoint is expected to take. Nothing implements it yet: without a
81
- * bridge the surface shows the pages with visits pending ("—") rather than
82
- * fake zeros.
83
- */
84
- export interface SitesPerformanceBridge {
85
- visitsByRoute(query: {
86
- range: DateRangeKey
87
- pages: Array<{
88
- id: string
89
- /** app manifest id of the deploy (fayz_projects.app_id) */
90
- appId?: string
91
- /** platform project id (fayz_projects.fayz_project_id) */
92
- fayzProjectId?: string
93
- siteUrl?: string
94
- route: string
95
- }>
96
- }): Promise<Array<{ id: string; visits: number }>>
97
- }
package/src/format.ts DELETED
@@ -1,40 +0,0 @@
1
- export interface MarketingCurrency {
2
- code: string
3
- locale: string
4
- symbol: string
5
- }
6
-
7
- export const DEFAULT_CURRENCY: MarketingCurrency = { code: 'USD', locale: 'en-US', symbol: '$' }
8
-
9
- export function formatCurrency(value: number, currency: MarketingCurrency): string {
10
- try {
11
- return new Intl.NumberFormat(currency.locale, {
12
- style: 'currency',
13
- currency: currency.code,
14
- maximumFractionDigits: value >= 1000 ? 0 : 2,
15
- }).format(value)
16
- } catch {
17
- return `${currency.symbol}${Math.round(value).toLocaleString()}`
18
- }
19
- }
20
-
21
- export function formatNumber(value: number): string {
22
- return Math.round(value).toLocaleString()
23
- }
24
-
25
- export function formatPercent(value: number, digits = 1): string {
26
- return `${value.toFixed(digits)}%`
27
- }
28
-
29
- /** Compact form for big reach numbers (12.4k). */
30
- export function formatCompact(value: number): string {
31
- if (value >= 1000) return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)}k`
32
- return String(Math.round(value))
33
- }
34
-
35
- /** Percentage-point delta vs a previous value, signed. */
36
- export function trendDelta(current: number, previous: number): { pct: number; dir: 'up' | 'down' | 'neutral' } {
37
- if (previous === 0) return { pct: current > 0 ? 100 : 0, dir: current > 0 ? 'up' : 'neutral' }
38
- const pct = ((current - previous) / previous) * 100
39
- return { pct: Math.abs(pct), dir: pct > 0.5 ? 'up' : pct < -0.5 ? 'down' : 'neutral' }
40
- }
package/src/index.ts DELETED
@@ -1,457 +0,0 @@
1
- import React from 'react'
2
- import type { PluginManifest, PluginRegistryDef, PluginScope, VerticalId } from '@fayz-ai/core'
3
- import { createSafeDataProvider, registerTranslations } from '@fayz-ai/core'
4
- import { PluginSettingsPanel } from '@fayz-ai/saas'
5
- import { MarketingPage } from './MarketingPage'
6
- import { SettingsView } from './views/SettingsView'
7
- import { buildMarketingRegistries } from './registries'
8
- import { createMarketingDashboardWidgets } from './views/dashboardWidgets'
9
- import {
10
- MarketingContextProvider,
11
- type ResolvedMarketingConfig,
12
- type MarketingLabels,
13
- } from './MarketingContext'
14
- import type {
15
- AttributionBridge,
16
- MarketingDataProvider,
17
- SitesPerformanceBridge,
18
- } from './data/types'
19
- import { createMockMarketingProvider } from './data/mock'
20
- import { createSupabaseMarketingProvider } from './data/supabase'
21
- import { T } from './data/tables'
22
- import type { ContentPlannerProvider } from './data/contentTypes'
23
- import { createMockContentPlannerProvider } from './data/contentMock'
24
- import { createSupabaseContentPlannerProvider } from './data/contentSupabase'
25
- import { createMarketingStore } from './store'
26
- import { createContentPlannerStore } from './views/content/contentStore'
27
- import { MIGRATION_000_PLG_RENAME, MIGRATION_001_CONTENT_PLANNER, MIGRATION_002_MULTI_PLATFORM, MIGRATION_003_RECORDING_OPS, MIGRATION_004_RANKLAYER, MIGRATION_005_ANALYTICS_BASE, MIGRATION_006_ANALYTICS_VIEWS, MIGRATION_007_AGENT_RPCS, MIGRATION_008_LANDING_PAGES } from './migrations'
28
- import { ranklayerConnectorDef } from './integrations/ranklayer/connectorDef'
29
- import { DEFAULT_CURRENCY, type MarketingCurrency } from './format'
30
- import { MARKETING_PRESETS, type MarketingDomain, type MarketingDomainModules } from './presets'
31
- import type { AcquisitionChannel, ConversionModel } from './types'
32
- import { marketingLocales } from './locales'
33
-
34
- // ---------------------------------------------------------------------------
35
- // @fayz-ai/plugin-marketing — acquisition & conversion analytics. ONE plugin,
36
- // every vertical: a `domain` preset (or explicit conversion + channels) adapts
37
- // what a conversion is and where it's attributed from. Channels, campaigns,
38
- // funnel and landing-page CVR are computed generically.
39
- //
40
- // Ships a vertical-flavored mock today; real attribution comes via the
41
- // AttributionBridge / SitesPerformanceBridge seams (read from crm/agenda/
42
- // orders/sites). Outbound broadcasts + journeys are reserved behind flags.
43
- // ---------------------------------------------------------------------------
44
-
45
- export interface MarketingPluginOptions {
46
- /** Vertical preset for conversion model + channels + module defaults. */
47
- domain?: MarketingDomain
48
- /** Override the conversion model (else taken from the domain preset). */
49
- conversion?: ConversionModel
50
- /** Override the acquisition channels (else from the domain preset). */
51
- channels?: AcquisitionChannel[]
52
- modules?: Partial<MarketingDomainModules>
53
- currency?: Partial<MarketingCurrency>
54
- labels?: Partial<MarketingLabels>
55
- navPosition?: number
56
- navSection?: 'main' | 'secondary' | 'settings'
57
- navLabel?: string
58
- scope?: PluginScope
59
- verticalId?: VerticalId
60
- dataProvider?: MarketingDataProvider
61
- /** Content planner options (module toggled via `modules.contentPlanner`). */
62
- contentPlanner?: { dataProvider?: ContentPlannerProvider }
63
- /** App/vertical-owned settings registries rendered inside Marketing settings. */
64
- settingsRegistries?: PluginRegistryDef[]
65
- /** Optional DI to read real conversions (mounted later). */
66
- attributionBridge?: AttributionBridge
67
- /** Optional DI to read real landing-page visits (mounted later). */
68
- sitesBridge?: SitesPerformanceBridge
69
- /** Where the lead-tag catalog is read from for the landing-page form.
70
- * Defaults to plugin-crm's plg_crm_tags (slug/name). */
71
- leadTags?: { table?: string; valueField?: string; labelField?: string }
72
- }
73
-
74
- const DEFAULT_LABELS: MarketingLabels = {
75
- pageTitle: 'Marketing',
76
- pageSubtitle: 'Acquisition & conversion performance',
77
- overview: 'Overview',
78
- channels: 'Channels',
79
- campaigns: 'Campaigns',
80
- funnel: 'Funnel',
81
- landingPages: 'Landing pages',
82
- content: 'Content',
83
- blog: 'Blog',
84
- blogCategories: 'Categorias',
85
- settings: 'Settings',
86
- }
87
-
88
- function resolveConfig(options?: MarketingPluginOptions): { config: ResolvedMarketingConfig; domain: MarketingDomain } {
89
- const domain = options?.domain ?? 'agency'
90
- const preset = MARKETING_PRESETS[domain]
91
- const config: ResolvedMarketingConfig = {
92
- conversion: options?.conversion ?? preset.conversion,
93
- channels: options?.channels ?? preset.channels,
94
- modules: { ...preset.modules, ...options?.modules },
95
- currency: { ...DEFAULT_CURRENCY, ...options?.currency },
96
- labels: { ...DEFAULT_LABELS, ...options?.labels },
97
- }
98
- return { config, domain }
99
- }
100
-
101
- function createSafeProvider(
102
- config: ResolvedMarketingConfig,
103
- domain: MarketingDomain,
104
- options?: MarketingPluginOptions,
105
- ): MarketingDataProvider {
106
- return createSafeDataProvider(
107
- () => createSupabaseMarketingProvider({
108
- channels: config.channels,
109
- conversion: config.conversion,
110
- attributionBridge: options?.attributionBridge,
111
- sitesBridge: options?.sitesBridge,
112
- leadTags: options?.leadTags,
113
- }),
114
- () => createMockMarketingProvider({ channels: config.channels, conversion: config.conversion, domain }),
115
- )
116
- }
117
-
118
- export function createMarketingPlugin(options?: MarketingPluginOptions): PluginManifest {
119
- registerTranslations(marketingLocales)
120
- const { config, domain } = resolveConfig(options)
121
- const provider = options?.dataProvider ?? createSafeProvider(config, domain, options)
122
- const store = createMarketingStore(provider)
123
- const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store })
124
-
125
- // Content planner has its own provider seam: real Supabase CRUD (with mock
126
- // fallback) while the analytics provider stays mock-until-bridges.
127
- const contentProvider = options?.contentPlanner?.dataProvider ?? createSafeDataProvider(
128
- () => createSupabaseContentPlannerProvider(),
129
- () => createMockContentPlannerProvider(),
130
- )
131
- const contentStore = createContentPlannerStore(contentProvider)
132
-
133
- const PageComponent: React.ComponentType<unknown> = () =>
134
- React.createElement(MarketingContextProvider, { config, provider, store },
135
- React.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore }))
136
- PageComponent.displayName = 'MarketingPage'
137
-
138
- // Settings lives in the SDK-core central Settings area (not a module tab).
139
- // General preferences (channel tracking toggles) + a Channels registry tab,
140
- // mirroring how plugin-menu surfaces Allergens.
141
- const marketingRegistries = [
142
- ...buildMarketingRegistries(config.channels),
143
- ...(options?.settingsRegistries ?? []),
144
- ]
145
- const SettingsComponent: React.ComponentType<unknown> = () =>
146
- React.createElement(MarketingContextProvider, { config, provider, store },
147
- React.createElement(PluginSettingsPanel, {
148
- title: config.labels.settings ?? config.labels.pageTitle,
149
- subtitle: config.labels.pageSubtitle,
150
- generalSettings: React.createElement(SettingsView),
151
- registries: marketingRegistries,
152
- // Enables the "Integrações" tab (renders manifest connectors for this host).
153
- hostPluginId: 'marketing',
154
- routeBase: '/settings/marketing',
155
- }))
156
- SettingsComponent.displayName = 'MarketingSettings'
157
-
158
- return {
159
- id: 'marketing',
160
- name: options?.navLabel ?? config.labels.pageTitle,
161
- icon: 'Megaphone',
162
- version: '1.0.0',
163
- scope: options?.scope ?? 'universal',
164
- verticalId: options?.verticalId,
165
- // Admin content-planning surface (campaigns, content calendar) — targets the
166
- // saas/admin Panel world, not the ecommerce storefront/operator console.
167
- scaffolds: ['saas'],
168
- defaultEnabled: true,
169
- dependencies: [],
170
- declaredFeatures: [
171
- { id: 'marketing', label: config.labels.pageTitle, group: 'Engage' },
172
- ...(config.modules.landingPages ? [{ id: 'marketing.landing-pages', label: config.labels.landingPages, group: 'Engage' }] : []),
173
- ...(config.modules.contentPlanner ? [{ id: 'marketing.content', label: config.labels.content, group: 'Engage' }] : []),
174
- ...(config.modules.blog ? [{ id: 'marketing.blog', label: config.labels.blog, group: 'Engage' }] : []),
175
- ],
176
- declaredLimits: [
177
- ...(config.modules.campaigns ? [{ key: 'campaigns_active', label: 'Active campaigns', table: T.campaigns }] : []),
178
- ...(config.modules.contentPlanner ? [{ key: 'content_posts_month', label: 'Content posts / month', table: 'plg_marketing_content_posts', period: 'month' as const }] : []),
179
- ...(config.modules.blog ? [{ key: 'blog_posts', label: 'Blog posts', table: 'plg_blog_posts' }] : []),
180
- ],
181
- // Read-models for the agent's data primitives (searchRecords/queryData) —
182
- // mirrors agenda's `agenda:appointments`. Keys are stable ASCII; fields are
183
- // the provider's camelCase output shape.
184
- queryEntities: [
185
- ...(config.modules.campaigns ? [{
186
- key: 'marketing:campaigns',
187
- writable: false,
188
- entity: {
189
- name: 'Campaign',
190
- namePlural: 'Campaigns',
191
- icon: 'Megaphone',
192
- permission: { feature: 'marketing', action: 'read' as const },
193
- fields: [
194
- { key: 'name', label: 'Name', type: 'text' as const, searchable: true },
195
- { key: 'channelKey', label: 'Channel', type: 'text' as const, searchable: true },
196
- { key: 'channelLabel', label: 'Channel label', type: 'text' as const },
197
- { key: 'status', label: 'Status', type: 'text' as const },
198
- { key: 'startsAt', label: 'Starts at', type: 'text' as const },
199
- { key: 'endsAt', label: 'Ends at', type: 'text' as const },
200
- { key: 'spend', label: 'Spend', type: 'number' as const },
201
- { key: 'createdAt', label: 'Created at', type: 'text' as const },
202
- ],
203
- data: {
204
- table: 'v_marketing_campaigns',
205
- tenantScoped: true,
206
- searchColumns: ['name', 'channel_key'],
207
- },
208
- },
209
- }] : []),
210
- ...(config.modules.channels ? [{
211
- key: 'marketing:channels',
212
- writable: false,
213
- entity: {
214
- name: 'Marketing channel',
215
- namePlural: 'Marketing channels',
216
- icon: 'Radio',
217
- permission: { feature: 'marketing', action: 'read' as const },
218
- fields: [
219
- { key: 'channelKey', label: 'Key', type: 'text' as const, searchable: true },
220
- { key: 'label', label: 'Channel', type: 'text' as const, searchable: true },
221
- { key: 'kind', label: 'Type', type: 'text' as const },
222
- { key: 'isActive', label: 'Active', type: 'boolean' as const },
223
- { key: 'monthlySpend', label: 'Monthly spend', type: 'number' as const },
224
- ],
225
- data: {
226
- table: 'v_marketing_channels',
227
- tenantScoped: true,
228
- searchColumns: ['label', 'channel_key'],
229
- },
230
- },
231
- }] : []),
232
- ...(config.modules.contentPlanner ? [{
233
- key: 'marketing:contentPosts',
234
- writable: false,
235
- entity: {
236
- name: 'Content post',
237
- namePlural: 'Content posts',
238
- icon: 'FileText',
239
- permission: { feature: 'marketing.content', action: 'read' as const },
240
- fields: [
241
- { key: 'title', label: 'Title', type: 'text' as const, searchable: true },
242
- { key: 'format', label: 'Format', type: 'text' as const },
243
- { key: 'status', label: 'Status', type: 'text' as const },
244
- { key: 'weekNumber', label: 'Week', type: 'number' as const },
245
- { key: 'scheduledDate', label: 'Scheduled date', type: 'text' as const },
246
- { key: 'hook', label: 'Hook', type: 'text' as const, searchable: true },
247
- ],
248
- data: {
249
- table: T.contentPosts,
250
- tenantScoped: true,
251
- searchColumns: ['title', 'hook'],
252
- },
253
- },
254
- }] : []),
255
- ],
256
- declaredRpcs: [
257
- ...(config.modules.campaigns ? [{
258
- name: 'agent_marketing_create_campaign',
259
- kind: 'write' as const,
260
- description:
261
- 'Guarded campaign create: agent_guard (role→plan→campaigns_active cap), channel validated against the tenant channel set, audited.',
262
- audits: true,
263
- }] : []),
264
- ],
265
- navigation: [
266
- {
267
- section: options?.navSection ?? 'main',
268
- position: options?.navPosition ?? 5,
269
- label: options?.navLabel ?? config.labels.pageTitle,
270
- route: '/marketing',
271
- icon: 'Megaphone',
272
- permission: { feature: 'marketing', action: 'read' as const },
273
- },
274
- ],
275
- routes: [
276
- {
277
- path: '/marketing',
278
- component: PageComponent,
279
- permission: { feature: 'marketing', action: 'read' as const },
280
- },
281
- ],
282
- widgets: [],
283
- dashboardWidgets,
284
- events: [
285
- { name: 'marketing.conversion.tracked', description: 'A conversion was attributed to a channel/campaign' },
286
- { name: 'marketing.campaign.created', description: 'A campaign was created' },
287
- { name: 'marketing.campaign.updated', description: 'A campaign was updated' },
288
- { name: 'marketing.channel.synced', description: 'A channel pulled fresh attribution data' },
289
- { name: 'marketing.content.plan.updated', description: 'A content plan (brief/config) was updated' },
290
- { name: 'marketing.content.post.created', description: 'A content post was created' },
291
- { name: 'marketing.content.post.updated', description: 'A content post was updated' },
292
- ],
293
- migrations: [
294
- {
295
- id: 'marketing-000-plg-rename',
296
- version: '1.0.0',
297
- sql: MIGRATION_000_PLG_RENAME,
298
- description: 'Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)',
299
- },
300
- {
301
- id: 'marketing-001-content-planner',
302
- version: '1.0.0',
303
- sql: MIGRATION_001_CONTENT_PLANNER,
304
- description: 'Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts',
305
- },
306
- {
307
- id: 'marketing-002-multi-platform',
308
- version: '1.1.0',
309
- sql: MIGRATION_002_MULTI_PLATFORM,
310
- description: 'Accounts publish to many platforms (platforms[]); posts gain optional platform targets',
311
- },
312
- {
313
- id: 'marketing-003-recording-ops',
314
- version: '1.2.0',
315
- sql: MIGRATION_003_RECORDING_OPS,
316
- description: 'Recording-day checklist + media asset URL on posts; content-media storage bucket',
317
- },
318
- {
319
- id: 'marketing-004-ranklayer',
320
- version: '1.3.0',
321
- sql: MIGRATION_004_RANKLAYER,
322
- description: 'RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)',
323
- },
324
- {
325
- id: 'marketing-005-analytics-base',
326
- version: '1.4.0',
327
- sql: MIGRATION_005_ANALYTICS_BASE,
328
- description: 'Analytics base: plg_marketing_channels + plg_marketing_campaigns with canonical tenant RLS',
329
- },
330
- {
331
- id: 'marketing-006-analytics-views',
332
- version: '1.4.0',
333
- sql: MIGRATION_006_ANALYTICS_VIEWS,
334
- description: 'Read views v_marketing_channels/_campaigns + generic spine-only v_marketing_attribution',
335
- },
336
- {
337
- id: 'marketing-007-agent-rpcs',
338
- version: '1.4.0',
339
- sql: MIGRATION_007_AGENT_RPCS,
340
- description: 'agent_marketing_create_campaign — guarded, audited server-plane campaign create',
341
- },
342
- {
343
- id: 'marketing-008-landing-pages',
344
- version: '1.5.0',
345
- sql: MIGRATION_008_LANDING_PAGES,
346
- description: 'Sites + landing pages (site + route + lead tag) with tenant RLS, v_marketing_landing_pages and v_marketing_lead_tag_events',
347
- },
348
- ],
349
- // RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
350
- // (control plane only); the real sync lands via an external PR (RANKLAYER.md).
351
- connectors: [ranklayerConnectorDef],
352
- aiTools: [
353
- {
354
- id: 'marketing.channel-performance',
355
- name: 'channelPerformance',
356
- description: 'Returns acquisition performance per channel (reach, conversions, CVR, spend, CPA).',
357
- icon: 'Radio',
358
- mode: 'read' as const,
359
- category: 'Marketing',
360
- parameters: {
361
- type: 'object' as const,
362
- properties: { range: { type: 'string' as const, enum: ['7d', '30d', '90d'] } },
363
- },
364
- suggestions: [{ label: 'Which channel converts best?' }, { label: 'What is my cost per acquisition?' }],
365
- permission: { feature: 'marketing', action: 'read' as const },
366
- },
367
- {
368
- id: 'marketing.top-channels',
369
- name: 'topChannels',
370
- description: 'Returns the top acquisition channels ranked by conversions.',
371
- icon: 'Trophy',
372
- mode: 'read' as const,
373
- category: 'Marketing',
374
- permission: { feature: 'marketing', action: 'read' as const },
375
- },
376
- {
377
- id: 'marketing.campaign-cvr',
378
- name: 'campaignCvr',
379
- description: 'Lists campaigns with their conversion rate for a date range.',
380
- icon: 'Percent',
381
- mode: 'read' as const,
382
- category: 'Marketing',
383
- parameters: {
384
- type: 'object' as const,
385
- properties: { range: { type: 'string' as const, enum: ['7d', '30d', '90d'] } },
386
- },
387
- permission: { feature: 'marketing', action: 'read' as const },
388
- },
389
- {
390
- id: 'marketing.create-campaign',
391
- name: 'createCampaign',
392
- description:
393
- 'Creates an acquisition campaign on a channel. Resolve the channel first via queryData/searchRecords on marketing channels (use its channel key). The user confirms before creating.',
394
- icon: 'Plus',
395
- mode: 'persist' as const,
396
- limitKey: 'campaigns_active',
397
- execution: { plane: 'server' as const, kind: 'rpc' as const, rpc: 'agent_marketing_create_campaign' },
398
- category: 'Marketing',
399
- parameters: {
400
- type: 'object' as const,
401
- properties: {
402
- name: { type: 'string' as const, description: 'Campaign name' },
403
- channel_key: { type: 'string' as const, description: "Acquisition channel key (e.g. 'instagram') or its label" },
404
- status: { type: 'string' as const, enum: ['draft', 'active', 'paused'], description: 'Defaults to draft' },
405
- starts_at: { type: 'string' as const, description: 'ISO start date-time (defaults to now)' },
406
- ends_at: { type: 'string' as const, description: 'ISO end date-time (optional)' },
407
- spend: { type: 'number' as const, description: 'Planned/committed spend (optional)' },
408
- },
409
- required: ['name', 'channel_key'],
410
- },
411
- suggestions: [{ label: 'Create an Instagram campaign for this month' }],
412
- permission: { feature: 'marketing', action: 'create' as const },
413
- },
414
- ],
415
- registries: marketingRegistries,
416
- settings: [
417
- {
418
- id: 'marketing',
419
- label: config.labels.pageTitle,
420
- icon: 'Megaphone',
421
- component: SettingsComponent,
422
- order: 20,
423
- permission: { feature: 'marketing', action: 'read' as const },
424
- },
425
- ],
426
- locales: marketingLocales,
427
- }
428
- }
429
-
430
- export type {
431
- MarketingDataProvider,
432
- AttributionBridge,
433
- SitesPerformanceBridge,
434
- } from './data/types'
435
- export type {
436
- AcquisitionChannel,
437
- ConversionModel,
438
- Campaign,
439
- ChannelPerformance,
440
- LandingPagePerf,
441
- } from './types'
442
- export { MARKETING_PRESETS, type MarketingDomain } from './presets'
443
- export { createMockMarketingProvider } from './data/mock'
444
- export { createSupabaseMarketingProvider } from './data/supabase'
445
- export type {
446
- ContentPlannerProvider,
447
- ContentPlan,
448
- ContentPost,
449
- SocialAccount,
450
- PostFormat,
451
- PostStatus,
452
- SaveContentPlanInput,
453
- SaveContentPostInput,
454
- SaveSocialAccountInput,
455
- } from './data/contentTypes'
456
- export { createMockContentPlannerProvider } from './data/contentMock'
457
- export { createSupabaseContentPlannerProvider } from './data/contentSupabase'
@@ -1,40 +0,0 @@
1
- # RankLayer connector
2
-
3
- Surfaces in **Marketing → settings → Integrações** (rendered by the shared
4
- `ConnectorsHub`). Follows the connector contract in
5
- `packages/core/src/integrations/index.ts` and mirrors the PlugBank precedent
6
- (`beauty-saas/src/plugins/openbanking`).
7
-
8
- ## Status: SCAFFOLD
9
-
10
- This ships the control plane only:
11
-
12
- - `connectorDef.tsx` — the `ConnectorDefinition` (api-key form: API Key + site
13
- domain, status badge, `ExtraPanel` with a "sync coming soon" note).
14
- - `data/supabase.ts` — persists the connection row and reads the sync log.
15
- - `../../migrations/004_ranklayer.sql` — `plg_marketing_ranklayer_integrations`
16
- + `plg_marketing_ranklayer_sync_log` (tenant RLS).
17
-
18
- `testConnection` currently accepts any non-empty API key and **no real sync
19
- runs** — RankLayer has no public API wired yet.
20
-
21
- ## What the RankLayer PR needs to fill in
22
-
23
- 1. **Edge function** `functions/ranklayer-sync/index.ts` holding the RankLayer
24
- API key server-side (move it out of the `api_key` column into a secret if the
25
- provider requires it), with actions:
26
- - `test_connection` — validate the key against RankLayer.
27
- - `sync` — the agreed direction. RankLayer generates/publishes SEO pages and
28
- articles, so the likely first flow is **inbound**: pull RankLayer-published
29
- articles and upsert them as `plg_blog_posts` (status `published`) so they
30
- appear on the site's `/blog`. Confirm direction with the RankLayer team.
31
- 2. **Real `testConnection` / `sync`** in `data/supabase.ts` — replace the stubs
32
- to invoke the edge function; write a `plg_marketing_ranklayer_sync_log` row
33
- per run (the `SyncRun` audit shape).
34
- 3. **ExtraPanel** — swap the "coming soon" note for a "Sync now" button + the
35
- real history table (the history rendering is already wired).
36
-
37
- The connector is already registered on the marketing manifest
38
- (`connectors: [ranklayerConnectorDef]`) and the Integrações tab is enabled
39
- (`hostPluginId: 'marketing'` on the settings panel), so no wiring changes are
40
- needed — only the data-plane implementation above.