refactor: move /node into vue component (#3460)

This commit is contained in:
dni ⚡ 2025-11-06 09:45:41 +01:00 committed by GitHub
parent d142d76148
commit 8506199c2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1349 additions and 1256 deletions

View file

@ -0,0 +1,3 @@
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block page
%} {% endblock %}

View file

@ -1,379 +0,0 @@
<q-tab-panel name="channels">
<q-dialog v-model="connectPeerDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<q-input
dense
type="text"
filled
v-model="connectPeerDialog.data.uri"
label="Node URI"
hint="pubkey@host:port"
></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('connect')"
color="primary"
@click="connectPeer"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="setFeeDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<label class="text-h6">Set Channel Fee</label>
<p class="text-caption" v-text="setFeeDialog.channel_id"></p>
<q-separator></q-separator>
<q-form class="q-gutter-md">
<q-input
dense
type="number"
filled
v-model.number="setFeeDialog.data.fee_ppm"
label="Fee Rate PPM"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="setFeeDialog.data.fee_base_msat"
label="Fee Base msat"
></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('set')"
color="primary"
@click="setChannelFee(setFeeDialog.channel_id)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="openChannelDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<q-input
dense
type="text"
filled
v-model="openChannelDialog.data.peer_id"
label="Peer ID"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="openChannelDialog.data.funding_amount"
label="Funding Amount"
></q-input>
<q-expansion-item icon="warning" label="Advanced">
<q-card>
<q-card-section>
<div class="column q-gutter-md">
<q-input
dense
type="number"
filled
v-model.number="openChannelDialog.data.push_amount"
label="Push Amount"
hint="This gifts sats to the other side!"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="openChannelDialog.data.fee_rate"
label="Fee Rate"
></q-input>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<div class="row q-mt-lg">
<q-btn
:label="$t('open')"
color="primary"
@click="openChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="closeChannelDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<div>
<q-checkbox v-model="closeChannelDialog.data.force" label="Force" />
</div>
<div class="row q-mt-lg">
<q-btn
:label="$t('close')"
color="primary"
@click="closeChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-card-section class="q-pa-none">
<div class="row q-col-gutter-lg">
<div class="col-12 col-xl-6">
<q-card class="full-height">
<q-card-section class="q-gutter-y-sm">
<div class="row items-center q-mt-none q-gutter-x-sm no-wrap">
<div class="col-grow text-h6 q-my-none col-grow">Channels</div>
<q-input
filled
dense
clearable
v-model="channels.filter"
placeholder="Search..."
class="col-auto"
></q-input>
<q-select
dense
size="sm"
style="min-width: 200px"
filled
multiple
clearable
v-model="stateFilters"
:options="this.states"
class="col-auto"
></q-select>
<q-btn
unelevated
color="primary"
size="md"
class="col-auto"
@click="showOpenChannelDialog()"
>
Open channel
</q-btn>
</div>
<div>
<div class="text-subtitle1 col-grow">Total</div>
<lnbits-channel-balance
:balance="this.totalBalance"
></lnbits-channel-balance>
</div>
<q-separator></q-separator>
<q-table
dense
flat
:rows="this.filteredChannels"
:filter="channels.filter"
no-data-label="No channels opened"
>
<template v-slot:header="props">
<q-tr :props="props" style="height: 0"> </q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<div class="q-pb-sm">
<div class="row items-center q-gutter-sm">
<div class="text-subtitle1" v-text="props.row.name"></div>
<div class="text-caption" v-if="props.row.peer_id">
<span>Peer ID</span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.peer_id)"
></q-btn>
</div>
<div class="text-caption col-grow">
<span>Fees</span>
<q-btn
size="xs"
flat
dense
icon="settings"
@click="showSetFeeDialog(props.row.id)"
></q-btn>
<span v-if="props.row.fee_ppm">
<span v-text="props.row.fee_ppm"></span> ppm,
<span v-text="props.row.fee_base_msat"></span> msat
</span>
</div>
<div class="text-caption" v-if="props.row.id">
<span>Channel ID</span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.id)"
></q-btn>
</div>
<div class="text-caption" v-if="props.row.short_id">
<span v-text="props.row.short_id"></span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.short_id)"
></q-btn>
</div>
<q-badge
rounded
:color="states.find(s => s.value == props.row.state)?.color"
v-text="states.find(s => s.value == props.row.state)?.label"
>
</q-badge>
<q-btn
:disable='props.row.state !== "active"'
flat
dense
size="md"
@click="showCloseChannelDialog(props.row)"
icon="cancel"
color="pink"
></q-btn>
</div>
<lnbits-channel-balance
:balance="props.row.balance"
:color="props.row.color"
></lnbits-channel-balance>
</div>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-xl-6">
<q-card class="full-height">
<q-card-section class="column q-gutter-y-sm">
<div
class="row items-center q-mt-none justify-between q-gutter-x-md no-wrap"
>
<div class="col-grow text-h6 q-my-none">Peers</div>
<q-input
filled
dense
clearable
v-model="peers.filter"
placeholder="Search..."
class="col-auto"
></q-input>
<q-btn
class="col-auto"
color="primary"
@click="connectPeerDialog.show = true"
>
Connect Peer
</q-btn>
</div>
<q-separator></q-separator>
<q-table
dense
flat
:rows="peers.data"
:filter="peers.filter"
no-data-label="No transactions made yet"
>
<template v-slot:header="props">
<q-tr :props="props" style="height: 0"> </q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<div class="row no-wrap items-center q-gutter-sm">
<div class="q-my-sm col-grow">
<div
class="text-subtitle1 text-bold"
v-text="props.row.alias"
></div>
<div class="row items-center q-gutter-sm">
<q-badge
:style="`background-color: #${props.row.color}`"
class="text-bold"
v-text="'#'+props.row.color"
>
</q-badge>
<div
class="text-bold"
v-text="shortenNodeId(props.row.id)"
></div>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.id)"
></q-btn>
<q-btn
size="xs"
flat
dense
icon="qr_code"
@click="showNodeInfoDialog(props.row)"
></q-btn>
</div>
</div>
<q-btn
unelevated
color="primary"
@click="showOpenChannelDialog(props.row.id)"
>
Open channel
</q-btn>
<q-btn
flat
dense
size="md"
@click="disconnectPeer(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</div>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-tab-panel>

View file

