Add LndHub db/models, and quick stats for admin views
This commit is contained in:
parent
09d99ce9c2
commit
dd482d7f2e
3
app/components/quickstats_container_component.html.erb
Normal file
3
app/components/quickstats_container_component.html.erb
Normal file
@ -0,0 +1,3 @@
|
||||
<dl class="grid grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-12">
|
||||
<%= content %>
|
||||
</dl>
|
4
app/components/quickstats_container_component.rb
Normal file
4
app/components/quickstats_container_component.rb
Normal file
@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class QuickstatsContainerComponent < ViewComponent::Base
|
||||
end
|
18
app/components/quickstats_item_component.html.erb
Normal file
18
app/components/quickstats_item_component.html.erb
Normal file
@ -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>
|
13
app/components/quickstats_item_component.rb
Normal file
13
app/components/quickstats_item_component.rb
Normal file
@ -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
|
||||
|
12
app/controllers/admin/lightning_controller.rb
Normal file
12
app/controllers/admin/lightning_controller.rb
Normal file
@ -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
app/models/lndhub_account.rb
Normal file
21
app/models/lndhub_account.rb
Normal 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
app/models/lndhub_account_ledger.rb
Normal file
3
app/models/lndhub_account_ledger.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class LndhubAccountLedger < LndhubBase
|
||||
self.table_name = "account_ledgers"
|
||||
end
|
4
app/models/lndhub_base.rb
Normal file
4
app/models/lndhub_base.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class LndhubBase < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
establish_connection :lndhub
|
||||
end
|
15
app/models/lndhub_user.rb
Normal file
15
app/models/lndhub_user.rb
Normal 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,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
|
||||
|
@ -1,7 +1,12 @@
|
||||
<%= render HeaderComponent.new(title: "Admin Panel") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<p class="text-center">
|
||||
<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 %>
|
||||
|
@ -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' %>
|
||||
|
@ -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
app/views/admin/lightning/index.html.erb
Normal file
48
app/views/admin/lightning/index.html.erb
Normal 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,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 %>
|
||||
<p>
|
||||
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
@ -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) %>
|
||||
|
@ -10,17 +10,27 @@ default: &default
|
||||
timeout: 5000
|
||||
|
||||
development:
|
||||
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:
|
||||
primary:
|
||||
<<: *default
|
||||
database: db/test.sqlite3
|
||||
lndhub:
|
||||
<<: *default
|
||||
database_tasks: false
|
||||
database: db/test.lndhub.sqlite3
|
||||
|
||||
production:
|
||||
primary:
|
||||
<<: *default
|
||||
adapter: postgresql
|
||||
database: akkounts
|
||||
@ -28,3 +38,12 @@ production:
|
||||
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 %>
|
||||
|
@ -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
|
||||
|
1
public/img/illustrations/undraw_vault_re_s4my.svg
Normal file
1
public/img/illustrations/undraw_vault_re_s4my.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
15
spec/components/quickstats_container_component_spec.rb
Normal file
15
spec/components/quickstats_container_component_spec.rb
Normal file
@ -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
|
15
spec/components/quickstats_item_component_spec.rb
Normal file
15
spec/components/quickstats_item_component_spec.rb
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user