diff --git a/.changeset/famous-queens-protect.md b/.changeset/famous-queens-protect.md
new file mode 100644
index 000000000..cfb622a64
--- /dev/null
+++ b/.changeset/famous-queens-protect.md
@@ -0,0 +1,5 @@
+---
+'explorer': minor
+---
+
+Fixed an issue loading the host page for hosts that are not benchmarked yet.
diff --git a/.changeset/few-steaks-fail.md b/.changeset/few-steaks-fail.md
new file mode 100644
index 000000000..b354e5e82
--- /dev/null
+++ b/.changeset/few-steaks-fail.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+The maxDowntimeHours setting default value is now 336.
diff --git a/.changeset/funny-crabs-vanish.md b/.changeset/funny-crabs-vanish.md
new file mode 100644
index 000000000..f73f47e5c
--- /dev/null
+++ b/.changeset/funny-crabs-vanish.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+Added support for the minRecentScanFailures autopilot hosts setting.
diff --git a/.changeset/hot-humans-cover.md b/.changeset/hot-humans-cover.md
new file mode 100644
index 000000000..eeae6b150
--- /dev/null
+++ b/.changeset/hot-humans-cover.md
@@ -0,0 +1,5 @@
+---
+'website': minor
+---
+
+The navbar now includes the logo and "sia" as text.
diff --git a/.changeset/itchy-lizards-speak.md b/.changeset/itchy-lizards-speak.md
new file mode 100644
index 000000000..a31de8463
--- /dev/null
+++ b/.changeset/itchy-lizards-speak.md
@@ -0,0 +1,7 @@
+---
+'hostd': minor
+'renterd': minor
+'@siafoundation/design-system': minor
+---
+
+Extremely small siacoin values will now show as hastings by default rather than 0SC.
diff --git a/.changeset/lemon-dragons-hug.md b/.changeset/lemon-dragons-hug.md
new file mode 100644
index 000000000..d4d18d4ab
--- /dev/null
+++ b/.changeset/lemon-dragons-hug.md
@@ -0,0 +1,5 @@
+---
+'website': minor
+---
+
+The website now features an improved interactive host map.
diff --git a/.changeset/tender-crabs-peel.md b/.changeset/tender-crabs-peel.md
new file mode 100644
index 000000000..cb32e1a7c
--- /dev/null
+++ b/.changeset/tender-crabs-peel.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+Refined the warnings in the files feature navbar and file explorer empty states.
diff --git a/.changeset/yellow-files-approve.md b/.changeset/yellow-files-approve.md
new file mode 100644
index 000000000..2b19f26ca
--- /dev/null
+++ b/.changeset/yellow-files-approve.md
@@ -0,0 +1,5 @@
+---
+'explorer': minor
+---
+
+Fixed an issue where transaction values were incorrect on the address page transaction list.
diff --git a/README.md b/README.md
index 5ec30d767..b3d551626 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ The Sia web libraries provide developers with convenient TypeScript SDKs for usi
- [@siafoundation/sia-nodejs](libs/sia-nodejs) - Sia NodeJS client for controlling a v1 `siad`.
- [@siafoundation/design-system](libs/design-system) - React-based design system used across Sia apps and websites.
- [@siafoundation/data-sources](libs/data-sources) - Data sources used for stats on the website.
+- [@siafoundation/units](libs/units) - Methods and types for converting and displaying units.
## Internal
diff --git a/apps/explorer/app/host/[id]/opengraph-image.tsx b/apps/explorer/app/host/[id]/opengraph-image.tsx
index a53d460e0..aa6fe199b 100644
--- a/apps/explorer/app/host/[id]/opengraph-image.tsx
+++ b/apps/explorer/app/host/[id]/opengraph-image.tsx
@@ -6,10 +6,12 @@ import { getOGImage } from '../../../components/OGImageEntity'
import { siaCentralApi } from '../../../config'
import {
getDownloadCost,
+ getDownloadSpeed,
getStorageCost,
getUploadCost,
-} from '../../../lib/host'
-import { humanBytes, humanSpeed } from '@siafoundation/sia-js'
+ getUploadSpeed,
+} from '@siafoundation/units'
+import { humanBytes } from '@siafoundation/sia-js'
import { truncate } from '@siafoundation/design-system'
import { CurrencyOption, currencyOptions } from '@siafoundation/react-core'
@@ -76,10 +78,7 @@ export default async function Image({ params }) {
rate: r.rates.sc.usd,
},
}),
- subvalue: humanSpeed(
- (h.host.benchmark.data_size * 8) /
- (h.host.benchmark.download_time / 1000)
- ),
+ subvalue: h.host.benchmark && getDownloadSpeed(h.host),
},
{
label: 'upload',
@@ -90,9 +89,7 @@ export default async function Image({ params }) {
rate: r.rates.sc.usd,
},
}),
- subvalue: humanSpeed(
- (h.host.benchmark.data_size * 8) / (h.host.benchmark.upload_time / 1000)
- ),
+ subvalue: h.host.benchmark && getUploadSpeed(h.host),
},
]
diff --git a/apps/explorer/components/Address/index.tsx b/apps/explorer/components/Address/index.tsx
index 7af7fede8..03cd87213 100644
--- a/apps/explorer/components/Address/index.tsx
+++ b/apps/explorer/components/Address/index.tsx
@@ -52,10 +52,12 @@ export function Address({ id, address }: Props) {
...address.unconfirmed_transactions.map((tx) => ({
hash: tx.id,
sc: getTotal({
+ address: id,
inputs: tx.siacoin_inputs,
outputs: tx.siacoin_outputs,
}),
sf: getTotal({
+ address: id,
inputs: tx.siafund_inputs,
outputs: tx.siafund_outputs,
}).toNumber(),
@@ -72,10 +74,12 @@ export function Address({ id, address }: Props) {
...address.transactions.map((tx) => ({
hash: tx.id,
sc: getTotal({
+ address: id,
inputs: tx.siacoin_inputs,
outputs: tx.siacoin_outputs,
}),
sf: getTotal({
+ address: id,
inputs: tx.siafund_inputs,
outputs: tx.siafund_outputs,
}).toNumber(),
@@ -88,7 +92,7 @@ export function Address({ id, address }: Props) {
)
}
return list
- }, [address])
+ }, [id, address])
const utxos = useMemo(() => {
const list: EntityListItemProps[] = []
@@ -159,15 +163,23 @@ export function Address({ id, address }: Props) {
}
function getTotal({
+ address,
inputs,
outputs,
}: {
- inputs?: { value: string }[]
- outputs?: { value: string }[]
+ address: string
+ inputs?: { value: string; unlock_hash: string }[]
+ outputs?: { value: string; unlock_hash: string }[]
}) {
return (outputs || [])
- .reduce((acc, o) => acc.plus(o.value), new BigNumber(0))
+ .reduce(
+ (acc, o) => (o.unlock_hash === address ? acc.plus(o.value) : acc),
+ new BigNumber(0)
+ )
.minus(
- (inputs || []).reduce((acc, i) => acc.plus(i.value), new BigNumber(0))
+ (inputs || []).reduce(
+ (acc, i) => (i.unlock_hash === address ? acc.plus(i.value) : acc),
+ new BigNumber(0)
+ )
)
}
diff --git a/apps/explorer/components/Home/HostListItem.tsx b/apps/explorer/components/Home/HostListItem.tsx
index c039d75de..8a5423242 100644
--- a/apps/explorer/components/Home/HostListItem.tsx
+++ b/apps/explorer/components/Home/HostListItem.tsx
@@ -24,7 +24,7 @@ import {
getStorageCost,
getUploadCost,
getUploadSpeed,
-} from '../../lib/host'
+} from '@siafoundation/units'
import { useMemo } from 'react'
import { routes } from '../../config/routes'
import { useExchangeRate } from '../../hooks/useExchangeRate'
@@ -106,7 +106,7 @@ export function HostListItem({ host, rates, entity }: Props) {
- {downloadSpeed}
+ {downloadSpeed || '-'}
@@ -121,7 +121,7 @@ export function HostListItem({ host, rates, entity }: Props) {
- {uploadSpeed}
+ {uploadSpeed || '-'}
diff --git a/apps/explorer/components/Home/index.tsx b/apps/explorer/components/Home/index.tsx
index 02555c5ef..3b7383624 100644
--- a/apps/explorer/components/Home/index.tsx
+++ b/apps/explorer/components/Home/index.tsx
@@ -18,7 +18,11 @@ import {
SiaCentralHostsNetworkMetricsResponse,
} from '@siafoundation/sia-central'
import { hashToAvatar } from '../../lib/avatar'
-import { getDownloadCost, getStorageCost, getUploadCost } from '../../lib/host'
+import {
+ getDownloadCost,
+ getStorageCost,
+ getUploadCost,
+} from '@siafoundation/units'
import { HostListItem } from './HostListItem'
import { useExchangeRate } from '../../hooks/useExchangeRate'
@@ -62,7 +66,7 @@ export function Home({
{humanBytes(
metrics?.totals.total_storage -
- metrics?.totals.remaining_storage
+ metrics?.totals.remaining_storage
)}
diff --git a/apps/explorer/components/Host/HostPricing.tsx b/apps/explorer/components/Host/HostPricing.tsx
index 3b1346e02..7bd8cb48a 100644
--- a/apps/explorer/components/Host/HostPricing.tsx
+++ b/apps/explorer/components/Host/HostPricing.tsx
@@ -19,7 +19,7 @@ import {
getStorageCost,
getUploadCost,
getUploadSpeed,
-} from '../../lib/host'
+} from '@siafoundation/units'
import { useExchangeRate } from '../../hooks/useExchangeRate'
type Props = {
diff --git a/apps/explorer/components/Host/HostSettings.tsx b/apps/explorer/components/Host/HostSettings.tsx
index c14f6fe52..64a12ef60 100644
--- a/apps/explorer/components/Host/HostSettings.tsx
+++ b/apps/explorer/components/Host/HostSettings.tsx
@@ -14,7 +14,11 @@ import {
} from '@siafoundation/design-system'
import BigNumber from 'bignumber.js'
import { humanBytes, humanSiacoin, toSiacoins } from '@siafoundation/sia-js'
-import { getDownloadCost, getStorageCost, getUploadCost } from '../../lib/host'
+import {
+ getDownloadCost,
+ getStorageCost,
+ getUploadCost,
+} from '@siafoundation/units'
import { useExchangeRate } from '../../hooks/useExchangeRate'
import { siacoinToFiat } from '../../lib/currency'
diff --git a/apps/renterd/components/Files/EmptyState/index.tsx b/apps/renterd/components/Files/EmptyState/index.tsx
index 96c6c20ee..6ef9378ed 100644
--- a/apps/renterd/components/Files/EmptyState/index.tsx
+++ b/apps/renterd/components/Files/EmptyState/index.tsx
@@ -1,4 +1,4 @@
-import { LinkButton, Text } from '@siafoundation/design-system'
+import { Code, LinkButton, Text } from '@siafoundation/design-system'
import { CloudUpload32 } from '@siafoundation/react-icons'
import { routes } from '../../../config/routes'
import { useFiles } from '../../../contexts/files'
@@ -35,12 +35,13 @@ export function EmptyState() {
- Before you can upload files you must configure autopilot. Autopilot
- finds contracts with hosts based on the settings you choose.
- Autopilot also repairs your data as hosts come and go.
+ Before you can upload files you must configure your settings. Once
+ configured, renterd
will find contracts with hosts
+ based on the settings you choose. renterd
will also
+ repair your data as hosts come and go.
- Configure autopilot →
+ Configure
diff --git a/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx b/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx
index 4d4ca657c..ad2f80658 100644
--- a/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx
+++ b/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx
@@ -1,42 +1,9 @@
-import { Link, Text, Tooltip } from '@siafoundation/design-system'
+import { Text, Tooltip } from '@siafoundation/design-system'
import { Warning16 } from '@siafoundation/react-icons'
-import { useFiles } from '../../../contexts/files'
-import { routes } from '../../../config/routes'
import { useContractSetMismatch } from '../checks/useContractSetMismatch'
-import { useDefaultContractSetNotSet } from '../checks/useDefaultContractSetNotSet'
-import { useAutopilotNotConfigured } from '../checks/useAutopilotNotConfigured'
-import { useNotEnoughContracts } from '../checks/useNotEnoughContracts'
export function FilesStatsMenuWarnings() {
- const { dataState, isViewingRootOfABucket, isViewingBuckets } = useFiles()
const contractSetMismatch = useContractSetMismatch()
- const defaultContractSetNotSet = useDefaultContractSetNotSet()
- const autopilotNotConfigured = useAutopilotNotConfigured()
- const notEnoughContracts = useNotEnoughContracts()
-
- // onboard/warn about default contract set
- if (defaultContractSetNotSet.active) {
- return (
-
-
-
-
-
- Configure a default contract set to get started.{' '}
-
- Configuration →
-
-
-
- )
- }
// warn about contract set mismatch
if (contractSetMismatch.active) {
@@ -65,66 +32,5 @@ export function FilesStatsMenuWarnings() {
)
}
- const autopilotNotConfiguredViewingBuckets =
- autopilotNotConfigured.active && isViewingBuckets
- const autopilotNotConfiguredRootDirectory =
- autopilotNotConfigured.active &&
- isViewingRootOfABucket &&
- dataState !== 'noneYet'
- const autopilotNotConfiguredNotRootDirectory =
- autopilotNotConfigured.active && !isViewingRootOfABucket
- if (
- autopilotNotConfiguredViewingBuckets ||
- autopilotNotConfiguredRootDirectory ||
- autopilotNotConfiguredNotRootDirectory
- ) {
- return (
-
-
-
-
-
- Configure autopilot to get started.{' '}
-
- Autopilot →
-
-
-
- )
- }
-
- const notEnoughContractsViewingBuckets =
- notEnoughContracts.active && isViewingBuckets
- const notEnoughContractsRootDirectoryAndExistingFiles =
- notEnoughContracts.active &&
- isViewingRootOfABucket &&
- dataState !== 'noneYet'
- const notEnoughContractsNotRootDirectory =
- notEnoughContracts.active && !isViewingRootOfABucket
- if (
- notEnoughContractsViewingBuckets ||
- notEnoughContractsRootDirectoryAndExistingFiles ||
- notEnoughContractsNotRootDirectory
- ) {
- return (
-
-
-
-
-
- Not enought contracts to upload files. {notEnoughContracts.count}/
- {notEnoughContracts.required}
-
-
- )
- }
-
return null
}
diff --git a/apps/renterd/components/Hosts/HostMap/HostItem.tsx b/apps/renterd/components/Hosts/HostMap/HostItem.tsx
index a822ed138..17ece1bcd 100644
--- a/apps/renterd/components/Hosts/HostMap/HostItem.tsx
+++ b/apps/renterd/components/Hosts/HostMap/HostItem.tsx
@@ -7,10 +7,11 @@ import {
Tooltip,
countryCodeEmoji,
} from '@siafoundation/design-system'
-import { humanBytes, humanSiacoin, humanSpeed } from '@siafoundation/sia-js'
+import { humanBytes, humanSiacoin } from '@siafoundation/sia-js'
import { cx } from 'class-variance-authority'
import BigNumber from 'bignumber.js'
import { SiaCentralHost } from '@siafoundation/sia-central'
+import { getDownloadSpeed, getUploadSpeed } from '@siafoundation/units'
type Host = SiaCentralHost
@@ -96,18 +97,8 @@ export function HostItem({
{humanBytes(host.settings.total_storage)}
-
- {humanSpeed(
- (host.benchmark.data_size * 8) /
- (host.benchmark.download_time / 1000)
- )}{' '}
-
-
- {humanSpeed(
- (host.benchmark.data_size * 8) /
- (host.benchmark.upload_time / 1000)
- )}{' '}
-
+ {getDownloadSpeed(host)}
+ {getUploadSpeed(host)}
{storageCost}
@@ -149,12 +140,10 @@ export function HostItem({
host.public_key === activeHost?.public_key ? 'semibold' : 'regular'
}
>
- {humanBytes(host.settings.total_storage)} ·{' '}
- {humanSpeed(
- (host.benchmark.data_size * 8) /
- (host.benchmark.download_time / 1000)
- )}{' '}
- · {storageCost}
+ {humanBytes(host.settings.total_storage)}
+ {host.benchmark && ` · ${getDownloadSpeed(host)}`}
+ {' · '}
+ {storageCost}
diff --git a/apps/renterd/components/OnboardingBar.tsx b/apps/renterd/components/OnboardingBar.tsx
index 64459fc7d..4a493ded8 100644
--- a/apps/renterd/components/OnboardingBar.tsx
+++ b/apps/renterd/components/OnboardingBar.tsx
@@ -51,7 +51,9 @@ export function OnboardingBar() {
return null
}
- const walletBalance = new BigNumber(wallet.data?.confirmed || 0)
+ const walletBalance = new BigNumber(
+ wallet.data ? wallet.data.confirmed + wallet.data.unconfirmed : 0
+ )
const allowance = new BigNumber(autopilot.data?.contracts.allowance || 0)
const step1Configured = app.autopilot.state.data?.configured
diff --git a/apps/renterd/contexts/config/fields.tsx b/apps/renterd/contexts/config/fields.tsx
index 0ea009b6d..83dbcaa2c 100644
--- a/apps/renterd/contexts/config/fields.tsx
+++ b/apps/renterd/contexts/config/fields.tsx
@@ -244,6 +244,28 @@ export function getFields({
}
: {},
},
+ minRecentScanFailures: {
+ type: 'number',
+ category: 'hosts',
+ title: 'Min recent scan failures',
+ description: (
+ <>
+ The minimum number of recent scan failures that autopilot will
+ tolerate.
+ >
+ ),
+ units: 'scans',
+ decimalsLimit: 0,
+ suggestion: advancedDefaultAutopilot.minRecentScanFailures,
+ suggestionTip: `Defaults to ${advancedDefaultAutopilot.minRecentScanFailures.toNumber()}.`,
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
// wallet
defragThreshold: {
diff --git a/apps/renterd/contexts/config/index.tsx b/apps/renterd/contexts/config/index.tsx
index 4d4d465b5..36ae3862d 100644
--- a/apps/renterd/contexts/config/index.tsx
+++ b/apps/renterd/contexts/config/index.tsx
@@ -473,17 +473,26 @@ export function useConfigMain() {
throw Error(configAppResponse.error)
}
- triggerSuccessToast('Configuration has been saved.')
if (isAutopilotEnabled) {
+ // Sync default contract set if necessary. Only syncs if the setting
+ // is enabled in case the user changes in advanced mode and then
+ // goes back to simple mode.
+ // Might be simpler nice to just override in simple mode without a
+ // special setting since this is how other settings like allowance
+ // behave - but leaving for now.
syncDefaultContractSet(finalValues.autopilotContractSet)
+
+ // Trigger the autopilot loop with new settings applied.
autopilotTrigger.post({
payload: {
- forceScan: false,
+ forceScan: true,
},
})
}
- // if autopilot is being configured for the first time,
+ triggerSuccessToast('Configuration has been saved.')
+
+ // If autopilot is being configured for the first time,
// revalidate the empty hosts list.
if (firstTimeSettingConfig) {
const refreshHostsAfterDelay = async () => {
diff --git a/apps/renterd/contexts/config/transform.spec.ts b/apps/renterd/contexts/config/transform.spec.ts
index 942ee5c46..184215dc7 100644
--- a/apps/renterd/contexts/config/transform.spec.ts
+++ b/apps/renterd/contexts/config/transform.spec.ts
@@ -28,6 +28,7 @@ describe('tansforms', () => {
hosts: {
allowRedundantIPs: false,
maxDowntimeHours: 1440,
+ minRecentScanFailures: 10,
scoreOverrides: null,
},
contracts: {
@@ -75,6 +76,7 @@ describe('tansforms', () => {
storageTB: new BigNumber('1'),
allowRedundantIPs: false,
maxDowntimeHours: new BigNumber('1440'),
+ minRecentScanFailures: new BigNumber('10'),
defragThreshold: new BigNumber('1000'),
defaultContractSet: 'myset',
uploadPackingEnabled: true,
@@ -105,6 +107,7 @@ describe('tansforms', () => {
hosts: {
allowRedundantIPs: false,
maxDowntimeHours: 1440,
+ minRecentScanFailures: 10,
scoreOverrides: null,
},
contracts: {
@@ -152,6 +155,7 @@ describe('tansforms', () => {
storageTB: new BigNumber('1'),
allowRedundantIPs: false,
maxDowntimeHours: new BigNumber('1440'),
+ minRecentScanFailures: new BigNumber('10'),
defragThreshold: new BigNumber('1000'),
defaultContractSet: 'myset',
uploadPackingEnabled: true,
@@ -188,6 +192,7 @@ describe('tansforms', () => {
storageTB: new BigNumber('1'),
allowRedundantIPs: false,
maxDowntimeHours: new BigNumber('1440'),
+ minRecentScanFailures: new BigNumber('10'),
defragThreshold: new BigNumber('1000'),
},
undefined
@@ -199,6 +204,7 @@ describe('tansforms', () => {
hosts: {
allowRedundantIPs: false,
maxDowntimeHours: 1440,
+ minRecentScanFailures: 10,
scoreOverrides: null,
},
contracts: {
@@ -228,6 +234,7 @@ describe('tansforms', () => {
storageTB: new BigNumber('1'),
allowRedundantIPs: false,
maxDowntimeHours: new BigNumber('1440'),
+ minRecentScanFailures: new BigNumber('10'),
defragThreshold: new BigNumber('1000'),
},
{
@@ -255,6 +262,7 @@ describe('tansforms', () => {
foobar: 'value',
allowRedundantIPs: false,
maxDowntimeHours: 1440,
+ minRecentScanFailures: 10,
scoreOverrides: null,
},
contracts: {
@@ -303,6 +311,7 @@ describe('tansforms', () => {
storageTB: new BigNumber('1'),
allowRedundantIPs: false,
maxDowntimeHours: new BigNumber('1440'),
+ minRecentScanFailures: new BigNumber('10'),
defragThreshold: new BigNumber('1000'),
defaultContractSet: 'myset',
uploadPackingEnabled: false,
@@ -356,6 +365,7 @@ describe('tansforms', () => {
storageTB: new BigNumber('1'),
allowRedundantIPs: false,
maxDowntimeHours: new BigNumber('1440'),
+ minRecentScanFailures: new BigNumber('10'),
defragThreshold: new BigNumber('1000'),
defaultContractSet: 'myset',
uploadPackingEnabled: false,
@@ -514,6 +524,7 @@ function buildAllResponses() {
hosts: {
allowRedundantIPs: false,
maxDowntimeHours: 1440,
+ minRecentScanFailures: 10,
scoreOverrides: null,
},
contracts: {
diff --git a/apps/renterd/contexts/config/transform.ts b/apps/renterd/contexts/config/transform.ts
index 95eab430a..25f7ab56d 100644
--- a/apps/renterd/contexts/config/transform.ts
+++ b/apps/renterd/contexts/config/transform.ts
@@ -85,6 +85,7 @@ export function transformUpAutopilot(
hosts: {
...existingValues?.hosts,
maxDowntimeHours: v.maxDowntimeHours.toNumber(),
+ minRecentScanFailures: v.minRecentScanFailures.toNumber(),
allowRedundantIPs: v.allowRedundantIPs,
scoreOverrides: existingValues?.hosts.scoreOverrides || null,
},
@@ -241,6 +242,7 @@ export function transformDownAutopilot(
// hosts
allowRedundantIPs: config.hosts.allowRedundantIPs,
maxDowntimeHours: new BigNumber(config.hosts.maxDowntimeHours),
+ minRecentScanFailures: new BigNumber(config.hosts.minRecentScanFailures),
// wallet
defragThreshold: new BigNumber(config.wallet.defragThreshold),
}
diff --git a/apps/renterd/contexts/config/types.ts b/apps/renterd/contexts/config/types.ts
index 148316812..88c886d51 100644
--- a/apps/renterd/contexts/config/types.ts
+++ b/apps/renterd/contexts/config/types.ts
@@ -16,6 +16,7 @@ export const defaultAutopilot = {
// hosts
allowRedundantIPs: false,
maxDowntimeHours: undefined as BigNumber | undefined,
+ minRecentScanFailures: undefined as BigNumber | undefined,
// wallet
defragThreshold: undefined as BigNumber | undefined,
}
@@ -84,7 +85,8 @@ export const advancedDefaultAutopilot: AutopilotData = {
amountHosts: new BigNumber(50),
autopilotContractSet: 'autopilot',
allowRedundantIPs: false,
- maxDowntimeHours: new BigNumber(1440),
+ maxDowntimeHours: new BigNumber(336),
+ minRecentScanFailures: new BigNumber(10),
defragThreshold: new BigNumber(1000),
}
diff --git a/apps/website/assets/earth-dark-contrast.png b/apps/website/assets/earth-dark-contrast.png
new file mode 100644
index 000000000..16056c7a7
Binary files /dev/null and b/apps/website/assets/earth-dark-contrast.png differ
diff --git a/apps/website/assets/earth-topology.png b/apps/website/assets/earth-topology.png
new file mode 100644
index 000000000..666be0091
Binary files /dev/null and b/apps/website/assets/earth-topology.png differ
diff --git a/apps/website/assets/night-sky.png b/apps/website/assets/night-sky.png
new file mode 100644
index 000000000..f5d4f2beb
Binary files /dev/null and b/apps/website/assets/night-sky.png differ
diff --git a/apps/website/components/HostMap/Globe.tsx b/apps/website/components/HostMap/Globe.tsx
index 34c18ad83..96eee6506 100644
--- a/apps/website/components/HostMap/Globe.tsx
+++ b/apps/website/components/HostMap/Globe.tsx
@@ -11,15 +11,15 @@ import dynamic from 'next/dynamic'
import { GlobeMethods } from 'react-globe.gl'
import { random, sortBy } from 'lodash'
import { getHostLabel } from './utils'
-import { Host } from '../../content/geoHosts'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
import { getAssetUrl } from '../../content/assets'
import { useTheme } from 'next-themes'
import { useElementSize } from 'usehooks-ts'
type Props = {
- activeHost: Host
+ activeHost: SiaCentralPartialHost
selectActiveHost: (public_key: string) => void
- hosts: Host[]
+ hosts: SiaCentralPartialHost[]
rates: {
usd: string
}
@@ -27,8 +27,8 @@ type Props = {
type Route = {
distance: number
- src: Host
- dst: Host
+ src: SiaCentralPartialHost
+ dst: SiaCentralPartialHost
}
const GlobeGl = dynamic(() => import('./GlobeGl'), {
@@ -44,7 +44,7 @@ const ReactGlobe = forwardRef(function ReactGlobe(
function GlobeComponent({ activeHost, hosts, rates, selectActiveHost }: Props) {
const globeEl = useRef
(null)
- const moveToHost = useCallback((host: Host) => {
+ const moveToHost = useCallback((host: SiaCentralPartialHost) => {
globeEl.current?.pointOfView(
{
lat: host.location[0] - 8,
@@ -175,19 +175,21 @@ function GlobeComponent({ activeHost, hosts, rates, selectActiveHost }: Props) {
}}
arcsTransitionDuration={0}
pointsData={hosts}
- pointLat={(h: Host) => h.location[0]}
- pointLng={(h: Host) => h.location[1]}
- pointLabel={(h: Host) => getHostLabel({ host: h, rates })}
+ pointLat={(h: SiaCentralPartialHost) => h.location[0]}
+ pointLng={(h: SiaCentralPartialHost) => h.location[1]}
+ pointLabel={(h: SiaCentralPartialHost) =>
+ getHostLabel({ host: h, rates })
+ }
pointAltitude={0}
- pointColor={(h: Host) =>
+ pointColor={(h: SiaCentralPartialHost) =>
h.public_key === activeHost.public_key
? 'rgba(0,255,0,1)'
: 'rgba(0,255,0,1)'
}
- pointRadius={(h: Host) =>
+ pointRadius={(h: SiaCentralPartialHost) =>
h.public_key === activeHost.public_key ? 0.6 : 0.2
}
- onPointClick={(h: Host) => {
+ onPointClick={(h: SiaCentralPartialHost) => {
selectActiveHost(h.public_key)
}}
pointsMerge={false}
@@ -198,14 +200,20 @@ function GlobeComponent({ activeHost, hosts, rates, selectActiveHost }: Props) {
export const Globe = memo(GlobeComponent)
-function distanceBetweenHosts(h1: Host, h2: Host) {
+function distanceBetweenHosts(
+ h1: SiaCentralPartialHost,
+ h2: SiaCentralPartialHost
+) {
return Math.sqrt(
Math.pow(h1.location[0] - h2.location[0], 2) +
Math.pow(h1.location[1] - h2.location[1], 2)
)
}
-function doesIncludeActiveHost(route: Route, activeHost: Host) {
+function doesIncludeActiveHost(
+ route: Route,
+ activeHost: SiaCentralPartialHost
+) {
return (
route.dst.public_key === activeHost.public_key ||
route.src.public_key === activeHost.public_key
diff --git a/apps/website/components/HostMap/HostItem.tsx b/apps/website/components/HostMap/HostItem.tsx
index 77daf8171..eafb65148 100644
--- a/apps/website/components/HostMap/HostItem.tsx
+++ b/apps/website/components/HostMap/HostItem.tsx
@@ -6,15 +6,19 @@ import {
Text,
Tooltip,
countryCodeEmoji,
+ LinkButton,
+ webLinks,
} from '@siafoundation/design-system'
-import { humanBytes, humanSiacoin, humanSpeed } from '@siafoundation/sia-js'
+import { humanBytes, humanSiacoin } from '@siafoundation/sia-js'
import { cx } from 'class-variance-authority'
import BigNumber from 'bignumber.js'
-import { Host } from '../../content/geoHosts'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
+import { Launch16 } from '@siafoundation/react-icons'
+import { getDownloadSpeed, getUploadSpeed } from '@siafoundation/units'
type Props = {
- host: Host
- activeHost: Host
+ host: SiaCentralPartialHost
+ activeHost: SiaCentralPartialHost
setRef?: (el: HTMLButtonElement) => void
selectActiveHost: (public_key: string) => void
rates: {
@@ -81,9 +85,20 @@ export function HostItem({
-
- {countryCodeEmoji(host.country_code)} {host.country_code}
-
+
+
+ {countryCodeEmoji(host.country_code)} {host.country_code}
+
+
+
+
+
storage
@@ -94,18 +109,8 @@ export function HostItem({
{humanBytes(host.settings.total_storage)}
-
- {humanSpeed(
- (host.benchmark.data_size * 8) /
- (host.benchmark.download_time / 1000)
- )}{' '}
-
-
- {humanSpeed(
- (host.benchmark.data_size * 8) /
- (host.benchmark.upload_time / 1000)
- )}{' '}
-
+ {getDownloadSpeed(host)}
+ {getUploadSpeed(host)}
{storageCost}
@@ -146,12 +151,10 @@ export function HostItem({
host.public_key === activeHost?.public_key ? 'semibold' : 'regular'
}
>
- {humanBytes(host.settings.total_storage)} ·{' '}
- {humanSpeed(
- (host.benchmark.data_size * 8) /
- (host.benchmark.download_time / 1000)
- )}{' '}
- · {storageCost}
+ {humanBytes(host.settings.total_storage)}
+ {host.benchmark && ` · ${getDownloadSpeed(host)}`}
+ {' · '}
+ {storageCost}
diff --git a/apps/website/components/HostMap/index.tsx b/apps/website/components/HostMap/index.tsx
index 73ed1e099..b7886dae5 100644
--- a/apps/website/components/HostMap/index.tsx
+++ b/apps/website/components/HostMap/index.tsx
@@ -12,10 +12,10 @@ import { random, throttle } from 'lodash'
import { HostItem } from './HostItem'
import { Globe } from './Globe'
import { cx } from 'class-variance-authority'
-import { Host } from '../../content/geoHosts'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
type Props = {
- hosts: Host[]
+ hosts: SiaCentralPartialHost[]
rates: {
usd: string
diff --git a/apps/website/components/HostMap/utils.ts b/apps/website/components/HostMap/utils.ts
index f7f539814..0568a9b0f 100644
--- a/apps/website/components/HostMap/utils.ts
+++ b/apps/website/components/HostMap/utils.ts
@@ -3,15 +3,16 @@ import {
TBToBytes,
countryCodeEmoji,
} from '@siafoundation/design-system'
-import { humanBytes, humanSiacoin, humanSpeed } from '@siafoundation/sia-js'
+import { humanBytes, humanSiacoin } from '@siafoundation/sia-js'
import BigNumber from 'bignumber.js'
-import { Host } from '../../content/geoHosts'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
+import { getDownloadSpeed } from '@siafoundation/units'
export function getHostLabel({
host,
rates,
}: {
- host: Host
+ host: SiaCentralPartialHost
rates: {
usd: string
}
@@ -32,7 +33,5 @@ export function getHostLabel({
return `${countryCodeEmoji(host.country_code)} · ${humanBytes(
host.settings.total_storage
- )} · ${humanSpeed(
- (host.benchmark.data_size * 8) / (host.benchmark.download_time / 1000)
- )} · ${storageCost}`
+ )}${host.benchmark && ` · ${getDownloadSpeed(host)}`} · ${storageCost}`
}
diff --git a/apps/website/components/Layout/Navbar/index.tsx b/apps/website/components/Layout/Navbar/index.tsx
index 8ad2ecce5..19e1ddeab 100644
--- a/apps/website/components/Layout/Navbar/index.tsx
+++ b/apps/website/components/Layout/Navbar/index.tsx
@@ -12,10 +12,16 @@ import { menuSections } from '../../../config/siteMap'
import { SiteMenu } from '../SiteMenu'
import { NavMenu } from './NavMenu'
-export function Navbar({ scrolledDown }: { scrolledDown: boolean }) {
+export function Navbar({
+ size = '4',
+ scrolledDown,
+}: {
+ size?: '4' | 'full'
+ scrolledDown: boolean
+}) {
return (
-
-
+
+
+
+ sia
+
diff --git a/apps/website/components/Map/Globe.tsx b/apps/website/components/Map/Globe.tsx
new file mode 100644
index 000000000..ccd2de29f
--- /dev/null
+++ b/apps/website/components/Map/Globe.tsx
@@ -0,0 +1,206 @@
+import { useEffect, useRef, useCallback, useMemo } from 'react'
+import { GlobeMethods } from 'react-globe.gl'
+import { getHostLabel } from './utils'
+import { useElementSize } from 'usehooks-ts'
+import { useTryUntil } from '@siafoundation/react-core'
+import earthDarkContrast from '../../assets/earth-dark-contrast.png'
+import earthTopology from '../../assets/earth-topology.png'
+import nightSky from '../../assets/night-sky.png'
+import { GlobeDyn } from './GlobeDyn'
+import { useSiaCentralExchangeRates } from '@siafoundation/react-sia-central'
+import { colors } from '@siafoundation/design-system'
+import { useDecRoutes } from './useRoutes'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
+
+export type Commands = {
+ moveToLocation: (
+ location: [number, number] | undefined,
+ altitude?: number
+ ) => void
+}
+
+export const emptyCommands: Commands = {
+ moveToLocation: (location: [number, number], altitude?: number) => null,
+}
+
+type Props = {
+ activeHost?: SiaCentralPartialHost
+ hosts?: SiaCentralPartialHost[]
+ onHostClick: (public_key: string, location: [number, number]) => void
+ onHostHover: (public_key: string, location: [number, number]) => void
+ onMount?: (cmd: Commands) => void
+}
+
+type Route = {
+ distance: number
+ src: SiaCentralPartialHost
+ dst: SiaCentralPartialHost
+}
+
+export function Globe({
+ activeHost,
+ hosts,
+ onMount,
+ onHostClick,
+ onHostHover,
+}: Props) {
+ const rates = useSiaCentralExchangeRates({
+ config: {
+ swr: {
+ revalidateOnFocus: false,
+ },
+ },
+ })
+ const globeEl = useRef(null)
+ const cmdRef = useRef(emptyCommands)
+ const moveToLocation = useCallback(
+ (location: [number, number] | undefined, altitude?: number) => {
+ if (!location) {
+ return
+ }
+ globeEl.current?.pointOfView(
+ {
+ lat: location[0] - 8,
+ lng: location[1],
+ altitude: altitude || 1.5,
+ },
+ 700
+ )
+ },
+ []
+ )
+
+ useEffect(() => {
+ cmdRef.current.moveToLocation = moveToLocation
+ }, [moveToLocation])
+
+ useTryUntil(() => {
+ if (!globeEl.current) {
+ return false
+ }
+
+ moveToLocation(activeHost?.location || [48.8323, 2.4075], 1.5)
+
+ const directionalLight = globeEl.current
+ ?.scene()
+ .children.find((obj3d) => obj3d.type === 'DirectionalLight')
+ if (directionalLight) {
+ // directionalLight.position.set(1, 1, 1)
+ directionalLight.intensity = 10
+ }
+ return true
+ })
+
+ useEffect(() => {
+ if (onMount) {
+ onMount(cmdRef.current)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const routes = useDecRoutes({ hosts, activeHost })
+
+ const [containerRef, { height, width }] = useElementSize()
+
+ const points = useMemo(() => hosts || [], [hosts])
+
+ return (
+
+
+ getHostLabel({ host: r.dst, rates: rates.data?.rates.sc })
+ }
+ arcStartLat={(r: Route) => +r.src.location[0]}
+ arcStartLng={(r: Route) => +r.src.location[1]}
+ arcEndLat={(r: Route) => +r.dst.location[0]}
+ arcEndLng={(r: Route) => +r.dst.location[1]}
+ arcDashLength={0.75}
+ arcAltitude={0}
+ arcDashGap={0.1}
+ arcDashInitialGap={() => Math.random()}
+ arcDashAnimateTime={5000}
+ // arcDashAnimateTime={(route: Route) =>
+ // doesIncludeActiveHost(route, activeHost) ? 5000 : 0
+ // }
+ arcColor={(r: Route) =>
+ doesIncludeActiveHost(r, activeHost)
+ ? [`rgba(187, 229, 201, 0.25)`, `rgba(187, 229, 201, 0.25)`]
+ : [`rgba(187, 229, 201, 0.10)`, `rgba(187, 229, 201, 0.10)`]
+ }
+ // onArcClick={(r: Route) => {
+ // selectActiveHost(r.dst.public_key)
+ // }}
+ arcsTransitionDuration={0}
+ pointsData={points}
+ pointLat={(h: SiaCentralPartialHost) => h.location[0]}
+ pointLng={(h: SiaCentralPartialHost) => h.location[1]}
+ pointLabel={(h: SiaCentralPartialHost) =>
+ getHostLabel({ host: h, rates: rates.data?.rates.sc })
+ }
+ // pointAltitude={
+ // (h: SiaCentralPartialHost) => h.settings.remainingstorage / 1e13 / 100
+ // // h.public_key === activeHost.public_key ? 0.6 : 0.2
+ // }
+ pointAltitude={(h: SiaCentralPartialHost) => {
+ return 0.05
+ }}
+ pointsTransitionDuration={0}
+ pointColor={(h: SiaCentralPartialHost) => {
+ return activeHost?.public_key === h.public_key
+ ? colors.green[600]
+ : colorWithOpacity(colors.green[200], 1)
+ }}
+ pointRadius={(h: SiaCentralPartialHost) => {
+ let radius = 0
+ radius = h.settings.total_storage / 1e13 / 3
+ return Math.max(radius, 0.1)
+ }}
+ onPointHover={(h: SiaCentralPartialHost) => {
+ if (!h) {
+ return
+ }
+ onHostHover?.(h.public_key, h.location)
+ }}
+ onPointClick={(h: SiaCentralPartialHost) => {
+ if (!h) {
+ return
+ }
+ onHostClick?.(h.public_key, h.location)
+ }}
+ pointsMerge={false}
+ />
+
+ )
+}
+
+function doesIncludeActiveHost(
+ route: Route,
+ activeHost?: SiaCentralPartialHost
+) {
+ if (!activeHost) {
+ return false
+ }
+ return (
+ route.dst.public_key === activeHost.public_key ||
+ route.src.public_key === activeHost.public_key
+ )
+}
+
+function colorWithOpacity(hexColor: string, opacity: number) {
+ const r = parseInt(hexColor.slice(1, 3), 16)
+ const g = parseInt(hexColor.slice(3, 5), 16)
+ const b = parseInt(hexColor.slice(5, 7), 16)
+
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`
+}
diff --git a/apps/website/components/Map/GlobeDyn.tsx b/apps/website/components/Map/GlobeDyn.tsx
new file mode 100644
index 000000000..7c4214a05
--- /dev/null
+++ b/apps/website/components/Map/GlobeDyn.tsx
@@ -0,0 +1,14 @@
+import { forwardRef, MutableRefObject } from 'react'
+import dynamic from 'next/dynamic'
+import { GlobeMethods } from 'react-globe.gl'
+
+const GlobeGl = dynamic(() => import('./GlobeImp'), {
+ ssr: false,
+})
+
+export const GlobeDyn = forwardRef(function ReactGlobe(
+ props: Omit, 'forwardRef'>,
+ ref: MutableRefObject
+) {
+ return
+})
diff --git a/apps/website/components/Map/GlobeImp.tsx b/apps/website/components/Map/GlobeImp.tsx
new file mode 100644
index 000000000..af0372b6c
--- /dev/null
+++ b/apps/website/components/Map/GlobeImp.tsx
@@ -0,0 +1,11 @@
+import { MutableRefObject } from 'react'
+import GlobeTmpl, { GlobeMethods } from 'react-globe.gl'
+
+const GlobeImp = ({
+ forwardRef,
+ ...otherProps
+}: React.ComponentProps & {
+ forwardRef: MutableRefObject
+}) =>
+
+export default GlobeImp
diff --git a/apps/website/components/Map/HostItem.tsx b/apps/website/components/Map/HostItem.tsx
new file mode 100644
index 000000000..191fab1c6
--- /dev/null
+++ b/apps/website/components/Map/HostItem.tsx
@@ -0,0 +1,144 @@
+import { useMemo } from 'react'
+import {
+ Button,
+ monthsToBlocks,
+ TBToBytes,
+ Text,
+ countryCodeEmoji,
+ LinkButton,
+ webLinks,
+} from '@siafoundation/design-system'
+import { humanBytes, humanSiacoin } from '@siafoundation/sia-js'
+import { cx } from 'class-variance-authority'
+import BigNumber from 'bignumber.js'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
+import { Launch16 } from '@carbon/icons-react'
+import { getDownloadSpeed, getUploadSpeed } from '@siafoundation/units'
+
+type Props = {
+ host: SiaCentralPartialHost
+ activeHost: SiaCentralPartialHost
+ setRef?: (el: HTMLButtonElement) => void
+ selectActiveHost: (public_key: string) => void
+ rates: {
+ usd: string
+ }
+}
+
+export function HostItem({
+ host,
+ activeHost,
+ selectActiveHost,
+ setRef,
+ rates,
+}: Props) {
+ const storageCost = useMemo(
+ () =>
+ rates
+ ? `$${new BigNumber(host.settings.storage_price)
+ .times(TBToBytes(1))
+ .times(monthsToBlocks(1))
+ .div(1e24)
+ .times(rates?.usd || 1)
+ .toFixed(2)}/TB`
+ : `${humanSiacoin(
+ new BigNumber(host.settings.storage_price)
+ .times(TBToBytes(1))
+ .times(monthsToBlocks(1)),
+ { fixed: 0 }
+ )}/TB`,
+ [rates, host]
+ )
+
+ const downloadCost = useMemo(
+ () =>
+ rates
+ ? `$${new BigNumber(host.settings.download_price)
+ .times(TBToBytes(1))
+ .div(1e24)
+ .times(rates?.usd || 1)
+ .toFixed(2)}/TB`
+ : `${humanSiacoin(
+ new BigNumber(host.settings.download_price).times(TBToBytes(1)),
+ { fixed: 0 }
+ )}/TB`,
+ [rates, host]
+ )
+
+ const uploadCost = useMemo(
+ () =>
+ rates
+ ? `$${new BigNumber(host.settings.upload_price)
+ .times(TBToBytes(1))
+ .div(1e24)
+ .times(rates?.usd || 1)
+ .toFixed(2)}/TB`
+ : `${humanSiacoin(
+ new BigNumber(host.settings.upload_price).times(TBToBytes(1)),
+ { fixed: 0 }
+ )}/TB`,
+ [rates, host]
+ )
+
+ return (
+ selectActiveHost(host.public_key)}
+ className={cx(
+ 'flex flex-col py-1 px-2 gap-1 h-[100px] cursor-pointer',
+ activeHost?.public_key === host.public_key && 'ring !ring-green-600'
+ )}
+ key={host.public_key}
+ id={host.public_key}
+ >
+
+
+ {countryCodeEmoji(host.country_code)} {host.country_code}
+
+
+
+
+
+
+
+
+ storage
+
+
+ download
+
+
+ upload
+
+
+
+
+ {humanBytes(host.settings.total_storage)}
+
+
+ {getDownloadSpeed(host)}
+
+
+ {getUploadSpeed(host)}
+
+
+
+
+ {storageCost}
+
+
+ {downloadCost}
+
+
+ {uploadCost}
+
+
+
+
+ )
+}
diff --git a/apps/website/components/Map/index.tsx b/apps/website/components/Map/index.tsx
new file mode 100644
index 000000000..cf01a86b2
--- /dev/null
+++ b/apps/website/components/Map/index.tsx
@@ -0,0 +1,176 @@
+import { useAppSettings } from '@siafoundation/react-core'
+import { Globe } from './Globe'
+import { useCallback, useMemo, useRef, useState } from 'react'
+import { ScrollArea, Text, Tooltip } from '@siafoundation/design-system'
+import { HostItem } from './HostItem'
+import { Navbar } from '../Layout/Navbar'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
+import { PageHead } from '../PageHead'
+import { previews } from '../../content/assets'
+import { Stats } from '../../content/stats'
+
+export type Commands = {
+ moveToLocation: (
+ location: [number, number] | undefined,
+ altitude?: number
+ ) => void
+}
+
+export const emptyCommands: Commands = {
+ moveToLocation: (location: [number, number], altitude?: number) => null,
+}
+
+type Props = {
+ hosts: SiaCentralPartialHost[]
+ rates: { usd: string }
+ stats: Stats
+}
+
+export function Map({ hosts, rates, stats }: Props) {
+ const { gpu, settings } = useAppSettings()
+
+ const [activeHostPublicKey, setActiveHostPublicKey] = useState(
+ hosts[0].public_key
+ )
+
+ const activeHost = useMemo(
+ () => hosts?.find((d) => d.public_key === activeHostPublicKey),
+ [hosts, activeHostPublicKey]
+ )
+
+ const cmdRef = useRef(emptyCommands)
+
+ const setCmd = useCallback(
+ (cmd: Commands) => {
+ cmdRef.current = cmd
+ },
+ [cmdRef]
+ )
+
+ const scrollToHost = useCallback((publicKey: string) => {
+ // move table to host, select via data id data-table
+ const selectedElement = document.getElementById(publicKey)
+ const scrollContainer = document.getElementById('scroll-hosts')
+
+ const containerWidth = scrollContainer.offsetWidth
+ const selectedElementWidth = selectedElement.offsetWidth
+ const selectedElementLeft = selectedElement.offsetLeft
+
+ const scrollPosition =
+ selectedElementLeft - containerWidth / 2 + selectedElementWidth / 2
+ scrollContainer.scroll({
+ left: scrollPosition,
+ behavior: 'smooth',
+ })
+ }, [])
+
+ const onHostMapClick = useCallback(
+ (publicKey: string, location?: [number, number]) => {
+ if (activeHostPublicKey === publicKey) {
+ setActiveHostPublicKey(undefined)
+ return
+ }
+ setActiveHostPublicKey(publicKey)
+ if (location) {
+ cmdRef.current.moveToLocation(location)
+ }
+ scrollToHost(publicKey)
+ },
+ [setActiveHostPublicKey, cmdRef, activeHostPublicKey, scrollToHost]
+ )
+
+ const onHostListClick = useCallback(
+ (publicKey: string, location?: [number, number]) => {
+ if (activeHostPublicKey === publicKey) {
+ setActiveHostPublicKey(undefined)
+ return
+ }
+ setActiveHostPublicKey(publicKey)
+ if (location) {
+ cmdRef.current.moveToLocation(location)
+ }
+ scrollToHost(publicKey)
+ },
+ [setActiveHostPublicKey, cmdRef, activeHostPublicKey, scrollToHost]
+ )
+
+ const onHostMapHover = useCallback(
+ (publicKey: string, location?: [number, number]) => null,
+ []
+ )
+
+ if (settings.siaCentral && !gpu.shouldRender) {
+ return null
+ }
+
+ return (
+
+
+
+
+ {
+ setCmd(cmd)
+ }}
+ />
+
+
+
+
+
+
+ {stats.activeHosts} hosts
+
+
+
+
+ {stats.totalStorage}
+
+
+
+
+
+ {hosts.map((h, i) => (
+
+
+ onHostListClick(h.public_key, h.location)
+ }
+ rates={rates}
+ />
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/apps/website/components/Map/useRoutes.tsx b/apps/website/components/Map/useRoutes.tsx
new file mode 100644
index 000000000..13e83233b
--- /dev/null
+++ b/apps/website/components/Map/useRoutes.tsx
@@ -0,0 +1,92 @@
+import { useMemo } from 'react'
+import { random, sortBy } from 'lodash'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
+
+type Props = {
+ activeHost?: SiaCentralPartialHost
+ hosts?: SiaCentralPartialHost[]
+}
+
+type Route = {
+ distance: number
+ src: SiaCentralPartialHost
+ dst: SiaCentralPartialHost
+}
+
+export function useDecRoutes({ activeHost, hosts }: Props) {
+ const backgroundRoutes = useMemo(() => {
+ const routes: Route[] = []
+ if (!hosts) {
+ return routes
+ }
+ for (let i = 0; i < hosts.length; i++) {
+ const host1 = hosts[i]
+ let hostRoutes: Route[] = []
+ for (let j = 0; j < hosts.length; j++) {
+ if (i === j) {
+ continue
+ }
+ const host2 = hosts[j]
+ const distance = distanceBetweenHosts(host1, host2)
+ hostRoutes.push({
+ distance,
+ src: host1,
+ dst: host2,
+ })
+ }
+ hostRoutes = sortBy(hostRoutes, 'distance')
+ const closest = hostRoutes.slice(4, 6)
+ routes.push(...closest)
+
+ const addExtra = Math.random() < 0.1
+ if (addExtra) {
+ const randomDistantIndex = random(
+ // Math.round((hostRoutes.length - 1) / 2),
+ 0,
+ hostRoutes.length - 1
+ )
+ const extra = hostRoutes[randomDistantIndex]
+ routes.push(extra)
+ }
+ }
+ return routes
+ }, [hosts])
+
+ const activeRoutes = useMemo(() => {
+ let routes: Route[] = []
+ if (!hosts || !activeHost) {
+ return routes
+ }
+ for (let i = 0; i < hosts.length; i++) {
+ const host = hosts[i]
+ if (activeHost.public_key === host.public_key) {
+ continue
+ }
+ const distance = distanceBetweenHosts(activeHost, host)
+ routes.push({
+ distance,
+ src: activeHost,
+ dst: host,
+ })
+ }
+ routes = sortBy(routes, 'distance')
+ return routes.slice(0, 5)
+ }, [activeHost, hosts])
+
+ const routes = useMemo(
+ () => [...backgroundRoutes, ...activeRoutes],
+ [backgroundRoutes, activeRoutes]
+ )
+
+ return routes
+}
+
+function distanceBetweenHosts(
+ h1: SiaCentralPartialHost,
+ h2: SiaCentralPartialHost
+) {
+ return Math.sqrt(
+ Math.pow(h1.location[0] - h2.location[0], 2) +
+ Math.pow(h1.location[1] - h2.location[1], 2)
+ )
+}
diff --git a/apps/website/components/Map/utils.ts b/apps/website/components/Map/utils.ts
new file mode 100644
index 000000000..e811e5049
--- /dev/null
+++ b/apps/website/components/Map/utils.ts
@@ -0,0 +1,44 @@
+import {
+ monthsToBlocks,
+ TBToBytes,
+ countryCodeEmoji,
+} from '@siafoundation/design-system'
+import { humanBytes, humanSiacoin } from '@siafoundation/sia-js'
+import { SiaCentralPartialHost } from '../../content/geoHosts'
+import BigNumber from 'bignumber.js'
+
+export function getHostLabel({
+ host,
+ rates,
+}: {
+ host: SiaCentralPartialHost
+ rates?: {
+ usd: string
+ }
+}) {
+ const storageCost = rates
+ ? `$${new BigNumber(host.settings.storage_price)
+ .times(TBToBytes(1))
+ .times(monthsToBlocks(1))
+ .div(1e24)
+ .times(rates?.usd || 1)
+ .toFixed(2)}/TB`
+ : `${humanSiacoin(
+ new BigNumber(host.settings.storage_price)
+ .times(TBToBytes(1))
+ .times(monthsToBlocks(1)),
+ { fixed: 0 }
+ )}/TB`
+
+ const usedStorage = `${humanBytes(
+ host.settings.total_storage - host.settings.remaining_storage
+ )} utilized`
+
+ const availableStorage = `${humanBytes(
+ host.settings.remaining_storage
+ )} / ${humanBytes(host.settings.total_storage)} available`
+
+ return `${countryCodeEmoji(
+ host.country_code
+ )} · ${storageCost} · ${usedStorage} · ${availableStorage}`
+}
diff --git a/apps/website/content/geoHosts.ts b/apps/website/content/geoHosts.ts
index 4da6c2f55..a69cbf042 100644
--- a/apps/website/content/geoHosts.ts
+++ b/apps/website/content/geoHosts.ts
@@ -7,14 +7,14 @@ const maxAge = getMinutesInSeconds(5)
const maxHosts = 1000
const minDegreesApart = 1
-export async function getGeoHosts(): Promise {
+export async function getGeoHosts(): Promise {
return getCacheValue(
'geoHosts',
async () => {
const { data: siaCentralHosts, error } = await getSiaCentralHosts({
params: {
- limit: 300
- }
+ limit: 300,
+ },
})
if (error) {
return []
@@ -23,7 +23,7 @@ export async function getGeoHosts(): Promise {
hosts.sort((a, b) =>
a.settings.total_storage - a.settings.remaining_storage <
- b.settings.total_storage - a.settings.remaining_storage
+ b.settings.total_storage - a.settings.remaining_storage
? 1
: -1
)
@@ -43,9 +43,9 @@ export async function getGeoHosts(): Promise {
for (const hostToDisplay of hostsToDisplay) {
if (
Math.abs(hostToDisplay.location[0] - host.location[0]) <
- minDegreesApart &&
+ minDegreesApart &&
Math.abs(hostToDisplay.location[1] - host.location[1]) <
- minDegreesApart
+ minDegreesApart
) {
unique = false
break
@@ -65,7 +65,7 @@ export async function getGeoHosts(): Promise {
)
}
-export type Host = {
+export type SiaCentralPartialHost = {
public_key: string
country_code: string
location: [number, number]
@@ -74,8 +74,9 @@ export type Host = {
download_price: string
upload_price: string
total_storage: number
+ remaining_storage: number
}
- benchmark: {
+ benchmark?: {
data_size: number
download_time: number
upload_time: number
@@ -83,7 +84,7 @@ export type Host = {
}
// transform to only necessary data to limit transfer size
-export function transformHost(h: SiaCentralHost): Host {
+export function transformHost(h: SiaCentralHost): SiaCentralPartialHost {
return {
public_key: h.public_key,
country_code: h.country_code,
@@ -93,11 +94,12 @@ export function transformHost(h: SiaCentralHost): Host {
download_price: h.settings.download_price,
upload_price: h.settings.upload_price,
total_storage: h.settings.total_storage,
+ remaining_storage: h.settings.remaining_storage,
},
benchmark: {
- data_size: h.benchmark.data_size,
- download_time: h.benchmark.download_time,
- upload_time: h.benchmark.upload_time,
+ data_size: h.benchmark?.data_size || null,
+ download_time: h.benchmark?.download_time || null,
+ upload_time: h.benchmark?.upload_time || null,
},
}
}
diff --git a/apps/website/pages/globe.tsx b/apps/website/pages/map.tsx
similarity index 56%
rename from apps/website/pages/globe.tsx
rename to apps/website/pages/map.tsx
index 1337bcd6e..b5a922eb6 100644
--- a/apps/website/pages/globe.tsx
+++ b/apps/website/pages/map.tsx
@@ -1,22 +1,27 @@
import { AsyncReturnType } from '../lib/types'
-import { HostMap } from '../components/HostMap'
import { getGeoHosts } from '../content/geoHosts'
import { getExchangeRates } from '../content/exchangeRates'
import { getMinutesInSeconds } from '../lib/time'
+import { Map } from '../components/Map'
+import { getStats } from '../content/stats'
type Props = AsyncReturnType['props']
-export default function Globe({ hosts, rates }: Props) {
- return
+export default function MapPage({ hosts, rates, stats }: Props) {
+ return
}
export async function getStaticProps() {
- const hosts = await getGeoHosts()
- const { data: rates } = await getExchangeRates()
+ const [hosts, { data: rates }, stats] = await Promise.all([
+ getGeoHosts(),
+ getExchangeRates(),
+ getStats(),
+ ])
const props = {
hosts,
rates: rates?.rates.sc,
+ stats,
}
return {
diff --git a/libs/design-system/src/components/ValueSc.tsx b/libs/design-system/src/components/ValueSc.tsx
index 5813d0eb4..54e20093f 100644
--- a/libs/design-system/src/components/ValueSc.tsx
+++ b/libs/design-system/src/components/ValueSc.tsx
@@ -13,6 +13,7 @@ type Props = {
tooltip?: string
fixed?: number
dynamicUnits?: boolean
+ hastingUnits?: boolean
extendedSuffix?: string
showTooltip?: boolean
}
@@ -25,6 +26,7 @@ export function ValueSc({
variant = 'change',
fixed = 3,
dynamicUnits = true,
+ hastingUnits = true,
extendedSuffix,
showTooltip = true,
}: Props) {
@@ -52,7 +54,7 @@ export function ValueSc({
fixed,
dynamicUnits,
})}`
- : humanSiacoin(value, { fixed, dynamicUnits })}
+ : humanSiacoin(value, { fixed, dynamicUnits, hastingUnits })}
{extendedSuffix ? `${extendedSuffix}` : ''}
diff --git a/libs/design-system/src/components/Wordmark.tsx b/libs/design-system/src/components/Wordmark.tsx
index 6ec83d671..fdd3930df 100644
--- a/libs/design-system/src/components/Wordmark.tsx
+++ b/libs/design-system/src/components/Wordmark.tsx
@@ -1,14 +1,32 @@
-import wordmark from '../assets/wordmark.svg'
-import { Image } from '../core/Image'
+type Props = {
+ size?: number
+ color: string
+ className?: string
+}
-export function Wordmark({ size = 20 }) {
+export function Wordmark({ size = 20, className, color = '#333333' }: Props) {
return (
-
+ viewBox="0 0 65 39"
+ className={className}
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+
+
+
+
)
}
diff --git a/libs/react-renterd/src/siaTypes.ts b/libs/react-renterd/src/siaTypes.ts
index e4e38ff42..06cda247f 100644
--- a/libs/react-renterd/src/siaTypes.ts
+++ b/libs/react-renterd/src/siaTypes.ts
@@ -346,6 +346,7 @@ export interface AutopilotHostsConfig {
allowRedundantIPs: boolean
scoreOverrides: { [key: PublicKey]: number }
maxDowntimeHours: number
+ minRecentScanFailures: number
}
export interface AutopilotContractsConfig {
diff --git a/libs/units/.babelrc b/libs/units/.babelrc
new file mode 100644
index 000000000..1ea870ead
--- /dev/null
+++ b/libs/units/.babelrc
@@ -0,0 +1,12 @@
+{
+ "presets": [
+ [
+ "@nx/react/babel",
+ {
+ "runtime": "automatic",
+ "useBuiltIns": "usage"
+ }
+ ]
+ ],
+ "plugins": []
+}
diff --git a/libs/units/.eslintrc.json b/libs/units/.eslintrc.json
new file mode 100644
index 000000000..1e06b264c
--- /dev/null
+++ b/libs/units/.eslintrc.json
@@ -0,0 +1,13 @@
+{
+ "extends": ["plugin:@nx/react", "../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.json"],
+ "parser": "jsonc-eslint-parser",
+ "rules": {
+ "@nx/dependency-checks": "error"
+ }
+ }
+ ]
+}
diff --git a/libs/units/CHANGELOG.md b/libs/units/CHANGELOG.md
new file mode 100644
index 000000000..7fa4e4862
--- /dev/null
+++ b/libs/units/CHANGELOG.md
@@ -0,0 +1 @@
+# @siafoundation/units
diff --git a/libs/units/README.md b/libs/units/README.md
new file mode 100644
index 000000000..85e79a04d
--- /dev/null
+++ b/libs/units/README.md
@@ -0,0 +1,7 @@
+# units
+
+Methods and types for converting and displaying units.
+
+## Running unit tests
+
+Run `nx test units` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/libs/units/jest.config.ts b/libs/units/jest.config.ts
new file mode 100644
index 000000000..e014298d9
--- /dev/null
+++ b/libs/units/jest.config.ts
@@ -0,0 +1,10 @@
+/* eslint-disable */
+export default {
+ displayName: 'units',
+ preset: '../../jest.preset.js',
+ transform: {
+ '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
+ },
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+ coverageDirectory: '../../coverage/libs/units',
+}
diff --git a/libs/units/package.json b/libs/units/package.json
new file mode 100644
index 000000000..f967574e9
--- /dev/null
+++ b/libs/units/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@siafoundation/units",
+ "description": "Methods and types for interacting with the Sia Central API.",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@siafoundation/design-system": "^0.58.1",
+ "@siafoundation/sia-central": "^0.1.0",
+ "bignumber.js": "^9.0.2",
+ "@siafoundation/sia-js": "^0.11.0",
+ "@siafoundation/react-core": "^0.15.0"
+ },
+ "types": "./src/index.d.ts"
+}
diff --git a/libs/units/project.json b/libs/units/project.json
new file mode 100644
index 000000000..c917025e2
--- /dev/null
+++ b/libs/units/project.json
@@ -0,0 +1,48 @@
+{
+ "name": "units",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/units/src",
+ "projectType": "library",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@nrwl/rollup:rollup",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/libs/units",
+ "tsConfig": "libs/units/tsconfig.lib.json",
+ "project": "libs/units/package.json",
+ "entryFile": "libs/units/src/index.ts",
+ "external": ["react/jsx-runtime"],
+ "rollupConfig": "@nx/react/plugins/bundle-rollup",
+ "compiler": "swc",
+ "assets": [
+ {
+ "glob": "libs/units/*.md",
+ "input": ".",
+ "output": "."
+ }
+ ]
+ },
+ "configurations": {}
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/units/**/*.{ts,tsx,js,jsx}",
+ "libs/units/package.json"
+ ]
+ }
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/libs/units"],
+ "options": {
+ "jestConfig": "libs/units/jest.config.ts",
+ "passWithNoTests": true
+ }
+ }
+ }
+}
diff --git a/apps/explorer/lib/host.ts b/libs/units/src/index.ts
similarity index 77%
rename from apps/explorer/lib/host.ts
rename to libs/units/src/index.ts
index c23c282ce..28174b118 100644
--- a/apps/explorer/lib/host.ts
+++ b/libs/units/src/index.ts
@@ -50,16 +50,28 @@ export function getUploadCost({ price, exchange }: Props) {
})}/TB`
}
-export function getDownloadSpeed(host: SiaCentralHost) {
- return humanSpeed(
- (host.benchmark.data_size * 8) / (host.benchmark.download_time / 1000)
- )
+type SiaCentralPartialHost = {
+ benchmark?: {
+ data_size: number
+ download_time: number
+ upload_time: number
+ }
+}
+
+export function getDownloadSpeed(host: SiaCentralPartialHost) {
+ return host.benchmark
+ ? humanSpeed(
+ (host.benchmark.data_size * 8) / (host.benchmark.download_time / 1000)
+ )
+ : '-'
}
-export function getUploadSpeed(host: SiaCentralHost) {
- return humanSpeed(
- (host.benchmark.data_size * 8) / (host.benchmark.upload_time / 1000)
- )
+export function getUploadSpeed(host: SiaCentralPartialHost) {
+ return host.benchmark
+ ? humanSpeed(
+ (host.benchmark.data_size * 8) / (host.benchmark.upload_time / 1000)
+ )
+ : '-'
}
export function getRemainingOverTotalStorage(host: SiaCentralHost) {
diff --git a/libs/units/tsconfig.json b/libs/units/tsconfig.json
new file mode 100644
index 000000000..4c089585e
--- /dev/null
+++ b/libs/units/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowJs": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/libs/units/tsconfig.lib.json b/libs/units/tsconfig.lib.json
new file mode 100644
index 000000000..621db72d7
--- /dev/null
+++ b/libs/units/tsconfig.lib.json
@@ -0,0 +1,23 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": ["node"]
+ },
+ "files": [
+ "../../node_modules/@nx/react/typings/cssmodule.d.ts",
+ "../../node_modules/@nx/react/typings/image.d.ts"
+ ],
+ "exclude": [
+ "**/*.spec.ts",
+ "**/*.test.ts",
+ "**/*.spec.tsx",
+ "**/*.test.tsx",
+ "**/*.spec.js",
+ "**/*.test.js",
+ "**/*.spec.jsx",
+ "**/*.test.jsx",
+ "jest.config.ts"
+ ],
+ "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+}
diff --git a/libs/units/tsconfig.spec.json b/libs/units/tsconfig.spec.json
new file mode 100644
index 000000000..a85d573fc
--- /dev/null
+++ b/libs/units/tsconfig.spec.json
@@ -0,0 +1,20 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["jest", "node"]
+ },
+ "include": [
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "**/*.test.tsx",
+ "**/*.spec.tsx",
+ "**/*.test.js",
+ "**/*.spec.js",
+ "**/*.test.jsx",
+ "**/*.spec.jsx",
+ "**/*.d.ts",
+ "jest.config.ts"
+ ]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index c9c06d882..816901039 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -27,7 +27,8 @@
"@siafoundation/react-walletd": ["libs/react-walletd/src/index.ts"],
"@siafoundation/sia-central": ["libs/sia-central/src/index.ts"],
"@siafoundation/sia-js": ["libs/sia-js/src/index.ts"],
- "@siafoundation/sia-nodejs": ["libs/sia-nodejs/src/index.ts"]
+ "@siafoundation/sia-nodejs": ["libs/sia-nodejs/src/index.ts"],
+ "@siafoundation/units": ["libs/units/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]