app-manager-edge-worker 1.0.2 โ 1.0.4
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/README.md +38 -0
- package/package.json +1 -1
- package/src/banner.js +16 -6
- package/src/client.js +2 -2
- package/src/config.js +1 -1
- package/src/handlers/previewHandler.js +181 -19
package/README.md
CHANGED
|
@@ -177,6 +177,22 @@ No manual setup required!
|
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
180
|
+
## ๐ Log Output Format
|
|
181
|
+
|
|
182
|
+
Every log line is prefixed with a timestamp in `YYYY-MM-DD HH:mm:ss` format:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
[2026-09-15 14:32:07] [INFO] Connected to Apps Manager server: https://apps-manager.onrender.com
|
|
186
|
+
[2026-09-15 14:32:07] [SUCCESS] Chromium detected: /usr/bin/chromium-browser
|
|
187
|
+
[2026-09-15 14:32:10] [TASK] SCRAPE_CHROMIUM -> https://example.com/product (1243ms)
|
|
188
|
+
[2026-09-15 14:32:11] [WARN] Scrape returned no data for selector: .price-value
|
|
189
|
+
[2026-09-15 14:32:11] [ERROR] Socket connection lost, reconnecting in 5s...
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Log levels: `[INFO]` ยท `[SUCCESS]` ยท `[WARN]` ยท `[ERROR]` ยท `[TASK]` (job dispatch with optional duration).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
180
196
|
## ๐ Security & Sandboxing
|
|
181
197
|
|
|
182
198
|
- **Outbound Only**: All socket tunnels are initiated outbound to the central Apps Manager server over TLS (`wss://`). No public IP, dynamic DNS, or port forwarding required.
|
|
@@ -185,6 +201,28 @@ No manual setup required!
|
|
|
185
201
|
|
|
186
202
|
---
|
|
187
203
|
|
|
204
|
+
## ๐ Changelog
|
|
205
|
+
|
|
206
|
+
### v1.0.4
|
|
207
|
+
- **Timestamped logs**: every log line now starts with `[YYYY-MM-DD HH:mm:ss]` for debugging and production monitoring
|
|
208
|
+
- **Version fix**: banner, help text, and user-agent string now correctly report `v1.0.4`
|
|
209
|
+
|
|
210
|
+
### v1.0.3
|
|
211
|
+
- Improved element inspector selector generation (CSS/XPath ranked candidates)
|
|
212
|
+
|
|
213
|
+
### v1.0.2
|
|
214
|
+
- Comprehensive CSS/XPath selector generation for visual element picker
|
|
215
|
+
|
|
216
|
+
### v1.0.1
|
|
217
|
+
- Chunked streaming for large payloads (256 KB chunks over Socket.IO)
|
|
218
|
+
|
|
219
|
+
### v1.0.0
|
|
220
|
+
- Initial release โ SCRAPE_CHROMIUM, SCRAPE_HTTP, PREVIEW_PAGE, INSPECT_ELEMENT handlers
|
|
221
|
+
- Multi-layer Chromium auto-detection + auto-install (Windows/macOS/Linux/Alpine/Termux)
|
|
222
|
+
- Keep-alive ping, single-flight deduplication, PM2/Docker support
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
188
226
|
## ๐ค Author
|
|
189
227
|
|
|
190
228
|
**Chetan Mohite**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-manager-edge-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Distributed Scraper Edge Worker for Apps Manager. Run on any local machine, VPS, or Termux Android device to serve browser and HTTP scraping jobs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/banner.js
CHANGED
|
@@ -11,6 +11,16 @@ const colors = {
|
|
|
11
11
|
gray: "\x1b[90m",
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Returns a formatted timestamp string: [YYYY-MM-DD HH:mm:ss]
|
|
16
|
+
*/
|
|
17
|
+
function timestamp() {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const date = now.toISOString().slice(0, 10);
|
|
20
|
+
const time = now.toISOString().slice(11, 19);
|
|
21
|
+
return `${colors.gray}[${date} ${time}]${colors.reset}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
export function printBanner({ workerName, serverUrl, concurrency, platform, isTermux, keepAliveMinutes }) {
|
|
15
25
|
console.clear();
|
|
16
26
|
console.log(`${colors.cyan}${colors.bright}`);
|
|
@@ -21,7 +31,7 @@ export function printBanner({ workerName, serverUrl, concurrency, platform, isTe
|
|
|
21
31
|
console.log(" โโโ โโโโโโ โโโ โโโโโโโโโโโโโโ โโโ โโโโโโ โโโโโโ โโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โโโ");
|
|
22
32
|
console.log(" โโโ โโโโโโ โโโ โโโโโโโโโโโโโโ โโโโโโ โโโโโโ โโโโโโโโ โโโ โโโโโโโ โโโโโโโโโโโ โโโ");
|
|
23
33
|
console.log(`${colors.reset}`);
|
|
24
|
-
console.log(`${colors.bright} ๐ Distributed Scraper Edge Worker Client v1.0.
|
|
34
|
+
console.log(`${colors.bright} ๐ Distributed Scraper Edge Worker Client v1.0.4${colors.reset}`);
|
|
25
35
|
console.log(`${colors.dim} Zero-cost, persistent tunneling & residential edge scraping${colors.reset}`);
|
|
26
36
|
console.log("--------------------------------------------------------------------------------");
|
|
27
37
|
console.log(` ${colors.bright}Device Name :${colors.reset} ${colors.green}${workerName}${colors.reset}`);
|
|
@@ -33,24 +43,24 @@ export function printBanner({ workerName, serverUrl, concurrency, platform, isTe
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
export function logInfo(msg) {
|
|
36
|
-
console.log(`${colors.cyan}[INFO]${colors.reset} ${msg}`);
|
|
46
|
+
console.log(`${timestamp()} ${colors.cyan}[INFO]${colors.reset} ${msg}`);
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
export function logSuccess(msg) {
|
|
40
|
-
console.log(`${colors.green}[SUCCESS]${colors.reset} ${msg}`);
|
|
50
|
+
console.log(`${timestamp()} ${colors.green}[SUCCESS]${colors.reset} ${msg}`);
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
export function logWarn(msg) {
|
|
44
|
-
console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`);
|
|
54
|
+
console.log(`${timestamp()} ${colors.yellow}[WARN]${colors.reset} ${msg}`);
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export function logError(msg) {
|
|
48
|
-
console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`);
|
|
58
|
+
console.log(`${timestamp()} ${colors.red}[ERROR]${colors.reset} ${msg}`);
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
export function logJob(jobType, url, durationMs = null) {
|
|
52
62
|
const timeStr = durationMs ? ` ${colors.gray}(${durationMs}ms)${colors.reset}` : "";
|
|
53
63
|
console.log(
|
|
54
|
-
`${colors.magenta}[TASK]${colors.reset} ${colors.bright}${jobType}${colors.reset} -> ${url}${timeStr}`
|
|
64
|
+
`${timestamp()} ${colors.magenta}[TASK]${colors.reset} ${colors.bright}${jobType}${colors.reset} -> ${url}${timeStr}`
|
|
55
65
|
);
|
|
56
66
|
}
|
package/src/client.js
CHANGED
|
@@ -215,7 +215,7 @@ export async function startWorkerClient(config) {
|
|
|
215
215
|
timeout: 15000,
|
|
216
216
|
headers: {
|
|
217
217
|
"x-worker-key": workerKey || "",
|
|
218
|
-
"user-agent": "AppsManagerEdgeWorker/1.0.
|
|
218
|
+
"user-agent": "AppsManagerEdgeWorker/1.0.4",
|
|
219
219
|
},
|
|
220
220
|
});
|
|
221
221
|
logInfo(`[Keep-Alive] Server awake ping OK (${res.status}) - Render instance active`);
|
|
@@ -226,7 +226,7 @@ export async function startWorkerClient(config) {
|
|
|
226
226
|
await axios.get(fallbackUrl, {
|
|
227
227
|
timeout: 10000,
|
|
228
228
|
headers: {
|
|
229
|
-
"user-agent": "AppsManagerEdgeWorker/1.0.
|
|
229
|
+
"user-agent": "AppsManagerEdgeWorker/1.0.4",
|
|
230
230
|
},
|
|
231
231
|
});
|
|
232
232
|
logInfo(`[Keep-Alive] Fallback health ping OK - Render instance active`);
|
package/src/config.js
CHANGED
|
@@ -93,48 +93,210 @@ export async function handleInspectElement(payload) {
|
|
|
93
93
|
height: Math.round(rect.height),
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
// โโโ Helper: count matching elements for a CSS selector โโโ
|
|
97
|
+
const countMatches = (sel) => {
|
|
98
|
+
try { return document.querySelectorAll(sel).length; } catch { return 999; }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// โโโ Helper: Full positional CSS path anchored at nearest ID โโโ
|
|
102
|
+
const buildCssPath = (element, maxDepth = 8) => {
|
|
103
|
+
const parts = [];
|
|
104
|
+
let curr = element;
|
|
105
|
+
while (curr && curr.nodeType === 1 && curr !== document.documentElement) {
|
|
106
|
+
if (curr === document.body) { parts.unshift("body"); break; }
|
|
107
|
+
let seg = curr.tagName.toLowerCase();
|
|
108
|
+
if (curr.id && /^[a-zA-Z][\w-]*$/.test(curr.id)) {
|
|
109
|
+
parts.unshift(`#${curr.id}`);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (curr.parentElement) {
|
|
113
|
+
const pos = Array.from(curr.parentElement.children).indexOf(curr) + 1;
|
|
114
|
+
seg += `:nth-child(${pos})`;
|
|
115
|
+
}
|
|
116
|
+
parts.unshift(seg);
|
|
117
|
+
curr = curr.parentElement;
|
|
118
|
+
if (parts.length >= maxDepth) break;
|
|
119
|
+
}
|
|
120
|
+
return parts.join(" > ");
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// โโโ Helper: Shortest unique CSS path using class combos + nth-child โโโ
|
|
124
|
+
const buildShortUniqueSelector = (element) => {
|
|
125
|
+
const pathParts = [];
|
|
126
|
+
let curr = element;
|
|
127
|
+
while (curr && curr !== document.documentElement) {
|
|
128
|
+
if (curr === document.body) { pathParts.unshift("body"); break; }
|
|
129
|
+
let seg = curr.tagName.toLowerCase();
|
|
130
|
+
if (curr.id && /^[a-zA-Z][\w-]*$/.test(curr.id)) {
|
|
131
|
+
pathParts.unshift(`#${curr.id}`);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
const classes = (curr.className && typeof curr.className === "string")
|
|
135
|
+
? curr.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))
|
|
136
|
+
: [];
|
|
137
|
+
let foundUnique = false;
|
|
138
|
+
for (let len = Math.min(classes.length, 5); len >= 1; len--) {
|
|
139
|
+
const combo = classes.slice(0, len);
|
|
140
|
+
const candidate = `${seg}.${combo.join(".")}`;
|
|
141
|
+
const full = [...pathParts, candidate].join(" > ");
|
|
142
|
+
if (countMatches(full) === 1) {
|
|
143
|
+
pathParts.unshift(candidate);
|
|
144
|
+
foundUnique = true;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!foundUnique) {
|
|
149
|
+
if (curr.parentElement) {
|
|
150
|
+
const pos = Array.from(curr.parentElement.children).indexOf(curr) + 1;
|
|
151
|
+
seg += `:nth-child(${pos})`;
|
|
152
|
+
}
|
|
153
|
+
pathParts.unshift(seg);
|
|
154
|
+
}
|
|
155
|
+
const full = pathParts.join(" > ");
|
|
156
|
+
if (countMatches(full) === 1) break;
|
|
157
|
+
curr = curr.parentElement;
|
|
158
|
+
}
|
|
159
|
+
return pathParts.join(" > ");
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// โโโ Helper: Robust absolute XPath โโโ
|
|
163
|
+
const buildXPath = (element) => {
|
|
164
|
+
if (!element || element === document) return "";
|
|
165
|
+
if (element.id && /^[a-zA-Z][\w-]*$/.test(element.id)) {
|
|
166
|
+
return `//*[@id="${element.id}"]`;
|
|
167
|
+
}
|
|
168
|
+
if (element === document.body) return "/html/body";
|
|
169
|
+
const parts = [];
|
|
170
|
+
let curr = element;
|
|
171
|
+
while (curr && curr.nodeType === 1 && curr !== document.documentElement) {
|
|
172
|
+
if (curr === document.body) { parts.unshift("body"); break; }
|
|
173
|
+
if (curr.id && /^[a-zA-Z][\w-]*$/.test(curr.id)) {
|
|
174
|
+
parts.unshift(`//*[@id="${curr.id}"]`);
|
|
175
|
+
return parts.join("/");
|
|
176
|
+
}
|
|
177
|
+
const tag = curr.tagName.toLowerCase();
|
|
178
|
+
let idx = 1;
|
|
179
|
+
let sib = curr.previousElementSibling;
|
|
180
|
+
while (sib) {
|
|
181
|
+
if (sib.tagName.toLowerCase() === tag) idx++;
|
|
182
|
+
sib = sib.previousElementSibling;
|
|
183
|
+
}
|
|
184
|
+
const total = curr.parentElement
|
|
185
|
+
? Array.from(curr.parentElement.children).filter(c => c.tagName.toLowerCase() === tag).length
|
|
186
|
+
: 1;
|
|
187
|
+
parts.unshift(total > 1 ? `${tag}[${idx}]` : tag);
|
|
188
|
+
curr = curr.parentElement;
|
|
189
|
+
}
|
|
190
|
+
return "/" + parts.join("/");
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// โโโ Build ranked candidates โโโ
|
|
96
194
|
const candidates = [];
|
|
97
|
-
|
|
98
|
-
|
|
195
|
+
const seen = new Set();
|
|
196
|
+
const addCandidate = (type, value, extra = {}) => {
|
|
197
|
+
if (!value || seen.has(value)) return;
|
|
198
|
+
seen.add(value);
|
|
199
|
+
const matchCount = type === "css" ? countMatches(value) : null;
|
|
200
|
+
candidates.push({ type, value, attribute: "text", matchCount, ...extra });
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// 1. ID (most reliable, unique)
|
|
204
|
+
if (el.id && /^[a-zA-Z][\w-]*$/.test(el.id)) {
|
|
205
|
+
addCandidate("css", `#${el.id}`, { specificity: "unique", label: "ID Selector" });
|
|
99
206
|
}
|
|
100
207
|
|
|
208
|
+
// 2. data-* and aria-* attributes
|
|
101
209
|
for (const attr of el.attributes) {
|
|
102
|
-
if (attr.name.startsWith("data-") && attr.value) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
value: `[${attr.name}='${attr.value}']`,
|
|
106
|
-
attribute: "text",
|
|
210
|
+
if ((attr.name.startsWith("data-") || attr.name.startsWith("aria-")) && attr.value && attr.value.length < 80) {
|
|
211
|
+
addCandidate("css", `${el.tagName.toLowerCase()}[${attr.name}="${attr.value}"]`, {
|
|
212
|
+
specificity: "high", label: `Attribute: ${attr.name}`
|
|
107
213
|
});
|
|
108
214
|
}
|
|
109
215
|
}
|
|
110
216
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
217
|
+
// 3. Shortest unique CSS path
|
|
218
|
+
const shortUnique = buildShortUniqueSelector(el);
|
|
219
|
+
if (shortUnique) {
|
|
220
|
+
const mc = countMatches(shortUnique);
|
|
221
|
+
addCandidate("css", shortUnique, {
|
|
222
|
+
specificity: mc === 1 ? "unique" : "medium",
|
|
223
|
+
label: "Smart CSS Path",
|
|
224
|
+
matchCount: mc,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 4. Class-based selectors (increasing combos until unique)
|
|
229
|
+
const classes = (el.className && typeof el.className === "string")
|
|
230
|
+
? el.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))
|
|
231
|
+
: [];
|
|
232
|
+
for (let len = 1; len <= Math.min(classes.length, 5); len++) {
|
|
233
|
+
const sel = `${el.tagName.toLowerCase()}.${classes.slice(0, len).join(".")}`;
|
|
234
|
+
const mc = countMatches(sel);
|
|
235
|
+
addCandidate("css", sel, {
|
|
236
|
+
specificity: mc === 1 ? "unique" : mc <= 3 ? "medium" : "generic",
|
|
237
|
+
label: "Class Selector",
|
|
238
|
+
matchCount: mc,
|
|
239
|
+
});
|
|
240
|
+
if (mc === 1) break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 5. Table-specific selectors
|
|
244
|
+
const tr = el.closest("tr");
|
|
245
|
+
if (tr && tr.parentElement) {
|
|
246
|
+
const rowIdx = Array.from(tr.parentElement.children).indexOf(tr) + 1;
|
|
247
|
+
const td = el.closest("td, th");
|
|
248
|
+
if (td) {
|
|
249
|
+
const colIdx = Array.from(tr.children).indexOf(td) + 1;
|
|
250
|
+
addCandidate("css", `table tr:nth-child(${rowIdx}) td:nth-child(${colIdx})`, {
|
|
251
|
+
specificity: "medium", label: "Table Cell"
|
|
252
|
+
});
|
|
253
|
+
addCandidate("css", `table tbody tr:nth-child(${rowIdx}) td:nth-child(${colIdx})`, {
|
|
254
|
+
specificity: "medium", label: "Table Cell (tbody)"
|
|
120
255
|
});
|
|
121
256
|
}
|
|
122
257
|
}
|
|
123
258
|
|
|
124
|
-
|
|
125
|
-
|
|
259
|
+
// 6. Full structural CSS path
|
|
260
|
+
const fullCss = buildCssPath(el, 8);
|
|
261
|
+
addCandidate("css", fullCss, { specificity: "unique", label: "Full CSS Path (Structural)" });
|
|
262
|
+
|
|
263
|
+
// 7. XPath โ most reliable for scrapers
|
|
264
|
+
const xpath = buildXPath(el);
|
|
265
|
+
addCandidate("xpath", xpath, { specificity: "high", label: "XPath" });
|
|
266
|
+
|
|
267
|
+
// Sort by specificity: unique > high > medium > generic
|
|
268
|
+
const rank = { unique: 0, high: 1, medium: 2, generic: 3 };
|
|
269
|
+
candidates.sort((a, b) => (rank[a.specificity] ?? 4) - (rank[b.specificity] ?? 4));
|
|
270
|
+
|
|
271
|
+
// โโโ Breadcrumb hierarchy for UI โโโ
|
|
272
|
+
const hierarchy = [];
|
|
273
|
+
let curr = el;
|
|
274
|
+
while (curr && curr !== document.body && hierarchy.length < 7) {
|
|
275
|
+
let desc = curr.tagName.toLowerCase();
|
|
276
|
+
if (curr.id) {
|
|
277
|
+
desc += `#${curr.id}`;
|
|
278
|
+
} else if (curr.classList && curr.classList.length > 0) {
|
|
279
|
+
const cls = Array.from(curr.classList)
|
|
280
|
+
.filter(c => /^[a-zA-Z_-][\w-]*$/.test(c))
|
|
281
|
+
.slice(0, 2);
|
|
282
|
+
if (cls.length) desc += `.${cls.join(".")}`;
|
|
283
|
+
}
|
|
284
|
+
hierarchy.unshift(desc);
|
|
285
|
+
curr = curr.parentElement;
|
|
286
|
+
}
|
|
126
287
|
|
|
127
288
|
const text = el.innerText ? el.innerText.trim() : el.textContent?.trim() || "";
|
|
128
289
|
|
|
129
290
|
return {
|
|
130
291
|
found: true,
|
|
131
|
-
tagName,
|
|
292
|
+
tagName: el.tagName.toLowerCase(),
|
|
132
293
|
text,
|
|
133
294
|
attributes: Array.from(el.attributes).reduce((acc, a) => {
|
|
134
295
|
acc[a.name] = a.value;
|
|
135
296
|
return acc;
|
|
136
297
|
}, {}),
|
|
137
298
|
candidates,
|
|
299
|
+
hierarchy,
|
|
138
300
|
boundingRect,
|
|
139
301
|
};
|
|
140
302
|
}, x, y);
|