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 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.2",
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.2${colors.reset}`);
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.2",
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.2",
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
@@ -106,7 +106,7 @@ export async function resolveWorkerConfig() {
106
106
 
107
107
  if (cli.help) {
108
108
  console.log(`
109
- Apps Manager Scraper Edge Worker CLI v1.0.2
109
+ Apps Manager Scraper Edge Worker CLI v1.0.4
110
110
 
111
111
  Usage:
112
112
  npx app-manager-edge-worker [options]
@@ -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
- if (el.id) {
98
- candidates.push({ type: "css", value: `#${el.id}`, attribute: "text" });
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
- candidates.push({
104
- type: "css",
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
- if (el.className && typeof el.className === "string") {
112
- const classes = el.className
113
- .split(/\s+/)
114
- .filter((c) => c && !c.includes(":") && !c.includes("/") && !c.includes("["));
115
- if (classes.length > 0) {
116
- candidates.push({
117
- type: "css",
118
- value: `${el.tagName.toLowerCase()}.${classes.slice(0, 2).join(".")}`,
119
- attribute: "text",
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
- const tagName = el.tagName.toLowerCase();
125
- candidates.push({ type: "css", value: tagName, attribute: "text" });
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);