Publish startup settlement ntfy notifications
All checks were successful
deploy / deploy (push) Successful in 1m1s

Proof: Targeted notification/history-writer tests pass; full npm test passes with 285 tests; operator dashboard bundle builds.

Assumptions: Startup outcome refresh may recompute settlements whose inventory movement occurred before process start, and notification_deliveries prevents duplicate ntfy posts for already delivered keys.

Still fake: Venue-native terminal fill ids and fee-complete realized PnL remain unavailable; ntfy remains an external utility service outside the unrip app deployment ownership.
This commit is contained in:
philipp 2026-06-12 15:14:14 +02:00
parent 5542a6f00b
commit 4a8766d4ae
3 changed files with 15 additions and 3 deletions

View file

@ -123,6 +123,7 @@ const rawQuoteTopics = [
config.kafkaTopicRawNearIntentsQuote, config.kafkaTopicRawNearIntentsQuote,
]; ];
const quoteLifecycleRetentionMaintenanceIntervalMs = 60 * 1000; const quoteLifecycleRetentionMaintenanceIntervalMs = 60 * 1000;
const historyWriterStartedAt = new Date().toISOString();
const liveEvidenceTopics = [ const liveEvidenceTopics = [
config.kafkaTopicNormSwapDemand, config.kafkaTopicNormSwapDemand,
config.kafkaTopicDecisionTradeDecision, config.kafkaTopicDecisionTradeDecision,
@ -219,10 +220,14 @@ const state = {
await refreshPortfolioMetrics().catch((error) => { await refreshPortfolioMetrics().catch((error) => {
state.metrics_error = serializeError(error); state.metrics_error = serializeError(error);
}); });
await refreshQuoteOutcomeAttributions().catch((error) => { await refreshQuoteOutcomeAttributions().then((records) => publishQuoteOutcomeNotifications(records, {
minObservedAt: historyWriterStartedAt,
})).catch((error) => {
state.quote_outcomes_error = serializeError(error); state.quote_outcomes_error = serializeError(error);
}); });
await refreshIntentRequestOutcomeAttributions().catch((error) => { await refreshIntentRequestOutcomeAttributions().then((records) => publishIntentRequestOutcomeNotifications(records, {
minObservedAt: historyWriterStartedAt,
})).catch((error) => {
state.intent_request_outcomes_error = serializeError(error); state.intent_request_outcomes_error = serializeError(error);
}); });
await refreshQuoteLifecycleRetentionState().catch((error) => { await refreshQuoteLifecycleRetentionState().catch((error) => {

View file

@ -43,3 +43,10 @@ test('history writer passes tracked assets into portfolio valuation', () => {
assert.match(source, /valuationAssets:\s*inputs\.valuationAssets \|\| \[\]/); assert.match(source, /valuationAssets:\s*inputs\.valuationAssets \|\| \[\]/);
assert.match(source, /inputs\.valuationAssets[\s\S]+asset\.priceId/); assert.match(source, /inputs\.valuationAssets[\s\S]+asset\.priceId/);
}); });
test('history writer startup outcome refresh publishes newly computed ntfy notifications', () => {
assert.match(source, /historyWriterStartedAt\s*=\s*new Date\(\)\.toISOString\(\)/);
assert.match(source, /refreshQuoteOutcomeAttributions\(\)\.then\(\(records\) => publishQuoteOutcomeNotifications\(records, \{\s*minObservedAt:\s*historyWriterStartedAt,\s*\}\)\)/);
assert.match(source, /refreshIntentRequestOutcomeAttributions\(\)\.then\(\(records\) => publishIntentRequestOutcomeNotifications\(records, \{\s*minObservedAt:\s*historyWriterStartedAt,\s*\}\)\)/);
assert.match(source, /outcomeNotificationObservedAtOrAfter/);
});

View file

@ -200,7 +200,7 @@ test('notification dispatcher publishes each durable notification key once', asy
assert.equal(finished.length, 1); assert.equal(finished.length, 1);
}); });
test('outcome recency filter keeps startup refresh from sending historical notifications', () => { test('observed timestamp recency filter rejects stale outcome observations', () => {
assert.equal(observedAtOrAfter('2026-04-16T10:00:00.000Z', '2026-04-16T09:59:59.000Z'), true); assert.equal(observedAtOrAfter('2026-04-16T10:00:00.000Z', '2026-04-16T09:59:59.000Z'), true);
assert.equal(observedAtOrAfter('2026-04-16T09:00:00.000Z', '2026-04-16T09:59:59.000Z'), false); assert.equal(observedAtOrAfter('2026-04-16T09:00:00.000Z', '2026-04-16T09:59:59.000Z'), false);
}); });