seo-intel 1.1.5 → 1.1.6
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/cli.js +41 -0
- package/package.json +1 -1
- package/reports/generate-html.js +101 -66
- package/server.js +17 -3
package/cli.js
CHANGED
|
@@ -91,6 +91,38 @@ async function checkOllamaAvailability() {
|
|
|
91
91
|
// ── EXTRACTION PROGRESS TRACKER ──────────────────────────────────────────
|
|
92
92
|
const PROGRESS_FILE = join(__dirname, '.extraction-progress.json');
|
|
93
93
|
|
|
94
|
+
// ── Graceful shutdown support ──
|
|
95
|
+
// Cleanup callbacks registered by crawl/extract commands (e.g. close browser)
|
|
96
|
+
const _shutdownCallbacks = [];
|
|
97
|
+
let _shuttingDown = false;
|
|
98
|
+
|
|
99
|
+
function onShutdown(fn) { _shutdownCallbacks.push(fn); }
|
|
100
|
+
function clearShutdownCallbacks() { _shutdownCallbacks.length = 0; }
|
|
101
|
+
|
|
102
|
+
async function _gracefulExit(signal) {
|
|
103
|
+
if (_shuttingDown) return;
|
|
104
|
+
_shuttingDown = true;
|
|
105
|
+
console.log(chalk.yellow(`\n⏹ Received ${signal} — stopping gracefully…`));
|
|
106
|
+
|
|
107
|
+
// Update progress file
|
|
108
|
+
try {
|
|
109
|
+
const progress = readProgress();
|
|
110
|
+
if (progress && progress.status === 'running' && progress.pid === process.pid) {
|
|
111
|
+
writeProgress({ ...progress, status: 'stopped', stopped_at: Date.now() });
|
|
112
|
+
}
|
|
113
|
+
} catch { /* best-effort */ }
|
|
114
|
+
|
|
115
|
+
// Run cleanup callbacks (close browsers, etc.)
|
|
116
|
+
for (const fn of _shutdownCallbacks) {
|
|
117
|
+
try { await Promise.resolve(fn()); } catch { /* best-effort */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
process.on('SIGTERM', () => _gracefulExit('SIGTERM'));
|
|
124
|
+
process.on('SIGINT', () => _gracefulExit('SIGINT'));
|
|
125
|
+
|
|
94
126
|
function writeProgress(data) {
|
|
95
127
|
try {
|
|
96
128
|
writeFileSync(PROGRESS_FILE, JSON.stringify({
|
|
@@ -481,6 +513,7 @@ program
|
|
|
481
513
|
page_index: totalExtracted + 1,
|
|
482
514
|
started_at: crawlStart,
|
|
483
515
|
failed: totalFailed,
|
|
516
|
+
stealth: !!crawlOpts.stealth,
|
|
484
517
|
});
|
|
485
518
|
upsertTechnical(db, { pageId, hasCanonical: page.hasCanonical, hasOgTags: page.hasOgTags, hasSchema: page.hasSchema, hasRobots: page.hasRobots });
|
|
486
519
|
try {
|
|
@@ -1559,6 +1592,14 @@ program
|
|
|
1559
1592
|
console.log(chalk.magenta(' 🥷 Advanced mode — full browser rendering, persistent sessions\n'));
|
|
1560
1593
|
}
|
|
1561
1594
|
|
|
1595
|
+
// Register cleanup so SIGTERM closes the browser gracefully
|
|
1596
|
+
onShutdown(async () => {
|
|
1597
|
+
if (stealthSession) {
|
|
1598
|
+
await stealthSession.close();
|
|
1599
|
+
console.log(chalk.magenta(' 🥷 Stealth session closed'));
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1562
1603
|
try {
|
|
1563
1604
|
for (const row of pendingPages) {
|
|
1564
1605
|
process.stdout.write(chalk.gray(` [${done + failed + 1}/${pendingPages.length}] ${row.url.slice(0, 65)} → `));
|
package/package.json
CHANGED
package/reports/generate-html.js
CHANGED
|
@@ -418,17 +418,21 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
418
418
|
border-radius: var(--radius);
|
|
419
419
|
padding: 14px 20px;
|
|
420
420
|
display: flex;
|
|
421
|
-
|
|
422
|
-
gap:
|
|
421
|
+
flex-direction: column;
|
|
422
|
+
gap: 12px;
|
|
423
423
|
font-size: 0.78rem;
|
|
424
424
|
}
|
|
425
425
|
.extraction-status.is-running {
|
|
426
426
|
border-color: rgba(232,213,163,0.3);
|
|
427
427
|
}
|
|
428
|
+
.es-top-row {
|
|
429
|
+
display: flex; align-items: center; gap: 16px; width: 100%;
|
|
430
|
+
}
|
|
428
431
|
.es-indicator {
|
|
429
432
|
display: flex; align-items: center; gap: 8px;
|
|
430
433
|
font-family: var(--font-display); font-weight: 700;
|
|
431
434
|
font-size: 0.8rem; white-space: nowrap;
|
|
435
|
+
flex-shrink: 0;
|
|
432
436
|
}
|
|
433
437
|
.es-dot {
|
|
434
438
|
width: 8px; height: 8px; border-radius: 50%;
|
|
@@ -444,21 +448,27 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
444
448
|
50% { opacity: 0.7; box-shadow: 0 0 0 6px rgba(232,213,163,0); }
|
|
445
449
|
}
|
|
446
450
|
.es-domains {
|
|
447
|
-
display:
|
|
451
|
+
display: grid;
|
|
452
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
453
|
+
gap: 6px 20px;
|
|
454
|
+
flex: 1;
|
|
448
455
|
}
|
|
449
456
|
.es-domain {
|
|
450
457
|
display: flex; align-items: center; gap: 6px;
|
|
451
458
|
}
|
|
452
459
|
.es-domain-name {
|
|
453
460
|
color: var(--text-secondary); font-size: 0.72rem;
|
|
461
|
+
white-space: nowrap;
|
|
462
|
+
width: 68px; min-width: 68px; flex-shrink: 0;
|
|
463
|
+
overflow: hidden; text-overflow: ellipsis;
|
|
454
464
|
}
|
|
455
465
|
.es-domain-name.is-target { color: var(--accent-gold); }
|
|
456
466
|
.es-bar-wrap {
|
|
457
|
-
|
|
458
|
-
border-radius:
|
|
467
|
+
flex: 1; height: 5px; background: var(--border-subtle);
|
|
468
|
+
border-radius: 2.5px; overflow: hidden;
|
|
459
469
|
}
|
|
460
470
|
.es-bar-fill {
|
|
461
|
-
height: 100%; border-radius:
|
|
471
|
+
height: 100%; border-radius: 2.5px;
|
|
462
472
|
background: var(--color-success);
|
|
463
473
|
transition: width 0.3s;
|
|
464
474
|
}
|
|
@@ -466,7 +476,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
466
476
|
.es-bar-fill.low { background: var(--color-danger); }
|
|
467
477
|
.es-pct {
|
|
468
478
|
font-size: 0.68rem; color: var(--text-muted);
|
|
469
|
-
min-width:
|
|
479
|
+
width: 32px; min-width: 32px; text-align: right; flex-shrink: 0;
|
|
470
480
|
}
|
|
471
481
|
.es-live {
|
|
472
482
|
font-size: 0.7rem; color: var(--accent-gold);
|
|
@@ -479,9 +489,14 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
479
489
|
background: var(--color-danger);
|
|
480
490
|
animation: pulse-dot 2s ease-in-out infinite;
|
|
481
491
|
}
|
|
492
|
+
.es-bottom-row {
|
|
493
|
+
display: flex; align-items: center; gap: 12px; width: 100%;
|
|
494
|
+
padding-top: 10px;
|
|
495
|
+
border-top: 1px solid var(--border-subtle);
|
|
496
|
+
}
|
|
482
497
|
.es-meta {
|
|
483
498
|
display: flex; gap: 12px; align-items: center;
|
|
484
|
-
|
|
499
|
+
white-space: nowrap; font-size: 0.7rem;
|
|
485
500
|
}
|
|
486
501
|
.es-meta-item { color: var(--text-muted); }
|
|
487
502
|
.es-meta-item i { margin-right: 3px; font-size: 0.62rem; }
|
|
@@ -493,8 +508,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
493
508
|
/* ─── Extraction Controls (server mode) ──────────────────────────────── */
|
|
494
509
|
.es-controls {
|
|
495
510
|
display: flex; align-items: center; gap: 10px;
|
|
496
|
-
margin-left:
|
|
497
|
-
border-left: 1px solid var(--border-subtle);
|
|
511
|
+
margin-left: auto;
|
|
498
512
|
}
|
|
499
513
|
.es-controls.hidden { display: none; }
|
|
500
514
|
.es-btn {
|
|
@@ -1866,69 +1880,85 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
1866
1880
|
extractionStatus.liveProgress?.status === 'running' ? 'is-running' :
|
|
1867
1881
|
extractionStatus.liveProgress?.status === 'crashed' ? 'is-crashed' : ''
|
|
1868
1882
|
}">
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
? `<span style="color:var(--
|
|
1878
|
-
:
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
<div class="es-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
<span class="es-meta-item" style="color:var(--accent-gold);">
|
|
1896
|
-
<i class="fa-solid fa-spinner fa-spin"></i>
|
|
1897
|
-
${extractionStatus.liveProgress.current_url ? extractionStatus.liveProgress.current_url.replace(/https?:\/\/[^/]+/, '').slice(0, 30) : ''}
|
|
1898
|
-
${extractionStatus.liveProgress.total ? ` · ${extractionStatus.liveProgress.page_index}/${extractionStatus.liveProgress.total}` : ''}
|
|
1899
|
-
</span>
|
|
1900
|
-
` : ''}
|
|
1901
|
-
${extractionStatus.liveProgress?.status === 'crashed' ? `
|
|
1902
|
-
<span class="es-meta-item blocked">
|
|
1903
|
-
<i class="fa-solid fa-skull"></i> PID ${extractionStatus.liveProgress.pid} dead
|
|
1904
|
-
</span>
|
|
1905
|
-
` : ''}
|
|
1906
|
-
${extractionStatus.liveProgress?.skipped > 0 ? `
|
|
1907
|
-
<span class="es-meta-item skipped">
|
|
1908
|
-
<i class="fa-solid fa-forward"></i> ${extractionStatus.liveProgress.skipped} skipped
|
|
1909
|
-
</span>
|
|
1910
|
-
` : ''}
|
|
1911
|
-
${extractionStatus.hashedPages > 0 ? `
|
|
1912
|
-
<span class="es-meta-item">
|
|
1913
|
-
<i class="fa-solid fa-fingerprint"></i> ${extractionStatus.hashedPages} hashed
|
|
1914
|
-
</span>
|
|
1915
|
-
` : ''}
|
|
1883
|
+
<!-- Row 1: Status indicator + domain coverage bars (full width) -->
|
|
1884
|
+
<div class="es-top-row">
|
|
1885
|
+
<div class="es-indicator">
|
|
1886
|
+
<span class="es-dot ${
|
|
1887
|
+
extractionStatus.liveProgress?.status === 'running' ? 'running' :
|
|
1888
|
+
extractionStatus.liveProgress?.status === 'crashed' ? 'crashed' : ''
|
|
1889
|
+
}"></span>
|
|
1890
|
+
${extractionStatus.liveProgress?.status === 'running'
|
|
1891
|
+
? `<span style="color:var(--accent-gold);">Extracting</span>`
|
|
1892
|
+
: extractionStatus.liveProgress?.status === 'crashed'
|
|
1893
|
+
? `<span style="color:var(--color-danger);">Crashed</span>`
|
|
1894
|
+
: `<span style="color:var(--text-muted);">${extractionStatus.overallPct === 100 ? 'Fully Extracted' : extractionStatus.overallPct + '% Extracted'}</span>`
|
|
1895
|
+
}
|
|
1896
|
+
</div>
|
|
1897
|
+
<div class="es-domains">
|
|
1898
|
+
${extractionStatus.coverage.map(c => {
|
|
1899
|
+
const pct = c.total_pages > 0 ? Math.round((c.extracted_pages / c.total_pages) * 100) : 0;
|
|
1900
|
+
const barClass = pct === 100 ? '' : pct > 50 ? 'partial' : 'low';
|
|
1901
|
+
return `
|
|
1902
|
+
<div class="es-domain">
|
|
1903
|
+
<span class="es-domain-name ${c.role === 'target' ? 'is-target' : ''}">${getDomainShortName(c.domain)}</span>
|
|
1904
|
+
<div class="es-bar-wrap"><div class="es-bar-fill ${barClass}" style="width:${pct}%;"></div></div>
|
|
1905
|
+
<span class="es-pct">${pct}%</span>
|
|
1906
|
+
</div>`;
|
|
1907
|
+
}).join('')}
|
|
1908
|
+
</div>
|
|
1916
1909
|
</div>
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1910
|
+
<!-- Row 2: Meta info + controls -->
|
|
1911
|
+
<div class="es-bottom-row">
|
|
1912
|
+
<div class="es-meta">
|
|
1913
|
+
${extractionStatus.liveProgress?.status === 'running' ? `
|
|
1914
|
+
<span class="es-meta-item" style="color:var(--accent-gold);">
|
|
1915
|
+
<i class="fa-solid fa-spinner fa-spin"></i>
|
|
1916
|
+
${extractionStatus.liveProgress.current_url ? extractionStatus.liveProgress.current_url.replace(/https?:\/\/[^/]+/, '').slice(0, 30) : ''}
|
|
1917
|
+
${extractionStatus.liveProgress.total ? ` · ${extractionStatus.liveProgress.page_index}/${extractionStatus.liveProgress.total}` : ''}
|
|
1918
|
+
</span>
|
|
1919
|
+
` : ''}
|
|
1920
|
+
${extractionStatus.liveProgress?.status === 'crashed' ? `
|
|
1921
|
+
<span class="es-meta-item blocked">
|
|
1922
|
+
<i class="fa-solid fa-skull"></i> PID ${extractionStatus.liveProgress.pid} dead
|
|
1923
|
+
</span>
|
|
1924
|
+
` : ''}
|
|
1925
|
+
${extractionStatus.liveProgress?.skipped > 0 ? `
|
|
1926
|
+
<span class="es-meta-item skipped">
|
|
1927
|
+
<i class="fa-solid fa-forward"></i> ${extractionStatus.liveProgress.skipped} skipped
|
|
1928
|
+
</span>
|
|
1929
|
+
` : ''}
|
|
1930
|
+
${extractionStatus.hashedPages > 0 ? `
|
|
1931
|
+
<span class="es-meta-item">
|
|
1932
|
+
<i class="fa-solid fa-fingerprint"></i> ${extractionStatus.hashedPages} hashed
|
|
1933
|
+
</span>
|
|
1934
|
+
` : ''}
|
|
1935
|
+
</div>
|
|
1936
|
+
<div class="es-controls" id="esControls${suffix}">
|
|
1937
|
+
${extractionStatus.liveProgress?.status === 'running' && extractionStatus.liveProgress?.command === 'crawl'
|
|
1938
|
+
? `<button class="es-btn running" id="btnCrawl${suffix}" onclick="startJob('crawl','${project}')" disabled>
|
|
1939
|
+
<i class="fa-solid fa-spinner fa-spin"></i> Crawling\u2026
|
|
1940
|
+
</button>`
|
|
1941
|
+
: `<button class="es-btn" id="btnCrawl${suffix}" onclick="startJob('crawl','${project}')"${extractionStatus.liveProgress?.status === 'running' ? ' disabled' : ''}>
|
|
1942
|
+
<i class="fa-solid fa-spider"></i> Crawl
|
|
1943
|
+
</button>`
|
|
1944
|
+
}
|
|
1945
|
+
${extractionStatus.liveProgress?.status === 'running' && extractionStatus.liveProgress?.command === 'extract'
|
|
1946
|
+
? `<button class="es-btn running" id="btnExtract${suffix}" onclick="startJob('extract','${project}')" disabled>
|
|
1947
|
+
<i class="fa-solid fa-spinner fa-spin"></i> Extracting\u2026
|
|
1948
|
+
</button>`
|
|
1949
|
+
: `<button class="es-btn" id="btnExtract${suffix}" onclick="startJob('extract','${project}')"${extractionStatus.liveProgress?.status === 'running' ? ' disabled' : ''}>
|
|
1950
|
+
<i class="fa-solid fa-brain"></i> Extract
|
|
1951
|
+
</button>`
|
|
1952
|
+
}
|
|
1953
|
+
<button class="es-btn es-btn-stop" id="btnStop${suffix}" onclick="stopJob()" style="display:${extractionStatus.liveProgress?.status === 'running' ? 'inline-flex' : 'none'};">
|
|
1925
1954
|
<i class="fa-solid fa-stop"></i> Stop
|
|
1926
1955
|
</button>
|
|
1927
1956
|
<label class="es-stealth-toggle">
|
|
1928
|
-
<input type="checkbox" id="stealthToggle${suffix}">
|
|
1957
|
+
<input type="checkbox" id="stealthToggle${suffix}"${extractionStatus.liveProgress?.stealth ? ' checked' : ''}>
|
|
1929
1958
|
<i class="fa-solid fa-user-ninja"></i> Stealth
|
|
1930
1959
|
</label>
|
|
1931
1960
|
</div>
|
|
1961
|
+
</div>
|
|
1932
1962
|
</div>
|
|
1933
1963
|
|
|
1934
1964
|
<!-- ═══ INTEGRATED TERMINAL + EXPORT SIDEBAR ═══ -->
|
|
@@ -3646,6 +3676,11 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3646
3676
|
fetch('/api/progress')
|
|
3647
3677
|
.then(function(r) { return r.json(); })
|
|
3648
3678
|
.then(function(data) {
|
|
3679
|
+
// Sync stealth toggle with progress state
|
|
3680
|
+
var stealthEl = document.getElementById('stealthToggle' + sfx);
|
|
3681
|
+
if (stealthEl && data.stealth !== undefined) {
|
|
3682
|
+
stealthEl.checked = !!data.stealth;
|
|
3683
|
+
}
|
|
3649
3684
|
if (data.status === 'running') {
|
|
3650
3685
|
setButtonsState(true, data.command);
|
|
3651
3686
|
startPolling();
|
package/server.js
CHANGED
|
@@ -345,16 +345,18 @@ async function handleRequest(req, res) {
|
|
|
345
345
|
return;
|
|
346
346
|
}
|
|
347
347
|
try {
|
|
348
|
+
// Graceful: SIGTERM lets the CLI close browsers / write progress
|
|
348
349
|
process.kill(progress.pid, 'SIGTERM');
|
|
349
|
-
//
|
|
350
|
+
// Escalate: SIGKILL after 5s if still alive (stealth browser cleanup needs time)
|
|
350
351
|
setTimeout(() => {
|
|
352
|
+
try { process.kill(progress.pid, 0); } catch { return; } // already dead
|
|
351
353
|
try { process.kill(progress.pid, 'SIGKILL'); } catch {}
|
|
352
|
-
},
|
|
354
|
+
}, 5000);
|
|
353
355
|
} catch (e) {
|
|
354
356
|
if (e.code !== 'ESRCH') throw e;
|
|
355
357
|
// Already dead
|
|
356
358
|
}
|
|
357
|
-
// Update progress file
|
|
359
|
+
// Update progress file (CLI also writes this on SIGTERM, but server does it too as safety net)
|
|
358
360
|
try {
|
|
359
361
|
writeFileSync(PROGRESS_FILE, JSON.stringify({
|
|
360
362
|
...progress,
|
|
@@ -628,6 +630,18 @@ const server = createServer((req, res) => {
|
|
|
628
630
|
// Start background update check
|
|
629
631
|
checkForUpdates();
|
|
630
632
|
|
|
633
|
+
server.on('error', (err) => {
|
|
634
|
+
if (err.code === 'EADDRINUSE') {
|
|
635
|
+
console.log(`\n ⚠️ Port ${PORT} is already in use — opening existing dashboard…\n`);
|
|
636
|
+
const url = `http://localhost:${PORT}`;
|
|
637
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
638
|
+
import('child_process').then(({ exec }) => exec(`${cmd} "${url}"`));
|
|
639
|
+
setTimeout(() => process.exit(0), 500);
|
|
640
|
+
} else {
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
631
645
|
server.listen(PORT, '127.0.0.1', () => {
|
|
632
646
|
console.log(`\n SEO Intel Dashboard Server`);
|
|
633
647
|
console.log(` http://localhost:${PORT}\n`);
|