feat: user labels for transactions (#2265)

* feat: user labels poc

* feat: backend for transaction user labels

Add PATCH /api/transactions/:paymentHash/label that merges a
user-supplied {key:value} map into the existing transaction metadata
under the user_label key, preserving NIP-47 fields. Empty map clears
the labels. Trims whitespace, drops blank rows, caps key/value length.

Wire the frontend editor to call the endpoint and revalidate the
transactions SWR cache on success.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: include user labels as columns in CSV export

Collect the union of user_label keys across all exported transactions
and emit each one as its own label_<key> column. The existing metadata
JSON column is preserved so importers like Raccoin keep working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: drop hardcoded label suggestions, autocomplete from prior keys

Open the editor with a single blank row instead of four pre-seeded
fields. Suggest label keys via a datalist populated from any
user_label keys already present in the SWR transactions cache, so
suggestions reflect the user's own taxonomy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: inline label editor and indicator-only list row

Replace the nested label dialog with an inline editor that toggles
within the existing transaction detail dialog, removing dialog
stacking. In the transactions list, replace per-label badges with a
single tag icon next to the timestamp so row height stays uniform; the
full labels remain visible in the detail view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: don't bump updated_at on metadata-only edits

GORM's Update auto-touches updated_at, which made the transactions
list reorder labeled transactions to the top. Switch
SetTransactionMetadata to UpdateColumn so only the metadata column
changes. Add regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: route PATCH transaction label requests in wails

The desktop app routes API calls through WailsRequestRouter rather
than HTTP. Add a handler for PATCH /api/transactions/:hash/label
before the existing transaction lookup so labels work in Wails too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: finalize backend

* chore: rename to labels

* chore: finalize frontend

* chore: use tx id to add user labels

* chore: address minor nits

* fix: linting

* fix: use explicit primary key lookups

* chore: simplify transaction csv label export

* fix(frontend): drop label count from transaction list badge

The count adds visual weight without informing any decision from the
list view. The icon-as-badge already signals labels exist; the actual
values are in the details dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(frontend): revert transaction list label indicator to bare icon

The Badge wrapper made the icon-only indicator wider than tall, which
looked off. Restores the pre-PR look — a small TagIcon next to the
timestamp — since the in-dialog editor is the place to see actual labels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
This commit is contained in:
René Aaron 2026-04-30 13:13:07 +02:00 committed by GitHub
parent 7195c09de3
commit ed7e10cf25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 568 additions and 37 deletions

View file

@ -48,6 +48,7 @@ type API interface {
SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}) (*SendPaymentResponse, error)
CreateInvoice(ctx context.Context, amountMsat uint64, description string) (*MakeInvoiceResponse, error)
LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error)
SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error
RequestMempoolApi(ctx context.Context, endpoint string) (interface{}, error)
GetInfo(ctx context.Context) (*InfoResponse, error)
GetMnemonic(unlockPassword string) (*MnemonicResponse, error)
@ -381,6 +382,10 @@ type SendPaymentResponse = Transaction
type MakeInvoiceResponse = Transaction
type LookupInvoiceResponse = Transaction
type SetTransactionUserLabelsRequest struct {
Labels map[string]string `json:"labels"`
}
type ListTransactionsResponse struct {
TotalCount uint64 `json:"totalCount"`
Transactions []Transaction `json:"transactions"`
@ -388,6 +393,7 @@ type ListTransactionsResponse struct {
// TODO: camelCase
type Transaction struct {
ID uint `json:"id"`
Type string `json:"type"`
State string `json:"state"`
Invoice string `json:"invoice"`

View file

@ -36,6 +36,10 @@ func (api *api) LookupInvoice(ctx context.Context, paymentHash string) (*LookupI
return toApiTransaction(transaction), nil
}
func (api *api) SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error {
return api.svc.GetTransactionsService().SetTransactionUserLabels(ctx, id, labels)
}
func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
@ -113,6 +117,7 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction {
}
return &Transaction{
ID: transaction.ID,
Type: transaction.Type,
State: strings.ToLower(transaction.State),
Invoice: transaction.PaymentRequest,