mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(p2p stats): clarify browser peer data
This commit is contained in:
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "إلغاء",
|
"revoke": "إلغاء",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "প্রত্যাহার করুন",
|
"revoke": "প্রত্যাহার করুন",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Odvolat",
|
"revoke": "Odvolat",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Tilbagekald",
|
"revoke": "Tilbagekald",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Widerrufen",
|
"revoke": "Widerrufen",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Ανάκληση",
|
"revoke": "Ανάκληση",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Revoke",
|
"revoke": "Revoke",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Revocar",
|
"revoke": "Revocar",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "لغو",
|
"revoke": "لغو",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Peruuta",
|
"revoke": "Peruuta",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Bawiin",
|
"revoke": "Bawiin",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Révoquer",
|
"revoke": "Révoquer",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "בטל",
|
"revoke": "בטל",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "रद्द करें",
|
"revoke": "रद्द करें",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Visszavonás",
|
"revoke": "Visszavonás",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Cabut",
|
"revoke": "Cabut",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Revoca",
|
"revoke": "Revoca",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "取り消す",
|
"revoke": "取り消す",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "취소",
|
"revoke": "취소",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "रद्द करा",
|
"revoke": "रद्द करा",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Intrekken",
|
"revoke": "Intrekken",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Tilbakekall",
|
"revoke": "Tilbakekall",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Odwołaj",
|
"revoke": "Odwołaj",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Revogar",
|
"revoke": "Revogar",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Revocă",
|
"revoke": "Revocă",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Отозвать",
|
"revoke": "Отозвать",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Revoko",
|
"revoke": "Revoko",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Återkalla",
|
"revoke": "Återkalla",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "రద్దు చేయి",
|
"revoke": "రద్దు చేయి",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "เพิกถอน",
|
"revoke": "เพิกถอน",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "İptal et",
|
"revoke": "İptal et",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Відкликати",
|
"revoke": "Відкликати",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "منسوخ کریں",
|
"revoke": "منسوخ کریں",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "Thu hồi",
|
"revoke": "Thu hồi",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"revoke": "撤销",
|
"revoke": "撤销",
|
||||||
"p2p_stats": "P2P stats",
|
"p2p_stats": "P2P stats",
|
||||||
"enable_pure_p2p": "Enable pure P2P",
|
"enable_pure_p2p": "Enable pure P2P",
|
||||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
"enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.",
|
||||||
"p2p_stats_loading": "Loading P2P stats...",
|
"p2p_stats_loading": "Loading P2P stats...",
|
||||||
"p2p_stats_updated": "Updated",
|
"p2p_stats_updated": "Updated",
|
||||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
"p2p_stats_starting": "P2P stats appear after the P2P client starts.",
|
||||||
|
|||||||
+61
-8
@@ -42,6 +42,9 @@ const renderSettings = async (isElectron = false) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStatRows = () =>
|
||||||
|
new Map(Array.from(container.querySelectorAll('tr')).map((row) => [row.children.item(0)?.textContent ?? '', row.children.item(1)?.textContent ?? '']));
|
||||||
|
|
||||||
describe('P2PStatsSettings', () => {
|
describe('P2PStatsSettings', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -86,7 +89,14 @@ describe('P2PStatsSettings', () => {
|
|||||||
},
|
},
|
||||||
_helia: {
|
_helia: {
|
||||||
libp2p: {
|
libp2p: {
|
||||||
getConnections: () => ['connection-1'],
|
getConnections: () => [
|
||||||
|
{
|
||||||
|
direction: 'outbound',
|
||||||
|
remoteAddr: { toString: () => '/ip4/127.0.0.1/tcp/4001/ws/p2p/peer-1' },
|
||||||
|
remotePeer: { toString: () => 'peer-1' },
|
||||||
|
status: 'open',
|
||||||
|
},
|
||||||
|
],
|
||||||
getMultiaddrs: () => ['/ip4/127.0.0.1/tcp/4001'],
|
getMultiaddrs: () => ['/ip4/127.0.0.1/tcp/4001'],
|
||||||
getPeers: () => ['peer-1', 'peer-2'],
|
getPeers: () => ['peer-1', 'peer-2'],
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -121,16 +131,26 @@ describe('P2PStatsSettings', () => {
|
|||||||
await renderSettings(false);
|
await renderSettings(false);
|
||||||
await act(async () => Promise.resolve());
|
await act(async () => Promise.resolve());
|
||||||
|
|
||||||
expect(container.textContent).toContain('leeching');
|
const rows = getStatRows();
|
||||||
|
expect(container.textContent).toContain('Leeching');
|
||||||
expect(container.textContent).not.toContain('browser Helia');
|
expect(container.textContent).not.toContain('browser Helia');
|
||||||
expect(container.textContent).not.toContain('seed mode');
|
expect(container.textContent).not.toContain('seed mode');
|
||||||
expect(container.textContent).not.toContain('status');
|
expect(container.textContent).not.toContain('status');
|
||||||
|
expect(rows.get('Connected peers')).toContain('2 peers, 1 connection');
|
||||||
|
expect(rows.get('Connected peers')).toContain('WebSocket');
|
||||||
|
expect(rows.get('Connected peers')).toContain('/ip4/127.0.0.1/tcp/4001/ws/p2p/peer-1');
|
||||||
|
expect(rows.has('connections')).toBe(false);
|
||||||
|
expect(rows.has('Listen addresses')).toBe(false);
|
||||||
|
expect(rows.has('p2p_stats_updated')).toBe(true);
|
||||||
expect(container.textContent).toContain('self-peer');
|
expect(container.textContent).toContain('self-peer');
|
||||||
expect(container.textContent).toContain('2 peers');
|
expect(container.textContent).toContain('Peer ID');
|
||||||
expect(container.textContent).toContain('downloaded data');
|
expect(container.textContent).toContain('Data received');
|
||||||
expect(container.textContent).toContain('2.00 KB');
|
expect(container.textContent).toContain('2.00 KB');
|
||||||
expect(container.textContent).toContain('uploaded data');
|
expect(container.textContent).toContain('Data sent');
|
||||||
expect(container.textContent).toContain('1.00 KB');
|
expect(container.textContent).toContain('1.00 KB');
|
||||||
|
expect(container.textContent).not.toContain('client key');
|
||||||
|
expect(container.textContent).not.toContain('pubsub peers');
|
||||||
|
expect(container.textContent).not.toContain('routers');
|
||||||
expect(container.textContent).not.toContain('pubsub topics');
|
expect(container.textContent).not.toContain('pubsub topics');
|
||||||
expect(container.textContent).not.toContain('topic subscribers');
|
expect(container.textContent).not.toContain('topic subscribers');
|
||||||
});
|
});
|
||||||
@@ -176,12 +196,45 @@ describe('P2PStatsSettings', () => {
|
|||||||
await renderSettings(false);
|
await renderSettings(false);
|
||||||
await act(async () => Promise.resolve());
|
await act(async () => Promise.resolve());
|
||||||
|
|
||||||
expect(container.textContent).toContain('downloaded data');
|
expect(container.textContent).toContain('Data received');
|
||||||
expect(container.textContent).toContain('4.00 KB');
|
expect(container.textContent).toContain('4.00 KB');
|
||||||
expect(container.textContent).toContain('uploaded data');
|
expect(container.textContent).toContain('Data sent');
|
||||||
expect(container.textContent).toContain('2.00 KB');
|
expect(container.textContent).toContain('2.00 KB');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('starts browser transfer counters at zero when Helia exposes no byte totals yet', async () => {
|
||||||
|
testState.account = {
|
||||||
|
...testState.account,
|
||||||
|
pkcOptions: {
|
||||||
|
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||||
|
},
|
||||||
|
pkc: {
|
||||||
|
clients: {
|
||||||
|
libp2pJsClients: {
|
||||||
|
libp2pjs: {
|
||||||
|
key: 'libp2pjs',
|
||||||
|
_helia: {
|
||||||
|
libp2p: {
|
||||||
|
getConnections: () => [],
|
||||||
|
getMultiaddrs: () => [],
|
||||||
|
getPeers: () => [],
|
||||||
|
peerId: { toString: () => 'self-peer' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await renderSettings(false);
|
||||||
|
await act(async () => Promise.resolve());
|
||||||
|
|
||||||
|
const rows = getStatRows();
|
||||||
|
expect(rows.get('Data received')).toBe('0 B');
|
||||||
|
expect(rows.get('Data sent')).toBe('0 B');
|
||||||
|
});
|
||||||
|
|
||||||
it('reports seeding only when browser Helia can add and publish provider records', async () => {
|
it('reports seeding only when browser Helia can add and publish provider records', async () => {
|
||||||
testState.account = {
|
testState.account = {
|
||||||
...testState.account,
|
...testState.account,
|
||||||
@@ -220,7 +273,7 @@ describe('P2PStatsSettings', () => {
|
|||||||
await renderSettings(false);
|
await renderSettings(false);
|
||||||
await act(async () => Promise.resolve());
|
await act(async () => Promise.resolve());
|
||||||
|
|
||||||
expect(container.textContent).toContain('seeding');
|
expect(container.textContent).toContain('Seeding');
|
||||||
expect(container.textContent).not.toContain('seed mode');
|
expect(container.textContent).not.toContain('seed mode');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
|
|
||||||
.statName {
|
.statName {
|
||||||
width: 34%;
|
width: 34%;
|
||||||
text-transform: capitalize;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +38,46 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connectedPeers summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectedPeerList {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectedPeer {
|
||||||
|
border-top: 1px solid var(--border-color, #d9d9d9);
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectedPeerMeta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionTransport {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peerId,
|
||||||
|
.connectionAddress {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionAddress {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectedPeerEmpty {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.statsMeta {
|
.statsMeta {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
|
|||||||
@@ -6,11 +6,31 @@ import styles from './p2p-stats-settings.module.css';
|
|||||||
|
|
||||||
type AccountShape = Record<string, any>;
|
type AccountShape = Record<string, any>;
|
||||||
|
|
||||||
type StatRow = {
|
type TextStatRow = {
|
||||||
name: string;
|
name: string;
|
||||||
|
type?: 'text';
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ConnectedPeerEntry = {
|
||||||
|
address: string;
|
||||||
|
direction?: string;
|
||||||
|
id: string;
|
||||||
|
peerId: string;
|
||||||
|
status?: string;
|
||||||
|
transport: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConnectedPeersStatRow = {
|
||||||
|
connectionCount: number;
|
||||||
|
entries: ConnectedPeerEntry[];
|
||||||
|
name: string;
|
||||||
|
peerCount: number;
|
||||||
|
type: 'connectedPeers';
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatRow = ConnectedPeersStatRow | TextStatRow;
|
||||||
|
|
||||||
type StatsState = {
|
type StatsState = {
|
||||||
error?: string;
|
error?: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -63,10 +83,18 @@ type TransferStats = {
|
|||||||
uploadedBytes?: number;
|
uploadedBytes?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ObservedTransferStats = {
|
||||||
|
connections: WeakSet<object>;
|
||||||
|
downloadedBytes: number;
|
||||||
|
streams: WeakSet<object>;
|
||||||
|
uploadedBytes: number;
|
||||||
|
};
|
||||||
|
|
||||||
const KUBO_API_URL = 'http://localhost:50019/api/v0';
|
const KUBO_API_URL = 'http://localhost:50019/api/v0';
|
||||||
const STATS_REFRESH_MS = 5000;
|
const STATS_REFRESH_MS = 5000;
|
||||||
const MAX_TRANSFER_COUNTER_DEPTH = 10;
|
const MAX_TRANSFER_COUNTER_DEPTH = 10;
|
||||||
const MAX_TRANSFER_COUNTER_OBJECTS = 400;
|
const MAX_TRANSFER_COUNTER_OBJECTS = 400;
|
||||||
|
const observedBrowserTransferStats = new WeakMap<object, ObservedTransferStats>();
|
||||||
|
|
||||||
const statsReducer = (state: StatsState, action: StatsAction): StatsState => {
|
const statsReducer = (state: StatsState, action: StatsAction): StatsState => {
|
||||||
if (action.type === 'loading') return { ...state, error: undefined, loading: true };
|
if (action.type === 'loading') return { ...state, error: undefined, loading: true };
|
||||||
@@ -94,17 +122,20 @@ const formatBytes = (value: unknown) => {
|
|||||||
|
|
||||||
const formatRate = (value: unknown) => `${formatBytes(value)}/s`;
|
const formatRate = (value: unknown) => `${formatBytes(value)}/s`;
|
||||||
|
|
||||||
const stringifyList = (items?: unknown[], maxItems = 5) => {
|
|
||||||
if (!items?.length) return 'none';
|
|
||||||
const values = items.map((item) => String(item));
|
|
||||||
const visibleValues = values.slice(0, maxItems);
|
|
||||||
return values.length > maxItems ? `${visibleValues.join(', ')} +${values.length - maxItems} more` : visibleValues.join(', ');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFirstObjectValue = <T,>(value?: Record<string, T>) => (value ? Object.values(value)[0] : undefined);
|
const getFirstObjectValue = <T,>(value?: Record<string, T>) => (value ? Object.values(value)[0] : undefined);
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object';
|
const isRecord = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object';
|
||||||
|
|
||||||
|
const getStringValue = (value: unknown, fallback = 'unknown') => {
|
||||||
|
if (value === null || value === undefined) return fallback;
|
||||||
|
try {
|
||||||
|
const stringValue = String(value);
|
||||||
|
return stringValue || fallback;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getFiniteNumber = (value: unknown) => {
|
const getFiniteNumber = (value: unknown) => {
|
||||||
if (value === null || value === undefined) return undefined;
|
if (value === null || value === undefined) return undefined;
|
||||||
const numericValue = typeof value === 'bigint' ? Number(value) : Number(value);
|
const numericValue = typeof value === 'bigint' ? Number(value) : Number(value);
|
||||||
@@ -142,6 +173,24 @@ const getSafeArray = async (getValue?: () => unknown[] | Promise<unknown[]> | un
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getByteLength = (value: unknown): number | undefined => {
|
||||||
|
if (value === null || value === undefined) return undefined;
|
||||||
|
if (typeof value === 'string') return new TextEncoder().encode(value).byteLength;
|
||||||
|
if (value instanceof ArrayBuffer) return value.byteLength;
|
||||||
|
if (ArrayBuffer.isView(value)) return value.byteLength;
|
||||||
|
if (isRecord(value)) {
|
||||||
|
const directByteLength = getFiniteNumber(value.byteLength);
|
||||||
|
if (directByteLength !== undefined) return directByteLength;
|
||||||
|
const dataByteLength = getByteLength(value.data);
|
||||||
|
if (dataByteLength !== undefined) return dataByteLength;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const total = value.reduce((sum, entry) => sum + (getByteLength(entry) ?? 0), 0);
|
||||||
|
return total > 0 ? total : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const getTransferStatsFromHeliaCounters = (helia: unknown): TransferStats => {
|
const getTransferStatsFromHeliaCounters = (helia: unknown): TransferStats => {
|
||||||
const stats: TransferStats = {};
|
const stats: TransferStats = {};
|
||||||
const visited = new WeakSet<object>();
|
const visited = new WeakSet<object>();
|
||||||
@@ -199,17 +248,18 @@ const classifyTransferMetricPath = (path: string[]) => {
|
|||||||
|
|
||||||
const getMetricSnapshot = async (source: unknown) => {
|
const getMetricSnapshot = async (source: unknown) => {
|
||||||
if (!isRecord(source)) return source;
|
if (!isRecord(source)) return source;
|
||||||
for (const method of ['getMetrics', 'getMetricValues', 'toJSON']) {
|
const snapshots = await Promise.all(
|
||||||
const candidate = source[method];
|
['getMetrics', 'getMetricValues', 'toJSON'].map(async (method) => {
|
||||||
if (typeof candidate !== 'function') continue;
|
const candidate = source[method];
|
||||||
try {
|
if (typeof candidate !== 'function') return undefined;
|
||||||
const snapshot = await candidate.call(source);
|
try {
|
||||||
if (snapshot !== undefined) return snapshot;
|
return await candidate.call(source);
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
return source;
|
);
|
||||||
|
return snapshots.find((snapshot) => snapshot !== undefined) ?? source;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTransferStatsFromMetricSnapshot = (snapshot: unknown): TransferStats => {
|
const getTransferStatsFromMetricSnapshot = (snapshot: unknown): TransferStats => {
|
||||||
@@ -249,9 +299,92 @@ const mergeTransferStats = (primary: TransferStats, fallback: TransferStats): Tr
|
|||||||
uploadedBytes: primary.uploadedBytes ?? fallback.uploadedBytes,
|
uploadedBytes: primary.uploadedBytes ?? fallback.uploadedBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getBrowserTransferStats = async (client?: Libp2pClientShape): Promise<TransferStats> => {
|
const getTransferStatsFromClientShape = (client?: Libp2pClientShape): TransferStats => {
|
||||||
|
const clientRecord = isRecord(client) ? (client as Record<string, unknown>) : undefined;
|
||||||
|
const statsSources = [clientRecord, clientRecord?.stats, clientRecord?.sessionStats].filter(Boolean);
|
||||||
|
return statsSources.reduce<TransferStats>((stats, source) => {
|
||||||
|
if (!isRecord(source)) return stats;
|
||||||
|
const downloadedBytes = source.totalIn ?? source.downloadedBytes ?? source.bytesReceived ?? source.receivedBytes;
|
||||||
|
const uploadedBytes = source.totalOut ?? source.uploadedBytes ?? source.bytesSent ?? source.sentBytes;
|
||||||
|
addTransferStats(stats, 'downloadedBytes', downloadedBytes);
|
||||||
|
addTransferStats(stats, 'uploadedBytes', uploadedBytes);
|
||||||
|
return stats;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getObservedTransferStats = (client?: Libp2pClientShape): ObservedTransferStats | undefined => {
|
||||||
|
if (!isRecord(client)) return undefined;
|
||||||
|
let stats = observedBrowserTransferStats.get(client);
|
||||||
|
if (!stats) {
|
||||||
|
stats = {
|
||||||
|
connections: new WeakSet<object>(),
|
||||||
|
downloadedBytes: 0,
|
||||||
|
streams: new WeakSet<object>(),
|
||||||
|
uploadedBytes: 0,
|
||||||
|
};
|
||||||
|
observedBrowserTransferStats.set(client, stats);
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
const instrumentStreamTransferStats = (stream: unknown, stats: ObservedTransferStats) => {
|
||||||
|
if (!isRecord(stream) || stats.streams.has(stream)) return;
|
||||||
|
stats.streams.add(stream);
|
||||||
|
|
||||||
|
const send = stream.send;
|
||||||
|
if (typeof send === 'function') {
|
||||||
|
try {
|
||||||
|
stream.send = function sendWithTransferStats(this: unknown, data: unknown, ...args: unknown[]) {
|
||||||
|
addTransferStats(stats, 'uploadedBytes', getByteLength(data));
|
||||||
|
return send.call(this, data, ...args);
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Some stream implementations may expose read-only methods.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addEventListener = stream.addEventListener;
|
||||||
|
if (typeof addEventListener === 'function') {
|
||||||
|
try {
|
||||||
|
addEventListener.call(stream, 'message', (event: unknown) => {
|
||||||
|
const data = isRecord(event) ? (event.data ?? event.detail) : undefined;
|
||||||
|
addTransferStats(stats, 'downloadedBytes', getByteLength(data));
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const instrumentConnectionTransferStats = (connection: unknown, stats: ObservedTransferStats) => {
|
||||||
|
if (!isRecord(connection)) return;
|
||||||
|
|
||||||
|
if (!stats.connections.has(connection)) {
|
||||||
|
stats.connections.add(connection);
|
||||||
|
const newStream = connection.newStream;
|
||||||
|
if (typeof newStream === 'function') {
|
||||||
|
try {
|
||||||
|
connection.newStream = async function newStreamWithTransferStats(this: unknown, ...args: unknown[]) {
|
||||||
|
const stream = await newStream.apply(this, args);
|
||||||
|
instrumentStreamTransferStats(stream, stats);
|
||||||
|
return stream;
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Some connection implementations may expose read-only methods.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const stream of toArray(connection.streams)) instrumentStreamTransferStats(stream, stats);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBrowserTransferStats = async (client?: Libp2pClientShape, connections: unknown[] = []): Promise<TransferStats> => {
|
||||||
try {
|
try {
|
||||||
const helia = client?._helia;
|
const helia = client?._helia;
|
||||||
|
const observedStats = getObservedTransferStats(client);
|
||||||
|
if (observedStats) connections.forEach((connection) => instrumentConnectionTransferStats(connection, observedStats));
|
||||||
|
|
||||||
|
const clientStats = getTransferStatsFromClientShape(client);
|
||||||
const counterStats = getTransferStatsFromHeliaCounters(helia);
|
const counterStats = getTransferStatsFromHeliaCounters(helia);
|
||||||
const metricSources = [helia?.metrics, helia?.libp2p?.metrics].filter(Boolean);
|
const metricSources = [helia?.metrics, helia?.libp2p?.metrics].filter(Boolean);
|
||||||
const metricSnapshots = await Promise.all(metricSources.map((source) => getMetricSnapshot(source)));
|
const metricSnapshots = await Promise.all(metricSources.map((source) => getMetricSnapshot(source)));
|
||||||
@@ -259,7 +392,7 @@ const getBrowserTransferStats = async (client?: Libp2pClientShape): Promise<Tran
|
|||||||
.map((snapshot) => getTransferStatsFromMetricSnapshot(snapshot))
|
.map((snapshot) => getTransferStatsFromMetricSnapshot(snapshot))
|
||||||
.reduce<TransferStats>((stats, nextStats) => mergeTransferStats(stats, nextStats), {});
|
.reduce<TransferStats>((stats, nextStats) => mergeTransferStats(stats, nextStats), {});
|
||||||
|
|
||||||
return mergeTransferStats(counterStats, metricStats);
|
return mergeTransferStats(mergeTransferStats(mergeTransferStats(clientStats, counterStats), metricStats), observedStats ?? {});
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -290,33 +423,98 @@ const hasProviderPublishingRouter = (client?: Libp2pClientShape) =>
|
|||||||
(client?._helia?.routing?.routers ?? []).some((router) => isRecord(router) && !isKnownNoopProvide(router.provide));
|
(client?._helia?.routing?.routers ?? []).some((router) => isRecord(router) && !isKnownNoopProvide(router.provide));
|
||||||
|
|
||||||
const getBrowserMode = (client?: Libp2pClientShape) => {
|
const getBrowserMode = (client?: Libp2pClientShape) => {
|
||||||
if (!client) return 'unknown';
|
if (!client) return 'Unknown';
|
||||||
return hasSupportedAdd(client) && hasProviderPublishingRouter(client) ? 'seeding' : 'leeching';
|
return hasSupportedAdd(client) && hasProviderPublishingRouter(client) ? 'Seeding' : 'Leeching';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTransportLabel = (address: string) => {
|
||||||
|
const normalizedAddress = address.toLowerCase();
|
||||||
|
let transport = 'Unknown transport';
|
||||||
|
if (normalizedAddress.includes('/webtransport')) transport = 'WebTransport';
|
||||||
|
else if (normalizedAddress.includes('/webrtc-direct')) transport = 'WebRTC direct';
|
||||||
|
else if (normalizedAddress.includes('/webrtc')) transport = 'WebRTC';
|
||||||
|
else if (normalizedAddress.includes('/wss')) transport = 'Secure WebSocket';
|
||||||
|
else if (normalizedAddress.includes('/ws')) transport = 'WebSocket';
|
||||||
|
else if (normalizedAddress.includes('/quic')) transport = 'QUIC';
|
||||||
|
else if (normalizedAddress.includes('/tcp')) transport = 'TCP';
|
||||||
|
else if (normalizedAddress.includes('/udp')) transport = 'UDP';
|
||||||
|
|
||||||
|
return normalizedAddress.includes('/p2p-circuit') ? `${transport} through relay` : transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConnectionPeerId = (connection: unknown) => (isRecord(connection) ? getStringValue(connection.remotePeer) : 'unknown');
|
||||||
|
|
||||||
|
const getConnectionAddress = (connection: unknown) => (isRecord(connection) ? getStringValue(connection.remoteAddr, 'address unavailable') : 'address unavailable');
|
||||||
|
|
||||||
|
const getBrowserConnectedPeersRow = (peers: unknown[], connections: unknown[]): ConnectedPeersStatRow => {
|
||||||
|
const entries = connections.map<ConnectedPeerEntry>((connection) => {
|
||||||
|
const address = getConnectionAddress(connection);
|
||||||
|
const peerId = getConnectionPeerId(connection);
|
||||||
|
const fallbackId = `${peerId}-${address}`;
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
direction: isRecord(connection) ? getStringValue(connection.direction, '') : undefined,
|
||||||
|
id: isRecord(connection) ? getStringValue(connection.id, fallbackId) : fallbackId,
|
||||||
|
peerId,
|
||||||
|
status: isRecord(connection) ? getStringValue(connection.status, '') : undefined,
|
||||||
|
transport: getTransportLabel(address),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const peerIds = [...peers, ...entries].reduce<Set<string>>((ids, peerOrEntry) => {
|
||||||
|
const peerId = isRecord(peerOrEntry) && 'peerId' in peerOrEntry ? getStringValue(peerOrEntry.peerId) : getStringValue(peerOrEntry);
|
||||||
|
if (peerId && peerId !== 'unknown') ids.add(peerId);
|
||||||
|
return ids;
|
||||||
|
}, new Set());
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionCount: connections.length,
|
||||||
|
entries,
|
||||||
|
name: 'Connected peers',
|
||||||
|
peerCount: peerIds.size || entries.length,
|
||||||
|
type: 'connectedPeers',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getElectronConnectedPeersRow = (peers: unknown): ConnectedPeersStatRow => {
|
||||||
|
const peerEntries = isRecord(peers) && Array.isArray(peers.Peers) ? peers.Peers : [];
|
||||||
|
const entries = peerEntries.map<ConnectedPeerEntry>((peer) => {
|
||||||
|
const address = isRecord(peer) ? getStringValue(peer.Addr, 'address unavailable') : 'address unavailable';
|
||||||
|
const peerId = isRecord(peer) ? getStringValue(peer.Peer) : 'unknown';
|
||||||
|
const direction = isRecord(peer) ? getStringValue(peer.Direction, '') : undefined;
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
direction,
|
||||||
|
id: `${peerId}-${address}-${direction ?? ''}`,
|
||||||
|
peerId,
|
||||||
|
transport: getTransportLabel(address),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const peerIds = entries.reduce<Set<string>>((ids, entry) => {
|
||||||
|
if (entry.peerId && entry.peerId !== 'unknown') ids.add(entry.peerId);
|
||||||
|
return ids;
|
||||||
|
}, new Set());
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionCount: entries.length,
|
||||||
|
entries,
|
||||||
|
name: 'Connected peers',
|
||||||
|
peerCount: peerIds.size || entries.length,
|
||||||
|
type: 'connectedPeers',
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBrowserLibp2pStats = async (account?: AccountShape): Promise<StatRow[]> => {
|
const getBrowserLibp2pStats = async (account?: AccountShape): Promise<StatRow[]> => {
|
||||||
const client = getFirstObjectValue(account?.pkc?.clients?.libp2pJsClients) as Libp2pClientShape | undefined;
|
const client = getFirstObjectValue(account?.pkc?.clients?.libp2pJsClients) as Libp2pClientShape | undefined;
|
||||||
const libp2p = client?._helia?.libp2p;
|
const libp2p = client?._helia?.libp2p;
|
||||||
const pubsub = libp2p?.services?.pubsub;
|
const [peers, connections] = await Promise.all([getSafeArray(() => libp2p?.getPeers?.()), getSafeArray(() => libp2p?.getConnections?.())]);
|
||||||
const [peers, connections, multiaddrs, pubsubPeers, transferStats] = await Promise.all([
|
const transferStats = await getBrowserTransferStats(client, connections);
|
||||||
getSafeArray(() => libp2p?.getPeers?.()),
|
|
||||||
getSafeArray(() => libp2p?.getConnections?.()),
|
|
||||||
getSafeArray(() => libp2p?.getMultiaddrs?.()),
|
|
||||||
getSafeArray(() => pubsub?.getPeers?.()),
|
|
||||||
getBrowserTransferStats(client),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ name: 'mode', value: getBrowserMode(client) },
|
{ name: 'Mode', value: getBrowserMode(client) },
|
||||||
{ name: 'peer id', value: libp2p?.peerId?.toString() ?? 'unknown' },
|
{ name: 'Peer ID', value: libp2p?.peerId?.toString() ?? 'unknown' },
|
||||||
{ name: 'client key', value: client?.key ?? 'unknown' },
|
getBrowserConnectedPeersRow(peers, connections),
|
||||||
{ name: 'connected peers', value: formatCount(peers.length, 'peer') },
|
{ name: 'Data received', value: transferStats.downloadedBytes === undefined ? 'unknown' : formatBytes(transferStats.downloadedBytes) },
|
||||||
{ name: 'connections', value: formatCount(connections.length, 'connection') },
|
{ name: 'Data sent', value: transferStats.uploadedBytes === undefined ? 'unknown' : formatBytes(transferStats.uploadedBytes) },
|
||||||
{ name: 'downloaded data', value: transferStats.downloadedBytes === undefined ? 'unknown' : formatBytes(transferStats.downloadedBytes) },
|
|
||||||
{ name: 'uploaded data', value: transferStats.uploadedBytes === undefined ? 'unknown' : formatBytes(transferStats.uploadedBytes) },
|
|
||||||
{ name: 'listen addresses', value: stringifyList(multiaddrs) },
|
|
||||||
{ name: 'pubsub peers', value: formatCount(pubsubPeers.length, 'peer') },
|
|
||||||
{ name: 'routers', value: stringifyList(account?.pkcOptions?.httpRoutersOptions) },
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -345,17 +543,17 @@ const getElectronKuboStats = async (rpcState?: string, signal?: AbortSignal): Pr
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ name: 'mode', value: 'desktop Kubo' },
|
{ name: 'Mode', value: 'Desktop Kubo' },
|
||||||
{ name: 'PKC RPC', value: rpcState ?? 'unknown' },
|
{ name: 'PKC RPC', value: rpcState ?? 'unknown' },
|
||||||
{ name: 'peer id', value: identity.ID ?? 'unknown' },
|
{ name: 'Peer ID', value: identity.ID ?? 'unknown' },
|
||||||
{ name: 'agent', value: identity.AgentVersion ?? version.Version ?? 'unknown' },
|
{ name: 'Agent', value: identity.AgentVersion ?? version.Version ?? 'unknown' },
|
||||||
{ name: 'connected peers', value: formatCount(Array.isArray(peers.Peers) ? peers.Peers.length : 0, 'peer') },
|
getElectronConnectedPeersRow(peers),
|
||||||
{ name: 'bandwidth in', value: `${formatBytes(bandwidth.TotalIn)} total, ${formatRate(bandwidth.RateIn)}` },
|
{ name: 'Bandwidth in', value: `${formatBytes(bandwidth.TotalIn)} total, ${formatRate(bandwidth.RateIn)}` },
|
||||||
{ name: 'bandwidth out', value: `${formatBytes(bandwidth.TotalOut)} total, ${formatRate(bandwidth.RateOut)}` },
|
{ name: 'Bandwidth out', value: `${formatBytes(bandwidth.TotalOut)} total, ${formatRate(bandwidth.RateOut)}` },
|
||||||
{ name: 'repo size', value: formatBytes(repo.RepoSize) },
|
{ name: 'Repo size', value: formatBytes(repo.RepoSize) },
|
||||||
{ name: 'repo objects', value: String(repo.NumObjects ?? 'unknown') },
|
{ name: 'Repo objects', value: String(repo.NumObjects ?? 'unknown') },
|
||||||
{ name: 'bitswap peers', value: formatCount(Array.isArray(bitswap.Peers) ? bitswap.Peers.length : 0, 'peer') },
|
{ name: 'Bitswap peers', value: formatCount(Array.isArray(bitswap.Peers) ? bitswap.Peers.length : 0, 'peer') },
|
||||||
{ name: 'bitswap wantlist', value: formatCount(Array.isArray(bitswap.Wantlist) ? bitswap.Wantlist.length : 0, 'item') },
|
{ name: 'Bitswap wantlist', value: formatCount(Array.isArray(bitswap.Wantlist) ? bitswap.Wantlist.length : 0, 'item') },
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -364,6 +562,33 @@ const getP2PStats = async (mode: P2PRuntimeMode, account?: AccountShape, rpcStat
|
|||||||
return getElectronKuboStats(rpcState, signal);
|
return getElectronKuboStats(rpcState, signal);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ConnectedPeersValue = ({ row }: { row: ConnectedPeersStatRow }) => (
|
||||||
|
<details className={styles.connectedPeers} data-testid='connected-peers'>
|
||||||
|
<summary>
|
||||||
|
{formatCount(row.peerCount, 'peer')}, {formatCount(row.connectionCount, 'connection')}
|
||||||
|
</summary>
|
||||||
|
<div className={styles.connectedPeerList}>
|
||||||
|
{row.entries.length ? (
|
||||||
|
row.entries.map((entry) => (
|
||||||
|
<div className={styles.connectedPeer} key={entry.id}>
|
||||||
|
<div className={styles.connectedPeerMeta}>
|
||||||
|
<span className={styles.connectionTransport}>{entry.transport}</span>
|
||||||
|
{entry.direction && <span>{entry.direction}</span>}
|
||||||
|
{entry.status && <span>{entry.status}</span>}
|
||||||
|
</div>
|
||||||
|
<div className={styles.peerId}>{entry.peerId}</div>
|
||||||
|
<code className={styles.connectionAddress}>{entry.address}</code>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className={styles.connectedPeerEmpty}>No active peer addresses</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StatValue = ({ row }: { row: StatRow }) => (row.type === 'connectedPeers' ? <ConnectedPeersValue row={row} /> : row.value);
|
||||||
|
|
||||||
const P2PStatsSettings = () => {
|
const P2PStatsSettings = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const account = useAccount() as AccountShape | undefined;
|
const account = useAccount() as AccountShape | undefined;
|
||||||
@@ -413,13 +638,21 @@ const P2PStatsSettings = () => {
|
|||||||
{statsState.rows.map((row) => (
|
{statsState.rows.map((row) => (
|
||||||
<tr key={row.name}>
|
<tr key={row.name}>
|
||||||
<td className={styles.statName}>{row.name}</td>
|
<td className={styles.statName}>{row.name}</td>
|
||||||
<td className={styles.statValue}>{row.value}</td>
|
<td className={styles.statValue}>
|
||||||
|
<StatValue row={row} />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
{updatedAtLabel && (
|
||||||
|
<tr>
|
||||||
|
<td className={styles.statName}>{t('p2p_stats_updated')}</td>
|
||||||
|
<td className={styles.statValue}>{updatedAtLabel}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className={styles.statsMeta}>
|
<div className={styles.statsMeta}>
|
||||||
{statsState.loading ? t('p2p_stats_loading') : updatedAtLabel ? `${t('p2p_stats_updated')} ${updatedAtLabel}` : null}
|
{statsState.loading ? t('p2p_stats_loading') : null}
|
||||||
{statsState.error && <div className={styles.error}>{statsState.error}</div>}
|
{statsState.error && <div className={styles.error}>{statsState.error}</div>}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user