cognova 0.2.7 → 0.2.9

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 (175) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{Bb9LNSu_.js → -JdScH3W.js} +1 -1
  3. package/.output/public/_nuxt/{A5lrd5_j.js → 0EY9Msdx.js} +1 -1
  4. package/.output/public/_nuxt/{BsXPllgv.js → 1lOC4Si0.js} +1 -1
  5. package/.output/public/_nuxt/{CeDxLTxX.js → 1zIQX9mh.js} +1 -1
  6. package/.output/public/_nuxt/{D3V98LVR.js → 4Qy4OdWt.js} +1 -1
  7. package/.output/public/_nuxt/{MdebRWqA.js → 4nN41Tyd.js} +1 -1
  8. package/.output/public/_nuxt/{BrICHPil.js → 5MZBbx-H.js} +1 -1
  9. package/.output/public/_nuxt/{Brqrl-cH.js → 6lIxuL6n.js} +1 -1
  10. package/.output/public/_nuxt/AUdD1uDD.js +1 -0
  11. package/.output/public/_nuxt/{B9Kd_0HK.js → B-513wJW.js} +1 -1
  12. package/.output/public/_nuxt/{CNwGp6eP.js → B1vDCixB.js} +1 -1
  13. package/.output/public/_nuxt/{deSeNTOh.js → B2MVZlI-.js} +1 -1
  14. package/.output/public/_nuxt/{CVr40yKH.js → B5JDxHxC.js} +1 -1
  15. package/.output/public/_nuxt/{CvMqUdkl.js → B63LIwsW.js} +3 -3
  16. package/.output/public/_nuxt/{C5nfaH9w.js → B6bnHNWY.js} +1 -1
  17. package/.output/public/_nuxt/{BaJEGr8G.js → B6wYZD_K.js} +1 -1
  18. package/.output/public/_nuxt/{_16_b82K.js → B8Ou5yh2.js} +1 -1
  19. package/.output/public/_nuxt/{BaVaTug8.js → BE4Ndhrq.js} +1 -1
  20. package/.output/public/_nuxt/{CphXSvBo.js → BES6FzF5.js} +1 -1
  21. package/.output/public/_nuxt/{BmTCDcNm.js → BE_OHTBv.js} +1 -1
  22. package/.output/public/_nuxt/{CBrXB4hk.js → BJ8jbKuq.js} +1 -1
  23. package/.output/public/_nuxt/{vM-wdhkZ.js → BNBGEa-U.js} +1 -1
  24. package/.output/public/_nuxt/{XzRnkkwG.js → BOGXUgGg.js} +1 -1
  25. package/.output/public/_nuxt/BQ1T6zWA.js +1 -0
  26. package/.output/public/_nuxt/{Bb1B0ntt.js → BVc_fOF1.js} +1 -1
  27. package/.output/public/_nuxt/{B-5roYpk.js → BX6vcvkp.js} +1 -1
  28. package/.output/public/_nuxt/{CLZS246x.js → BYIq3eP8.js} +1 -1
  29. package/.output/public/_nuxt/{B_28CKij.js → BZqdzrSw.js} +2 -2
  30. package/.output/public/_nuxt/{D-wVwRIx.js → BaYtugUf.js} +1 -1
  31. package/.output/public/_nuxt/{EgYyfo1g.js → Be5L93GM.js} +3 -3
  32. package/.output/public/_nuxt/{BLszW9T3.js → BfECQayu.js} +1 -1
  33. package/.output/public/_nuxt/{Ci-MVdCd.js → BgG2k-zy.js} +1 -1
  34. package/.output/public/_nuxt/BhnONkIm.js +1 -0
  35. package/.output/public/_nuxt/{BjFs8Ujb.js → BjI5wb_q.js} +1 -1
  36. package/.output/public/_nuxt/{CK5Ef4YR.js → Bk9-HxOg.js} +1 -1
  37. package/.output/public/_nuxt/{MzEmU9ER.js → BltasKce.js} +1 -1
  38. package/.output/public/_nuxt/{BvuV9uCn.js → Bp9kHdw6.js} +1 -1
  39. package/.output/public/_nuxt/{CnW8zyuA.js → Bsdcw5gr.js} +1 -1
  40. package/.output/public/_nuxt/{Zjvb5YJR.js → BsnhHCOa.js} +1 -1
  41. package/.output/public/_nuxt/{DMsHLHmB.js → BwJVN9k0.js} +1 -1
  42. package/.output/public/_nuxt/{CgQKyBSS.js → BwVt3BzR.js} +1 -1
  43. package/.output/public/_nuxt/{Bc0NIKN7.js → BwdxQyyo.js} +1 -1
  44. package/.output/public/_nuxt/{CfWDee_q.js → BzTjmQCD.js} +1 -1
  45. package/.output/public/_nuxt/{RRoc4m9d.js → C1FhgWcI.js} +1 -1
  46. package/.output/public/_nuxt/{DvgLoqaC.js → C2DZeD9G.js} +1 -1
  47. package/.output/public/_nuxt/{B43p0So7.js → C3JOLqW1.js} +1 -1
  48. package/.output/public/_nuxt/{atVSHCxP.js → C4ScCUjy.js} +1 -1
  49. package/.output/public/_nuxt/{Bn8loaeQ.js → C6HEirAm.js} +1 -1
  50. package/.output/public/_nuxt/{BLL1JLwq.js → C6UTTaCP.js} +1 -1
  51. package/.output/public/_nuxt/{DNpsjOxN.js → C8gtQaKB.js} +1 -1
  52. package/.output/public/_nuxt/{BhOIdu3Z.js → C8tXHRIo.js} +1 -1
  53. package/.output/public/_nuxt/{DaiWSWdm.js → C8wa4Wh4.js} +1 -1
  54. package/.output/public/_nuxt/{DZdh7_lh.js → CDradKPE.js} +1 -1
  55. package/.output/public/_nuxt/{DB3y_21M.js → CEJh-OAl.js} +1 -1
  56. package/.output/public/_nuxt/CGfVTQUF.js +1 -0
  57. package/.output/public/_nuxt/{Dx2Tgm6S.js → CGgZuPnb.js} +1 -1
  58. package/.output/public/_nuxt/{CwaMSU1O.js → CH8kNq6W.js} +1 -1
  59. package/.output/public/_nuxt/{NlScC3GW.js → CIJ2MJdc.js} +1 -1
  60. package/.output/public/_nuxt/{CzTTKfet.js → CIUZAp6g.js} +1 -1
  61. package/.output/public/_nuxt/{CwCEQ4Dw.js → CXBOHJ2W.js} +1 -1
  62. package/.output/public/_nuxt/{BJ_GgKV0.js → CY3x8iy3.js} +1 -1
  63. package/.output/public/_nuxt/{ByfSJZV6.js → CaLO8sre.js} +1 -1
  64. package/.output/public/_nuxt/{jeu3Xx8e.js → CbTu13wM.js} +1 -1
  65. package/.output/public/_nuxt/{CjYUi9c8.js → CfO_d5gC.js} +1 -1
  66. package/.output/public/_nuxt/{DpYns7cT.js → Clr3MHUr.js} +1 -1
  67. package/.output/public/_nuxt/{1Hz98MKF.js → Cmdr5BNz.js} +1 -1
  68. package/.output/public/_nuxt/{DZ_azKLI.js → Cmj-BR0q.js} +1 -1
  69. package/.output/public/_nuxt/{CSQ-nblB.js → Cn83lLUb.js} +1 -1
  70. package/.output/public/_nuxt/{BdCB2PoJ.js → CqMgz85Z.js} +1 -1
  71. package/.output/public/_nuxt/{Dkzqkzki.js → CrW6y2-s.js} +1 -1
  72. package/.output/public/_nuxt/{DAs574wU.js → CrjNndEO.js} +1 -1
  73. package/.output/public/_nuxt/{CB8_A8N4.js → Cs_udj6A.js} +1 -1
  74. package/.output/public/_nuxt/{qW7F87d8.js → CsbHYpNi.js} +3 -3
  75. package/.output/public/_nuxt/{DuOsLoVR.js → CwKe2KkK.js} +1 -1
  76. package/.output/public/_nuxt/{Db5XIfWS.js → CwOUYOe5.js} +1 -1
  77. package/.output/public/_nuxt/{0J8pJb0Z.js → CybU4a95.js} +1 -1
  78. package/.output/public/_nuxt/{WWNeb45u.js → CziltI1u.js} +1 -1
  79. package/.output/public/_nuxt/{DgPiIWA2.js → CzurfTnt.js} +1 -1
  80. package/.output/public/_nuxt/{DAFasgHH.js → D-O8wAju.js} +1 -1
  81. package/.output/public/_nuxt/{CJ85o8FK.js → D1t0272g.js} +1 -1
  82. package/.output/public/_nuxt/D3RROe4s.js +1 -0
  83. package/.output/public/_nuxt/{Cedv1M65.js → D5G5UL_g.js} +1 -1
  84. package/.output/public/_nuxt/{1IuG3yiQ.js → D76Dw8rw.js} +1 -1
  85. package/.output/public/_nuxt/{w3t8WD8v.js → D7DTZ0iS.js} +1 -1
  86. package/.output/public/_nuxt/{C6Kc4XvS.js → D7WbsJsa.js} +1 -1
  87. package/.output/public/_nuxt/D7iYVmH6.js +1 -0
  88. package/.output/public/_nuxt/{CXuKrnTA.js → D8gh62A0.js} +1 -1
  89. package/.output/public/_nuxt/{CW8xtqwN.js → DA2RowFe.js} +1 -1
  90. package/.output/public/_nuxt/{4bC1z31I.js → DBKxzQ3U.js} +1 -1
  91. package/.output/public/_nuxt/DBuW01Fx.js +1 -0
  92. package/.output/public/_nuxt/{B7-0ADn3.js → DCL9gWFw.js} +1 -1
  93. package/.output/public/_nuxt/{NBE83ZQn.js → DGJKfutb.js} +1 -1
  94. package/.output/public/_nuxt/{B5OWqWCh.js → DHScQdvh.js} +1 -1
  95. package/.output/public/_nuxt/{BTk5mgdf.js → DI1FIvaM.js} +1 -1
  96. package/.output/public/_nuxt/{0LyRrTRM.js → DJ4pqxg-.js} +1 -1
  97. package/.output/public/_nuxt/{DImdNj-F.js → DJoRBCVm.js} +1 -1
  98. package/.output/public/_nuxt/{DfpSaqgV.js → DLuRYg2p.js} +1 -1
  99. package/.output/public/_nuxt/DMy2sxuC.js +1 -0
  100. package/.output/public/_nuxt/{BJi2ZHtT.js → DQd-GXKJ.js} +1 -1
  101. package/.output/public/_nuxt/{CpC3C9gj.js → DSgubznn.js} +1 -1
  102. package/.output/public/_nuxt/{BZFXnEwe.js → DSynhNOO.js} +1 -1
  103. package/.output/public/_nuxt/{BsPj3ifk.js → DTrFhdRO.js} +1 -1
  104. package/.output/public/_nuxt/{BNlugvM3.js → DTu0K_pX.js} +1 -1
  105. package/.output/public/_nuxt/{DV0JMebZ.js → DUDbJONU.js} +1 -1
  106. package/.output/public/_nuxt/{C14DGwaT.js → DYu_NYj3.js} +1 -1
  107. package/.output/public/_nuxt/{Bf34lExB.js → D_tCgCFX.js} +1 -1
  108. package/.output/public/_nuxt/{pkbwpgSD.js → DdXVAzIg.js} +1 -1
  109. package/.output/public/_nuxt/DfIqFkyp.js +1 -0
  110. package/.output/public/_nuxt/{BoPAYize.js → Dj2opPDu.js} +1 -1
  111. package/.output/public/_nuxt/{Drr-RSSP.js → DjmFlCHK.js} +1 -1
  112. package/.output/public/_nuxt/{DgKeJf5C.js → DkhxRAb3.js} +1 -1
  113. package/.output/public/_nuxt/DlSJ4TF_.js +1 -0
  114. package/.output/public/_nuxt/{S4a_N6rD.js → DmmQUhEb.js} +1 -1
  115. package/.output/public/_nuxt/{CG0fS-BQ.js → DvATBMPl.js} +1 -1
  116. package/.output/public/_nuxt/{CxJr4mB0.js → DwAJIRGg.js} +1 -1
  117. package/.output/public/_nuxt/{CjRmY2OQ.js → FYOLNuMV.js} +1 -1
  118. package/.output/public/_nuxt/Gy6_ehml.js +1 -0
  119. package/.output/public/_nuxt/{zBoPDS4z.js → HPOXd4gt.js} +1 -1
  120. package/.output/public/_nuxt/{HCCuOAgZ.js → HlbU_z9a.js} +1 -1
  121. package/.output/public/_nuxt/{DsKy-gee.js → JQV21WEV.js} +1 -1
  122. package/.output/public/_nuxt/{JJIcQEna.js → NusIKwmA.js} +1 -1
  123. package/.output/public/_nuxt/{HC3657Xq.js → OrcbFmB1.js} +1 -1
  124. package/.output/public/_nuxt/{fTEmwfo4.js → Oy4_RPEZ.js} +1 -1
  125. package/.output/public/_nuxt/{Dpp5cOwJ.js → Pa936BQP.js} +1 -1
  126. package/.output/public/_nuxt/PzQHm02e.js +1 -0
  127. package/.output/public/_nuxt/{Dysa6np_.js → REP2-rzf.js} +1 -1
  128. package/.output/public/_nuxt/{-2wkydbj.js → RRbnMIVe.js} +1 -1
  129. package/.output/public/_nuxt/{CzqLfT8k.js → TGVM5w2K.js} +1 -1
  130. package/.output/public/_nuxt/UbZP7wjm.js +1 -0
  131. package/.output/public/_nuxt/{BW-E-Sar.js → XJYoLoTi.js} +1 -1
  132. package/.output/public/_nuxt/{CrjKigv8.js → a9_LvIZ4.js} +1 -1
  133. package/.output/public/_nuxt/builds/latest.json +1 -1
  134. package/.output/public/_nuxt/builds/meta/613faa5d-8ace-45e0-8274-b611cc4fd1ad.json +1 -0
  135. package/.output/public/_nuxt/entry.DkvuF_CR.css +1 -0
  136. package/.output/public/_nuxt/{CwJR22-0.js → fVkHtgGF.js} +1 -1
  137. package/.output/public/_nuxt/{sdvIKLwS.js → hm3ShyaF.js} +1 -1
  138. package/.output/public/_nuxt/{CqiOV9n1.js → k16puyWi.js} +1 -1
  139. package/.output/public/_nuxt/{B8Kve-DH.js → myNwwtfj.js} +1 -1
  140. package/.output/public/_nuxt/{DxR8g9So.js → nTCVPCKg.js} +1 -1
  141. package/.output/public/_nuxt/{DytaiikM.js → pHQcHl8n.js} +1 -1
  142. package/.output/public/_nuxt/{Cebw7Q7c.js → pNqWPbXW.js} +1 -1
  143. package/.output/public/_nuxt/{Cow1TrPj.js → qWWnsfRV.js} +1 -1
  144. package/.output/public/_nuxt/{DCco_rlV.js → rRYEvKFu.js} +1 -1
  145. package/.output/public/_nuxt/{hqpqUuXS.js → svWfwR0T.js} +1 -1
  146. package/.output/public/_nuxt/{BMLwzSDu.js → wgX1ldgZ.js} +1 -1
  147. package/.output/public/_nuxt/{CEfkp9Rl.js → xrh9hzzH.js} +2 -2
  148. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  149. package/.output/server/chunks/build/server.mjs +1 -1
  150. package/.output/server/chunks/build/{settings-DuT6LGJZ.mjs → settings-B2KXoGcz.mjs} +1679 -203
  151. package/.output/server/chunks/build/settings-B2KXoGcz.mjs.map +1 -0
  152. package/.output/server/chunks/build/styles.mjs +4 -4
  153. package/.output/server/chunks/nitro/nitro.mjs +922 -866
  154. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  155. package/.output/server/package.json +1 -1
  156. package/app/pages/settings.vue +581 -6
  157. package/package.json +1 -1
  158. package/server/api/skills/import.post.ts +1 -1
  159. package/server/bridge/adapters/telegram.ts +84 -9
  160. package/.output/public/_nuxt/B-YQztaH.js +0 -1
  161. package/.output/public/_nuxt/B1gFtqXK.js +0 -1
  162. package/.output/public/_nuxt/BTfZ7nek.js +0 -1
  163. package/.output/public/_nuxt/BbPnqQJ0.js +0 -1
  164. package/.output/public/_nuxt/BhSWIRpp.js +0 -1
  165. package/.output/public/_nuxt/By6kmkhJ.js +0 -1
  166. package/.output/public/_nuxt/C4XVJnNJ.js +0 -1
  167. package/.output/public/_nuxt/CkvyAKbu.js +0 -1
  168. package/.output/public/_nuxt/D2f7BlK2.js +0 -1
  169. package/.output/public/_nuxt/D2lmhTjQ.js +0 -1
  170. package/.output/public/_nuxt/D64-rCSQ.js +0 -1
  171. package/.output/public/_nuxt/DGP2JJt8.js +0 -1
  172. package/.output/public/_nuxt/builds/meta/9863fe43-d78c-4b29-a1e8-e6de61119507.json +0 -1
  173. package/.output/public/_nuxt/entry._7ZkP07A.css +0 -1
  174. package/.output/public/_nuxt/f7zDjk6q.js +0 -1
  175. package/.output/server/chunks/build/settings-DuT6LGJZ.mjs.map +0 -1
