@fayz-ai/plugin-marketing 0.2.1 → 0.8.1

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 (58) hide show
  1. package/README.md +2 -0
  2. package/dist/MarketingContext.d.ts +2 -0
  3. package/dist/MarketingContext.d.ts.map +1 -1
  4. package/dist/MarketingPage.d.ts.map +1 -1
  5. package/dist/data/blogEntities.d.ts +28 -0
  6. package/dist/data/blogEntities.d.ts.map +1 -0
  7. package/dist/data/contentSupabase.d.ts.map +1 -1
  8. package/dist/data/tables.d.ts +6 -0
  9. package/dist/data/tables.d.ts.map +1 -0
  10. package/dist/index.cjs +630 -127
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +636 -133
  14. package/dist/index.js.map +1 -1
  15. package/dist/integrations/ranklayer/connectorDef.d.ts +3 -0
  16. package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -0
  17. package/dist/integrations/ranklayer/data/supabase.d.ts +16 -0
  18. package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -0
  19. package/dist/integrations/ranklayer/types.d.ts +16 -0
  20. package/dist/integrations/ranklayer/types.d.ts.map +1 -0
  21. package/dist/migrations/index.d.ts +9 -3
  22. package/dist/migrations/index.d.ts.map +1 -1
  23. package/dist/presets.d.ts +2 -0
  24. package/dist/presets.d.ts.map +1 -1
  25. package/dist/schema/index.d.ts +44 -44
  26. package/dist/schema/index.d.ts.map +1 -1
  27. package/dist/schema.cjs +9 -9
  28. package/dist/schema.cjs.map +1 -1
  29. package/dist/schema.js +9 -9
  30. package/dist/schema.js.map +1 -1
  31. package/dist/views/blog/BlogAdminView.d.ts +5 -0
  32. package/dist/views/blog/BlogAdminView.d.ts.map +1 -0
  33. package/dist/views/blog/BlogEntityForm.d.ts +11 -0
  34. package/dist/views/blog/BlogEntityForm.d.ts.map +1 -0
  35. package/dist/views/blog/useEntityCrud.d.ts +29 -0
  36. package/dist/views/blog/useEntityCrud.d.ts.map +1 -0
  37. package/package.json +8 -5
  38. package/src/MarketingContext.tsx +2 -0
  39. package/src/MarketingPage.tsx +22 -1
  40. package/src/data/blogEntities.tsx +91 -0
  41. package/src/data/contentSupabase.ts +18 -17
  42. package/src/data/tables.ts +7 -0
  43. package/src/index.ts +27 -3
  44. package/src/integrations/ranklayer/RANKLAYER.md +40 -0
  45. package/src/integrations/ranklayer/connectorDef.tsx +98 -0
  46. package/src/integrations/ranklayer/data/supabase.ts +106 -0
  47. package/src/integrations/ranklayer/types.ts +16 -0
  48. package/src/migrations/000_plg_rename.sql +15 -0
  49. package/src/migrations/001_content_planner.sql +48 -48
  50. package/src/migrations/002_multi_platform.sql +5 -5
  51. package/src/migrations/003_recording_ops.sql +10 -10
  52. package/src/migrations/004_ranklayer.sql +50 -0
  53. package/src/migrations/index.ts +152 -74
  54. package/src/presets.ts +5 -3
  55. package/src/schema/index.ts +9 -9
  56. package/src/views/blog/BlogAdminView.tsx +99 -0
  57. package/src/views/blog/BlogEntityForm.tsx +92 -0
  58. package/src/views/blog/useEntityCrud.ts +50 -0
package/dist/index.cjs CHANGED
@@ -219,11 +219,11 @@ function ChannelDetailView({ channelId, onBack }) {
219
219
  const range = useMarketingStore((s) => s.range);
220
220
  const channels = useMarketingStore((s) => s.channels);
221
221
  const lookup = useChannelLookup();
222
- const provider = useMarketingProvider();
222
+ const provider2 = useMarketingProvider();
223
223
  const [campaigns, setCampaigns] = React8__default.default.useState([]);
224
224
  React8__default.default.useEffect(() => {
225
- void provider.listCampaigns({ range, channelId }).then(setCampaigns);
226
- }, [provider, range, channelId]);
225
+ void provider2.listCampaigns({ range, channelId }).then(setCampaigns);
226
+ }, [provider2, range, channelId]);
227
227
  const perf = channels.find((c) => c.channelId === channelId);
228
228
  const channel = lookup.get(channelId);
