feat(backend): implement analytics plan-statistics endpoint - #1055
Merged
ONEONUORA merged 2 commits intoAug 26, 2026
Merged
Conversation
Adds GET /api/analytics/plan-statistics, protected by jwt_auth_middleware, so the admin dashboard's PlansAPI.getPlanStatistics() call has a handler to hit instead of 404ing.
|
@TheWeirdDee Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Contributor
|
@TheWeirdDee |
Contributor
|
@TheWeirdDee |
Contributor
Author
I will soon, thank you |
CI runs `cargo fmt --all -- --check`; a few lines in the new plan-statistics code exceeded rustfmt line-width defaults.
ONEONUORA
approved these changes
Aug 26, 2026
ONEONUORA
left a comment
Contributor
There was a problem hiding this comment.
Great job @TheWeirdDee
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
Implements the missing
GET /api/analytics/plan-statisticsendpoint that the Admin Dashboard already calls viaPlansAPI.getPlanStatistics()(frontend/app/lib/api/plans.ts), which previously 404'd because no Axum route handler existed for it.Closes #1036
What's in this PR
backend/src/api.rsGET /api/analytics/plan-statisticsroute, registered on theadmin_routesrouter and protected byjwt_auth_middleware(admin-role JWT only), exactly as the issue specifies.PlanStatisticsQueryextractor with three optional filters —start_date,end_date(both RFC 3339 timestamps, filtered againstplans.created_at), andasset_type(filtered againstplans.token_address) — all applied consistently across every metric in the response.sqlx::QueryBuilderto keep the dynamic filters injection-safe:total_plans,active_plans,expired_plans,triggered_plans,claimed_plans, computed withCOUNT(*) FILTER (WHERE ...)in a single round trip.by_status: aGROUP BY statusbreakdown, for the dashboard's status chart.locked_value_by_asset:SUM(amount)and plan count grouped bytoken_address, restricted tois_active = trueplans, i.e. this is the "total value locked" the issue asks for, broken out per asset currency.active_plans=status = 'ACTIVE'and not yet pastinactivity_deadline_at.expired_plans=status = 'ACTIVE'and pastinactivity_deadline_at— this mirrors theExpiredPlanconcept the inactivity watchdog (backend/src/inactivity_watchdog.rs) already uses for plans that are due for a sweep but haven't been picked up yet.triggered_plans=status IN ('TRIGGERING', 'TRIGGERED', 'TRIGGER_FAILED').claimed_plans=status IN ('CLAIMABLE', 'PAID_OUT')(set by the claim flow / payout pipeline).400ifstart_date > end_date.{ "data": ... }, consistent with the other newer plan endpoints in this file (claim_plan,cancel_plan, etc.).backend/src/cache.rsget_stats<T>()/set_stats<T>()methods toPlanCache(backed by new privateget_raw/set_rawhelpers), so any JSON-serializable value can be cached through the sameDisabled/Redis/ in-memory-fallback enum the plan-list cache already uses — without reusing the plan-list's query-index/invalidation machinery, which doesn't apply here.RedisPlanCache),set_statstakes an explicit per-call TTL, so the statistics cache can have its own, independently configurable TTL.plan_statistics_cache_key()to build a normalized cache key from the query filters.PLAN_STATISTICS_CACHE_TTL_SECS(default60s), read inbackend/src/config.rsand threaded ontoAppState— deliberately separate fromPLAN_CACHE_TTL_SECS(default15s) since this query aggregates the wholeplanstable and is far more expensive than a single plan lookup, and this endpoint is only ever hit by the admin dashboard, so a longer staleness window is fine. No cache invalidation hooks were added on plan mutations; the short TTL is what bounds staleness here, matching the issue's ask ("prevent database load spikes on admin dashboard refresh").frontend/app/lib/api/plans.tsPlanStatisticswithlocked_value_by_asset: AssetLockedValue[](new exported type) to match the new response shape.getPlanStatistics()now accepts an optionalPlanStatisticsFiltersargument (startDate/endDate/assetType) and forwards them as query params.frontend/app/lib/api/index.ts.backend/.env.examplePLAN_STATISTICS_CACHE_TTL_SECSvariable.Why these design choices
backend/src/api.rs,frontend/app/lib/api/plans.ts,backend/src/cache.rs) is exactly what's touched here — no unrelated files or refactors.PlanCacheenum (Redis-or-memory-or-disabled) instead of introducing a second cache type, since the issue explicitly namesbackend/src/cache.rsas the place for this.sqlx::QueryBuilderover hand-rolled string concatenation for the optional filters so date/asset-type values are always parameter-bound, never interpolated into SQL.status,created_at,token_address,amount,is_active,inactivity_deadline_at) already exists onplans.Test plan
backend/tests/api_tests.rs::test_plan_statistics_requires_auth— asserts the route 401s without a JWT (mirrors the existingtest_freeze_loans_requires_auth-style auth-guard tests in that file).backend/src/cache.rsunit tests:memory_cache_round_trips_generic_statsandplan_statistics_cache_keys_are_normalized_and_stable.AppStateconstruction sites (backend/src/main.rs,backend/tests/api_tests.rsx2,backend/tests/kyc_webhook_test.rs) for the newplan_statistics_cache_ttl_secsfield.cargo build/cargo test— I wasn't able to run these in my environment (no Rust toolchain installed), so please run the full suite in CI before merging.planstable to eyeball the aggregate numbers, since I couldn't spin up Postgres locally either.