mirror of
https://github.com/Such-Software/xmrcheckout.git
synced 2026-10-17 05:51:34 +02:00
merge upstream: system status, health monitoring, connectivity caching
Merges xmrcheckout/xmrcheckout main (7 commits since 655d037):
- Add Monero system status and reconciler heartbeat
- Surface Monero health status in UI (dashboard + invoice pages)
- Harden reconciler status heartbeat startup
- Avoid false Monero outage states in UI
- Cache Monero connectivity in system status
- Next.js security patch (15.5.9 → 15.5.14)
- Bump Monero Docker defaults to 0.18.4.6
Resolved conflicts in reconciler.py (kept our advisory lock + upstream
heartbeat), routes.py (kept both imports), README.md (kept both sections).
This commit is contained in:
@@ -160,6 +160,47 @@ reconciler:
|
||||
- "api.example.com:192.168.1.100"
|
||||
```
|
||||
|
||||
### 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)
|
||||
|
||||
This repository includes an optional `db-backup` service that runs `pg_dump` hourly and writes backups to `./backups/postgres` on the host.
|
||||
|
||||
@@ -255,6 +255,81 @@ def startup():
|
||||
connection.execute(
|
||||
text("CREATE INDEX IF NOT EXISTS ix_users_payment_address ON users (payment_address)")
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"CREATE TABLE IF NOT EXISTS system_status ("
|
||||
"name VARCHAR PRIMARY KEY, "
|
||||
"last_reconcile_started_at TIMESTAMP WITH TIME ZONE, "
|
||||
"last_reconcile_completed_at TIMESTAMP WITH TIME ZONE, "
|
||||
"last_reconcile_error VARCHAR, "
|
||||
"wallet_rpc VARCHAR, "
|
||||
"daemon VARCHAR, "
|
||||
"daemon_height INTEGER, "
|
||||
"checked_at TIMESTAMP WITH TIME ZONE, "
|
||||
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()"
|
||||
")"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS last_reconcile_started_at TIMESTAMP WITH TIME ZONE"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS last_reconcile_completed_at TIMESTAMP WITH TIME ZONE"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS last_reconcile_error VARCHAR"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS wallet_rpc VARCHAR"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS daemon VARCHAR"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS daemon_height INTEGER"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS checked_at TIMESTAMP WITH TIME ZONE"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"INSERT INTO system_status (name) VALUES ('reconciler') "
|
||||
"ON CONFLICT (name) DO NOTHING"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"INSERT INTO system_status (name) VALUES ('monero_connectivity') "
|
||||
"ON CONFLICT (name) DO NOTHING"
|
||||
)
|
||||
)
|
||||
finally:
|
||||
if lock_acquired:
|
||||
connection.execute(
|
||||
|
||||
@@ -143,3 +143,17 @@ class ProfileHistory(Base):
|
||||
new_value = Column(String, nullable=True)
|
||||
value_encrypted = Column(Boolean, nullable=False, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class SystemStatus(Base):
|
||||
__tablename__ = "system_status"
|
||||
|
||||
name = Column(String, primary_key=True)
|
||||
last_reconcile_started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_reconcile_completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_reconcile_error = Column(String, nullable=True)
|
||||
wallet_rpc = Column(String, nullable=True)
|
||||
daemon = Column(String, nullable=True)
|
||||
daemon_height = Column(Integer, nullable=True)
|
||||
checked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
@@ -98,12 +98,17 @@ class MoneroWalletService:
|
||||
self._daemon_address = _normalize_daemon_address(MONERO_DAEMON_URL)
|
||||
self._wallet_dir = MONERO_WALLET_RPC_WALLET_DIR.strip()
|
||||
|
||||
def get_status(self) -> dict[str, str]:
|
||||
def get_status(self) -> dict[str, str | int | None]:
|
||||
daemon_height = self._daemon_height()
|
||||
for backend in self._backends:
|
||||
try:
|
||||
backend.client.raw_request("get_version")
|
||||
except Exception:
|
||||
return {"wallet_rpc": "unreachable"}
|
||||
return {
|
||||
"wallet_rpc": "unreachable",
|
||||
"daemon": "ok" if daemon_height is not None else "unreachable",
|
||||
"daemon_height": daemon_height,
|
||||
}
|
||||
|
||||
if self._daemon_address:
|
||||
try:
|
||||
@@ -114,8 +119,16 @@ class MoneroWalletService:
|
||||
except RPCError as exc:
|
||||
message = str(exc)
|
||||
if "no connection to daemon" in message or "no_connection_to_daemon" in message:
|
||||
return {"wallet_rpc": "ok", "daemon": "unreachable"}
|
||||
return {"wallet_rpc": "ok", "daemon": "ok" if self._daemon_address else "unknown"}
|
||||
return {
|
||||
"wallet_rpc": "ok",
|
||||
"daemon": "unreachable",
|
||||
"daemon_height": daemon_height,
|
||||
}
|
||||
return {
|
||||
"wallet_rpc": "ok",
|
||||
"daemon": "ok" if self._daemon_address else "unknown",
|
||||
"daemon_height": daemon_height,
|
||||
}
|
||||
|
||||
def create_subaddress(self, user: User, label: str) -> SubaddressResult:
|
||||
start_total = time.monotonic()
|
||||
|
||||
+122
-2
@@ -6,16 +6,18 @@ from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
|
||||
from sqlalchemy import and_, or_, text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .btcpay_webhooks import dispatch_btcpay_webhooks
|
||||
from .config import INVOICE_RECONCILE_INTERVAL_SECONDS, LATE_PAYMENT_LOOKBACK_HOURS
|
||||
from .db import SessionLocal
|
||||
from .models import Invoice, InvoiceTransfer, User
|
||||
from .models import Invoice, InvoiceTransfer, SystemStatus, User
|
||||
from .monero_service import MoneroWalletService, TransferDetail
|
||||
from .webhooks import dispatch_webhooks
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MONERO_CONNECTIVITY_STATUS_NAME = "monero_connectivity"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -26,12 +28,36 @@ def main() -> None:
|
||||
level_name = "INFO"
|
||||
level = getattr(logging, level_name.upper(), logging.INFO)
|
||||
logging.basicConfig(level=level)
|
||||
service = MoneroWalletService()
|
||||
while True:
|
||||
status_db: Session | None = None
|
||||
try:
|
||||
status_db = SessionLocal()
|
||||
service = MoneroWalletService()
|
||||
_safe_update_monero_connectivity_status(status_db, service)
|
||||
_safe_update_reconciler_status(
|
||||
status_db,
|
||||
started_at=datetime.now(timezone.utc),
|
||||
completed_at=None,
|
||||
error_message=None,
|
||||
)
|
||||
_reconcile_invoices(service)
|
||||
_safe_update_reconciler_status(
|
||||
status_db,
|
||||
completed_at=datetime.now(timezone.utc),
|
||||
error_message=None,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("Invoice reconcile failed: %s", exc)
|
||||
if status_db is None:
|
||||
status_db = SessionLocal()
|
||||
_safe_update_monero_connectivity_error(status_db)
|
||||
_safe_update_reconciler_status(
|
||||
status_db,
|
||||
error_message=str(exc),
|
||||
)
|
||||
finally:
|
||||
if status_db is not None:
|
||||
status_db.close()
|
||||
time.sleep(INVOICE_RECONCILE_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
@@ -286,6 +312,100 @@ def _sync_invoice_transfers(
|
||||
return changed
|
||||
|
||||
|
||||
def _update_reconciler_status(
|
||||
db: Session,
|
||||
*,
|
||||
started_at: datetime | None = None,
|
||||
completed_at: datetime | None = None,
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
status_row = db.query(SystemStatus).filter(SystemStatus.name == "reconciler").first()
|
||||
if status_row is None:
|
||||
status_row = SystemStatus(name="reconciler")
|
||||
if started_at is not None:
|
||||
status_row.last_reconcile_started_at = started_at
|
||||
if completed_at is not None:
|
||||
status_row.last_reconcile_completed_at = completed_at
|
||||
status_row.last_reconcile_error = error_message
|
||||
db.add(status_row)
|
||||
db.commit()
|
||||
|
||||
|
||||
def _update_monero_connectivity_status(
|
||||
db: Session,
|
||||
*,
|
||||
wallet_rpc: str,
|
||||
daemon: str,
|
||||
daemon_height: int | None,
|
||||
) -> None:
|
||||
status_row = (
|
||||
db.query(SystemStatus)
|
||||
.filter(SystemStatus.name == MONERO_CONNECTIVITY_STATUS_NAME)
|
||||
.first()
|
||||
)
|
||||
if status_row is None:
|
||||
status_row = SystemStatus(name=MONERO_CONNECTIVITY_STATUS_NAME)
|
||||
status_row.wallet_rpc = wallet_rpc
|
||||
status_row.daemon = daemon
|
||||
status_row.daemon_height = daemon_height
|
||||
status_row.checked_at = datetime.now(timezone.utc)
|
||||
db.add(status_row)
|
||||
db.commit()
|
||||
|
||||
|
||||
def _safe_update_monero_connectivity_status(
|
||||
db: Session,
|
||||
service: MoneroWalletService,
|
||||
) -> None:
|
||||
try:
|
||||
status_payload = service.get_status()
|
||||
_update_monero_connectivity_status(
|
||||
db,
|
||||
wallet_rpc=str(status_payload.get("wallet_rpc", "unreachable")),
|
||||
daemon=str(status_payload.get("daemon", "unknown")),
|
||||
daemon_height=(
|
||||
int(status_payload["daemon_height"])
|
||||
if isinstance(status_payload.get("daemon_height"), int)
|
||||
else None
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.warning("Unable to persist Monero connectivity status", exc_info=True)
|
||||
|
||||
|
||||
def _safe_update_monero_connectivity_error(db: Session) -> None:
|
||||
try:
|
||||
_update_monero_connectivity_status(
|
||||
db,
|
||||
wallet_rpc="unreachable",
|
||||
daemon="unknown",
|
||||
daemon_height=None,
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
logger.warning("Unable to persist Monero connectivity fallback", exc_info=True)
|
||||
|
||||
|
||||
def _safe_update_reconciler_status(
|
||||
db: Session,
|
||||
*,
|
||||
started_at: datetime | None = None,
|
||||
completed_at: datetime | None = None,
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
try:
|
||||
_update_reconciler_status(
|
||||
db,
|
||||
started_at=started_at,
|
||||
completed_at=completed_at,
|
||||
error_message=error_message,
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
logger.warning("Unable to persist reconciler status heartbeat", exc_info=True)
|
||||
|
||||
|
||||
|
||||
|
||||
def _xmr_to_atomic(amount: Decimal) -> int:
|
||||
|
||||
+53
-2
@@ -22,12 +22,13 @@ from .config import (
|
||||
FOUNDER_PAYMENT_ADDRESS,
|
||||
FOUNDER_VIEW_KEY,
|
||||
INVOICE_DEFAULT_EXPIRY_HOURS,
|
||||
INVOICE_RECONCILE_INTERVAL_SECONDS,
|
||||
OPEN_REGISTRATION,
|
||||
QR_STORAGE_DIR,
|
||||
)
|
||||
from .db import get_db
|
||||
from .formatting import format_xmr_amount
|
||||
from .models import Invoice, ProfileHistory, User, Webhook, WebhookDelivery
|
||||
from .models import Invoice, ProfileHistory, SystemStatus, User, Webhook, WebhookDelivery
|
||||
from monero.address import Address, IntegratedAddress, SubAddress
|
||||
from .rates import get_xmr_rate
|
||||
from .subaddress_allocator import MAX_SUBADDRESS_INDEX, create_subaddress_for_user
|
||||
@@ -43,6 +44,7 @@ from .schemas import (
|
||||
LoginResponse,
|
||||
ProfileResponse,
|
||||
ProfileUpdate,
|
||||
SystemStatusResponse,
|
||||
WebhookCreate,
|
||||
WebhookDeliveryResponse,
|
||||
WebhookResponse,
|
||||
@@ -59,7 +61,6 @@ from .security import (
|
||||
)
|
||||
from .webhooks import build_webhook_payload, dispatch_webhooks
|
||||
from .qr_codes import ensure_invoice_qr_png, invoice_qr_url, resolve_qr_settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
WEBHOOK_EVENTS = (
|
||||
@@ -78,12 +79,57 @@ def _require_donations_enabled() -> None:
|
||||
)
|
||||
|
||||
MAX_QR_LOGO_DATA_URL_LENGTH = 120_000
|
||||
MONERO_CONNECTIVITY_STATUS_NAME = "monero_connectivity"
|
||||
|
||||
|
||||
def _get_user_for_api_key(db: Session, api_key: str) -> User | None:
|
||||
return db.query(User).filter(User.api_key_hash == hash_api_key(api_key)).first()
|
||||
|
||||
|
||||
def _status_response_from_rows(
|
||||
monero_status: SystemStatus | None,
|
||||
reconciler_status: SystemStatus | None,
|
||||
) -> SystemStatusResponse:
|
||||
wallet_rpc = (
|
||||
str(monero_status.wallet_rpc)
|
||||
if monero_status and monero_status.wallet_rpc
|
||||
else "unreachable"
|
||||
)
|
||||
daemon = (
|
||||
str(monero_status.daemon)
|
||||
if monero_status and monero_status.daemon
|
||||
else "unknown"
|
||||
)
|
||||
daemon_height = monero_status.daemon_height if monero_status else None
|
||||
return SystemStatusResponse(
|
||||
wallet_rpc=wallet_rpc,
|
||||
daemon=daemon,
|
||||
daemon_height=daemon_height,
|
||||
invoice_reconcile_interval_seconds=INVOICE_RECONCILE_INTERVAL_SECONDS,
|
||||
last_reconcile_started_at=(
|
||||
reconciler_status.last_reconcile_started_at if reconciler_status else None
|
||||
),
|
||||
last_reconcile_completed_at=(
|
||||
reconciler_status.last_reconcile_completed_at if reconciler_status else None
|
||||
),
|
||||
last_reconcile_error=(
|
||||
reconciler_status.last_reconcile_error if reconciler_status else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _load_public_system_status(db: Session) -> SystemStatusResponse:
|
||||
reconciler_status = (
|
||||
db.query(SystemStatus).filter(SystemStatus.name == "reconciler").first()
|
||||
)
|
||||
monero_status = (
|
||||
db.query(SystemStatus)
|
||||
.filter(SystemStatus.name == MONERO_CONNECTIVITY_STATUS_NAME)
|
||||
.first()
|
||||
)
|
||||
return _status_response_from_rows(monero_status, reconciler_status)
|
||||
|
||||
|
||||
def _get_founder_user(db: Session) -> User:
|
||||
if not FOUNDER_PAYMENT_ADDRESS or not FOUNDER_VIEW_KEY:
|
||||
raise HTTPException(
|
||||
@@ -541,6 +587,11 @@ def get_donation_status(
|
||||
return _public_invoice_status_response(db, invoice, request)
|
||||
|
||||
|
||||
@router.get("/api/core/public/system/status", response_model=SystemStatusResponse)
|
||||
def get_public_system_status(db: Session = Depends(get_db)):
|
||||
return _load_public_system_status(db)
|
||||
|
||||
|
||||
@router.post("/api/core/webhooks", response_model=WebhookResponse)
|
||||
def register_webhook(
|
||||
payload: WebhookCreate,
|
||||
|
||||
@@ -184,6 +184,16 @@ class InvoiceStatusResponse(BaseModel):
|
||||
return format_xmr_amount(value)
|
||||
|
||||
|
||||
class SystemStatusResponse(BaseModel):
|
||||
wallet_rpc: Literal["ok", "unreachable"]
|
||||
daemon: Literal["ok", "unreachable", "unknown"]
|
||||
daemon_height: int | None = None
|
||||
invoice_reconcile_interval_seconds: int
|
||||
last_reconcile_started_at: datetime | None = None
|
||||
last_reconcile_completed_at: datetime | None = None
|
||||
last_reconcile_error: str | None = None
|
||||
|
||||
|
||||
class ProfileResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
+8
-8
@@ -66,8 +66,8 @@ services:
|
||||
build:
|
||||
context: ./docker/monero
|
||||
args:
|
||||
MONERO_VERSION: ${MONERO_VERSION:-0.18.4.5}
|
||||
image: monero-local:${MONERO_VERSION:-0.18.4.5}
|
||||
MONERO_VERSION: ${MONERO_VERSION:-0.18.4.6}
|
||||
image: monero-local:${MONERO_VERSION:-0.18.4.6}
|
||||
entrypoint: ["monerod"]
|
||||
command:
|
||||
[
|
||||
@@ -97,8 +97,8 @@ services:
|
||||
build:
|
||||
context: ./docker/monero
|
||||
args:
|
||||
MONERO_VERSION: ${MONERO_VERSION:-0.18.4.5}
|
||||
image: monero-local:${MONERO_VERSION:-0.18.4.5}
|
||||
MONERO_VERSION: ${MONERO_VERSION:-0.18.4.6}
|
||||
image: monero-local:${MONERO_VERSION:-0.18.4.6}
|
||||
command:
|
||||
[
|
||||
"monero-wallet-rpc",
|
||||
@@ -126,8 +126,8 @@ services:
|
||||
build:
|
||||
context: ./docker/monero
|
||||
args:
|
||||
MONERO_VERSION: ${MONERO_VERSION:-0.18.4.5}
|
||||
image: monero-local:${MONERO_VERSION:-0.18.4.5}
|
||||
MONERO_VERSION: ${MONERO_VERSION:-0.18.4.6}
|
||||
image: monero-local:${MONERO_VERSION:-0.18.4.6}
|
||||
command:
|
||||
[
|
||||
"monero-wallet-rpc",
|
||||
@@ -155,8 +155,8 @@ services:
|
||||
build:
|
||||
context: ./docker/monero
|
||||
args:
|
||||
MONERO_VERSION: ${MONERO_VERSION:-0.18.4.5}
|
||||
image: monero-local:${MONERO_VERSION:-0.18.4.5}
|
||||
MONERO_VERSION: ${MONERO_VERSION:-0.18.4.6}
|
||||
image: monero-local:${MONERO_VERSION:-0.18.4.6}
|
||||
command:
|
||||
[
|
||||
"monero-wallet-rpc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
ARG MONERO_VERSION=0.18.4.4
|
||||
ARG MONERO_VERSION=0.18.4.6
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates curl bzip2 \
|
||||
|
||||
@@ -8,6 +8,7 @@ import BtcpayCheckoutStyleSection from "../../../components/btcpay-checkout-styl
|
||||
import DefaultConfirmationTargetSection from "../../../components/default-confirmation-target-section";
|
||||
import DefaultQrLogoSection from "../../../components/default-qr-logo-section";
|
||||
import InvoicePanel from "../../../components/invoice-panel";
|
||||
import { formatRelativeTime } from "../../../components/relative-time";
|
||||
import WebhookHistoryPanel from "../../../components/webhook-history-panel";
|
||||
import WebhookSecretSection from "../../../components/webhook-secret-section";
|
||||
import WebhookSection from "../../../components/webhook-section";
|
||||
@@ -75,6 +76,60 @@ type ProfileSummary = {
|
||||
btcpay_checkout_style?: "standard" | "btcpay_classic";
|
||||
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;
|
||||
};
|
||||
|
||||
const walletRpcBadge = (systemStatus: SystemStatusSummary | null) => {
|
||||
if (!systemStatus) {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Wallet RPC unavailable",
|
||||
};
|
||||
}
|
||||
if (systemStatus.wallet_rpc === "ok") {
|
||||
return {
|
||||
className: "bg-emerald-100 text-emerald-900",
|
||||
label: "Wallet RPC ok",
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: "bg-red-100 text-red-700",
|
||||
label: "Wallet RPC down",
|
||||
};
|
||||
};
|
||||
|
||||
const daemonBadge = (systemStatus: SystemStatusSummary | null) => {
|
||||
if (!systemStatus) {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Daemon unavailable",
|
||||
};
|
||||
}
|
||||
if (systemStatus.daemon === "ok") {
|
||||
return {
|
||||
className: "bg-emerald-100 text-emerald-900",
|
||||
label: "Daemon connected",
|
||||
};
|
||||
}
|
||||
if (systemStatus.daemon === "unknown") {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Daemon not configured",
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: "bg-red-100 text-red-700",
|
||||
label: "Daemon down",
|
||||
};
|
||||
};
|
||||
|
||||
export default async function DashboardPage({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -205,6 +260,7 @@ export default async function DashboardPage({
|
||||
|
||||
const allInvoices = invoicesData?.items ?? [];
|
||||
let profileData: ProfileSummary | null = null;
|
||||
let systemStatus: SystemStatusSummary | null = null;
|
||||
|
||||
if (activeTab === "overview" || activeTab === "profile" || activeTab === "invoices") {
|
||||
const response = await fetch(`${apiBaseUrl}/api/core/profile`, {
|
||||
@@ -220,6 +276,14 @@ export default async function DashboardPage({
|
||||
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 date = new Date(value);
|
||||
@@ -240,7 +304,14 @@ export default async function DashboardPage({
|
||||
const confirmedCount = allInvoices.filter(
|
||||
(invoice) => invoice.status === "confirmed"
|
||||
).length;
|
||||
|
||||
const walletStatusBadge = walletRpcBadge(systemStatus);
|
||||
const daemonStatusBadge = daemonBadge(systemStatus);
|
||||
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 =
|
||||
"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 =
|
||||
@@ -346,6 +417,68 @@ export default async function DashboardPage({
|
||||
</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 ${walletStatusBadge.className}`}
|
||||
>
|
||||
{walletStatusBadge.label}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 ${daemonStatusBadge.className}`}
|
||||
>
|
||||
{daemonStatusBadge.label}
|
||||
</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">
|
||||
We never hold funds. All payments move from the customer to your
|
||||
wallet.
|
||||
|
||||
@@ -48,6 +48,11 @@ const endpointGroups = [
|
||||
path: "/api/core/public/invoice/{invoice_id}/continue",
|
||||
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).",
|
||||
],
|
||||
},
|
||||
{
|
||||
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",
|
||||
path: "/api/core/invoices",
|
||||
|
||||
@@ -37,6 +37,59 @@ type InvoiceStatusResponse = {
|
||||
detected_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 walletRpcBadge = (systemStatus: SystemStatusResponse | null) => {
|
||||
if (!systemStatus) {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Wallet RPC unavailable",
|
||||
};
|
||||
}
|
||||
if (systemStatus.wallet_rpc === "ok") {
|
||||
return {
|
||||
className: "bg-emerald-100 text-emerald-900",
|
||||
label: "Wallet RPC ok",
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: "bg-red-100 text-red-700",
|
||||
label: "Wallet RPC down",
|
||||
};
|
||||
};
|
||||
|
||||
const daemonBadge = (systemStatus: SystemStatusResponse | null) => {
|
||||
if (!systemStatus) {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Daemon unavailable",
|
||||
};
|
||||
}
|
||||
if (systemStatus.daemon === "ok") {
|
||||
return {
|
||||
className: "bg-emerald-100 text-emerald-900",
|
||||
label: "Daemon connected",
|
||||
};
|
||||
}
|
||||
if (systemStatus.daemon === "unknown") {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Daemon not configured",
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: "bg-red-100 text-red-700",
|
||||
label: "Daemon down",
|
||||
};
|
||||
};
|
||||
|
||||
const formatStatus = (status: InvoiceStatus) => {
|
||||
if (status === "payment_detected") {
|
||||
@@ -98,10 +151,12 @@ export default async function InvoiceStatusDetailPage({
|
||||
params: Promise<{ invoiceId: string }>;
|
||||
}) {
|
||||
const { invoiceId } = await params;
|
||||
const response = await fetch(
|
||||
`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
const [response, systemStatusResponse] = await Promise.all([
|
||||
fetch(`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`, {
|
||||
cache: "no-store",
|
||||
}),
|
||||
fetch(`${apiBaseUrl}/api/core/public/system/status`, { cache: "no-store" }),
|
||||
]);
|
||||
|
||||
if (response.status === 404) {
|
||||
return (
|
||||
@@ -157,6 +212,9 @@ export default async function InvoiceStatusDetailPage({
|
||||
}
|
||||
|
||||
const invoice = (await response.json()) as InvoiceStatusResponse;
|
||||
const systemStatus = systemStatusResponse.ok
|
||||
? ((await systemStatusResponse.json()) as SystemStatusResponse)
|
||||
: null;
|
||||
const statusLabel = formatStatus(invoice.status);
|
||||
const confirmations = Math.max(0, invoice.confirmations ?? 0);
|
||||
const confirmationTarget = Math.max(0, invoice.confirmation_target);
|
||||
@@ -204,6 +262,9 @@ export default async function InvoiceStatusDetailPage({
|
||||
const showConfirmedStamp = invoice.status === "confirmed";
|
||||
const createdTimestamp = formatTimestamp(invoice.created_at);
|
||||
const expiresTimestamp = formatTimestamp(invoice.expires_at);
|
||||
const lastReconcileCompleted = formatTimestamp(systemStatus?.last_reconcile_completed_at ?? null);
|
||||
const walletStatusBadge = walletRpcBadge(systemStatus);
|
||||
const daemonStatusBadge = daemonBadge(systemStatus);
|
||||
|
||||
return (
|
||||
<main className="px-[6vw] pb-20 pt-10 text-ink">
|
||||
@@ -230,6 +291,9 @@ export default async function InvoiceStatusDetailPage({
|
||||
</summary>
|
||||
<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">
|
||||
Current block height: {systemStatus?.daemon_height?.toLocaleString() ?? "Unavailable"}
|
||||
</p>
|
||||
<p>
|
||||
This status page is public. Anyone with the link can view the current
|
||||
invoice state.
|
||||
@@ -259,6 +323,59 @@ export default async function InvoiceStatusDetailPage({
|
||||
<StatusRefreshButton label="Refresh" />
|
||||
</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 ${walletStatusBadge.className}`}
|
||||
>
|
||||
{walletStatusBadge.label}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 ${daemonStatusBadge.className}`}
|
||||
>
|
||||
{daemonStatusBadge.label}
|
||||
</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>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft">
|
||||
|
||||
@@ -52,6 +52,59 @@ type InvoiceStatusResponse = {
|
||||
quoted_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 walletRpcBadge = (systemStatus: SystemStatusResponse | null) => {
|
||||
if (!systemStatus) {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Wallet RPC unavailable",
|
||||
};
|
||||
}
|
||||
if (systemStatus.wallet_rpc === "ok") {
|
||||
return {
|
||||
className: "bg-emerald-100 text-emerald-900",
|
||||
label: "Wallet RPC ok",
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: "bg-red-100 text-red-700",
|
||||
label: "Wallet RPC down",
|
||||
};
|
||||
};
|
||||
|
||||
const daemonBadge = (systemStatus: SystemStatusResponse | null) => {
|
||||
if (!systemStatus) {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Daemon unavailable",
|
||||
};
|
||||
}
|
||||
if (systemStatus.daemon === "ok") {
|
||||
return {
|
||||
className: "bg-emerald-100 text-emerald-900",
|
||||
label: "Daemon connected",
|
||||
};
|
||||
}
|
||||
if (systemStatus.daemon === "unknown") {
|
||||
return {
|
||||
className: "bg-amber-100 text-amber-900",
|
||||
label: "Daemon not configured",
|
||||
};
|
||||
}
|
||||
return {
|
||||
className: "bg-red-100 text-red-700",
|
||||
label: "Daemon down",
|
||||
};
|
||||
};
|
||||
|
||||
const formatStatus = (status: InvoiceStatus) => {
|
||||
if (status === "payment_detected") {
|
||||
@@ -93,10 +146,12 @@ export default async function BtcpayModalInvoicePage({
|
||||
params: Promise<{ invoiceId: string }>;
|
||||
}) {
|
||||
const { invoiceId } = await params;
|
||||
const response = await fetch(
|
||||
`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
const [response, systemStatusResponse] = await Promise.all([
|
||||
fetch(`${apiBaseUrl}/api/core/public/invoice/${encodeURIComponent(invoiceId)}`, {
|
||||
cache: "no-store",
|
||||
}),
|
||||
fetch(`${apiBaseUrl}/api/core/public/system/status`, { cache: "no-store" }),
|
||||
]);
|
||||
|
||||
if (response.status === 404) {
|
||||
return (
|
||||
@@ -139,6 +194,9 @@ export default async function BtcpayModalInvoicePage({
|
||||
}
|
||||
|
||||
const invoice = (await response.json()) as InvoiceStatusResponse;
|
||||
const systemStatus = systemStatusResponse.ok
|
||||
? ((await systemStatusResponse.json()) as SystemStatusResponse)
|
||||
: null;
|
||||
const statusLabel = formatStatus(invoice.status);
|
||||
const confirmations = Math.max(0, invoice.confirmations ?? 0);
|
||||
const confirmationTarget = Math.max(0, invoice.confirmation_target);
|
||||
@@ -146,6 +204,9 @@ export default async function BtcpayModalInvoicePage({
|
||||
invoice.status === "payment_detected" || invoice.status === "confirmed";
|
||||
const createdTimestamp = formatTimestamp(invoice.created_at);
|
||||
const expiresTimestamp = formatTimestamp(invoice.expires_at);
|
||||
const lastReconcileCompleted = formatTimestamp(systemStatus?.last_reconcile_completed_at ?? null);
|
||||
const walletStatusBadge = walletRpcBadge(systemStatus);
|
||||
const daemonStatusBadge = daemonBadge(systemStatus);
|
||||
const isBtcpayInvoice = Boolean(invoice.btcpay_amount && invoice.btcpay_currency);
|
||||
const useClassicCheckout =
|
||||
isBtcpayInvoice && invoice.btcpay_checkout_style === "btcpay_classic";
|
||||
@@ -246,6 +307,59 @@ export default async function BtcpayModalInvoicePage({
|
||||
</p>
|
||||
</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 ${walletStatusBadge.className}`}
|
||||
>
|
||||
{walletStatusBadge.label}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 ${daemonStatusBadge.className}`}
|
||||
>
|
||||
{daemonStatusBadge.label}
|
||||
</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 ? (
|
||||
<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"
|
||||
|
||||
Generated
+48
-48
@@ -9,7 +9,7 @@
|
||||
"@marsidev/react-turnstile": "^1.4.2",
|
||||
"doctrine": "^3.0.0",
|
||||
"esutils": "^2.0.3",
|
||||
"next": "15.5.9",
|
||||
"next": "15.5.14",
|
||||
"qrcode": "1.5.4",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
@@ -23,7 +23,7 @@
|
||||
"@types/react-dom": "18.3.0",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "9.9.0",
|
||||
"eslint-config-next": "15.5.9",
|
||||
"eslint-config-next": "15.5.14",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "5.5.4"
|
||||
@@ -733,15 +733,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
|
||||
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz",
|
||||
"integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "15.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.9.tgz",
|
||||
"integrity": "sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.14.tgz",
|
||||
"integrity": "sha512-ogBjgsFrPPz19abP3VwcYSahbkUOMMvJjxCOYWYndw+PydeMuLuB4XrvNkNutFrTjC9St2KFULRdKID8Sd/CMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -749,9 +749,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
|
||||
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz",
|
||||
"integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -765,9 +765,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
|
||||
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz",
|
||||
"integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -781,9 +781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
|
||||
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz",
|
||||
"integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -797,9 +797,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
|
||||
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz",
|
||||
"integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -813,9 +813,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
|
||||
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz",
|
||||
"integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -829,9 +829,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
|
||||
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz",
|
||||
"integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -845,9 +845,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
|
||||
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz",
|
||||
"integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -861,9 +861,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
|
||||
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz",
|
||||
"integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2694,13 +2694,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-next": {
|
||||
"version": "15.5.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.9.tgz",
|
||||
"integrity": "sha512-852JYI3NkFNzW8CqsMhI0K2CDRxTObdZ2jQJj5CtpEaOkYHn13107tHpNuD/h0WRpU4FAbCdUaxQsrfBtNK9Kw==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.14.tgz",
|
||||
"integrity": "sha512-lmJ5F8ZgOYogq0qtH4L5SpxuASY2SPdOzqUprN2/56+P3GPsIpXaUWIJC66kYIH+yZdsM4nkHE5MIBP6s1NiBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.5.9",
|
||||
"@next/eslint-plugin-next": "15.5.14",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
@@ -4445,12 +4445,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.5.9",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
|
||||
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
|
||||
"version": "15.5.14",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz",
|
||||
"integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.9",
|
||||
"@next/env": "15.5.14",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -4463,14 +4463,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.5.7",
|
||||
"@next/swc-darwin-x64": "15.5.7",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.7",
|
||||
"@next/swc-linux-arm64-musl": "15.5.7",
|
||||
"@next/swc-linux-x64-gnu": "15.5.7",
|
||||
"@next/swc-linux-x64-musl": "15.5.7",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.7",
|
||||
"@next/swc-win32-x64-msvc": "15.5.7",
|
||||
"@next/swc-darwin-arm64": "15.5.14",
|
||||
"@next/swc-darwin-x64": "15.5.14",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.14",
|
||||
"@next/swc-linux-arm64-musl": "15.5.14",
|
||||
"@next/swc-linux-x64-gnu": "15.5.14",
|
||||
"@next/swc-linux-x64-musl": "15.5.14",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.14",
|
||||
"@next/swc-win32-x64-msvc": "15.5.14",
|
||||
"sharp": "^0.34.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"doctrine": "^3.0.0",
|
||||
"esutils": "^2.0.3",
|
||||
"next": "15.5.9",
|
||||
"next": "15.5.14",
|
||||
"qrcode": "1.5.4",
|
||||
"react": "19.2.3",
|
||||
"@marsidev/react-turnstile": "^1.4.2",
|
||||
@@ -26,7 +26,7 @@
|
||||
"@types/react-dom": "18.3.0",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "9.9.0",
|
||||
"eslint-config-next": "15.5.9",
|
||||
"eslint-config-next": "15.5.14",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "5.5.4"
|
||||
|
||||
Reference in New Issue
Block a user