@@ -195,6 +195,50 @@ function formatDate(dateStr: string) {
195
195
  })
196
196
  }
197
197
 
198
+ // === App URL ===
199
+ const appUrl = ref('')
200
+ const appUrlSaving = ref(false)
201
+
202
+ async function loadAppUrl() {
203
+ try {
204
+ const { data } = await $fetch<{ data: { key: string, value: string } }>('/api/secrets/APP_URL')
205
+ appUrl.value = data.value || ''
206
+ } catch {
207
+ appUrl.value = ''
208
+ }
209
+ }
210
+
211
+ async function saveAppUrl() {
212
+ appUrlSaving.value = true
213
+ try {
214
+ if (appUrl.value) {
215
+ // Upsert the APP_URL secret
216
+ const exists = secretsData.value.some(s => s.key === 'APP_URL')
217
+ if (exists) {
218
+ await $fetch('/api/secrets/APP_URL', {
219
+ method: 'PUT',
220
+ body: { value: appUrl.value, description: 'Public URL for webhooks and auth callbacks' }
221
+ })
222
+ } else {
223
+ await $fetch('/api/secrets', {
224
+ method: 'POST',
225
+ body: { key: 'APP_URL', value: appUrl.value, description: 'Public URL for webhooks and auth callbacks' }
226
+ })
227
+ }
228
+ } else {
229
+ // Remove the secret if cleared
230
+ const exists = secretsData.value.some(s => s.key === 'APP_URL')
231
+ if (exists)
232
+ await $fetch('/api/secrets/APP_URL', { method: 'DELETE' })
233
+ }
234
+ toast.add({ title: 'Public URL saved', description: 'Restart running integrations for changes to take effect.', color: 'success' })
235
+ await fetchSecrets()
236
+ } catch {
237
+ toast.add({ title: 'Failed to save URL', color: 'error' })
238
+ }
239
+ appUrlSaving.value = false
240
+ }
241
+
198
242
  // === Notification Preferences ===
