@fayz-ai/plugin-marketing 0.9.1 → 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.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ import { type MarketingCurrency } from './lib/format';
6
6
  import { type MarketingDomain, type MarketingDomainModules } from './lib/presets';
7
7
  import type { AcquisitionChannel, ConversionModel } from './types';
8
8
  export interface MarketingPluginOptions {
9
+ /** Where the module's sub-nav lives. MarketingPage already reads it. */
10
+ moduleNav?: 'rail' | 'tabs' | 'sidebar';
9
11
  /** Vertical preset for conversion model + channels + module defaults. */
10
12
  domain?: MarketingDomain;
11
13
  /** Override the conversion model (else taken from the domain preset). */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAO/F,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EACV,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,cAAc,CAAA;AAIrB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAOjE,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACvE,OAAO,EAAqB,KAAK,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAA;AACpG,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAclE,MAAM,WAAW,sBAAsB;IACrC,yEAAyE;IACzE,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,yEAAyE;IACzE,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,6EAA6E;IAC7E,cAAc,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAA;IAC1D,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,CAAA;IACxC,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,oEAAoE;IACpE,WAAW,CAAC,EAAE,sBAAsB,CAAA;IACpC;6DACyD;IACzD,QAAQ,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACxE;AA8CD,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,cAAc,CAsTtF;AAED,YAAY,EACV,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,eAAe,GAChB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,iBAAiB,CAAA;AACjE,YAAY,EACV,sBAAsB,EACtB,WAAW,EACX,WAAW,EACX,aAAa,EACb,UAAU,EACV,UAAU,EACV,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,gCAAgC,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,oCAAoC,EAAE,MAAM,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAO/F,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EACV,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,cAAc,CAAA;AAIrB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAOjE,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACvE,OAAO,EAAqB,KAAK,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAA;AACpG,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAclE,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACvC,yEAAyE;IACzE,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,yEAAyE;IACzE,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,6EAA6E;IAC7E,cAAc,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAA;IAC1D,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,CAAA;IACxC,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,oEAAoE;IACpE,WAAW,CAAC,EAAE,sBAAsB,CAAA;IACpC;6DACyD;IACzD,QAAQ,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACxE;AA+CD,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,cAAc,CA0TtF;AAED,YAAY,EACV,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,eAAe,GAChB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,iBAAiB,CAAA;AACjE,YAAY,EACV,sBAAsB,EACtB,WAAW,EACX,WAAW,EACX,aAAa,EACb,UAAU,EACV,UAAU,EACV,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,gCAAgC,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,oCAAoC,EAAE,MAAM,wBAAwB,CAAA"}
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import React9, { useState, useEffect, useMemo, useCallback } from 'react';
2
- import { getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, useDataChanged, resolveDataProvider } from '@fayz-ai/core';
3
- import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, PermissionGate, useLimitGuard, useOrganizationStore, CrudFormPage, invalidateLimit, CrudListView } from '@fayz-ai/admin';
2
+ import { connectionSettings, connectionStatus, settingsStatus, createConnectionStore, getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, useDataChanged, resolveDataProvider } from '@fayz-ai/core';
3
+ import { createPluginContext, fayzConnectorOAuth, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, PermissionGate, useLimitGuard, useOrganizationStore, CrudFormPage, invalidateLimit, CrudListView } from '@fayz-ai/admin';
4
4
  import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, DashboardCanvas, SegmentedControl, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, Badge, SelectSeparator, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, Checkbox, toast, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