@ -1,68 +0,0 @@
<q-tab-panel name="dashboard">
<q-card-section class="q-pa-none">
<lnbits-node-info :info="this.info"></lnbits-node-info>
<div class="row q-col-gutter-lg q-mt-sm">
<div class="col-12 col-md-8 q-gutter-y-md">
<div class="row q-col-gutter-md q-pb-lg">
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('total_capacity')"
:msat="this.channel_stats.total_capacity"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat title="Balance" :msat="this.info.balance_msat" />
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Fees collected"
:msat="this.info.fees?.total_msat"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Onchain Balance"
:btc="this.info.onchain_balance_sat / 100000000"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Onchain Confirmed"
:btc="this.info.onchain_confirmed_sat / 100000000"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('avg_channel_size')"
:msat="this.channel_stats.avg_size"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('biggest_channel_size')"
:msat="this.channel_stats.biggest_size"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
</div>
</div>
<div class="column col-12 col-md-4 q-gutter-y-md">
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
<lnbits-channel-stats
:stats="this.channel_stats"
></lnbits-channel-stats>
</div>
</div>
</q-card-section>
</q-tab-panel>

View file

@ -1,314 +0,0 @@
<q-tab-panel name="transactions">
<q-card-section class="q-pa-none">
<q-dialog v-model="transactionDetailsDialog.show" position="top">
<q-card class="my-card">
<q-card-section>
<div class="text-center q-mb-lg">
<div
v-if="transactionDetailsDialog.data.isIn && transactionDetailsDialog.data.pending"
>
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
<span v-text="$t('payment_received')"></span>
</div>
<div class="row q-my-md">
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
<div class="col-9 text-wrap mono">
<span
v-text="transactionDetailsDialog.data.payment_hash"
></span>
<q-icon
name="content_copy"
@click="copyText(transactionDetailsDialog.data.payment_hash)"
size="1em"
color="grey"
class="q-mb-xs cursor-pointer"
/>
</div>
<div
class="row"
v-if="transactionDetailsDialog.data.preimage && !transactionDetailsDialog.data.pending"
>
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
<div class="col-9 text-wrap mono">
<span v-text="transactionDetailsDialog.data.preimage"></span>
<q-icon
name="content_copy"
@click="copyText(transactionDetailsDialog.data.preimage)"
size="1em"
color="grey"
class="q-mb-xs cursor-pointer"
/>
</div>
</div>
</div>
<div
v-if="transactionDetailsDialog.data.bolt11"
class="text-center q-mb-lg"
>
<a :href="'lightning:' + transactionDetailsDialog.data.bolt11">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode-vue
:value="'lightning:' + transactionDetailsDialog.data.bolt11.toUpperCase()"
:options="{width: 340}"
class="rounded-borders"
></qrcode-vue>
</q-responsive>
</a>
<q-btn
outline
color="grey"
@click="copyText(transactionDetailsDialog.data.bolt11)"
:label="$t('copy_invoice')"
class="q-mt-sm"
></q-btn>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
<div class="row q-col-gutter-md q-pb-lg"></div>
<div class="row q-col-gutter-lg">
<div class="col-12 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col text-h6 q-my-none">Payments</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
class="q-mb-md"
>
</q-input>
</div>
<q-table
dense
flat
:rows="paymentsTable.data"
:columns="paymentsTable.columns"
v-model:pagination="paymentsTable.pagination"
row-key="payment_hash"
no-data-label="No transactions made yet"
:filter="paymentsTable.filter"
@request="getPayments"
>
<template v-slot:body-cell-pending="props">
<q-td auto-width class="text-center">
<q-icon
v-if="!props.row.pending"
size="xs"
name="call_made"
color="green"
@click="showTransactionDetailsDialog(props.row)"
></q-icon>
<q-icon
v-else
size="xs"
name="settings_ethernet"
color="grey"
@click="showTransactionDetailsDialog(props.row)"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
<q-dialog
v-model="props.row.expand"
:props="props"
position="top"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<div v-if="props.row.isIn && props.row.pending">
<q-icon
name="settings_ethernet"
color="grey"
></q-icon>
<span v-text="$t('invoice_waiting')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
<div
v-if="props.row.bolt11"
class="text-center q-mb-lg"
>
<a :href="'lightning:' + props.row.bolt11">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode-vue
:value="'lightning:' + props.row.bolt11.toUpperCase()"
:options="{width: 340}"
class="rounded-borders"
></qrcode-vue>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(props.row.bolt11)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</div>
</div>
<div v-else-if="props.row.isPaid && props.row.isIn">
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
<span v-text="$t('payment_received')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div v-else-if="props.row.isPaid && props.row.isOut">
<q-icon
size="18px"
:name="'call_made'"
:color="'pink'"
></q-icon>
<span v-text="$t('payment_sent')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div v-else-if="props.row.isOut && props.row.pending">
<q-icon
name="settings_ethernet"
color="grey"
></q-icon>
<span v-text="$t('outgoing_payment_pending')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
</div>
</q-card>
</q-dialog>
</q-td>
</template>
<template v-slot:body-cell-date="props">
<q-td auto-width key="date" :props="props">
<lnbits-date :ts="props.row.time"></lnbits-date>
</q-td>
</template>
<template v-slot:body-cell-destination="props">
<q-td auto-width key="destination">
<div class="row items-center justify-between no-wrap">
<q-badge
:style="`background-color: #${props.row.destination?.color}`"
class="text-bold"
v-text="props.row.destination?.alias"
></q-badge>
<div>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(info.id)"
></q-btn>
<q-btn
size="xs"
flat
dense
icon="qr_code"
@click="showNodeInfoDialog(props.row.destination)"
></q-btn>
</div>
</div>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col text-h6 q-my-none">Invoices</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
class="q-mb-md"
>
</q-input>
</div>
<q-table
dense
flat
:rows="invoiceTable.data"
:columns="invoiceTable.columns"
v-model:pagination="invoiceTable.pagination"
no-data-label="No transactions made yet"
:filter="invoiceTable.filter"
@request="getInvoices"
>
<template v-slot:body-cell-pending="props">
<q-td auto-width class="text-center">
<q-icon
v-if="!props.row.pending"
size="xs"
name="call_received"
color="green"
@click="showTransactionDetailsDialog(props.row)"
></q-icon>
<q-icon
v-else
size="xs"
name="settings_ethernet"
color="grey"
@click="showTransactionDetailsDialog(props.row)"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
</q-td>
</template>
<template v-slot:body-cell-paid_at="props">
<q-td auto-width :props="props">
<lnbits-date
v-if="props.row.paid_at"
:ts="props.row.paid_at"
></lnbits-date>
</q-td>
</template>
<template v-slot:body-cell-expiry="props">
<q-td auto-width :props="props">
<lnbits-date
v-if="props.row.expiry"
:ts="props.row.expiry"
></lnbits-date>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-tab-panel>