199
243
  const notifPrefs = ref<NotificationPreferences>({ ...defaultNotificationPreferences })
200
244
  const notifLoading = ref(false)
@@ -272,6 +316,71 @@ const bridgeForm = reactive({
272
316
  name: ''
273
317
  })
274
318
 
319
+ // Bridge config modal state
320
+ const bridgeConfigModal = ref(false)
321
+ const editingBridge = ref<Bridge | null>(null)
322
+ const bridgeConfigSaving = ref(false)
323
+
324
+ const bridgeConfigForm = reactive({
325
+ name: '',
326
+ // Telegram
327
+ botUsername: '',
328
+ allowedChatIds: '',
329
+ // Discord
330
+ listenMode: 'mentions' as 'mentions' | 'dm' | 'all',
331
+ guildId: '',
332
+ channelId: '',
333
+ // iMessage
334
+ strategy: 'imsg' as 'imsg' | 'bluebubbles',
335
+ allowedNumbers: '',
336
+ blueBubblesUrl: '',
337
+ // Google
338
+ enabledServices: [] as string[],
339
+ account: '',
340
+ // Email
341
+ imapHost: '',
342
+ imapPort: '',
343
+ smtpHost: '',
344
+ smtpPort: '',
345
+ emailAddress: ''
346
+ })
347
+
348
+ const discordListenModeOptions = [
349
+ { value: 'mentions', label: 'Mentions & DMs' },
350
+ { value: 'dm', label: 'DMs only' },
351
+ { value: 'all', label: 'All messages' }
352
+ ]
353
+
354
+ const imessageStrategyOptions = [
355
+ { value: 'imsg', label: 'Local (imsg CLI)' },
356
+ { value: 'bluebubbles', label: 'BlueBubbles' }
357
+ ]
358
+
359
+ const googleServiceOptions = [
360
+ { value: 'gmail', label: 'Gmail' },
361
+ { value: 'calendar', label: 'Calendar' },
362
+ { value: 'drive', label: 'Drive' },
363
+ { value: 'contacts', label: 'Contacts' },
364
+ { value: 'tasks', label: 'Tasks' }
365
+ ]
366
+
367
+ const platformNamePlaceholders: Record<BridgePlatform, string> = {
368
+ telegram: 'My Telegram Bot',
369
+ discord: 'My Discord Bot',
370
+ imessage: 'iMessage Bridge',
371
+ google: 'Google Suite',
372
+ email: 'Work Email'
373
+ }
374
+
375
+ // Secrets required to create a bridge (blocks creation if missing)
376
+ const platformRequiredSecrets: Record<BridgePlatform, string[]> = {
377
+ telegram: ['TELEGRAM_BOT_TOKEN'],
378
+ discord: ['DISCORD_BOT_TOKEN'],
379
+ imessage: [],
380
+ google: [],
381
+ email: []
382
+ }
383
+
275
384
  const platformOptions: { value: BridgePlatform, label: string, icon: string }[] = [
276
385
  { value: 'telegram', label: 'Telegram', icon: 'i-lucide-send' },
277
386
  { value: 'discord', label: 'Discord', icon: 'i-lucide-message-circle' },
@@ -280,6 +389,13 @@ const platformOptions: { value: BridgePlatform, label: string, icon: string }[]
280
389
  { value: 'email', label: 'Email (IMAP)', icon: 'i-lucide-at-sign' }
281
390
  ]