- import { Sparkles, History, CheckCircle2, AlertCircle, ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, ExternalLink, Pencil, Layers, MousePointerClick, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Send, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
6
+ import { History, CheckCircle2, AlertCircle, ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, ExternalLink, Sparkles, Pencil, Layers, MousePointerClick, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Send, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
7
7
  import { createStore } from 'zustand/vanilla';
8
8
 
9
9
  // src/index.ts
@@ -2740,7 +2740,7 @@ function buildNav(config, view, navigate) {
2740
2740
  }
2741
2741
  return items;
2742
2742
  }
2743
- function MarketingPage({ config, provider: provider2, store, contentProvider, contentStore }) {
2743
+ function MarketingPage({ config, provider: provider2, store: store3, contentProvider, contentStore }) {
2744
2744
  const t = useTranslation();
2745
2745
  const { view, direction, navigate } = useModuleNavigation("/marketing", {
2746
2746
  overview: 0,
@@ -2759,11 +2759,11 @@ function MarketingPage({ config, provider: provider2, store, contentProvider, co
2759
2759
  "blog-cat": 1
2760
2760
  }, "overview");
2761
2761
  React9.useEffect(() => {
2762
- void store.getState().load();
2763
- }, [store]);
2762
+ void store3.getState().load();
2763
+ }, [store3]);
2764
2764
  useDataChanged({ table: "plg_marketing_campaigns" }, () => {
2765
- void store.getState().refresh();
2766
- }, [store]);
2765
+ void store3.getState().refresh();
2766
+ }, [store3]);
2767
2767
  const isOverview = view === "overview";
2768
2768
  const nav = buildNav(config, view, navigate);
2769
2769
  const blogPostEntity = React9.useMemo(() => buildBlogPostEntity(), []);
@@ -2787,7 +2787,7 @@ function MarketingPage({ config, provider: provider2, store, contentProvider, co
2787
2787
  { id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
2788
2788
  { id: "overview", render: () => /* @__PURE__ */ jsx(OverviewView, {}) }
2789
2789
  ], "overview");
2790
- return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
2790
+ return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider: provider2, store: store3, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
2791
2791
  ModulePage,
2792
2792
  {
2793
2793
  title: config.labels.pageTitle,
@@ -4593,57 +4593,6 @@ CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authen
4593
4593
  CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
4594
4594
  CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
4595
4595
  `;
4596
- var MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
4597
- -- Control-plane state for the RankLayer connector rendered in Marketing \u2192
4598
- -- Integra\xE7\xF5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
4599
- -- (edge function + RankLayer API calls) lands via an external PR \u2014 see
4600
- -- src/integrations/ranklayer/RANKLAYER.md.
4601
-
4602
- CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
4603
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4604
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
4605
- api_key text,
4606
- site_domain text,
4607
- active boolean NOT NULL DEFAULT true,
4608
- last_sync_at timestamptz,
4609
- created_at timestamptz NOT NULL DEFAULT now(),
4610
- updated_at timestamptz NOT NULL DEFAULT now()
4611
- );
4612
- ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
4613
- CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
4614
- ON public.plg_marketing_ranklayer_integrations(tenant_id);
4615
-
4616
- CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
4617
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4618
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
4619
- integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
4620
- status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
4621
- message text,
4622
- fetched integer NOT NULL DEFAULT 0,
4623
- written integer NOT NULL DEFAULT 0,
4624
- created_at timestamptz NOT NULL DEFAULT now()
4625
- );
4626
- ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
4627
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
4628
- ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
4629
-
4630
- -- RLS: authenticated tenant CRUD
4631
- DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
4632
- DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
4633
- DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
4634
- DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
4635
- CREATE POLICY plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
4636
- CREATE POLICY plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
4637
- CREATE POLICY plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
4638
- CREATE POLICY plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
4639
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
4640
-
4641
- DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
4642
- DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
4643
- CREATE POLICY plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
4644
- CREATE POLICY plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
4645
- GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
4646
- `;
4647
4596
  var MIGRATION_005_ANALYTICS_BASE = `-- Marketing Plugin: analytics base tables (channels + campaigns).
4648
4597
  -- Prefix: plg_marketing_
4649
4598
  -- Channels are tenant-owned rows lazily seeded from the app's domain preset
@@ -5007,75 +4956,122 @@ WHERE p.kind = 'lead'
5007
4956
  AND COALESCE(p.is_active, true);
5008
4957
  GRANT SELECT ON public.v_marketing_lead_tag_events TO authenticated;
5009
4958
  `;
5010
- var INTEGRATIONS = "plg_marketing_ranklayer_integrations";
5011
- var SYNC_LOG = "plg_marketing_ranklayer_sync_log";
5012
- function getClient3() {
5013
- const supabase = getSupabaseClientOptional();
5014
- if (!supabase) throw new Error("[ranklayer] Supabase not initialized");
5015
- return supabase;
4959
+ var MIGRATION_010_RANKLAYER_ONTO_CONNECTIONS = `-- ============================================================================
4960
+ -- Marketing 010 \u2014 RankLayer stops owning tables.
4961
+ --
4962
+ -- Retires plg_marketing_ranklayer_integrations and plg_marketing_ranklayer_sync_log.
4963
+ -- Both were the generic shape under a private name: tenant, active, one config
4964
+ -- field, last sync \u2014 plus a run history beside it. They are now rows of
4965
+ -- public.plg_connections / public.plg_sync_runs (packages/db 029), reached
4966
+ -- through createConnectionStore('ranklayer'). See FAY-1376.
4967
+ --
4968
+ -- Where each column went:
4969
+ -- integrations.site_domain \u2192 plg_connections.settings->>'siteDomain'
4970
+ -- integrations.active \u2192 plg_connections.active
4971
+ -- integrations.last_sync_at \u2192 plg_connections.last_sync_at
4972
+ -- UNIQUE (tenant_id) \u2192 UNIQUE (tenant_id, connector_id, instance_key),
4973
+ -- with instance_key = '' (RankLayer publishes to
4974
+ -- one site per tenant; a second site is a second
4975
+ -- instance_key the day it is asked for)
4976
+ -- sync_log.created_at \u2192 plg_sync_runs.started_at
4977
+ -- sync_log.status \u2192 plg_sync_runs.status
4978
+ -- sync_log.fetched/written \u2192 plg_sync_runs.fetched/written
4979
+ -- sync_log.message \u2192 plg_sync_runs.error on a failed run, and
4980
+ -- plg_sync_runs.stats->>'message' otherwise
4981
+ -- sync_log.integration_id \u2192 plg_sync_runs.connection_id
4982
+ --
4983
+ -- 004 (which created the pair) and 009 (which dropped its api_key column) are
4984
+ -- deleted rather than edited: their ids stay in the ledger of any pool that ran
4985
+ -- them, and this migration is what removes what they left behind. Nothing is in
4986
+ -- production, so this is a drop, not a data migration.
4987
+ --
4988
+ -- One thing is not merely moved: 004 granted INSERT on the sync log to
4989
+ -- \`authenticated\`, so any member of the tenant could forge publication history.
4990
+ -- plg_sync_runs carries no such grant \u2014 runs are written server-side. The
4991
+ -- exposure closes by moving.
4992
+ -- ============================================================================
4993
+
4994
+ DROP TABLE IF EXISTS public.plg_marketing_ranklayer_sync_log CASCADE;
4995
+ DROP TABLE IF EXISTS public.plg_marketing_ranklayer_integrations CASCADE;
4996
+ `;
4997
+
4998
+ // src/integrations/ranklayer/settings.ts
4999
+ var RANKLAYER_SETTINGS = [
5000
+ {
5001
+ key: "siteDomain",
5002
+ label: "Dom\xEDnio do site",
5003
+ description: "O site em que a RankLayer publica os artigos deste blog.",
5004
+ type: "text",
5005
+ placeholder: "exemplo.com.br",
5006
+ requiredToOperate: true
5007
+ }
5008
+ ];
5009
+
5010
+ // src/integrations/ranklayer/data/supabase.ts
5011
+ var RANKLAYER_CONNECTOR_ID = "ranklayer";
5012
+ var INSTANCE_KEY = "";
5013
+ function ranklayerConnectionStore() {
5014
+ return createConnectionStore(RANKLAYER_CONNECTOR_ID, {
5015
+ settings: RANKLAYER_SETTINGS,
5016
+ instanceKey: INSTANCE_KEY
5017
+ });
5016
5018
  }
5017
- function requireTenantId3() {
5018
- const tenantId = getActiveTenantId();
5019
- if (!tenantId) throw new Error("[ranklayer] No active tenant");
5020
- return tenantId;
5019
+ var store = ranklayerConnectionStore;
5020
+ function toIntegration(connection) {
5021
+ return {
5022
+ id: connection.id ?? "",
5023
+ siteDomain: connection.settings?.siteDomain || void 0,
5024
+ active: connection.active,
5025
+ lastSyncAt: connection.lastSyncAt ?? void 0
5026
+ };
5021
5027
  }
5022
- function toIntegration(row) {
5028
+ function toLogEntry(run) {
5023
5029
  return {
5024
- id: row.id,
5025
- apiKey: row.api_key ?? void 0,
5026
- siteDomain: row.site_domain ?? void 0,
5027
- active: row.active ?? true,
5028
- lastSyncAt: row.last_sync_at ?? void 0
5030
+ id: run.id ?? "",
5031
+ createdAt: run.startedAt ?? "",
5032
+ status: run.status === "success" ? "success" : "error",
5033
+ message: run.error ?? run.message ?? void 0,
5034
+ fetched: run.fetched ?? 0,
5035
+ written: run.written ?? 0
5029
5036
  };
5030
5037
  }
5031
5038
  function createRankLayerProvider() {
5032
5039
  return {
5033
5040
  async getIntegration() {
5034
- const tenantId = getActiveTenantId();
5035
- if (!tenantId) return null;
5036
- const { data } = await getClient3().from(INTEGRATIONS).select("*").eq("tenant_id", tenantId).maybeSingle();
5037
- return data ? toIntegration(data) : null;
5041
+ try {
5042
+ const connection = await store().get();
5043
+ return connection ? toIntegration(connection) : null;
5044
+ } catch {
5045
+ return null;
5046
+ }
5038
5047
  },
5039
5048
  async saveIntegration(values) {
5040
- const tenantId = requireTenantId3();
5041
- const payload = {
5042
- tenant_id: tenantId,
5043
- api_key: values.apiKey,
5044
- site_domain: values.siteDomain ?? null,
5045
- active: true,
5046
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
5047
- };
5048
- const { data, error } = await getClient3().from(INTEGRATIONS).upsert(payload, { onConflict: "tenant_id" }).select("*").single();
5049
- if (error) throw error;
5050
- return toIntegration(data);
5049
+ const saved = await store().save({
5050
+ settings: { siteDomain: values.siteDomain ?? null },
5051
+ active: true
5052
+ });
5053
+ return toIntegration(saved);
5051
5054
  },
5055
+ /**
5056
+ * Soft, where the old table hard-deleted the row. The publication history
5057
+ * survives a disconnect now — it is attributed to the connection, and a
5058
+ * tenant that reconnects next week should still see what was published.
5059
+ */
5052
5060
  async disconnect() {
5053
- const tenantId = getActiveTenantId();
5054
- if (!tenantId) return;
5055
- await getClient3().from(INTEGRATIONS).delete().eq("tenant_id", tenantId);
5061
+ await store().disconnect();
5056
5062
  },
5057
- // SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
5058
- // flow can be exercised. Replace with a real credential check in the PR.
5059
- async testConnection(values) {
5060
- if (!values.apiKey || values.apiKey.trim().length < 8) {
5061
- return { ok: false, message: "Informe uma API Key v\xE1lida da RankLayer." };
5063
+ async getSyncLog() {
5064
+ try {
5065
+ const runs = await store().listRuns({ limit: 20 });
5066
+ return runs.map(toLogEntry);
5067
+ } catch {
5068
+ return [];
5062
5069
  }
5063
- return { ok: true, message: "Chave aceita (valida\xE7\xE3o real pendente \u2014 integra\xE7\xE3o em prepara\xE7\xE3o)." };
5064
- },
5065
- async getSyncLog(integrationId) {
5066
- const { data } = await getClient3().from(SYNC_LOG).select("*").eq("integration_id", integrationId).order("created_at", { ascending: false }).limit(20);
5067
- return (data ?? []).map((r) => ({
5068
- id: r.id,
5069
- createdAt: r.created_at,
5070
- status: r.status,
5071
- message: r.message ?? void 0,
5072
- fetched: r.fetched ?? 0,
5073
- written: r.written ?? 0
5074
- }));
5075
5070
  }
5076
5071
  };
5077
5072
  }
5078
5073
  var provider = createRankLayerProvider();
5074
+ var store2 = ranklayerConnectionStore();
5079
5075
  function RankLayerExtraPanel() {
5080
5076
  const [integration, setIntegration] = useState(null);
5081
5077
  const [log, setLog] = useState([]);
@@ -5087,7 +5083,10 @@ function RankLayerExtraPanel() {
5087
5083
  const found = await provider.getIntegration();
5088
5084
  if (cancelled) return;
5089
5085
  setIntegration(found);
5090
- if (found) setLog(await provider.getSyncLog(found.id));
5086
+ if (found) {
5087
+ const rows = await provider.getSyncLog();
5088
+ if (!cancelled) setLog(rows);
5089
+ }
5091
5090
  } catch {
5092
5091
  } finally {
5093
5092
  if (!cancelled) setLoaded(true);
@@ -5097,35 +5096,27 @@ function RankLayerExtraPanel() {
5097
5096
  cancelled = true;
5098
5097
  };
5099
5098
  }, []);
5100
- if (!loaded || !integration) return null;
5099
+ if (!loaded || !integration?.active) return null;
5101
5100
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4 border-t pt-4", children: [
5102
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 rounded-md border border-dashed p-3", children: [
5103
- /* @__PURE__ */ jsx(Sparkles, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
5104
- /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
5105
- /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Sincroniza\xE7\xE3o de conte\xFAdo \u2014 em breve" }),
5106
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
5107
- "A publica\xE7\xE3o autom\xE1tica de p\xE1ginas e artigos de SEO com a RankLayer est\xE1 em prepara\xE7\xE3o.",
5108
- integration.siteDomain ? ` Dom\xEDnio configurado: ${integration.siteDomain}.` : "",
5109
- " Assim que a integra\xE7\xE3o estiver ativa, o hist\xF3rico de sincroniza\xE7\xF5es aparece aqui."
5110
- ] })
5111
- ] })
5101
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
5102
+ /* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
5103
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico de publica\xE7\xF5es" })
5112
5104
  ] }),
5113
- log.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
5114
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
5115
- /* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
5116
- /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
5105
+ log.length === 0 ? /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
5106
+ "Nenhuma publica\xE7\xE3o recebida at\xE9 agora. Quem envia \xE9 a RankLayer",
5107
+ integration.siteDomain ? ` (site ${integration.siteDomain})` : "",
5108
+ ": a cada envio, uma linha aparece aqui com a data e quantos artigos foram gravados."
5109
+ ] }) : /* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", title: r.message, children: [
5110
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
5111
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
5112
+ r.written,
5113
+ "/",
5114
+ r.fetched,
5115
+ " publicadas"
5117
5116
  ] }),
5118
- /* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
5119
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
5120
- /* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
5121
- r.written,
5122
- "/",
5123
- r.fetched,
5124
- " publicadas"
5125
- ] }),
5126
- r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
5127
- ] }, r.id)) })
5128
- ] })
5117
+ r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
5118
+ ] }, r.id)) }),
5119
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "A chave que a RankLayer usa para publicar aqui \xE9 emitida na plataforma Fayz e n\xE3o fica guardada neste aplicativo." })
5129
5120
  ] });
5130
5121
  }
5131
5122
  var RANKLAYER_MARK = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj48cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSI3IiBmaWxsPSIjMUYyNDMxIi8+PHBvbHlnb24gcG9pbnRzPSIxNiw1LjYgMjYuNCwxMS42IDE2LDE3LjYgNS42LDExLjYiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIxLjI1IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PGcgb3BhY2l0eT0iMC41NSI+PHBvbHlnb24gcG9pbnRzPSI1LjYsMTYuNCAxNiwyMi40IDI2LjQsMTYuNCAyNi40LDE4LjIgMTYsMjQuMiA1LjYsMTguMiIgZmlsbD0iI0ZGRkZGRiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjEuMjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L2c+PGcgb3BhY2l0eT0iMC4yOCI+PHBvbHlnb24gcG9pbnRzPSI1LjYsMjEuMiAxNiwyNy4yIDI2LjQsMjEuMiAyNi40LDIzIDE2LDI5IDUuNiwyMyIgZmlsbD0iI0ZGRkZGRiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjEuMjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L2c+PC9zdmc+";
@@ -5136,22 +5127,84 @@ var ranklayerConnectorDef = {
5136
5127
  description: "Publica\xE7\xE3o de conte\xFAdo e p\xE1ginas de SEO por servi\xE7o/bairro para ser encontrado no Google e em assistentes de IA.",
5137
5128
  icon: "Search",
5138
5129
  logoUrl: RANKLAYER_MARK,
5139
- authKind: "api-key",
5140
- fields: [
5141
- { key: "apiKey", label: "API Key", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" },
5142
- { key: "siteDomain", label: "Dom\xEDnio do site", type: "text", placeholder: "exemplo.com.br" }
5143
- ],
5144
- async getStatus() {
5145
- try {
5146
- const found = await provider.getIntegration();
5147
- return { connected: !!found?.apiKey, detail: found?.siteDomain };
5148
- } catch {
5149
- return { connected: false };
5130
+ // OAuth, not a pasted key: what the tenant is doing is linking their RankLayer
5131
+ // account, and a key typed into this app would be a second copy of a secret
5132
+ // the platform already holds — the exact shape migration 009 deleted.
5133
+ authKind: "oauth",
5134
+ // The catalogue files RankLayer under "Analytics & Marketing"
5135
+ // (docs/site/data/integrations.json).
5136
+ category: "analytics",
5137
+ // Inbound because RankLayer reaches us: it writes the articles in. The edit
5138
+ // that goes back is delivered by the Fayz ingress, not by this app.
5139
+ direction: "inbound",
5140
+ docsUrl: "https://ranklayer.app",
5141
+ // The write is grounded: RankLayer's server calls `blog_upsert_external_post`
5142
+ // (plugin-blog migrations §external), which INSERTs/UPDATEs plg_blog_posts and
5143
+ // refuses a credential not scoped to exactly one tenant.
5144
+ //
5145
+ // The read is grounded only as far as the managed automation below — sending
5146
+ // an edit back means reading the article. That push is declared and its
5147
+ // anti-loop guard exists (./outbound.ts `shouldPushToRankLayer`), but nothing
5148
+ // calls it: the ingress that would is the same one that does not exist. Kept
5149
+ // rather than dropped because the automation IS declared, and a permission
5150
+ // list that contradicts the automation next to it is the defect this whole
5151
+ // declaration exists to prevent.
5152
+ permissions: [
5153
+ {
5154
+ kind: "data",
5155
+ resource: "blog_post",
5156
+ actions: ["read", "write"],
5157
+ reason: "Publicar aqui os artigos que a RankLayer escreve, e ler os artigos daqui para mandar de volta as edi\xE7\xF5es que voc\xEA fizer."
5158
+ },
5159
+ {
5160
+ kind: "event",
5161
+ resource: "blog.post.*",
5162
+ actions: ["listen"],
5163
+ reason: "Saber quando um artigo \xE9 criado, editado, publicado ou apagado aqui."
5150
5164
  }
5151
- },
5152
- testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
5153
- saveConnection: async (values) => {
5154
- await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain });
5165
+ ],
5166
+ /**
5167
+ * No form. The connect step authorizes and nothing else; the site domain is
5168
+ * configuration and lives on the Configurações tab (./settings.ts).
5169
+ *
5170
+ * TODO(ranklayer): the consent screen only opens once the platform has a
5171
+ * `ranklayer` OAuth provider registered — client id/secret and the authorize
5172
+ * and token URLs, plus our redirect URI allow-listed on RankLayer's side. Until
5173
+ * then Conectar answers `config_missing` and the tenant is told, in as many
5174
+ * words, that this is not something they can fix. Nothing in this repository
5175
+ * changes when it lands.
5176
+ */
5177
+ startOAuth: fayzConnectorOAuth(RANKLAYER_CONNECTOR_ID),
5178
+ settings: RANKLAYER_SETTINGS,
5179
+ subscribesTo: ["blog.post.*"],
5180
+ managedAutomations: [
5181
+ { eventKey: "blog.post.updated", label: "Enviar para a RankLayer as edi\xE7\xF5es feitas no artigo aqui" }
5182
+ ],
5183
+ // Read from the row the list already batched — one request for N connectors.
5184
+ ...connectionStatus(
5185
+ RANKLAYER_CONNECTOR_ID,
5186
+ (connection) => {
5187
+ const found = connection ? toIntegration(connection) : null;
5188
+ return {
5189
+ connected: !!found?.active,
5190
+ detail: found?.siteDomain,
5191
+ plane: "sync",
5192
+ // "Conectado, falta configurar" while the domain is empty — which is
5193
+ // true, and is the difference between not ready and broken.
5194
+ ...settingsStatus(RANKLAYER_SETTINGS, connection)
5195
+ };
5196
+ },
5197
+ { store: () => store2 }
5198
+ ),
5199
+ ...connectionSettings(store2),
5200
+ /**
5201
+ * Called with no values: the credential is not ours to see, and the platform's
5202
+ * redirect route does not write this pool's `plg_connections` (FAY-1388) — the
5203
+ * app writes its own row, or the tenant authorizes, comes back, and reads "not
5204
+ * connected" forever. Bling shipped with the other belief and looped.
5205
+ */
5206
+ saveConnection: async () => {
5207
+ await store2.save({ active: true });
5155
5208
  },
5156
5209
  disconnect: () => provider.disconnect(),
5157
5210
  ExtraPanel: RankLayerExtraPanel
@@ -5573,7 +5626,8 @@ function resolveConfig(options) {
5573
5626
  channels: options?.channels ?? preset.channels,
5574
5627
  modules: { ...preset.modules, ...options?.modules },
5575
5628
  currency: { ...DEFAULT_CURRENCY, ...options?.currency },
5576
- labels: { ...DEFAULT_LABELS, ...options?.labels }
5629
+ labels: { ...DEFAULT_LABELS, ...options?.labels },
5630
+ moduleNav: options?.moduleNav
5577
5631
  };
5578
5632
  return { config, domain };
5579
5633
  }
@@ -5593,8 +5647,8 @@ function createMarketingPlugin(options) {
5593
5647
  registerTranslations(marketingLocales);
5594
5648
  const { config, domain } = resolveConfig(options);
5595
5649
  const provider2 = options?.dataProvider ?? createSafeProvider(config, domain, options);
5596
- const store = createMarketingStore(provider2);
5597
- const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store });
5650
+ const store3 = createMarketingStore(provider2);
5651
+ const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store: store3 });
5598
5652
  const contentProvider = options?.contentPlanner?.dataProvider ?? createSafeDataProvider(
5599
5653
  () => createSupabaseContentPlannerProvider(),
5600
5654
  () => createMockContentPlannerProvider()
@@ -5602,8 +5656,8 @@ function createMarketingPlugin(options) {
5602
5656
  const contentStore = createContentPlannerStore(contentProvider);
5603
5657
  const PageComponent = () => React9.createElement(
5604
5658
  MarketingContextProvider,
5605
- { config, provider: provider2, store },
5606
- React9.createElement(MarketingPage, { config, provider: provider2, store, contentProvider, contentStore })
5659
+ { config, provider: provider2, store: store3 },
5660
+ React9.createElement(MarketingPage, { config, provider: provider2, store: store3, contentProvider, contentStore })
5607
5661
  );
5608
5662
  PageComponent.displayName = "MarketingPage";
5609
5663
  const marketingRegistries = [
@@ -5612,7 +5666,7 @@ function createMarketingPlugin(options) {
5612
5666
  ];
5613
5667
  const SettingsComponent = () => React9.createElement(
5614
5668
  MarketingContextProvider,
5615
- { config, provider: provider2, store },
5669
+ { config, provider: provider2, store: store3 },
5616
5670
  React9.createElement(PluginSettingsPanel, {
5617
5671
  title: config.labels.settings ?? config.labels.pageTitle,
5618
5672
  subtitle: config.labels.pageSubtitle,
@@ -5783,12 +5837,6 @@ function createMarketingPlugin(options) {
5783
5837
  sql: MIGRATION_003_RECORDING_OPS,
5784
5838
  description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
5785
5839
  },
5786
- {
5787
- id: "marketing-004-ranklayer",
5788
- version: "1.3.0",
5789
- sql: MIGRATION_004_RANKLAYER,
5790
- description: "RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)"
5791
- },
5792
5840
  {
5793
5841
  id: "marketing-005-analytics-base",
5794
5842
  version: "1.4.0",
@@ -5812,10 +5860,17 @@ function createMarketingPlugin(options) {
5812
5860
  version: "1.5.0",
5813
5861
  sql: MIGRATION_008_LANDING_PAGES,
5814
5862
  description: "Sites + landing pages (site + route + lead tag) with tenant RLS, v_marketing_landing_pages and v_marketing_lead_tag_events"
5863
+ },
5864
+ {
5865
+ id: "marketing-010-ranklayer-onto-connections",
5866
+ version: "1.6.0",
5867
+ sql: MIGRATION_010_RANKLAYER_ONTO_CONNECTIONS,
5868
+ description: "Retire plg_marketing_ranklayer_integrations + _sync_log \u2014 the connection and its publication history are rows of plg_connections/plg_sync_runs (packages/db 029)"
5815
5869
  }
5816
5870
  ],
5817
- // RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
5818
- // (control plane only); the real sync lands via an external PR (RANKLAYER.md).
5871
+ // RankLayer SEO connector — renders in the Integrações settings tab. A push
5872
+ // connector: RankLayer writes posts in and subscribes to blog.post.* to hear
5873
+ // the edits back. See src/integrations/ranklayer/RANKLAYER.md.
5819
5874
  connectors: [ranklayerConnectorDef],
5820
5875
  aiTools: [
5821
5876
  {
@@ -5890,6 +5945,9 @@ function createMarketingPlugin(options) {
5890
5945
  permission: { feature: "marketing", action: "read" }
5891
5946
  }
5892
5947
  ],
5948
+ // Google Meu Negócio sits on the same shelf as RankLayer above: being
5949
+ // found locally, not being measured.
5950
+ integrations: ["google-my-business"],
5893
5951
  locales: marketingLocales
5894
5952
  };
5895
5953
  }