Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16048e2ce3 |
@@ -7,4 +7,5 @@
|
|||||||
|
|
||||||
<!-- AUTO-GENERATED, DO NOT EDIT -->
|
<!-- AUTO-GENERATED, DO NOT EDIT -->
|
||||||
<p>Comprehensive API reference generated from the repository root <code>openapi.yaml</code>.</p>
|
<p>Comprehensive API reference generated from the repository root <code>openapi.yaml</code>.</p>
|
||||||
|
<p>The edge broker's <code>/api/health</code> response additionally exposes a <code>lastActivityAt</code> field (ISO 8601 timestamp). It reports the most recent successful HTTP request handled by the broker container and defaults to the container's start time when no request has been processed yet.</p>
|
||||||
</topic>
|
</topic>
|
||||||
|
|||||||
@@ -405,6 +405,10 @@ def render_api_reference_topic() -> str:
|
|||||||
' title="API Reference" id="API-Reference">\n'
|
' title="API Reference" id="API-Reference">\n'
|
||||||
f"\n <!-- {AUTOGEN_NOTE} -->\n"
|
f"\n <!-- {AUTOGEN_NOTE} -->\n"
|
||||||
" <p>Comprehensive API reference generated from the repository root <code>openapi.yaml</code>.</p>\n"
|
" <p>Comprehensive API reference generated from the repository root <code>openapi.yaml</code>.</p>\n"
|
||||||
|
" <p>The edge broker's <code>/api/health</code> response additionally exposes a "
|
||||||
|
"<code>lastActivityAt</code> field (ISO 8601 timestamp). It reports the most recent "
|
||||||
|
"successful HTTP request handled by the broker container and defaults to the container's "
|
||||||
|
"start time when no request has been processed yet.</p>\n"
|
||||||
"</topic>\n"
|
"</topic>\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ export function createBrokerServer(options = {}) {
|
|||||||
const browserStreamSessions = new Map();
|
const browserStreamSessions = new Map();
|
||||||
const gatewayStreamSessions = new Map();
|
const gatewayStreamSessions = new Map();
|
||||||
const inflightGatewaySyncs = new Map();
|
const inflightGatewaySyncs = new Map();
|
||||||
|
const containerStartedAt = currentTimestamp();
|
||||||
|
let lastActivityAt = containerStartedAt;
|
||||||
|
|
||||||
const managerRequest = async (path, body = {}, method = "POST") => {
|
const managerRequest = async (path, body = {}, method = "POST") => {
|
||||||
if (!managerUrl) {
|
if (!managerUrl) {
|
||||||
@@ -480,6 +482,7 @@ export function createBrokerServer(options = {}) {
|
|||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url, "http://localhost");
|
const url = new URL(req.url, "http://localhost");
|
||||||
|
lastActivityAt = currentTimestamp();
|
||||||
if (req.method === "GET" && url.pathname === "/api/health") {
|
if (req.method === "GET" && url.pathname === "/api/health") {
|
||||||
jsonResponse(res, 200, {
|
jsonResponse(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -488,6 +491,7 @@ export function createBrokerServer(options = {}) {
|
|||||||
manager_url_configured: Boolean(managerUrl),
|
manager_url_configured: Boolean(managerUrl),
|
||||||
shared_secret_configured: Boolean(sharedSecret),
|
shared_secret_configured: Boolean(sharedSecret),
|
||||||
agents_connected: agents.size,
|
agents_connected: agents.size,
|
||||||
|
lastActivityAt,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1083,6 +1087,10 @@ export function createBrokerServer(options = {}) {
|
|||||||
pendingCommands,
|
pendingCommands,
|
||||||
managerUrl,
|
managerUrl,
|
||||||
authMode,
|
authMode,
|
||||||
|
containerStartedAt,
|
||||||
|
get lastActivityAt() {
|
||||||
|
return lastActivityAt;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ test("broker exposes health and shared-secret diagnostics", async () => {
|
|||||||
assert.equal(healthJson.auth_mode, "manager");
|
assert.equal(healthJson.auth_mode, "manager");
|
||||||
assert.equal(healthJson.manager_url_configured, true);
|
assert.equal(healthJson.manager_url_configured, true);
|
||||||
assert.equal(healthJson.shared_secret_configured, true);
|
assert.equal(healthJson.shared_secret_configured, true);
|
||||||
|
assert.equal(typeof healthJson.lastActivityAt, "string");
|
||||||
|
assert.match(healthJson.lastActivityAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
||||||
|
assert.ok(healthJson.lastActivityAt >= broker.state.containerStartedAt);
|
||||||
|
assert.equal(healthJson.lastActivityAt, broker.state.lastActivityAt);
|
||||||
|
|
||||||
const invalidSecretResponse = await fetch(`http://127.0.0.1:${port}/api/diagnostics/shared-secret`, {
|
const invalidSecretResponse = await fetch(`http://127.0.0.1:${port}/api/diagnostics/shared-secret`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -247,6 +251,41 @@ test("broker exposes health and shared-secret diagnostics", async () => {
|
|||||||
await broker.close();
|
await broker.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("broker updates lastActivityAt after each successful request", async () => {
|
||||||
|
const broker = createBrokerServer({ authMode: "manager", sharedSecret: "secret", managerUrl: "http://manager.test" });
|
||||||
|
const address = await broker.listen(0);
|
||||||
|
const port = address.port;
|
||||||
|
|
||||||
|
assert.equal(broker.state.lastActivityAt, broker.state.containerStartedAt);
|
||||||
|
|
||||||
|
const firstResponse = await fetch(`http://127.0.0.1:${port}/api/health`);
|
||||||
|
const firstJson = await firstResponse.json();
|
||||||
|
const firstActivityAt = broker.state.lastActivityAt;
|
||||||
|
|
||||||
|
assert.equal(typeof firstJson.lastActivityAt, "string");
|
||||||
|
assert.equal(firstJson.lastActivityAt, firstActivityAt);
|
||||||
|
assert.ok(firstActivityAt >= broker.state.containerStartedAt);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
|
||||||
|
await fetch(`http://127.0.0.1:${port}/api/diagnostics/shared-secret`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"x-edge-broker-secret": "secret",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.notEqual(broker.state.lastActivityAt, firstActivityAt);
|
||||||
|
assert.ok(broker.state.lastActivityAt > firstActivityAt);
|
||||||
|
|
||||||
|
const secondResponse = await fetch(`http://127.0.0.1:${port}/api/health`);
|
||||||
|
const secondJson = await secondResponse.json();
|
||||||
|
|
||||||
|
assert.equal(secondJson.lastActivityAt, broker.state.lastActivityAt);
|
||||||
|
|
||||||
|
await broker.close();
|
||||||
|
});
|
||||||
|
|
||||||
test("broker bridges browser shell sessions through the connected agent", async () => {
|
test("broker bridges browser shell sessions through the connected agent", async () => {
|
||||||
const closedSessions = [];
|
const closedSessions = [];
|
||||||
const broker = createBrokerServer({
|
const broker = createBrokerServer({
|
||||||
|
|||||||
@@ -1495,6 +1495,7 @@ class invoice_period_flag_service
|
|||||||
{
|
{
|
||||||
$product = (string)($params['product'] ?? 'Item');
|
$product = (string)($params['product'] ?? 'Item');
|
||||||
$expectedProduct = (string)($params['expected_product'] ?? 'expected product');
|
$expectedProduct = (string)($params['expected_product'] ?? 'expected product');
|
||||||
|
$washId = (string)($params['wash_id'] ?? '');
|
||||||
return match ($definitionKey) {
|
return match ($definitionKey) {
|
||||||
'price_mismatch' => "{$product} product price differs from expected.",
|
'price_mismatch' => "{$product} product price differs from expected.",
|
||||||
'customer_rule_restrict_addon_services' => "{$product} violates restricted addon services.",
|
'customer_rule_restrict_addon_services' => "{$product} violates restricted addon services.",
|
||||||
@@ -1514,7 +1515,9 @@ class invoice_period_flag_service
|
|||||||
'duplicate_vehicle_subscription_charge_same_month' => "Duplicate vehicle subscription charges exist in the same month.",
|
'duplicate_vehicle_subscription_charge_same_month' => "Duplicate vehicle subscription charges exist in the same month.",
|
||||||
'vehicle_subscription_type_mismatch' => "{$product} does not match the vehicle subscription type {$expectedProduct}.",
|
'vehicle_subscription_type_mismatch' => "{$product} does not match the vehicle subscription type {$expectedProduct}.",
|
||||||
'historical_primary_product_mismatch' => "{$product} differs from the registration number's usual product {$expectedProduct}.",
|
'historical_primary_product_mismatch' => "{$product} differs from the registration number's usual product {$expectedProduct}.",
|
||||||
'xlvask_missing_order_link' => "XL Vask wash is neither ignored nor linked to an order in the selected period.",
|
'xlvask_missing_order_link' => $washId === ''
|
||||||
|
? 'XL Vask wash is neither ignored nor linked to an order in the selected period.'
|
||||||
|
: "XL Vask wash {$washId} is neither ignored nor linked to an order in the selected period.",
|
||||||
default => "Automatically detected invoice-period issue.",
|
default => "Automatically detected invoice-period issue.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1541,7 +1544,8 @@ class invoice_period_flag_service
|
|||||||
['type' => 'text', 'text' => ' is attached without a wash certificate item.'],
|
['type' => 'text', 'text' => ' is attached without a wash certificate item.'],
|
||||||
],
|
],
|
||||||
'xlvask_missing_order_link' => [
|
'xlvask_missing_order_link' => [
|
||||||
['type' => 'xlvask_usage_log', 'text' => 'XL Vask wash'],
|
['type' => 'text', 'text' => 'XL Vask wash '],
|
||||||
|
['type' => 'xlvask_usage_log', 'text' => (string)($params['wash_id'] ?? '')],
|
||||||
['type' => 'text', 'text' => ' is neither ignored nor linked to an order in the selected period.'],
|
['type' => 'text', 'text' => ' is neither ignored nor linked to an order in the selected period.'],
|
||||||
],
|
],
|
||||||
default => [],
|
default => [],
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ it('builds interactive message parts for order and wash certificate warnings', f
|
|||||||
['type' => 'text', 'text' => ' is present without a wash certificate.'],
|
['type' => 'text', 'text' => ' is present without a wash certificate.'],
|
||||||
]);
|
]);
|
||||||
expect($xlVaskFlag['message_parts'])->toBe([
|
expect($xlVaskFlag['message_parts'])->toBe([
|
||||||
['type' => 'xlvask_usage_log', 'text' => 'XL Vask wash'],
|
['type' => 'text', 'text' => 'XL Vask wash '],
|
||||||
|
['type' => 'xlvask_usage_log', 'text' => 'wash-55'],
|
||||||
['type' => 'text', 'text' => ' is neither ignored nor linked to an order in the selected period.'],
|
['type' => 'text', 'text' => ' is neither ignored nor linked to an order in the selected period.'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user