Skip to content

fix: sort Recently Used Apps by settled transactions#2012

Open
keshav0479 wants to merge 1 commit intogetAlby:masterfrom
keshav0479:fix/recently-used-apps
Open

fix: sort Recently Used Apps by settled transactions#2012
keshav0479 wants to merge 1 commit intogetAlby:masterfrom
keshav0479:fix/recently-used-apps

Conversation

@keshav0479
Copy link
Contributor

@keshav0479 keshav0479 commented Jan 11, 2026

Description

Fixes #1998

Sorts "Recently Used Apps" by settled transactions instead of LastUsedAt, so apps that actively move value appear at the top.

Changes

  • api/api.go: Modified ListApps to LEFT JOIN transactions where state = 'SETTLED' and sort by MAX(transactions.created_at).
  • nip47/event_handler.go: Minor cleanup (fixed log field name).

Notes

  • LastUsedAt still tracks connection activity (useful for debugging).
  • No database schema changes required.

Summary by CodeRabbit

  • New Features
    • Apps can be sorted by most-recent settled transaction time (with fallback to last activity); the UI now follows server-provided ordering instead of re-sorting on the client.
  • Bug Fixes
    • Improved logging for failures when updating app activity to make diagnostics clearer.

@rolznz
Copy link
Contributor

rolznz commented Jan 12, 2026

Hi,

Thanks for the PR. But I still think it's important to know when there was any activity at all, as well as if there was monetary activity.

There are also problems with the current solution that simply requesting an invoice (but it not being paid) would count as monetary activity. I think this needs more planning before working on an implementation.

@rolznz rolznz marked this pull request as draft January 12, 2026 04:52
@keshav0479
Copy link
Contributor Author

Fair point,it is useful to know when an app was last online/connected, even if it didn't spend anything.

To fix the sorting without losing that data, how about we split it into two fields?
LastConnectedAt (updates on everything, like get_balance) and LastActionAt (updates only on value movements)
Then we can just update the UI to sort by LastActionAt so the list stays relevant.Curious to know what u think?

@rolznz
Copy link
Contributor

rolznz commented Jan 12, 2026

I wonder if it is possible already to find these apps by doing a DB query that joins apps on the transaction list? then there's no DB changes needed. (as long as the query is efficient enough)

@keshav0479
Copy link
Contributor Author

Oh that's a great point, i hadn't considered using the existing transactions table.
I just checked and Transaction does have AppId, so i can try updating the ListApps query to join on settled transactions instead of relying on LastUsedAt for sorting.
Would it be correct to filter by settled_at IS NOT NULL (or a specific state) to only count completed payments? Just want to make sure i get the right definition of "movement of value."
Once you confirm, i'll revert my current changes and try this approach!

@rolznz
Copy link
Contributor

rolznz commented Jan 12, 2026

@keshav0479 only settled payments count. I think you can look at the transaction state as the settled constant (in constants.go)

@keshav0479
Copy link
Contributor Author

Got it, that makes perfect sense regarding the SETTLED state.
So checking the transactions table where state = 'SETTLED' seems like the best approach.
This way we get the best of both worlds: LastUsedAt can stay as "Last Connected" (useful for debugging), while the UI sorts purely by confirmed financial activity.
Does that sound like the right direction? happy to update the PR if you agree!

@rolznz
Copy link
Contributor

rolznz commented Jan 13, 2026

@keshav0479 sounds good to me, thanks!

@keshav0479 keshav0479 force-pushed the fix/recently-used-apps branch from dc8d2cd to 25c5517 Compare January 13, 2026 11:48
@keshav0479 keshav0479 changed the title fix: only update 'last used' on value-moving requests fix: sort Recently Used Apps by settled transactions Jan 13, 2026
@keshav0479 keshav0479 marked this pull request as ready for review January 13, 2026 11:56
api/api.go Outdated

