intelwatch 1.0.0
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.
- package/CHANGELOG.md +39 -0
- package/README.md +175 -0
- package/bin/intelwatch.js +8 -0
- package/package.json +43 -0
- package/src/ai/client.js +130 -0
- package/src/commands/ai-summary.js +147 -0
- package/src/commands/check.js +267 -0
- package/src/commands/compare.js +124 -0
- package/src/commands/diff.js +118 -0
- package/src/commands/digest.js +156 -0
- package/src/commands/discover.js +301 -0
- package/src/commands/history.js +60 -0
- package/src/commands/list.js +43 -0
- package/src/commands/notify.js +121 -0
- package/src/commands/pitch.js +156 -0
- package/src/commands/report.js +82 -0
- package/src/commands/track.js +94 -0
- package/src/config.js +65 -0
- package/src/index.js +182 -0
- package/src/report/html.js +499 -0
- package/src/report/json.js +44 -0
- package/src/report/markdown.js +156 -0
- package/src/scrapers/brave-search.js +268 -0
- package/src/scrapers/google-news.js +111 -0
- package/src/scrapers/google.js +113 -0
- package/src/scrapers/pappers.js +119 -0
- package/src/scrapers/site-analyzer.js +252 -0
- package/src/storage.js +168 -0
- package/src/trackers/brand.js +76 -0
- package/src/trackers/competitor.js +268 -0
- package/src/trackers/keyword.js +121 -0
- package/src/trackers/person.js +132 -0
- package/src/utils/display.js +102 -0
- package/src/utils/fetcher.js +82 -0
- package/src/utils/parser.js +110 -0
- package/src/utils/sentiment.js +95 -0
- package/src/utils/tech-detect.js +94 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
export function generateHtmlReport(data) {
|
|
2
|
+
const date = new Date(data.generatedAt).toLocaleString();
|
|
3
|
+
const totalChanges =
|
|
4
|
+
data.competitors.reduce((s, c) => s + c.changes.length, 0) +
|
|
5
|
+
data.keywords.reduce((s, k) => s + k.changes.length, 0) +
|
|
6
|
+
data.brands.reduce((s, b) => s + b.changes.length, 0);
|
|
7
|
+
|
|
8
|
+
const sortedCompetitors = [...data.competitors].sort((a, b) => b.threatScore - a.threatScore);
|
|
9
|
+
|
|
10
|
+
return `<!DOCTYPE html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="UTF-8">
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15
|
+
<title>IntelWatch Report — ${date}</title>
|
|
16
|
+
<style>
|
|
17
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
18
|
+
|
|
19
|
+
:root {
|
|
20
|
+
--bg: #0f1117;
|
|
21
|
+
--surface: #1a1d27;
|
|
22
|
+
--surface2: #22263a;
|
|
23
|
+
--border: #2d3148;
|
|
24
|
+
--text: #e2e8f0;
|
|
25
|
+
--text-muted: #8892a4;
|
|
26
|
+
--accent: #6366f1;
|
|
27
|
+
--accent2: #818cf8;
|
|
28
|
+
--green: #22c55e;
|
|
29
|
+
--yellow: #f59e0b;
|
|
30
|
+
--red: #ef4444;
|
|
31
|
+
--blue: #3b82f6;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
body {
|
|
35
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
36
|
+
background: var(--bg);
|
|
37
|
+
color: var(--text);
|
|
38
|
+
line-height: 1.6;
|
|
39
|
+
min-height: 100vh;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.header {
|
|
43
|
+
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e1b4b 100%);
|
|
44
|
+
border-bottom: 1px solid var(--border);
|
|
45
|
+
padding: 40px 48px;
|
|
46
|
+
position: relative;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.header::before {
|
|
51
|
+
content: '';
|
|
52
|
+
position: absolute;
|
|
53
|
+
top: -50%;
|
|
54
|
+
left: -50%;
|
|
55
|
+
width: 200%;
|
|
56
|
+
height: 200%;
|
|
57
|
+
background: radial-gradient(circle at 30% 50%, rgba(99,102,241,0.15) 0%, transparent 50%);
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.header-inner { position: relative; z-index: 1; max-width: 1200px; margin: 0 auto; }
|
|
62
|
+
.header h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.5px; }
|
|
63
|
+
.header h1 span { color: var(--accent2); }
|
|
64
|
+
.header-meta { color: var(--text-muted); margin-top: 8px; font-size: 0.9rem; }
|
|
65
|
+
.header-meta strong { color: var(--text); }
|
|
66
|
+
|
|
67
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 40px 48px; }
|
|
68
|
+
|
|
69
|
+
/* Stats cards */
|
|
70
|
+
.stats-grid {
|
|
71
|
+
display: grid;
|
|
72
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
73
|
+
gap: 16px;
|
|
74
|
+
margin-bottom: 40px;
|
|
75
|
+
}
|
|
76
|
+
.stat-card {
|
|
77
|
+
background: var(--surface);
|
|
78
|
+
border: 1px solid var(--border);
|
|
79
|
+
border-radius: 12px;
|
|
80
|
+
padding: 20px;
|
|
81
|
+
text-align: center;
|
|
82
|
+
}
|
|
83
|
+
.stat-card .value { font-size: 2.5rem; font-weight: 700; color: var(--accent2); line-height: 1; }
|
|
84
|
+
.stat-card .label { color: var(--text-muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; margin-top: 6px; }
|
|
85
|
+
|
|
86
|
+
/* Sections */
|
|
87
|
+
.section { margin-bottom: 48px; }
|
|
88
|
+
.section-title {
|
|
89
|
+
font-size: 1.3rem;
|
|
90
|
+
font-weight: 700;
|
|
91
|
+
margin-bottom: 20px;
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 10px;
|
|
95
|
+
color: var(--text);
|
|
96
|
+
padding-bottom: 12px;
|
|
97
|
+
border-bottom: 1px solid var(--border);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Cards */
|
|
101
|
+
.card {
|
|
102
|
+
background: var(--surface);
|
|
103
|
+
border: 1px solid var(--border);
|
|
104
|
+
border-radius: 12px;
|
|
105
|
+
margin-bottom: 20px;
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
}
|
|
108
|
+
.card-header {
|
|
109
|
+
padding: 16px 20px;
|
|
110
|
+
background: var(--surface2);
|
|
111
|
+
border-bottom: 1px solid var(--border);
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: space-between;
|
|
115
|
+
gap: 12px;
|
|
116
|
+
}
|
|
117
|
+
.card-header h3 { font-size: 1rem; font-weight: 600; }
|
|
118
|
+
.card-header a { color: var(--accent2); text-decoration: none; }
|
|
119
|
+
.card-header a:hover { text-decoration: underline; }
|
|
120
|
+
.card-body { padding: 20px; }
|
|
121
|
+
|
|
122
|
+
/* Tables */
|
|
123
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
|
124
|
+
th {
|
|
125
|
+
text-align: left;
|
|
126
|
+
padding: 10px 14px;
|
|
127
|
+
background: var(--surface2);
|
|
128
|
+
color: var(--text-muted);
|
|
129
|
+
font-size: 0.75rem;
|
|
130
|
+
text-transform: uppercase;
|
|
131
|
+
letter-spacing: 0.5px;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
border-bottom: 1px solid var(--border);
|
|
134
|
+
}
|
|
135
|
+
td { padding: 10px 14px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
136
|
+
tr:last-child td { border-bottom: none; }
|
|
137
|
+
tr:hover td { background: rgba(255,255,255,0.02); }
|
|
138
|
+
|
|
139
|
+
/* Badges */
|
|
140
|
+
.badge {
|
|
141
|
+
display: inline-flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: 4px;
|
|
144
|
+
padding: 3px 10px;
|
|
145
|
+
border-radius: 100px;
|
|
146
|
+
font-size: 0.75rem;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
}
|
|
149
|
+
.badge-high { background: rgba(239,68,68,0.15); color: #fca5a5; border: 1px solid rgba(239,68,68,0.3); }
|
|
150
|
+
.badge-med { background: rgba(245,158,11,0.15); color: #fcd34d; border: 1px solid rgba(245,158,11,0.3); }
|
|
151
|
+
.badge-low { background: rgba(34,197,94,0.15); color: #86efac; border: 1px solid rgba(34,197,94,0.3); }
|
|
152
|
+
.badge-new { background: rgba(34,197,94,0.1); color: var(--green); }
|
|
153
|
+
.badge-changed { background: rgba(245,158,11,0.1); color: var(--yellow); }
|
|
154
|
+
.badge-removed { background: rgba(239,68,68,0.1); color: var(--red); }
|
|
155
|
+
.badge-neutral { background: rgba(99,102,241,0.1); color: var(--accent2); }
|
|
156
|
+
|
|
157
|
+
/* Changes list */
|
|
158
|
+
.changes { list-style: none; }
|
|
159
|
+
.changes li {
|
|
160
|
+
padding: 8px 0;
|
|
161
|
+
border-bottom: 1px solid var(--border);
|
|
162
|
+
font-size: 0.875rem;
|
|
163
|
+
display: flex;
|
|
164
|
+
gap: 10px;
|
|
165
|
+
align-items: baseline;
|
|
166
|
+
}
|
|
167
|
+
.changes li:last-child { border-bottom: none; }
|
|
168
|
+
.change-field { color: var(--text-muted); min-width: 120px; font-size: 0.8rem; }
|
|
169
|
+
.change-value { color: var(--text); }
|
|
170
|
+
|
|
171
|
+
/* Tech pills */
|
|
172
|
+
.tech-pills { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
173
|
+
.tech-pill {
|
|
174
|
+
background: var(--surface2);
|
|
175
|
+
border: 1px solid var(--border);
|
|
176
|
+
border-radius: 6px;
|
|
177
|
+
padding: 3px 10px;
|
|
178
|
+
font-size: 0.75rem;
|
|
179
|
+
color: var(--text-muted);
|
|
180
|
+
}
|
|
181
|
+
.tech-pill:hover { border-color: var(--accent); color: var(--text); }
|
|
182
|
+
|
|
183
|
+
/* Social links */
|
|
184
|
+
.social-links { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
185
|
+
.social-link {
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: 4px;
|
|
189
|
+
padding: 4px 12px;
|
|
190
|
+
background: var(--surface2);
|
|
191
|
+
border: 1px solid var(--border);
|
|
192
|
+
border-radius: 6px;
|
|
193
|
+
font-size: 0.8rem;
|
|
194
|
+
color: var(--accent2);
|
|
195
|
+
text-decoration: none;
|
|
196
|
+
}
|
|
197
|
+
.social-link:hover { border-color: var(--accent); }
|
|
198
|
+
|
|
199
|
+
/* Mentions */
|
|
200
|
+
.mention {
|
|
201
|
+
padding: 12px;
|
|
202
|
+
border: 1px solid var(--border);
|
|
203
|
+
border-radius: 8px;
|
|
204
|
+
margin-bottom: 8px;
|
|
205
|
+
background: var(--surface2);
|
|
206
|
+
}
|
|
207
|
+
.mention-title { font-weight: 500; margin-bottom: 4px; }
|
|
208
|
+
.mention-title a { color: var(--accent2); text-decoration: none; }
|
|
209
|
+
.mention-title a:hover { text-decoration: underline; }
|
|
210
|
+
.mention-meta { font-size: 0.8rem; color: var(--text-muted); display: flex; gap: 12px; }
|
|
211
|
+
.mention.negative { border-left: 3px solid var(--red); }
|
|
212
|
+
.mention.positive { border-left: 3px solid var(--green); }
|
|
213
|
+
|
|
214
|
+
/* Rankings table */
|
|
215
|
+
.rank-num { font-weight: 700; color: var(--accent2); min-width: 30px; }
|
|
216
|
+
.rank-domain { font-weight: 500; }
|
|
217
|
+
.rank-title { color: var(--text-muted); font-size: 0.82rem; }
|
|
218
|
+
.featured { color: var(--yellow); font-size: 0.75rem; }
|
|
219
|
+
|
|
220
|
+
/* Empty state */
|
|
221
|
+
.empty { color: var(--text-muted); font-style: italic; padding: 20px 0; text-align: center; }
|
|
222
|
+
|
|
223
|
+
/* Grid 2 cols */
|
|
224
|
+
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
225
|
+
@media (max-width: 768px) { .grid-2 { grid-template-columns: 1fr; } }
|
|
226
|
+
|
|
227
|
+
.meta-item { margin-bottom: 8px; font-size: 0.875rem; }
|
|
228
|
+
.meta-label { color: var(--text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
229
|
+
.meta-value { margin-top: 2px; }
|
|
230
|
+
|
|
231
|
+
.footer {
|
|
232
|
+
text-align: center;
|
|
233
|
+
padding: 32px;
|
|
234
|
+
color: var(--text-muted);
|
|
235
|
+
font-size: 0.8rem;
|
|
236
|
+
border-top: 1px solid var(--border);
|
|
237
|
+
margin-top: 40px;
|
|
238
|
+
}
|
|
239
|
+
.footer a { color: var(--accent2); text-decoration: none; }
|
|
240
|
+
</style>
|
|
241
|
+
</head>
|
|
242
|
+
<body>
|
|
243
|
+
|
|
244
|
+
<div class="header">
|
|
245
|
+
<div class="header-inner">
|
|
246
|
+
<h1>🔍 <span>Intel</span>Watch</h1>
|
|
247
|
+
<div class="header-meta">
|
|
248
|
+
Intelligence Report · Generated <strong>${date}</strong>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div class="container">
|
|
254
|
+
|
|
255
|
+
<!-- Stats -->
|
|
256
|
+
<div class="stats-grid">
|
|
257
|
+
<div class="stat-card">
|
|
258
|
+
<div class="value">${data.competitors.length}</div>
|
|
259
|
+
<div class="label">Competitors</div>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="stat-card">
|
|
262
|
+
<div class="value">${data.keywords.length}</div>
|
|
263
|
+
<div class="label">Keywords</div>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="stat-card">
|
|
266
|
+
<div class="value">${data.brands.length}</div>
|
|
267
|
+
<div class="label">Brands</div>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="stat-card">
|
|
270
|
+
<div class="value">${totalChanges}</div>
|
|
271
|
+
<div class="label">Changes</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
${data.competitors.length > 0 ? `
|
|
276
|
+
<!-- Competitors Section -->
|
|
277
|
+
<div class="section">
|
|
278
|
+
<div class="section-title">🏢 Competitors</div>
|
|
279
|
+
|
|
280
|
+
<!-- Threat Matrix -->
|
|
281
|
+
<div class="card" style="margin-bottom: 32px;">
|
|
282
|
+
<div class="card-header"><h3>Threat Matrix</h3></div>
|
|
283
|
+
<table>
|
|
284
|
+
<thead><tr>
|
|
285
|
+
<th>Competitor</th>
|
|
286
|
+
<th>Threat Level</th>
|
|
287
|
+
<th>Pages</th>
|
|
288
|
+
<th>Tech</th>
|
|
289
|
+
<th>Jobs</th>
|
|
290
|
+
<th>Changes</th>
|
|
291
|
+
</tr></thead>
|
|
292
|
+
<tbody>
|
|
293
|
+
${sortedCompetitors.map(({ tracker, snapshot, changes, threatScore }) => {
|
|
294
|
+
const threat = threatScore >= 8 ? '<span class="badge badge-high">🔴 HIGH</span>'
|
|
295
|
+
: threatScore >= 4 ? '<span class="badge badge-med">🟡 MED</span>'
|
|
296
|
+
: '<span class="badge badge-low">🟢 LOW</span>';
|
|
297
|
+
return `<tr>
|
|
298
|
+
<td><a href="${tracker.url}" target="_blank" rel="noopener" style="color:var(--accent2)">${tracker.name || tracker.url}</a></td>
|
|
299
|
+
<td>${threat} <span style="color:var(--text-muted);font-size:0.8rem;">${threatScore}/10</span></td>
|
|
300
|
+
<td>${snapshot.pageCount || 0}</td>
|
|
301
|
+
<td>${(snapshot.techStack || []).length}</td>
|
|
302
|
+
<td>${snapshot.jobs?.estimatedOpenings ?? '?'}</td>
|
|
303
|
+
<td><strong style="color:${changes.length > 0 ? 'var(--yellow)' : 'var(--text-muted)'}">${changes.length}</strong></td>
|
|
304
|
+
</tr>`;
|
|
305
|
+
}).join('')}
|
|
306
|
+
</tbody>
|
|
307
|
+
</table>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
${data.competitors.map(({ tracker, snapshot, changes, threatScore }) => `
|
|
311
|
+
<div class="card">
|
|
312
|
+
<div class="card-header">
|
|
313
|
+
<h3><a href="${tracker.url}" target="_blank" rel="noopener">${tracker.name || tracker.url}</a></h3>
|
|
314
|
+
${threatScore >= 8 ? '<span class="badge badge-high">🔴 HIGH THREAT</span>'
|
|
315
|
+
: threatScore >= 4 ? '<span class="badge badge-med">🟡 MEDIUM THREAT</span>'
|
|
316
|
+
: '<span class="badge badge-low">🟢 LOW THREAT</span>'}
|
|
317
|
+
</div>
|
|
318
|
+
<div class="card-body">
|
|
319
|
+
<div class="grid-2">
|
|
320
|
+
<div>
|
|
321
|
+
<div class="meta-item">
|
|
322
|
+
<div class="meta-label">URL</div>
|
|
323
|
+
<div class="meta-value"><a href="${tracker.url}" target="_blank" style="color:var(--accent2)">${tracker.url}</a></div>
|
|
324
|
+
</div>
|
|
325
|
+
<div class="meta-item">
|
|
326
|
+
<div class="meta-label">Last checked</div>
|
|
327
|
+
<div class="meta-value">${new Date(snapshot.checkedAt).toLocaleString()}</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="meta-item">
|
|
330
|
+
<div class="meta-label">Pages found</div>
|
|
331
|
+
<div class="meta-value">${snapshot.pageCount || 0}</div>
|
|
332
|
+
</div>
|
|
333
|
+
${snapshot.jobs ? `
|
|
334
|
+
<div class="meta-item">
|
|
335
|
+
<div class="meta-label">Open positions</div>
|
|
336
|
+
<div class="meta-value">~${snapshot.jobs.estimatedOpenings} (<a href="${snapshot.jobs.url}" target="_blank" style="color:var(--accent2)">see jobs</a>)</div>
|
|
337
|
+
</div>` : ''}
|
|
338
|
+
${snapshot.pricing?.prices?.length > 0 ? `
|
|
339
|
+
<div class="meta-item">
|
|
340
|
+
<div class="meta-label">Pricing detected</div>
|
|
341
|
+
<div class="meta-value" style="color:var(--green)">${snapshot.pricing.prices.slice(0, 5).join(' · ')}</div>
|
|
342
|
+
</div>` : ''}
|
|
343
|
+
${snapshot.keyPages?.['/']?.title ? `
|
|
344
|
+
<div class="meta-item">
|
|
345
|
+
<div class="meta-label">Homepage title</div>
|
|
346
|
+
<div class="meta-value" style="color:var(--text-muted)">${escHtml(snapshot.keyPages['/'].title)}</div>
|
|
347
|
+
</div>` : ''}
|
|
348
|
+
</div>
|
|
349
|
+
<div>
|
|
350
|
+
${snapshot.techStack?.length > 0 ? `
|
|
351
|
+
<div class="meta-label" style="margin-bottom:8px;">Tech Stack</div>
|
|
352
|
+
<div class="tech-pills">
|
|
353
|
+
${snapshot.techStack.map(t => `<span class="tech-pill" title="${t.category}">${t.name}</span>`).join('')}
|
|
354
|
+
</div>` : '<div class="meta-label">No tech detected</div>'}
|
|
355
|
+
|
|
356
|
+
${Object.keys(snapshot.socialLinks || {}).length > 0 ? `
|
|
357
|
+
<div class="meta-label" style="margin-top:16px;margin-bottom:8px;">Social</div>
|
|
358
|
+
<div class="social-links">
|
|
359
|
+
${Object.entries(snapshot.socialLinks).map(([platform, url]) =>
|
|
360
|
+
`<a href="${url}" target="_blank" class="social-link">${platform}</a>`
|
|
361
|
+
).join('')}
|
|
362
|
+
</div>` : ''}
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
${changes.length > 0 ? `
|
|
367
|
+
<div style="margin-top:20px;">
|
|
368
|
+
<div class="meta-label" style="margin-bottom:8px;">Changes (${changes.length})</div>
|
|
369
|
+
<ul class="changes">
|
|
370
|
+
${changes.map(c => {
|
|
371
|
+
const badgeClass = c.type === 'new' ? 'badge-new' : c.type === 'removed' ? 'badge-removed' : 'badge-changed';
|
|
372
|
+
const icon = c.type === 'new' ? '+ NEW' : c.type === 'removed' ? '- REM' : '~ CHG';
|
|
373
|
+
return `<li>
|
|
374
|
+
<span class="badge ${badgeClass}">${icon}</span>
|
|
375
|
+
<span class="change-field">${escHtml(c.field)}</span>
|
|
376
|
+
<span class="change-value">${escHtml(c.value || '')}</span>
|
|
377
|
+
</li>`;
|
|
378
|
+
}).join('')}
|
|
379
|
+
</ul>
|
|
380
|
+
</div>` : '<div style="margin-top:16px;color:var(--text-muted);font-size:0.875rem;">✓ No changes detected</div>'}
|
|
381
|
+
</div>
|
|
382
|
+
</div>`).join('')}
|
|
383
|
+
</div>` : ''}
|
|
384
|
+
|
|
385
|
+
${data.keywords.length > 0 ? `
|
|
386
|
+
<!-- Keywords Section -->
|
|
387
|
+
<div class="section">
|
|
388
|
+
<div class="section-title">🔍 Keyword Rankings</div>
|
|
389
|
+
${data.keywords.map(({ tracker, snapshot, changes }) => `
|
|
390
|
+
<div class="card">
|
|
391
|
+
<div class="card-header">
|
|
392
|
+
<h3>"${escHtml(tracker.keyword)}"</h3>
|
|
393
|
+
<span style="color:var(--text-muted);font-size:0.8rem;">${snapshot.resultCount || 0} results · ${new Date(snapshot.checkedAt).toLocaleDateString()}</span>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="card-body">
|
|
396
|
+
${changes.length > 0 ? `
|
|
397
|
+
<div style="margin-bottom:16px;">
|
|
398
|
+
<div class="meta-label" style="margin-bottom:8px;">Changes</div>
|
|
399
|
+
<ul class="changes">
|
|
400
|
+
${changes.slice(0, 10).map(c => {
|
|
401
|
+
const badgeClass = c.type === 'new' ? 'badge-new' : c.type === 'removed' ? 'badge-removed' : 'badge-changed';
|
|
402
|
+
const icon = c.type === 'new' ? '+ NEW' : c.type === 'removed' ? '- REM' : '~ CHG';
|
|
403
|
+
return `<li><span class="badge ${badgeClass}">${icon}</span><span class="change-value">${escHtml(c.value || '')}</span></li>`;
|
|
404
|
+
}).join('')}
|
|
405
|
+
</ul>
|
|
406
|
+
</div>` : ''}
|
|
407
|
+
|
|
408
|
+
${snapshot.results?.length > 0 ? `
|
|
409
|
+
<table>
|
|
410
|
+
<thead><tr>
|
|
411
|
+
<th style="width:50px">#</th>
|
|
412
|
+
<th>Domain</th>
|
|
413
|
+
<th>Title</th>
|
|
414
|
+
</tr></thead>
|
|
415
|
+
<tbody>
|
|
416
|
+
${snapshot.results.slice(0, 10).map(r => `
|
|
417
|
+
<tr>
|
|
418
|
+
<td class="rank-num">${r.position}</td>
|
|
419
|
+
<td class="rank-domain">${escHtml(r.domain)}${r.isFeaturedSnippet ? ' <span class="featured">⭐ Featured</span>' : ''}</td>
|
|
420
|
+
<td class="rank-title">${escHtml((r.title || '').slice(0, 80))}</td>
|
|
421
|
+
</tr>`).join('')}
|
|
422
|
+
</tbody>
|
|
423
|
+
</table>` : '<div class="empty">No results data available</div>'}
|
|
424
|
+
</div>
|
|
425
|
+
</div>`).join('')}
|
|
426
|
+
</div>` : ''}
|
|
427
|
+
|
|
428
|
+
${data.brands.length > 0 ? `
|
|
429
|
+
<!-- Brands Section -->
|
|
430
|
+
<div class="section">
|
|
431
|
+
<div class="section-title">📣 Brand Mentions</div>
|
|
432
|
+
${data.brands.map(({ tracker, snapshot, changes }) => {
|
|
433
|
+
const mentions = snapshot.mentions || [];
|
|
434
|
+
const negative = mentions.filter(m => m.sentiment === 'negative' || m.sentiment === 'slightly_negative');
|
|
435
|
+
const positive = mentions.filter(m => m.sentiment === 'positive' || m.sentiment === 'slightly_positive');
|
|
436
|
+
|
|
437
|
+
return `
|
|
438
|
+
<div class="card">
|
|
439
|
+
<div class="card-header">
|
|
440
|
+
<h3>"${escHtml(tracker.brandName)}"</h3>
|
|
441
|
+
<div style="display:flex;gap:8px;">
|
|
442
|
+
<span class="badge badge-low">😊 ${positive.length} pos</span>
|
|
443
|
+
${negative.length > 0 ? `<span class="badge badge-high">😞 ${negative.length} neg</span>` : ''}
|
|
444
|
+
<span class="badge badge-neutral">${snapshot.mentionCount || 0} total</span>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
<div class="card-body">
|
|
448
|
+
${negative.length > 0 ? `
|
|
449
|
+
<div style="margin-bottom:20px;">
|
|
450
|
+
<div class="meta-label" style="margin-bottom:8px;color:var(--red);">⚠ Negative mentions</div>
|
|
451
|
+
${negative.slice(0, 3).map(m => `
|
|
452
|
+
<div class="mention negative">
|
|
453
|
+
<div class="mention-title"><a href="${m.url}" target="_blank">${escHtml((m.title || m.url).slice(0, 100))}</a></div>
|
|
454
|
+
<div class="mention-meta">
|
|
455
|
+
<span>${escHtml(m.domain)}</span>
|
|
456
|
+
<span>${m.category}</span>
|
|
457
|
+
</div>
|
|
458
|
+
</div>`).join('')}
|
|
459
|
+
</div>` : ''}
|
|
460
|
+
|
|
461
|
+
${mentions.length > 0 ? `
|
|
462
|
+
<div class="meta-label" style="margin-bottom:8px;">Recent mentions</div>
|
|
463
|
+
${mentions.slice(0, 8).map(m => {
|
|
464
|
+
const sentClass = m.sentiment?.includes('negative') ? 'negative' : m.sentiment?.includes('positive') ? 'positive' : '';
|
|
465
|
+
const sentEmoji = m.sentiment === 'positive' ? '😊' : m.sentiment === 'negative' ? '😞' : '😐';
|
|
466
|
+
return `
|
|
467
|
+
<div class="mention ${sentClass}">
|
|
468
|
+
<div class="mention-title">${sentEmoji} <a href="${m.url}" target="_blank">${escHtml((m.title || m.url).slice(0, 100))}</a></div>
|
|
469
|
+
<div class="mention-meta">
|
|
470
|
+
<span>${escHtml(m.domain)}</span>
|
|
471
|
+
<span>${m.category}</span>
|
|
472
|
+
<span>${m.source === 'google_news' ? '📰 News' : '🌐 Web'}</span>
|
|
473
|
+
</div>
|
|
474
|
+
</div>`;
|
|
475
|
+
}).join('')}` : '<div class="empty">No mentions found yet</div>'}
|
|
476
|
+
</div>
|
|
477
|
+
</div>`;
|
|
478
|
+
}).join('')}
|
|
479
|
+
</div>` : ''}
|
|
480
|
+
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
<div class="footer">
|
|
484
|
+
Generated by <a href="https://github.com/intelwatch/intelwatch">intelwatch</a> — competitive intelligence from the terminal
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
</body>
|
|
488
|
+
</html>`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function escHtml(str) {
|
|
492
|
+
if (!str) return '';
|
|
493
|
+
return String(str)
|
|
494
|
+
.replace(/&/g, '&')
|
|
495
|
+
.replace(/</g, '<')
|
|
496
|
+
.replace(/>/g, '>')
|
|
497
|
+
.replace(/"/g, '"')
|
|
498
|
+
.replace(/'/g, ''');
|
|
499
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function generateJsonReport(data) {
|
|
2
|
+
const report = {
|
|
3
|
+
generatedAt: data.generatedAt,
|
|
4
|
+
summary: {
|
|
5
|
+
competitors: data.competitors.length,
|
|
6
|
+
keywords: data.keywords.length,
|
|
7
|
+
brands: data.brands.length,
|
|
8
|
+
totalChanges:
|
|
9
|
+
data.competitors.reduce((s, c) => s + c.changes.length, 0) +
|
|
10
|
+
data.keywords.reduce((s, k) => s + k.changes.length, 0) +
|
|
11
|
+
data.brands.reduce((s, b) => s + b.changes.length, 0),
|
|
12
|
+
},
|
|
13
|
+
competitors: data.competitors.map(({ tracker, snapshot, changes, threatScore }) => ({
|
|
14
|
+
id: tracker.id,
|
|
15
|
+
name: tracker.name,
|
|
16
|
+
url: tracker.url,
|
|
17
|
+
threatScore,
|
|
18
|
+
checkedAt: snapshot.checkedAt,
|
|
19
|
+
pageCount: snapshot.pageCount,
|
|
20
|
+
techStack: snapshot.techStack,
|
|
21
|
+
socialLinks: snapshot.socialLinks,
|
|
22
|
+
pricing: snapshot.pricing,
|
|
23
|
+
jobs: snapshot.jobs,
|
|
24
|
+
changes,
|
|
25
|
+
})),
|
|
26
|
+
keywords: data.keywords.map(({ tracker, snapshot, changes }) => ({
|
|
27
|
+
id: tracker.id,
|
|
28
|
+
keyword: tracker.keyword,
|
|
29
|
+
checkedAt: snapshot.checkedAt,
|
|
30
|
+
topResults: (snapshot.results || []).slice(0, 10),
|
|
31
|
+
changes,
|
|
32
|
+
})),
|
|
33
|
+
brands: data.brands.map(({ tracker, snapshot, changes }) => ({
|
|
34
|
+
id: tracker.id,
|
|
35
|
+
brandName: tracker.brandName,
|
|
36
|
+
checkedAt: snapshot.checkedAt,
|
|
37
|
+
mentionCount: snapshot.mentionCount,
|
|
38
|
+
mentions: (snapshot.mentions || []).slice(0, 20),
|
|
39
|
+
changes,
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return JSON.stringify(report, null, 2);
|
|
44
|
+
}
|