View file

@ -1,46 +0,0 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
<q-dialog v-model="nodeInfoDialog.show" position="top">
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
</q-dialog>
<div class="row q-col-gutter-md justify-center">
<div class="col q-gutter-y-md">
<q-card>
<div class="q-pa-md">
<div class="q-gutter-y-md">
<q-tabs v-model="tab" active-color="primary" align="justify">
<q-tab
name="dashboard"
:label="$t('dashboard')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="channels"
:label="$t('channels')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="transactions"
:label="$t('transactions')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div>
</div>
<q-form name="settings_form" id="settings_form">
<q-tab-panels v-model="tab" animated>
{% include "node/_tab_dashboard.html" %} {% include
"node/_tab_channels.html" %} {% include "node/_tab_transactions.html"
%}
</q-tab-panels>
</q-form>
</q-card>
</div>
</div>
{% endblock %}

View file

@ -1,133 +0,0 @@
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
context %} {% block page %}
<div class="q-ma-lg-xl q-mx-auto q-ma-xl" style="max-width: 1048px">
<lnbits-node-info :info="this.info"></lnbits-node-info>
<div class="row q-col-gutter-lg q-mt-sm">
<div class="col-12 col-md-8 q-gutter-y-md">
<div class="row q-col-gutter-md q-pb-lg">
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('total_capacity')"
:msat="this.channel_stats.total_capacity"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('avg_channel_size')"
:msat="this.channel_stats.avg_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('biggest_channel_size')"
:msat="this.channel_stats.biggest_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
</div>
</div>
<div class="column col-12 col-md-4 q-gutter-y-md">
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
<lnbits-channel-stats :stats="this.channel_stats"></lnbits-channel-stats>
</div>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
<script>
window.app = Vue.createApp({
el: '#vue',
config: {
globalProperties: {
LNbits,
msg: 'hello'
}
},
mixins: [window.windowMixin],
data: function () {
return {
isSuperUser: false,
wallet: {},
tab: 'dashboard',
payments: 1000,
info: {},
channel_stats: {},
channels: [],
activeBalance: {},
ranks: {},
peers: [],
connectPeerDialog: {
show: false,
data: {}
},
openChannelDialog: {
show: false,
data: {}
},
closeChannelDialog: {
show: false,
data: {}
},
nodeInfoDialog: {
show: false,
data: {}
},
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
]
}
},
created: function () {
this.getInfo()
this.get1MLStats()
},
methods: {
formatMsat: function (msat) {
return LNbits.utils.formatMsat(msat)
},
api: function (method, url, data) {
return LNbits.api.request(method, '/node/public/api/v1' + url, {}, data)
},
getInfo: function () {
this.api('GET', '/info', {}).then(response => {
this.info = response.data
this.channel_stats = response.data.channel_stats
})
},
get1MLStats: function () {
this.api('GET', '/rank', {}).then(response => {
this.ranks = response.data
})
}
}
})
</script>
{% endblock %}

View file

@ -426,43 +426,6 @@ async def manifest(request: Request, usr: str):
} }
@generic_router.get("/node", response_class=HTMLResponse)
async def node(request: Request, user: User = Depends(check_admin)):
if not settings.lnbits_node_ui:
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
funding_source = get_funding_source()
_, balance = await funding_source.status()
return template_renderer().TemplateResponse(
request,
"node/index.html",
{
"user": user.json(),
"balance": balance,
"wallets": user.wallets[0].json(),
"ajax": _is_ajax_request(request),
},
)
@generic_router.get("/node/public", response_class=HTMLResponse)
async def node_public(request: Request):
if not settings.lnbits_public_node_ui:
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
funding_source = get_funding_source()
_, balance = await funding_source.status()
return template_renderer().TemplateResponse(
request,
"node/public.html",
{
"balance": balance,
},
)
@generic_router.get("/admin", response_class=HTMLResponse) @generic_router.get("/admin", response_class=HTMLResponse)
async def admin_index(request: Request, user: User = Depends(check_admin)): async def admin_index(request: Request, user: User = Depends(check_admin)):
if not settings.lnbits_admin_ui: if not settings.lnbits_admin_ui:
@ -517,14 +480,30 @@ async def audit_index(request: Request, user: User = Depends(check_admin)):
@generic_router.get("/payments", response_class=HTMLResponse) @generic_router.get("/payments", response_class=HTMLResponse)
async def empty_index(request: Request, user: User = Depends(check_user_exists)): async def empty_index(request: Request, user: User = Depends(check_user_exists)):
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
request,
"empty.html", "empty.html",
{ {
"request": request,
"user": user.json(), "user": user.json(),
}, },
) )
@generic_router.get("/node", response_class=HTMLResponse)
async def empty_admin_index(request: Request, admin: User = Depends(check_admin)):
return template_renderer().TemplateResponse(
request,
"empty.html",
{
"user": admin.json(),
},
)
@generic_router.get("/node/public", response_class=HTMLResponse)
async def node_public(request: Request):
return template_renderer().TemplateResponse(request, "empty_public.html")
@generic_router.get("/uuidv4/{hex_value}") @generic_router.get("/uuidv4/{hex_value}")
async def hex_to_uuid4(hex_value: str): async def hex_to_uuid4(hex_value: str):
try: try:

File diff suppressed because one or more lines are too long

View file