if orderBy == "" {
orderBy = "last_used_at"
var totalCount int64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect - counting must be done after filtering.

Also, I would undo your changes and make a new sorting parameter for last_economic_activity or something like that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or last_settled_transaction maybe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated! Changed the log field to app_id and added last_settled_transaction as a new sorting option, keeping the original logic intact

@keshav0479 keshav0479 force-pushed the fix/recently-used-apps branch from 25c5517 to 1e5cbd8 Compare January 24, 2026 14:23
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 24, 2026

Warning

Rate limit exceeded

@keshav0479 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 48 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9137e5db-4a9e-4cd3-8f7c-4eaf206fb3ed

📥 Commits

Reviewing files that changed from the base of the PR and between 47f0597 and 2c9de78.

📒 Files selected for processing (4)
  • api/api.go
  • frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx
  • frontend/src/hooks/useApps.ts
  • nip47/event_handler.go
📝 Walkthrough

Walkthrough

Adds a new ListApps ordering option "last_settled_transaction" (sorts by most recent settled transaction timestamp), updates frontend hook and widget to request that ordering and stop client-side resorting, and corrects a structured log field name in an event handler.

Changes

Cohort / File(s) Summary
App Listing Query Logic
api/api.go
Adds orderBy == "last_settled_transaction": SELECTs apps.* plus MAX(transactions.created_at) AS last_transaction_at, LEFT JOINs settled transactions, GROUP BY apps.id, and orders by last_transaction_at IS NULL, last_transaction_at DESC, then apps.last_used_at.
Frontend: widget & hook
frontend/src/hooks/useApps.ts, frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx
useApps signature accepts last_settled_transaction; widget requests that order and removes client-side sorting, relying on server-returned order while retaining filtering/display of lastUsedAt.
Event Logging
nip47/event_handler.go
Renamed structured log field from "it" to "app_id" when app.last_used_at update errors; no control-flow changes.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Frontend as Frontend (Browser)
    participant API as API Server
    participant DB as Database

    User->>Frontend: Open "Latest Used Apps"
    Frontend->>API: GET /api/apps?order_by=last_settled_transaction
    API->>DB: Query apps LEFT JOIN transactions (state=SETTLED), SELECT MAX(transactions.created_at) AS last_transaction_at, GROUP BY apps.id, ORDER BY last_transaction_at DESC, apps.last_used_at
    DB-->>API: Return ordered apps list
    API-->>Frontend: Respond with ordered apps
    Frontend-->>User: Render apps in received order
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hop through rows and timestamps bright,
Chasing coins that settled right.
Not every ping gets a cheer — only value in flight,
Now recent apps gleam true by settled light. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding sorted Recently Used Apps based on settled transactions instead of any NWC request.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from #1998: sorting Recently Used Apps by settled transactions rather than any NWC request activity.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue objectives: backend sorting logic, minor logging cleanup, and frontend integration to consume the settled-transaction timestamp.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/api.go (1)

526-531: Count query may return incorrect results with GROUP BY.

When orderBy == "last_settled_transaction", the query has a GROUP BY apps.id clause. Calling .Model(&db.App{}).Count(&totalCount) on this grouped query may not return the correct total count of apps—it could count the number of groups or behave unexpectedly depending on the database driver.

Consider computing the count before applying the grouping/aggregation for the last_settled_transaction case, or use a separate unmodified query for counting.

🔧 Suggested approach
+	// Count before applying aggregation for last_settled_transaction
+	countQuery := api.db
+	if filters.Name != "" {
+		if api.db.Dialector.Name() == "postgres" {
+			countQuery = countQuery.Where("name ILIKE ?", filters.Name+"%")
+		} else {
+			countQuery = countQuery.Where("name LIKE ?", filters.Name+"%")
+		}
+	}
+	// ... apply other filters to countQuery ...
+
 	var totalCount int64
-	result := query.Model(&db.App{}).Count(&totalCount)
+	result := countQuery.Model(&db.App{}).Count(&totalCount)

Alternatively, you could clone the query before adding the SELECT/JOIN/GROUP BY clauses and use the clone for counting.

🤖 Fix all issues with AI agents
In `@api/api.go`:
- Around line 514-521: The orderBy string already contains DESC and causes a
duplicated "DESC" in query.Order(orderBy + " DESC"), and NULL
last_transaction_at values need to be ordered to the end; when handling the
last_settled_transaction branch (the block that calls query.Select(...
MAX(transactions.created_at) as last_transaction_at) / Joins(...) /
Group("apps.id")), set orderBy to a value that includes NULL-safe ordering (for
example using COALESCE(last_transaction_at, '1970-01-01') DESC,
apps.last_used_at DESC) and then stop appending " DESC" later — change the final
call to use query.Order(orderBy) (or ensure orderBy contains no direction if you
intend to append " DESC"), so you neither produce duplicate DESC nor leave NULL
last_transaction_at entries unordered.

@im-adithya
Copy link
Member

@keshav0479 LGTM, nice work 🙌 But can we use last_settled_transaction in the LatestUsedAppsWidget (and here)?

@keshav0479 keshav0479 closed this Mar 23, 2026
@keshav0479 keshav0479 force-pushed the fix/recently-used-apps branch from 6ebe358 to 3cacb18 Compare March 23, 2026 12:42
@keshav0479 keshav0479 reopened this Mar 23, 2026
@keshav0479 keshav0479 force-pushed the fix/recently-used-apps branch from 03fd58d to 47f0597 Compare March 23, 2026 12:50
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/api.go (1)

528-543: ⚠️ Potential issue | 🔴 Critical

Reject unknown orderBy values before building the SQL clause.

orderBy still comes from request input, and Line 543 concatenates it directly into Order(...). The new frontend literal union does not protect this endpoint from crafted calls, so please whitelist the supported values and build the full order clause server-side.

🔒 Safer pattern
+	var orderClause string
+
 	if orderBy == "" {
 		orderBy = "last_used_at"
 	}
-	if orderBy == "last_used_at" {
-		// when ordering by last used at, apps with last_used_at is NULL should be ordered last
-		orderBy = "last_used_at IS NULL, " + orderBy
-	}
-
-	if orderBy == "last_settled_transaction" {
+
+	switch orderBy {
+	case "last_used_at":
+		orderClause = "last_used_at IS NULL, last_used_at DESC"
+	case "created_at":
+		orderClause = "created_at DESC"
+	case "last_settled_transaction":
 		query = query.Select("apps.*, MAX(transactions.created_at) as last_transaction_at").
 			Joins("LEFT JOIN transactions ON transactions.app_id = apps.id AND transactions.state = ?", constants.TRANSACTION_STATE_SETTLED).
 			Group("apps.id")
-		orderBy = "last_transaction_at IS NULL, last_transaction_at DESC, apps.last_used_at"
+		orderClause = "last_transaction_at IS NULL, last_transaction_at DESC, apps.last_used_at DESC"
+	default:
+		return nil, fmt.Errorf("invalid orderBy: %s", orderBy)
 	}
 
-	query = query.Order(orderBy + " DESC")
+	query = query.Order(orderClause)
As per coding guidelines, `{api,http}/**/*.go`: Validate all user input at system boundaries; trust internal service calls.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/api.go` around lines 528 - 543, Reject/whitelist untrusted orderBy values
before using them in the SQL ORDER clause: validate the incoming orderBy
variable against an allowed set (e.g., "", "last_used_at",
"last_settled_transaction", any other supported keys) and map each allowed key
to a fully formed, safe SQL ordering expression (e.g., "last_used_at IS NULL,
last_used_at DESC" and for "last_settled_transaction" ensure you build the
JOIN/Group and use "last_transaction_at IS NULL, last_transaction_at DESC,
apps.last_used_at"); if the value is not in the whitelist, fall back to the
default ordering, and only then call query.Order(...). Reference the orderBy
variable, the query builder block that sets MAX(transactions.created_at) with
constants.TRANSACTION_STATE_SETTLED, and ensure no raw request string is
concatenated into Order().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/api.go`:
- Around line 537-540: The current SELECT and GROUP uses
MAX(transactions.created_at) to compute last_transaction_at but should use the
settlement timestamp; change the derived column to MAX(transactions.settled_at)
(the DB column for db.Transaction.SettledAt) in the query.Select and keep the
same JOIN filter using constants.TRANSACTION_STATE_SETTLED, and keep the orderBy
logic ("last_transaction_at IS NULL, last_transaction_at DESC,
apps.last_used_at") so apps are ranked by the actual settled time instead of
request creation time.

In `@frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx`:
- Around line 16-18: The widget is still keyed and filtered by lastUsedAt
despite requesting last_settled_transaction from the API; update
LatestUsedAppsWidget to use the API's lastSettledTransaction field instead:
change the useApps consumer to read appsData?.apps[*].lastSettledTransaction
(not lastUsedAt), update usedApps = apps?.filter(x => x.lastSettledTransaction),
and render the relative-time label from lastSettledTransaction; also make the
same replacement for the other occurrences in this component (around the 35-45
region) so filtering, sorting, and display consistently use
lastSettledTransaction.

---

Outside diff comments:
In `@api/api.go`:
- Around line 528-543: Reject/whitelist untrusted orderBy values before using
them in the SQL ORDER clause: validate the incoming orderBy variable against an
allowed set (e.g., "", "last_used_at", "last_settled_transaction", any other
supported keys) and map each allowed key to a fully formed, safe SQL ordering
expression (e.g., "last_used_at IS NULL, last_used_at DESC" and for
"last_settled_transaction" ensure you build the JOIN/Group and use
"last_transaction_at IS NULL, last_transaction_at DESC, apps.last_used_at"); if
the value is not in the whitelist, fall back to the default ordering, and only
then call query.Order(...). Reference the orderBy variable, the query builder
block that sets MAX(transactions.created_at) with
constants.TRANSACTION_STATE_SETTLED, and ensure no raw request string is
concatenated into Order().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09c37dda-6350-48ce-a943-9fb6df1dba1c

📥 Commits

Reviewing files that changed from the base of the PR and between 6ebe358 and 03fd58d.

📒 Files selected for processing (4)
  • api/api.go
  • frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx
  • frontend/src/hooks/useApps.ts
  • nip47/event_handler.go
✅ Files skipped from review due to trivial changes (1)
  • nip47/event_handler.go

Comment on lines +16 to 18
const { data: appsData } = useApps(3, undefined, undefined, "last_settled_transaction");
const apps = appsData?.apps;
const usedApps = apps?.filter((x) => x.lastUsedAt);
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't keep keying this widget off lastUsedAt.

This now asks the API for last_settled_transaction, but it still filters rows and renders the relative-time label from lastUsedAt, which the PR explicitly keeps as connection activity. A recent balance check can therefore still make an app look newer than its last paid action, and the post-limit filter can leave the widget under-filled. Please plumb a dedicated lastSettledTransaction field through the API and use that here instead.

Also applies to: 35-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx` around lines
16 - 18, The widget is still keyed and filtered by lastUsedAt despite requesting
last_settled_transaction from the API; update LatestUsedAppsWidget to use the
API's lastSettledTransaction field instead: change the useApps consumer to read
appsData?.apps[*].lastSettledTransaction (not lastUsedAt), update usedApps =
apps?.filter(x => x.lastSettledTransaction), and render the relative-time label
from lastSettledTransaction; also make the same replacement for the other
occurrences in this component (around the 35-45 region) so filtering, sorting,
and display consistently use lastSettledTransaction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lastUsedAt filter just checks if the app has connected before, the sorting itself comes from the backend. Can add a dedicated field in a follow-up if needed!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@keshav0479, that's a fair point — using lastUsedAt as a gate to filter out apps that have never connected at all is reasonable, and the backend-driven sorting by settled transactions is the most impactful fix here.

The one remaining UX concern is the displayed time label (dayjs(app.lastUsedAt).fromNow()): a user seeing "2 minutes ago" next to an app that last did a balance check (but hasn't moved value in days) could still be misled. That said, since the ordering itself is now correct, this is a cosmetic/UX issue — a follow-up to plumb lastSettledTransaction through to the frontend for the label sounds like a pragmatic approach. Would you like to open a follow-up issue to track that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be fixed in this PR. If we filter by last economic activity, then we should also show when the last economic activity was, not the last time it was just accessed via NWC.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@keshav0479 keshav0479 force-pushed the fix/recently-used-apps branch from 47f0597 to 2c9de78 Compare March 23, 2026 12:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx (1)

16-18: ⚠️ Potential issue | 🟠 Major

Use settled-transaction timestamp consistently in this widget.

Line 18 filters on lastUsedAt, and Line 45 renders relative time from lastUsedAt. That keeps this widget tied to connection activity and can hide apps that were correctly ranked by settled transactions.

Suggested fix
-  const usedApps = apps?.filter((x) => x.lastUsedAt);
+  const usedApps = apps?.filter((x) => x.lastSettledTransaction);

...
-                {app.lastUsedAt ? dayjs(app.lastUsedAt).fromNow() : "never"}
+                {app.lastSettledTransaction
+                  ? dayjs(app.lastSettledTransaction).fromNow()
+                  : "never"}

Also applies to: 45-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx` around lines
16 - 18, The widget is filtering and rendering by connection activity
(`lastUsedAt`) but the query sorts by `last_settled_transaction`; update the
filtering and display to use the settled-transaction timestamp instead.
Specifically, change the filter that defines usedApps (currently using
apps?.filter((x) => x.lastUsedAt)) to check the settled timestamp property
(e.g., x.lastSettledTransactionAt or the exact field returned for
"last_settled_transaction"), and update any rendering logic that shows relative
time (currently reading x.lastUsedAt) to read that settled timestamp property
with optional chaining. Ensure you reference the same property name returned by
useApps so filtering and rendering are consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx`:
- Around line 16-18: The widget is filtering and rendering by connection
activity (`lastUsedAt`) but the query sorts by `last_settled_transaction`;
update the filtering and display to use the settled-transaction timestamp
instead. Specifically, change the filter that defines usedApps (currently using
apps?.filter((x) => x.lastUsedAt)) to check the settled timestamp property
(e.g., x.lastSettledTransactionAt or the exact field returned for
"last_settled_transaction"), and update any rendering logic that shows relative
time (currently reading x.lastUsedAt) to read that settled timestamp property
with optional chaining. Ensure you reference the same property name returned by
useApps so filtering and rendering are consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a1d308d-ac3c-4bfa-9e58-2d543f9bb743

📥 Commits

Reviewing files that changed from the base of the PR and between 03fd58d and 47f0597.

📒 Files selected for processing (4)
  • api/api.go
  • frontend/src/components/home/widgets/LatestUsedAppsWidget.tsx
  • frontend/src/hooks/useApps.ts
  • nip47/event_handler.go
✅ Files skipped from review due to trivial changes (1)
  • nip47/event_handler.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/hooks/useApps.ts
  • api/api.go

@keshav0479
Copy link
Contributor Author

Updated! Now using last_settled_transaction in LatestUsedAppsWidget and useApps.ts, removed the client-side re-sort since the backend handles ordering, and switched to settled_at for more accurate sorting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recently Used Apps should be based on movement of value, not any NWC request

3 participants