229
229
  const columns = React8__default.default.useMemo(() => [
@@ -1968,7 +1968,7 @@ function toDraft2(post) {
1968
1968
  }
1969
1969
  function PostPage({ postId, onBack }) {
1970
1970
  const t = core.useTranslation();
1971
- const provider = useContentPlannerProvider();
1971
+ const provider2 = useContentPlannerProvider();
1972
1972
  const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
1973
1973
  const savePost = useContentPlannerStore((s) => s.savePost);
1974
1974
  const deletePost = useContentPlannerStore((s) => s.deletePost);
@@ -1985,13 +1985,13 @@ function PostPage({ postId, onBack }) {
1985
1985
  setDraft(toDraft2(storePost));
1986
1986
  return;
1987
1987
  }
1988
- void provider.getPost(postId).then((fetched) => {
1988
+ void provider2.getPost(postId).then((fetched) => {
1989
1989
  if (fetched) {
1990
1990
  setPost(fetched);
1991
1991
  setDraft(toDraft2(fetched));
1992
1992
  }
1993
1993
  });
1994
- }, [postId, storePost, post?.id, provider]);
1994
+ }, [postId, storePost, post?.id, provider2]);
1995
1995
  if (!post || !draft) {
1996
1996
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
1997
1997
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
@@ -2178,6 +2178,239 @@ function PostPage({ postId, onBack }) {
2178
2178
  post.id
2179
2179
  );
2180
2180
  }
2181
+
2182
+ // src/data/blogEntities.tsx
2183
+ var BLOG_POSTS_TABLE = "plg_blog_posts";
2184
+ var BLOG_CATEGORIES_TABLE = "plg_blog_categories";
2185
+ function buildBlogPostEntity() {
2186
+ const fields = [
2187
+ { key: "title", label: "Title", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
2188
+ { key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
2189
+ { key: "status", label: "Status", type: "select", options: ["draft", "published"], showInTable: true, defaultValue: "draft" },
2190
+ { key: "categoryId", label: "Category", type: "relation", relation: { table: BLOG_CATEGORIES_TABLE, valueField: "id", labelField: "name", tenantScoped: true }, showInTable: false },
2191
+ { key: "excerpt", label: "Excerpt", type: "textarea", span: 2, showInTable: false, placeholder: "Short summary shown on cards" },
2192
+ { key: "body", label: "Body", type: "markdown", span: 2, showInTable: false },
2193
+ { key: "coverImage", label: "Cover image", type: "image", showInTable: false, placeholder: "https://\u2026" },
2194
+ { key: "readTime", label: "Read time", type: "text", showInTable: false, placeholder: "5 min" },
2195
+ { key: "publishedAt", label: "Published at", type: "date", showInTable: true, sortable: true },
2196
+ { key: "authorName", label: "Author name", type: "text", showInTable: false },
2197
+ { key: "authorRole", label: "Author role", type: "text", showInTable: false, placeholder: "e.g. Cirurgi\xE3o-Dentista" },
2198
+ { key: "authorAvatarUrl", label: "Author avatar", type: "image", showInTable: false, placeholder: "https://\u2026" },
2199
+ { key: "authorBio", label: "Author bio", type: "textarea", span: 2, showInTable: false }
2200
+ ];
2201
+ return {
2202
+ name: "Post",
2203
+ namePlural: "Posts",
2204
+ icon: "Newspaper",
2205
+ fields,
2206
+ displayField: "title",
2207
+ subtitleField: "slug",
2208
+ facets: [{ field: "status", allLabel: "All" }],
2209
+ data: {
2210
+ table: BLOG_POSTS_TABLE,
2211
+ tenantScoped: true,
2212
+ searchColumns: ["title", "slug", "excerpt"]
2213
+ }
2214
+ };
2215
+ }
2216
+ function buildBlogCategoryEntity() {
2217
+ const fields = [
2218
+ { key: "name", label: "Name", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
2219
+ { key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: true, hint: "URL identifier" },
2220
+ { key: "description", label: "Description", type: "textarea", span: 2, showInTable: false }
2221
+ ];
2222
+ return {
2223
+ name: "Category",
2224
+ namePlural: "Categories",
2225
+ icon: "Tags",
2226
+ fields,
2227
+ displayField: "name",
2228
+ data: {
2229
+ table: BLOG_CATEGORIES_TABLE,
2230
+ tenantScoped: true,
2231
+ searchColumns: ["name", "slug"]
2232
+ }
2233
+ };
2234
+ }
2235
+ function useEntityCrud(entity) {
2236
+ const provider2 = React8.useMemo(() => core.resolveDataProvider(entity), [entity]);
2237
+ const [items, setItems] = React8.useState(null);
2238
+ const [total, setTotal] = React8.useState(0);
2239
+ const fetch = React8.useCallback(async (query = {}) => {
2240
+ try {
2241
+ const res = await provider2.list(query);
2242
+ setItems(res.data);
2243
+ setTotal(res.total);
2244
+ } catch (err) {
2245
+ console.warn("[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.", err);
2246
+ setItems([]);
2247
+ setTotal(0);
2248
+ }
2249
+ }, [provider2]);
2250
+ const getById = React8.useCallback(async (id) => {
2251
+ const res = await provider2.list({ filters: { id }, page: 1, pageSize: 1 });
2252
+ return res.data[0] ?? null;
2253
+ }, [provider2]);
2254
+ const create = React8.useCallback((data) => provider2.create(data), [provider2]);
2255
+ const update = React8.useCallback((id, data) => provider2.update(id, data), [provider2]);
2256
+ const remove = React8.useCallback((id) => provider2.remove(id), [provider2]);
2257
+ return { items, total, fetch, getById, create, update, remove };
2258
+ }
2259
+ function PostsList({ navigate }) {
2260
+ const entity = React8.useMemo(() => buildBlogPostEntity(), []);
2261
+ const { items, total, fetch } = useEntityCrud(entity);
2262
+ const [search, setSearch] = React8.useState("");
2263
+ const [statusFilter, setStatusFilter] = React8.useState();
2264
+ const [loadedOnce, setLoadedOnce] = React8.useState(false);
2265
+ React8.useEffect(() => {
2266
+ let active = true;
2267
+ Promise.resolve(
2268
+ fetch({ search: search || void 0, filters: statusFilter ? { status: statusFilter } : void 0 })
2269
+ ).finally(() => {
2270
+ if (active) setLoadedOnce(true);
2271
+ });
2272
+ return () => {
2273
+ active = false;
2274
+ };
2275
+ }, [search, statusFilter, fetch]);
2276
+ const facets = React8.useMemo(() => [{
2277
+ field: "status",
2278
+ allLabel: "All",
2279
+ options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }]
2280
+ }], []);
2281
+ return /* @__PURE__ */ jsxRuntime.jsx(
2282
+ saas.CrudListView,
2283
+ {
2284
+ entityDef: entity,
2285
+ items: loadedOnce ? items : null,
2286
+ total,
2287
+ search,
2288
+ onSearchChange: setSearch,
2289
+ searchPlaceholder: "Search posts\u2026",
2290
+ facets,
2291
+ activeFilters: { status: statusFilter },
2292
+ onFacetChange: (_field, value) => setStatusFilter(value),
2293
+ onNew: () => navigate("blog-new"),
2294
+ addLabel: "New post",
2295
+ onRowClick: (row) => navigate(`blog-post:${row.id}`)
2296
+ }
2297
+ );
2298
+ }
2299
+ function CategoriesList({ navigate }) {
2300
+ const entity = React8.useMemo(() => buildBlogCategoryEntity(), []);
2301
+ const { items, total, fetch } = useEntityCrud(entity);
2302
+ const [search, setSearch] = React8.useState("");
2303
+ const [loadedOnce, setLoadedOnce] = React8.useState(false);
2304
+ React8.useEffect(() => {
2305
+ let active = true;
2306
+ Promise.resolve(fetch({ search: search || void 0 })).finally(() => {
2307
+ if (active) setLoadedOnce(true);
2308
+ });
2309
+ return () => {
2310
+ active = false;
2311
+ };
2312
+ }, [search, fetch]);
2313
+ return /* @__PURE__ */ jsxRuntime.jsx(
2314
+ saas.CrudListView,
2315
+ {
2316
+ entityDef: entity,
2317
+ items: loadedOnce ? items : null,
2318
+ total,
2319
+ search,
2320
+ onSearchChange: setSearch,
2321
+ searchPlaceholder: "Search categories\u2026",
2322
+ onNew: () => navigate("blog-cat-new"),
2323
+ addLabel: "New category",
2324
+ onRowClick: (row) => navigate(`blog-cat:${row.id}`)
2325
+ }
2326
+ );
2327
+ }
2328
+ function BlogAdminView({ navigate }) {
2329
+ const { labels } = useMarketingConfig();
2330
+ const [lens, setLens] = React8.useState("posts");
2331
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
2332
+ /* @__PURE__ */ jsxRuntime.jsx(
2333
+ ui.SegmentedControl,
2334
+ {
2335
+ "aria-label": "Blog section",
2336
+ options: [
2337
+ { value: "posts", label: labels.blog },
2338
+ { value: "categories", label: labels.blogCategories }
2339
+ ],
2340
+ value: lens,
2341
+ onChange: (v) => setLens(v)
2342
+ }
2343
+ ),
2344
+ lens === "posts" ? /* @__PURE__ */ jsxRuntime.jsx(PostsList, { navigate }) : /* @__PURE__ */ jsxRuntime.jsx(CategoriesList, { navigate })
2345
+ ] });
2346
+ }
2347
+ function BlogEntityForm({
2348
+ entity,
2349
+ editId,
2350
+ parentLabel,
2351
+ onDone
2352
+ }) {
2353
+ const { getById, create, update, remove } = useEntityCrud(entity);
2354
+ const isEdit = !!editId;
2355
+ const namePlural = entity.namePlural ?? entity.name + "s";
2356
+ const [initialData, setInitialData] = React8__default.default.useState(isEdit ? null : {});
2357
+ React8__default.default.useEffect(() => {
2358
+ if (!editId) return;
2359
+ let cancelled = false;
2360
+ (async () => {
2361
+ const item = await getById(editId);
2362
+ if (!cancelled) setInitialData(item ?? {});
2363
+ })();
2364
+ return () => {
2365
+ cancelled = true;
2366
+ };
2367
+ }, [editId, getById]);
2368
+ async function save(values) {
2369
+ try {
2370
+ if (isEdit) await update(editId, values);
2371
+ else await create(values);
2372
+ ui.toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`);
2373
+ onDone();
2374
+ } catch (err) {
2375
+ ui.toast.error(`Failed to save ${entity.name.toLowerCase()}`);
2376
+ throw err;
2377
+ }
2378
+ }
2379
+ async function del() {
2380
+ if (!editId) return;
2381
+ try {
2382
+ await remove(editId);
2383
+ ui.toast.success(`${entity.name} deleted`);
2384
+ onDone();
2385
+ } catch {
2386
+ ui.toast.error(`Failed to delete ${entity.name.toLowerCase()}`);
2387
+ }
2388
+ }
2389
+ const title = isEdit ? initialData?.[entity.displayField ?? "name"] ?? entity.name : `New ${entity.name.toLowerCase()}`;
2390
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
2391
+ /* @__PURE__ */ jsxRuntime.jsx(
2392
+ ui.SubpageHeader,
2393
+ {
2394
+ title: String(title),
2395
+ onBack: onDone,
2396
+ parentLabel,
2397
+ actions: isEdit ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", className: "text-destructive hover:text-destructive", onClick: del, children: "Delete" }) : void 0
2398
+ }
2399
+ ),
2400
+ initialData !== null && /* @__PURE__ */ jsxRuntime.jsx(
2401
+ saas.CrudFormPage,
2402
+ {
2403
+ entityDef: entity,
2404
+ mode: isEdit ? "edit" : "create",
2405
+ initialData,
2406
+ namePlural,
2407
+ onCancel: onDone,
2408
+ onSubmit: save,
2409
+ hideBreadcrumb: true
2410
+ }
2411
+ )
2412
+ ] });
2413
+ }
2181
2414
  function buildNav(config, view, navigate) {
2182
2415
  const items = [
2183
2416
  { id: "overview", label: config.labels.overview, icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
@@ -2209,9 +2442,18 @@ function buildNav(config, view, navigate) {
2209
2442
  onClick: () => navigate("content")
2210
2443
  });
2211
2444
  }
2445
+ if (config.modules.blog) {
2446
+ items.push({
2447
+ id: "blog",
2448
+ label: config.labels.blog,
2449
+ icon: "Newspaper",
2450
+ active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
2451
+ onClick: () => navigate("blog")
2452
+ });
2453
+ }
2212
2454
  return items;
2213
2455
  }
2214
- function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
2456
+ function MarketingPage({ config, provider: provider2, store, contentProvider, contentStore }) {
2215
2457
  const t = core.useTranslation();
2216
2458
  const { view, direction, navigate } = saas.useModuleNavigation("/marketing", {
2217
2459
  overview: 0,
@@ -2220,15 +2462,22 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
2220
2462
  funnel: 0,
2221
2463
  "landing-pages": 0,
2222
2464
  content: 0,
2465
+ blog: 0,
2223
2466
  "channel-detail": 1,
2224
2467
  "content-post": 1,
2225
- "content-brief": 1
2468
+ "content-brief": 1,
2469
+ "blog-new": 1,
2470
+ "blog-post": 1,
2471
+ "blog-cat-new": 1,
2472
+ "blog-cat": 1
2226
2473
  }, "overview");
2227
2474
  React8__default.default.useEffect(() => {
2228
2475
  void store.getState().load();
2229
2476
  }, [store]);
2230
2477
  const isOverview = view === "overview";
2231
2478
  const nav = buildNav(config, view, navigate);
2479
+ const blogPostEntity = React8__default.default.useMemo(() => buildBlogPostEntity(), []);
2480
+ const blogCategoryEntity = React8__default.default.useMemo(() => buildBlogCategoryEntity(), []);
2232
2481
  const quickActions = React8__default.default.useMemo(() => [
2233
2482
  { id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
2234
2483
  ], [t, config.labels.campaigns]);
@@ -2241,9 +2490,14 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
2241
2490
  { id: "content", render: () => /* @__PURE__ */ jsxRuntime.jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
2242
2491
  { id: "content-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
2243
2492
  { id: "content-brief", render: () => /* @__PURE__ */ jsxRuntime.jsx(PlanBriefView, { onBack: () => navigate("content") }) },
2493
+ { id: "blog", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogAdminView, { navigate }) },
2494
+ { id: "blog-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
2495
+ { id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
2496
+ { id: "blog-cat-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
2497
+ { id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
2244
2498
  { id: "overview", render: () => /* @__PURE__ */ jsxRuntime.jsx(OverviewView, {}) }
2245
2499
  ], "overview");
2246
- return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
2500
+ return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
2247
2501
  ui.ModulePage,
2248
2502
  {
2249
2503
  title: config.labels.pageTitle,
@@ -2518,7 +2772,7 @@ var AGENCY = {
2518
2772
  { id: "referral", label: "Referral", icon: "Users", kind: "referral" },
2519
2773
  { id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
2520
2774
  ],
2521
- modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
2775
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
2522
2776
  };
2523
2777
  var BEAUTY = {
2524
2778
  conversion: {
@@ -2536,7 +2790,7 @@ var BEAUTY = {
2536
2790
  { id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
2537
2791
  { id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
2538
2792
  ],
2539
- modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
2793
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false }
2540
2794
  };
2541
2795
  var RESTO = {
2542
2796
  conversion: {
@@ -2554,7 +2808,7 @@ var RESTO = {
2554
2808
  { id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
2555
2809
  { id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
2556
2810
  ],
2557
- modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
2811
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
2558
2812
  };
2559
2813
  var MARKETING_PRESETS = {
2560
2814
  agency: AGENCY,
@@ -3034,6 +3288,15 @@ function createMockContentPlannerProvider() {
3034
3288
  }
3035
3289
  };
3036
3290
  }
3291
+
3292
+ // src/data/tables.ts
3293
+ var T = {
3294
+ socialAccounts: "plg_marketing_social_accounts",
3295
+ contentPlans: "plg_marketing_content_plans",
3296
+ contentPosts: "plg_marketing_content_posts"
3297
+ };
3298
+
3299
+ // src/data/contentSupabase.ts
3037
3300
  function getClient() {
3038
3301
  const supabase = core.getSupabaseClientOptional();
3039
3302
  if (!supabase) throw new Error("Supabase not initialized");
@@ -3075,7 +3338,7 @@ function createSupabaseContentPlannerProvider() {
3075
3338
  async listAccounts() {
3076
3339
  const tenantId = core.getActiveTenantId();
3077
3340
  if (!tenantId) return [];
3078
- const { data } = await getClient().from("mkt_social_accounts").select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
3341
+ const { data } = await getClient().from(T.socialAccounts).select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
3079
3342
  return (data ?? []).map(toAccount);
3080
3343
  },
3081
3344
  async saveAccount(input) {
@@ -3085,11 +3348,11 @@ function createSupabaseContentPlannerProvider() {
3085
3348
  if (input.handle !== void 0) payload.handle = input.handle;
3086
3349
  if (input.platforms !== void 0) payload.platforms = input.platforms;
3087
3350
  if (input.isActive !== void 0) payload.is_active = input.isActive;
3088
- const { data: data2, error: error2 } = await getClient().from("mkt_social_accounts").update(payload).eq("id", input.id).select("*").single();
3351
+ const { data: data2, error: error2 } = await getClient().from(T.socialAccounts).update(payload).eq("id", input.id).select("*").single();
3089
3352
  if (error2) throw error2;
3090
3353
  return toAccount(data2);
3091
3354
  }
3092
- const { data, error } = await getClient().from("mkt_social_accounts").insert({
3355
+ const { data, error } = await getClient().from(T.socialAccounts).insert({
3093
3356
  tenant_id: requireTenantId(),
3094
3357
  name: input.name,
3095
3358
  handle: input.handle ?? null,
@@ -3100,17 +3363,17 @@ function createSupabaseContentPlannerProvider() {
3100
3363
  return toAccount(data);
3101
3364
  },
3102
3365
  async deleteAccount(id) {
3103
- const { error } = await getClient().from("mkt_social_accounts").delete().eq("id", id);
3366
+ const { error } = await getClient().from(T.socialAccounts).delete().eq("id", id);
3104
3367
  if (error) throw error;
3105
3368
  },
3106
3369
  async listPlans(accountId) {
3107
3370
  const tenantId = core.getActiveTenantId();
3108
3371
  if (!tenantId) return [];
3109
- const { data } = await getClient().from("mkt_content_plans").select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
3372
+ const { data } = await getClient().from(T.contentPlans).select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
3110
3373
  return (data ?? []).map(toPlan);
3111
3374
  },
3112
3375
  async getPlan(id) {
3113
- const { data } = await getClient().from("mkt_content_plans").select("*").eq("id", id).maybeSingle();
3376
+ const { data } = await getClient().from(T.contentPlans).select("*").eq("id", id).maybeSingle();
3114
3377
  return data ? toPlan(data) : null;
3115
3378
  },
3116
3379
  async savePlan(input) {
@@ -3126,12 +3389,12 @@ function createSupabaseContentPlannerProvider() {
3126
3389
  if (input.formats !== void 0) payload.formats = input.formats;
3127
3390
  if (input.weeklyFrequency !== void 0) payload.weekly_frequency = input.weeklyFrequency;
3128
3391
  if (input.briefMd !== void 0) payload.brief_md = input.briefMd;
3129
- const { data: data2, error: error2 } = await getClient().from("mkt_content_plans").update(payload).eq("id", input.id).select("*").single();
3392
+ const { data: data2, error: error2 } = await getClient().from(T.contentPlans).update(payload).eq("id", input.id).select("*").single();
3130
3393
  if (error2) throw error2;
3131
3394
  return toPlan(data2);
3132
3395
  }
3133
3396
  if (!input.accountId) throw new Error("accountId is required");
3134
- const { data, error } = await getClient().from("mkt_content_plans").insert({
3397
+ const { data, error } = await getClient().from(T.contentPlans).insert({
3135
3398
  tenant_id: requireTenantId(),
3136
3399
  account_id: input.accountId,
3137
3400
  name: input.name ?? "Novo plano",
@@ -3149,14 +3412,14 @@ function createSupabaseContentPlannerProvider() {
3149
3412
  return toPlan(data);
3150
3413
  },
3151
3414
  async deletePlan(id) {
3152
- await getClient().from("mkt_content_plans").delete().eq("id", id);
3415
+ await getClient().from(T.contentPlans).delete().eq("id", id);
3153
3416
  },
3154
3417
  async listPosts(planId) {
3155
- const { data } = await getClient().from("mkt_content_posts").select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
3418
+ const { data } = await getClient().from(T.contentPosts).select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
3156
3419
  return (data ?? []).map(toPost);
3157
3420
  },
3158
3421
  async getPost(id) {
3159
- const { data } = await getClient().from("mkt_content_posts").select("*").eq("id", id).maybeSingle();
3422
+ const { data } = await getClient().from(T.contentPosts).select("*").eq("id", id).maybeSingle();
3160
3423
  return data ? toPost(data) : null;
3161
3424
  },
3162
3425
  async savePost(input) {
@@ -3174,15 +3437,15 @@ function createSupabaseContentPlannerProvider() {
3174
3437
  if (input.hook !== void 0) payload.hook = input.hook;
3175
3438
  if (input.cta !== void 0) payload.cta = input.cta;
3176
3439
  if (input.contentMd !== void 0) payload.content_md = input.contentMd;
3177
- const { data: data2, error: error2 } = await getClient().from("mkt_content_posts").update(payload).eq("id", input.id).select("*").single();
3440
+ const { data: data2, error: error2 } = await getClient().from(T.contentPosts).update(payload).eq("id", input.id).select("*").single();
3178
3441
  if (error2) throw error2;
3179
3442
  return toPost(data2);
3180
3443
  }
3181
3444
  if (!input.planId) throw new Error("planId is required");
3182
3445
  const week = input.weekNumber ?? 1;
3183
- const { data: maxData } = await getClient().from("mkt_content_posts").select("position").eq("plan_id", input.planId).eq("week_number", week).order("position", { ascending: false }).limit(1);
3446
+ const { data: maxData } = await getClient().from(T.contentPosts).select("position").eq("plan_id", input.planId).eq("week_number", week).order("position", { ascending: false }).limit(1);
3184
3447
  const maxPos = maxData?.[0]?.position ?? -1;
3185
- const { data, error } = await getClient().from("mkt_content_posts").insert({
3448
+ const { data, error } = await getClient().from(T.contentPosts).insert({
3186
3449
  tenant_id: requireTenantId(),
3187
3450
  plan_id: input.planId,
3188
3451
  week_number: week,
@@ -3202,11 +3465,11 @@ function createSupabaseContentPlannerProvider() {
3202
3465
  return toPost(data);
3203
3466
  },
3204
3467
  async deletePost(id) {
3205
- await getClient().from("mkt_content_posts").delete().eq("id", id);
3468
+ await getClient().from(T.contentPosts).delete().eq("id", id);
3206
3469
  }
3207
3470
  };
3208
3471
  }
3209
- function createMarketingStore(provider) {
3472
+ function createMarketingStore(provider2) {
3210
3473
  return vanilla.createStore((set, get) => ({
3211
3474
  range: "30d",
3212
3475
  overview: null,
@@ -3219,11 +3482,11 @@ function createMarketingStore(provider) {
3219
3482
  const range = get().range;
3220
3483
  set({ loading: true });
3221
3484
  const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
3222
- provider.overview({ range }),
3223
- provider.channelPerformance({ range }),
3224
- provider.listCampaigns({ range }),
3225
- provider.funnel({ range }),
3226
- provider.landingPages({ range })
3485
+ provider2.overview({ range }),
3486
+ provider2.channelPerformance({ range }),
3487
+ provider2.listCampaigns({ range }),
3488
+ provider2.funnel({ range }),
3489
+ provider2.landingPages({ range })
3227
3490
  ]);
3228
3491
  set({ overview, channels, campaigns, funnel, landingPages, loading: false });
3229
3492
  },
@@ -3232,21 +3495,21 @@ function createMarketingStore(provider) {
3232
3495
  await get().load();
3233
3496
  },
3234
3497
  async saveCampaign(input) {
3235
- const created = await provider.saveCampaign(input);
3498
+ const created = await provider2.saveCampaign(input);
3236
3499
  await get().load();
3237
3500
  return created;
3238
3501
  },
3239
3502
  async deleteCampaign(id) {
3240
- await provider.deleteCampaign(id);
3503
+ await provider2.deleteCampaign(id);
3241
3504
  await get().load();
3242
3505
  }
3243
3506
  }));
3244
3507
  }
3245
- function createContentPlannerStore(provider) {
3508
+ function createContentPlannerStore(provider2) {
3246
3509
  async function loadPlansFor(accountId) {
3247
- const plans = await provider.listPlans(accountId);
3510
+ const plans = await provider2.listPlans(accountId);
3248
3511
  const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
3249
- const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
3512
+ const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
3250
3513
  return { plans, activePlan, posts };
3251
3514
  }
3252
3515
  return vanilla.createStore((set, get) => ({
@@ -3266,7 +3529,7 @@ function createContentPlannerStore(provider) {
3266
3529
  if (get().loading) return;
3267
3530
  set({ loading: true });
3268
3531
  try {
3269
- const accounts = await provider.listAccounts();
3532
+ const accounts = await provider2.listAccounts();
3270
3533
  const current = get().activeAccountId;
3271
3534
  const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
3272
3535
  const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
@@ -3297,19 +3560,19 @@ function createContentPlannerStore(provider) {
3297
3560
  },
3298
3561
  async selectPlan(planId) {
3299
3562
  const plan = get().plans.find((p) => p.id === planId) ?? null;
3300
- const posts = plan ? await provider.listPosts(plan.id) : [];
3563
+ const posts = plan ? await provider2.listPosts(plan.id) : [];
3301
3564
  set({ activePlan: plan, posts });
3302
3565
  },
3303
3566
  async saveAccount(input) {
3304
- const account = await provider.saveAccount(input);
3305
- const accounts = await provider.listAccounts();
3567
+ const account = await provider2.saveAccount(input);
3568
+ const accounts = await provider2.listAccounts();
3306
3569
  set({ accounts });
3307
3570
  if (!get().activeAccountId) await get().setAccount(account.id);
3308
3571
  return account;
3309
3572
  },
3310
3573
  async deleteAccount(id) {
3311
- await provider.deleteAccount(id);
3312
- const accounts = await provider.listAccounts();
3574
+ await provider2.deleteAccount(id);
3575
+ const accounts = await provider2.listAccounts();
3313
3576
  const wasActive = get().activeAccountId === id;
3314
3577
  set({ accounts });
3315
3578
  if (wasActive) {
@@ -3322,30 +3585,30 @@ function createContentPlannerStore(provider) {
3322
3585
  }
3323
3586
  },
3324
3587
  async savePlan(input) {
3325
- const plan = await provider.savePlan(input);
3588
+ const plan = await provider2.savePlan(input);
3326
3589
  const accountId = get().activeAccountId;
3327
3590
  if (accountId) {
3328
- const plans = await provider.listPlans(accountId);
3591
+ const plans = await provider2.listPlans(accountId);
3329
3592
  const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
3330
- const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
3593
+ const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
3331
3594
  set({ plans, activePlan, posts });
3332
3595
  }
3333
3596
  return plan;
3334
3597
  },
3335
3598
  async savePost(input) {
3336
- const post = await provider.savePost(input);
3599
+ const post = await provider2.savePost(input);
3337
3600
  const planId = get().activePlan?.id;
3338
3601
  if (planId && post.planId === planId) {
3339
- const posts = await provider.listPosts(planId);
3602
+ const posts = await provider2.listPosts(planId);
3340
3603
  set({ posts });
3341
3604
  }
3342
3605
  return post;
3343
3606
  },
3344
3607
  async deletePost(id) {
3345
- await provider.deletePost(id);
3608
+ await provider2.deletePost(id);
3346
3609
  const planId = get().activePlan?.id;
3347
3610
  if (planId) {
3348
- const posts = await provider.listPosts(planId);
3611
+ const posts = await provider2.listPosts(planId);
3349
3612
  set({ posts });
3350
3613
  }
3351
3614
  }
@@ -3353,13 +3616,29 @@ function createContentPlannerStore(provider) {
3353
3616
  }
3354
3617
 
3355
3618
  // src/migrations/index.ts
3619
+ var MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql \u2014 rename legacy marketing tables to plg_marketing_* for pools
3620
+ -- provisioned before the industry-pool rename. Guarded: fires only when legacy
3621
+ -- name exists and target does not, so fresh pools skip every branch.
3622
+ DO $$
3623
+ BEGIN
3624
+ IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
3625
+ ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
3626
+ END IF;
3627
+ IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
3628
+ ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
3629
+ END IF;
3630
+ IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
3631
+ ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
3632
+ END IF;
3633
+ END $$;
3634
+ `;
3356
3635
  var MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
3357
- -- Prefix: mkt_
3636
+ -- Prefix: plg_marketing_
3358
3637
  -- Social accounts + content plans + posts (Notion-style markdown pages).
3359
3638
 
3360
- CREATE TABLE IF NOT EXISTS public.mkt_social_accounts (
3639
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
3361
3640
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3362
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
3641
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3363
3642
  name text NOT NULL,
3364
3643
  handle text,
3365
3644
  platform text NOT NULL DEFAULT 'instagram'
@@ -3367,13 +3646,13 @@ CREATE TABLE IF NOT EXISTS public.mkt_social_accounts (
3367
3646
  is_active boolean NOT NULL DEFAULT true,
3368
3647
  created_at timestamptz NOT NULL DEFAULT now()
3369
3648
  );
3370
- ALTER TABLE public.mkt_social_accounts ENABLE ROW LEVEL SECURITY;
3371
- CREATE INDEX IF NOT EXISTS idx_mkt_social_accounts_tenant ON public.mkt_social_accounts(tenant_id);
3649
+ ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
3650
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
3372
3651
 
3373
- CREATE TABLE IF NOT EXISTS public.mkt_content_plans (
3652
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
3374
3653
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3375
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
3376
- account_id uuid NOT NULL REFERENCES public.mkt_social_accounts(id) ON DELETE CASCADE,
3654
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3655
+ account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
3377
3656
  name text NOT NULL,
3378
3657
  status text NOT NULL DEFAULT 'active'
3379
3658
  CHECK (status IN ('draft', 'active', 'archived')),
@@ -3388,14 +3667,14 @@ CREATE TABLE IF NOT EXISTS public.mkt_content_plans (
3388
3667
  created_at timestamptz NOT NULL DEFAULT now(),
3389
3668
  updated_at timestamptz NOT NULL DEFAULT now()
3390
3669
  );
3391
- ALTER TABLE public.mkt_content_plans ENABLE ROW LEVEL SECURITY;
3392
- CREATE INDEX IF NOT EXISTS idx_mkt_content_plans_tenant ON public.mkt_content_plans(tenant_id);
3393
- CREATE INDEX IF NOT EXISTS idx_mkt_content_plans_account ON public.mkt_content_plans(account_id);
3670
+ ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
3671
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
3672
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
3394
3673
 
3395
- CREATE TABLE IF NOT EXISTS public.mkt_content_posts (
3674
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
3396
3675
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3397
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
3398
- plan_id uuid NOT NULL REFERENCES public.mkt_content_plans(id) ON DELETE CASCADE,
3676
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3677
+ plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
3399
3678
  week_number integer NOT NULL DEFAULT 1,
3400
3679
  position integer NOT NULL DEFAULT 0,
3401
3680
  title text NOT NULL,
@@ -3410,49 +3689,50 @@ CREATE TABLE IF NOT EXISTS public.mkt_content_posts (
3410
3689
  created_at timestamptz NOT NULL DEFAULT now(),
3411
3690
  updated_at timestamptz NOT NULL DEFAULT now()
3412
3691
  );
3413
- ALTER TABLE public.mkt_content_posts ENABLE ROW LEVEL SECURITY;
3414
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_tenant ON public.mkt_content_posts(tenant_id);
3415
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_plan ON public.mkt_content_posts(plan_id, week_number, position);
3416
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_status ON public.mkt_content_posts(tenant_id, status);
3417
-
3418
- -- RLS policies for mkt_social_accounts
3419
- DROP POLICY IF EXISTS mkt_social_accounts_select ON public.mkt_social_accounts;
3420
- DROP POLICY IF EXISTS mkt_social_accounts_insert ON public.mkt_social_accounts;
3421
- DROP POLICY IF EXISTS mkt_social_accounts_update ON public.mkt_social_accounts;
3422
- DROP POLICY IF EXISTS mkt_social_accounts_delete ON public.mkt_social_accounts;
3423
- CREATE POLICY mkt_social_accounts_select ON public.mkt_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3424
- CREATE POLICY mkt_social_accounts_insert ON public.mkt_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3425
- CREATE POLICY mkt_social_accounts_update ON public.mkt_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3426
- CREATE POLICY mkt_social_accounts_delete ON public.mkt_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3427
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_social_accounts TO authenticated;
3428
-
3429
- -- RLS policies for mkt_content_plans
3430
- DROP POLICY IF EXISTS mkt_content_plans_select ON public.mkt_content_plans;
3431
- DROP POLICY IF EXISTS mkt_content_plans_insert ON public.mkt_content_plans;
3432
- DROP POLICY IF EXISTS mkt_content_plans_update ON public.mkt_content_plans;
3433
- DROP POLICY IF EXISTS mkt_content_plans_delete ON public.mkt_content_plans;
3434
- CREATE POLICY mkt_content_plans_select ON public.mkt_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3435
- CREATE POLICY mkt_content_plans_insert ON public.mkt_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3436
- CREATE POLICY mkt_content_plans_update ON public.mkt_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3437
- CREATE POLICY mkt_content_plans_delete ON public.mkt_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3438
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_content_plans TO authenticated;
3439
-
3440
- -- RLS policies for mkt_content_posts
3441
- DROP POLICY IF EXISTS mkt_content_posts_select ON public.mkt_content_posts;
3442
- DROP POLICY IF EXISTS mkt_content_posts_insert ON public.mkt_content_posts;
3443
- DROP POLICY IF EXISTS mkt_content_posts_update ON public.mkt_content_posts;
3444
- DROP POLICY IF EXISTS mkt_content_posts_delete ON public.mkt_content_posts;
3445
- CREATE POLICY mkt_content_posts_select ON public.mkt_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3446
- CREATE POLICY mkt_content_posts_insert ON public.mkt_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3447
- CREATE POLICY mkt_content_posts_update ON public.mkt_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3448
- CREATE POLICY mkt_content_posts_delete ON public.mkt_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3449
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_content_posts TO authenticated;`;
3692
+ ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
3693
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
3694
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
3695
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
3696
+
3697
+ -- RLS policies for plg_marketing_social_accounts
3698
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
3699
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
3700
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
3701
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
3702
+ CREATE POLICY plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3703
+ CREATE POLICY plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3704
+ CREATE POLICY plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3705
+ CREATE POLICY plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3706
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
3707
+
3708
+ -- RLS policies for plg_marketing_content_plans
3709
+ DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
3710
+ DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
3711
+ DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
3712
+ DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
3713
+ CREATE POLICY plg_marketing_content_plans_select ON public.plg_marketing_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3714
+ CREATE POLICY plg_marketing_content_plans_insert ON public.plg_marketing_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3715
+ CREATE POLICY plg_marketing_content_plans_update ON public.plg_marketing_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3716
+ CREATE POLICY plg_marketing_content_plans_delete ON public.plg_marketing_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3717
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
3718
+
3719
+ -- RLS policies for plg_marketing_content_posts
3720
+ DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
3721
+ DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
3722
+ DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
3723
+ DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
3724
+ CREATE POLICY plg_marketing_content_posts_select ON public.plg_marketing_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3725
+ CREATE POLICY plg_marketing_content_posts_insert ON public.plg_marketing_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3726
+ CREATE POLICY plg_marketing_content_posts_update ON public.plg_marketing_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3727
+ CREATE POLICY plg_marketing_content_posts_delete ON public.plg_marketing_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3728
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
3729
+ `;
3450
3730
  var MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
3451
3731
  -- An account is a brand/profile that publishes to MANY platforms (Instagram,
3452
3732
  -- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
3453
3733
  -- empty array means "inherit the account's platforms".
3454
3734
 
3455
- ALTER TABLE public.mkt_social_accounts
3735
+ ALTER TABLE public.plg_marketing_social_accounts
3456
3736
  ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
3457
3737
 
3458
3738
  -- Backfill from the legacy single-platform column, then drop it (also drops
@@ -3461,26 +3741,27 @@ DO $$
3461
3741
  BEGIN
3462
3742
  IF EXISTS (
3463
3743
  SELECT 1 FROM information_schema.columns
3464
- WHERE table_schema = 'public' AND table_name = 'mkt_social_accounts' AND column_name = 'platform'
3744
+ WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
3465
3745
  ) THEN
3466
- UPDATE public.mkt_social_accounts
3746
+ UPDATE public.plg_marketing_social_accounts
3467
3747
  SET platforms = ARRAY[platform]
3468
3748
  WHERE platform IS NOT NULL AND platforms = '{instagram}';
3469
- ALTER TABLE public.mkt_social_accounts DROP COLUMN platform;
3749
+ ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
3470
3750
  END IF;
3471
3751
  END $$;
3472
3752
 
3473
- ALTER TABLE public.mkt_content_posts
3474
- ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';`;
3753
+ ALTER TABLE public.plg_marketing_content_posts
3754
+ ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
3755
+ `;
3475
3756
  var MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
3476
3757
  -- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set
3477
3758
  -- companion for recording day. media_url: uploaded asset (static posts get a
3478
3759
  -- caption + final art instead of a script).
3479
3760
 
3480
- ALTER TABLE public.mkt_content_posts
3761
+ ALTER TABLE public.plg_marketing_content_posts
3481
3762
  ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
3482
3763
 
3483
- ALTER TABLE public.mkt_content_posts
3764
+ ALTER TABLE public.plg_marketing_content_posts
3484
3765
  ADD COLUMN IF NOT EXISTS media_url text;
3485
3766
 
3486
3767
  -- Public media bucket for content assets (mirrors the avatars-bucket pattern:
@@ -3489,14 +3770,213 @@ INSERT INTO storage.buckets (id, name, public)
3489
3770
  VALUES ('mkt-media', 'mkt-media', true)
3490
3771
  ON CONFLICT (id) DO NOTHING;
3491
3772
 
3492
- DROP POLICY IF EXISTS mkt_media_read ON storage.objects;
3493
- DROP POLICY IF EXISTS mkt_media_insert ON storage.objects;
3494
- DROP POLICY IF EXISTS mkt_media_update ON storage.objects;
3495
- DROP POLICY IF EXISTS mkt_media_delete ON storage.objects;
3496
- CREATE POLICY mkt_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
3497
- CREATE POLICY mkt_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
3498
- CREATE POLICY mkt_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
3499
- CREATE POLICY mkt_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');`;
3773
+ DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
3774
+ DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
3775
+ DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
3776
+ DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
3777
+ CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
3778
+ CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
3779
+ CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
3780
+ CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
3781
+ `;
3782
+ var MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
3783
+ -- Control-plane state for the RankLayer connector rendered in Marketing \u2192
3784
+ -- Integra\xE7\xF5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
3785
+ -- (edge function + RankLayer API calls) lands via an external PR \u2014 see
3786
+ -- src/integrations/ranklayer/RANKLAYER.md.
3787
+
3788
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
3789
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3790
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3791
+ api_key text,
3792
+ site_domain text,
3793
+ active boolean NOT NULL DEFAULT true,
3794
+ last_sync_at timestamptz,
3795
+ created_at timestamptz NOT NULL DEFAULT now(),
3796
+ updated_at timestamptz NOT NULL DEFAULT now()
3797
+ );
3798
+ ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
3799
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
3800
+ ON public.plg_marketing_ranklayer_integrations(tenant_id);
3801
+
3802
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
3803
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3804
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3805
+ integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
3806
+ status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
3807
+ message text,
3808
+ fetched integer NOT NULL DEFAULT 0,
3809
+ written integer NOT NULL DEFAULT 0,
3810
+ created_at timestamptz NOT NULL DEFAULT now()
3811
+ );
3812
+ ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
3813
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
3814
+ ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
3815
+
3816
+ -- RLS: authenticated tenant CRUD
3817
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
3818
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
3819
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
3820
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
3821
+ 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()));
3822
+ 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()));
3823
+ 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()));
3824
+ 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()));
3825
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
3826
+
3827
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
3828
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
3829
+ 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()));
3830
+ 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()));
3831
+ GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
3832
+ `;
3833
+ var INTEGRATIONS = "plg_marketing_ranklayer_integrations";
3834
+ var SYNC_LOG = "plg_marketing_ranklayer_sync_log";
3835
+ function getClient2() {
3836
+ const supabase = core.getSupabaseClientOptional();
3837
+ if (!supabase) throw new Error("[ranklayer] Supabase not initialized");
3838
+ return supabase;
3839
+ }
3840
+ function requireTenantId2() {
3841
+ const tenantId = core.getActiveTenantId();
3842
+ if (!tenantId) throw new Error("[ranklayer] No active tenant");
3843
+ return tenantId;
3844
+ }
3845
+ function toIntegration(row) {
3846
+ return {
3847
+ id: row.id,
3848
+ apiKey: row.api_key ?? void 0,
3849
+ siteDomain: row.site_domain ?? void 0,
3850
+ active: row.active ?? true,
3851
+ lastSyncAt: row.last_sync_at ?? void 0
3852
+ };
3853
+ }
3854
+ function createRankLayerProvider() {
3855
+ return {
3856
+ async getIntegration() {
3857
+ const tenantId = core.getActiveTenantId();
3858
+ if (!tenantId) return null;
3859
+ const { data } = await getClient2().from(INTEGRATIONS).select("*").eq("tenant_id", tenantId).maybeSingle();
3860
+ return data ? toIntegration(data) : null;
3861
+ },
3862
+ async saveIntegration(values) {
3863
+ const tenantId = requireTenantId2();
3864
+ const payload = {
3865
+ tenant_id: tenantId,
3866
+ api_key: values.apiKey,
3867
+ site_domain: values.siteDomain ?? null,
3868
+ active: true,
3869
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
3870
+ };
3871
+ const { data, error } = await getClient2().from(INTEGRATIONS).upsert(payload, { onConflict: "tenant_id" }).select("*").single();
3872
+ if (error) throw error;
3873
+ return toIntegration(data);
3874
+ },
3875
+ async disconnect() {
3876
+ const tenantId = core.getActiveTenantId();
3877
+ if (!tenantId) return;
3878
+ await getClient2().from(INTEGRATIONS).delete().eq("tenant_id", tenantId);
3879
+ },
3880
+ // SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
3881
+ // flow can be exercised. Replace with a real credential check in the PR.
3882
+ async testConnection(values) {
3883
+ if (!values.apiKey || values.apiKey.trim().length < 8) {
3884
+ return { ok: false, message: "Informe uma API Key v\xE1lida da RankLayer." };
3885
+ }
3886
+ return { ok: true, message: "Chave aceita (valida\xE7\xE3o real pendente \u2014 integra\xE7\xE3o em prepara\xE7\xE3o)." };
3887
+ },
3888
+ async getSyncLog(integrationId) {
3889
+ const { data } = await getClient2().from(SYNC_LOG).select("*").eq("integration_id", integrationId).order("created_at", { ascending: false }).limit(20);
3890
+ return (data ?? []).map((r) => ({
3891
+ id: r.id,
3892
+ createdAt: r.created_at,
3893
+ status: r.status,
3894
+ message: r.message ?? void 0,
3895
+ fetched: r.fetched ?? 0,
3896
+ written: r.written ?? 0
3897
+ }));
3898
+ }
3899
+ };
3900
+ }
3901
+ var provider = createRankLayerProvider();
3902
+ function RankLayerExtraPanel() {
3903
+ const [integration, setIntegration] = React8.useState(null);
3904
+ const [log, setLog] = React8.useState([]);
3905
+ const [loaded, setLoaded] = React8.useState(false);
3906
+ React8.useEffect(() => {
3907
+ let cancelled = false;
3908
+ (async () => {
3909
+ try {
3910
+ const found = await provider.getIntegration();
3911
+ if (cancelled) return;
3912
+ setIntegration(found);
3913
+ if (found) setLog(await provider.getSyncLog(found.id));
3914
+ } catch {
3915
+ } finally {
3916
+ if (!cancelled) setLoaded(true);
3917
+ }
3918
+ })();
3919
+ return () => {
3920
+ cancelled = true;
3921
+ };
3922
+ }, []);
3923
+ if (!loaded || !integration) return null;
3924
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 border-t pt-4", children: [
3925
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 rounded-md border border-dashed p-3", children: [
3926
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
3927
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
3928
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold", children: "Sincroniza\xE7\xE3o de conte\xFAdo \u2014 em breve" }),
3929
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
3930
+ "A publica\xE7\xE3o autom\xE1tica de p\xE1ginas e artigos de SEO com a RankLayer est\xE1 em prepara\xE7\xE3o.",
3931
+ integration.siteDomain ? ` Dom\xEDnio configurado: ${integration.siteDomain}.` : "",
3932
+ " Assim que a integra\xE7\xE3o estiver ativa, o hist\xF3rico de sincroniza\xE7\xF5es aparece aqui."
3933
+ ] })
3934
+ ] })
3935
+ ] }),
3936
+ log.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
3937
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3938
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.History, { className: "h-4 w-4 text-muted-foreground" }),
3939
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
3940
+ ] }),
3941
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
3942
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
3943
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto text-xs", children: [
3944
+ r.written,
3945
+ "/",
3946
+ r.fetched,
3947
+ " publicadas"
3948
+ ] }),
3949
+ r.status === "success" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
3950
+ ] }, r.id)) })
3951
+ ] })
3952
+ ] });
3953
+ }
3954
+ var ranklayerConnectorDef = {
3955
+ id: "ranklayer",
3956
+ hostPluginId: "marketing",
3957
+ name: "RankLayer",
3958
+ 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.",
3959
+ icon: "Search",
3960
+ authKind: "api-key",
3961
+ fields: [
3962
+ { key: "apiKey", label: "API Key", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" },
3963
+ { key: "siteDomain", label: "Dom\xEDnio do site", type: "text", placeholder: "exemplo.com.br" }
3964
+ ],
3965
+ async getStatus() {
3966
+ try {
3967
+ const found = await provider.getIntegration();
3968
+ return { connected: !!found?.apiKey, detail: found?.siteDomain };
3969
+ } catch {
3970
+ return { connected: false };
3971
+ }
3972
+ },
3973
+ testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
3974
+ saveConnection: async (values) => {
3975
+ await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain });
3976
+ },
3977
+ disconnect: () => provider.disconnect(),
3978
+ ExtraPanel: RankLayerExtraPanel
3979
+ };
3500
3980
 
