Add LndHub db/models, and quick stats for admin views

This commit is contained in:
Râu Cao committed 2023-02-13 16:25:35 +08:00
1 parent 09d99ce9c2
commit dd482d7f2e
23 files changed
+282 -37

No files matched your search

@@ -0,0 +1,3 @@
<dl class="grid grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-12">
<%= content %>
</dl>
@@ -0,0 +1,4 @@
# frozen_string_literal: true
class QuickstatsContainerComponent < ViewComponent::Base
end
@@ -0,0 +1,18 @@
<div class="">
<dt class="mb-2 text-gray-500">
<%= @title %>
</dt>
<dd>
<% if @type == :number %>
<span class="text-2xl"><%= number_with_delimiter @value %></span>
<% else %>
<span class="text-2xl"><%= @value %></span>
<% end %>
<% if @unit %>
<span><%= @unit %></span>
<% end %>
<% if @meta %>
<span class="text-gray-500"><%= @meta %></span>
<% end %>
</dd>
</div>
@@ -0,0 +1,13 @@
# frozen_string_literal: true
class QuickstatsItemComponent < ViewComponent::Base
def initialize(type:, title:, value:, unit: nil, meta: nil, icon_name: nil, icon_color_class: nil)
@type = type
@title = title
@value = value
@unit = unit
@meta = meta
@icon_name = icon_name
@icon_color_class = icon_color_class
end
end
@@ -5,7 +5,11 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations
# GET /donations.json
def index
@donations = Donation.all
@donations = Donation.all.order('created_at desc')
@stats = {
overall_sats: @donations.all.sum("amount_sats"),
donor_count: Donation.distinct.count(:user_id)
}
end
# GET /donations/1
@@ -1,8 +1,11 @@
class Admin::InvitationsController < Admin::BaseController
def index
@current_section = :invitations
@invitations_unused_count = Invitation.unused.count
@users_with_referrals_count = Invitation.used.distinct.count(:user_id)
@invitations_used = Invitation.used.order('used_at desc')
@stats = {
available: Invitation.unused.count,
accepted: @invitations_used.length,
users_with_referrals: Invitation.used.distinct.count(:user_id)
}
end
end
@@ -0,0 +1,12 @@
class Admin::LightningController < Admin::BaseController
def index
@current_section = :lightning
@users = User.pluck(:cn, :ou, :ln_account)
@accounts = LndhubAccount.with_balances.order(balance: :desc).to_a
@ln = {}
@ln[:current_balance] = LndhubAccount.current.joins(:ledgers).sum("account_ledgers.amount")
@ln[:users_with_sats] = @accounts.length
end
end
+21
View File
@@ -0,0 +1,21 @@
class LndhubAccount < LndhubBase
self.table_name = "accounts"
self.inheritance_column = :_type_disabled
has_many :ledgers, class_name: "LndhubAccountLedger",
foreign_key: "account_id"
belongs_to :user, class_name: "LndhubUser",
foreign_key: "user_id"
scope :current, -> { where(type: "current") }
scope :outgoing, -> { where(type: "outgoing") }
scope :incoming, -> { where(type: "incoming") }
scope :fees, -> { where(type: "fees") }
scope :with_balances, -> {
current.joins(:user).joins(:ledgers)
.group("accounts.id", "users.login")
.select("accounts.id, users.login, SUM(account_ledgers.amount) AS balance")
}
end
+3
View File
@@ -0,0 +1,3 @@
class LndhubAccountLedger < LndhubBase
self.table_name = "account_ledgers"
end
+4
View File
@@ -0,0 +1,4 @@
class LndhubBase < ActiveRecord::Base
self.abstract_class = true
establish_connection :lndhub
end
+15
View File
@@ -0,0 +1,15 @@
class LndhubUser < LndhubBase
self.table_name = "users"
self.inheritance_column = :_type_disabled
has_many :accounts, class_name: "LndhubAccount",
foreign_key: "user_id"
belongs_to :user, class_name: "User",
primary_key: "ln_account",
foreign_key: "login"
def balance
accounts.current.first.ledgers.sum("account_ledgers.amount")
end
end
+5
View File
@@ -5,6 +5,11 @@ class User < ApplicationRecord
has_many :invitations, dependent: :destroy
has_many :donations, dependent: :nullify
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user
validates_uniqueness_of :cn
validates_length_of :cn, :minimum => 3
validates_uniqueness_of :email
+8 -3
View File
@@ -1,7 +1,12 @@
<%= render HeaderComponent.new(title: "Admin Panel") %>
<%= render MainSimpleComponent.new do %>
<p class="text-center">
With great power comes great responsibility.
</p>
<div class="text-center">
<p class="my-12 inline-flex align-center items-center">
<%= image_tag("/img/illustrations/undraw_vault_re_s4my.svg", class: 'h-48') %>
</p>
<p class="text-gray-500">
With great power comes great responsibility.
</p>
</div>
<% end %>
+28 -6
View File
@@ -1,7 +1,26 @@
<%= render HeaderComponent.new(title: "Donations") %>
<%= render MainSimpleComponent.new do %>
<section>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Overall',
value: @stats[:overall_sats],
unit: 'sats'
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Donors',
value: @stats[:donor_count],
meta: "/ #{User.count} users"
) %>
<% end %>
</section>
<section>
<% if @donations.any? %>
<h3>Recent Donations</h3>
<table>
<thead>
<tr>
@@ -11,7 +30,7 @@
<th class="text-right">in USD</th>
<th class="pl-2">Public name</th>
<th>Date</th>
<th colspan="3"></th>
<th></th>
</tr>
</thead>
@@ -23,11 +42,13 @@
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
<td class="pl-2"><%= donation.public_name %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d") : "" %></td>
<td><%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %></td>
<td><%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %></td>
<td><%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
<td class="text-right">
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</tbody>
@@ -37,6 +58,7 @@
No donations yet.
</p>
<% end %>
</section>
<p class="mt-12">
<%= link_to 'Record an out-of-system donation', new_admin_donation_path, class: 'btn-md btn-gray' %>
+19 -7
View File
@@ -2,16 +2,28 @@
<%= render MainSimpleComponent.new do %>
<section>
<p>
There are currently <strong><%= @invitations_unused_count %>
unused invitations</strong> available to existing users.
<strong><%= @users_with_referrals_count %> users</strong> have successfully
invited new users.
</p>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Available',
value: @stats[:available],
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Accepted',
value: @stats[:accepted],
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Users with referrals',
value: @stats[:users_with_referrals],
meta: "/ #{User.count}"
) %>
<% end %>
</section>
<% if @invitations_used.any? %>
<section>
<h3>Accepted (<%= @invitations_used.length %>)</h3>
<h3>Recently Accepted</h3>
<table>
<thead>
<tr>
+48
View File
@@ -0,0 +1,48 @@
<%= render HeaderComponent.new(title: "Lightning Network") %>
<%= render MainSimpleComponent.new do %>
<section>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Current user balance',
value: @ln[:current_balance],
unit: 'sats'
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Users with sats',
value: @ln[:users_with_sats],
meta: "/ #{User.count}"
) %>
<% end %>
</section>
<section>
<h3>Accounts</h3>
<table>
<thead>
<tr>
<th>LN Account</th>
<th>User</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
<% @accounts.each do |account| %>
<tr>
<td class="font-mono">
<%= account.login[0, 8] %>...<%= account.login[12, 19] %>
</td>
<td>
<% if user = @users.find{ |u| u[2] == account.login } %>
<%= "#{user[0]}@#{user[1]}" %>
<% end %>
</td>
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>
</tr>
<% end %>
</tbody>
</table>
</section>
<% end %>
+4 -4
View File
@@ -4,10 +4,10 @@
<section>
<h3>Password</h3>
<p class="mb-8">Use the following button to request an email with a password reset link:</p>
<p>
<%= form_with(url: settings_reset_password_path, method: :post) do %>
<%= form_with(url: settings_reset_password_path, method: :post) do %>
<p>
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
<% end %>
</p>
</p>
<% end %>
</section>
<% end %>
+4 -2
View File
@@ -1,8 +1,10 @@
<%= link_to "Dashboard", admin_root_path,
class: main_nav_class(@current_section, :dashboard) %>
<%= link_to "Users", admin_ldap_users_path,
class: main_nav_class(@current_section, :ldap_users) %>
<%= link_to "Invitations", admin_invitations_path,
class: main_nav_class(@current_section, :invitations) %>
<%= link_to "Donations", admin_donations_path,
class: main_nav_class(@current_section, :donations) %>
<%= link_to "LDAP Users", admin_ldap_users_path,
class: main_nav_class(@current_section, :ldap_users) %>
<%= link_to "Lightning", admin_lightning_path,
class: main_nav_class(@current_section, :lightning) %>
+30 -11
View File
@@ -10,21 +10,40 @@ default: &default
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
primary:
<<: *default
database: db/development.sqlite3
# lndhub:
# <<: *default
# database: db/lndhub.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
primary:
<<: *default
database: db/test.sqlite3
lndhub:
<<: *default
database_tasks: false
database: db/test.lndhub.sqlite3
production:
<<: *default
adapter: postgresql
database: akkounts
port: 5432
host: <%= Rails.application.credentials.postgres[:host] rescue nil %>
username: <%= Rails.application.credentials.postgres[:username] rescue nil %>
password: <%= Rails.application.credentials.postgres[:password] rescue nil %>
primary:
<<: *default
adapter: postgresql
database: akkounts
port: 5432
host: <%= Rails.application.credentials.postgres[:host] rescue nil %>
username: <%= Rails.application.credentials.postgres[:username] rescue nil %>
password: <%= Rails.application.credentials.postgres[:password] rescue nil %>
lndhub:
<<: *default
adapter: postgresql
database_tasks: false
database: lndhub
port: 5432
host: <%= Rails.application.credentials.postgres[:host] rescue nil %>
username: <%= Rails.application.credentials.postgres[:username] rescue nil %>
password: <%= Rails.application.credentials.postgres[:password] rescue nil %>
+2 -1
View File
@@ -39,9 +39,10 @@ Rails.application.routes.draw do
namespace :admin do
root to: 'dashboard#index'
get 'invitations', to: 'invitations#index'
get 'ldap_users', to: 'ldap_users#index'
get 'invitations', to: 'invitations#index'
resources :donations
get 'lightning', to: 'lightning#index'
end
authenticate :user, ->(user) { user.is_admin? } do
@@ -0,0 +1 @@
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="940.67538" height="433.96214" viewBox="0 0 940.67538 433.96214" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M329.153,642.83294c-22.523-14.678-38.89925-41.06206-35.54763-67.73564a123.86333,123.86333,0,0,0,76.43469,37.27763c11.2188,1.12255,23.81371,1.203,31.78082,9.181,4.95743,4.964,7.06374,12.21938,7.17209,19.23382.10818,7.01478-1.57724,13.919-3.25174,20.7319l.41151,1.70229C379.28745,662.23116,351.676,657.51093,329.153,642.83294Z" transform="translate(-129.66231 -233.01893)" fill="#f0f0f0"/><path d="M294.16944,575.09153a105.8698,105.8698,0,0,0,42.01323,49.7772,45.59136,45.59136,0,0,0,13.05747,5.79236,26.14868,26.14868,0,0,0,13.75175-.52792c4.17968-1.22258,8.31345-2.95716,12.71753-3.14244a16.01086,16.01086,0,0,1,12.09044,5.02735c3.96164,3.99276,6.25234,9.17088,8.31649,14.3125,2.29184,5.70878,4.5819,11.67473,9.24952,15.903.56555.51231-.26246,1.3599-.82716.84835-8.12086-7.35651-9.01471-19.15919-15.39806-27.6937-2.9786-3.98236-7.17537-7.09121-12.306-7.22264-4.48656-.11493-8.75087,1.67014-12.97218,2.9446a27.90252,27.90252,0,0,1-13.427,1.01719,42.148,42.148,0,0,1-13.232-5.269,102.71281,102.71281,0,0,1-24.765-20.42666A107.7473,107.7473,0,0,1,293.027,575.40658c-.28775-.704.85653-1.01448,1.14241-.315Z" transform="translate(-129.66231 -233.01893)" fill="#fff"/><path d="M329.878,621.43847a15.88382,15.88382,0,0,1-20.25535,3.572c-.65656-.38743-.08535-1.42589.57208-1.03794a14.70808,14.70808,0,0,0,18.83491-3.36126c.4837-.58934,1.32929.24119.84836.82716Z" transform="translate(-129.66231 -233.01893)" fill="#fff"/><path d="M360.95427,631.03218a30.61481,30.61481,0,0,0-9.85329-20.01752c-.56372-.51431.26412-1.362.82716-.84836a31.844,31.844,0,0,1,10.2109,20.85089c.06223.76138-1.12287.77223-1.18477.015Z" transform="translate(-129.66231 -233.01893)" fill="#fff"/><path d="M305.73685,596.77673a8.991,8.991,0,0,0,3.34676-7.84756c-.07185-.76022,1.11338-.77026,1.18477-.015a10.07518,10.07518,0,0,1-3.68318,8.68971.61233.61233,0,0,1-.83775.0106.59543.59543,0,0,1-.0106-.83776Z" transform="translate(-129.66231 -233.01893)" fill="#fff"/><path d="M370.43292,526.28515c.05836.47341.11673.94682.18388,1.42507a118.40619,118.40619,0,0,0,3.986,18.78915c.13435.47733.27769.95906.42547,1.43154a124.82721,124.82721,0,0,0,21.46806,40.63524,121.2228,121.2228,0,0,0,12.52975,13.5c6.23281,5.76649,13.44959,11.52292,17.30008,18.79753a20.798,20.798,0,0,1,1.08726,2.33295l-19.21583,39.39325c-.0886.06761-.16847.14-.25728.20807l-.69531,1.60793c-.24779-.1991-.50051-.41093-.7483-.61-.14422-.1149-.28416-.23858-.42838-.35347-.09479-.07938-.18942-.15923-.27949-.22544-.03165-.0263-.06309-.053-.0858-.075-.09007-.06621-.16671-.13728-.24779-.1991q-2.12385-1.77024-4.23021-3.5765c-.009-.00439-.009-.00439-.01371-.01756-10.67621-9.2025-20.67794-19.37715-28.8174-30.74642-.24477-.34214-.49876-.68823-.73475-1.04837a115.67336,115.67336,0,0,1-9.60945-16.24781,102.26243,102.26243,0,0,1-4.01577-9.53594,85.03679,85.03679,0,0,1-4.997-26.114c-.5678-17.76774,4.55317-35.51929,16.47547-48.39615C369.81759,526.93084,370.11869,526.61,370.43292,526.28515Z" transform="translate(-129.66231 -233.01893)" fill="#f0f0f0"/><path d="M370.8897,526.6176a105.86971,105.86971,0,0,0,3.5759,65.03916,45.59123,45.59123,0,0,0,6.93826,12.48638,26.14854,26.14854,0,0,0,11.29785,7.858c4.07331,1.54029,8.41823,2.64414,12.04619,5.14777a16.01082,16.01082,0,0,1,6.62673,11.29334c.75923,5.57317-.52935,11.08677-1.97686,16.43482-1.60717,5.938-3.37061,12.08024-2.18949,18.26653.1431.74956-1.02832.92778-1.17121.17936-2.05493-10.76309,4.33741-20.725,4.379-31.38257.01942-4.973-1.45972-9.982-5.47715-13.176-3.51307-2.793-7.99263-3.93512-12.13042-5.459a27.90252,27.90252,0,0,1-11.33314-7.27183A42.14812,42.14812,0,0,1,374.08266,593.86a102.71241,102.71241,0,0,1-7.47521-31.21979,107.747,107.747,0,0,1,3.18042-36.45885c.19411-.73538,1.29468-.29432,1.10183.43626Z" transform="translate(-129.66231 -233.01893)" fill="#fff"/><path d="M371.4969,585.122a15.88383,15.88383,0,0,1-18.32338-9.343c-.291-.70464.79034-1.18989,1.08169-.48431a14.70807,14.70807,0,0,0,17.06233,8.65614c.741-.17934.91615.9929.17936,1.17121Z" transform="translate(-129.66231 -233.01893)" fill="#fff"/><path d="M390.53351,611.49215a30.61477,30.61477,0,0,0,4.18463-21.91524c-.14045-.75,1.03093-.92849,1.17121-.17936a31.844,31.844,0,0,1-4.40084,22.79594c-.40871.64539-1.36149-.05946-.955-.70134Z" transform="translate(-129.66231 -233.01893)" fill="#fff"/><path d="M367.06964,550.89639a8.991,8.991,0,0,0,7.397-4.25086c.40033-.65025,1.35272.05533.955.70134a10.07517,10.07517,0,0,1-8.17261,4.72073.61235.61235,0,0,1-.67529-.49593.59543.59543,0,0,1,.49593-.67528Z" transform="translate(-129.66231 -233.01893)" fill="#fff"/><path d="M1016.50054,642.83294c22.523-14.678,38.89925-41.06206,35.54763-67.73564a123.8633,123.8633,0,0,1-76.43468,37.27763c-11.21881,1.12255-23.81372,1.203-31.78083,9.181-4.95742,4.964-7.06373,12.21938-7.17208,19.23382-.10818,7.01478,1.57723,13.919,3.25173,20.7319l-.41151,1.70229C966.36609,662.23116,993.9Line truncated

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,15 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe QuickstatsContainerComponent, type: :component do
pending "add some examples to (or delete) #{__FILE__}"
# it "renders something useful" do
# expect(
# render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html
# ).to include(
# "Hello, components!"
# )
# end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe QuickstatsItemComponent, type: :component do
pending "add some examples to (or delete) #{__FILE__}"
# it "renders something useful" do
# expect(
# render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html
# ).to include(
# "Hello, components!"
# )
# end
end