@alizadefaig/pos-server 1.2.29 → 1.2.31

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.
@@ -170,9 +170,15 @@ const buildAnswer = ({nonce, httpPort, identity}) => {
170
170
  * Проверка ответа мастера (UDP и HTTP). address — откуда ответ пришёл на самом деле: он обязан
171
171
  * быть в подписанных hosts. port — если известен (HTTP): мастер обязан назвать тот же.
172
172
  */
173
- const accept = (message, {key, nonce, address, companyId = null, port = null}) => {
174
- if (!message || message.t !== ANSWER || message.nonce !== nonce) {
175
- return null;
173
+ const check = (message, {key, nonce, address, companyId = null, port = null}) => {
174
+ if (!message) {
175
+ return 'bad_packet';
176
+ }
177
+ if (message.t !== ANSWER) {
178
+ return 'not_answer';
179
+ }
180
+ if (message.nonce !== nonce) {
181
+ return 'stale_nonce';
176
182
  }
177
183
  const hosts = Array.isArray(message.hosts) ? message.hosts.map(String) : [];
178
184
  const fields = {
@@ -183,16 +189,20 @@ const accept = (message, {key, nonce, address, companyId = null, port = null}) =
183
189
  hosts,
184
190
  };
185
191
  if (!Number.isInteger(fields.port) || fields.port <= 0 || fields.port > 65535) {
186
- return null;
192
+ return 'bad_port';
187
193
  }
188
194
  if (port !== null && fields.port !== port) {
189
- return null;
195
+ return 'port_mismatch';
190
196
  }
191
- if (!hosts.includes(address) || !proofMatches(key, fields, message.proof)) {
192
- return null;
197
+ if (!hosts.includes(address)) {
198
+ return 'address_not_in_hosts';
199
+ }
200
+ // Другой ключ зала: мастер и ведомый настроены разными ключами (смена ключа не доехала).
201
+ if (!proofMatches(key, fields, message.proof)) {
202
+ return 'bad_proof';
193
203
  }
194
204
  if (companyId && fields.company_id !== companyId) {
195
- return null;
205
+ return 'other_company';
196
206
  }
197
207
  return {
198
208
  url: `http://${address}:${fields.port}`,
@@ -203,8 +213,31 @@ const accept = (message, {key, nonce, address, companyId = null, port = null}) =
203
213
  };
204
214
  };
205
215
 
216
+ const accept = (message, options) => {
217
+ const result = check(message, options);
218
+ return typeof result === 'string' ? null : result;
219
+ };
220
+
221
+ const bump = (map, key) => {
222
+ map[key] = (map[key] || 0) + 1;
223
+ };
224
+
206
225
  // ---------------------------------------------------------------- мастер: отвечает
207
226
 
227
+ /** Почему buildAnswer вернул null — для диагностики. */
228
+ const noAnswerReason = (identity) => {
229
+ if (!config.lanKey()) {
230
+ return 'no_lan_key';
231
+ }
232
+ if (!identity || !identity.company_id) {
233
+ return 'no_session';
234
+ }
235
+ if (!interfaces().length) {
236
+ return 'no_interfaces';
237
+ }
238
+ return 'bad_nonce';
239
+ };
240
+
208
241
  class Responder {
209
242
  /**
210
243
  * @param {{httpPort: () => number, identity: () => ({company_id, terminal_id}|null)}} options
@@ -217,6 +250,13 @@ class Responder {
217
250
  this.socket = null;
218
251
  this.answered = 0;
219
252
  this.limiter = new Limiter(MAX_ANSWERS_PER_SECOND);
253
+ // Диагностика (/local/status.discovery): доходят ли запросы ведомых до мастера.
254
+ this.received = 0;
255
+ this.rejected = {};
256
+ this.sendErrors = 0;
257
+ this.lastQuery = null;
258
+ this.bound = null;
259
+ this.error = null;
220
260
  }
221
261
 
222
262
  start() {
@@ -228,12 +268,20 @@ class Responder {
228
268
  const socket = dgram.createSocket('udp4');
229
269
  socket.on('error', (e) => {
230
270
  console.warn('[discovery] responder error:', e.message);
271
+ this.error = `${e.code || ''} ${e.message}`.trim();
231
272
  this.stop();
232
273
  });
233
274
  socket.on('message', (buffer, rinfo) => {
275
+ this.received += 1;
276
+ this.lastQuery = {from: rinfo.address, at: new Date().toISOString()};
234
277
  this.answer(socket, parse(buffer), rinfo).catch(() => {});
235
278
  });
236
- socket.bind(port, () => log(`answering on udp/${port}`));
279
+ socket.bind(port, () => {
280
+ const address = socket.address();
281
+ this.bound = `${address.address}:${address.port}`;
282
+ this.error = null;
283
+ log(`answering on udp/${port}`);
284
+ });
237
285
  socket.unref();
238
286
  this.socket = socket;
239
287
  return this;
@@ -241,19 +289,40 @@ class Responder {
241
289
 
242
290
  async answer(socket, message, rinfo) {
243
291
  if (!message || message.t !== QUERY) {
292
+ bump(this.rejected, 'bad_packet');
244
293
  return;
245
294
  }
246
295
  const identity = this.identity();
247
296
  // Ведомый другого заведения в той же сети — не наш.
248
297
  if (identity && message.company_id && message.company_id !== identity.company_id) {
298
+ bump(this.rejected, 'other_company');
249
299
  return;
250
300
  }
251
301
  const answer = buildAnswer({nonce: message.nonce, httpPort: this.httpPort(), identity});
252
- if (!answer || !this.limiter.take()) {
302
+ if (!answer) {
303
+ bump(this.rejected, noAnswerReason(identity));
304
+ return;
305
+ }
306
+ if (!this.limiter.take()) {
307
+ bump(this.rejected, 'rate_limited');
253
308
  return;
254
309
  }
255
310
  this.answered += 1;
256
- await send(socket, answer, rinfo.port, rinfo.address);
311
+ if (!await send(socket, answer, rinfo.port, rinfo.address)) {
312
+ this.sendErrors += 1;
313
+ }
314
+ }
315
+
316
+ status() {
317
+ return {
318
+ bound: this.bound,
319
+ error: this.error,
320
+ received: this.received,
321
+ answered: this.answered,
322
+ rejected: this.rejected,
323
+ send_errors: this.sendErrors,
324
+ last_query: this.lastQuery,
325
+ };
257
326
  }
258
327
 
259
328
  stop() {
@@ -271,21 +340,37 @@ class Responder {
271
340
 
272
341
  const httpLimiter = new Limiter(MAX_ANSWERS_PER_SECOND);
273
342
  let httpAnswered = 0;
343
+ const httpStats = {requests: 0, rejected: {}, last: null};
274
344
 
275
345
  /**
276
346
  * GET /local/whoami?nonce= — тот же подписанный ответ по HTTP (ступени 1 и 3 поиска). Путь
277
347
  * открыт без ключа зала (lan.js): ответ сам доказывает знание ключа и ничего не раскрывает.
278
348
  * null — не отвечаем (не мастер, нет ключа или сессии, превышен лимит).
279
349
  */
280
- const whoami = ({nonce, httpPort, identity}) => {
350
+ const whoami = ({nonce, httpPort, identity, from = null}) => {
351
+ httpStats.requests += 1;
352
+ httpStats.last = {from, at: new Date().toISOString()};
281
353
  const answer = buildAnswer({nonce, httpPort, identity});
282
- if (!answer || !httpLimiter.take()) {
354
+ if (!answer) {
355
+ bump(httpStats.rejected, noAnswerReason(identity));
356
+ return null;
357
+ }
358
+ if (!httpLimiter.take()) {
359
+ bump(httpStats.rejected, 'rate_limited');
283
360
  return null;
284
361
  }
285
362
  httpAnswered += 1;
286
363
  return answer;
287
364
  };
288
365
 
366
+ /** Счётчики /local/whoami мастера: доходят ли HTTP-проверки ведомых. */
367
+ const whoamiStatus = () => ({
368
+ requests: httpStats.requests,
369
+ answered: httpAnswered,
370
+ rejected: {...httpStats.rejected},
371
+ last: httpStats.last,
372
+ });
373
+
289
374
  // ---------------------------------------------------------------- ведомый: ищет
290
375
 
291
376
  /**
@@ -295,9 +380,12 @@ const whoami = ({nonce, httpPort, identity}) => {
295
380
  * @param {{companyId?: string, targets?: string[]}} options
296
381
  * targets — куда слать вместо широковещательных адресов (тесты, сети без broadcast).
297
382
  */
298
- const probe = ({companyId = null, targets = null} = {}) => new Promise((resolve) => {
383
+ const probe = ({companyId = null, targets = null, stats = null} = {}) => new Promise((resolve) => {
299
384
  const key = config.lanKey();
300
385
  if (!key) {
386
+ if (stats) {
387
+ stats.error = 'no_lan_key';
388
+ }
301
389
  resolve([]);
302
390
  return;
303
391
  }
@@ -321,12 +409,24 @@ const probe = ({companyId = null, targets = null} = {}) => new Promise((resolve)
321
409
  };
322
410
  const timer = setTimeout(finish, window);
323
411
 
324
- socket.on('error', finish);
412
+ socket.on('error', (e) => {
413
+ if (stats) {
414
+ stats.error = `${e.code || ''} ${e.message}`.trim();
415
+ }
416
+ finish();
417
+ });
325
418
  socket.on('message', (buffer, rinfo) => {
326
- const candidate = accept(parse(buffer), {key, nonce, address: rinfo.address, companyId});
327
- if (candidate) {
328
- found.set(rinfo.address, candidate);
419
+ const result = check(parse(buffer), {key, nonce, address: rinfo.address, companyId});
420
+ if (stats) {
421
+ stats.answers += 1;
422
+ }
423
+ if (typeof result === 'string') {
424
+ if (stats && Object.keys(stats.rejected).length < 20) {
425
+ bump(stats.rejected, `${result}@${rinfo.address}`);
426
+ }
427
+ return;
329
428
  }
429
+ found.set(rinfo.address, result);
330
430
  });
331
431
  socket.bind(0, async () => {
332
432
  socket.setBroadcast(true);
@@ -334,7 +434,12 @@ const probe = ({companyId = null, targets = null} = {}) => new Promise((resolve)
334
434
  const addresses = targets && targets.length
335
435
  ? targets
336
436
  : [...new Set([...interfaces().map((item) => item.broadcast), '255.255.255.255'])];
337
- await Promise.all(addresses.map((address) => send(socket, message, port, address)));
437
+ const results = await Promise.all(addresses.map((address) => send(socket, message, port, address)));
438
+ if (stats) {
439
+ stats.targets = addresses;
440
+ stats.sent = results.filter(Boolean).length;
441
+ stats.send_errors = results.length - stats.sent;
442
+ }
338
443
  });
339
444
  });
340
445
 
@@ -342,16 +447,34 @@ const probe = ({companyId = null, targets = null} = {}) => new Promise((resolve)
342
447
  * Проверка адреса по HTTP: GET <url>/local/whoami со свежим nonce. Отдаёт кандидата или null.
343
448
  * Только http и IPv4 — подпись мастера перечисляет его IPv4, по имени хоста её не сверить.
344
449
  */
345
- const verify = async (url, {companyId = null, timeout = VERIFY_TIMEOUT_MS} = {}) => {
450
+ const verify = async (url, options = {}) => {
451
+ const result = await verifyWhy(url, options);
452
+ return typeof result === 'string' ? null : result;
453
+ };
454
+
455
+ /** Сетевая ошибка fetch → короткий код: timeout, ECONNREFUSED, EHOSTUNREACH, … */
456
+ const fetchError = (e) => {
457
+ if (e && e.name === 'AbortError') {
458
+ return 'timeout';
459
+ }
460
+ const cause = e && e.cause;
461
+ return (cause && (cause.code || cause.name)) || (e && (e.code || e.name)) || 'error';
462
+ };
463
+
464
+ /** Как verify, но вместо null — причина отказа строкой (для диагностики). */
465
+ const verifyWhy = async (url, {companyId = null, timeout = VERIFY_TIMEOUT_MS} = {}) => {
346
466
  const key = config.lanKey();
347
467
  let target;
348
468
  try {
349
469
  target = new URL(url);
350
470
  } catch (e) {
351
- return null;
471
+ return 'bad_url';
352
472
  }
353
- if (!key || target.protocol !== 'http:' || !IPV4.test(target.hostname)) {
354
- return null;
473
+ if (!key) {
474
+ return 'no_lan_key';
475
+ }
476
+ if (target.protocol !== 'http:' || !IPV4.test(target.hostname)) {
477
+ return 'bad_url';
355
478
  }
356
479
  const port = Number(target.port || 80);
357
480
  const nonce = randomBytes(16).toString('hex');
@@ -363,12 +486,12 @@ const verify = async (url, {companyId = null, timeout = VERIFY_TIMEOUT_MS} = {})
363
486
  redirect: 'error',
364
487
  });
365
488
  if (!response.ok) {
366
- return null;
489
+ return `http_${response.status}`;
367
490
  }
368
491
  const body = Buffer.from(await response.arrayBuffer());
369
- return accept(parse(body), {key, nonce, address: target.hostname, companyId, port});
492
+ return check(parse(body), {key, nonce, address: target.hostname, companyId, port});
370
493
  } catch (e) {
371
- return null;
494
+ return fetchError(e);
372
495
  } finally {
373
496
  clearTimeout(timer);
374
497
  }
@@ -378,12 +501,30 @@ const verify = async (url, {companyId = null, timeout = VERIFY_TIMEOUT_MS} = {})
378
501
  * Скан подсети по HTTP пачками по SCAN_BATCH. Останавливается на первой пачке, где кто-то
379
502
  * ответил. hosts — вместо своей подсети (тесты).
380
503
  */
381
- const scan = async ({companyId = null, hosts = null, port = settings().httpPort} = {}) => {
504
+ const scan = async ({companyId = null, hosts = null, port = settings().httpPort, stats = null} = {}) => {
382
505
  const list = hosts || subnetHosts();
506
+ if (stats) {
507
+ stats.hosts = list.length;
508
+ stats.first = list[0] || null;
509
+ stats.last = list[list.length - 1] || null;
510
+ }
383
511
  for (let i = 0; i < list.length; i += SCAN_BATCH) {
384
- const answers = await Promise.all(list.slice(i, i + SCAN_BATCH)
385
- .map((host) => verify(`http://${host}:${port}`, {companyId, timeout: SCAN_TIMEOUT_MS})));
386
- const found = answers.filter(Boolean);
512
+ const batch = list.slice(i, i + SCAN_BATCH);
513
+ const answers = await Promise.all(batch
514
+ .map((host) => verifyWhy(`http://${host}:${port}`, {companyId, timeout: SCAN_TIMEOUT_MS})));
515
+ if (stats) {
516
+ answers.forEach((answer, index) => {
517
+ if (typeof answer !== 'string') {
518
+ return;
519
+ }
520
+ bump(stats.errors, answer);
521
+ // Кто-то ответил по HTTP, но не как наш мастер — адрес интересен: возможно, это он.
522
+ if (answer !== 'timeout' && answer !== 'ECONNREFUSED' && stats.responded.length < 10) {
523
+ stats.responded.push(`${batch[index]}:${answer}`);
524
+ }
525
+ });
526
+ }
527
+ const found = answers.filter((answer) => typeof answer !== 'string');
387
528
  if (found.length) {
388
529
  return found;
389
530
  }
@@ -420,6 +561,9 @@ class Finder {
420
561
  this.lastFoundAt = null;
421
562
  this.candidates = [];
422
563
  this.conflict = false;
564
+ this.searches = 0;
565
+ this.lastSearch = null;
566
+ this.lastScan = null;
423
567
  }
424
568
 
425
569
  start() {
@@ -468,30 +612,44 @@ class Finder {
468
612
 
469
613
  async search() {
470
614
  this.lastProbeAt = new Date().toISOString();
615
+ this.searches += 1;
471
616
  const companyId = this.companyId();
472
617
  let candidates = [];
473
618
  let via = null;
619
+ // Диагностика: что дала каждая ступень последнего поиска.
620
+ const report = {at: this.lastProbeAt, company_id: companyId, known: {}, udp: null};
474
621
  // 1. известный адрес: текущий или сохранённый с прошлого запуска
475
622
  const known = [...new Set([this.current, this.remembered()].filter(Boolean))];
476
623
  for (const url of known) {
477
- const candidate = await verify(url, {companyId});
478
- if (candidate) {
479
- candidates = [candidate];
480
- via = 'saved';
481
- break;
624
+ const candidate = await verifyWhy(url, {companyId});
625
+ if (typeof candidate === 'string') {
626
+ report.known[url] = candidate;
627
+ continue;
482
628
  }
629
+ report.known[url] = 'ok';
630
+ candidates = [candidate];
631
+ via = 'saved';
632
+ break;
483
633
  }
484
634
  // 2. широковещательный запрос
485
635
  if (!candidates.length && !this.stopped) {
486
- candidates = await probe({companyId, targets: this.targets});
636
+ const stats = {targets: [], sent: 0, send_errors: 0, answers: 0, rejected: {}, error: null};
637
+ candidates = await probe({companyId, targets: this.targets, stats});
638
+ report.udp = stats;
487
639
  via = 'udp';
488
640
  }
489
641
  // 3. скан подсети — широковещание могло не дойти (фильтр точки доступа, брандмауэр)
490
642
  if (!candidates.length && !this.stopped && this.scanDue()) {
491
643
  this.lastScanAt = Date.now();
492
- candidates = await scan({companyId, hosts: this.scanHosts});
644
+ const stats = {at: new Date().toISOString(), hosts: 0, first: null, last: null,
645
+ errors: {}, responded: [], found: 0};
646
+ candidates = await scan({companyId, hosts: this.scanHosts, stats});
647
+ stats.found = candidates.length;
648
+ this.lastScan = stats;
493
649
  via = 'scan';
494
650
  }
651
+ report.found = candidates.map((c) => c.url);
652
+ this.lastSearch = report;
495
653
  if (this.stopped) {
496
654
  return;
497
655
  }
@@ -532,6 +690,10 @@ class Finder {
532
690
  conflict: this.conflict,
533
691
  via: this.via,
534
692
  last_scan_at: this.lastScanAt ? new Date(this.lastScanAt).toISOString() : null,
693
+ searches: this.searches,
694
+ interfaces: interfaces(),
695
+ last_search: this.lastSearch,
696
+ last_scan: this.lastScan,
535
697
  };
536
698
  }
537
699
 
@@ -543,4 +705,5 @@ class Finder {
543
705
 
544
706
  const httpAnswers = () => httpAnswered;
545
707
 
546
- module.exports = {Responder, Finder, probe, verify, scan, whoami, httpAnswers, sign, interfaces, settings};
708
+ module.exports = {Responder, Finder, probe, verify, verifyWhy, scan, whoami, whoamiStatus, httpAnswers, sign,
709
+ interfaces, settings};
package/app/sync/index.js CHANGED
@@ -7,6 +7,7 @@ const {CloudSignal} = require('./signal');
7
7
  const {MasterBridge} = require('./bridge');
8
8
  const {lanHeaders} = require('./lan');
9
9
  const discovery = require('./discovery');
10
+ const netstats = require('../netstats');
10
11
  const lanDevices = require('./lanDevices');
11
12
  const lanFront = require('./lanFront');
12
13
  const {getConfig} = require('../config');
@@ -1125,8 +1126,8 @@ const startDiscovery = () => {
1125
1126
  };
1126
1127
 
1127
1128
  /** Ответ мастера на GET /local/whoami (ступени поиска ведомого). null — 404. */
1128
- const whoami = (nonce) => (config.role() === 'master' && state.started
1129
- ? discovery.whoami({nonce, httpPort: getConfig().port, identity: state.session})
1129
+ const whoami = (nonce, from = null) => (config.role() === 'master' && state.started
1130
+ ? discovery.whoami({nonce, httpPort: getConfig().port, identity: state.session, from})
1130
1131
  : null);
1131
1132
 
1132
1133
  /**
@@ -1441,7 +1442,7 @@ const resume = async ({code} = {}) => {
1441
1442
  const key = throttleKey(session.team_id, session.terminal_id);
1442
1443
  unlockThrottle.assertAllowed(key);
1443
1444
  const user = code
1444
- ? await findUnlockUser(state.store, Array.isArray(blob.users) ? blob.users : [], code, blob.pin_salt)
1445
+ ? await findUnlockUser(state.store, Array.isArray(blob.users) ? blob.users : [], code, blob.pin_salt, blob.terminal)
1445
1446
  : null;
1446
1447
  if (!user) {
1447
1448
  unlockThrottle.fail(key);
@@ -1525,6 +1526,10 @@ const status = () => {
1525
1526
 
1526
1527
  const result = {
1527
1528
  enabled,
1529
+ // Фронт деплоится раньше, чем pos-server доезжает до касс: по версии и списку событий
1530
+ // ядра он понимает, что этот Node умеет офлайн (лениво: ядро само требует app/sync).
1531
+ version: PACKAGE_VERSION,
1532
+ core_events: require('../core').offlineEvents(),
1528
1533
  mode: enabled ? state.mode : 'cloud',
1529
1534
  role: config.role(),
1530
1535
  session: Boolean(state.session),
@@ -1576,12 +1581,16 @@ const status = () => {
1576
1581
  ready: Boolean(state.signal && state.signal.relayReady),
1577
1582
  };
1578
1583
  result.discovery = state.responder
1579
- ? {port: discovery.settings().port, answering: Boolean(state.responder.socket), answered: state.responder.answered}
1584
+ ? {port: discovery.settings().port, answering: Boolean(state.responder.socket),
1585
+ answered: state.responder.answered, udp: state.responder.status(),
1586
+ whoami: discovery.whoamiStatus(), interfaces: discovery.interfaces()}
1580
1587
  : null;
1581
1588
  result.lan = config.isLanFrontEnabled()
1582
1589
  ? {urls: lanFront.urls(getConfig().port), front: lanFront.status(), devices: lanDevices.status()}
1583
1590
  : null;
1584
1591
  }
1592
+ // Слушает ли Node на 0.0.0.0 и доходят ли до него соединения из зала (app/netstats.js).
1593
+ result.net = netstats.snapshot();
1585
1594
  return result;
1586
1595
  };
1587
1596
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alizadefaig/pos-server",
3
- "version": "1.2.29",
3
+ "version": "1.2.31",
4
4
  "description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
5
5
  "homepage": "https://gitlab.com/faigalizade/pos-server#readme",
6
6
  "bugs": {
@@ -99,7 +99,7 @@ registerOfflineRoutes(router, '/api', {
99
99
  const key = throttleKey(context.teamId, context.terminalId);
100
100
  unlockThrottle.assertAllowed(key);
101
101
  const blob = readInit();
102
- const user = await findUnlockUser(db, Array.isArray(blob.users) ? blob.users : [], code, blob.pin_salt);
102
+ const user = await findUnlockUser(db, Array.isArray(blob.users) ? blob.users : [], code, blob.pin_salt, blob.terminal);
103
103
  if (!user) {
104
104
  unlockThrottle.fail(key);
105
105
  return null;
@@ -16,18 +16,40 @@ const appConfig = getConfig();
16
16
  let PROXY_TARGET = appConfig.apiUrl || 'http://localhost:8000';
17
17
  const getUsbPrinters = async () => {
18
18
  let result = [];
19
- const usbDevices = escpos.USB.findPrinter();
19
+ let usbDevices = [];
20
+ try {
21
+ usbDevices = escpos.USB.findPrinter();
22
+ } catch (e) {
23
+ console.warn('[printers] usb scan failed:', e.message);
24
+ return result;
25
+ }
20
26
  for (const usbDevice of usbDevices) {
21
- usbDevice.open((error) => {
22
- console.log(error)
23
- });
24
- result.push({
25
- type: 'USB',
26
- name: await getStringDescriptor(usbDevice, usbDevice.deviceDescriptor.iProduct),
27
- vendor_id: usbDevice.deviceDescriptor.idVendor,
28
- product_id: usbDevice.deviceDescriptor.idProduct
29
- });
30
- usbDevice.close();
27
+ const {idVendor, idProduct, iProduct} = usbDevice.deviceDescriptor;
28
+ const printer = {type: 'USB', name: null, vendor_id: idVendor, product_id: idProduct};
29
+ // На Windows libusb открывает только устройство на драйвере WinUSB. Принтер на родном
30
+ // драйвере (usbprint.sys / драйвер производителя) даёт LIBUSB_ERROR_NOT_SUPPORTED —
31
+ // раньше это исключение роняло весь скан в 500, и не было видно даже LAN-принтеров.
32
+ let opened = false;
33
+ try {
34
+ usbDevice.open();
35
+ opened = true;
36
+ printer.name = await getStringDescriptor(usbDevice, iProduct);
37
+ } catch (e) {
38
+ printer.error = e.message;
39
+ console.warn(`[printers] usb ${idVendor}:${idProduct} not opened:`, e.message);
40
+ } finally {
41
+ if (opened) {
42
+ try {
43
+ usbDevice.close();
44
+ } catch (e) {
45
+ }
46
+ }
47
+ }
48
+ if (!printer.name) {
49
+ const hex = (n) => Number(n).toString(16).padStart(4, '0');
50
+ printer.name = `USB ${hex(idVendor)}:${hex(idProduct)}`;
51
+ }
52
+ result.push(printer);
31
53
  }
32
54
 
33
55
  return result;
@@ -61,7 +61,7 @@ router.get('/local/status', (req, res) => {
61
61
 
62
62
  // Ведомый ищет мастера (app/sync/discovery.js): подписанный ответ на nonce, без ключа зала.
63
63
  router.get('/local/whoami', (req, res) => {
64
- const answer = isSyncEnabled() ? sync.whoami(req.query.nonce) : null;
64
+ const answer = isSyncEnabled() ? sync.whoami(req.query.nonce, String(req.socket.remoteAddress || '').replace(/^::ffff:/, '')) : null;
65
65
  if (!answer) {
66
66
  return res.status(404).json({success: false});
67
67
  }
@@ -17,7 +17,7 @@ const init = (services) => new InitService(services);
17
17
 
18
18
  registerOfflineRoutes(router, '/api', {
19
19
  /** AuthController::init. `user` в ответ не входит: токен кладётся в модель и не выводится. */
20
- 'app.init': [CLOUD_READ, async (services) => init(services).init()],
20
+ 'app.init': [CLOUD_READ, async (services, params, extra) => init(services).init(extra)],
21
21
 
22
22
  /** ProductController::get / OrderProductController::get — оба ProductService::allProducts. */
23
23
  'products.get': [CLOUD_READ, async (services) => init(services).products(services.context.teamId)],
package/server.js CHANGED
@@ -13,6 +13,7 @@ const {internalGuard, originGuard} = require("./app/security");
13
13
  const {isSyncEnabled} = require("./app/sync/config");
14
14
  const multer = require('multer');
15
15
  const {getLogger} = require("./app/logger/Log");
16
+ const netstats = require("./app/netstats");
16
17
 
17
18
 
18
19
 
@@ -69,6 +70,8 @@ const bodyParsers = [
69
70
  express.urlencoded({extended: true, limit: BODY_LIMIT}),
70
71
  upload.any(),
71
72
  ];
73
+ // Запросы из зала (не loopback) и их исход — для диагностики кассы (/local/status.net).
74
+ app.use(netstats.middleware);
72
75
  // Мастер раздаёт фронт телефонам зала (LAN_FRONT): страница и её файлы открыты без ключа и
73
76
  // токена — данных в них нет, а API за ними проверяется ниже. No-op без флага.
74
77
  app.use(require('./app/sync/lanFront').middleware());
@@ -178,6 +181,7 @@ const runExpress = (conf = null) => {
178
181
 
179
182
  // ==== Start Listen ====
180
183
  const hostname = appConfig.host || 'localhost';
184
+ netstats.attach(server, {host: hostname, port});
181
185
  server.listen(port, hostname, async () => {
182
186
  if (hostname === 'localhost') {
183
187
  console.log(`Server started on ${hostname} http://${hostname}:${port}`);