282
391
 
392
+ function getMissingSecrets(platform: BridgePlatform): string[] {
393
+ const required = platformRequiredSecrets[platform]
394
+ if (!required.length) return []
395
+ const existing = new Set(secretsData.value.map(s => s.key))
396
+ return required.filter(k => !existing.has(k))
397
+ }
398
+
283
399
  const healthColors: Record<string, string> = {
284
400
  connected: 'text-success',
285
401
  disconnected: 'text-dimmed',
@@ -298,6 +414,119 @@ async function fetchBridges() {
298
414
  bridgesLoading.value = false
299
415
  }
300
416
 
417
+ function openBridgeConfig(bridge: Bridge) {
418
+ editingBridge.value = bridge
419
+ bridgeConfigForm.name = bridge.name
420
+
421
+ const config = bridge.config ? JSON.parse(bridge.config) : {}
422
+
423
+ // Reset all fields
424
+ bridgeConfigForm.botUsername = ''
425
+ bridgeConfigForm.allowedChatIds = ''
426
+ bridgeConfigForm.listenMode = 'mentions'
427
+ bridgeConfigForm.guildId = ''
428
+ bridgeConfigForm.channelId = ''
429
+ bridgeConfigForm.strategy = 'imsg'
430
+ bridgeConfigForm.allowedNumbers = ''
431
+ bridgeConfigForm.blueBubblesUrl = ''
432
+ bridgeConfigForm.enabledServices = []
433
+ bridgeConfigForm.account = ''
434
+ bridgeConfigForm.imapHost = ''
435
+ bridgeConfigForm.imapPort = ''
436
+ bridgeConfigForm.smtpHost = ''
437
+ bridgeConfigForm.smtpPort = ''
438
+ bridgeConfigForm.emailAddress = ''
439
+
440
+ // Populate platform-specific fields from config
441
+ switch (bridge.platform) {
442
+ case 'telegram':
443
+ bridgeConfigForm.botUsername = config.botUsername || ''
444
+ bridgeConfigForm.allowedChatIds = (config.allowedChatIds || []).join(', ')
445
+ break
446
+ case 'discord':
447
+ bridgeConfigForm.listenMode = config.listenMode || 'mentions'
448
+ bridgeConfigForm.guildId = config.guildId || ''
449
+ bridgeConfigForm.channelId = config.channelId || ''
450
+ break
451
+ case 'imessage':
452
+ bridgeConfigForm.strategy = config.strategy || 'imsg'
453
+ bridgeConfigForm.allowedNumbers = (config.allowedNumbers || []).join(', ')
454
+ bridgeConfigForm.blueBubblesUrl = config.blueBubblesUrl || ''
455
+ break
456
+ case 'google':
457
+ bridgeConfigForm.enabledServices = config.enabledServices || []
458
+ bridgeConfigForm.account = config.account || ''
459
+ break
460
+ case 'email':
461
+ bridgeConfigForm.imapHost = config.imapHost || ''
462
+ bridgeConfigForm.imapPort = config.imapPort?.toString() || ''
463
+ bridgeConfigForm.smtpHost = config.smtpHost || ''
464
+ bridgeConfigForm.smtpPort = config.smtpPort?.toString() || ''
465
+ bridgeConfigForm.emailAddress = config.emailAddress || ''
466
+ break
467
+ }
468
+
469
+ bridgeConfigModal.value = true
470
+ }
471
+
472
+ async function handleBridgeConfigSave() {
473
+ if (!editingBridge.value) return
474
+
475
+ const platform = editingBridge.value.platform
476
+ let config: Record<string, unknown> = {}
477
+
478
+ // Preserve existing config (like webhookSecret) and merge new fields
479
+ if (editingBridge.value.config)
480
+ config = JSON.parse(editingBridge.value.config)
481
+
482
+ switch (platform) {
483
+ case 'telegram':
484
+ config.botUsername = bridgeConfigForm.botUsername || undefined
485
+ config.allowedChatIds = bridgeConfigForm.allowedChatIds
486
+ ? bridgeConfigForm.allowedChatIds.split(',').map(s => s.trim()).filter(Boolean)
487
+ : undefined
488
+ break
489
+ case 'discord':
490
+ config.listenMode = bridgeConfigForm.listenMode
491
+ config.guildId = bridgeConfigForm.guildId || undefined
492
+ config.channelId = bridgeConfigForm.channelId || undefined
493
+ break
494
+ case 'imessage':
495
+ config.strategy = bridgeConfigForm.strategy
496
+ config.allowedNumbers = bridgeConfigForm.allowedNumbers
497
+ ? bridgeConfigForm.allowedNumbers.split(',').map(s => s.trim()).filter(Boolean)
498
+ : undefined
499
+ config.blueBubblesUrl = bridgeConfigForm.blueBubblesUrl || undefined
500
+ break
501
+ case 'google':
502
+ config.enabledServices = bridgeConfigForm.enabledServices
503
+ config.account = bridgeConfigForm.account || undefined
504
+ break
505
+ case 'email':
506
+ config.imapHost = bridgeConfigForm.imapHost || undefined
507
+ config.imapPort = bridgeConfigForm.imapPort ? Number(bridgeConfigForm.imapPort) : undefined
508
+ config.smtpHost = bridgeConfigForm.smtpHost || undefined
509
+ config.smtpPort = bridgeConfigForm.smtpPort ? Number(bridgeConfigForm.smtpPort) : undefined
510
+ config.emailAddress = bridgeConfigForm.emailAddress || undefined
511
+ break
512
+ }
513
+
514
+ bridgeConfigSaving.value = true
515
+ try {
516
+ await $fetch(`/api/bridges/${editingBridge.value.id}` as string, {
517
+ method: 'PUT',
518
+ body: { name: bridgeConfigForm.name, config }
519
+ })
520
+ toast.add({ title: 'Integration updated', color: 'success' })
521
+ bridgeConfigModal.value = false
522
+ await fetchBridges()
523
+ } catch (err: unknown) {
524
+ const error = err as { data?: { message?: string } }
525
+ toast.add({ title: 'Failed to update integration', description: error.data?.message, color: 'error' })
526
+ }
527
+ bridgeConfigSaving.value = false
528
+ }
529
+
301
530
  function openCreateBridge() {
302
531
  bridgeForm.platform = 'telegram'
303
532
  bridgeForm.name = ''
@@ -309,6 +538,15 @@ async function handleBridgeCreate() {
309
538
  toast.add({ title: 'Name is required', color: 'error' })
310
539
  return
311
540
  }
541
+ const missing = getMissingSecrets(bridgeForm.platform)
542
+ if (missing.length) {
543
+ toast.add({
544
+ title: 'Missing required secrets',
545
+ description: `Add ${missing.join(', ')} in the Secrets tab first.`,
546
+ color: 'error'
547
+ })
548
+ return
549
+ }
312
550
  bridgeSaving.value = true
313
551
  try {
314
552
  await $fetch('/api/bridges', {
@@ -327,7 +565,7 @@ async function handleBridgeCreate() {
327
565
 
328
566
  async function toggleBridge(bridge: Bridge) {
329
567
  try {
330
- await $fetch(`/api/bridges/${bridge.id}`, {
568
+ await $fetch(`/api/bridges/${bridge.id}` as string, {
331
569
  method: 'PUT',
332
570
  body: { enabled: !bridge.enabled }
333
571
  })
@@ -345,7 +583,7 @@ function confirmDeleteBridge(bridge: Bridge) {
345
583
  async function handleDeleteBridge() {
346
584
  if (!bridgeToDelete.value) return
347
585
  try {
348
- await $fetch(`/api/bridges/${bridgeToDelete.value.id}`, { method: 'DELETE' })
586
+ await $fetch(`/api/bridges/${bridgeToDelete.value.id}` as string, { method: 'DELETE' })
349
587
  toast.add({ title: 'Integration deleted', color: 'success' })
350
588
  bridgeDeleteConfirm.value = false
351
589
  bridgeToDelete.value = null
@@ -355,11 +593,12 @@ async function handleDeleteBridge() {
355
593
  }
356
594
  }
357
595
 
358
- // Load secrets + notification prefs + bridges when component mounts
596
+ // Load secrets + notification prefs + bridges + app url when component mounts
359
597
  onMounted(() => {
360
598
  fetchSecrets()
361
599
  loadNotificationPrefs()
362
600
  fetchBridges()
601
+ loadAppUrl()
363
602
  })
364
603
 
365
604
  // Form handlers
@@ -717,7 +956,8 @@ async function handlePasswordSubmit() {
717
956
  <div
718
957
  v-for="bridge in bridgesData"
719
958
  :key="bridge.id"
720
- class="border border-default rounded-lg px-4 py-3"
959
+ class="border border-default rounded-lg px-4 py-3 cursor-pointer hover:bg-elevated/50 transition-colors"
960
+ @click="openBridgeConfig(bridge)"
721
961
  >
722
962
  <div class="flex items-center justify-between">
723
963
  <div class="flex items-center gap-3">
@@ -739,6 +979,7 @@ async function handlePasswordSubmit() {
739
979
  </span>
740
980
  <USwitch
741
981
  :model-value="bridge.enabled"
982
+ @click.stop
742
983
  @update:model-value="toggleBridge(bridge)"
743
984
  />
744
985
  <UButton
@@ -746,7 +987,7 @@ async function handlePasswordSubmit() {
746
987
  color="error"
747
988
  icon="i-lucide-trash-2"
748
989
  size="xs"
749
- @click="confirmDeleteBridge(bridge)"
990
+ @click.stop="confirmDeleteBridge(bridge)"
750
991
  />
751
992
  </div>
752
993
  </div>
@@ -764,6 +1005,35 @@ async function handlePasswordSubmit() {
764
1005
  <!-- App Tab -->
765
1006
  <template #app>
766
1007
  <div class="max-w-2xl mx-auto py-6">
1008
+ <!-- Public URL -->
1009
+ <div class="mb-8">
1010
+ <h3 class="text-lg font-semibold mb-1">
1011
+ Public URL
1012
+ </h3>
1013
+ <p class="text-sm text-dimmed mb-4">
1014
+ The publicly accessible URL for this instance. Used for Telegram webhooks, auth callbacks, and other integrations that need to reach your server.
1015
+ </p>
1016
+ <div class="flex gap-2">
1017
+ <UInput
1018
+ v-model="appUrl"
1019
+ placeholder="https://example.com"
1020
+ class="flex-1"
1021
+ />
1022
+ <UButton
1023
+ :loading="appUrlSaving"
1024
+ @click="saveAppUrl"
1025
+ >
1026
+ Save
1027
+ </UButton>
1028
+ </div>
1029
+ <p class="text-xs text-dimmed mt-2">
1030
+ Leave empty to use long-polling for Telegram instead of webhooks. Changes require restarting running integrations.
1031
+ </p>
1032
+ </div>
1033
+
1034
+ <USeparator class="mb-8" />
1035
+
1036
+ <!-- Notification Preferences -->
767
1037
  <div class="mb-6">
768
1038
  <h3 class="text-lg font-semibold mb-1">
769
1039
  Notification Preferences
@@ -974,13 +1244,41 @@ async function handlePasswordSubmit() {
974
1244
  class="w-full"
975
1245
  />
976
1246
  </UFormField>
1247
+
1248
+ <!-- Missing secrets warning -->
1249
+ <div
1250
+ v-if="getMissingSecrets(bridgeForm.platform).length"
1251
+ class="flex items-start gap-2 rounded-lg bg-error/10 border border-error/20 p-3 text-sm"
1252
+ >
1253
+ <UIcon
1254
+ name="i-lucide-alert-triangle"
1255
+ class="size-4 mt-0.5 text-error shrink-0"
1256
+ />
1257
+ <div>
1258
+ <p class="font-medium text-error">
1259
+ Missing required secrets
1260
+ </p>
1261
+ <p class="text-dimmed mt-1">
1262
+ Add the following in Settings &rarr; Secrets before creating:
1263
+ </p>
1264
+ <ul class="mt-1 space-y-0.5">
1265
+ <li
1266
+ v-for="key in getMissingSecrets(bridgeForm.platform)"
1267
+ :key="key"
1268
+ >
1269
+ <code class="text-xs bg-elevated px-1.5 py-0.5 rounded">{{ key }}</code>
1270
+ </li>
1271
+ </ul>
1272
+ </div>
1273
+ </div>
1274
+
977
1275
  <UFormField
978
1276
  label="Name"
979
1277
  name="name"
980
1278
  >
981
1279
  <UInput
982
1280
  v-model="bridgeForm.name"
983
- placeholder="My Telegram Bot"
1281
+ :placeholder="platformNamePlaceholders[bridgeForm.platform]"
984
1282
  class="w-full"
985
1283
  />
986
1284
  </UFormField>
@@ -993,6 +1291,7 @@ async function handlePasswordSubmit() {
993
1291
  </UButton>
994
1292
  <UButton
995
1293
  :loading="bridgeSaving"
1294
+ :disabled="getMissingSecrets(bridgeForm.platform).length > 0"
996
1295
  @click="handleBridgeCreate"
997
1296
  >
998
1297
  Create
@@ -1032,5 +1331,281 @@ async function handlePasswordSubmit() {
1032
1331
  </div>
1033
1332
  </template>
1034
1333
  </UModal>
1334
+
1335
+ <!-- Bridge Config Modal -->
1336
+ <UModal v-model:open="bridgeConfigModal">
1337
+ <template #header>
1338
+ <div class="flex items-center gap-2">
1339
+ <UIcon
1340
+ :name="platformOptions.find(p => p.value === editingBridge?.platform)?.icon || 'i-lucide-plug'"
1341
+ class="size-5"
1342
+ />
1343
+ <h3 class="text-lg font-semibold">
1344
+ Configure {{ editingBridge?.name }}
1345
+ </h3>
1346
+ </div>
1347
+ </template>
1348
+ <template #body>
1349
+ <div class="space-y-4">
1350
+ <!-- Secret hint -->
1351
+ <div
1352
+ v-if="editingBridge && platformRequiredSecrets[editingBridge.platform].length"
1353
+ class="flex items-start gap-2 rounded-lg bg-elevated p-3 text-sm"
1354
+ >
1355
+ <UIcon
1356
+ name="i-lucide-info"
1357
+ class="size-4 mt-0.5 text-dimmed shrink-0"
1358
+ />
1359
+ <span class="text-dimmed">
1360
+ Requires
1361
+ <template
1362
+ v-for="(key, i) in platformRequiredSecrets[editingBridge.platform]"
1363
+ :key="key"
1364
+ >
1365
+ <code class="bg-elevated px-1 py-0.5 rounded text-xs">{{ key }}</code><template v-if="i < platformRequiredSecrets[editingBridge.platform].length - 1">, </template>
1366
+ </template>
1367
+ in the Secrets tab.
1368
+ </span>
1369
+ </div>
1370
+
1371
+ <!-- Common: Name -->
1372
+ <UFormField
1373
+ label="Name"
1374
+ name="name"
1375
+ >
1376
+ <UInput
1377
+ v-model="bridgeConfigForm.name"
1378
+ placeholder="Integration name"
1379
+ class="w-full"
1380
+ />
1381
+ </UFormField>
1382
+
1383
+ <!-- Telegram -->
1384
+ <template v-if="editingBridge?.platform === 'telegram'">
1385
+ <UFormField
1386
+ label="Bot Username"
1387
+ name="botUsername"
1388
+ hint="Without the @ prefix"
1389
+ >
1390
+ <UInput
1391
+ v-model="bridgeConfigForm.botUsername"
1392
+ placeholder="my_bot"
1393
+ class="w-full"
1394
+ />
1395
+ </UFormField>
1396
+ <UFormField
1397
+ label="Allowed Chat IDs"
1398
+ name="allowedChatIds"
1399
+ hint="Comma-separated. Leave empty for all."
1400
+ >
1401
+ <UInput
1402
+ v-model="bridgeConfigForm.allowedChatIds"
1403
+ placeholder="123456, -100789"
1404
+ class="w-full"
1405
+ />
1406
+ </UFormField>
1407
+ </template>
1408
+
1409
+ <!-- Discord -->
1410
+ <template v-if="editingBridge?.platform === 'discord'">
1411
+ <UFormField
1412
+ label="Listen Mode"
1413
+ name="listenMode"
1414
+ >
1415
+ <USelect
1416
+ v-model="bridgeConfigForm.listenMode"
1417
+ :items="discordListenModeOptions"
1418
+ value-key="value"
1419
+ class="w-full"
1420
+ />
1421
+ </UFormField>
1422
+ <UFormField
1423
+ label="Server ID"
1424
+ name="guildId"
1425
+ hint="Optional. Limit to one server."
1426
+ >
1427
+ <UInput
1428
+ v-model="bridgeConfigForm.guildId"
1429
+ placeholder="Discord server ID"
1430
+ class="w-full"
1431
+ />
1432
+ </UFormField>
1433
+ <UFormField
1434
+ label="Channel ID"
1435
+ name="channelId"
1436
+ hint="Optional. Limit to one channel (for 'all' mode)."
1437
+ >
1438
+ <UInput
1439
+ v-model="bridgeConfigForm.channelId"
1440
+ placeholder="Discord channel ID"
1441
+ class="w-full"
1442
+ />
1443
+ </UFormField>
1444
+ </template>
1445
+
1446
+ <!-- iMessage -->
1447
+ <template v-if="editingBridge?.platform === 'imessage'">
1448
+ <UFormField
1449
+ label="Strategy"
1450
+ name="strategy"
1451
+ >
1452
+ <USelect
1453
+ v-model="bridgeConfigForm.strategy"
1454
+ :items="imessageStrategyOptions"
1455
+ value-key="value"
1456
+ class="w-full"
1457
+ />
1458
+ </UFormField>
1459
+ <div
1460
+ v-if="bridgeConfigForm.strategy === 'bluebubbles' && !secretsData.some(s => s.key === 'BLUEBUBBLES_PASSWORD')"
1461
+ class="flex items-start gap-2 rounded-lg bg-error/10 border border-error/20 p-3 text-sm"
1462
+ >
1463
+ <UIcon
1464
+ name="i-lucide-alert-triangle"
1465
+ class="size-4 mt-0.5 text-error shrink-0"
1466
+ />
1467
+ <span class="text-dimmed">
1468
+ BlueBubbles requires secret <code class="text-xs bg-elevated px-1.5 py-0.5 rounded">BLUEBUBBLES_PASSWORD</code> in the Secrets tab.
1469
+ </span>
1470
+ </div>
1471
+ <UFormField
1472
+ v-if="bridgeConfigForm.strategy === 'bluebubbles'"
1473
+ label="BlueBubbles URL"
1474
+ name="blueBubblesUrl"
1475
+ >
1476
+ <UInput
1477
+ v-model="bridgeConfigForm.blueBubblesUrl"
1478
+ placeholder="http://192.168.1.100:1234"
1479
+ class="w-full"
1480
+ />
1481
+ </UFormField>
1482
+ <UFormField
1483
+ label="Allowed Numbers"
1484
+ name="allowedNumbers"
1485
+ hint="Comma-separated. Leave empty for all."
1486
+ >
1487
+ <UInput
1488
+ v-model="bridgeConfigForm.allowedNumbers"
1489
+ placeholder="+15551234567, +15559876543"
1490
+ class="w-full"
1491
+ />
1492
+ </UFormField>
1493
+ </template>
1494
+
1495
+ <!-- Google Suite -->
1496
+ <template v-if="editingBridge?.platform === 'google'">
1497
+ <UFormField
1498
+ label="Enabled Services"
1499
+ name="enabledServices"
1500
+ >
1501
+ <div class="space-y-2">
1502
+ <label
1503
+ v-for="svc in googleServiceOptions"
1504
+ :key="svc.value"
1505
+ class="flex items-center gap-2 cursor-pointer"
1506
+ >
1507
+ <input
1508
+ type="checkbox"
1509
+ :checked="bridgeConfigForm.enabledServices.includes(svc.value)"
1510
+ class="rounded"
1511
+ @change="
1512
+ bridgeConfigForm.enabledServices.includes(svc.value)
1513
+ ? bridgeConfigForm.enabledServices = bridgeConfigForm.enabledServices.filter(s => s !== svc.value)
1514
+ : bridgeConfigForm.enabledServices.push(svc.value)
1515
+ "
1516
+ >
1517
+ <span class="text-sm">{{ svc.label }}</span>
1518
+ </label>
1519
+ </div>
1520
+ </UFormField>
1521
+ <UFormField
1522
+ label="Google Account"
1523
+ name="account"
1524
+ hint="Email used with gogcli"
1525
+ >
1526
+ <UInput
1527
+ v-model="bridgeConfigForm.account"
1528
+ placeholder="user@gmail.com"
1529
+ class="w-full"
1530
+ />
1531
+ </UFormField>
1532
+ </template>
1533
+
1534
+ <!-- Email (IMAP/SMTP) -->
1535
+ <template v-if="editingBridge?.platform === 'email'">
1536
+ <UFormField
1537
+ label="Email Address"
1538
+ name="emailAddress"
1539
+ >
1540
+ <UInput
1541
+ v-model="bridgeConfigForm.emailAddress"
1542
+ placeholder="user@example.com"
1543
+ class="w-full"
1544
+ />
1545
+ </UFormField>
1546
+ <div class="grid grid-cols-2 gap-4">
1547
+ <UFormField
1548
+ label="IMAP Host"
1549
+ name="imapHost"
1550
+ >
1551
+ <UInput
1552
+ v-model="bridgeConfigForm.imapHost"
1553
+ placeholder="imap.example.com"
1554
+ class="w-full"
1555
+ />
1556
+ </UFormField>
1557
+ <UFormField
1558
+ label="IMAP Port"
1559
+ name="imapPort"
1560
+ >
1561
+ <UInput
1562
+ v-model="bridgeConfigForm.imapPort"
1563
+ placeholder="993"
1564
+ class="w-full"
1565
+ />
1566
+ </UFormField>
1567
+ </div>
1568
+ <div class="grid grid-cols-2 gap-4">
1569
+ <UFormField
1570
+ label="SMTP Host"
1571
+ name="smtpHost"
1572
+ >
1573
+ <UInput
1574
+ v-model="bridgeConfigForm.smtpHost"
1575
+ placeholder="smtp.example.com"
1576
+ class="w-full"
1577
+ />
1578
+ </UFormField>
1579
+ <UFormField
1580
+ label="SMTP Port"
1581
+ name="smtpPort"
1582
+ >
1583
+ <UInput
1584
+ v-model="bridgeConfigForm.smtpPort"
1585
+ placeholder="587"
1586
+ class="w-full"
1587
+ />
1588
+ </UFormField>
1589
+ </div>
1590
+ </template>
1591
+
1592
+ <!-- Save / Cancel -->
1593
+ <div class="flex justify-end gap-2 pt-4">
1594
+ <UButton
1595
+ variant="ghost"
1596
+ @click="bridgeConfigModal = false"
1597
+ >
1598
+ Cancel
1599
+ </UButton>
1600
+ <UButton
1601
+ :loading="bridgeConfigSaving"
1602
+ @click="handleBridgeConfigSave"
1603
+ >
1604
+ Save
1605
+ </UButton>
1606
+ </div>
1607
+ </div>
1608
+ </template>
1609
+ </UModal>
1035
1610
  </UDashboardPanel>
1036
1611
  </template>