feat: Discord ops activity feed for key user flows#458
Merged
Conversation
Add a fire-and-forget ops event feed that posts color-coded Discord embeds to OPS_DISCORD_WEBHOOK_URL as users move through key flows, so the ops channel reads as a funnel (otp-sent -> otp-verified -> promoted share an account/user id). - src/services/alerts/ops-events.ts: notifyOpsEvent() mirroring alertBridge (returns void, never throws, no-op when env unset, 3s timeout). Sequential in-process FIFO queue, honors Discord 429 retry_after once, drops oldest past 50 queued and emits one "N events dropped" summary embed. Identity is always masked (phone -> +1876...00, email -> j***@gmail.com) and long ids truncated to 8 chars. Env label from NETWORK. - config: OPS_DISCORD_WEBHOOK_URL optional zod env var + export. - Hooks (all post-outcome, never alter control flow): verification: requestPhoneCodeForAuthedUser, verifyPhone, addEmailToIdentity, verifyEmail, upgradeAccountFromDeviceToPhone upgrade: createUpgradeRequest, updateAccountLevel cashout: CashoutManager.executeCashout (initiated/succeeded), ValidOffer.execute (per-step failures: draftCashout / payInvoice / submitCashout) deposit: bridge depositHandler, IBEX on-receive (ln + onchain) transfer: bridge transferHandler, intraledger wallet-id sends, payInvoiceByWalletId - Tests: masking/truncation edge cases, embed shape/colors, no-op when unset, 429 retry, queue-overflow drop summary, plus success/failure call-site coverage for every hooked app function. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7
Hook loginDeviceUpgradeWithPhone into the ops event feed: the two phone-collision returns (device balance must be swept / zero-balance cannot-upgrade) emit "upgrade-collision" failed events with the error class name and meta.deviceHasBalance distinguishing the variant, so ops sees every user silently hitting the collision wall. Happy-path error returns (invalid OTP, onboarding check, kratos schema upgrade, account promotion failure) emit "upgrade-failed" via the same small helper. Rate-limit failures are intentionally not notified (noise). Control flow is unchanged. Tests cover both collision variants, upgrade-failed on invalid OTP and kratos failure, and that the successful path adds no extra events beyond the promotion emitted by upgradeAccountFromDeviceToPhone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7
…amount lightning hooks - Cashout partial failure no longer reads as success: ValidOffer.execute surfaces the ERPNext submit outcome via InitiatedCashout.erpSubmitted (default true; GraphQL output unchanged) and CashoutManager gates its "succeeded" emit on it — exactly one truthful terminal event per cashout (the failed(submitCashout) event comes from execute()). - Cashout amounts now render in display units via toDisplayAmount() ($95.40, not "9540 USD"): USDAmount/JMDAmount.asDollars(), USDTAmount.asNumber(2). - ValidOffer.from() validation failure now emits a terminal failed(validation) event so an "initiated" emit can never be left permanently amber. - payNoAmountInvoiceByWalletIdForBtcWallet/ForUsdWallet hooked like payInvoiceByWalletId via a shared notifyLightningSendResult helper; intraledger notifications moved into the exported wrappers so their wallet-currency validation failures are captured too. - Transfer events carry display amounts where the args have them (intraledger + no-amount lightning: cents -> USD dollars, sats for BTC); invoice-amount lightning sends have no amount arg and stay amountless. - Failed transfer events distinguish meta.reason "error-return" vs "status-failure" (Ibex status 3 / PaymentSendStatus.Failure). Tests updated/added: erpSubmitted flag on both execute() outcomes, no "succeeded" after partial failure, from()-validation terminal event, display-amount assertions across cashout/intraledger/lightning, no-amount wrapper hooks (USD success + BTC validation failure), wrapper-validation intraledger failure, reason distinction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7
yarn audit now flags a critical DoS advisory in tar (<7.5.19, npmjs.com/advisories/1123940, pulled in via @apollo/rover > binary-install), which fails the CI Audit gate for every PR. Bump the existing **/tar resolution from 7.5.11 to the patched 7.5.19. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7
bobodread876
approved these changes
Jul 23, 2026
islandbitcoin
pushed a commit
to lnflash/charts
that referenced
this pull request
Jul 23, 2026
…ents (chart 3.2.42) Adds OPS_DISCORD_WEBHOOK_URL and ALERT_DISCORD_WEBHOOK_URL to the api, ibex-webhook, and bridge-webhook deployments via secretKeyRef against a new 'discord-webhooks' secret (keys: ops-webhook-url, alert-webhook-url), optional: true so environments without the secret still schedule pods. Follows the *ExistingSecret convention; galoy-secrets.yaml renders the secret for secrets.create=true installs, omitting keys whose values are empty (an empty-string env var would fail the backend's zod .url() validation and crash pods at boot — caught by the dev smoketest). Backend consumer: lnflash/flash#458 (ops activity feed + alerts module). Real deployments provision the secret out-of-band (secrets.create=false). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7
islandbitcoin
added a commit
to lnflash/charts
that referenced
this pull request
Jul 23, 2026
…ents (chart 3.2.42) (#240) Adds OPS_DISCORD_WEBHOOK_URL and ALERT_DISCORD_WEBHOOK_URL to the api, ibex-webhook, and bridge-webhook deployments via secretKeyRef against a new 'discord-webhooks' secret (keys: ops-webhook-url, alert-webhook-url), optional: true so environments without the secret still schedule pods. Follows the *ExistingSecret convention; galoy-secrets.yaml renders the secret for secrets.create=true installs, omitting keys whose values are empty (an empty-string env var would fail the backend's zod .url() validation and crash pods at boot — caught by the dev smoketest). Backend consumer: lnflash/flash#458 (ops activity feed + alerts module). Real deployments provision the secret out-of-band (secrets.create=false). Claude-Session: https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7 Co-authored-by: Dread <dread@example.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a fire-and-forget ops notification feed: the backend posts color-coded Discord embeds to a webhook (
OPS_DISCORD_WEBHOOK_URL) as users move through key flows, so ops can watch flows happen live and pinpoint mid-flow breakage. Built as a sibling of the existingalertBridgemodule insrc/services/alerts/and follows the same never-throw, never-awaited idiom.Flows covered
upgrade-collisionevents forPhoneAccountAlreadyExistsNeedToSweepFundsError/PhoneAccountAlreadyExistsCannotUpgradeError(withdeviceHasBalanceflag) andupgrade-failedfor bad OTP / onboarding / kratos schema failures. Note: phoneotp-sentfires only on the add-phone flow (userPhoneRegistrationInitiate); the upgrade path requests its OTP via the captcha/login mutation, which is deliberately unhooked (login noise), so the upgrade funnel's first visible event is its outcome. Email funnel: sent → verified (Kratos identity only, no level change — by design).updateAccountLevel, old→new level).validation/draftCashout/payInvoice/submitCashout). On the submit double-failure (payment sent, ERPNext record failed) exactly one truthfulfailedevent fires — the client-facing return is unchanged, but the feed shows the desync (InitiatedCashout.erpSubmitted).error-returnvsstatus-failure), Bridge transfer webhook.Privacy / safety
+1876…00, email toj***@gmail.com; all IDs truncated to 8 chars. No names, no full wallet IDs. Amounts are display units (95.40 USD, not cents).OPS_DISCORD_WEBHOOK_URL⇒ complete no-op. Delivery is sequential with one 429retry_afterretry, 3s timeout, 50-event queue cap with a dropped-events summary embed. Call sites never change control flow.Config
OPS_DISCORD_WEBHOOK_URL(optional) added to the env schema. Needs wiring into api, IBEX-webhook, and Bridge-webhook deployments (charts follow-up;ALERT_DISCORD_WEBHOOK_URLis also not in templates yet).Tests
8 spec files, ~80 new unit tests (masking, embed shape, real queue/429/overflow behavior, per-hook flow/phase assertions incl. collision variants, no-succeeded-after-partial-failure, display-unit amounts). Full unit suite: 945 passed, 0 failed;
yarn tsc-checkand eslint clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01UXnQYUbKQe5wUCWEvdAau7