瀏覽代碼

Workspace Improvements

Benjamin Harris 3 周之前
父節點
當前提交
065de8f350

+ 38 - 4
services/gateway/server.js

@@ -132,24 +132,34 @@ app.options('*', async (request, reply) => {
 // Compound credential key: workspaceId + credential type
 function credId(ws, type) { return `${ws}:${type}`; }
 
+// Global credential types are not workspace-scoped (AI config, app credentials)
+const GLOBAL_CREDENTIAL_TYPES = new Set([
+  'ai_config', 'openai_config', 'groq_config', 'gemini_config',
+  'meta_app', 'pinterest_app', 'tiktok_app', 'google_places',
+]);
+
 async function getCredentials(ws, type) {
   const db = await getDb();
-  return db.collection('platform_credentials').findOne({ _id: credId(ws, type) });
+  const id = GLOBAL_CREDENTIAL_TYPES.has(type) ? type : credId(ws, type);
+  return db.collection('platform_credentials').findOne({ _id: id });
 }
 
 async function setCredentials(ws, type, data) {
-  const id = credId(ws, type);
+  const isGlobal = GLOBAL_CREDENTIAL_TYPES.has(type);
+  const id = isGlobal ? type : credId(ws, type);
   const db = await getDb();
+  const meta = isGlobal ? {} : { workspaceId: ws };
   await db.collection('platform_credentials').updateOne(
     { _id: id },
-    { $set: { _id: id, workspaceId: ws, type, ...data, updatedAt: new Date() } },
+    { $set: { _id: id, type, ...meta, ...data, updatedAt: new Date() } },
     { upsert: true }
   );
 }
 
 async function deleteCredentials(ws, type) {
   const db = await getDb();
-  await db.collection('platform_credentials').deleteOne({ _id: credId(ws, type) });
+  const id = GLOBAL_CREDENTIAL_TYPES.has(type) ? type : credId(ws, type);
+  await db.collection('platform_credentials').deleteOne({ _id: id });
 }
 
 // ─── Workspaces ───────────────────────────────────────────────────────────────
@@ -2201,10 +2211,12 @@ app.get('/credentials', async (request) => {
     facebook: {
       connected: fbPages.length > 0,
       pages: fbPages.map(({ id, name, picture }) => ({ id, name, picture })),
+      allPages: (fb?.pages || []).map(({ id, name, picture, selected }) => ({ id, name, picture, selected: !!selected })),
     },
     instagram: {
       connected: igAccounts.length > 0,
       accounts: igAccounts.map(({ id, username, avatar }) => ({ id, username, avatar })),
+      allAccounts: (ig?.accounts || []).map(({ id, username, avatar, selected }) => ({ id, username, avatar, selected: !!selected })),
     },
     pinterest: {
       connected: pinterestBoards.length > 0,
@@ -2223,6 +2235,28 @@ app.get('/credentials', async (request) => {
 
 // ─── Schedule Suggestions ────────────────────────────────────────────────────
 
+// Update which Facebook pages are selected for this workspace
+app.post('/credentials/facebook/pages', async (request) => {
+  const ws = request.workspaceId;
+  const { selectedPageIds } = request.body;
+  const fb = await getCredentials(ws, 'facebook');
+  if (!fb) return { success: false, error: 'Not connected' };
+  const updated = (fb.pages || []).map((p) => ({ ...p, selected: selectedPageIds.includes(p.id) }));
+  await setCredentials(ws, 'facebook', { pages: updated });
+  return { success: true };
+});
+
+// Update which Instagram accounts are selected for this workspace
+app.post('/credentials/instagram/accounts', async (request) => {
+  const ws = request.workspaceId;
+  const { selectedAccountIds } = request.body;
+  const ig = await getCredentials(ws, 'instagram');
+  if (!ig) return { success: false, error: 'Not connected' };
+  const updated = (ig.accounts || []).map((a) => ({ ...a, selected: selectedAccountIds.includes(a.id) }));
+  await setCredentials(ws, 'instagram', { accounts: updated });
+  return { success: true };
+});
+
 // [dayOfWeek (0=Sun), hourUTC] pairs — research-based best-practice defaults
 const INDUSTRY_DEFAULTS = {
   facebook:  [[2,9],[3,9],[4,9],[2,12],[4,10]],

+ 12 - 11
ui/src/components/NavBar.vue

@@ -10,7 +10,7 @@
         :key="link.to"
         :to="link.to"
         class="px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap"
-        :class="$route.path === link.to
+        :class="$route.path.startsWith(link.to) && link.to !== '/'
           ? 'bg-gray-800 text-white'
           : 'text-gray-400 hover:text-white hover:bg-gray-800'"
       >
@@ -98,6 +98,7 @@
 
 <script setup lang="ts">
 import { ref, computed } from 'vue'
+import { storeToRefs } from 'pinia'
 import { useI18n } from 'vue-i18n'
 import { useRouter } from 'vue-router'
 import { SUPPORTED_LOCALES } from '../locales'
@@ -109,18 +110,18 @@ const showLangMenu = ref(false)
 const showWorkspaceMenu = ref(false)
 
 const workspaceStore = useWorkspaceStore()
-const { workspaces, activeWorkspaceId } = workspaceStore
-const activeWorkspace = computed(() => workspaceStore.activeWorkspace)
+const { workspaces, activeWorkspaceId, activeWorkspace } = storeToRefs(workspaceStore)
 
 const navLinks = [
-  { to: '/dashboard', label: 'nav.feed' },
-  { to: '/compose',   label: 'nav.compose' },
-  { to: '/media',     label: 'nav.media' },
-  { to: '/scheduler', label: 'nav.scheduler' },
-  { to: '/analytics',      label: 'nav.analytics' },
-  { to: '/calendar-plan',  label: 'nav.calendarPlan' },
-  { to: '/competitors',    label: 'nav.competitors' },
-  { to: '/settings',       label: 'nav.settings' },
+  { to: '/dashboard',        label: 'nav.feed' },
+  { to: '/compose',          label: 'nav.compose' },
+  { to: '/media',            label: 'nav.media' },
+  { to: '/scheduler',        label: 'nav.scheduler' },
+  { to: '/analytics',        label: 'nav.analytics' },
+  { to: '/calendar-plan',    label: 'nav.calendarPlan' },
+  { to: '/competitors',      label: 'nav.competitors' },
+  { to: '/settings',         label: 'nav.settings' },
+  { to: '/global-settings',  label: 'nav.globalSettings' },
 ]
 
 const currentLocale = computed(

+ 7 - 0
ui/src/locales/en.ts

@@ -8,6 +8,7 @@ export default {
     calendarPlan: 'Content Plan',
     competitors: 'Competitors',
     settings: 'Settings',
+    globalSettings: 'Global Settings',
   },
 
   analytics: {
@@ -308,6 +309,7 @@ export default {
     notConnected: 'Not connected',
     refreshStatus: '↻ Refresh Status',
     envHint: 'Configuration required',
+    configureInGlobal: 'Configure in Global Settings',
 
     profiles: {
       sectionTitle: 'Account Profiles',
@@ -466,6 +468,11 @@ export default {
 
       connectedPages: 'Connected Pages',
       connectedAccounts: 'Connected Accounts',
+      appNotConfigured: 'App credentials not configured',
+      selectPages: 'Select Facebook pages for this workspace',
+      selectAccounts: 'Select Instagram accounts for this workspace',
+      selectionSaved: 'Selection saved',
+      saveSelection: 'Save Selection',
 
       errorTitle: 'OAuth Error',
       getAppHelp: 'Get your App ID and Secret from',

+ 7 - 0
ui/src/locales/tr.ts

@@ -8,6 +8,7 @@ export default {
     calendarPlan: 'İçerik Planı',
     competitors: 'Rakipler',
     settings: 'Ayarlar',
+    globalSettings: 'Genel Ayarlar',
   },
 
   analytics: {
@@ -308,6 +309,7 @@ export default {
     notConnected: 'Bağlı değil',
     refreshStatus: '↻ Durumları Yenile',
     envHint: 'Yapılandırma gerekli',
+    configureInGlobal: 'Genel Ayarlarda Yapılandır',
 
     profiles: {
       sectionTitle: 'Hesap Profilleri',
@@ -466,6 +468,11 @@ export default {
 
       connectedPages: 'Bağlı Sayfalar',
       connectedAccounts: 'Bağlı Hesaplar',
+      appNotConfigured: 'Uygulama kimlik bilgileri yapılandırılmadı',
+      selectPages: 'Bu çalışma alanı için Facebook sayfaları seçin',
+      selectAccounts: 'Bu çalışma alanı için Instagram hesapları seçin',
+      selectionSaved: 'Seçim kaydedildi',
+      saveSelection: 'Seçimi Kaydet',
 
       errorTitle: 'OAuth Hatası',
       getAppHelp: 'Uygulama Kimliği ve Gizli Anahtarını şuradan al:',

+ 11 - 0
ui/src/router/index.ts

@@ -2,6 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router'
 
 const router = createRouter({
   history: createWebHistory(),
+  scrollBehavior(to) {
+    if (to.hash) {
+      return { el: to.hash, behavior: 'smooth', top: 20 }
+    }
+    return { top: 0 }
+  },
   routes: [
     {
       path: '/',
@@ -47,6 +53,11 @@ const router = createRouter({
       name: 'settings',
       component: () => import('../views/Settings.vue'),
     },
+    {
+      path: '/global-settings',
+      name: 'globalSettings',
+      component: () => import('../views/GlobalSettings.vue'),
+    },
   ],
 })
 

+ 34 - 2
ui/src/stores/platforms.ts

@@ -84,6 +84,9 @@ export const usePlatformsStore = defineStore('platforms', () => {
   // Connected pages/accounts (fetched from gateway)
   const connectedPages = ref<MetaPage[]>([])
   const connectedIgAccounts = ref<MetaIgAccount[]>([])
+  // All pages/accounts including unselected ones (for per-workspace selection UI)
+  const allFbPages = ref<(MetaPage & { selected: boolean })[]>([])
+  const allIgAccounts = ref<(MetaIgAccount & { selected: boolean })[]>([])
 
   // Pinterest
   const pinterestCredentials = ref<PinterestCredentials>({ configured: false })
@@ -129,7 +132,9 @@ export const usePlatformsStore = defineStore('platforms', () => {
       const res = await fetch('/api/credentials')
       const data = await res.json()
       connectedPages.value = data.facebook?.pages || []
+      allFbPages.value = data.facebook?.allPages || []
       connectedIgAccounts.value = data.instagram?.accounts || []
+      allIgAccounts.value = data.instagram?.allAccounts || []
       connectedPinterestBoards.value = data.pinterest?.boards || []
       allPinterestBoards.value = data.pinterest?.allBoards || []
       tiktokConnected.value = data.tiktok?.connected ?? false
@@ -360,12 +365,39 @@ export const usePlatformsStore = defineStore('platforms', () => {
     }
   }
 
+  async function saveFbPageSelection(selectedPageIds: string[]) {
+    metaLoading.value = true
+    try {
+      await axios.post('/api/credentials/facebook/pages', { selectedPageIds })
+      allFbPages.value = allFbPages.value.map((p) => ({ ...p, selected: selectedPageIds.includes(p.id) }))
+      connectedPages.value = allFbPages.value.filter((p) => p.selected)
+    } catch (err: any) {
+      metaError.value = err.response?.data?.error || 'Failed to save page selection'
+    } finally {
+      metaLoading.value = false
+    }
+  }
+
+  async function saveIgAccountSelection(selectedAccountIds: string[]) {
+    metaLoading.value = true
+    try {
+      await axios.post('/api/credentials/instagram/accounts', { selectedAccountIds })
+      allIgAccounts.value = allIgAccounts.value.map((a) => ({ ...a, selected: selectedAccountIds.includes(a.id) }))
+      connectedIgAccounts.value = allIgAccounts.value.filter((a) => a.selected)
+    } catch (err: any) {
+      metaError.value = err.response?.data?.error || 'Failed to save account selection'
+    } finally {
+      metaLoading.value = false
+    }
+  }
+
   return {
     statuses, loading, fetchStatuses, getStatus, isConnected,
     metaCredentials, metaDiscovery, metaLoading, metaError,
-    connectedPages, connectedIgAccounts, fetchMetaConnections,
-    fetchMetaCredentials, saveMetaApp, startMetaOAuth,
+    connectedPages, connectedIgAccounts, allFbPages, allIgAccounts,
+    fetchMetaConnections, fetchMetaCredentials, saveMetaApp, startMetaOAuth,
     fetchMetaDiscovery, saveMetaSelection, disconnectMeta,
+    saveFbPageSelection, saveIgAccountSelection,
     tokenExpiry, expiringAccounts, hasExpiryWarning,
     fetchTokenExpiry, dismissTokenWarning, refreshMetaTokens,
     pinterestCredentials, pinterestLoading, pinterestError,

+ 478 - 0
ui/src/views/GlobalSettings.vue

@@ -0,0 +1,478 @@
+<template>
+  <div class="min-h-screen bg-gray-950 text-gray-100 p-6">
+    <div class="max-w-2xl mx-auto space-y-8">
+
+      <div>
+        <h1 class="text-2xl font-bold mb-1">{{ $t('globalSettings.title') }}</h1>
+        <p class="text-sm text-gray-400">{{ $t('globalSettings.subtitle') }}</p>
+      </div>
+
+      <!-- ═══ FACEBOOK & INSTAGRAM APP CREDENTIALS ═════════════════════════════ -->
+      <div id="meta-app" class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
+        <div class="p-5 border-b border-gray-800 flex items-center gap-3">
+          <div class="flex gap-1.5">
+            <span class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-bold" style="background:#1877F2">f</span>
+            <span class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-bold" style="background:#E1306C">I</span>
+          </div>
+          <div>
+            <p class="font-semibold">{{ $t('settings.meta.sectionTitle') }}</p>
+            <p class="text-xs text-gray-500 mt-0.5">{{ $t('globalSettings.appCredentialsNote') }}</p>
+          </div>
+        </div>
+
+        <div class="p-5">
+          <div v-if="metaAppConfigured && !editingMetaApp" class="flex items-center justify-between">
+            <div class="flex items-center gap-2 text-sm text-green-400">
+              <span>✓</span>
+              <span>{{ $t('settings.meta.appConfigured') }}</span>
+              <span class="text-gray-600 font-mono text-xs">({{ platformsStore.metaCredentials.appId }})</span>
+            </div>
+            <button @click="editingMetaApp = true" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
+              Edit
+            </button>
+          </div>
+
+          <div v-else class="space-y-3">
+            <div>
+              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.meta.appIdLabel') }}</label>
+              <input v-model="metaAppId" type="text" :placeholder="$t('settings.meta.appIdPlaceholder')"
+                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500" />
+            </div>
+            <div>
+              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.meta.appSecretLabel') }}</label>
+              <input v-model="metaAppSecret" type="password"
+                :placeholder="metaAppConfigured ? platformsStore.metaCredentials.appSecretHint : $t('settings.meta.appSecretPlaceholder')"
+                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500" />
+            </div>
+            <div class="flex items-center justify-between">
+              <p class="text-xs text-gray-600">
+                {{ $t('settings.meta.getAppHelp') }}
+                <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener" class="text-blue-400 hover:text-blue-300 underline">{{ $t('settings.meta.devPortal') }}</a>
+              </p>
+              <button @click="saveMetaApp" :disabled="!metaAppId || !metaAppSecret || platformsStore.metaLoading"
+                class="px-4 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors">
+                {{ platformsStore.metaLoading ? $t('settings.meta.saving') : $t('settings.meta.saveApp') }}
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- ═══ PINTEREST APP CREDENTIALS ════════════════════════════════════════ -->
+      <div id="pinterest-app" class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
+        <div class="p-5 border-b border-gray-800 flex items-center gap-3">
+          <span class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-bold" style="background:#E60023">P</span>
+          <div>
+            <p class="font-semibold">{{ $t('settings.pinterest.sectionTitle') }}</p>
+            <p class="text-xs text-gray-500 mt-0.5">{{ $t('globalSettings.appCredentialsNote') }}</p>
+          </div>
+        </div>
+
+        <div class="p-5">
+          <div v-if="pinterestAppConfigured && !editingPinterestApp" class="flex items-center justify-between">
+            <div class="flex items-center gap-2 text-sm text-green-400">
+              <span>✓</span>
+              <span>{{ $t('settings.pinterest.appConfigured') }}</span>
+              <span class="text-gray-600 font-mono text-xs">({{ platformsStore.pinterestCredentials.clientId }})</span>
+            </div>
+            <button @click="editingPinterestApp = true" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">Edit</button>
+          </div>
+
+          <div v-else class="space-y-3">
+            <div>
+              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.pinterest.clientIdLabel') }}</label>
+              <input v-model="pinterestClientId" type="text" :placeholder="$t('settings.pinterest.clientIdPlaceholder')"
+                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-red-500" />
+            </div>
+            <div>
+              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.pinterest.clientSecretLabel') }}</label>
+              <input v-model="pinterestClientSecret" type="password"
+                :placeholder="pinterestAppConfigured ? platformsStore.pinterestCredentials.clientSecretHint : $t('settings.pinterest.clientSecretPlaceholder')"
+                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-red-500" />
+            </div>
+            <div class="flex items-center justify-between">
+              <p class="text-xs text-gray-600">
+                <a href="https://developers.pinterest.com/apps/" target="_blank" rel="noopener" class="text-red-400 hover:text-red-300 underline">{{ $t('settings.pinterest.devPortal') }}</a>
+              </p>
+              <button @click="savePinterestApp" :disabled="!pinterestClientId || !pinterestClientSecret || platformsStore.pinterestLoading"
+                class="px-4 py-1.5 bg-red-600 hover:bg-red-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors">
+                {{ platformsStore.pinterestLoading ? $t('settings.pinterest.saving') : $t('settings.pinterest.saveApp') }}
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- ═══ TIKTOK APP CREDENTIALS ════════════════════════════════════════════ -->
+      <div id="tiktok-app" class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
+        <div class="p-5 border-b border-gray-800 flex items-center gap-3">
+          <span class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-bold" style="background:#EE1D52">T</span>
+          <div>
+            <p class="font-semibold">{{ $t('settings.tiktok.sectionTitle') }}</p>
+            <p class="text-xs text-gray-500 mt-0.5">{{ $t('globalSettings.appCredentialsNote') }}</p>
+          </div>
+        </div>
+
+        <div class="p-5">
+          <div v-if="tiktokAppConfigured && !editingTikTokApp" class="flex items-center justify-between">
+            <div class="flex items-center gap-2 text-sm text-green-400">
+              <span>✓</span>
+              <span>{{ $t('settings.tiktok.appConfigured') }}</span>
+              <span class="text-gray-600 font-mono text-xs">({{ platformsStore.tiktokCredentials.clientKey }})</span>
+            </div>
+            <button @click="editingTikTokApp = true" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">Edit</button>
+          </div>
+
+          <div v-else class="space-y-3">
+            <div>
+              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.tiktok.clientKeyLabel') }}</label>
+              <input v-model="tiktokClientKey" type="text" :placeholder="$t('settings.tiktok.clientKeyPlaceholder')"
+                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-pink-500" />
+            </div>
+            <div>
+              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.tiktok.clientSecretLabel') }}</label>
+              <input v-model="tiktokClientSecret" type="password"
+                :placeholder="tiktokAppConfigured ? platformsStore.tiktokCredentials.clientSecretHint : $t('settings.tiktok.clientSecretPlaceholder')"
+                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-pink-500" />
+            </div>
+            <div class="flex items-center justify-between">
+              <p class="text-xs text-gray-600">
+                <a href="https://developers.tiktok.com/" target="_blank" rel="noopener" class="text-pink-400 hover:text-pink-300 underline">{{ $t('settings.tiktok.devPortal') }}</a>
+              </p>
+              <button @click="saveTikTokApp" :disabled="!tiktokClientKey || !tiktokClientSecret || platformsStore.tiktokLoading"
+                class="px-4 py-1.5 bg-pink-600 hover:bg-pink-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors">
+                {{ platformsStore.tiktokLoading ? $t('settings.tiktok.saving') : $t('settings.tiktok.saveApp') }}
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- ═══ AI INTEGRATION — Ollama ══════════════════════════════════════════ -->
+      <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
+        <div class="p-5 border-b border-gray-800 flex items-center gap-3">
+          <div class="w-9 h-9 rounded-full bg-violet-700 flex items-center justify-center text-white text-sm font-bold shrink-0">AI</div>
+          <div>
+            <p class="font-semibold">{{ $t('ai.sectionTitle') }}</p>
+            <p class="text-xs text-gray-500 mt-0.5">{{ $t('ai.sectionSubtitle') }}</p>
+          </div>
+          <div class="ml-auto flex items-center gap-2 shrink-0">
+            <span v-if="aiStore.config.provider === 'ollama'" class="text-xs px-2 py-0.5 rounded-full font-medium bg-violet-900/50 text-violet-300 border border-violet-700">{{ $t('ai.active') }}</span>
+            <span v-if="aiConnected !== null" class="text-xs px-2 py-0.5 rounded-full font-medium" :class="aiConnected ? 'bg-green-900/50 text-green-400 border border-green-700' : 'bg-red-900/40 text-red-400 border border-red-800'">
+              {{ aiConnected ? $t('ai.connected') : $t('ai.connectionFailed') }}
+            </span>
+          </div>
+        </div>
+        <div class="p-5 space-y-4">
+          <div>
+            <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.endpointLabel') }}</label>
+            <div class="flex gap-2">
+              <input v-model="aiEndpoint" type="text" :placeholder="$t('ai.endpointPlaceholder')"
+                class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500" />
+              <button @click="testAiConnection" :disabled="aiStore.modelsLoading || !aiEndpoint"
+                class="px-3 py-2 bg-gray-700 hover:bg-gray-600 disabled:opacity-40 border border-gray-600 rounded-lg text-xs font-medium transition-colors whitespace-nowrap">
+                {{ aiStore.modelsLoading ? $t('ai.testing') : $t('ai.testConnection') }}
+              </button>
+            </div>
+            <p class="text-xs text-gray-600 mt-1">{{ $t('ai.endpointHint') }}</p>
+          </div>
+          <div>
+            <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
+            <select v-model="aiModel" :disabled="!aiModels.length"
+              class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500 disabled:opacity-40">
+              <option value="">{{ $t('ai.modelPlaceholder') }}</option>
+              <option v-for="m in aiModels" :key="m" :value="m">{{ m }}</option>
+            </select>
+          </div>
+          <div>
+            <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.visionModelLabel') }}</label>
+            <input v-model="aiVisionModel" type="text" :placeholder="$t('ai.visionModelPlaceholder')"
+              class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500" />
+            <p class="text-xs text-gray-600 mt-1">{{ $t('ai.visionModelHint') }}</p>
+          </div>
+          <div class="flex items-center justify-end gap-3">
+            <span v-if="aiSaved" class="text-xs text-green-400">{{ $t('ai.saved') }}</span>
+            <button @click="saveAiConfig" :disabled="aiStore.saving || !aiEndpoint"
+              class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors">
+              {{ aiStore.saving ? $t('ai.saving') : $t('ai.saveConfig') }}
+            </button>
+          </div>
+        </div>
+      </div>
+
+      <!-- ═══ AI PROVIDERS — OpenAI, Groq, Gemini ═══════════════════════════════ -->
+      <template v-for="providerName in ['openai', 'groq', 'gemini']" :key="providerName">
+        <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
+          <div class="p-5 border-b border-gray-800 flex items-center gap-3">
+            <div class="w-9 h-9 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0"
+              :class="providerName === 'openai' ? 'bg-emerald-700' : providerName === 'groq' ? 'bg-orange-700' : 'bg-blue-700'">
+              {{ providerName === 'openai' ? 'OAI' : providerName === 'groq' ? 'GRQ' : 'GEM' }}
+            </div>
+            <div>
+              <p class="font-semibold">{{ $t(`ai.${providerName}.sectionTitle`) }}</p>
+              <p class="text-xs text-gray-500 mt-0.5">{{ $t(`ai.${providerName}.sectionSubtitle`) }}</p>
+            </div>
+            <div class="ml-auto flex items-center gap-2 shrink-0">
+              <span v-if="aiStore.config.provider === providerName" class="text-xs px-2 py-0.5 rounded-full font-medium bg-violet-900/50 text-violet-300 border border-violet-700">{{ $t('ai.active') }}</span>
+              <span v-else-if="getProvider(providerName)?.configured" class="text-xs px-2 py-0.5 rounded-full font-medium bg-green-900/50 text-green-400 border border-green-700">✓ {{ $t('ai.apiKeyConfigured') }}</span>
+            </div>
+          </div>
+          <div class="p-5 space-y-4">
+            <div v-if="getProvider(providerName)?.configured && !providerForms[providerName].editing">
+              <div class="flex items-center justify-between text-sm">
+                <div class="space-y-1">
+                  <p class="text-xs text-gray-400">{{ $t('ai.apiKeyLabel') }}: <span class="font-mono text-gray-300">{{ getProvider(providerName)?.apiKeyHint }}</span></p>
+                  <p v-if="providerForms[providerName].saved" class="text-xs text-green-400">{{ $t('ai.providerSaved') }}</p>
+                </div>
+                <div class="flex gap-2">
+                  <button @click="providerForms[providerName].editing = true" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">Edit</button>
+                  <button v-if="aiStore.config.provider !== providerName" @click="setActiveProvider(providerName)" :disabled="aiStore.saving"
+                    class="text-xs px-2.5 py-1 bg-violet-700 hover:bg-violet-600 disabled:opacity-40 rounded-md text-white transition-colors">
+                    {{ $t('ai.setAsActive') }}
+                  </button>
+                  <button @click="disconnectCloudProvider(providerName)" class="text-xs px-2.5 py-1 bg-red-900/40 hover:bg-red-900/60 border border-red-800 rounded-md text-red-400 hover:text-red-300 transition-colors">{{ $t('ai.disconnect') }}</button>
+                </div>
+              </div>
+              <div class="mt-3">
+                <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
+                <select v-model="providerForms[providerName].model" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500">
+                  <option v-for="m in (PROVIDER_MODELS[providerName] || [])" :key="m" :value="m">{{ m }}</option>
+                </select>
+              </div>
+              <div class="flex justify-end mt-3">
+                <button @click="saveCloudProvider(providerName, aiStore.config.provider === providerName)" :disabled="providerForms[providerName].saving"
+                  class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors">
+                  {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.saveProvider') }}
+                </button>
+              </div>
+            </div>
+            <div v-else class="space-y-3">
+              <div>
+                <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.apiKeyLabel') }}</label>
+                <input v-model="providerForms[providerName].apiKey" type="password" :placeholder="$t('ai.apiKeyPlaceholder')"
+                  class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500" />
+                <p class="text-xs text-gray-600 mt-1">{{ $t(`ai.${providerName}.getKeyHint`) }}</p>
+              </div>
+              <div>
+                <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
+                <select v-model="providerForms[providerName].model" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500">
+                  <option v-for="m in (PROVIDER_MODELS[providerName] || [])" :key="m" :value="m">{{ m }}</option>
+                </select>
+              </div>
+              <div class="flex items-center justify-end gap-2">
+                <button v-if="providerForms[providerName].editing" @click="providerForms[providerName].editing = false" class="text-xs px-3 py-2 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 transition-colors">Cancel</button>
+                <button @click="saveCloudProvider(providerName, false)" :disabled="providerForms[providerName].saving || !providerForms[providerName].apiKey"
+                  class="px-3 py-2 bg-gray-700 hover:bg-gray-600 disabled:opacity-40 border border-gray-600 rounded-lg text-xs font-medium transition-colors">
+                  {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.saveProvider') }}
+                </button>
+                <button @click="saveCloudProvider(providerName, true)" :disabled="providerForms[providerName].saving || !providerForms[providerName].apiKey"
+                  class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors">
+                  {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.connectAndActivate') }}
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </template>
+
+      <!-- ═══ GOOGLE PLACES ═════════════════════════════════════════════════════ -->
+      <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
+        <div class="p-5 border-b border-gray-800 flex items-center gap-3">
+          <div class="w-9 h-9 rounded-full bg-green-700 flex items-center justify-center shrink-0">
+            <i class="fa-solid fa-location-dot text-white text-sm"></i>
+          </div>
+          <div>
+            <p class="font-semibold">{{ $t('settings.googlePlaces.sectionTitle') }}</p>
+            <p class="text-xs text-gray-500 mt-0.5">{{ $t('settings.googlePlaces.sectionSubtitle') }}</p>
+          </div>
+        </div>
+        <div class="p-5 space-y-4">
+          <div v-if="placesConfigured" class="flex items-center justify-between">
+            <span class="text-sm text-gray-300">{{ $t('settings.googlePlaces.keyConfigured', { hint: placesKeyHint }) }}</span>
+            <button @click="removePlacesKey" class="text-xs px-3 py-1.5 border border-red-800/60 text-red-400 hover:bg-red-900/20 rounded-lg transition-colors">{{ $t('settings.googlePlaces.disconnect') }}</button>
+          </div>
+          <div v-else class="space-y-3">
+            <p class="text-xs text-gray-500">{{ $t('settings.googlePlaces.getKeyHint') }}</p>
+            <div class="flex gap-2">
+              <input v-model="placesApiKey" type="password" :placeholder="$t('settings.googlePlaces.keyPlaceholder')"
+                class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-green-500" />
+              <button @click="savePlacesKey" :disabled="!placesApiKey.trim() || placesSaving"
+                class="px-4 py-2 bg-green-700 hover:bg-green-600 disabled:opacity-40 rounded-lg text-sm font-medium text-white transition-colors">
+                {{ placesSaving ? $t('settings.googlePlaces.saving') : $t('settings.googlePlaces.save') }}
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+import axios from 'axios'
+import { usePlatformsStore } from '../stores/platforms'
+import { useAiStore, PROVIDER_MODELS } from '../stores/ai'
+
+const { t } = useI18n()
+const platformsStore = usePlatformsStore()
+const aiStore = useAiStore()
+
+// ─── Meta app credentials ─────────────────────────────────────────────────────
+const metaAppId = ref('')
+const metaAppSecret = ref('')
+const editingMetaApp = ref(false)
+const metaAppConfigured = computed(() => platformsStore.metaCredentials.configured)
+
+async function saveMetaApp() {
+  await platformsStore.saveMetaApp(metaAppId.value, metaAppSecret.value)
+  if (!platformsStore.metaError) {
+    editingMetaApp.value = false
+    metaAppSecret.value = ''
+  }
+}
+
+// ─── Pinterest app credentials ────────────────────────────────────────────────
+const pinterestClientId = ref('')
+const pinterestClientSecret = ref('')
+const editingPinterestApp = ref(false)
+const pinterestAppConfigured = computed(() => platformsStore.pinterestCredentials.configured)
+
+async function savePinterestApp() {
+  await platformsStore.savePinterestApp(pinterestClientId.value, pinterestClientSecret.value)
+  if (!platformsStore.pinterestError) {
+    editingPinterestApp.value = false
+    pinterestClientSecret.value = ''
+  }
+}
+
+// ─── TikTok app credentials ───────────────────────────────────────────────────
+const tiktokClientKey = ref('')
+const tiktokClientSecret = ref('')
+const editingTikTokApp = ref(false)
+const tiktokAppConfigured = computed(() => platformsStore.tiktokCredentials.configured)
+
+async function saveTikTokApp() {
+  await platformsStore.saveTikTokApp(tiktokClientKey.value, tiktokClientSecret.value)
+  if (!platformsStore.tiktokError) {
+    editingTikTokApp.value = false
+    tiktokClientSecret.value = ''
+  }
+}
+
+// ─── Google Places ────────────────────────────────────────────────────────────
+const placesApiKey = ref('')
+const placesConfigured = ref(false)
+const placesKeyHint = ref<string | null>(null)
+const placesSaving = ref(false)
+
+async function loadPlacesConfig() {
+  try {
+    const res = await axios.get('/api/credentials/google-places')
+    placesConfigured.value = res.data.configured
+    placesKeyHint.value = res.data.keyHint
+  } catch { /* not configured */ }
+}
+
+async function savePlacesKey() {
+  if (!placesApiKey.value.trim()) return
+  placesSaving.value = true
+  try {
+    await axios.post('/api/credentials/google-places', { apiKey: placesApiKey.value.trim() })
+    placesConfigured.value = true
+    placesKeyHint.value = `****${placesApiKey.value.trim().slice(-4)}`
+    placesApiKey.value = ''
+  } finally {
+    placesSaving.value = false
+  }
+}
+
+async function removePlacesKey() {
+  if (!confirm(t('settings.googlePlaces.disconnectConfirm'))) return
+  await axios.delete('/api/credentials/google-places')
+  placesConfigured.value = false
+  placesKeyHint.value = null
+}
+
+// ─── AI Configuration — Ollama ────────────────────────────────────────────────
+const aiEndpoint = ref('')
+const aiModel = ref('')
+const aiVisionModel = ref('')
+const aiModels = computed(() => aiStore.models)
+const aiConnected = ref<boolean | null>(null)
+const aiSaved = ref(false)
+
+async function testAiConnection() {
+  const ok = await aiStore.fetchModels(aiEndpoint.value)
+  aiConnected.value = ok
+  if (ok && !aiModel.value && aiStore.models.length) aiModel.value = aiStore.models[0]
+}
+
+async function saveAiConfig() {
+  const ok = await aiStore.saveProvider('ollama', { endpoint: aiEndpoint.value, model: aiModel.value, visionModel: aiVisionModel.value, setActive: true })
+  if (ok) {
+    aiSaved.value = true
+    setTimeout(() => { aiSaved.value = false }, 2500)
+  }
+}
+
+// ─── Cloud AI providers ───────────────────────────────────────────────────────
+interface ProviderFormState {
+  apiKey: string; model: string; editing: boolean; saving: boolean; saved: boolean
+}
+
+const providerForms = ref<Record<string, ProviderFormState>>({
+  openai: { apiKey: '', model: '', editing: false, saving: false, saved: false },
+  groq:   { apiKey: '', model: '', editing: false, saving: false, saved: false },
+  gemini: { apiKey: '', model: '', editing: false, saving: false, saved: false },
+})
+
+function getProvider(name: string) {
+  return aiStore.providers.find((p) => p.name === name)
+}
+
+async function saveCloudProvider(name: string, setActive = false) {
+  const form = providerForms.value[name]
+  form.saving = true
+  const ok = await aiStore.saveProvider(name, { apiKey: form.apiKey || undefined, model: form.model || undefined, setActive })
+  form.saving = false
+  if (ok) {
+    form.saved = true
+    form.editing = false
+    form.apiKey = ''
+    setTimeout(() => { form.saved = false }, 2500)
+  }
+}
+
+async function setActiveProvider(name: string) {
+  if (!getProvider(name)?.configured) return
+  await aiStore.saveProvider(name, { setActive: true })
+}
+
+async function disconnectCloudProvider(name: string) {
+  if (!confirm(t('ai.disconnectConfirm'))) return
+  await aiStore.deleteProvider(name)
+}
+
+// ─── On mount ─────────────────────────────────────────────────────────────────
+onMounted(async () => {
+  await Promise.all([
+    platformsStore.fetchMetaCredentials(),
+    platformsStore.fetchPinterestCredentials(),
+    platformsStore.fetchTikTokCredentials(),
+    aiStore.fetchConfig(),
+    aiStore.fetchProviders(),
+    loadPlacesConfig(),
+  ])
+  aiEndpoint.value = aiStore.config.endpoint
+  aiModel.value = aiStore.config.model
+  aiVisionModel.value = aiStore.config.visionModel
+  for (const p of aiStore.providers) {
+    if (p.name === 'ollama') continue
+    const form = providerForms.value[p.name]
+    if (form) form.model = p.model || ''
+  }
+})
+</script>

+ 109 - 577
ui/src/views/Settings.vue

@@ -29,56 +29,20 @@
           <span><strong>{{ $t('settings.meta.errorTitle') }}:</strong> {{ oauthError }}</span>
         </div>
 
-        <!-- Step 1: App credentials -->
-        <div class="p-5 border-b border-gray-800/60">
-          <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Step 1 — Facebook Developer App</p>
-
-          <div v-if="metaAppConfigured" class="flex items-center justify-between">
-            <div class="flex items-center gap-2 text-sm text-green-400">
-              <span>✓</span>
-              <span>{{ $t('settings.meta.appConfigured') }}</span>
-              <span class="text-gray-600 font-mono text-xs">({{ platformsStore.metaCredentials.appId }})</span>
-            </div>
-            <button @click="editingApp = !editingApp" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
-              Edit
-            </button>
-          </div>
-
-          <div v-if="!metaAppConfigured || editingApp" class="space-y-3 mt-2">
-            <div>
-              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.meta.appIdLabel') }}</label>
-              <input
-                v-model="appId"
-                type="text"
-                :placeholder="$t('settings.meta.appIdPlaceholder')"
-                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500"
-              />
-            </div>
-            <div>
-              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.meta.appSecretLabel') }}</label>
-              <input
-                v-model="appSecret"
-                type="password"
-                :placeholder="metaAppConfigured ? platformsStore.metaCredentials.appSecretHint : $t('settings.meta.appSecretPlaceholder')"
-                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500"
-              />
-            </div>
-            <div class="flex items-center justify-between">
-              <p class="text-xs text-gray-600">
-                {{ $t('settings.meta.getAppHelp') }}
-                <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener" class="text-blue-400 hover:text-blue-300 underline">
-                  {{ $t('settings.meta.devPortal') }}
-                </a>
-              </p>
-              <button
-                @click="saveApp"
-                :disabled="!appId || !appSecret || platformsStore.metaLoading"
-                class="px-4 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
-              >
-                {{ platformsStore.metaLoading ? $t('settings.meta.saving') : $t('settings.meta.saveApp') }}
-              </button>
-            </div>
+        <!-- Step 1: App credentials (configure in Global Settings) -->
+        <div class="p-5 border-b border-gray-800/60 flex items-center justify-between">
+          <div v-if="metaAppConfigured" class="flex items-center gap-2 text-sm text-green-400">
+            <span>✓</span>
+            <span>{{ $t('settings.meta.appConfigured') }}</span>
+            <span class="text-gray-600 font-mono text-xs">({{ platformsStore.metaCredentials.appId }})</span>
           </div>
+          <div v-else class="text-sm text-gray-500">{{ $t('settings.meta.appNotConfigured') }}</div>
+          <router-link
+            to="/global-settings#meta-app"
+            class="text-xs px-2.5 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-gray-200 transition-colors"
+          >
+            {{ $t('settings.configureInGlobal') }} →
+          </router-link>
         </div>
 
         <!-- Step 2: OAuth connect -->
@@ -87,22 +51,52 @@
 
           <!-- Already connected — show summary + manage -->
           <div v-if="fbConnected || igConnected" class="space-y-3">
-            <div v-if="fbPages.length" class="space-y-1.5">
-              <p class="text-xs text-gray-500">{{ $t('settings.meta.connectedPages') }}</p>
-              <div v-for="page in fbPages" :key="page.id" class="flex items-center gap-2 bg-gray-800/60 rounded-lg px-3 py-2">
-                <img v-if="page.picture" :src="page.picture" class="w-6 h-6 rounded-full" />
-                <span v-else class="w-6 h-6 rounded-full bg-blue-700 flex items-center justify-center text-xs font-bold">f</span>
-                <span class="text-sm">{{ page.name }}</span>
-                <span class="ml-auto w-2 h-2 rounded-full bg-green-400"></span>
+            <div v-if="platformsStore.allFbPages.length" class="space-y-1.5">
+              <p class="text-xs text-gray-500">{{ $t('settings.meta.selectPages') }}</p>
+              <label
+                v-for="page in platformsStore.allFbPages"
+                :key="page.id"
+                class="flex items-center gap-3 bg-gray-800/60 hover:bg-gray-800 rounded-lg px-3 py-2 cursor-pointer transition-colors"
+              >
+                <input type="checkbox" :value="page.id" v-model="workspaceFbPageIds" class="w-4 h-4 accent-blue-500" />
+                <img v-if="page.picture" :src="page.picture" class="w-6 h-6 rounded-full shrink-0" />
+                <span v-else class="w-6 h-6 rounded-full bg-blue-700 flex items-center justify-center text-xs font-bold shrink-0">f</span>
+                <span class="text-sm flex-1">{{ page.name }}</span>
+              </label>
+              <div class="flex items-center justify-between pt-1">
+                <span v-if="fbPagesSaved" class="text-xs text-green-400">{{ $t('settings.meta.selectionSaved') }}</span>
+                <span v-else />
+                <button
+                  @click="saveFbPageSelection"
+                  :disabled="platformsStore.metaLoading"
+                  class="text-xs px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-40 rounded-lg font-medium transition-colors"
+                >
+                  {{ $t('settings.meta.saveSelection') }}
+                </button>
               </div>
             </div>
-            <div v-if="igAccounts.length" class="space-y-1.5">
-              <p class="text-xs text-gray-500">{{ $t('settings.meta.connectedAccounts') }}</p>
-              <div v-for="account in igAccounts" :key="account.id" class="flex items-center gap-2 bg-gray-800/60 rounded-lg px-3 py-2">
-                <img v-if="account.avatar" :src="account.avatar" class="w-6 h-6 rounded-full" />
-                <span v-else class="w-6 h-6 rounded-full bg-pink-700 flex items-center justify-center text-xs font-bold">I</span>
-                <span class="text-sm">@{{ account.username }}</span>
-                <span class="ml-auto w-2 h-2 rounded-full bg-green-400"></span>
+            <div v-if="platformsStore.allIgAccounts.length" class="space-y-1.5">
+              <p class="text-xs text-gray-500">{{ $t('settings.meta.selectAccounts') }}</p>
+              <label
+                v-for="account in platformsStore.allIgAccounts"
+                :key="account.id"
+                class="flex items-center gap-3 bg-gray-800/60 hover:bg-gray-800 rounded-lg px-3 py-2 cursor-pointer transition-colors"
+              >
+                <input type="checkbox" :value="account.id" v-model="workspaceIgAccountIds" class="w-4 h-4 accent-pink-500" />
+                <img v-if="account.avatar" :src="account.avatar" class="w-6 h-6 rounded-full shrink-0" />
+                <span v-else class="w-6 h-6 rounded-full bg-pink-700 flex items-center justify-center text-xs font-bold shrink-0">I</span>
+                <span class="text-sm flex-1">@{{ account.username }}</span>
+              </label>
+              <div class="flex items-center justify-between pt-1">
+                <span v-if="igAccountsSaved" class="text-xs text-green-400">{{ $t('settings.meta.selectionSaved') }}</span>
+                <span v-else />
+                <button
+                  @click="saveIgAccountSelection"
+                  :disabled="platformsStore.metaLoading"
+                  class="text-xs px-3 py-1.5 bg-pink-600 hover:bg-pink-700 disabled:opacity-40 rounded-lg font-medium transition-colors"
+                >
+                  {{ $t('settings.meta.saveSelection') }}
+                </button>
               </div>
             </div>
             <!-- Token expiry warning banner -->
@@ -188,56 +182,20 @@
           <span><strong>{{ $t('settings.pinterest.errorTitle') }}:</strong> {{ pinterestOauthError }}</span>
         </div>
 
-        <!-- Step 1: App credentials -->
-        <div class="p-5 border-b border-gray-800/60">
-          <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Step 1 — Pinterest Developer App</p>
-
-          <div v-if="pinterestAppConfigured" class="flex items-center justify-between">
-            <div class="flex items-center gap-2 text-sm text-green-400">
-              <span>✓</span>
-              <span>{{ $t('settings.pinterest.appConfigured') }}</span>
-              <span class="text-gray-600 font-mono text-xs">({{ platformsStore.pinterestCredentials.clientId }})</span>
-            </div>
-            <button @click="editingPinterestApp = !editingPinterestApp" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
-              Edit
-            </button>
-          </div>
-
-          <div v-if="!pinterestAppConfigured || editingPinterestApp" class="space-y-3 mt-2">
-            <div>
-              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.pinterest.clientIdLabel') }}</label>
-              <input
-                v-model="pinterestClientId"
-                type="text"
-                :placeholder="$t('settings.pinterest.clientIdPlaceholder')"
-                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-red-500"
-              />
-            </div>
-            <div>
-              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.pinterest.clientSecretLabel') }}</label>
-              <input
-                v-model="pinterestClientSecret"
-                type="password"
-                :placeholder="pinterestAppConfigured ? platformsStore.pinterestCredentials.clientSecretHint : $t('settings.pinterest.clientSecretPlaceholder')"
-                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-red-500"
-              />
-            </div>
-            <div class="flex items-center justify-between">
-              <p class="text-xs text-gray-600">
-                {{ $t('settings.pinterest.getAppHelp') }}
-                <a href="https://developers.pinterest.com/apps/" target="_blank" rel="noopener" class="text-red-400 hover:text-red-300 underline">
-                  {{ $t('settings.pinterest.devPortal') }}
-                </a>
-              </p>
-              <button
-                @click="savePinterestApp"
-                :disabled="!pinterestClientId || !pinterestClientSecret || platformsStore.pinterestLoading"
-                class="px-4 py-1.5 bg-red-600 hover:bg-red-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
-              >
-                {{ platformsStore.pinterestLoading ? $t('settings.pinterest.saving') : $t('settings.pinterest.saveApp') }}
-              </button>
-            </div>
+        <!-- Step 1: App credentials (configure in Global Settings) -->
+        <div class="p-5 border-b border-gray-800/60 flex items-center justify-between">
+          <div v-if="pinterestAppConfigured" class="flex items-center gap-2 text-sm text-green-400">
+            <span>✓</span>
+            <span>{{ $t('settings.pinterest.appConfigured') }}</span>
+            <span class="text-gray-600 font-mono text-xs">({{ platformsStore.pinterestCredentials.clientId }})</span>
           </div>
+          <div v-else class="text-sm text-gray-500">{{ $t('settings.meta.appNotConfigured') }}</div>
+          <router-link
+            to="/global-settings#pinterest-app"
+            class="text-xs px-2.5 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-gray-200 transition-colors"
+          >
+            {{ $t('settings.configureInGlobal') }} →
+          </router-link>
         </div>
 
         <!-- Step 2: OAuth connect + boards -->
@@ -328,56 +286,20 @@
           <span><strong>{{ $t('settings.tiktok.errorTitle') }}:</strong> {{ tiktokOauthError }}</span>
         </div>
 
-        <!-- Step 1: App credentials -->
-        <div class="p-5 border-b border-gray-800/60">
-          <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Step 1 — TikTok Developer App</p>
-
-          <div v-if="tiktokAppConfigured" class="flex items-center justify-between">
-            <div class="flex items-center gap-2 text-sm text-green-400">
-              <span>✓</span>
-              <span>{{ $t('settings.tiktok.appConfigured') }}</span>
-              <span class="text-gray-600 font-mono text-xs">({{ platformsStore.tiktokCredentials.clientKey }})</span>
-            </div>
-            <button @click="editingTikTokApp = !editingTikTokApp" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
-              Edit
-            </button>
-          </div>
-
-          <div v-if="!tiktokAppConfigured || editingTikTokApp" class="space-y-3 mt-2">
-            <div>
-              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.tiktok.clientKeyLabel') }}</label>
-              <input
-                v-model="tiktokClientKey"
-                type="text"
-                :placeholder="$t('settings.tiktok.clientKeyPlaceholder')"
-                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-pink-500"
-              />
-            </div>
-            <div>
-              <label class="block text-xs text-gray-400 mb-1">{{ $t('settings.tiktok.clientSecretLabel') }}</label>
-              <input
-                v-model="tiktokClientSecret"
-                type="password"
-                :placeholder="tiktokAppConfigured ? platformsStore.tiktokCredentials.clientSecretHint : $t('settings.tiktok.clientSecretPlaceholder')"
-                class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-pink-500"
-              />
-            </div>
-            <div class="flex items-center justify-between">
-              <p class="text-xs text-gray-600">
-                {{ $t('settings.tiktok.getAppHelp') }}
-                <a href="https://developers.tiktok.com/" target="_blank" rel="noopener" class="text-pink-400 hover:text-pink-300 underline">
-                  {{ $t('settings.tiktok.devPortal') }}
-                </a>
-              </p>
-              <button
-                @click="saveTikTokApp"
-                :disabled="!tiktokClientKey || !tiktokClientSecret || platformsStore.tiktokLoading"
-                class="px-4 py-1.5 bg-pink-600 hover:bg-pink-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
-              >
-                {{ platformsStore.tiktokLoading ? $t('settings.tiktok.saving') : $t('settings.tiktok.saveApp') }}
-              </button>
-            </div>
+        <!-- Step 1: App credentials (configure in Global Settings) -->
+        <div class="p-5 border-b border-gray-800/60 flex items-center justify-between">
+          <div v-if="tiktokAppConfigured" class="flex items-center gap-2 text-sm text-green-400">
+            <span>✓</span>
+            <span>{{ $t('settings.tiktok.appConfigured') }}</span>
+            <span class="text-gray-600 font-mono text-xs">({{ platformsStore.tiktokCredentials.clientKey }})</span>
           </div>
+          <div v-else class="text-sm text-gray-500">{{ $t('settings.meta.appNotConfigured') }}</div>
+          <router-link
+            to="/global-settings#tiktok-app"
+            class="text-xs px-2.5 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-gray-200 transition-colors"
+          >
+            {{ $t('settings.configureInGlobal') }} →
+          </router-link>
         </div>
 
         <!-- Step 2: OAuth connect -->
@@ -1160,261 +1082,6 @@
         </div>
       </div>
 
-      <!-- ═══════════════════════════════════════════════════════════════════
-           AI INTEGRATION — Ollama configuration card
-      ════════════════════════════════════════════════════════════════════ -->
-      <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
-
-        <!-- Header -->
-        <div class="p-5 border-b border-gray-800 flex items-center gap-3">
-          <div class="w-9 h-9 rounded-full bg-violet-700 flex items-center justify-center text-white text-sm font-bold shrink-0">AI</div>
-          <div>
-            <p class="font-semibold">{{ $t('ai.sectionTitle') }}</p>
-            <p class="text-xs text-gray-500 mt-0.5">{{ $t('ai.sectionSubtitle') }}</p>
-          </div>
-          <div class="ml-auto flex items-center gap-2 shrink-0">
-            <span v-if="aiStore.config.provider === 'ollama'" class="text-xs px-2 py-0.5 rounded-full font-medium bg-violet-900/50 text-violet-300 border border-violet-700">
-              {{ $t('ai.active') }}
-            </span>
-            <span v-if="aiConnected !== null" class="text-xs px-2 py-0.5 rounded-full font-medium" :class="aiConnected ? 'bg-green-900/50 text-green-400 border border-green-700' : 'bg-red-900/40 text-red-400 border border-red-800'">
-              {{ aiConnected ? $t('ai.connected') : $t('ai.connectionFailed') }}
-            </span>
-          </div>
-        </div>
-
-        <div class="p-5 space-y-4">
-
-          <!-- Endpoint -->
-          <div>
-            <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.endpointLabel') }}</label>
-            <div class="flex gap-2">
-              <input
-                v-model="aiEndpoint"
-                type="text"
-                :placeholder="$t('ai.endpointPlaceholder')"
-                class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500"
-              />
-              <button
-                @click="testAiConnection"
-                :disabled="aiStore.modelsLoading || !aiEndpoint"
-                class="px-3 py-2 bg-gray-700 hover:bg-gray-600 disabled:opacity-40 border border-gray-600 rounded-lg text-xs font-medium transition-colors whitespace-nowrap"
-              >
-                {{ aiStore.modelsLoading ? $t('ai.testing') : $t('ai.testConnection') }}
-              </button>
-            </div>
-            <p class="text-xs text-gray-600 mt-1">{{ $t('ai.endpointHint') }}</p>
-          </div>
-
-          <!-- Model selector -->
-          <div>
-            <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
-            <select
-              v-model="aiModel"
-              :disabled="!aiModels.length"
-              class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500 disabled:opacity-40"
-            >
-              <option value="">{{ $t('ai.modelPlaceholder') }}</option>
-              <option v-for="m in aiModels" :key="m" :value="m">{{ m }}</option>
-            </select>
-            <p v-if="aiConnected === false" class="text-xs text-red-400 mt-1">{{ $t('ai.noModels') }}</p>
-            <p v-else-if="aiModels.length" class="text-xs text-gray-600 mt-1">
-              {{ $t('ai.modelsAvailable', aiModels.length) }}
-            </p>
-          </div>
-
-          <!-- Vision model -->
-          <div>
-            <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.visionModelLabel') }}</label>
-            <input
-              v-model="aiVisionModel"
-              type="text"
-              :placeholder="$t('ai.visionModelPlaceholder')"
-              class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500"
-            />
-            <p class="text-xs text-gray-600 mt-1">{{ $t('ai.visionModelHint') }}</p>
-          </div>
-
-          <!-- Save -->
-          <div class="flex items-center justify-end gap-3">
-            <span v-if="aiSaved" class="text-xs text-green-400">{{ $t('ai.saved') }}</span>
-            <button
-              @click="saveAiConfig"
-              :disabled="aiStore.saving || !aiEndpoint"
-              class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
-            >
-              {{ aiStore.saving ? $t('ai.saving') : $t('ai.saveConfig') }}
-            </button>
-          </div>
-
-        </div>
-      </div>
-
-      <!-- ═══════════════════════════════════════════════════════════════════
-           AI PROVIDERS — OpenAI, Groq, Gemini cards
-      ════════════════════════════════════════════════════════════════════ -->
-      <template v-for="providerName in ['openai', 'groq', 'gemini']" :key="providerName">
-        <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
-
-          <!-- Header -->
-          <div class="p-5 border-b border-gray-800 flex items-center gap-3">
-            <div
-              class="w-9 h-9 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0"
-              :class="providerName === 'openai' ? 'bg-emerald-700' : providerName === 'groq' ? 'bg-orange-700' : 'bg-blue-700'"
-            >
-              {{ providerName === 'openai' ? 'OAI' : providerName === 'groq' ? 'GRQ' : 'GEM' }}
-            </div>
-            <div>
-              <p class="font-semibold">{{ $t(`ai.${providerName}.sectionTitle`) }}</p>
-              <p class="text-xs text-gray-500 mt-0.5">{{ $t(`ai.${providerName}.sectionSubtitle`) }}</p>
-            </div>
-            <div class="ml-auto flex items-center gap-2 shrink-0">
-              <span v-if="aiStore.config.provider === providerName" class="text-xs px-2 py-0.5 rounded-full font-medium bg-violet-900/50 text-violet-300 border border-violet-700">
-                {{ $t('ai.active') }}
-              </span>
-              <span v-else-if="getProvider(providerName)?.configured" class="text-xs px-2 py-0.5 rounded-full font-medium bg-green-900/50 text-green-400 border border-green-700">
-                ✓ {{ $t('ai.apiKeyConfigured') }}
-              </span>
-            </div>
-          </div>
-
-          <div class="p-5 space-y-4">
-
-            <!-- Configured state -->
-            <div v-if="getProvider(providerName)?.configured && !providerForms[providerName].editing">
-              <div class="flex items-center justify-between text-sm">
-                <div class="space-y-1">
-                  <p class="text-xs text-gray-400">{{ $t('ai.apiKeyLabel') }}: <span class="font-mono text-gray-300">{{ getProvider(providerName)?.apiKeyHint }}</span></p>
-                  <p v-if="providerForms[providerName].saved" class="text-xs text-green-400">{{ $t('ai.providerSaved') }}</p>
-                </div>
-                <div class="flex gap-2">
-                  <button @click="providerForms[providerName].editing = true" class="text-xs px-2.5 py-1 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-gray-400 hover:text-gray-200 transition-colors">
-                    Edit
-                  </button>
-                  <button
-                    v-if="aiStore.config.provider !== providerName"
-                    @click="setActiveProvider(providerName)"
-                    :disabled="aiStore.saving"
-                    class="text-xs px-2.5 py-1 bg-violet-700 hover:bg-violet-600 disabled:opacity-40 rounded-md text-white transition-colors"
-                  >
-                    {{ $t('ai.setActive') }}
-                  </button>
-                  <button @click="disconnectCloudProvider(providerName)" class="text-xs px-2.5 py-1 bg-red-900/40 hover:bg-red-900/60 border border-red-800 rounded-md text-red-400 hover:text-red-300 transition-colors">
-                    {{ $t('ai.disconnect') }}
-                  </button>
-                </div>
-              </div>
-
-              <!-- Model selector for configured provider -->
-              <div class="mt-3">
-                <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
-                <select
-                  v-model="providerForms[providerName].model"
-                  class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500"
-                >
-                  <option v-for="m in (PROVIDER_MODELS[providerName] || [])" :key="m" :value="m">{{ m }}</option>
-                </select>
-              </div>
-              <div class="flex justify-end mt-3">
-                <button
-                  @click="saveCloudProvider(providerName, aiStore.config.provider === providerName)"
-                  :disabled="providerForms[providerName].saving"
-                  class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
-                >
-                  {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.saveProvider') }}
-                </button>
-              </div>
-            </div>
-
-            <!-- Unconfigured / editing state -->
-            <div v-else class="space-y-3">
-              <div>
-                <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.apiKeyLabel') }}</label>
-                <input
-                  v-model="providerForms[providerName].apiKey"
-                  type="password"
-                  :placeholder="$t('ai.apiKeyPlaceholder')"
-                  class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-violet-500"
-                />
-                <p class="text-xs text-gray-600 mt-1">{{ $t(`ai.${providerName}.getKeyHint`) }}</p>
-              </div>
-
-              <div>
-                <label class="block text-xs text-gray-500 mb-1">{{ $t('ai.modelLabel') }}</label>
-                <select
-                  v-model="providerForms[providerName].model"
-                  class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:outline-none focus:border-violet-500"
-                >
-                  <option v-for="m in (PROVIDER_MODELS[providerName] || [])" :key="m" :value="m">{{ m }}</option>
-                </select>
-              </div>
-
-              <div class="flex items-center justify-end gap-2">
-                <button v-if="providerForms[providerName].editing" @click="providerForms[providerName].editing = false" class="text-xs px-3 py-2 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 transition-colors">
-                  Cancel
-                </button>
-                <button
-                  @click="saveCloudProvider(providerName, false)"
-                  :disabled="providerForms[providerName].saving || !providerForms[providerName].apiKey"
-                  class="px-3 py-2 bg-gray-700 hover:bg-gray-600 disabled:opacity-40 border border-gray-600 rounded-lg text-xs font-medium transition-colors"
-                >
-                  {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.saveProvider') }}
-                </button>
-                <button
-                  @click="saveCloudProvider(providerName, true)"
-                  :disabled="providerForms[providerName].saving || !providerForms[providerName].apiKey"
-                  class="px-4 py-2 bg-violet-600 hover:bg-violet-700 disabled:opacity-40 rounded-lg text-sm font-medium transition-colors"
-                >
-                  {{ providerForms[providerName].saving ? $t('ai.saving') : $t('ai.connectAndActivate') }}
-                </button>
-              </div>
-            </div>
-
-          </div>
-        </div>
-      </template>
-
-      <!-- ═══════════════════════════════════════════════════════════════════
-           GOOGLE PLACES — local competitor discovery
-      ════════════════════════════════════════════════════════════════════ -->
-      <div class="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden">
-        <div class="p-5 border-b border-gray-800 flex items-center gap-3">
-          <div class="w-9 h-9 rounded-full bg-green-700 flex items-center justify-center shrink-0">
-            <i class="fa-solid fa-location-dot text-white text-sm"></i>
-          </div>
-          <div>
-            <p class="font-semibold">{{ $t('settings.googlePlaces.sectionTitle') }}</p>
-            <p class="text-xs text-gray-500 mt-0.5">{{ $t('settings.googlePlaces.sectionSubtitle') }}</p>
-          </div>
-        </div>
-
-        <div class="p-5 space-y-4">
-          <div v-if="placesConfigured" class="flex items-center justify-between">
-            <span class="text-sm text-gray-300">{{ $t('settings.googlePlaces.keyConfigured', { hint: placesKeyHint }) }}</span>
-            <button @click="removePlacesKey" class="text-xs px-3 py-1.5 border border-red-800/60 text-red-400 hover:bg-red-900/20 rounded-lg transition-colors">
-              {{ $t('settings.googlePlaces.disconnect') }}
-            </button>
-          </div>
-          <div v-else class="space-y-3">
-            <p class="text-xs text-gray-500">{{ $t('settings.googlePlaces.getKeyHint') }}</p>
-            <div class="flex gap-2">
-              <input
-                v-model="placesApiKey"
-                type="password"
-                :placeholder="$t('settings.googlePlaces.keyPlaceholder')"
-                class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-green-500"
-              />
-              <button
-                @click="savePlacesKey"
-                :disabled="!placesApiKey.trim() || placesSaving"
-                class="px-4 py-2 bg-green-700 hover:bg-green-600 disabled:opacity-40 rounded-lg text-sm font-medium text-white transition-colors"
-              >
-                {{ placesSaving ? $t('settings.googlePlaces.saving') : $t('settings.googlePlaces.save') }}
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-
       <!-- ═══════════════════════════════════════════════════════════════════
            WORKSPACE MANAGEMENT
       ════════════════════════════════════════════════════════════════════ -->
@@ -1522,7 +1189,6 @@ import { useRoute } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import axios from 'axios'
 import { usePlatformsStore, PLATFORM_META } from '../stores/platforms'
-import { useAiStore, PROVIDER_MODELS } from '../stores/ai'
 import { useHashtagStore, type HashtagGroup } from '../stores/hashtags'
 import { useWorkspaceStore, type Workspace } from '../stores/workspace'
 import { COMMON_TIMEZONES } from '../utils/timezone'
@@ -1533,7 +1199,6 @@ const { t } = useI18n()
 const route = useRoute()
 const router = useRouter()
 const platformsStore = usePlatformsStore()
-const aiStore = useAiStore()
 const hashtagStore = useHashtagStore()
 const workspaceStore = useWorkspaceStore()
 
@@ -1608,20 +1273,8 @@ async function switchTo(id: string) {
 
 // ─── App credential form state ──────────────────────────────────────────────
 
-const appId = ref('')
-const appSecret = ref('')
-const editingApp = ref(false)
-
 const metaAppConfigured = computed(() => platformsStore.metaCredentials.configured)
 
-async function saveApp() {
-  await platformsStore.saveMetaApp(appId.value, appSecret.value)
-  if (!platformsStore.metaError) {
-    editingApp.value = false
-    appSecret.value = ''
-  }
-}
-
 // ─── Connected platforms derived from statuses ───────────────────────────────
 
 const fbStatus = computed(() => platformsStore.getStatus('facebook'))
@@ -1668,9 +1321,6 @@ const oauthError = ref<string | null>(null)
 
 // ─── Pinterest ────────────────────────────────────────────────────────────────
 
-const pinterestClientId = ref('')
-const pinterestClientSecret = ref('')
-const editingPinterestApp = ref(false)
 const pinterestOauthError = ref<string | null>(null)
 const pinterestBoardsSaved = ref(false)
 const selectedBoardIds = ref<string[]>([])
@@ -1678,14 +1328,6 @@ const selectedBoardIds = ref<string[]>([])
 const pinterestAppConfigured = computed(() => platformsStore.pinterestCredentials.configured)
 const pinterestConnected = computed(() => platformsStore.allPinterestBoards.length > 0)
 
-async function savePinterestApp() {
-  await platformsStore.savePinterestApp(pinterestClientId.value, pinterestClientSecret.value)
-  if (!platformsStore.pinterestError) {
-    editingPinterestApp.value = false
-    pinterestClientSecret.value = ''
-  }
-}
-
 async function savePinterestBoards() {
   await platformsStore.savePinterestBoards(selectedBoardIds.value)
   if (!platformsStore.pinterestError) {
@@ -1702,27 +1344,39 @@ function confirmPinterestDisconnect() {
 
 // ─── TikTok ──────────────────────────────────────────────────────────────────
 
-const tiktokClientKey = ref('')
-const tiktokClientSecret = ref('')
-const editingTikTokApp = ref(false)
 const tiktokOauthError = ref<string | null>(null)
 
 const tiktokAppConfigured = computed(() => platformsStore.tiktokCredentials.configured)
 
-async function saveTikTokApp() {
-  await platformsStore.saveTikTokApp(tiktokClientKey.value, tiktokClientSecret.value)
-  if (!platformsStore.tiktokError) {
-    editingTikTokApp.value = false
-    tiktokClientSecret.value = ''
-  }
-}
-
 function confirmTikTokDisconnect() {
   if (window.confirm(t('settings.tiktok.disconnectConfirm'))) {
     platformsStore.disconnectTikTok().then(loadMetaConnections)
   }
 }
 
+// ─── Workspace page/account selection ────────────────────────────────────────
+
+const workspaceFbPageIds = ref<string[]>([])
+const workspaceIgAccountIds = ref<string[]>([])
+const fbPagesSaved = ref(false)
+const igAccountsSaved = ref(false)
+
+async function saveFbPageSelection() {
+  await platformsStore.saveFbPageSelection(workspaceFbPageIds.value)
+  if (!platformsStore.metaError) {
+    fbPagesSaved.value = true
+    setTimeout(() => { fbPagesSaved.value = false }, 2500)
+  }
+}
+
+async function saveIgAccountSelection() {
+  await platformsStore.saveIgAccountSelection(workspaceIgAccountIds.value)
+  if (!platformsStore.metaError) {
+    igAccountsSaved.value = true
+    setTimeout(() => { igAccountsSaved.value = false }, 2500)
+  }
+}
+
 // ─── Hashtag Groups ──────────────────────────────────────────────────────────
 
 const addingHashtagGroup = ref(false)
@@ -1949,41 +1603,6 @@ async function toggleProfile(key: string) {
   }
 }
 
-// ─── Google Places ────────────────────────────────────────────────────────────
-
-const placesApiKey    = ref('')
-const placesSaving    = ref(false)
-const placesConfigured = ref(false)
-const placesKeyHint   = ref<string | null>(null)
-
-async function loadPlacesConfig() {
-  try {
-    const res = await axios.get('/api/credentials/google-places')
-    placesConfigured.value = res.data.configured
-    placesKeyHint.value = res.data.keyHint
-  } catch { /* not configured */ }
-}
-
-async function savePlacesKey() {
-  if (!placesApiKey.value.trim()) return
-  placesSaving.value = true
-  try {
-    await axios.post('/api/credentials/google-places', { apiKey: placesApiKey.value.trim() })
-    placesConfigured.value = true
-    placesKeyHint.value = `****${placesApiKey.value.trim().slice(-4)}`
-    placesApiKey.value = ''
-  } finally {
-    placesSaving.value = false
-  }
-}
-
-async function removePlacesKey() {
-  if (!confirm(t('settings.googlePlaces.disconnectConfirm'))) return
-  await axios.delete('/api/credentials/google-places')
-  placesConfigured.value = false
-  placesKeyHint.value = null
-}
-
 async function runFiveForces(key: string) {
   fiveForcesRunning.value = key
   try {
@@ -2033,88 +1652,6 @@ async function saveProfile(key: string) {
   }
 }
 
-// ─── AI Configuration ─────────────────────────────────────────────────────────
-
-const aiEndpoint = ref('')
-const aiModel = ref('')
-const aiVisionModel = ref('')
-const aiModels = computed(() => aiStore.models)
-const aiConnected = ref<boolean | null>(null)
-const aiSaved = ref(false)
-
-async function testAiConnection() {
-  const ok = await aiStore.fetchModels(aiEndpoint.value)
-  aiConnected.value = ok
-  if (ok && !aiModel.value && aiStore.models.length) {
-    aiModel.value = aiStore.models[0]
-  }
-}
-
-async function saveAiConfig() {
-  const ok = await aiStore.saveProvider('ollama', { endpoint: aiEndpoint.value, model: aiModel.value, visionModel: aiVisionModel.value, setActive: true })
-  if (ok) {
-    aiSaved.value = true
-    setTimeout(() => { aiSaved.value = false }, 2500)
-  }
-}
-
-// ─── Cloud AI providers (OpenAI, Groq, Gemini) ───────────────────────────────
-
-interface ProviderFormState {
-  apiKey: string
-  model: string
-  editing: boolean
-  saving: boolean
-  saved: boolean
-  testResult: boolean | null
-}
-
-function makeProviderState(): ProviderFormState {
-  return { apiKey: '', model: '', editing: false, saving: false, saved: false, testResult: null }
-}
-
-const providerForms = ref<Record<string, ProviderFormState>>({
-  openai: makeProviderState(),
-  groq:   makeProviderState(),
-  gemini: makeProviderState(),
-})
-
-function getProvider(name: string) {
-  return aiStore.providers.find((p) => p.name === name)
-}
-
-async function saveCloudProvider(name: string, setActive = false) {
-  const form = providerForms.value[name]
-  form.saving = true
-  const ok = await aiStore.saveProvider(name, { apiKey: form.apiKey || undefined, model: form.model || undefined, setActive })
-  form.saving = false
-  if (ok) {
-    form.saved = true
-    form.editing = false
-    form.apiKey = ''
-    setTimeout(() => { form.saved = false }, 2500)
-  }
-}
-
-async function setActiveProvider(name: string) {
-  const provider = getProvider(name)
-  if (!provider?.configured) return
-  await aiStore.saveProvider(name, { setActive: true })
-}
-
-async function disconnectCloudProvider(name: string) {
-  if (!confirm(t('ai.disconnectConfirm'))) return
-  await aiStore.deleteProvider(name)
-}
-
-function seedProviderForms() {
-  for (const p of aiStore.providers) {
-    if (p.name === 'ollama') continue
-    const form = providerForms.value[p.name]
-    if (form) form.model = p.model || ''
-  }
-}
-
 // ─── On mount ────────────────────────────────────────────────────────────────
 
 onMounted(async () => {
@@ -2149,19 +1686,14 @@ onMounted(async () => {
     platformsStore.fetchTikTokCredentials(),
     loadMetaConnections(),
     platformsStore.fetchTokenExpiry(),
-    aiStore.fetchConfig(),
-    aiStore.fetchProviders(),
     hashtagStore.fetchGroups(),
-    loadPlacesConfig(),
   ])
 
   // Seed board checkboxes from current selection
   selectedBoardIds.value = platformsStore.allPinterestBoards.filter((b) => b.selected).map((b) => b.id)
 
-  // Seed local form from fetched config
-  aiEndpoint.value = aiStore.config.endpoint
-  aiModel.value = aiStore.config.model
-  aiVisionModel.value = aiStore.config.visionModel
-  seedProviderForms()
+  // Seed workspace page/account selection from current saved state
+  workspaceFbPageIds.value = platformsStore.allFbPages.filter((p) => p.selected).map((p) => p.id)
+  workspaceIgAccountIds.value = platformsStore.allIgAccounts.filter((a) => a.selected).map((a) => a.id)
 })
 </script>