@ -656,3 +656,261 @@ window.app.component('separator-text', {
} }
} }
}) })
window.app.component('lnbits-node-ranks', {
props: ['ranks'],
data() {
return {
stats: [
{label: 'Capacity', key: 'capacity'},
{label: 'Channels', key: 'channelcount'},
{label: 'Age', key: 'age'},
{label: 'Growth', key: 'growth'},
{label: 'Availability', key: 'availability'}
]
}
},
template: `
<q-card class='q-my-none'>
<div class='column q-ma-md'>
<h5 class='text-subtitle1 text-bold q-my-none'>1ml Node Rank</h5>
<div v-for='stat in stats' class='q-gutter-sm'>
<div class='row items-center'>
<div class='col-9'>{{ stat.label }}</div>
<div class='col-3 text-subtitle1 text-bold'>
{{ (ranks && ranks[stat.key]) ?? '-' }}
</div>
</div>
</div>
</div>
</q-card>
`
})
window.app.component('lnbits-channel-stats', {
props: ['stats'],
data() {
return {
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
]
}
},
template: `
<q-card>
<div class='column q-ma-md'>
<h5 class='text-subtitle1 text-bold q-my-none'>Channels</h5>
<div v-for='state in states' class='q-gutter-sm'>
<div class='row'>
<div class='col-9'>
<q-badge rounded size='md' :color='state.color'>{{ state.label }}</q-badge>
</div>
<div class='col-3 text-subtitle1 text-bold'>
{{ (stats?.counts && stats.counts[state.value]) ?? "-" }}
</div>
</div>
</div>
</div>
</q-card>
`
})
window.app.component('lnbits-stat', {
props: ['title', 'amount', 'msat', 'btc'],
computed: {
value() {
return (
this.amount ??
(this.btc
? LNbits.utils.formatSat(this.btc)
: LNbits.utils.formatMsat(this.msat))
)
}
},
template: `
<q-card>
<q-card-section>
<div class='text-overline text-primary'>
{{ title }}
</div>
<div>
<span class='text-h4 text-bold q-my-none'>{{ value }}</span>
<span class='text-h5' v-if='msat != undefined'>sats</span>
<span class='text-h5' v-if='btc != undefined'>BTC</span>
</div>
</q-card-section>
</q-card>
`
})
window.app.component('lnbits-node-qrcode', {
props: ['info'],
mixins: [window.windowMixin],
template: `
<q-card class="my-card">
<q-card-section>
<div class="text-h6">
<div style="text-align: center">
<vue-qrcode
:value="info.addresses[0]"
:options="{width: 250}"
v-if='info.addresses[0]'
class="rounded-borders"
></vue-qrcode>
<div v-else class='text-subtitle1'>
No addresses available
</div>
</div>
</div>
</q-card-section>
<q-card-actions vertical>
<q-btn
dense
unelevated
size="md"
@click="copyText(info.id)"
>Public Key<q-tooltip> Click to copy </q-tooltip>
</q-btn>
</q-card-actions>
</q-card>
`
})
window.app.component('lnbits-channel-balance', {
props: ['balance', 'color'],
methods: {
formatMsat(msat) {
return LNbits.utils.formatMsat(msat)
}
},
template: `
<div>
<div class="row items-center justify-between">
<span class="text-weight-thin">
Local: {{ formatMsat(balance.local_msat) }}
sats
</span>
<span class="text-weight-thin">
Remote: {{ formatMsat(balance.remote_msat) }}
sats
</span>
</div>
<q-linear-progress
rounded
size="25px"
:value="balance.local_msat / balance.total_msat"
:color="color"
:style="\`color: #\${this.color}\`"
>
<div class="absolute-full flex flex-center">
<q-badge
color="white"
text-color="accent"
:label="formatMsat(balance.total_msat) + ' sats'"
>
{{ balance.alias }}
</q-badge>
</div>
</q-linear-progress>
</div>
`
})
window.app.component('lnbits-date', {
props: ['ts'],
computed: {
date() {
return LNbits.utils.formatDate(this.ts)
},
dateFrom() {
return moment.utc(this.date).local().fromNow()
}
},
template: `
<div>
<q-tooltip>{{ this.date }}</q-tooltip>
{{ this.dateFrom }}
</div>
`
})
window.app.component('lnbits-node-info', {
props: ['info'],
data() {
return {
showDialog: false
}
},
mixins: [window.windowMixin],
methods: {
shortenNodeId(nodeId) {
return nodeId
? nodeId.substring(0, 5) + '...' + nodeId.substring(nodeId.length - 5)
: '...'
}
},
template: `
<div class='row items-baseline q-gutter-x-sm'>
<div class='text-h4 text-bold'>{{ this.info.alias }}</div>
<div class='row items-center q-gutter-sm'>
<div class='text-subtitle1 text-light'>{{ this.info.backend_name }}</div>
<q-badge
:style='\`background-color: #\${this.info.color}\`'
class='text-bold'
>
#{{ this.info.color }}
</q-badge>
<div class='text-bold'>{{ shortenNodeId(this.info.id) }}</div>
<q-btn
size='xs'
flat
dense
icon='content_paste'
@click='copyText(info.id)'
></q-btn>
<q-btn
size='xs'
flat
dense
icon='qr_code'
@click='showDialog = true'
></q-btn>
</div>
<q-dialog v-model="showDialog">
<lnbits-node-qrcode :info='info'></lnbits-node-qrcode>
</q-dialog>
</div>
`
})
window.app.component('lnbits-stat', {
props: ['title', 'amount', 'msat', 'btc'],
computed: {
value() {
return (
this.amount ??
(this.btc
? LNbits.utils.formatSat(this.btc)
: LNbits.utils.formatMsat(this.msat))
)
}
},
template: `
<q-card>
<q-card-section>
<div class='text-overline text-primary'>
{{ title }}
</div>
<div>
<span class='text-h4 text-bold q-my-none'>{{ value }}</span>
<span class='text-h5' v-if='msat != undefined'>sats</span>
<span class='text-h5' v-if='btc != undefined'>BTC</span>
</div>
</q-card-section>
</q-card>
`
})

View file

