PageSourceSearch

https://backlinkchecker.pro/assets/controllers/validation_report_c…cbd621546f1fbef7a57a32c0c0dd7309eee0518f.js

js backlinkchecker.pro collected 2026-10-02 01:09:04 UTC 11,047 bytes, 316 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = ["progress", "progressBar", "status", "qualityRating", "itemsList", "summary", "exports", "processingText"]
5  static values = { id: String, shortCode: String, sessionId: String }
6
7  connect() {
8    this.channel = this.application.consumer.subscriptions.create(
9      {
10        channel: 'ValidationReportChannel',
11        id: this.idValue,
12        session_id: this.sessionIdValue
13      },
14      {
15        connected: () => {
16          console.log("Connected to ValidationReportChannel")
17        },
18        disconnected: () => {
19          console.log("Disconnected from ValidationReportChannel")
20        },
21        received: (data) => {
22          this.handleReceived(data)
23        }
24      }
25    )
26  }
27
28  disconnect() {
29    if (this.channel) {
30      this.channel.unsubscribe()
31    }
32  }
33
34  handleReceived(data) {
35    console.log("ValidationReport received:", data.type, data)
36    switch (data.type) {
37      case 'status_update':
38        this.handleStatusUpdate(data)
39        break
40      case 'items_extracted':
41        this.handleItemsExtracted(data)
42        break
43      case 'item_update':
44        this.handleItemUpdate(data)
45        break
46      case 'completed':
47        this.handleCompleted(data)
48        break
49      case 'exports_ready':
50        this.handleExportsReady(data)
51        break
52    }
53  }
54
55  handleStatusUpdate(data) {
56    if (this.hasStatusTarget) {
57      this.statusTarget.textContent = this.humanizeStatus(data.status)
58      this.statusTarget.className = `badge bg-${this.statusBadgeClass(data.status)}`
59    }
60    this.updateProgress(data.summary)
61  }
62
63  handleItemsExtracted(data) {
64    console.log("handleItemsExtracted - hasItemsListTarget:", this.hasItemsListTarget, "items count:", data.items?.length)
65    if (!this.hasItemsListTarget) {
66      console.error("No itemsList target found!")
67      return
68    }
69
70    // Remove all data rows but keep the header (first li)
71    const rows = this.itemsListTarget.querySelectorAll('li.item-container[data-item-id]')
72    console.log("Removing", rows.length, "existing rows")
73    rows.forEach(row => row.remove())
74
75    data.items.forEach((item, index) => {
76      console.log("Appending item", index + 1, item.id)
77      this.appendItemRow(item, index + 1)
78    })
79  }
80
81  handleItemUpdate(data) {
82    console.log("handleItemUpdate - item:", data.item?.id, data.item?.status)
83    this.updateProgress(data.summary)
84    this.appendOrUpdateItem(data.item)
85  }
86
87  handleCompleted(data) {
88    if (this.hasStatusTarget) {
89      this.statusTarget.textContent = 'Completed'
90      this.statusTarget.className = 'badge fs-6 bg-success'
91    }
92    this.updateProgress(data.summary)
93    if (this.hasProgressTarget) {
94      this.progressTarget.classList.add('d-none')
95    }
96    // Hide processing text
97    if (this.hasProcessingTextTarget) {
98      this.processingTextTarget.classList.add('d-none')
99    }
100    // Show quality rating
101    if (data.summary.quality_rating_label) {
102      this.showQualityRating(data.summary)
103    }
104  }
105
106  showQualityRating(summary) {
107    // Find or create quality rating badge
108    if (this.hasQualityRatingTarget) {
109      this.qualityRatingTarget.innerHTML = `<i class="bi bi-star-fill me-1"></i>${summary.quality_rating_label}`
110      this.qualityRatingTarget.className = `badge fs-6 bg-${summary.quality_rating_class}`
111      this.qualityRatingTarget.classList.remove('d-none')
112    } else if (this.hasStatusTarget) {
113      // Create and insert after status badge
114      const qualityBadge = document.createElement('span')
115      qualityBadge.className = `badge fs-6 bg-${summary.quality_rating_class}`
116      qualityBadge.innerHTML = `<i class="bi bi-star-fill me-1"></i>${summary.quality_rating_label}`
117      qualityBadge.setAttribute('data-validation-report-target', 'qualityRating')
118      this.statusTarget.parentNode.insertBefore(qualityBadge, this.statusTarget.nextSibling)
119    }
120  }
121
122  handleExportsReady(data) {
123    if (this.hasExportsTarget) {
124      this.exportsTarget.innerHTML = `
125        <a href="${data.csv_url}" class="btn btn-outline-primary">
126          <i class="bi bi-file-earmark-csv me-1"></i> Download CSV
127        </a>
128        <a href="${data.pdf_url}" class="btn btn-primary">
129          <i class="bi bi-file-earmark-pdf me-1"></i> Download PDF
130        </a>
131        <button class="btn btn-outline-secondary" type="button" data-action="click->
131validation-report#copyLink">
132          <i class="bi bi-link-45deg me-1"></i> Copy Link
133        </button>
134      `
135      this.exportsTarget.classList.remove('d-none')
136    }
137  }
138
139  copyLink(event) {
140    const url = `${window.location.origin}/validate/${this.shortCodeValue}`
141    const button = event.currentTarget
142
143    this.copyToClipboard(url).then(() => {
144      // Update button to show success
145      const originalHTML = button.innerHTML
146      button.innerHTML = '<i class="bi bi-check me-1"></i> Copied!'
147      button.classList.remove('btn-outline-secondary')
148      button.classList.add('btn-success')
149
150      setTimeout(() => {
151        button.innerHTML = originalHTML
152        button.classList.remove('btn-success')
153        button.classList.add('btn-outline-secondary')
154      }, 2000)
155    })
156  }
157
158  async copyToClipboard(text) {
159    // Try modern clipboard API first
160    if (navigator.clipboard && navigator.clipboard.writeText) {
161      try {
162        await navigator.clipboard.writeText(text)
163        return
164      } catch (err) {
165        // Fall through to fallback
166      }
167    }
168
169    // Fallback for older browsers or non-HTTPS
170    const textArea = document.createElement('textarea')
171    textArea.value = text
172    textArea.style.position = 'fixed'
173    textArea.style.left = '-9999px'
174    document.body.appendChild(textArea)
175    textArea.select()
176    try {
177      document.execCommand('copy')
178    } catch (err) {
179      prompt('Copy this link:', text)
180    }
181    document.body.removeChild(textArea)
182  }
183
184  updateProgress(summary) {
185    if (this.hasProgressBarTarget && summary.total > 0) {
186      const percentage = summary.progress
187      this.progressBarTarget.style.width = `${percentage}%`
188    }
189
190    if (this.hasSummaryTarget) {
191      this.summaryTarget.innerHTML = `
192        <div class="col-md-3 col-6 mb-3 mb-md-0">
193          <div class="fs-3 fw-bold text-slate-700">${summary.completed}/${summary.total}</div>
194          <div class="small text-muted">Checked</div>
195        </div>
196        <div class="col-md-3 col-6 mb-3 mb-md-0">
197          <div class="fs-3 fw-bold text-success">${summary.ok}</div>
198          <div class="small text-muted">OK (${summary.ok_pct}%)</div>
199        </div>
200        <div class="col-md-3 col-6">
201          <div class="fs-3 fw-bold text-warning">${summary.warnings}</div>
202          <div class="small text-muted">Warnings (${summary.warnings_pct}%)</div>
203        </div>
204        <div class="col-md-3 col-6">
205          <div class="fs-3 fw-bold text-danger">${summary.errors}</div>
206          <div class="small text-muted">Errors (${summary.errors_pct}%)</div>
207        </div>
208      `
209    }
210  }
211
212  appendOrUpdateItem(item) {
213    console.log("appendOrUpdateItem - hasItemsListTarget:", this.hasItemsListTarget, "item:", item?.id)
214    if (!this.hasItemsListTarget) {
215      console.error("No itemsListTarget!")
216      return
217    }
218
219    const existingRow = this.itemsListTarget.querySelector(`[data-item-id="${item.id}"]`)
220    console.log("existingRow found:", !!existingRow)
221
222    if (existingRow) {
223      const rowNumber = existingRow.querySelector('.attribute').textContent
224      existingRow.innerHTML = this.buildRowHtml(item, rowNumber)
225    } else {
226      // Count items excluding the header row (first li)
227      const rowCount = this.itemsListTarget.querySelectorAll('li.item-container').length - 1
228      console.log("Appending new row, current count:", rowCount)
229      this.appendItemRow(item, rowCount + 1)
230    }
231  }
232
233  appendItemRow(item, rowNumber) {
234    const row = document.createElement('li')
235    row.className = 'item item-container'
236    row.setAttribute('data-item-id', item.id)
237    row.innerHTML = this.buildRowHtml(item, rowNumber)
238    this.itemsListTarget.appendChild(row)
239  }
240
241  buildRowHtml(item, rowNumber) {
242    const statusClass = this.itemStatusClass(item.status)
243    const textClass = item.status === 'WARNING' ? 'text-dark' : ''
244    const statusLabel = item.status === 'pending' ? 'Pending' : item.status
245
246    return `
247      <div class="attribute" data-name="#">${rowNumber}</div>
248      <div class="attribute" data-name="Page URL" data-controller="clipboard">
249        <div class="url-display">
250          <a class="serp-icon" href="https://www.google.com/search?q=site:${encodeURIComponent(item.page_url)}" target="_blank" rel="noopener nofollow" title="Check in Google">
251            <i class="bi bi-search"></i>
252          </a>
253          <span class="copy-icon" role="button" data-action="click->clipboard#copy" data-clipboard-text="${this.escapeHtml(item.page_url)}" title="Copy URL">
254            <i class="bi bi-clipboard"></i>
255          </span>
256          <a class="url-link" href="${item.page_url}" target="_blank" rel="noopener nofollow" title="${this.escapeHtml(item.page_url)}">
257            <span class="url-text">${this.escapeHtml(item.page_url)}</span>
258            <i class="bi bi-box-arrow-up-right ms-1 small"></i>
259          </a>
260        </div>
261      </div>
262      <div class="attribute" data-name="Target URL">
263        <div class="url-display">
264          <a class="url-link" href="${item.target_url}" target="_blank" rel="noopener nofollow" title="${this.escapeHtml(item.target_url)}">
265            <span class="url-text">${this.escapeHtml(item.target_url)}</span>
266            <i class="bi bi-box-arrow-up-right ms-1 small"></i>
267          </a>
268        </div>
269      </div>
270      <div class="attribute" data-name="Anchor Text">
271        <span class="text-muted small">${item.anchor_text ? this.truncateText(this.escapeHtml(item.anchor_text), 50) : '—'}</span>
272      </div>
273      <div class="attribute" data-name="Status">
274        <span class="badge bg-${statusClass} ${textClass}">${statusLabel}</span>
275      </div>
276      <div class="attribute" data-name="Issues">${item.issues ? item.issues.join(', ') : ''}</div>
277    `
278  }
279
280  escapeHtml(text) {
281    if (!text) return ''
282    const div = document.createElement('div')
283    div.textContent = text
284    return div.innerHTML
285  }
286
287  truncateText(text, length) {
288    if (!text) return ''
289    return text.length > length ? text.substring(0, length - 3) + '...' : text
290  }
291
292  itemStatusClass(status) {
293    const classes = {
294      'OK': 'success',
295      'WARNING': 'warning',
296      'ERROR': 'danger',
297      'pending': 'secondary'
298    }
299    return classes[status] || 'secondary'
300  }
301
302  statusBadgeClass(status) {
303    const classes = {
304      'pending': 'secondary',
305      'extracting': 'info',
306      'validating': 'primary',
307      'completed': 'success',
308      'failed': 'danger'
309    }
310    return classes[status] || 'secondary'
311  }
312
313  humanizeStatus(status) {
314    return status.charAt(0).toUpperCase() + status.slice(1)
315  }
316};

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.