seo-intel 1.1.4 β 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/Setup SEO Intel.command +12 -24
- package/cli.js +41 -0
- package/package.json +1 -1
- package/reports/generate-html.js +157 -68
- package/server.js +25 -2
- package/setup/openclaw-bridge.js +29 -3
- package/setup/wizard.html +2 -0
package/Setup SEO Intel.command
CHANGED
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
cd "$(dirname "$0")"
|
|
3
|
-
clear
|
|
2
|
+
cd "$(dirname "$0")"
|
|
4
3
|
echo ""
|
|
5
|
-
echo "
|
|
6
|
-
echo " Opening in your browser..."
|
|
4
|
+
echo " SEO Intel β Setup Wizard"
|
|
5
|
+
echo " Opening setup in your browser..."
|
|
7
6
|
echo ""
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
open "http://localhost:3000/setup" 2>/dev/null || xdg-open "http://localhost:3000/setup" 2>/dev/null
|
|
18
|
-
|
|
19
|
-
echo " Setup wizard is open at http://localhost:3000/setup"
|
|
20
|
-
echo " Keep this window open while using the wizard."
|
|
21
|
-
echo ""
|
|
22
|
-
read -n 1 -s -r -p " Press any key to stop the server and exit..."
|
|
23
|
-
|
|
24
|
-
# Clean up
|
|
25
|
-
if [ -n "$SERVER_PID" ]; then
|
|
26
|
-
kill $SERVER_PID 2>/dev/null
|
|
27
|
-
fi
|
|
7
|
+
node cli.js serve &
|
|
8
|
+
SERVER_PID=$!
|
|
9
|
+
# Wait for server to be ready
|
|
10
|
+
for i in {1..10}; do
|
|
11
|
+
sleep 1
|
|
12
|
+
if curl -s http://localhost:3000/ > /dev/null 2>&1; then break; fi
|
|
13
|
+
done
|
|
14
|
+
open "http://localhost:3000/setup"
|
|
15
|
+
wait $SERVER_PID
|
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 βββ -->
|
|
@@ -2188,6 +2218,53 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2188
2218
|
if (scopeIdx > -1 && parts[scopeIdx + 1]) extra.scope = parts[scopeIdx + 1];
|
|
2189
2219
|
runCommand(cmd, proj, extra);
|
|
2190
2220
|
});
|
|
2221
|
+
|
|
2222
|
+
// Autorun: if URL has ?autorun=setup-classic, fire seo-intel setup --classic via SSE
|
|
2223
|
+
(function() {
|
|
2224
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
2225
|
+
if (urlParams.get('autorun') === 'setup-classic') {
|
|
2226
|
+
// Remove the param so it doesn't re-trigger on refresh
|
|
2227
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
2228
|
+
// Wait a tick for the panel to be ready, then stream the command
|
|
2229
|
+
setTimeout(function() {
|
|
2230
|
+
if (!isServed) {
|
|
2231
|
+
appendLine('Not connected to server. Cannot run setup --classic automatically.', 'error');
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
running = true;
|
|
2235
|
+
status.textContent = 'running...';
|
|
2236
|
+
status.style.color = 'var(--color-warning)';
|
|
2237
|
+
appendLine('$ seo-intel setup --classic', 'cmd');
|
|
2238
|
+
const params = new URLSearchParams({ command: 'setup', classic: 'true' });
|
|
2239
|
+
eventSource = new EventSource('/api/terminal?' + params.toString());
|
|
2240
|
+
eventSource.onmessage = function(e) {
|
|
2241
|
+
try {
|
|
2242
|
+
const msg = JSON.parse(e.data);
|
|
2243
|
+
if (msg.type === 'stdout') appendLine(msg.data, 'stdout');
|
|
2244
|
+
else if (msg.type === 'stderr') appendLine(msg.data, 'stderr');
|
|
2245
|
+
else if (msg.type === 'error') { appendLine('Error: ' + msg.data, 'error'); }
|
|
2246
|
+
else if (msg.type === 'exit') {
|
|
2247
|
+
const code = msg.data?.code ?? msg.data;
|
|
2248
|
+
appendLine(code === 0 ? 'Done.' : 'Exited with code ' + code, code === 0 ? 'exit-ok' : 'exit-err');
|
|
2249
|
+
running = false;
|
|
2250
|
+
status.textContent = code === 0 ? 'done' : 'failed';
|
|
2251
|
+
status.style.color = code === 0 ? 'var(--color-success)' : 'var(--color-danger)';
|
|
2252
|
+
eventSource.close();
|
|
2253
|
+
eventSource = null;
|
|
2254
|
+
}
|
|
2255
|
+
} catch (_) {}
|
|
2256
|
+
};
|
|
2257
|
+
eventSource.onerror = function() {
|
|
2258
|
+
if (running) { appendLine('Connection lost.', 'error'); }
|
|
2259
|
+
running = false;
|
|
2260
|
+
status.textContent = 'disconnected';
|
|
2261
|
+
status.style.color = 'var(--color-danger)';
|
|
2262
|
+
eventSource?.close();
|
|
2263
|
+
eventSource = null;
|
|
2264
|
+
};
|
|
2265
|
+
}, 300);
|
|
2266
|
+
}
|
|
2267
|
+
})();
|
|
2191
2268
|
})();
|
|
2192
2269
|
</script>
|
|
2193
2270
|
|
|
@@ -3499,7 +3576,6 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3499
3576
|
};
|
|
3500
3577
|
|
|
3501
3578
|
window.stopJob = function() {
|
|
3502
|
-
if (!confirm('Stop the running job?')) return;
|
|
3503
3579
|
fetch('/api/stop', { method: 'POST' })
|
|
3504
3580
|
.then(function(r) { return r.json(); })
|
|
3505
3581
|
.then(function(data) {
|
|
@@ -3582,6 +3658,11 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3582
3658
|
dot.className = 'es-dot';
|
|
3583
3659
|
label.style.color = 'var(--color-success)';
|
|
3584
3660
|
label.textContent = 'Completed (' + (data.extracted || 0) + ' extracted' + (data.failed ? ', ' + data.failed + ' failed' : '') + ')';
|
|
3661
|
+
} else if (data.status === 'stopped') {
|
|
3662
|
+
panel.classList.remove('is-running', 'is-crashed');
|
|
3663
|
+
dot.className = 'es-dot';
|
|
3664
|
+
label.style.color = 'var(--accent-gold)';
|
|
3665
|
+
label.textContent = 'Stopped' + (data.extracted ? ' (' + data.extracted + ' extracted)' : '');
|
|
3585
3666
|
} else if (data.status === 'crashed') {
|
|
3586
3667
|
panel.classList.remove('is-running');
|
|
3587
3668
|
panel.classList.add('is-crashed');
|
|
@@ -3595,6 +3676,11 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3595
3676
|
fetch('/api/progress')
|
|
3596
3677
|
.then(function(r) { return r.json(); })
|
|
3597
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
|
+
}
|
|
3598
3684
|
if (data.status === 'running') {
|
|
3599
3685
|
setButtonsState(true, data.command);
|
|
3600
3686
|
startPolling();
|
|
@@ -4691,7 +4777,6 @@ function buildMultiHtmlTemplate(allProjectData) {
|
|
|
4691
4777
|
};
|
|
4692
4778
|
|
|
4693
4779
|
window.stopJob = function() {
|
|
4694
|
-
if (!confirm('Stop the running job?')) return;
|
|
4695
4780
|
fetch('/api/stop', { method: 'POST' })
|
|
4696
4781
|
.then(function(r) { return r.json(); })
|
|
4697
4782
|
.then(function(data) {
|
|
@@ -4750,6 +4835,10 @@ function buildMultiHtmlTemplate(allProjectData) {
|
|
|
4750
4835
|
panel.classList.remove('is-running', 'is-crashed');
|
|
4751
4836
|
dot.className = 'es-dot'; label.style.color = 'var(--color-success)';
|
|
4752
4837
|
label.textContent = 'Completed (' + (data.extracted || 0) + ' extracted)';
|
|
4838
|
+
} else if (data.status === 'stopped') {
|
|
4839
|
+
panel.classList.remove('is-running', 'is-crashed');
|
|
4840
|
+
dot.className = 'es-dot'; label.style.color = 'var(--accent-gold)';
|
|
4841
|
+
label.textContent = 'Stopped' + (data.extracted ? ' (' + data.extracted + ' extracted)' : '');
|
|
4753
4842
|
} else if (data.status === 'crashed') {
|
|
4754
4843
|
panel.classList.remove('is-running'); panel.classList.add('is-crashed');
|
|
4755
4844
|
dot.className = 'es-dot crashed'; label.style.color = 'var(--color-danger)';
|
package/server.js
CHANGED
|
@@ -345,15 +345,26 @@ 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
|
}
|
|
359
|
+
// Update progress file (CLI also writes this on SIGTERM, but server does it too as safety net)
|
|
360
|
+
try {
|
|
361
|
+
writeFileSync(PROGRESS_FILE, JSON.stringify({
|
|
362
|
+
...progress,
|
|
363
|
+
status: 'stopped',
|
|
364
|
+
stopped_at: Date.now(),
|
|
365
|
+
updated_at: Date.now(),
|
|
366
|
+
}, null, 2));
|
|
367
|
+
} catch { /* best-effort */ }
|
|
357
368
|
json(res, 200, { stopped: true, pid: progress.pid, command: progress.command });
|
|
358
369
|
} catch (e) {
|
|
359
370
|
json(res, 500, { error: e.message });
|
|
@@ -619,6 +630,18 @@ const server = createServer((req, res) => {
|
|
|
619
630
|
// Start background update check
|
|
620
631
|
checkForUpdates();
|
|
621
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
|
+
|
|
622
645
|
server.listen(PORT, '127.0.0.1', () => {
|
|
623
646
|
console.log(`\n SEO Intel Dashboard Server`);
|
|
624
647
|
console.log(` http://localhost:${PORT}\n`);
|
package/setup/openclaw-bridge.js
CHANGED
|
@@ -66,13 +66,39 @@ async function askAgent(messages, opts = {}) {
|
|
|
66
66
|
*/
|
|
67
67
|
export async function isGatewayReady() {
|
|
68
68
|
try {
|
|
69
|
+
// Try to read token from env or openclaw config file
|
|
70
|
+
let token = process.env.OPENCLAW_TOKEN;
|
|
71
|
+
if (!token) {
|
|
72
|
+
try {
|
|
73
|
+
const configPath = join(process.env.HOME || '~', '.openclaw', 'openclaw.json');
|
|
74
|
+
const { readFileSync } = await import('fs');
|
|
75
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
76
|
+
// Gateway auth token is a 48-char hex string in the gateway.auth block
|
|
77
|
+
const matches = [...raw.matchAll(/"token":\s*"([a-f0-9]{40,})"/g)];
|
|
78
|
+
if (matches.length > 0) token = matches[matches.length - 1][1];
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
if (!token) return false;
|
|
82
|
+
|
|
83
|
+
// Verify gateway is reachable via a lightweight chat completions ping
|
|
69
84
|
const controller = new AbortController();
|
|
70
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
71
|
-
const res = await fetch(`${OPENCLAW_API}/v1/
|
|
85
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
86
|
+
const res = await fetch(`${OPENCLAW_API}/v1/chat/completions`, {
|
|
87
|
+
method: 'POST',
|
|
72
88
|
signal: controller.signal,
|
|
89
|
+
headers: {
|
|
90
|
+
'Authorization': `Bearer ${token}`,
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
model: 'anthropic/claude-haiku-4-5',
|
|
95
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
96
|
+
max_tokens: 1,
|
|
97
|
+
}),
|
|
73
98
|
});
|
|
74
99
|
clearTimeout(timeout);
|
|
75
|
-
|
|
100
|
+
const ct = res.headers.get('content-type') || '';
|
|
101
|
+
return res.ok && ct.includes('application/json');
|
|
76
102
|
} catch {
|
|
77
103
|
return false;
|
|
78
104
|
}
|
package/setup/wizard.html
CHANGED
|
@@ -54,6 +54,7 @@ body {
|
|
|
54
54
|
max-width: var(--max-width);
|
|
55
55
|
margin: 0 auto 24px;
|
|
56
56
|
text-align: center;
|
|
57
|
+
position: relative;
|
|
57
58
|
}
|
|
58
59
|
.wizard-header h1 {
|
|
59
60
|
font-family: var(--font-display);
|
|
@@ -1172,6 +1173,7 @@ input::placeholder {
|
|
|
1172
1173
|
<div class="wizard-header">
|
|
1173
1174
|
<h1>SEO Intel</h1>
|
|
1174
1175
|
<div class="subtitle">Setup Wizard</div>
|
|
1176
|
+
<a href="http://localhost:3000/?autorun=setup-classic" title="Run setup in Terminal instead" style="position:absolute;top:14px;right:18px;font-size:0.68rem;color:var(--color-muted,#888);text-decoration:none;opacity:0.7;transition:opacity 0.15s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'"><i class="fa-solid fa-terminal"></i> Terminal setup</a>
|
|
1175
1177
|
</div>
|
|
1176
1178
|
|
|
1177
1179
|
<!-- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|