@ -205,16 +205,12 @@ const routes = [
{ {
path: '/node', path: '/node',
name: 'Node', name: 'Node',
component: DynamicComponent, component: PageNode
props: {
fetchUrl: '/node',
scripts: ['/static/js/node.js']
}
}, },
{ {
path: '/node', path: '/node/public',
name: 'Node', name: 'NodePublic',
component: DynamicComponent component: PageNodePublic
}, },
{ {
path: '/payments', path: '/payments',

View file

@ -0,0 +1,74 @@
window.PageNodePublic = {
template: '#page-node-public',
mixins: [window.windowMixin],
data() {
return {
enabled: false,
isSuperUser: false,
wallet: {},
tab: 'dashboard',
payments: 1000,
info: {},
channel_stats: {},
channels: [],
activeBalance: {},
ranks: {},
peers: [],
connectPeerDialog: {
show: false,
data: {}
},
openChannelDialog: {
show: false,
data: {}
},
closeChannelDialog: {
show: false,
data: {}
},
nodeInfoDialog: {
show: false,
data: {}
},
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
]
}
},
created() {
this.getInfo()
this.get1MLStats()
},
methods: {
formatMsat(msat) {
return LNbits.utils.formatMsat(msat)
},
api(method, url, data) {
return LNbits.api.request(method, '/node/public/api/v1' + url, {}, data)
},
getInfo() {
this.api('GET', '/info', {})
.then(response => {
this.info = response.data
this.channel_stats = response.data.channel_stats
this.enabled = true
})
.catch(() => {
this.info = {}
this.channel_stats = {}
})
},
get1MLStats() {
this.api('GET', '/rank', {})
.then(response => {
this.ranks = response.data
})
.catch(() => {
this.ranks = {}
})
}
}
}

View file

@ -1,5 +1,6 @@
window.NodePageLogic = { window.PageNode = {
mixins: [window.windowMixin], mixins: [window.windowMixin],
template: '#page-node',
config: { config: {
globalProperties: { globalProperties: {
LNbits, LNbits,
@ -225,15 +226,24 @@ window.NodePageLogic = {
}) })
}, },
getInfo() { getInfo() {
return this.api('GET', '/info').then(response => { return this.api('GET', '/info')
.then(response => {
this.info = response.data this.info = response.data
this.channel_stats = response.data.channel_stats this.channel_stats = response.data.channel_stats
}) })
.catch(() => {
this.info = {}
this.channel_stats = {}
})
}, },
get1MLStats() { get1MLStats() {
return this.api('GET', '/rank').then(response => { return this.api('GET', '/rank')
.then(response => {
this.ranks = response.data this.ranks = response.data
}) })
.catch(() => {
this.ranks = {}
})
}, },
getPayments(props) { getPayments(props) {
if (props) { if (props) {
@ -266,7 +276,6 @@ window.NodePageLogic = {
getPeers() { getPeers() {
return this.api('GET', '/peers').then(response => { return this.api('GET', '/peers').then(response => {
this.peers.data = response.data this.peers.data = response.data
console.log('peers', this.peers)
}) })
}, },
connectPeer() { connectPeer() {
@ -343,7 +352,6 @@ window.NodePageLogic = {
showTransactionDetailsDialog(details) { showTransactionDetailsDialog(details) {
this.transactionDetailsDialog.show = true this.transactionDetailsDialog.show = true
this.transactionDetailsDialog.data = details this.transactionDetailsDialog.data = details
console.log('details', details)
}, },
shortenNodeId(nodeId) { shortenNodeId(nodeId) {
return nodeId return nodeId
@ -352,261 +360,3 @@ window.NodePageLogic = {
} }
} }
} }
window.app.component('lnbits-node-ranks', {
props: ['ranks'],
data() {
return {
stats: [
{label: 'Capacity', key: 'capacity'},
{label: 'Channels', key: 'channelcount'},
{label: 'Age', key: 'age'},
{label: 'Growth', key: 'growth'},
{label: 'Availability', key: 'availability'}
]
}
},
template: `
<q-card class='q-my-none'>
<div class='column q-ma-md'>
<h5 class='text-subtitle1 text-bold q-my-none'>1ml Node Rank</h5>
<div v-for='stat in stats' class='q-gutter-sm'>
<div class='row items-center'>
<div class='col-9'>{{ stat.label }}</div>
<div class='col-3 text-subtitle1 text-bold'>
{{ (ranks && ranks[stat.key]) ?? '-' }}
</div>
</div>
</div>
</div>
</q-card>
`
})
window.app.component('lnbits-channel-stats', {
props: ['stats'],
data() {
return {
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
]
}
},
template: `
<q-card>
<div class='column q-ma-md'>
<h5 class='text-subtitle1 text-bold q-my-none'>Channels</h5>
<div v-for='state in states' class='q-gutter-sm'>
<div class='row'>
<div class='col-9'>
<q-badge rounded size='md' :color='state.color'>{{ state.label }}</q-badge>
</div>
<div class='col-3 text-subtitle1 text-bold'>
{{ (stats?.counts && stats.counts[state.value]) ?? "-" }}
</div>
</div>
</div>
</div>
</q-card>
`
})
window.app.component('lnbits-stat', {
props: ['title', 'amount', 'msat', 'btc'],
computed: {
value() {
return (
this.amount ??
(this.btc
? LNbits.utils.formatSat(this.btc)
: LNbits.utils.formatMsat(this.msat))
)
}
},
template: `
<q-card>
<q-card-section>
<div class='text-overline text-primary'>
{{ title }}
</div>
<div>
<span class='text-h4 text-bold q-my-none'>{{ value }}</span>
<span class='text-h5' v-if='msat != undefined'>sats</span>
<span class='text-h5' v-if='btc != undefined'>BTC</span>
</div>
</q-card-section>
</q-card>
`
})
window.app.component('lnbits-node-qrcode', {
props: ['info'],
mixins: [window.windowMixin],
template: `
<q-card class="my-card">
<q-card-section>
<div class="text-h6">
<div style="text-align: center">
<vue-qrcode
:value="info.addresses[0]"
:options="{width: 250}"
v-if='info.addresses[0]'
class="rounded-borders"
></vue-qrcode>
<div v-else class='text-subtitle1'>
No addresses available
</div>
</div>
</div>
</q-card-section>
<q-card-actions vertical>
<q-btn
dense
unelevated
size="md"
@click="copyText(info.id)"
>Public Key<q-tooltip> Click to copy </q-tooltip>
</q-btn>
</q-card-actions>
</q-card>
`
})
window.app.component('lnbits-node-info', {
props: ['info'],
data() {
return {
showDialog: false
}
},
mixins: [window.windowMixin],
methods: {
shortenNodeId(nodeId) {
return nodeId
? nodeId.substring(0, 5) + '...' + nodeId.substring(nodeId.length - 5)
: '...'
}
},
template: `
<div class='row items-baseline q-gutter-x-sm'>
<div class='text-h4 text-bold'>{{ this.info.alias }}</div>
<div class='row items-center q-gutter-sm'>
<div class='text-subtitle1 text-light'>{{ this.info.backend_name }}</div>
<q-badge
:style='\`background-color: #\${this.info.color}\`'
class='text-bold'
>
#{{ this.info.color }}
</q-badge>
<div class='text-bold'>{{ shortenNodeId(this.info.id) }}</div>
<q-btn
size='xs'
flat
dense
icon='content_paste'
@click='copyText(info.id)'
></q-btn>
<q-btn
size='xs'
flat
dense
icon='qr_code'
@click='showDialog = true'
></q-btn>
</div>
<q-dialog v-model="showDialog">
<lnbits-node-qrcode :info='info'></lnbits-node-qrcode>
</q-dialog>
</div>
`
})
window.app.component('lnbits-stat', {
props: ['title', 'amount', 'msat', 'btc'],
computed: {
value() {
return (
this.amount ??
(this.btc
? LNbits.utils.formatSat(this.btc)
: LNbits.utils.formatMsat(this.msat))
)
}
},
template: `
<q-card>
<q-card-section>
<div class='text-overline text-primary'>
{{ title }}
</div>
<div>
<span class='text-h4 text-bold q-my-none'>{{ value }}</span>
<span class='text-h5' v-if='msat != undefined'>sats</span>
<span class='text-h5' v-if='btc != undefined'>BTC</span>
</div>
</q-card-section>
</q-card>
`
})
window.app.component('lnbits-channel-balance', {
props: ['balance', 'color'],
methods: {
formatMsat(msat) {
return LNbits.utils.formatMsat(msat)
}
},
template: `
<div>
<div class="row items-center justify-between">
<span class="text-weight-thin">
Local: {{ formatMsat(balance.local_msat) }}
sats
</span>
<span class="text-weight-thin">
Remote: {{ formatMsat(balance.remote_msat) }}
sats
</span>
</div>
<q-linear-progress
rounded
size="25px"
:value="balance.local_msat / balance.total_msat"
:color="color"
:style="\`color: #\${this.color}\`"
>
<div class="absolute-full flex flex-center">
<q-badge
color="white"
text-color="accent"
:label="formatMsat(balance.total_msat) + ' sats'"
>
{{ balance.alias }}
</q-badge>
</div>
</q-linear-progress>
</div>
`
})
window.app.component('lnbits-date', {
props: ['ts'],
computed: {
date() {
return LNbits.utils.formatDate(this.ts)
},
dateFrom() {
return moment.utc(this.date).local().fromNow()
}
},
template: `
<div>
<q-tooltip>{{ this.date }}</q-tooltip>
{{ this.dateFrom }}
</div>
`
})

View file

@ -44,6 +44,8 @@
], ],
"components": [ "components": [
"js/pages/payments.js", "js/pages/payments.js",
"js/pages/node.js",
"js/pages/node-public.js",
"js/components/lnbits-qrcode.js", "js/components/lnbits-qrcode.js",
"js/components/lnbits-qrcode-lnurl.js", "js/components/lnbits-qrcode-lnurl.js",
"js/components/lnbits-funding-sources.js", "js/components/lnbits-funding-sources.js",

View file

@ -483,7 +483,7 @@
window[key] = WINDOW_SETTINGS[key] window[key] = WINDOW_SETTINGS[key]
}) })
</script> </script>
{% include('components.vue') %} {% include('pages/payments.vue') %} {% block {% include('components.vue') %} {% include('pages.vue') %} {% block
vue_templates %}{% endblock %} {% for url in INCLUDED_JS %} vue_templates %}{% endblock %} {% for url in INCLUDED_JS %}
<script src="{{ static_url_for('static', url) }}"></script> <script src="{{ static_url_for('static', url) }}"></script>
{% endfor %} {% block scripts %}{% endblock %} {% for url in {% endfor %} {% block scripts %}{% endblock %} {% for url in

View file

@ -0,0 +1 @@
{% include('pages/payments.vue') %} {% include('pages/node.vue') %}

View file

@ -0,0 +1,968 @@
<template id="page-node">
<q-dialog v-model="nodeInfoDialog.show" position="top">
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
</q-dialog>
<div class="row q-col-gutter-md justify-center">
<div class="col q-gutter-y-md">
<q-card>
<div class="q-pa-md">
<div class="q-gutter-y-md">
<q-tabs v-model="tab" active-color="primary" align="justify">
<q-tab
name="dashboard"
:label="$t('dashboard')"
@update="val => (tab = val.name)"
></q-tab>
<q-tab
name="channels"
:label="$t('channels')"
@update="val => (tab = val.name)"
></q-tab>
<q-tab
name="transactions"
:label="$t('transactions')"
@update="val => (tab = val.name)"
></q-tab>
</q-tabs>
</div>
</div>
<q-form name="settings_form" id="settings_form">
<q-tab-panels v-model="tab" animated>
<q-tab-panel name="dashboard">
<q-card-section class="q-pa-none">
<lnbits-node-info :info="this.info"></lnbits-node-info>
<div class="row q-col-gutter-lg q-mt-sm">
<div class="col-12 col-md-8 q-gutter-y-md">
<div class="row q-col-gutter-md q-pb-lg">
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('total_capacity')"
:msat="this.channel_stats.total_capacity"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Balance"
:msat="this.info.balance_msat"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Fees collected"
:msat="this.info.fees?.total_msat"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Onchain Balance"
:btc="this.info.onchain_balance_sat / 100000000"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Onchain Confirmed"
:btc="this.info.onchain_confirmed_sat / 100000000"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Peers"
:amount="this.info.num_peers"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('avg_channel_size')"
:msat="this.channel_stats.avg_size"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('biggest_channel_size')"
:msat="this.channel_stats.biggest_size"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
</div>
</div>
<div class="column col-12 col-md-4 q-gutter-y-md">
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
<lnbits-channel-stats
:stats="this.channel_stats"
></lnbits-channel-stats>
</div>
</div>
</q-card-section>
</q-tab-panel>
<q-tab-panel name="channels">
<q-dialog v-model="connectPeerDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<q-input
dense
type="text"
filled
v-model="connectPeerDialog.data.uri"
label="Node URI"
hint="pubkey@host:port"
></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('connect')"
color="primary"
@click="connectPeer"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="setFeeDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<label class="text-h6">Set Channel Fee</label>
<p class="text-caption" v-text="setFeeDialog.channel_id"></p>
<q-separator></q-separator>
<q-form class="q-gutter-md">
<q-input
dense
type="number"
filled
v-model.number="setFeeDialog.data.fee_ppm"
label="Fee Rate PPM"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="setFeeDialog.data.fee_base_msat"
label="Fee Base msat"
></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('set')"
color="primary"
@click="setChannelFee(setFeeDialog.channel_id)"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="openChannelDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<q-input
dense
type="text"
filled
v-model="openChannelDialog.data.peer_id"
label="Peer ID"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="openChannelDialog.data.funding_amount"
label="Funding Amount"
></q-input>
<q-expansion-item icon="warning" label="Advanced">
<q-card>
<q-card-section>
<div class="column q-gutter-md">
<q-input
dense
type="number"
filled
v-model.number="
openChannelDialog.data.push_amount
"
label="Push Amount"
hint="This gifts sats to the other side!"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="openChannelDialog.data.fee_rate"
label="Fee Rate"
></q-input>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<div class="row q-mt-lg">
<q-btn
:label="$t('open')"
color="primary"
@click="openChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="closeChannelDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<div>
<q-checkbox
v-model="closeChannelDialog.data.force"
label="Force"
/>
</div>
<div class="row q-mt-lg">
<q-btn
:label="$t('close')"
color="primary"
@click="closeChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-card-section class="q-pa-none">
<div class="row q-col-gutter-lg">
<div class="col-12 col-xl-6">
<q-card class="full-height">
<q-card-section class="q-gutter-y-sm">
<div
class="row items-center q-mt-none q-gutter-x-sm no-wrap"
>
<div class="col-grow text-h6 q-my-none col-grow">
Channels
</div>
<q-input
filled
dense
clearable
v-model="channels.filter"
placeholder="Search..."
class="col-auto"
></q-input>
<q-select
dense
size="sm"
style="min-width: 200px"
filled
multiple
clearable
v-model="stateFilters"
:options="this.states"
class="col-auto"
></q-select>
<q-btn
unelevated
color="primary"
size="md"
class="col-auto"
@click="showOpenChannelDialog()"
>
Open channel
</q-btn>
</div>
<div>
<div class="text-subtitle1 col-grow">Total</div>
<lnbits-channel-balance
:balance="this.totalBalance"
></lnbits-channel-balance>
</div>
<q-separator></q-separator>
<q-table
dense
flat
:rows="this.filteredChannels"
:filter="channels.filter"
no-data-label="No channels opened"
>
<template v-slot:header="props">
<q-tr :props="props" style="height: 0"> </q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<div class="q-pb-sm">
<div class="row items-center q-gutter-sm">
<div
class="text-subtitle1"
v-text="props.row.name"
></div>
<div
class="text-caption"
v-if="props.row.peer_id"
>
<span>Peer ID</span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.peer_id)"
></q-btn>
</div>
<div class="text-caption col-grow">
<span>Fees</span>
<q-btn
size="xs"
flat
dense
icon="settings"
@click="showSetFeeDialog(props.row.id)"
></q-btn>
<span v-if="props.row.fee_ppm">
<span v-text="props.row.fee_ppm"></span>
ppm,
<span
v-text="props.row.fee_base_msat"
></span>
msat
</span>
</div>
<div class="text-caption" v-if="props.row.id">
<span>Channel ID</span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.id)"
></q-btn>
</div>
<div
class="text-caption"
v-if="props.row.short_id"
>
<span v-text="props.row.short_id"></span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.short_id)"
></q-btn>
</div>
<q-badge
rounded
:color="
states.find(
s => s.value == props.row.state
)?.color
"
v-text="
states.find(
s => s.value == props.row.state
)?.label
"
>
</q-badge>
<q-btn
:disable="props.row.state !== 'active'"
flat
dense
size="md"
@click="showCloseChannelDialog(props.row)"
icon="cancel"
color="pink"
></q-btn>
</div>
<lnbits-channel-balance
:balance="props.row.balance"
:color="props.row.color"
></lnbits-channel-balance>
</div>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-xl-6">
<q-card class="full-height">
<q-card-section class="column q-gutter-y-sm">
<div
class="row items-center q-mt-none justify-between q-gutter-x-md no-wrap"
>
<div class="col-grow text-h6 q-my-none">Peers</div>
<q-input
filled
dense
clearable
v-model="peers.filter"
placeholder="Search..."
class="col-auto"
></q-input>
<q-btn
class="col-auto"
color="primary"
@click="connectPeerDialog.show = true"
>
Connect Peer
</q-btn>
</div>
<q-separator></q-separator>
<q-table
dense
flat
:rows="peers.data"
:filter="peers.filter"
no-data-label="No transactions made yet"
>
<template v-slot:header="props">
<q-tr :props="props" style="height: 0"> </q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<div class="row no-wrap items-center q-gutter-sm">
<div class="q-my-sm col-grow">
<div
class="text-subtitle1 text-bold"
v-text="props.row.alias"
></div>
<div class="row items-center q-gutter-sm">
<q-badge
:style="`background-color: #${props.row.color}`"
class="text-bold"
v-text="'#' + props.row.color"
>
</q-badge>
<div
class="text-bold"
v-text="shortenNodeId(props.row.id)"
></div>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.id)"
></q-btn>
<q-btn
size="xs"
flat
dense
icon="qr_code"
@click="showNodeInfoDialog(props.row)"
></q-btn>
</div>
</div>
<q-btn
unelevated
color="primary"
@click="showOpenChannelDialog(props.row.id)"
>
Open channel
</q-btn>
<q-btn
flat
dense
size="md"
@click="disconnectPeer(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</div>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-tab-panel>
<q-tab-panel name="transactions">
<q-card-section class="q-pa-none">
<q-dialog
v-model="transactionDetailsDialog.show"
position="top"
>
<q-card class="my-card">
<q-card-section>
<div class="text-center q-mb-lg">
<div
v-if="
transactionDetailsDialog.data.isIn &&
transactionDetailsDialog.data.pending
"
>
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
<span v-text="$t('payment_received')"></span>
</div>
<div class="row q-my-md">
<div class="col-3">
<b v-text="$t('payment_hash')"></b>:
</div>
<div class="col-9 text-wrap mono">
<span
v-text="
transactionDetailsDialog.data.payment_hash
"
></span>
<q-icon
name="content_copy"
@click="
copyText(
transactionDetailsDialog.data.payment_hash
)
"
size="1em"
color="grey"
class="q-mb-xs cursor-pointer"
/>
</div>
<div
class="row"
v-if="
transactionDetailsDialog.data.preimage &&
!transactionDetailsDialog.data.pending
"
>
<div class="col-3">
<b v-text="$t('payment_proof')"></b>:
</div>
<div class="col-9 text-wrap mono">
<span
v-text="transactionDetailsDialog.data.preimage"
></span>
<q-icon
name="content_copy"
@click="
copyText(
transactionDetailsDialog.data.preimage
)
"
size="1em"
color="grey"
class="q-mb-xs cursor-pointer"
/>
</div>
</div>
</div>
<div
v-if="transactionDetailsDialog.data.bolt11"
class="text-center q-mb-lg"
>
<a
:href="
'lightning:' +
transactionDetailsDialog.data.bolt11
"
>
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode-vue
:value="
'lightning:' +
transactionDetailsDialog.data.bolt11.toUpperCase()
"
:options="{width: 340}"
class="rounded-borders"
></qrcode-vue>
</q-responsive>
</a>
<q-btn
outline
color="grey"
@click="
copyText(transactionDetailsDialog.data.bolt11)
"
:label="$t('copy_invoice')"
class="q-mt-sm"
></q-btn>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
<div class="row q-col-gutter-md q-pb-lg"></div>
<div class="row q-col-gutter-lg">
<div class="col-12 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col text-h6 q-my-none">Payments</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
class="q-mb-md"
>
</q-input>
</div>
<q-table
dense
flat
:rows="paymentsTable.data"
:columns="paymentsTable.columns"
v-model:pagination="paymentsTable.pagination"
row-key="payment_hash"
no-data-label="No transactions made yet"
:filter="paymentsTable.filter"
@request="getPayments"
>
<template v-slot:body-cell-pending="props">
<q-td auto-width class="text-center">
<q-icon
v-if="!props.row.pending"
size="xs"
name="call_made"
color="green"
@click="showTransactionDetailsDialog(props.row)"
></q-icon>
<q-icon
v-else
size="xs"
name="settings_ethernet"
color="grey"
@click="showTransactionDetailsDialog(props.row)"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
<q-dialog
v-model="props.row.expand"
:props="props"
position="top"
>
<q-card
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-center q-mb-lg">
<div
v-if="props.row.isIn && props.row.pending"
>
<q-icon
name="settings_ethernet"
color="grey"
></q-icon>
<span
v-text="$t('invoice_waiting')"
></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
<div
v-if="props.row.bolt11"
class="text-center q-mb-lg"
>
<a
:href="
'lightning:' + props.row.bolt11
"
>
<q-responsive
:ratio="1"
class="q-mx-xl"
>
<qrcode-vue
:value="
'lightning:' +
props.row.bolt11.toUpperCase()
"
:options="{width: 340}"
class="rounded-borders"
></qrcode-vue>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(props.row.bolt11)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</div>
</div>
<div
v-else-if="
props.row.isPaid && props.row.isIn
"
>
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
<span
v-text="$t('payment_received')"
></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div
v-else-if="
props.row.isPaid && props.row.isOut
"
>
<q-icon
size="18px"
:name="'call_made'"
:color="'pink'"
></q-icon>
<span v-text="$t('payment_sent')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div
v-else-if="
props.row.isOut && props.row.pending
"
>
<q-icon
name="settings_ethernet"
color="grey"
></q-icon>
<span
v-text="$t('outgoing_payment_pending')"
></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
</div>
</q-card>
</q-dialog>
</q-td>
</template>
<template v-slot:body-cell-date="props">
<q-td auto-width key="date" :props="props">
<lnbits-date :ts="props.row.time"></lnbits-date>
</q-td>
</template>
<template v-slot:body-cell-destination="props">
<q-td auto-width key="destination">
<div
class="row items-center justify-between no-wrap"
>
<q-badge
:style="`background-color: #${props.row.destination?.color}`"
class="text-bold"
v-text="props.row.destination?.alias"
></q-badge>
<div>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(info.id)"
></q-btn>
<q-btn
size="xs"
flat
dense
icon="qr_code"
@click="
showNodeInfoDialog(props.row.destination)
"
></q-btn>
</div>
</div>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col text-h6 q-my-none">Invoices</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
class="q-mb-md"
>
</q-input>
</div>
<q-table
dense
flat
:rows="invoiceTable.data"
:columns="invoiceTable.columns"
v-model:pagination="invoiceTable.pagination"
no-data-label="No transactions made yet"
:filter="invoiceTable.filter"
@request="getInvoices"
>
<template v-slot:body-cell-pending="props">
<q-td auto-width class="text-center">
<q-icon
v-if="!props.row.pending"
size="xs"
name="call_received"
color="green"
@click="showTransactionDetailsDialog(props.row)"
></q-icon>
<q-icon
v-else
size="xs"
name="settings_ethernet"
color="grey"
@click="showTransactionDetailsDialog(props.row)"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
</q-td>
</template>
<template v-slot:body-cell-paid_at="props">
<q-td auto-width :props="props">
<lnbits-date
v-if="props.row.paid_at"
:ts="props.row.paid_at"
></lnbits-date>
</q-td>
</template>
<template v-slot:body-cell-expiry="props">
<q-td auto-width :props="props">
<lnbits-date
v-if="props.row.expiry"
:ts="props.row.expiry"
></lnbits-date>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-tab-panel>
</q-tab-panels>
</q-form>
</q-card>
</div>
</div>
</template>
<template id="page-node-public">
<div
v-if="!enabled"
class="q-ma-lg-xl q-mx-auto q-ma-xl"
style="max-width: 1048px"
>
<h2>Node public page not enabled.</h2>
</div>
<div
v-if="enabled"
class="q-ma-lg-xl q-mx-auto q-ma-xl"
style="max-width: 1048px"
>
<lnbits-node-info :info="this.info"></lnbits-node-info>
<div class="row q-col-gutter-lg q-mt-sm">
<div class="col-12 col-md-8 q-gutter-y-md">
<div class="row q-col-gutter-md q-pb-lg">
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('total_capacity')"
:msat="this.channel_stats.total_capacity"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('avg_channel_size')"
:msat="this.channel_stats.avg_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('biggest_channel_size')"
:msat="this.channel_stats.biggest_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
</div>
</div>
<div class="column col-12 col-md-4 q-gutter-y-md">
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
<lnbits-channel-stats
:stats="this.channel_stats"
></lnbits-channel-stats>
</div>
</div>
</div>
</template>

View file

@ -96,6 +96,8 @@
], ],
"components": [ "components": [
"js/pages/payments.js", "js/pages/payments.js",
"js/pages/node.js",
"js/pages/node-public.js",
"js/components/lnbits-qrcode.js", "js/components/lnbits-qrcode.js",
"js/components/lnbits-qrcode-lnurl.js", "js/components/lnbits-qrcode-lnurl.js",
"js/components/lnbits-funding-sources.js", "js/components/lnbits-funding-sources.js",