PageSourceSearch

https://tailyx.ai/assets/controllers/lead_automation_engine_contro…3e3c92358f5586fd996fbb90a3cf279c47f02e73.js

js tailyx.ai collected 2026-10-05 13:42:42 UTC 25,338 bytes, 802 lines download raw bytes

1// app/javascript/controllers/lead_automation_engine_controller.js
2import { Controller } from "@hotwired/stimulus"
3
4// Identifier: data-controller="lead-automation-engine"
5export default class extends Controller {
6  static targets = [
7    "publishNotification",
8    "notificationSubtitle",
9    "tierListView",
10    "tierDetailView",
11    "tierDetailsPanel",
12    "detailTitle",
13    "actionLibrary",
14    "emailTemplates",
15    "actionModal",
16    "modalTitle",
17    "modalBody"
18  ]
19
20  static values = {
21    csrf: String,
22    restoreUrl: String
23  }
24
25  connect() {
26    // Hide publish banner by default
27    if (this.hasPublishNotificationTarget) {
28      this.publishNotificationTarget.classList.remove("show")
29    }
30
31    // Start with detail view hidden; we’ll show it once we pick a tier
32    if (this.hasTierDetailViewTarget) {
33      this.tierDetailViewTarget.classList.add("hidden")
34    }
35
36    // Internal state: per-tier welcome message (subject/body)
37    this.welcomeState = {}
38    this.dirty = false
39    this.senderDirty = false
40    this.calendarDirty = false   // 👈 track calendar changes separately
41
42    // Load templates from inline JSON so we can prefill welcome messages
43    this.templates = []
44    this._loadTemplatesFromDOM()
45
46    // Open Hot Leads by default
47    this._selectDefaultTier()
48  }
49
50
51  // ---------------------------------------------------------------------------
52  // TEMPLATE HANDLING
53  // ---------------------------------------------------------------------------
54
55  _loadTemplatesFromDOM() {
56    try {
57      const el = document.getElementById("automation-email-templates")
58      if (!el) return
59      const text = (el.textContent || "").trim()
60      if (!text) return
61
62      const parsed = JSON.parse(text)
63      const rawList = Array.isArray(parsed)
64        ? parsed
65        : Array.isArray(parsed?.templates)
66          ? parsed.templates
67          : []
68
69      this.templates = this._normalizeTemplates(rawList)
70    } catch (e) {
71      console.warn("[lead-automation-engine] inline template JSON parse failed:", e)
72      this.templates = []
73    }
74  }
75
76  _normalizeTemplates(list) {
77    const arr = Array.isArray(list) ? list : []
78    const byId = new Map()
79
80    const friendlyFromId = (id) => {
81      return String(id)
82        .replace(/^welcome_tier_/, "")
83        .replace(/_/g, " ")
84        .replace(/\b\w/g, (c) => c.toUpperCase())
85    }
86
87    for (const t of arr) {
88      const id = String(t?.id ?? t?.key ?? t?.slug ?? "")
89      if (!id) continue
90
91      // Infer band from id if missing
92      let band = t.band || null
93      const lowerId = id.toLowerCase()
94      if (!band) {
95        if (lowerId.includes("_hot")) {
96          band = "hot"
97        } else if (lowerId.includes("_warm")) {
98          band = "warm"
99        } else if (lowerId.includes("_lukewarm")) {
100          band = "lukewarm"
101        } else if (lowerId.includes("_cold")) {
102          band = "cold"
103        }
104      }
105
106      const name = t.name || t.title || friendlyFromId(id)
107
108      // Subject: use existing subject if present, else name
109      const subject = t.subject || name
110
111      // Email body: use existing html/text if present, else sensible defaults
112      let html_body = t.html_body || t.text_body || ""
113
114      if (!html_body) {
115        if (band === "hot") {
116          html_body = [
117            "<p>Hi {{lead_first_name}},</p>",
118            "<p>Thanks for reaching out and for sharing more about your needs.</p>",
119            "<p>Based on your answers, you look like a strong fit for what we do at {{company_name}}.</p>",
120            "<p>We’ll review your details and get back to you shortly with next steps.</p>",
121            "<p>Best,<br>The {{company_name}} Team</p>"
122          ].join("")
123        } else if (band === "warm") {
124          html_body = [
125            "<p>Hi {{lead_first_name}},</p>",
126            "<p>Thanks for getting in touch.</p>",
127            "<p>You look like a promising fit, and we’d love to explore how we can help your business.</p>",
128            "<p>We’ll review your answers and follow up with suggested next steps.</p>",
129            "<p>Best,<br>The {{company_name}} Team</p>"
130          ].join("")
131        } else if (band === "lukewarm") {
132          html_body = [
133            "<p>Hi {{lead_first_name}},</p>",
134            "<p>Thank you for your interest.</p>",
135            "<p>You might be a bit earlier in your journey, but we have resources that can help you clarify next steps.</p>",
136            "<p>We’ll send over a few suggestions and options for you to review.</p>",
137            "<p>Best,<br>The {{company_name}} Team</p>"
138          ].join("")
139        } else if (band === "cold") {
140          html_body = [
141            "<p>Hi {{lead_first_name}},</p>",
142            "<p>Thanks for reaching out.</p>",
143            "<p>
143Right now, you may not be an immediate fit for our core offer, but we’ll keep you in mind as your needs evolve.</p>",
144            "<p>In the meantime, we’ll share a few resources that might still be useful.</p>",
145            "<p>Best,<br>The {{company_name}} Team</p>"
146          ].join("")
147        } else {
148          // Generic fallback for non-tier-specific templates
149          html_body = [
150            "<p>Hi {{lead_first_name}},</p>",
151            "<p>Thanks for getting in touch with {{company_name}}.</p>",
152            "<p>We’ve received your details and will follow up with you shortly.</p>",
153            "<p>Best,<br>The {{company_name}} Team</p>"
154          ].join("")
155        }
156      }
157
158      const description =
159        t.description ||
160        (band ? `Welcome email for ${band} leads` : "Email template")
161
162      const base = {
163        id,
164        key: id,
165        name,
166        title: name,
167        subject,
168        html_body,
169        text_body: "", // we only use html_body in the UI
170        band,
171        description,
172        scope: t.scope || "global",
173        version: t.version || 1,
174        locale: t.locale || "en"
175      }
176
177      const existing = byId.get(id)
178      if (!existing) {
179        byId.set(id, base)
180      } else {
181        // Prefer company-scoped overrides if they exist
182        const isCompany = (t.scope === "company")
183        if (isCompany) byId.set(id, base)
184      }
185    }
186
187    return Array.from(byId.values()).sort((a, b) => {
188      const scopeWeight = (x) => (x.scope === "company" ? 0 : 1)
189      const sw = scopeWeight(a) - scopeWeight(b)
190      if (sw !== 0) return sw
191      return (a.name || "").localeCompare(b.name || "")
192    })
193  }
194
195   _templateForBand(band) {
196    if (!band) return null
197
198    const lower = String(band).toLowerCase()
199    const templates = this.templates || []
200
201    // 1) Explicit mapping: welcome_tier_* should always be the primary for each band
202    const welcomeKeyByBand = {
203      hot: "welcome_tier_hot",
204      warm: "welcome_tier_warm",
205      lukewarm: "welcome_tier_lukewarm",
206      cold: "welcome_tier_cold"
207    }
208
209    const preferredKey = welcomeKeyByBand[lower]
210
211    if (preferredKey) {
212      const preferred = templates.find((t) => {
213        const id = String(t.id || t.key || t.slug || "").toLowerCase()
214        return id === preferredKey
215      })
216
217      if (preferred) return preferred
218    }
219
220    // 2) Fallback: allow a template whose *id* contains the welcome key
221    if (preferredKey) {
222      const byIdHint = templates.find((t) => {
223        const id = String(t.id || t.key || t.slug || "").toLowerCase()
224        return id.includes(preferredKey)
225      })
226      if (byIdHint) return byIdHint
227    }
228
229    // 3) Last-resort fallback: something with matching band
230    const byBand = templates.find((t) => {
231      const b = String(t.band || "").toLowerCase()
232      return b === lower
233    })
234    if (byBand) return byBand
235
236    // 4) Final generic fallback (very unlikely to be needed)
237    return templates.find((t) => {
238      const id = String(t.id || t.key || t.slug || "").toLowerCase()
239      return id.includes(lower)
240    }) || null
241  }
242
243
244  // ---------------------------------------------------------------------------
245  // PUBLISH BANNER
246  // ---------------------------------------------------------------------------
247
248  showPublishBanner(message) {
249    if (!this.hasPublishNotificationTarget) return
250    if (message && this.hasNotificationSubtitleTarget) {
251      this.notificationSubtitleTarget.textContent = message
252    }
253    this.publishNotificationTarget.classList.add("show")
254  }
255
256  hidePublishNotification() {
257    if (!this.hasPublishNotificationTarget) return
258    this.publishNotificationTarget.classList.remove("show")
259  }
260
261  _markDirty(message) {
262    this.dirty = true
263    this.showPublishBanner(
264      message || "You have unsaved changes to your automation settings."
265    )
266  }
267
268  discardDraft() {
269    this.welcomeState = {}
270    this._loadTemplatesFromDOM()
271    this.dirty = false
272    this.senderDirty = false
273    this.hidePublishNotification()
274
275    if (this.selectedTierId) this._renderWelcomeEditor()
276  }
277
278  calendarFieldChanged() {
279    this.calendarDirty = true
280    this._markDirty(
281      "You have unsaved changes to your calendar link or which tiers it applies to."
282    )
283  }
284
285
286  senderFieldChanged() {
287    this.senderDirty = true
288    this._markDirty(
289      "You have unsaved changes to your sender configuration or welcome emails."
290    )
291  }
292
293  async handlePublishClick(event) {
294    event.preventDefault()
295
296    const senderForm   = document.getElementById("lead-automation-sender-form")
297    const calendarForm = document.getElementById("lead-automation-calendar-form")
298    const promises = []
299
300    // 1) Sender config
301    if (senderForm && this.senderDirty) {
302      promises.push(this._saveSenderConfig(senderForm))
303    }
304
305    // 2) Calendar config
306    if (calendarForm && this.calendarDirty) {
307      promises.push(this._saveCalendarConfig(calendarForm))
308    }
309
310    // 3) Tier email templates
311    const bands = Object.keys(this.welcomeState || {})
312
313    for (const band of bands) {
314      const state = this.welcomeState[band]
315      const tmpl = this._templateForBand(band)
316      if (!tmpl) continue
317
318      const originalSubject = tmpl.subject || ""
319      const originalBody    = tmpl.html_body || tmpl.text_body || ""
320
321      const currentSubject  = state.subject || ""
322      const currentBody     = state.body || ""
323
324      if (currentSubject !== originalSubject || currentBody !== originalBody) {
325        promises.push(this._saveBandWelcome(band))
326      }
327    }
328
329    if (promises.length === 0) {
330      this.showPublishBanner("No changes to publish.")
331      setTimeout(() => this.hidePublishNotification(), 2500)
332      return
333    }
334
335    try {
336      await Promise.all(promises)
337
338      this.senderDirty   = false
339      this.calendarDirty = false
340      this.dirty         = false
341
342      this.showPublishBanner("All sender, calendar & welcome email changes have been published.")
343      setTimeout(() => this.hidePublishNotification(), 3000)
344    } catch (err) {
345      console.error("[lead-automation-engine] publish failed:", err)
346      this.showPublishBanner("We couldn't publish all changes. Please try again.")
347    }
348  }
349
350
351  // ---------------------------------------------------------------------------
352  // TIER SELECTION
353  // ---------------------------------------------------------------------------
354
355  _selectDefaultTier() {
356    if (!this.hasTierListViewTarget) return
357    const hotPill = this.tierListViewTarget.querySelector('[data-tier-id="hot"]')
358    if (hotPill) {
359      this._selectTierByElement(hotPill)
360    }
361  }
362
363  selectTier(event) {
364    const pill = event.currentTarget
365    this._selectTierByElement(pill)
366  }
367
368  _selectTierByElement(pill) {
369    if (!pill) return
370
371    const tierId = pill.dataset.tierId
372    const tierName = pill.dataset.tierName || "Tier"
373
374    this.selectedTierId = tierId
375
376    if (this.hasTierDetailViewTarget) {
377      this.tierDetailViewTarget.classList.remove("hidden")
378    }
379
380    if (this.hasDetailTitleTarget) {
381      this.detailTitleTarget.textContent = tierName
382    }
383
384    if (this.hasTierListViewTarget) {
385      this.tierListViewTarget
386        .querySelectorAll(".tier-pill")
387        .forEach((el) => el.classList.remove("selected"))
388    }
389    pill.classList.add("selected")
390
391    this._renderWelcomeEditor()
392    this._renderActionLibrary()
393  }
394
395  backToList() {
396    if (this.hasTierDetailViewTarget) {
397      this.tierDetailViewTarget.classList.add("hidden")
398    }
399    if (this.hasTierListViewTarget) {
400      this.tierListViewTarget
401        .querySelectorAll(".tier-pill")
402        .forEach((el) => el.classList.remove("selected"))
403    }
404    this.selectedTierId = null
405  }
406
407  // ---------------------------------------------------------------------------
408  // WELCOME MESSAGE EDITOR (middle pane)
409  // ---------------------------------------------------------------------------
410
411  _currentWelcomeState() {
412    const tierId = this.selectedTierId
413    if (!tierId) return { subject: "", body: "" }
414
415    if (!this.welcomeState) this.welcomeState = {}
416    if (!this.welcomeState[tierId]) {
417      const tmpl = this._templateForBand(tierId)
418      this.welcomeState[tierId] = {
419        subject: tmpl?.subject || "",
420        body: tmpl?.html_body || tmpl?.text_body || ""
421      }
422    }
423    return this.welcomeState[tierId]
424  }
425
426  _renderWelcomeEditor() {
427    if (!this.hasTierDetailsPanelTarget || !this.selectedTierId) return
428
429    const tierId = this.selectedTierId
430    const tierName = this._humanTierName(tierId)
431    const welcome = this._currentWelcomeState()
432
433    this.tierDetailsPanelTarget.innerHTML = `
434      <div style="display:flex;flex-direction:column;gap:0.75rem;">
435        <p style="font-size:0.9rem;color:#4b5563;margin:0;">
436          <strong>${this._escape(tierName)} welcome email</strong> – sent immediately after a ${this._escape(
437      tierName.toLowerCase()
438    )} lead submits their contact details.
439        </p>
440
441        <label style="font-size:0.85rem;font-weight:600;color:#374151;margin-top:0.25rem;">
442          Subject line
443        </label>
444        <input type="text"
445               value="${this._escape(welcome.subject || "")}"
446               data-action="input->lead-automation-engine#updateWelcomeSubject"
447               style="border-radius:10px;border:1px solid #e5e7eb;padding:0.5rem 0.75rem;font-size:0.9rem;width:100%;">
448
449        <label style="font-size:0.85rem;font-weight:600;color:#374151;margin-top:0.5rem;">
450          Email body
451        </label>
452        <textarea
453          rows="10"
454          data-action="input->lead-automation-engine#updateWelcomeBody"
455          style="border-radius:10px;border:1px solid #e5e7eb;padding:0.75rem;font-size:0.9rem;width:100%;font-family:inherit;line-height:1.5;">${this._escape(
456            welcome.body || ""
457          )}</textarea>
458
459        <div style="font-size:0.8rem;color:#6b7280;margin-top:0.25rem;">
460          You can use personalization tags such as <code>{{lead_first_name}}</code> and <code>{{company_name}}</code>.
461        </div>
462
463        <div style="display:flex;justify-content:flex-end;margin-top:0.75rem;gap:0.5rem;">
464          <button type="button"
465                  class="btn-auto"
466                  data-action="click->lead-automation-engine#discardTierDraft">
467            <i class="bx bx-undo"></i>
468            Reset tier message
469          </button>
470          <button type="button"
471                  class="btn-auto"
472                  data-action="click->lead-automation-engine#saveCurrentWelcome">
473            <i class="bx bx-save"></i>
474            Save welcome message
475          </button>
476        </div>
477      </div>
478    `
479  }
480
481  updateWelcomeSubject(event) {
482    const state = this._currentWelcomeState()
483    state.subject = event.currentTarget.value
484    this._markDirty("You have unsaved changes to your welcome emails.")
485  }
486
487  updateWelcomeBody(event) {
488    const state = this._currentWelcomeState()
489    state.body = event.currentTarget.value
490    this._markDirty("You have unsaved changes to your welcome emails.")
491  }
492
493  discardTierDraft() {
494    if (!this.selectedTierId) return
495    if (!this.templates || this.templates.length === 0) return
496
497    const tierId = this.selectedTierId
498    const tmpl = this._templateForBand(tierId)
499    this.welcomeState[tierId] = {
500      subject: tmpl?.subject || "",
501      body: tmpl?.html_body || tmpl?.text_body || ""
502    }
503    this._renderWelcomeEditor()
504    this.dirty = false
505    this.showPublishBanner("Tier message reset to last saved template.")
506    setTimeout(() => this.hidePublishNotification(), 2500)
507  }
508
509  async saveCurrentWelcome() {
510    if (!this.selectedTierId) return
511    await this._saveBandWelcome(this.selectedTierId)
512  }
513
514  async _saveBandWelcome(band) {
515    if (!band) return false
516
517    const tmpl = this._templateForBand(band)
518    if (!tmpl) {
519      console.warn("[lead-automation-engine] No template found for band:", band)
520      this.showPublishBanner("We couldn't find a template for this tier.")
521      return false
522    }
523
524    const key = tmpl.key || tmpl.id || tmpl.slug
525    if (!key) {
526      console.warn("[lead-automation-engine] Template is missing a key/id:", tmpl)
527      this.showPublishBanner("Template key missing; cannot save.")
528      return false
529    }
530
531    const stateForBand =
532      (this.welcomeState && this.welcomeState[band]) ||
533      { subject: tmpl.subject || "", body: tmpl.html_body || tmpl.text_body || "" }
534
535    const payload = {
536      key: key,
537      locale: tmpl.locale || "en",
538      subject: stateForBand.subject || "",
539      html_body: stateForBand.body || "",
540      text_body: "",
541      band: tmpl.band || band
542    }
543
544    try {
545      const res = await fetch("/automation_actions/template", {
546        method: "POST",
547        headers: {
548          "Content-Type": "application/json",
549          "Accept": "application/json",
550          "X-CSRF-Token": this._csrfToken()
551        },
552        body: JSON.stringify(payload)
553      })
554
555      const data = await res.json().catch(() => ({}))
556
557      if (!res.ok || data.success === false) {
558        console.error("[lead-automation-engine] upsert_action_template failed:", data)
559        const msg = data.message || data.error || "We couldn't publish changes. Please try again."
560        this.showPublishBanner(msg)
561        return false
562      }
563
564      this.dirty = false
565      this.showPublishBanner("Welcome email saved and published.")
566      setTimeout(() => this.hidePublishNotification(), 3000)
567      return true
568    } catch (err) {
569      console.error("[lead-automation-engine] network error saving template:", err)
570      this.showPublishBanner("We couldn't publish changes. Please try again.")
571      return false
572    }
573  }
574
575  // ---------------------------------------------------------------------------
576  // ACTION LIBRARY (right-hand side – reuse templates list)
577  // ---------------------------------------------------------------------------
578
579  _renderActionLibrary() {
580    if (!this.hasEmailTemplatesTarget) return
581
582    const templates = this.templates || []
583    if (templates.length === 0) {
584      this.emailTemplatesTarget.innerHTML = `
585        <div style="padding:1rem;text-align:center;color:var(--auto-gray-400);font-size:0.875rem;">
586          No email templates available yet.
587        </div>
588      `
589      return
590    }
591
592    this.emailTemplatesTarget.innerHTML = templates
593      .map((tmpl) => {
594        const name = tmpl.name || tmpl.title || tmpl.id || "Untitled"
595        const desc = tmpl.description || tmpl.band || "Email template"
596        const templateId = tmpl.id || tmpl.key || tmpl.slug
597
598        return `
599          <div class="library-action-template" data-template-id="${this._escape(
600            templateId
601          )}">
602            <div class="library-action-icon">
603              <i class='bx bx-envelope'></i>
604            </div>
605            <div class="library-action-info">
606              <div class="library-action-name">${this._escape(name)}</div>
607              <div class="library-action-desc">${this._escape(desc)}</div>
608            </div>
609            <button class="library-action-add" type="button"
610                    data-action="click->lead-automation-engine#applyTemplateToCurrentTier">
611              +
612            </button>
613          </div>
614        `
615      })
616      .join("")
617  }
618
619  applyTemplateToCurrentTier(event) {
620    if (!this.selectedTierId) return
621    const card = event.currentTarget.closest("[data-template-id]")
622    if (!card) return
623    const templateId = card.dataset.templateId
624    const tmpl = (this.templates || []).find(
625      (t) => String(t.id || t.key || t.slug) === templateId
626    )
627    if (!tmpl) return
628
629    const state = this._currentWelcomeState()
630    state.subject = tmpl.subject || tmpl.title || state.subject
631    state.body = tmpl.html_body || tmpl.text_body || state.body
632
633    this._renderWelcomeEditor()
634    this._markDirty("You have unsaved changes to your welcome emails.")
635  }
636
637  // ---------------------------------------------------------------------------
638  // MODAL STUBS
639  // ---------------------------------------------------------------------------
640
641  closeActionModal() {
642    if (!this.hasActionModalTarget) return
643    this.actionModalTarget.classList.remove("show")
644  }
645
646  openActionModal() {
647    if (!this.hasActionModalTarget) return
648    this.actionModalTarget.classList.add("show")
649  }
650
651  // ---------------------------------------------------------------------------
652  // HELPERS
653  // ---------------------------------------------------------------------------
654
655  _humanTierName(id) {
656    switch (String(id)) {
657      case "hot":
658        return "Hot Leads"
659      case "warm":
660        return "Warm Leads"
661      case "lukewarm":
662        return "Lukewarm Leads"
663      case "cold":
664        return "Cold / Unqualified Leads"
665      default:
666        return "This tier"
667    }
668  }
669
670  _escape(str) {
671    if (str == null) return ""
672    return String(str)
673      .replace(/&/g, "&amp;")
674      .replace(/</g, "&lt;")
675      .replace(/>/g, "&gt;")
676      .replace(/"/g, "&quot;")
677      .replace(/'/g, "&#39;")
678  }
679
680  _csrfToken() {
681    if (this.hasCsrfValue && this.csrfValue) return this.csrfValue
682    const meta = document.querySelector("meta[name='csrf-token']")
683    return meta ? meta.content : ""
684  }
685
686  // ---------------------------------------------------------------------------
687  // SENDER CONFIG SAVE
688  // ---------------------------------------------------------------------------
689
690  _saveSenderConfig(form) {
691    const url = form.action
692    const method = (form.method || "post").toUpperCase()
693    const body = new FormData(form)
694
695    return fetch(url, {
696      method: method,
697      headers: {
698        "X-CSRF-Token": this._csrfToken(),
699        "Accept":
700          "text/vnd.turbo-stream.html, text/html, application/json;q=0.9,*/*;q=0.8"
701      },
702      body
703    }).then((res) => {
704      if (!res.ok) {
705        throw new Error(`Sender config save failed with HTTP ${res.status}`)
706      }
707      return res
708    })
709  }
710
711  _saveCalendarConfig(form) {
712    const url    = form.action
713    const method = (form.method || "post").toUpperCase()
714    const body   = new FormData(form)
715
716    return fetch(url, {
717      method: method,
718      headers: {
719        "X-CSRF-Token": this._csrfToken(),
720        "Accept":
721          "text/vnd.turbo-stream.html, text/html, application/json;q=0.9,*/*;q=0.8"
722      },
723      body
724    }).then((res) => {
725      if (!res.ok) {
726        throw new Error(`Calendar config save failed with HTTP ${res.status}`)
727      }
728      return res
729    })
730  }
731
732
733  // ---------------------------------------------------------------------------
734  // RESTORE DEFAULT EMAIL TEMPLATES
735  // ---------------------------------------------------------------------------
736
737  async restoreDefaults(event) {
738    event.preventDefault()
739
740    const url =
741      this.hasRestoreUrlValue && this.restoreUrlValue
742        ? this.restoreUrlValue
743        : "/lead_automation/restore_email_templates_defaults"
744
745    const confirmed = window.confirm(
746      "This will reset your lead email templates back to the default set for this company.\n\n" +
747        "It will NOT change your scoring bands or sender configuration.\n\nContinue?"
748    )
749
750    if (!confirmed) return
751
752    try {
753      const res = await fetch(url, {
754        method: "POST",
755        headers: {
756          "X-CSRF-Token": this._csrfToken(),
757          "Accept": "application/json",
758          "Content-Type": "application/json"
759        },
760        body: JSON.stringify({})
761      })
762
763      if (!res.ok) {
764        throw new Error(`Restore email templates failed with HTTP ${res.status}`)
765      }
766
767      const data = await res.json().catch(() => ({}))
768      const templates = Array.isArray(data.email_templates)
769        ? data.email_templates
770        : []
771
772      this.templates = this._normalizeTemplates(templates)
773
774      this.welcomeState = {}
775      this.dirty = false
776      this.senderDirty = false
777
778      const el = document.getElementById("automation-email-templates")
779      if (el) {
780        el.textContent = JSON.stringify(templates)
781      }
782
783      this._renderActionLibrary()
784      if (this.selectedTierId) {
785        this._renderWelcomeEditor()
786      }
787
788      const msg =
789        data.message ||
790        data.notice ||
791        "Email templates restored to defaults for this company."
792
793      this.showPublishBanner(msg)
794      setTimeout(() => this.hidePublishNotification(), 3000)
795    } catch (err) {
796      console.error("[lead-automation-engine] restoreDefaults error:", err)
797      this.showPublishBanner(
798        "We couldn't restore your email templates. Please try again."
799      )
800    }
801  }
802};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.