3501
3981
  // src/locales/en.ts
3502
3982
  var en = {
@@ -3856,6 +4336,8 @@ var DEFAULT_LABELS = {
3856
4336
  funnel: "Funnel",
3857
4337
  landingPages: "Landing pages",
3858
4338
  content: "Content",
4339
+ blog: "Blog",
4340
+ blogCategories: "Categorias",
3859
4341
  settings: "Settings"
3860
4342
  };
3861
4343
  function resolveConfig(options) {
@@ -3877,9 +4359,9 @@ function createSafeProvider(config, domain) {
3877
4359
  function createMarketingPlugin(options) {
3878
4360
  core.registerTranslations(marketingLocales);
3879
4361
  const { config, domain } = resolveConfig(options);
3880
- const provider = options?.dataProvider ?? createSafeProvider(config, domain);
3881
- const store = createMarketingStore(provider);
3882
- const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
4362
+ const provider2 = options?.dataProvider ?? createSafeProvider(config, domain);
4363
+ const store = createMarketingStore(provider2);
4364
+ const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store });
3883
4365
  const contentProvider = options?.contentPlanner?.dataProvider ?? core.createSafeDataProvider(
3884
4366
  () => createSupabaseContentPlannerProvider(),
3885
4367
  () => createMockContentPlannerProvider()
@@ -3887,8 +4369,8 @@ function createMarketingPlugin(options) {
3887
4369
  const contentStore = createContentPlannerStore(contentProvider);
3888
4370
  const PageComponent = () => React8__default.default.createElement(
3889
4371
  MarketingContextProvider,
3890
- { config, provider, store },
3891
- React8__default.default.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
4372
+ { config, provider: provider2, store },
4373
+ React8__default.default.createElement(MarketingPage, { config, provider: provider2, store, contentProvider, contentStore })
3892
4374
  );
3893
4375
  PageComponent.displayName = "MarketingPage";
3894
4376
  const marketingRegistries = [
@@ -3897,12 +4379,14 @@ function createMarketingPlugin(options) {
3897
4379
  ];
3898
4380
  const SettingsComponent = () => React8__default.default.createElement(
3899
4381
  MarketingContextProvider,
3900
- { config, provider, store },
4382
+ { config, provider: provider2, store },
3901
4383
  React8__default.default.createElement(saas.PluginSettingsPanel, {
3902
4384
  title: config.labels.settings ?? config.labels.pageTitle,
3903
4385
  subtitle: config.labels.pageSubtitle,
3904
4386
  generalSettings: React8__default.default.createElement(SettingsView),
3905
4387
  registries: marketingRegistries,
4388
+ // Enables the "Integrações" tab (renders manifest connectors for this host).
4389
+ hostPluginId: "marketing",
3906
4390
  routeBase: "/settings/marketing"
3907
4391
  })
3908
4392
  );
@@ -3914,12 +4398,16 @@ function createMarketingPlugin(options) {
3914
4398
  version: "1.0.0",
3915
4399
  scope: options?.scope ?? "universal",
3916
4400
  verticalId: options?.verticalId,
4401
+ // Admin content-planning surface (campaigns, content calendar) — targets the
4402
+ // saas/admin Panel world, not the ecommerce storefront/operator console.
4403
+ scaffolds: ["saas"],
3917
4404
  defaultEnabled: true,
3918
4405
  dependencies: [],
3919
4406
  declaredFeatures: [
3920
4407
  { id: "marketing", label: config.labels.pageTitle, group: "Engage" },
3921
4408
  ...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
3922
- ...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
4409
+ ...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : [],
4410
+ ...config.modules.blog ? [{ id: "marketing.blog", label: config.labels.blog, group: "Engage" }] : []
3923
4411
  ],
3924
4412
  navigation: [
3925
4413
  {
@@ -3950,11 +4438,17 @@ function createMarketingPlugin(options) {
3950
4438
  { name: "marketing.content.post.updated", description: "A content post was updated" }
3951
4439
  ],
3952
4440
  migrations: [
4441
+ {
4442
+ id: "marketing-000-plg-rename",
4443
+ version: "1.0.0",
4444
+ sql: MIGRATION_000_PLG_RENAME,
4445
+ description: "Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)"
4446
+ },
3953
4447
  {
3954
4448
  id: "marketing-001-content-planner",
3955
4449
  version: "1.0.0",
3956
4450
  sql: MIGRATION_001_CONTENT_PLANNER,
3957
- description: "Create mkt_social_accounts, mkt_content_plans and mkt_content_posts tables"
4451
+ description: "Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts"
3958
4452
  },
3959
4453
  {
3960
4454
  id: "marketing-002-multi-platform",
@@ -3966,9 +4460,18 @@ function createMarketingPlugin(options) {
3966
4460
  id: "marketing-003-recording-ops",
3967
4461
  version: "1.2.0",
3968
4462
  sql: MIGRATION_003_RECORDING_OPS,
3969
- description: "Recording-day checklist + media asset URL on posts; mkt-media storage bucket"
4463
+ description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
4464
+ },
4465
+ {
4466
+ id: "marketing-004-ranklayer",
4467
+ version: "1.3.0",
4468
+ sql: MIGRATION_004_RANKLAYER,
4469
+ description: "RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)"
3970
4470
  }
3971
4471
  ],
4472
+ // RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
4473
+ // (control plane only); the real sync lands via an external PR (RANKLAYER.md).
4474
+ connectors: [ranklayerConnectorDef],
3972
4475
  aiTools: [
3973
4476
  {
3974
4477
  id: "marketing.channel-performance",