mirror of
https://github.com/Such-Software/xmrcheckout.git
synced 2026-10-17 05:51:34 +02:00
Surface Monero health status in UI and docs
This commit is contained in:
@@ -147,6 +147,48 @@ If you’re running the bundled `monerod` service:
|
|||||||
docker compose --profile local-daemon up --build -d
|
docker compose --profile local-daemon up --build -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Troubleshooting first payment detection
|
||||||
|
|
||||||
|
If an invoice stays at `0` confirmations or never moves out of `Awaiting funds`, check these in order:
|
||||||
|
|
||||||
|
1. Open the dashboard overview and confirm:
|
||||||
|
- `Wallet RPC` shows `ok`
|
||||||
|
- `Daemon` shows `connected`
|
||||||
|
- `Current daemon height` is visible and keeps increasing
|
||||||
|
- `Last successful reconcile` is recent
|
||||||
|
|
||||||
|
2. Open the public invoice page and confirm the same health indicators are present there. If block height is unavailable, the node path is broken before invoice detection can work.
|
||||||
|
|
||||||
|
3. If the bundled wallet-rpc containers keep restarting, inspect their logs:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose logs wallet-rpc-reconciler-1
|
||||||
|
docker compose logs wallet-rpc-reconciler-2
|
||||||
|
docker compose logs wallet-rpc-reconciler-3
|
||||||
|
```
|
||||||
|
|
||||||
|
Common causes:
|
||||||
|
- wrong `MONERO_WALLET_RPC_WALLET_PASSWORD`
|
||||||
|
- wallet cache file does not match the `.keys` file
|
||||||
|
- daemon URL is unreachable from the wallet-rpc containers
|
||||||
|
|
||||||
|
4. If you are running the bundled `monerod`, check whether it is synced:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose logs monerod
|
||||||
|
```
|
||||||
|
|
||||||
|
Initial sync can take a long time. Payment detection is not reliable until the daemon is caught up.
|
||||||
|
|
||||||
|
5. If the reconciler is not reporting a recent successful scan, inspect the API and reconciler logs:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose logs api
|
||||||
|
docker compose logs reconciler
|
||||||
|
```
|
||||||
|
|
||||||
|
The reconciler is what updates invoice detection and confirmations. If it is down, invoices will not advance even when wallet-rpc and the daemon are healthy.
|
||||||
|
|
||||||
### Optional: Postgres backups (disabled by default)
|
### Optional: Postgres backups (disabled by default)
|
||||||
|
|
||||||
This repository includes an optional `db-backup` service that runs `pg_dump` hourly and writes backups to `./backups/postgres` on the host.
|
This repository includes an optional `db-backup` service that runs `pg_dump` hourly and writes backups to `./backups/postgres` on the host.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import BtcpayCheckoutStyleSection from "../../../components/btcpay-checkout-styl
|
|||||||
import DefaultConfirmationTargetSection from "../../../components/default-confirmation-target-section";
|
import DefaultConfirmationTargetSection from "../../../components/default-confirmation-target-section";
|
||||||
import DefaultQrLogoSection from "../../../components/default-qr-logo-section";
|
import DefaultQrLogoSection from "../../../components/default-qr-logo-section";
|
||||||
import InvoicePanel from "../../../components/invoice-panel";
|
import InvoicePanel from "../../../components/invoice-panel";
|
||||||
|
import { formatRelativeTime } from "../../../components/relative-time";
|
||||||
import WebhookHistoryPanel from "../../../components/webhook-history-panel";
|
import WebhookHistoryPanel from "../../../components/webhook-history-panel";
|
||||||
import WebhookSecretSection from "../../../components/webhook-secret-section";
|
import WebhookSecretSection from "../../../components/webhook-secret-section";
|
||||||
import WebhookSection from "../../../components/webhook-section";
|
import WebhookSection from "../../../components/webhook-section";
|
||||||
@@ -75,6 +76,15 @@ type ProfileSummary = {
|
|||||||
btcpay_checkout_style?: "standard" | "btcpay_classic";
|
btcpay_checkout_style?: "standard" | "btcpay_classic";
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
type SystemStatusSummary = {
|
||||||
|
wallet_rpc: "ok" | "unreachable";
|
||||||
|
daemon: "ok" | "unreachable" | "unknown";
|
||||||
|
daemon_height: number | null;
|
||||||
|
invoice_reconcile_interval_seconds: number;
|
||||||
|
last_reconcile_started_at: string | null;
|
||||||
|
last_reconcile_completed_at: string | null;
|
||||||
|
last_reconcile_error: string | null;
|
||||||
|
};
|
||||||
export default async function DashboardPage({
|
export default async function DashboardPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
@@ -205,6 +215,7 @@ export default async function DashboardPage({
|
|||||||
|
|
||||||
const allInvoices = invoicesData?.items ?? [];
|
const allInvoices = invoicesData?.items ?? [];
|
||||||
let profileData: ProfileSummary | null = null;
|
let profileData: ProfileSummary | null = null;
|
||||||
|
let systemStatus: SystemStatusSummary | null = null;
|
||||||
|
|
||||||
if (activeTab === "overview" || activeTab === "profile" || activeTab === "invoices") {
|
if (activeTab === "overview" || activeTab === "profile" || activeTab === "invoices") {
|
||||||
const response = await fetch(`${apiBaseUrl}/api/core/profile`, {
|
const response = await fetch(`${apiBaseUrl}/api/core/profile`, {
|
||||||
@@ -220,6 +231,14 @@ export default async function DashboardPage({
|
|||||||
profileData = (await response.json()) as ProfileSummary;
|
profileData = (await response.json()) as ProfileSummary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (activeTab === "overview") {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/api/core/public/system/status`, {
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
systemStatus = (await response.json()) as SystemStatusSummary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isToday = (value: string) => {
|
const isToday = (value: string) => {
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
@@ -240,7 +259,12 @@ export default async function DashboardPage({
|
|||||||
const confirmedCount = allInvoices.filter(
|
const confirmedCount = allInvoices.filter(
|
||||||
(invoice) => invoice.status === "confirmed"
|
(invoice) => invoice.status === "confirmed"
|
||||||
).length;
|
).length;
|
||||||
|
const lastReconcileCompleted = systemStatus?.last_reconcile_completed_at
|
||||||
|
? formatRelativeTime(systemStatus.last_reconcile_completed_at)
|
||||||
|
: null;
|
||||||
|
const lastReconcileStarted = systemStatus?.last_reconcile_started_at
|
||||||
|
? formatRelativeTime(systemStatus.last_reconcile_started_at)
|
||||||
|
: null;
|
||||||
const tabBaseClass =
|
const tabBaseClass =
|
||||||
"inline-flex items-center rounded-xl border border-stroke bg-white/70 px-4 py-2 text-xs font-semibold uppercase tracking-[0.12em] text-ink-soft transition hover:-translate-y-0.5";
|
"inline-flex items-center rounded-xl border border-stroke bg-white/70 px-4 py-2 text-xs font-semibold uppercase tracking-[0.12em] text-ink-soft transition hover:-translate-y-0.5";
|
||||||
const tabActiveClass =
|
const tabActiveClass =
|
||||||
@@ -346,6 +370,83 @@ export default async function DashboardPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4 rounded-2xl border border-stroke bg-white/70 p-5 shadow-soft">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Monero connectivity
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.12em]">
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 ${
|
||||||
|
systemStatus?.wallet_rpc === "ok"
|
||||||
|
? "bg-emerald-100 text-emerald-900"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Wallet RPC {systemStatus?.wallet_rpc === "ok" ? "ok" : "down"}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 ${
|
||||||
|
systemStatus?.daemon === "ok"
|
||||||
|
? "bg-emerald-100 text-emerald-900"
|
||||||
|
: systemStatus?.daemon === "unknown"
|
||||||
|
? "bg-amber-100 text-amber-900"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Daemon{" "}
|
||||||
|
{systemStatus?.daemon === "ok"
|
||||||
|
? "connected"
|
||||||
|
: systemStatus?.daemon === "unknown"
|
||||||
|
? "not configured"
|
||||||
|
: "down"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 grid gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Current daemon height
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold">
|
||||||
|
{systemStatus?.daemon_height?.toLocaleString() ?? "Unavailable"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Detection poll interval
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold">
|
||||||
|
{systemStatus?.invoice_reconcile_interval_seconds ?? 30}s
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Last successful reconcile
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold">
|
||||||
|
{lastReconcileCompleted ?? "Not yet recorded"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Last reconcile start
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold">
|
||||||
|
{lastReconcileStarted ?? "Not yet recorded"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm text-ink-soft">
|
||||||
|
If payments stay at 0 confirmations, check that the daemon is connected
|
||||||
|
and that the height keeps advancing.
|
||||||
|
</p>
|
||||||
|
{systemStatus?.last_reconcile_error ? (
|
||||||
|
<p className="mt-3 rounded-xl bg-red-100 px-3 py-2 text-sm text-red-700">
|
||||||
|
Last reconciler error: {systemStatus.last_reconcile_error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="mt-5 rounded-2xl border border-ink/10 bg-ink/10 px-4 py-3 text-sm font-semibold text-ink">
|
<div className="mt-5 rounded-2xl border border-ink/10 bg-ink/10 px-4 py-3 text-sm font-semibold text-ink">
|
||||||
We never hold funds. All payments move from the customer to your
|
We never hold funds. All payments move from the customer to your
|
||||||
wallet.
|
wallet.
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ const endpointGroups = [
|
|||||||
path: "/api/core/public/invoice/{invoice_id}/continue",
|
path: "/api/core/public/invoice/{invoice_id}/continue",
|
||||||
description: "After confirmation, redirect to the merchant-provided Continue URL (if configured).",
|
description: "After confirmation, redirect to the merchant-provided Continue URL (if configured).",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: "/api/core/public/system/status",
|
||||||
|
description: "Read daemon, wallet-rpc, block height, and reconciler heartbeat without auth.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -257,6 +262,17 @@ const endpointRequirements = [
|
|||||||
"Returns a 302 redirect to the merchant-provided checkout_continue_url (if configured).",
|
"Returns a 302 redirect to the merchant-provided checkout_continue_url (if configured).",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: "/api/core/public/system/status",
|
||||||
|
auth: "None",
|
||||||
|
required: [],
|
||||||
|
optional: [],
|
||||||
|
notes: [
|
||||||
|
"Returns wallet-rpc reachability, daemon reachability, current daemon height, and reconciler heartbeat timestamps.",
|
||||||
|
"Use this endpoint to debug setup issues before sending a real payment.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/api/core/invoices",
|
path: "/api/core/invoices",
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ type InvoiceStatusResponse = {
|
|||||||
detected_at: string | null;
|
detected_at: string | null;
|
||||||
confirmed_at: string | null;
|
confirmed_at: string | null;
|
||||||
};
|
};
|
||||||
|
type SystemStatusResponse = {
|
||||||
|
wallet_rpc: "ok" | "unreachable";
|
||||||
|
daemon: "ok" | "unreachable" | "unknown";
|
||||||
|
daemon_height: number | null;
|
||||||
|
invoice_reconcile_interval_seconds: number;
|
||||||
|
last_reconcile_started_at: string | null;
|
||||||
|
last_reconcile_completed_at: string | null;
|
||||||
|
last_reconcile_error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
const formatStatus = (status: InvoiceStatus) => {
|
const formatStatus = (status: InvoiceStatus) => {
|
||||||
if (status === "payment_detected") {
|
if (status === "payment_detected") {
|
||||||
@@ -98,10 +107,12 @@ export default async function InvoiceStatusDetailPage({
|
|||||||
params: Promise<{ invoiceId: string }>;
|
params: Promise<{ invoiceId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { invoiceId } = await params;
|
const { invoiceId } = await params;
|
||||||
const response = await fetch(
|
const [response, systemStatusResponse] = await Promise.all([
|
||||||
`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`,
|
fetch(`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`, {
|
||||||
{ cache: "no-store" }
|
cache: "no-store",
|
||||||
);
|
}),
|
||||||
|
fetch(`${apiBaseUrl}/api/core/public/system/status`, { cache: "no-store" }),
|
||||||
|
]);
|
||||||
|
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return (
|
return (
|
||||||
@@ -157,6 +168,9 @@ export default async function InvoiceStatusDetailPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const invoice = (await response.json()) as InvoiceStatusResponse;
|
const invoice = (await response.json()) as InvoiceStatusResponse;
|
||||||
|
const systemStatus = systemStatusResponse.ok
|
||||||
|
? ((await systemStatusResponse.json()) as SystemStatusResponse)
|
||||||
|
: null;
|
||||||
const statusLabel = formatStatus(invoice.status);
|
const statusLabel = formatStatus(invoice.status);
|
||||||
const confirmations = Math.max(0, invoice.confirmations ?? 0);
|
const confirmations = Math.max(0, invoice.confirmations ?? 0);
|
||||||
const confirmationTarget = Math.max(0, invoice.confirmation_target);
|
const confirmationTarget = Math.max(0, invoice.confirmation_target);
|
||||||
@@ -204,6 +218,7 @@ export default async function InvoiceStatusDetailPage({
|
|||||||
const showConfirmedStamp = invoice.status === "confirmed";
|
const showConfirmedStamp = invoice.status === "confirmed";
|
||||||
const createdTimestamp = formatTimestamp(invoice.created_at);
|
const createdTimestamp = formatTimestamp(invoice.created_at);
|
||||||
const expiresTimestamp = formatTimestamp(invoice.expires_at);
|
const expiresTimestamp = formatTimestamp(invoice.expires_at);
|
||||||
|
const lastReconcileCompleted = formatTimestamp(systemStatus?.last_reconcile_completed_at ?? null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="px-[6vw] pb-20 pt-10 text-ink">
|
<main className="px-[6vw] pb-20 pt-10 text-ink">
|
||||||
@@ -230,6 +245,9 @@ export default async function InvoiceStatusDetailPage({
|
|||||||
</summary>
|
</summary>
|
||||||
<div className="mt-3 grid gap-2">
|
<div className="mt-3 grid gap-2">
|
||||||
<p className="font-mono text-xs text-ink">Invoice ID: {invoiceId}</p>
|
<p className="font-mono text-xs text-ink">Invoice ID: {invoiceId}</p>
|
||||||
|
<p className="font-mono text-xs text-ink">
|
||||||
|
Current block height: {systemStatus?.daemon_height?.toLocaleString() ?? "Unavailable"}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This status page is public. Anyone with the link can view the current
|
This status page is public. Anyone with the link can view the current
|
||||||
invoice state.
|
invoice state.
|
||||||
@@ -259,6 +277,74 @@ export default async function InvoiceStatusDetailPage({
|
|||||||
<StatusRefreshButton label="Refresh" />
|
<StatusRefreshButton label="Refresh" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4 rounded-xl border border-stroke bg-white/70 p-4">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Monero node
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.12em]">
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 ${
|
||||||
|
systemStatus?.wallet_rpc === "ok"
|
||||||
|
? "bg-emerald-100 text-emerald-900"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Wallet RPC {systemStatus?.wallet_rpc === "ok" ? "ok" : "down"}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 ${
|
||||||
|
systemStatus?.daemon === "ok"
|
||||||
|
? "bg-emerald-100 text-emerald-900"
|
||||||
|
: systemStatus?.daemon === "unknown"
|
||||||
|
? "bg-amber-100 text-amber-900"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Daemon{" "}
|
||||||
|
{systemStatus?.daemon === "ok"
|
||||||
|
? "connected"
|
||||||
|
: systemStatus?.daemon === "unknown"
|
||||||
|
? "not configured"
|
||||||
|
: "down"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Current block height
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm font-semibold">
|
||||||
|
{systemStatus?.daemon_height?.toLocaleString() ?? "Unavailable"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Detection poll
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm font-semibold">
|
||||||
|
{systemStatus?.invoice_reconcile_interval_seconds ?? 30}s
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Last successful scan
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className="mt-1 text-sm font-semibold"
|
||||||
|
title={lastReconcileCompleted.relative ?? undefined}
|
||||||
|
>
|
||||||
|
{lastReconcileCompleted.label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{systemStatus?.last_reconcile_error ? (
|
||||||
|
<p className="mt-3 rounded-xl bg-red-100 px-3 py-2 text-sm text-red-700">
|
||||||
|
Reconciler error: {systemStatus.last_reconcile_error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="mt-6 grid gap-4 sm:grid-cols-3">
|
<div className="mt-6 grid gap-4 sm:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ type InvoiceStatusResponse = {
|
|||||||
quoted_at: string;
|
quoted_at: string;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
type SystemStatusResponse = {
|
||||||
|
wallet_rpc: "ok" | "unreachable";
|
||||||
|
daemon: "ok" | "unreachable" | "unknown";
|
||||||
|
daemon_height: number | null;
|
||||||
|
invoice_reconcile_interval_seconds: number;
|
||||||
|
last_reconcile_started_at: string | null;
|
||||||
|
last_reconcile_completed_at: string | null;
|
||||||
|
last_reconcile_error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
const formatStatus = (status: InvoiceStatus) => {
|
const formatStatus = (status: InvoiceStatus) => {
|
||||||
if (status === "payment_detected") {
|
if (status === "payment_detected") {
|
||||||
@@ -93,10 +102,12 @@ export default async function BtcpayModalInvoicePage({
|
|||||||
params: Promise<{ invoiceId: string }>;
|
params: Promise<{ invoiceId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { invoiceId } = await params;
|
const { invoiceId } = await params;
|
||||||
const response = await fetch(
|
const [response, systemStatusResponse] = await Promise.all([
|
||||||
`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`,
|
fetch(`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`, {
|
||||||
{ cache: "no-store" }
|
cache: "no-store",
|
||||||
);
|
}),
|
||||||
|
fetch(`${apiBaseUrl}/api/core/public/system/status`, { cache: "no-store" }),
|
||||||
|
]);
|
||||||
|
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return (
|
return (
|
||||||
@@ -139,6 +150,9 @@ export default async function BtcpayModalInvoicePage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const invoice = (await response.json()) as InvoiceStatusResponse;
|
const invoice = (await response.json()) as InvoiceStatusResponse;
|
||||||
|
const systemStatus = systemStatusResponse.ok
|
||||||
|
? ((await systemStatusResponse.json()) as SystemStatusResponse)
|
||||||
|
: null;
|
||||||
const statusLabel = formatStatus(invoice.status);
|
const statusLabel = formatStatus(invoice.status);
|
||||||
const confirmations = Math.max(0, invoice.confirmations ?? 0);
|
const confirmations = Math.max(0, invoice.confirmations ?? 0);
|
||||||
const confirmationTarget = Math.max(0, invoice.confirmation_target);
|
const confirmationTarget = Math.max(0, invoice.confirmation_target);
|
||||||
@@ -146,6 +160,7 @@ export default async function BtcpayModalInvoicePage({
|
|||||||
invoice.status === "payment_detected" || invoice.status === "confirmed";
|
invoice.status === "payment_detected" || invoice.status === "confirmed";
|
||||||
const createdTimestamp = formatTimestamp(invoice.created_at);
|
const createdTimestamp = formatTimestamp(invoice.created_at);
|
||||||
const expiresTimestamp = formatTimestamp(invoice.expires_at);
|
const expiresTimestamp = formatTimestamp(invoice.expires_at);
|
||||||
|
const lastReconcileCompleted = formatTimestamp(systemStatus?.last_reconcile_completed_at ?? null);
|
||||||
const isBtcpayInvoice = Boolean(invoice.btcpay_amount && invoice.btcpay_currency);
|
const isBtcpayInvoice = Boolean(invoice.btcpay_amount && invoice.btcpay_currency);
|
||||||
const useClassicCheckout =
|
const useClassicCheckout =
|
||||||
isBtcpayInvoice && invoice.btcpay_checkout_style === "btcpay_classic";
|
isBtcpayInvoice && invoice.btcpay_checkout_style === "btcpay_classic";
|
||||||
@@ -246,6 +261,74 @@ export default async function BtcpayModalInvoicePage({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4 rounded-xl border border-stroke bg-white/70 p-4">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Monero node
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.12em]">
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 ${
|
||||||
|
systemStatus?.wallet_rpc === "ok"
|
||||||
|
? "bg-emerald-100 text-emerald-900"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Wallet RPC {systemStatus?.wallet_rpc === "ok" ? "ok" : "down"}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 ${
|
||||||
|
systemStatus?.daemon === "ok"
|
||||||
|
? "bg-emerald-100 text-emerald-900"
|
||||||
|
: systemStatus?.daemon === "unknown"
|
||||||
|
? "bg-amber-100 text-amber-900"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Daemon{" "}
|
||||||
|
{systemStatus?.daemon === "ok"
|
||||||
|
? "connected"
|
||||||
|
: systemStatus?.daemon === "unknown"
|
||||||
|
? "not configured"
|
||||||
|
: "down"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Current block height
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm font-semibold">
|
||||||
|
{systemStatus?.daemon_height?.toLocaleString() ?? "Unavailable"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Detection poll
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm font-semibold">
|
||||||
|
{systemStatus?.invoice_reconcile_interval_seconds ?? 30}s
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||||
|
Last successful scan
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className="mt-1 text-sm font-semibold"
|
||||||
|
title={lastReconcileCompleted.relative ?? undefined}
|
||||||
|
>
|
||||||
|
{lastReconcileCompleted.label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{systemStatus?.last_reconcile_error ? (
|
||||||
|
<p className="mt-3 rounded-xl bg-red-100 px-3 py-2 text-sm text-red-700">
|
||||||
|
Reconciler error: {systemStatus.last_reconcile_error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
{checkoutContinueAvailable ? (
|
{checkoutContinueAvailable ? (
|
||||||
<a
|
<a
|
||||||
className="mt-5 inline-flex w-fit items-center justify-center rounded-full bg-ink px-5 py-2 text-sm font-semibold text-cream shadow-[0_16px_30px_rgba(16,18,23,0.18)] transition hover:-translate-y-0.5"
|
className="mt-5 inline-flex w-fit items-center justify-center rounded-full bg-ink px-5 py-2 text-sm font-semibold text-cream shadow-[0_16px_30px_rgba(16,18,23,0.18)] transition hover:-translate-y-0.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user