Compare commits
3 Commits
5e4cb5a3f8
...
f571328e0c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f571328e0c | ||
|
|
a717a05e8d | ||
|
|
c06a5e3413 |
13
.env.example
13
.env.example
@@ -51,4 +51,17 @@ OPENROUTER_PAID_MODEL=openai/gpt-6-luna:floor
|
|||||||
# BACKUP_HOURLY_RETENTION=50
|
# BACKUP_HOURLY_RETENTION=50
|
||||||
# BACKUP_DAILY_RETENTION=50
|
# BACKUP_DAILY_RETENTION=50
|
||||||
|
|
||||||
|
# Redis (Healthcheck Latency Metric)
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
REDIS_DB=0
|
||||||
|
REDIS_KEY=healthcheck:latency_ms
|
||||||
|
# REDIS_URL=redis://127.0.0.1:6379
|
||||||
|
|
||||||
|
# Health Monitor Service
|
||||||
|
# HEALTH_MONITOR_ENABLED=true
|
||||||
|
# HEALTH_MONITOR_INTERVAL_MS=60000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
|||||||
|
|
||||||
### Reusable types
|
### Reusable types
|
||||||
|
|
||||||
- `T1` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T19|null, shippings?: T19[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T9[] }`
|
- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T19|null, shippings?: T19[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T9[] }`
|
||||||
- `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T27, brand: T26|null, categoryIds: int[], photos: T24[], eans: str[], prices: T35[], suppliers: T11[] }`
|
- `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T27, brand: T26|null, categoryIds: int[], photos: T24[], eans: str[], prices: T35[], suppliers: T11[] }`
|
||||||
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T19[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }`
|
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T19[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }`
|
||||||
- `T4` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }`
|
- `T4` = `{ id: int, number: str, type: str, date: str, lines: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }`
|
||||||
- `T5` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }`
|
- `T5` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }`
|
||||||
- `T6` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T7|null }`
|
- `T6` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T7|null }`
|
||||||
- `T7` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }`
|
- `T7` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }`
|
||||||
- `T8` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T21[] }, thumb?: str|null }`
|
- `T8` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T21[] }, thumb?: str|null }`
|
||||||
@@ -403,9 +403,9 @@ _none_
|
|||||||
|
|
||||||
#### List deliveries — `GET /delivery`
|
#### List deliveries — `GET /delivery`
|
||||||
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
|
||||||
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T39 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T39 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Create delivery — `POST /delivery`
|
#### Create delivery — `POST /delivery`
|
||||||
- Params: `body: { externalNumber: str, date: str, lines: T23[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
|
- Params: `body: { externalNumber: str, date: str, supplierId: int, lines: T23[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
|
||||||
- Returns: `201 { delivery: T5 }`
|
- Returns: `201 { delivery: T5 }`
|
||||||
#### Delete delivery — `DELETE /delivery/{id}`
|
#### Delete delivery — `DELETE /delivery/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
@@ -619,7 +619,7 @@ _none_
|
|||||||
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
|
||||||
- Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
- Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Create order — `POST /order`
|
#### Create order — `POST /order`
|
||||||
- Params: `body: { number: str, date: str, lines: T23[], entityId?: int|null, externalNumber?: str|null }`
|
- Params: `body: { number: str, date: str, entityId: int, lines: T23[], externalNumber?: str|null }`
|
||||||
- Returns: `201 { order: T1 }`
|
- Returns: `201 { order: T1 }`
|
||||||
#### Delete order — `DELETE /order/{id}`
|
#### Delete order — `DELETE /order/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
|||||||
|
|
||||||
### Reusable types
|
### Reusable types
|
||||||
|
|
||||||
- `T1` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T14[], externalNumber?: str|null, createdAt?: str, shipping?: T13|null, shippings?: T13[], payment?: T24|null, payments?: T24[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T7[] }`
|
- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T14[], externalNumber?: str|null, createdAt?: str, shipping?: T13|null, shippings?: T13[], payment?: T24|null, payments?: T24[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T7[] }`
|
||||||
- `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T21, brand: T20|null, categoryIds: int[], photos: T18[], eans: str[], prices: T27[], suppliers: T8[] }`
|
- `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T21, brand: T20|null, categoryIds: int[], photos: T18[], eans: str[], prices: T27[], suppliers: T8[] }`
|
||||||
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T14[], shippings: T13[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T7[], state?: str }`
|
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T14[], shippings: T13[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T7[], state?: str }`
|
||||||
- `T4` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T14[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T7[] }`
|
- `T4` = `{ id: int, number: str, type: str, date: str, lines: T14[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T7[] }`
|
||||||
- `T5` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T14[], files: T7[], externalNumber?: str|null, createdAt?: str, ledger?: T31 }`
|
- `T5` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T14[], files: T7[], externalNumber?: str|null, createdAt?: str, ledger?: T31 }`
|
||||||
- `T6` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T15[] }, thumb?: str|null }`
|
- `T6` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T15[] }, thumb?: str|null }`
|
||||||
- `T7` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
|
- `T7` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
|
||||||
- `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T32|null }`
|
- `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T32|null }`
|
||||||
@@ -316,9 +316,9 @@ _none_
|
|||||||
|
|
||||||
#### List deliveries — `GET /delivery`
|
#### List deliveries — `GET /delivery`
|
||||||
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
|
||||||
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T31 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T31 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Create delivery — `POST /delivery`
|
#### Create delivery — `POST /delivery`
|
||||||
- Params: `body: { externalNumber: str, date: str, lines: T17[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
|
- Params: `body: { externalNumber: str, date: str, supplierId: int, lines: T17[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
|
||||||
- Returns: `201 { delivery: T5 }`
|
- Returns: `201 { delivery: T5 }`
|
||||||
#### Delete delivery — `DELETE /delivery/{id}`
|
#### Delete delivery — `DELETE /delivery/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
@@ -432,7 +432,7 @@ _none_
|
|||||||
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
|
||||||
- Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
- Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }`
|
||||||
#### Create order — `POST /order`
|
#### Create order — `POST /order`
|
||||||
- Params: `body: { number: str, date: str, lines: T17[], entityId?: int|null, externalNumber?: str|null }`
|
- Params: `body: { number: str, date: str, entityId: int, lines: T17[], externalNumber?: str|null }`
|
||||||
- Returns: `201 { order: T1 }`
|
- Returns: `201 { order: T1 }`
|
||||||
#### Delete order — `DELETE /order/{id}`
|
#### Delete order — `DELETE /order/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ The manifest `number` is allocated automatically via
|
|||||||
|-------|----------|------|-------------|
|
|-------|----------|------|-------------|
|
||||||
| `externalNumber` | yes | string | User-supplied reference number |
|
| `externalNumber` | yes | string | User-supplied reference number |
|
||||||
| `date` | yes | string | |
|
| `date` | yes | string | |
|
||||||
| `supplierId` | no | integer | |
|
| `supplierId` | yes | integer | |
|
||||||
| `lines` | yes | DeliveryLineInput[] | |
|
| `lines` | yes | DeliveryLineInput[] | |
|
||||||
| `files` | no | DeliveryFileInput[] | Attached documents (pdf or txt) stored with the delivery and returned in `files` on every delivery response. Thumbnails (AVIF) are generated server-side. |
|
| `files` | no | DeliveryFileInput[] | Attached documents (pdf or txt) stored with the delivery and returned in `files` on every delivery response. Thumbnails (AVIF) are generated server-side. |
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ Public. No cookie required.
|
|||||||
|
|
||||||
Database health check
|
Database health check
|
||||||
|
|
||||||
Verifies the database client can establish a connection.
|
Verifies database connectivity and transaction execution by updating a healthcheck sequence.
|
||||||
|
|
||||||
## Response `200`
|
## Response `200`
|
||||||
|
|
||||||
Database reachable
|
Database reachable and healthy
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Create order
|
|||||||
|
|
||||||
Creates a sales order with header and lines in one transaction.
|
Creates a sales order with header and lines in one transaction.
|
||||||
`date` is the document date (expected delivery).
|
`date` is the document date (expected delivery).
|
||||||
`entityId` must be a company customer (not Lieferanten), or null.
|
`entityId` must be a company customer (not Lieferanten).
|
||||||
Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`.
|
Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`.
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu`
|
|||||||
|-------|----------|------|-------------|
|
|-------|----------|------|-------------|
|
||||||
| `number` | yes | string | |
|
| `number` | yes | string | |
|
||||||
| `date` | yes | string | Document date / expected delivery |
|
| `date` | yes | string | Document date / expected delivery |
|
||||||
| `entityId` | no | integer | Customer entity (manifest.entity_id) |
|
| `entityId` | yes | integer | Customer entity (manifest.entity_id) |
|
||||||
| `externalNumber` | no | string | External customer reference number (manifest.external_number) |
|
| `externalNumber` | no | string | External customer reference number (manifest.external_number) |
|
||||||
| `lines` | yes | ManifestLineInput[] | |
|
| `lines` | yes | ManifestLineInput[] | |
|
||||||
|
|
||||||
|
|||||||
65
package-lock.json
generated
65
package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"google-auth-library": "^11.3.1",
|
"google-auth-library": "^11.3.1",
|
||||||
"ink": "^8.0.0",
|
"ink": "^8.0.0",
|
||||||
"ink-text-input": "^6.0.0",
|
"ink-text-input": "^6.0.0",
|
||||||
|
"ioredis": "^6.0.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"jszip": "^3.10.2",
|
"jszip": "^3.10.2",
|
||||||
"libdashstore": "file:../dashdbschema",
|
"libdashstore": "file:../dashdbschema",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"dotenv": "^18.0.8",
|
"dotenv": "^18.0.8",
|
||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"sharp": "^0.35.5",
|
"sharp": "^0.35.5",
|
||||||
|
"skelehash-wasm": "^0.1.0",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -70,11 +72,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../llmorbiter": {
|
"../llmorbiter": {
|
||||||
"name": "llmorbiter",
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^18.0.5"
|
"dotenv": "^18.0.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
@@ -769,6 +770,12 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ioredis/commands": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
|
||||||
@@ -1849,6 +1856,15 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cluster-key-slot": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/code-excerpt": {
|
"node_modules/code-excerpt": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
|
||||||
@@ -2198,6 +2214,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/denque": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -3460,6 +3485,27 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ioredis": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ioredis/commands": "2.0.0",
|
||||||
|
"cluster-key-slot": "1.1.1",
|
||||||
|
"debug": "4.4.3",
|
||||||
|
"denque": "2.1.0",
|
||||||
|
"redis-errors": "1.2.0",
|
||||||
|
"standard-as-callback": "2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ioredis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
"version": "10.7.4",
|
"version": "10.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz",
|
||||||
@@ -5125,6 +5171,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redis-errors": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect-metadata": {
|
"node_modules/reflect-metadata": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||||
@@ -5717,6 +5772,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/standard-as-callback": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"google-auth-library": "^11.3.1",
|
"google-auth-library": "^11.3.1",
|
||||||
"ink": "^8.0.0",
|
"ink": "^8.0.0",
|
||||||
"ink-text-input": "^6.0.0",
|
"ink-text-input": "^6.0.0",
|
||||||
|
"ioredis": "^6.0.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"jszip": "^3.10.2",
|
"jszip": "^3.10.2",
|
||||||
"libdashstore": "file:../dashdbschema",
|
"libdashstore": "file:../dashdbschema",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { createLogger } from '../util/logger.js';
|
|||||||
import v1Router from '../v1/index.js';
|
import v1Router from '../v1/index.js';
|
||||||
import { startBackendMonitor, stopBackendMonitor, isDevMode } from '../backendmonitor/server.js';
|
import { startBackendMonitor, stopBackendMonitor, isDevMode } from '../backendmonitor/server.js';
|
||||||
import { startBackupService, stopBackupService } from '../services/backupService.js';
|
import { startBackupService, stopBackupService } from '../services/backupService.js';
|
||||||
|
import { startHealthMonitorService, stopHealthMonitorService } from '../services/healthMonitorService.js';
|
||||||
|
|
||||||
const apiLog = createLogger('api');
|
const apiLog = createLogger('api');
|
||||||
|
|
||||||
@@ -252,6 +253,12 @@ export async function startApi() {
|
|||||||
apiLog.error('Failed to start Backup Service:', err.message);
|
apiLog.error('Failed to start Backup Service:', err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startHealthMonitorService();
|
||||||
|
} catch (err) {
|
||||||
|
apiLog.error('Failed to start Health Monitor Service:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -275,6 +282,7 @@ export async function stopApi() {
|
|||||||
// Same for the pooled sandbox threads that run `javascript_execute` / applets.
|
// Same for the pooled sandbox threads that run `javascript_execute` / applets.
|
||||||
await shutdownJavascriptSandboxWorkers();
|
await shutdownJavascriptSandboxWorkers();
|
||||||
await stopBackupService();
|
await stopBackupService();
|
||||||
|
await stopHealthMonitorService();
|
||||||
const closers = [];
|
const closers = [];
|
||||||
if (server) {
|
if (server) {
|
||||||
const s = server;
|
const s = server;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function respondWithAuthPayload(res, req, status, body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const authMiddleware = async (req, res, next) => {
|
export const authMiddleware = async (req, res, next) => {
|
||||||
if (req.path === '/' || req.path === '/api/auth/login' || req.path === '/api/auth/logout' || req.path === '/api/health' || req.path === '/api/openapi.json') {
|
if (req.path === '/' || req.path === '/api/auth/login' || req.path === '/api/auth/logout' || req.path === '/api/health' || req.path === '/health' || req.path === '/api/openapi.json') {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -746,7 +746,6 @@
|
|||||||
* enum: [delivery]
|
* enum: [delivery]
|
||||||
* supplierId:
|
* supplierId:
|
||||||
* type: integer
|
* type: integer
|
||||||
* nullable: true
|
|
||||||
* description: manifest.entity_id
|
* description: manifest.entity_id
|
||||||
* externalNumber:
|
* externalNumber:
|
||||||
* type: string
|
* type: string
|
||||||
@@ -980,7 +979,7 @@
|
|||||||
* nullable: true
|
* nullable: true
|
||||||
* QuoteSummary:
|
* QuoteSummary:
|
||||||
* type: object
|
* type: object
|
||||||
* required: [id, number, type, entityId, date]
|
* required: [id, number, type, date]
|
||||||
* properties:
|
* properties:
|
||||||
* id:
|
* id:
|
||||||
* type: integer
|
* type: integer
|
||||||
@@ -1044,7 +1043,6 @@
|
|||||||
* enum: [order]
|
* enum: [order]
|
||||||
* entityId:
|
* entityId:
|
||||||
* type: integer
|
* type: integer
|
||||||
* nullable: true
|
|
||||||
* description: Customer (manifest.entity_id)
|
* description: Customer (manifest.entity_id)
|
||||||
* externalNumber:
|
* externalNumber:
|
||||||
* type: string
|
* type: string
|
||||||
|
|||||||
@@ -169,8 +169,10 @@ function deliveryErrorStatus(error) {
|
|||||||
if (error.message === 'File not found') return 404;
|
if (error.message === 'File not found') return 404;
|
||||||
if (error.message === 'Supplier not found') return 404;
|
if (error.message === 'Supplier not found') return 404;
|
||||||
if (error.message === 'Warehouse not found') return 404;
|
if (error.message === 'Warehouse not found') return 404;
|
||||||
|
if (error.message === 'No supplier selected') return 400;
|
||||||
if (error.message === 'Invalid supplierId') return 400;
|
if (error.message === 'Invalid supplierId') return 400;
|
||||||
if (error.message === 'Article not found') return 404;
|
if (error.message === 'Article not found') return 404;
|
||||||
|
if (error.message === 'Delivery number already exists') return 409;
|
||||||
if (error.message === 'Invalid delivery') return 400;
|
if (error.message === 'Invalid delivery') return 400;
|
||||||
if (error.message === 'Invalid lines') return 400;
|
if (error.message === 'Invalid lines') return 400;
|
||||||
if (error.message === 'Delivery has no lines') return 400;
|
if (error.message === 'Delivery has no lines') return 400;
|
||||||
@@ -305,7 +307,7 @@ export function register(app, basePath = '') {
|
|||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* type: object
|
||||||
* required: [externalNumber, date, lines]
|
* required: [externalNumber, date, supplierId, lines]
|
||||||
* properties:
|
* properties:
|
||||||
* externalNumber:
|
* externalNumber:
|
||||||
* type: string
|
* type: string
|
||||||
@@ -315,7 +317,6 @@ export function register(app, basePath = '') {
|
|||||||
* format: date-time
|
* format: date-time
|
||||||
* supplierId:
|
* supplierId:
|
||||||
* type: integer
|
* type: integer
|
||||||
* nullable: true
|
|
||||||
* lines:
|
* lines:
|
||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
@@ -497,6 +498,9 @@ export function register(app, basePath = '') {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { externalNumber, date, createdAt, supplierId, lines, files } = parseDeliveryPayload(req);
|
const { externalNumber, date, createdAt, supplierId, lines, files } = parseDeliveryPayload(req);
|
||||||
|
if (supplierId === undefined || supplierId === null) {
|
||||||
|
return res.status(400).json({ error: 'No supplier selected' });
|
||||||
|
}
|
||||||
logDeliveryFiles(files, 'create');
|
logDeliveryFiles(files, 'create');
|
||||||
const delivery = await db.create(req.user.id, {
|
const delivery = await db.create(req.user.id, {
|
||||||
externalNumber,
|
externalNumber,
|
||||||
@@ -795,6 +799,9 @@ export function register(app, basePath = '') {
|
|||||||
patch.createdAt = parsed.createdAt;
|
patch.createdAt = parsed.createdAt;
|
||||||
}
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(parsed.payload, 'supplierId')) {
|
if (Object.prototype.hasOwnProperty.call(parsed.payload, 'supplierId')) {
|
||||||
|
if (parsed.supplierId === null) {
|
||||||
|
return res.status(400).json({ error: 'No supplier selected' });
|
||||||
|
}
|
||||||
patch.supplierId = parsed.supplierId;
|
patch.supplierId = parsed.supplierId;
|
||||||
}
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(parsed.payload, 'lines')) {
|
if (Object.prototype.hasOwnProperty.call(parsed.payload, 'lines')) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ensureConnection } from '../../db/store.js';
|
import { getStore } from '../../db/store.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a database connectivity health check route.
|
* Registers a database connectivity health check route.
|
||||||
@@ -12,12 +12,12 @@ export function register(app, basePath = '') {
|
|||||||
* get:
|
* get:
|
||||||
* tags: [Health]
|
* tags: [Health]
|
||||||
* summary: Database health check
|
* summary: Database health check
|
||||||
* description: Verifies the database client can establish a connection.
|
* description: Verifies database connectivity and transaction execution by updating a healthcheck sequence.
|
||||||
* security: []
|
* security: []
|
||||||
* x-markdown: health.md
|
* x-markdown: health.md
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Database reachable
|
* description: Database reachable and healthy
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
@@ -41,14 +41,20 @@ export function register(app, basePath = '') {
|
|||||||
*/
|
*/
|
||||||
// #region GET /health
|
// #region GET /health
|
||||||
|
|
||||||
app.get(`${basePath}/health`, async (_request, response) => {
|
const healthHandler = async (_request, response) => {
|
||||||
try {
|
try {
|
||||||
await ensureConnection();
|
const store = await getStore();
|
||||||
|
await store.healthcheck.check();
|
||||||
response.status(200).json({ ok: true });
|
response.status(200).json({ ok: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
response.status(503).json({ ok: false, error: error.message });
|
response.status(503).json({ ok: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
app.get(`${basePath}/health`, healthHandler);
|
||||||
|
if (basePath) {
|
||||||
|
app.get('/health', healthHandler);
|
||||||
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ function orderErrorStatus(error) {
|
|||||||
if (error.message === 'Attachment not found') return 404;
|
if (error.message === 'Attachment not found') return 404;
|
||||||
if (error.message === 'Entity not found') return 404;
|
if (error.message === 'Entity not found') return 404;
|
||||||
if (error.message === 'Article not found') return 404;
|
if (error.message === 'Article not found') return 404;
|
||||||
|
if (error.message === 'No entity selected') return 400;
|
||||||
|
if (error.message === 'Order number already exists') return 409;
|
||||||
if (error.message === 'Invalid entityId') return 400;
|
if (error.message === 'Invalid entityId') return 400;
|
||||||
if (error.message === 'Invalid order') return 400;
|
if (error.message === 'Invalid order') return 400;
|
||||||
if (error.message === 'Invalid lines') return 400;
|
if (error.message === 'Invalid lines') return 400;
|
||||||
@@ -148,7 +150,7 @@ export function register(app, basePath = '') {
|
|||||||
* description: |
|
* description: |
|
||||||
* Creates a sales order with header and lines in one transaction.
|
* Creates a sales order with header and lines in one transaction.
|
||||||
* `date` is the document date (expected delivery).
|
* `date` is the document date (expected delivery).
|
||||||
* `entityId` must be a company customer (not Lieferanten), or null.
|
* `entityId` must be a company customer (not Lieferanten).
|
||||||
* Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`.
|
* Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`.
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
@@ -159,7 +161,7 @@ export function register(app, basePath = '') {
|
|||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* type: object
|
||||||
* required: [number, date, lines]
|
* required: [number, date, entityId, lines]
|
||||||
* properties:
|
* properties:
|
||||||
* number:
|
* number:
|
||||||
* type: string
|
* type: string
|
||||||
@@ -169,7 +171,6 @@ export function register(app, basePath = '') {
|
|||||||
* description: Document date / expected delivery
|
* description: Document date / expected delivery
|
||||||
* entityId:
|
* entityId:
|
||||||
* type: integer
|
* type: integer
|
||||||
* nullable: true
|
|
||||||
* description: Customer entity (manifest.entity_id)
|
* description: Customer entity (manifest.entity_id)
|
||||||
* externalNumber:
|
* externalNumber:
|
||||||
* type: string
|
* type: string
|
||||||
@@ -297,7 +298,10 @@ export function register(app, basePath = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { number, date, createdAt, entityId, externalNumber, lines } = req.body ?? {};
|
const { number, date, createdAt, entityId, externalNumber, lines } = req.body ?? {};
|
||||||
if (entityId !== undefined && entityId !== null && (typeof entityId !== 'number' || !Number.isInteger(entityId) || entityId <= 0)) {
|
if (entityId === undefined || entityId === null) {
|
||||||
|
return res.status(400).json({ error: 'No entity selected' });
|
||||||
|
}
|
||||||
|
if (typeof entityId !== 'number' || !Number.isInteger(entityId) || entityId <= 0) {
|
||||||
return res.status(400).json({ error: 'Invalid entityId' });
|
return res.status(400).json({ error: 'Invalid entityId' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +542,10 @@ export function register(app, basePath = '') {
|
|||||||
patch.createdAt = body.createdAt;
|
patch.createdAt = body.createdAt;
|
||||||
}
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(body, 'entityId')) {
|
if (Object.prototype.hasOwnProperty.call(body, 'entityId')) {
|
||||||
if (body.entityId !== null && (typeof body.entityId !== 'number' || !Number.isInteger(body.entityId) || body.entityId <= 0)) {
|
if (body.entityId === null) {
|
||||||
|
return res.status(400).json({ error: 'No entity selected' });
|
||||||
|
}
|
||||||
|
if (typeof body.entityId !== 'number' || !Number.isInteger(body.entityId) || body.entityId <= 0) {
|
||||||
return res.status(400).json({ error: 'Invalid entityId' });
|
return res.status(400).json({ error: 'Invalid entityId' });
|
||||||
}
|
}
|
||||||
patch.entityId = body.entityId;
|
patch.entityId = body.entityId;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function quoteErrorStatus(error) {
|
|||||||
if (error.message === 'Entity not found') return 404;
|
if (error.message === 'Entity not found') return 404;
|
||||||
if (error.message === 'Article not found') return 404;
|
if (error.message === 'Article not found') return 404;
|
||||||
if (error.message === 'Invalid entityId') return 400;
|
if (error.message === 'Invalid entityId') return 400;
|
||||||
|
if (error.message === 'Quote number already exists') return 409;
|
||||||
if (error.message === 'Invalid quote') return 400;
|
if (error.message === 'Invalid quote') return 400;
|
||||||
if (error.message === 'Invalid order') return 400;
|
if (error.message === 'Invalid order') return 400;
|
||||||
if (error.message === 'Invalid lines') return 400;
|
if (error.message === 'Invalid lines') return 400;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ function shipmentErrorStatus(error) {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
error.message?.startsWith('Shipment amount exceeds') ||
|
error.message?.startsWith('Shipment amount exceeds') ||
|
||||||
|
error.message === 'Shipment number already exists' ||
|
||||||
error.message?.includes('cannot be deleted') ||
|
error.message?.includes('cannot be deleted') ||
|
||||||
error.message?.includes('cannot be undone') ||
|
error.message?.includes('cannot be undone') ||
|
||||||
error.message?.includes('already shipped')
|
error.message?.includes('already shipped')
|
||||||
|
|||||||
@@ -52,4 +52,16 @@ export default {
|
|||||||
hourlyRetentionCount: Number(process.env.BACKUP_HOURLY_RETENTION) || 50,
|
hourlyRetentionCount: Number(process.env.BACKUP_HOURLY_RETENTION) || 50,
|
||||||
dailyRetentionCount: Number(process.env.BACKUP_DAILY_RETENTION) || 50,
|
dailyRetentionCount: Number(process.env.BACKUP_DAILY_RETENTION) || 50,
|
||||||
},
|
},
|
||||||
|
redis: {
|
||||||
|
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||||
|
port: Number(process.env.REDIS_PORT) || 6379,
|
||||||
|
password: process.env.REDIS_PASSWORD || undefined,
|
||||||
|
db: Number(process.env.REDIS_DB) || 0,
|
||||||
|
url: process.env.REDIS_URL || undefined,
|
||||||
|
key: process.env.REDIS_KEY || process.env.REDIS_HEALTH_KEY || 'healthcheck:latency_ms',
|
||||||
|
},
|
||||||
|
healthMonitor: {
|
||||||
|
enabled: process.env.HEALTH_MONITOR_ENABLED !== 'false',
|
||||||
|
intervalMs: Number(process.env.HEALTH_MONITOR_INTERVAL_MS) || 60_000,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const STORE_NAMESPACES = [
|
|||||||
'articlelist',
|
'articlelist',
|
||||||
'optionslist',
|
'optionslist',
|
||||||
'mcp',
|
'mcp',
|
||||||
|
'healthcheck',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
181
src/services/healthMonitorService.js
Normal file
181
src/services/healthMonitorService.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
// #region Imports & Configuration
|
||||||
|
import http from 'node:http';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import env from '../config/env.js';
|
||||||
|
import { createLogger } from '../util/logger.js';
|
||||||
|
|
||||||
|
const healthLog = createLogger('health-monitor');
|
||||||
|
|
||||||
|
let scheduledTimer = null;
|
||||||
|
let scheduledInterval = null;
|
||||||
|
let isServiceRunning = false;
|
||||||
|
let redisClient = null;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Redis Client Management
|
||||||
|
// MARK: resolves or creates the lazy Redis client
|
||||||
|
export function getRedisClient() {
|
||||||
|
if (!redisClient) {
|
||||||
|
const opts = env.redis.url
|
||||||
|
? env.redis.url
|
||||||
|
: {
|
||||||
|
host: env.redis.host,
|
||||||
|
port: env.redis.port,
|
||||||
|
password: env.redis.password || undefined,
|
||||||
|
db: env.redis.db || 0,
|
||||||
|
lazyConnect: true,
|
||||||
|
enableReadyCheck: false,
|
||||||
|
maxRetriesPerRequest: 1,
|
||||||
|
retryStrategy(times) {
|
||||||
|
return Math.min(times * 1000, 30000);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
redisClient = new Redis(opts);
|
||||||
|
redisClient.on('error', (err) => {
|
||||||
|
healthLog.warn(`Redis connection error: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return redisClient;
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region TCP Health API Selftest
|
||||||
|
// MARK: executes an HTTP GET /health call over TCP loopback and measures response time
|
||||||
|
export function performHealthSelftest() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const start = performance.now();
|
||||||
|
const req = http.request(
|
||||||
|
{
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: env.port,
|
||||||
|
path: '/health',
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Dashback-HealthMonitor/1.0',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
resolve({ success: true, durationMs, statusCode: res.statusCode, body: data });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, durationMs, statusCode: res.statusCode, body: data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
||||||
|
reject({ error: err, durationMs });
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy(new Error('TCP connection or request timed out'));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: runs one selftest cycle and writes duration to Redis
|
||||||
|
async function executeHealthCheckCycle() {
|
||||||
|
try {
|
||||||
|
const result = await performHealthSelftest();
|
||||||
|
if (result.success) {
|
||||||
|
try {
|
||||||
|
const redis = getRedisClient();
|
||||||
|
await redis.set(env.redis.key, String(result.durationMs));
|
||||||
|
healthLog.debug(
|
||||||
|
`Health selftest passed in ${result.durationMs}ms; recorded in redis key "${env.redis.key}"`
|
||||||
|
);
|
||||||
|
} catch (redisErr) {
|
||||||
|
healthLog.warn(`Failed to write latency to Redis: ${redisErr.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
healthLog.warn(
|
||||||
|
`Health selftest returned HTTP ${result.statusCode} in ${result.durationMs}ms`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.error?.message || err?.message || String(err);
|
||||||
|
healthLog.warn(`Health selftest failed: ${msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Service Lifecycle
|
||||||
|
// MARK: starts the periodic health check selftest service
|
||||||
|
export function startHealthMonitorService() {
|
||||||
|
if (isServiceRunning) return;
|
||||||
|
if (env.healthMonitor?.enabled === false) {
|
||||||
|
healthLog.info('Health monitor service is disabled by configuration.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isServiceRunning = true;
|
||||||
|
const intervalMs = env.healthMonitor?.intervalMs || 60_000;
|
||||||
|
|
||||||
|
// Run initial selftest after 1s warmup
|
||||||
|
scheduledTimer = setTimeout(() => {
|
||||||
|
scheduledTimer = null;
|
||||||
|
executeHealthCheckCycle().catch((err) => {
|
||||||
|
healthLog.error(`Initial health selftest error: ${err.message}`);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Schedule recurring selftest every minute
|
||||||
|
scheduledInterval = setInterval(() => {
|
||||||
|
executeHealthCheckCycle().catch((err) => {
|
||||||
|
healthLog.error(`Recurring health selftest error: ${err.message}`);
|
||||||
|
});
|
||||||
|
}, intervalMs);
|
||||||
|
|
||||||
|
healthLog.info(`Health monitor service started (selftest every ${Math.round(intervalMs / 1000)}s via TCP).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: stops the health check service and closes Redis client
|
||||||
|
export async function stopHealthMonitorService() {
|
||||||
|
isServiceRunning = false;
|
||||||
|
if (scheduledTimer) {
|
||||||
|
clearTimeout(scheduledTimer);
|
||||||
|
scheduledTimer = null;
|
||||||
|
}
|
||||||
|
if (scheduledInterval) {
|
||||||
|
clearInterval(scheduledInterval);
|
||||||
|
scheduledInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redisClient) {
|
||||||
|
try {
|
||||||
|
await redisClient.quit();
|
||||||
|
} catch {
|
||||||
|
redisClient.disconnect();
|
||||||
|
} finally {
|
||||||
|
redisClient = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
healthLog.info('Health monitor service stopped.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: returns health monitor status
|
||||||
|
export function getHealthMonitorStatus() {
|
||||||
|
return {
|
||||||
|
enabled: env.healthMonitor?.enabled ?? true,
|
||||||
|
isRunning: isServiceRunning,
|
||||||
|
intervalMs: env.healthMonitor?.intervalMs || 60_000,
|
||||||
|
redisKey: env.redis.key,
|
||||||
|
redisHost: env.redis.host,
|
||||||
|
redisPort: env.redis.port,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
@@ -25,8 +25,8 @@ export const SYSTEM_PROMPT_ROUTE_MATRIX = [
|
|||||||
'- Attributes: /artikel/merkmale -> attributename, attributevalue, propertyname',
|
'- Attributes: /artikel/merkmale -> attributename, attributevalue, propertyname',
|
||||||
'- Categories: /kategorien | /kategorien/${slug1}/${slug2} -> category, article',
|
'- Categories: /kategorien | /kategorien/${slug1}/${slug2} -> category, article',
|
||||||
'- Deliveries: /lieferung | /lieferung/neu | /lieferung/${deliveryId} | /lieferung/lieferanten -> delivery, supplier, article, warehouse, stock, ledger',
|
'- Deliveries: /lieferung | /lieferung/neu | /lieferung/${deliveryId} | /lieferung/lieferanten -> delivery, supplier, article, warehouse, stock, ledger',
|
||||||
'- Quotes: /auftrag | /auftrag/neu | /auftrag/${quoteId} -> quote, entity, article',
|
'- Orders: /auftrag | /auftrag/bestellung/neu | /auftrag/bestellung/${orderId} | /auftrag/ausliefern | /auftrag/zahlungen | /auftrag/versandarten -> order, entity, account, payment, shipping, package',
|
||||||
'- Orders: /auftrag/bestellung | /auftrag/bestellung/neu | /auftrag/bestellung/${orderId} -> order, entity, account, payment, shipping, package',
|
'- Quotes: /auftrag/angebot | /auftrag/neuesangebot | /auftrag/angebot/${quoteId} -> quote, entity, article',
|
||||||
'- Customers: /kunde | /kunde/neu | /kunde/${entityId} | /kunde/gruppen -> entity, customergroup, number',
|
'- Customers: /kunde | /kunde/neu | /kunde/${entityId} | /kunde/gruppen -> entity, customergroup, number',
|
||||||
'- History & Stats: /infos/articlehistory | /infos/globalhistory | /infos/statistics -> article, company, order, delivery, ledger',
|
'- History & Stats: /infos/articlehistory | /infos/globalhistory | /infos/statistics -> article, company, order, delivery, ledger',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
@@ -263,7 +263,7 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
set_user_route: {
|
set_user_route: {
|
||||||
route: 'Frontend route from Route Matrix (e.g. "/artikel/ART-10042", "/auftrag/1001").',
|
route: 'Frontend route from Route Matrix (e.g. "/artikel/ART-10042", "/auftrag/bestellung/1001", "/auftrag/angebot/1001").',
|
||||||
},
|
},
|
||||||
|
|
||||||
subagent: {
|
subagent: {
|
||||||
|
|||||||
Reference in New Issue
Block a user