Fix stats reporting. Add Vite compatibility
This commit is contained in:
@@ -501,6 +501,7 @@ input[type="color"]:focus {
|
||||
color: var(--input_text);
|
||||
text-decoration: none;
|
||||
background: var(--input_hover_background);
|
||||
box-shadow: 0px 0px 0px 1px var(--highlight_color);
|
||||
}
|
||||
|
||||
button:active,
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
window.server_hostname = "{{.Hostname}}";
|
||||
</script>
|
||||
|
||||
<script defer src='/res/svelte/wrap.js?v{{cacheID}}'></script>
|
||||
<script defer src='/res/svelte/wrap.js?v{{cacheID}}' type="module"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
744
svelte/package-lock.json
generated
744
svelte/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,10 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w"
|
||||
"rbuild": "rollup -c",
|
||||
"rdev": "rollup -c -w",
|
||||
"vbuild": "vite build",
|
||||
"vdev": "NODE_ENV=development vite build --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
@@ -15,18 +17,21 @@
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/jsmediatags": "^3.9.6",
|
||||
"@types/node": "^24.10.0",
|
||||
"rollup": "^4.24.4",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
"rollup-plugin-svelte": "^7.2.2",
|
||||
"svelte": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"behave-js": "^1.5.0",
|
||||
"chart.js": "^4.4.6",
|
||||
"country-data-list": "^1.4.0",
|
||||
"pure-color": "^1.3.0",
|
||||
"rollup-plugin-includepaths": "^0.2.4",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"tslib": "^2.8.1"
|
||||
"tslib": "^2.8.1",
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<script>
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting";
|
||||
import Chart from "util/Chart.svelte";
|
||||
import { color_by_name } from "util/Util";
|
||||
import ServerDiagnostics from "./ServerDiagnostics.svelte";
|
||||
import PeerTable from "./PeerTable.svelte";
|
||||
import { get_endpoint } from "lib/PixeldrainAPI";
|
||||
|
||||
let graphViews = $state()
|
||||
let graphBandwidth = $state()
|
||||
let graphTimeout = null
|
||||
let graphEgress: Chart = $state()
|
||||
let graphDownloads: Chart = $state()
|
||||
let graphTimeout: NodeJS.Timeout = null
|
||||
|
||||
let start_time = $state("")
|
||||
let end_time = $state("")
|
||||
let total_bandwidth = $state(0)
|
||||
let total_bandwidth_paid = $state(0)
|
||||
let total_views = $state(0)
|
||||
let total_egress = $state(0)
|
||||
let total_downloads = $state(0)
|
||||
const loadGraph = (minutes, interval, live) => {
|
||||
const loadGraph = (minutes: number, interval: number, live: boolean) => {
|
||||
if (graphTimeout !== null) { clearTimeout(graphTimeout) }
|
||||
if (live) {
|
||||
graphTimeout = setTimeout(() => { loadGraph(minutes, interval, true) }, 10000)
|
||||
@@ -27,7 +26,7 @@ const loadGraph = (minutes, interval, live) => {
|
||||
start.setMinutes(start.getMinutes() - minutes)
|
||||
|
||||
fetch(
|
||||
window.api_endpoint + "/admin/files/timeseries" +
|
||||
get_endpoint() + "/admin/files/timeseries" +
|
||||
"?start=" + start.toISOString() +
|
||||
"&end=" + today.toISOString() +
|
||||
"&interval=" + interval
|
||||
@@ -35,37 +34,34 @@ const loadGraph = (minutes, interval, live) => {
|
||||
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
|
||||
return resp.json();
|
||||
}).then(resp => {
|
||||
resp.views.timestamps.forEach((val, idx) => {
|
||||
resp.egress.timestamps.forEach((val, idx) => {
|
||||
let date = new Date(val);
|
||||
let dateStr = date.getFullYear();
|
||||
let dateStr: string = date.getFullYear().toString();
|
||||
dateStr += "-" + ("00" + (date.getMonth() + 1)).slice(-2);
|
||||
dateStr += "-" + ("00" + date.getDate()).slice(-2);
|
||||
dateStr += " " + ("00" + date.getHours()).slice(-2);
|
||||
dateStr += ":" + ("00" + date.getMinutes()).slice(-2);
|
||||
resp.views.timestamps[idx] = " " + dateStr + " "; // Poor man's padding
|
||||
resp.egress.timestamps[idx] = " " + dateStr + " "; // Poor man's padding
|
||||
});
|
||||
graphViews.data().labels = resp.views.timestamps;
|
||||
graphViews.data().datasets[0].data = resp.views.amounts;
|
||||
graphViews.data().datasets[1].data = resp.downloads.amounts;
|
||||
graphBandwidth.data().labels = resp.views.timestamps;
|
||||
graphBandwidth.data().datasets[0].data = resp.bandwidth.amounts
|
||||
graphBandwidth.data().datasets[1].data = resp.bandwidth_paid.amounts
|
||||
graphViews.update()
|
||||
graphBandwidth.update()
|
||||
graphEgress.data().labels = resp.egress.timestamps;
|
||||
graphEgress.data().datasets[0].data = resp.egress.amounts;
|
||||
graphDownloads.data().labels = resp.downloads.timestamps;
|
||||
graphDownloads.data().datasets[0].data = resp.downloads.amounts
|
||||
graphEgress.update()
|
||||
graphDownloads.update()
|
||||
|
||||
start_time = resp.views.timestamps[0]
|
||||
end_time = resp.views.timestamps.slice(-1)[0];
|
||||
total_bandwidth = resp.bandwidth.amounts.reduce((acc, val) => acc + val)
|
||||
total_bandwidth_paid = resp.bandwidth_paid.amounts.reduce((acc, val) => acc + val)
|
||||
total_views = resp.views.amounts.reduce((acc, val) => acc + val)
|
||||
start_time = resp.egress.timestamps[0]
|
||||
end_time = resp.egress.timestamps.slice(-1)[0];
|
||||
total_egress = resp.egress.amounts.reduce((acc, val) => acc + val)
|
||||
total_downloads = resp.downloads.amounts.reduce((acc, val) => acc + val)
|
||||
})
|
||||
}
|
||||
|
||||
// Load performance statistics
|
||||
|
||||
let lastOrder = $state();
|
||||
let lastOrder: string = $state();
|
||||
let status = $state({
|
||||
pid: 0,
|
||||
cpu_profile_running_since: "",
|
||||
db_latency: 0,
|
||||
db_time: "",
|
||||
@@ -74,33 +70,32 @@ let status = $state({
|
||||
local_read_size_per_sec: 0,
|
||||
local_reads: 0,
|
||||
local_reads_per_sec: 0,
|
||||
local_readers: 0,
|
||||
neighbour_read_size: 0,
|
||||
neighbour_read_size_per_sec: 0,
|
||||
neighbour_reads: 0,
|
||||
neighbour_reads_per_sec: 0,
|
||||
peers: [],
|
||||
query_statistics: [],
|
||||
neighbour_readers: 0,
|
||||
remote_read_size: 0,
|
||||
remote_read_size_per_sec: 0,
|
||||
remote_reads: 0,
|
||||
remote_reads_per_sec: 0,
|
||||
remote_readers: 0,
|
||||
peers: [],
|
||||
query_statistics: [],
|
||||
running_since: "",
|
||||
stats_watcher_listeners: 0,
|
||||
stats_watcher_threads: 0,
|
||||
filesystem_watcher_listeners: 0,
|
||||
filesystem_watcher_threads: 0,
|
||||
rate_limit_watcher_threads: 0,
|
||||
rate_limit_watcher_listeners: 0,
|
||||
download_clients: 0,
|
||||
download_connections: 0,
|
||||
})
|
||||
let total_reads = $derived(status.local_reads + status.neighbour_reads + status.remote_reads)
|
||||
let total_read_size = $derived(status.local_read_size + status.neighbour_read_size + status.remote_read_size)
|
||||
|
||||
function getStats(order) {
|
||||
function getStats(order: string) {
|
||||
lastOrder = order
|
||||
|
||||
fetch(window.api_endpoint + "/status").then(
|
||||
fetch(get_endpoint() + "/status").then(
|
||||
resp => resp.json()
|
||||
).then(resp => {
|
||||
// Sort all queries by the selected sort column
|
||||
@@ -126,37 +121,25 @@ function getStats(order) {
|
||||
let statsInterval = null
|
||||
onMount(() => {
|
||||
// Prepare chart datasets
|
||||
graphBandwidth.data().datasets = [
|
||||
graphEgress.data().datasets = [
|
||||
{
|
||||
label: "Bandwidth (free)",
|
||||
label: "Egress",
|
||||
data: [],
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("highlight_color"),
|
||||
backgroundColor: color_by_name("highlight_color"),
|
||||
},
|
||||
{
|
||||
label: "Bandwidth (premium)",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("danger_color"),
|
||||
backgroundColor: color_by_name("danger_color"),
|
||||
},
|
||||
];
|
||||
graphViews.data().datasets = [
|
||||
graphDownloads.data().datasets = [
|
||||
{
|
||||
label: "Views",
|
||||
label: "Downloads",
|
||||
data: [],
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("highlight_color"),
|
||||
backgroundColor: color_by_name("highlight_color"),
|
||||
},
|
||||
{
|
||||
label: "Downloads",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("danger_color"),
|
||||
backgroundColor: color_by_name("danger_color"),
|
||||
},
|
||||
];
|
||||
|
||||
loadGraph(10080, 10, true);
|
||||
@@ -164,14 +147,15 @@ onMount(() => {
|
||||
statsInterval = setInterval(() => {
|
||||
getStats(lastOrder)
|
||||
}, 10000)
|
||||
})
|
||||
onDestroy(() => {
|
||||
|
||||
return () => {
|
||||
if (graphTimeout !== null) {
|
||||
clearTimeout(graphTimeout)
|
||||
}
|
||||
if (statsInterval !== null) {
|
||||
clearInterval(statsInterval)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -188,14 +172,12 @@ onDestroy(() => {
|
||||
<button onclick={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button>
|
||||
<button onclick={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button>
|
||||
</div>
|
||||
<Chart bind:this={graphBandwidth} data_type="bytes" />
|
||||
<Chart bind:this={graphViews} data_type="number" />
|
||||
<Chart bind:this={graphEgress} data_type="bytes" />
|
||||
<Chart bind:this={graphDownloads} data_type="number" />
|
||||
<div class="highlight_border">
|
||||
Total usage from {start_time} to {end_time}<br/>
|
||||
{formatDataVolume(total_bandwidth, 3)} bandwidth,
|
||||
{formatDataVolume(total_bandwidth_paid, 3)} paid bandwidth,
|
||||
{formatThousands(total_views, 3)} views and
|
||||
{formatThousands(total_downloads, 3)} downloads
|
||||
{formatDataVolume(total_egress, 3)} egress,
|
||||
{formatThousands(total_downloads)} downloads
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
@@ -222,6 +204,7 @@ onDestroy(() => {
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Source</td>
|
||||
<td>Current</td>
|
||||
<td>Reads</td>
|
||||
<td>Reads %</td>
|
||||
<td>Reads / s</td>
|
||||
@@ -233,6 +216,7 @@ onDestroy(() => {
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Local cache</td>
|
||||
<td>{status.local_readers}</td>
|
||||
<td>{status.local_reads}</td>
|
||||
<td>{((status.local_reads / total_reads) * 100).toPrecision(3)}%</td>
|
||||
<td>{status.local_reads_per_sec.toPrecision(4)}/s</td>
|
||||
@@ -242,6 +226,7 @@ onDestroy(() => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Neighbour</td>
|
||||
<td>{status.neighbour_readers}</td>
|
||||
<td>{status.neighbour_reads}</td>
|
||||
<td>{((status.neighbour_reads / total_reads) * 100).toPrecision(3)}%</td>
|
||||
<td>{status.neighbour_reads_per_sec.toPrecision(4)}/s</td>
|
||||
@@ -251,6 +236,7 @@ onDestroy(() => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reed-solomon</td>
|
||||
<td>{status.remote_readers}</td>
|
||||
<td>{status.remote_reads}</td>
|
||||
<td>{((status.remote_reads / total_reads) * 100).toPrecision(3)}%</td>
|
||||
<td>{status.remote_reads_per_sec.toPrecision(4)}/s</td>
|
||||
@@ -258,6 +244,16 @@ onDestroy(() => {
|
||||
<td>{((status.remote_read_size / total_read_size) * 100).toPrecision(3)}%</td>
|
||||
<td>{formatDataVolume(status.remote_read_size_per_sec, 4)}/s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td>{status.local_readers+status.neighbour_readers+status.remote_readers}</td>
|
||||
<td>{status.local_reads+status.neighbour_reads+status.remote_reads}</td>
|
||||
<td></td>
|
||||
<td>{(status.local_reads_per_sec+status.neighbour_reads_per_sec+status.remote_reads_per_sec).toPrecision(4)}/s</td>
|
||||
<td>{formatDataVolume(status.local_read_size+status.neighbour_read_size+status.remote_read_size, 4)}</td>
|
||||
<td></td>
|
||||
<td>{formatDataVolume(status.local_read_size_per_sec+status.neighbour_read_size_per_sec+status.remote_read_size_per_sec, 4)}/s</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -282,12 +278,6 @@ onDestroy(() => {
|
||||
<td>{status.filesystem_watcher_listeners}</td>
|
||||
<td>{(status.filesystem_watcher_listeners / status.filesystem_watcher_threads).toPrecision(3)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Downloads (per IP)</td>
|
||||
<td>{status.download_clients}</td>
|
||||
<td>{status.download_connections}</td>
|
||||
<td>{(status.download_connections / status.download_clients).toPrecision(3)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { preventDefault, stopPropagation } from 'svelte/legacy';
|
||||
import { onMount } from "svelte";
|
||||
import { formatDate } from "util/Formatting";
|
||||
import Expandable from "util/Expandable.svelte";
|
||||
import { loading_finish } from "lib/Loading";
|
||||
import { loading_finish, loading_start } from "lib/Loading";
|
||||
|
||||
const abuse_types = [
|
||||
"copyright",
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
import { flip } from "svelte/animate";
|
||||
import { formatDataVolume } from "util/Formatting";
|
||||
import SortButton from "layout/SortButton.svelte";
|
||||
|
||||
let { peers = $bindable([]) } = $props();
|
||||
let update_peers = (peers) => {
|
||||
let {
|
||||
peers = $bindable([])
|
||||
}: {
|
||||
peers: {
|
||||
id: string
|
||||
ip: string
|
||||
port: number
|
||||
hostname: string
|
||||
role: string
|
||||
reachable: boolean
|
||||
unreachable_count: number
|
||||
latency: number
|
||||
last_seen: string
|
||||
free_space: number
|
||||
min_free_space: number
|
||||
load_1_min: number
|
||||
load_5_min: number
|
||||
load_15_min: number
|
||||
avg_network_tx: number
|
||||
avg_network_rx: number
|
||||
port_speed: number
|
||||
cache_threshold: number
|
||||
|
||||
// Our props
|
||||
avg_network_total?: number
|
||||
usage_percent?: number
|
||||
network_ratio?: number
|
||||
}[]
|
||||
} = $props();
|
||||
|
||||
$effect(() => {
|
||||
for (let peer of peers) {
|
||||
peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx
|
||||
peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100
|
||||
@@ -13,11 +41,11 @@ let update_peers = (peers) => {
|
||||
}
|
||||
|
||||
sort("")
|
||||
}
|
||||
})
|
||||
|
||||
let sort_field = $state("hostname")
|
||||
let asc = $state(true)
|
||||
let sort = (field) => {
|
||||
let sort = (field: string) => {
|
||||
if (field !== "" && field === sort_field) {
|
||||
asc = !asc
|
||||
}
|
||||
@@ -26,7 +54,7 @@ let sort = (field) => {
|
||||
}
|
||||
sort_field = field
|
||||
|
||||
console.log("sorting by", field, "asc", asc)
|
||||
console.log("sorting by", field, "asc", $state.snapshot(asc))
|
||||
peers.sort((a, b) => {
|
||||
if (typeof (a[field]) === "number") {
|
||||
// Sort ints from high to low
|
||||
@@ -46,9 +74,6 @@ let sort = (field) => {
|
||||
})
|
||||
peers = peers
|
||||
}
|
||||
run(() => {
|
||||
update_peers(peers)
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="table_scroll">
|
||||
@@ -84,7 +109,7 @@ run(() => {
|
||||
<td>{(peer.latency/1000).toFixed(3)}</td>
|
||||
<td>{formatDataVolume(peer.avg_network_tx, 3)}/s</td>
|
||||
<td>{formatDataVolume(peer.avg_network_rx, 3)}/s</td>
|
||||
<td>{peer.network_ratio.toFixed(2)}</td>
|
||||
<td>{peer.network_ratio === undefined ? "" : peer.network_ratio.toFixed(2)}</td>
|
||||
<td>{formatDataVolume(peer.avg_network_total, 3)}/s</td>
|
||||
<td>{Math.round(peer.usage_percent)}%</td>
|
||||
<td>{formatDataVolume(peer.cache_threshold, 3)}</td>
|
||||
|
||||
@@ -40,7 +40,7 @@ let chart_timespans = [
|
||||
]
|
||||
|
||||
let total_downloads = $state(0)
|
||||
let total_transfer = $state(0)
|
||||
let total_egress = $state(0)
|
||||
|
||||
let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
||||
if (chart === undefined) {
|
||||
@@ -106,10 +106,9 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
||||
});
|
||||
|
||||
total_downloads = 0
|
||||
total_transfer = 0
|
||||
total_egress = 0
|
||||
resp.downloads.amounts.forEach(val => total_downloads += val);
|
||||
resp.transfer_free.amounts.forEach((val) => total_transfer += val);
|
||||
resp.transfer_paid.amounts.forEach((val) => total_transfer += val);
|
||||
resp.egress.amounts.forEach((val) => total_egress += val);
|
||||
|
||||
c.data.labels = resp.downloads.timestamps
|
||||
c.data.datasets = [
|
||||
@@ -117,25 +116,17 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
|
||||
label: "Downloads",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("chart_1_color"),
|
||||
backgroundColor: color_by_name("chart_1_color"),
|
||||
borderColor: color_by_name("highlight_color"),
|
||||
backgroundColor: color_by_name("highlight_color"),
|
||||
data: resp.downloads.amounts,
|
||||
}, {
|
||||
label: "Free transfer",
|
||||
label: "Egress",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("chart_2_color"),
|
||||
backgroundColor: color_by_name("chart_2_color"),
|
||||
borderColor: color_by_name("danger_color"),
|
||||
backgroundColor: color_by_name("danger_color"),
|
||||
yAxisID: "y1",
|
||||
data: resp.transfer_free.amounts,
|
||||
}, {
|
||||
label: "Premium transfer",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("chart_3_color"),
|
||||
backgroundColor: color_by_name("chart_3_color"),
|
||||
yAxisID: "y1",
|
||||
data: resp.transfer_paid.amounts,
|
||||
data: resp.egress.amounts,
|
||||
},
|
||||
];
|
||||
chart.update()
|
||||
@@ -201,11 +192,11 @@ run(() => {
|
||||
<td>{formatThousands(total_downloads)} (unique, counted once per IP)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Transfer used</td>
|
||||
<td>Egress bandwidth</td>
|
||||
<td>
|
||||
{formatDataVolume(total_transfer, 4)}
|
||||
( {formatThousands(total_transfer)} B ),
|
||||
{(total_transfer/$nav.base.file_size).toFixed(1)}x file size
|
||||
{formatDataVolume(total_egress, 4)}
|
||||
( {formatThousands(total_egress)} B ),
|
||||
{(total_egress/$nav.base.file_size).toFixed(1)}x file size
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>SHA256 sum</td><td>{$nav.base.sha256_sum}</td></tr>
|
||||
|
||||
@@ -14,7 +14,7 @@ let {
|
||||
|
||||
let loading = $state(true)
|
||||
let downloads = $state(0)
|
||||
let transfer_used = $state(0)
|
||||
let egress_used = $state(0)
|
||||
let socket = null
|
||||
let error_msg = $state("")
|
||||
|
||||
@@ -48,6 +48,9 @@ const update_base = async () => {
|
||||
} else if (connected_to === nav.base.path) {
|
||||
return // If we're already connected to the same path, don't reconnect
|
||||
}
|
||||
|
||||
console.debug("Websocket connection path changed from", connected_to, "to", nav.base.path)
|
||||
|
||||
connected_to = nav.base.path
|
||||
|
||||
// If the socket is already active we need to close it
|
||||
@@ -68,7 +71,7 @@ const update_base = async () => {
|
||||
error_msg = ""
|
||||
loading = false
|
||||
downloads = j.downloads
|
||||
transfer_used = j.transfer_free + j.transfer_paid
|
||||
egress_used = j.egress
|
||||
}
|
||||
socket.onerror = err => {
|
||||
console.error("WS error", err)
|
||||
@@ -133,7 +136,7 @@ const toggle_expand_keyboard = (e: KeyboardEvent) => {
|
||||
<div class="group">
|
||||
<div class="label">Egress</div>
|
||||
<div class="stat">
|
||||
{loading ? "Loading..." : formatDataVolume(transfer_used, 3)}
|
||||
{loading ? "Loading..." : formatDataVolume(egress_used, 3)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -32,10 +32,3 @@ import Euro from "util/Euro.svelte";
|
||||
{:else}
|
||||
<LoginRegister/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.page_content {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,45 +22,10 @@ let upload_widget
|
||||
<div class="page_content">
|
||||
<section>
|
||||
<p>
|
||||
Pixeldrain offers services for efficiently moving and storing
|
||||
digital files on the internet.
|
||||
FNX.storage is a platform for cost-effective cloud storage and
|
||||
content delivery. We will store and serve your files at an extremely
|
||||
competitive rate.
|
||||
</p>
|
||||
<h2>What pixeldrain is good at</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Serving large files to millions of people worldwide
|
||||
</li>
|
||||
<li>
|
||||
Storing files for less money than all the competition
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Things we take very seriously</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Performance</b> - Slow software is a waste of time. We
|
||||
don't want to make you wait, so pixeldrain is completely
|
||||
tuned for maximum performance
|
||||
</li>
|
||||
<li>
|
||||
<b>Privacy</b> - There is too much tracking on the web
|
||||
nowadays. Pixeldrain goes in the other direction, this site
|
||||
does not contain any advertisements or third party tracking
|
||||
scripts
|
||||
</li>
|
||||
<li>
|
||||
Bullet lists
|
||||
</li>
|
||||
</ul>
|
||||
<Pricing/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<h1>Pricing</h1>
|
||||
<span>(Prepaid plan. For monthly subscriptions, look further below)</span>
|
||||
</header>
|
||||
<div class="page_content">
|
||||
<section>
|
||||
<div class="prices">
|
||||
<div>
|
||||
<div>Storage pricing</div>
|
||||
@@ -70,16 +35,25 @@ let upload_widget
|
||||
<div>Egress pricing</div>
|
||||
<div>€ 1 / TB</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Minimum fee *</div>
|
||||
<div>€1 / month</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="text-align: center;">
|
||||
* The minimum fee is only charged when usage is less than €1
|
||||
</p>
|
||||
<h2>What FNX is good at</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Serving large files to millions of people worldwide
|
||||
</li>
|
||||
<li>
|
||||
Storing files for less money than all the competition
|
||||
</li>
|
||||
</ul>
|
||||
<Pricing/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h2>What you get</h2>
|
||||
<header>
|
||||
<h1>Features</h1>
|
||||
</header>
|
||||
<div class="page_content">
|
||||
<section>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="bold">Unlimited</span> storage space
|
||||
@@ -136,17 +110,16 @@ let upload_widget
|
||||
</header>
|
||||
<GetStarted/>
|
||||
|
||||
<header id="pro">
|
||||
<h1>Subscription plans</h1>
|
||||
</header>
|
||||
<div class="page_content">
|
||||
<FeatureTable/>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<svelte:head>
|
||||
<style>
|
||||
body {
|
||||
background-image: url("/res/img/catspaw.webp");
|
||||
background-image: url("/res/img/northernlights.webp");
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
@@ -164,10 +137,9 @@ header {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
header > h1,
|
||||
header > span {
|
||||
header > h1 {
|
||||
color: #ffffff;
|
||||
text-shadow: 0 0 4px #000000;
|
||||
text-shadow: 0 0 6px #000000;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -209,17 +181,23 @@ header > span {
|
||||
flex: 1 0 200px;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--card_color);
|
||||
}
|
||||
.prices > div > div {
|
||||
flex: 1 1 auto;
|
||||
padding: 4px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.prices > div > div:nth-child(2) {
|
||||
background: var(--card_color);
|
||||
font-size: 1.3em;
|
||||
}
|
||||
@media(max-width: 1000px) {
|
||||
.prices > div {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
<script>
|
||||
import { run } from 'svelte/legacy';
|
||||
import { onMount } from "svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
import ProgressBar from "util/ProgressBar.svelte";
|
||||
|
||||
let pixeldrain_storage = $state(0)
|
||||
let pixeldrain_egress = $state(0)
|
||||
let pixeldrain_total = $state(0)
|
||||
let fnx_storage = $state(0)
|
||||
let fnx_egress = $state(0)
|
||||
let fnx_total = $state(0)
|
||||
let backblaze_storage = $state(0)
|
||||
let backblaze_egress = $state(0)
|
||||
let backblaze_api = $state(0)
|
||||
let backblaze_total = $state(0)
|
||||
let wasabi_storage = $state(0)
|
||||
let wasabi_total = $state(0)
|
||||
let price_amazon = 0
|
||||
let price_azure = 0
|
||||
let price_google = 0
|
||||
let price_max = $state(0)
|
||||
|
||||
let storage = $state(10) // TB
|
||||
let egress = $state(10) // TB
|
||||
let avg_file_size = $state(1000) // kB
|
||||
|
||||
run(() => {
|
||||
pixeldrain_storage = storage * 4
|
||||
pixeldrain_egress = egress * 1
|
||||
pixeldrain_total = pixeldrain_storage + pixeldrain_egress
|
||||
$effect(() => {
|
||||
// FNX has a minimum file size of 10kB. Calculate the number of files from
|
||||
// storage and avg file size, then calculate storage usage based on that.
|
||||
fnx_storage = Math.max(storage * 4, ((storage*10)/avg_file_size)*4)
|
||||
fnx_egress = egress * 1
|
||||
fnx_total = fnx_storage + fnx_egress
|
||||
|
||||
// Egress at Backblaze is free up to three times the amount of storage, then
|
||||
// it's $10/TB
|
||||
@@ -34,45 +31,53 @@ run(() => {
|
||||
backblaze_api = ((egress * 1e12) / (avg_file_size * 1e3)) * 0.0000004
|
||||
backblaze_total = backblaze_storage + backblaze_egress + backblaze_api
|
||||
|
||||
|
||||
// Wasabi does not have egress fees
|
||||
wasabi_storage = storage * 6.99
|
||||
wasabi_total = (egress > storage) ? 0 : wasabi_storage
|
||||
|
||||
// price_amazon = (storage * 26) + (egress * 90)
|
||||
// price_azure = (storage * 20) + (egress * 80)
|
||||
// price_google = (storage * 20) + (egress * 20)
|
||||
|
||||
price_max = Math.max(pixeldrain_total, backblaze_total, wasabi_total, price_amazon, price_azure, price_google)
|
||||
price_max = Math.max(fnx_total, backblaze_total, wasabi_total)
|
||||
});
|
||||
|
||||
onMount(() => {})
|
||||
</script>
|
||||
|
||||
<h2>Price calculator</h2>
|
||||
<div class="inputs">
|
||||
<div>
|
||||
<div>Storage</div>
|
||||
<div><input type="number" bind:value={storage}> TB / month</div>
|
||||
<div class="usage_label">Storage</div>
|
||||
<div class="usage_input">
|
||||
<input type="number" bind:value={storage}/>
|
||||
<div>TB / month</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Egress</div>
|
||||
<div><input type="number" bind:value={egress}> TB / month</div>
|
||||
<div class="usage_label">Egress</div>
|
||||
<div class="usage_input">
|
||||
<input type="number" bind:value={egress}/>
|
||||
<div>TB / month</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Average file size</div>
|
||||
<div><input type="number" bind:value={avg_file_size}> kB</div>
|
||||
<div class="usage_label">Average file size</div>
|
||||
<div class="usage_input">
|
||||
<input type="number" bind:value={avg_file_size}/>
|
||||
<div>kB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bars">
|
||||
<div>
|
||||
<div>
|
||||
Pixeldrain - <Euro amount={pixeldrain_total*1e6}/> / month<br/>
|
||||
<Euro amount={pixeldrain_storage*1e6}/> storage,
|
||||
<Euro amount={pixeldrain_egress*1e6}/> egress
|
||||
<ProgressBar used={pixeldrain_total} total={price_max}/>
|
||||
FNX.storage - <Euro amount={fnx_total*1e6}/> / month<br/>
|
||||
<Euro amount={fnx_storage*1e6}/> storage,
|
||||
<Euro amount={fnx_egress*1e6}/> egress
|
||||
<ProgressBar used={fnx_total} total={price_max}/>
|
||||
</div>
|
||||
{#if avg_file_size < 10}
|
||||
<div>
|
||||
FNX counts a minimum file size of 10 kB. Files smaller than that
|
||||
are rounded up to 10 kB.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
@@ -97,42 +102,20 @@ onMount(() => {})
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- <div>
|
||||
<div>
|
||||
Amazon S3: <Euro symbol="$" amount={price_amazon*1e6}/> / month
|
||||
<ProgressBar used={price_amazon} total={price_max}/>
|
||||
</div>
|
||||
<div>
|
||||
Amazon's pricing is too complicated to accurately represent here.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
Microsoft Azure: <Euro symbol="$" amount={price_azure*1e6}/> / month
|
||||
<ProgressBar used={price_azure} total={price_max}/>
|
||||
</div>
|
||||
<div>
|
||||
Azure's pricing is too complicated to accurately represent here.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
Google: <Euro symbol="$" amount={price_google*1e6}/> / month
|
||||
<ProgressBar used={price_google} total={price_max}/>
|
||||
</div>
|
||||
<div>
|
||||
Google's pricing is too complicated to accurately represent here.
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Note that while pixeldrain might not seem to be the cheapest option in some
|
||||
Note that while FNX.storage might not seem to be the cheapest option in some
|
||||
cases, most cloud providers have extra hidden costs for API calls and
|
||||
region-specific prices. This makes it very hard to accurately compare the
|
||||
pricing of these platforms. Pixeldrain includes no hidden costs, I only
|
||||
pricing of these platforms. FNX.storage includes no hidden costs, I only
|
||||
charge for storage and egress.
|
||||
</p>
|
||||
<p>
|
||||
Large cloud providers like Amazon, Microsoft and Google are excluded from
|
||||
this calculation because their pricing is too complex accurately compare
|
||||
them. Just assume that FNX wil be cheaper.
|
||||
</p>
|
||||
|
||||
<style>
|
||||
.inputs {
|
||||
@@ -142,14 +125,40 @@ onMount(() => {})
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@media(max-width: 700px) {
|
||||
.inputs {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.inputs > div {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--card_color);
|
||||
}
|
||||
.usage_label {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
font-size: 1.3em;
|
||||
padding: 4px;
|
||||
}
|
||||
.usage_input {
|
||||
flex: 1 1 auto;
|
||||
background: var(--card_color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.usage_input > input {
|
||||
flex: 1 1 auto;
|
||||
background: var(--card_color);
|
||||
}
|
||||
.usage_input > div {
|
||||
flex: 0 0 auto;
|
||||
background: var(--card_color);
|
||||
}
|
||||
.bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -242,8 +242,7 @@ export type TimeSeries = {
|
||||
}
|
||||
export type NodeTimeSeries = {
|
||||
downloads: TimeSeries,
|
||||
transfer_free: TimeSeries,
|
||||
transfer_paid: TimeSeries,
|
||||
egress: TimeSeries,
|
||||
}
|
||||
|
||||
export const fs_timeseries = async (path: string, start: Date, end: Date, interval = 60) => {
|
||||
|
||||
@@ -52,7 +52,7 @@ let load_transfer_used = () => {
|
||||
start.setDate(start.getDate() - 30)
|
||||
|
||||
fetch(
|
||||
window.api_endpoint + "/user/time_series/transfer_paid" +
|
||||
window.api_endpoint + "/user/time_series/egress" +
|
||||
"?start=" + start.toISOString() +
|
||||
"&end=" + today.toISOString() +
|
||||
"&interval=60"
|
||||
|
||||
@@ -10,8 +10,8 @@ let { card_size = 1 }: {
|
||||
} = $props();
|
||||
|
||||
let chart_height = $derived((80 + (card_size * 60)) + "px")
|
||||
let graph_views_downloads = $state(null)
|
||||
let graph_bandwidth = $state(null)
|
||||
let graph_downloads = $state(null)
|
||||
let graph_egress = $state(null)
|
||||
|
||||
let load_graphs = async (minutes, interval) => {
|
||||
let end = new Date()
|
||||
@@ -19,34 +19,26 @@ let load_graphs = async (minutes, interval) => {
|
||||
start.setMinutes(start.getMinutes() - minutes)
|
||||
|
||||
try {
|
||||
let views_req = get_graph_data("views", start, end, interval);
|
||||
let downloads_req = get_graph_data("downloads", start, end, interval);
|
||||
let bandwidth_req = get_graph_data("bandwidth", start, end, interval);
|
||||
let transfer_paid_req = get_graph_data("transfer_paid", start, end, interval);
|
||||
let views = await views_req
|
||||
let egress_req = get_graph_data("egress", start, end, interval);
|
||||
let downloads = await downloads_req
|
||||
let bandwidth = await bandwidth_req
|
||||
let transfer_paid = await transfer_paid_req
|
||||
let egress = await egress_req
|
||||
|
||||
graph_views_downloads.data().labels = views.timestamps;
|
||||
graph_views_downloads.data().datasets[0].data = views.amounts
|
||||
graph_views_downloads.data().datasets[1].data = downloads.amounts
|
||||
graph_bandwidth.data().labels = bandwidth.timestamps;
|
||||
graph_bandwidth.data().datasets[0].data = bandwidth.amounts
|
||||
graph_bandwidth.data().datasets[1].data = transfer_paid.amounts
|
||||
graph_downloads.data().labels = downloads.timestamps;
|
||||
graph_downloads.data().datasets[0].data = downloads.amounts
|
||||
graph_egress.data().labels = egress.timestamps;
|
||||
graph_egress.data().datasets[0].data = egress.amounts
|
||||
|
||||
graph_views_downloads.update()
|
||||
graph_bandwidth.update()
|
||||
graph_downloads.update()
|
||||
graph_egress.update()
|
||||
} catch (err) {
|
||||
console.error("Failed to update graphs", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let total_views = $state(0)
|
||||
let total_downloads = $state(0)
|
||||
let total_bandwidth = $state(0)
|
||||
let total_transfer_paid = $state(0)
|
||||
let total_egress = $state(0)
|
||||
|
||||
let get_graph_data = async (stat: string, start: Date, end: Date, interval: number) => {
|
||||
let resp = await fetch(
|
||||
@@ -74,15 +66,10 @@ let get_graph_data = async (stat: string, start: Date, end: Date, interval: numb
|
||||
// Add up the total amount and save it in the correct place
|
||||
let total = resp_json.amounts.reduce((acc, cur) => { return acc + cur }, 0)
|
||||
|
||||
if (stat == "views") {
|
||||
total_views = total;
|
||||
} else if (stat == "downloads") {
|
||||
total_downloads = total;
|
||||
graph_views_downloads.update()
|
||||
} else if (stat == "bandwidth") {
|
||||
total_bandwidth = total;
|
||||
} else if (stat == "transfer_paid") {
|
||||
total_transfer_paid = total;
|
||||
if (stat == "downloads") {
|
||||
total_downloads = total
|
||||
} else if (stat == "egress") {
|
||||
total_egress = total
|
||||
}
|
||||
|
||||
return resp_json
|
||||
@@ -95,37 +82,23 @@ let update_graphs = (minutes, interval) => {
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
graph_views_downloads.data().datasets = [
|
||||
{
|
||||
label: "Views",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("highlight_color"),
|
||||
backgroundColor: color_by_name("highlight_color"),
|
||||
},
|
||||
graph_downloads.data().datasets = [
|
||||
{
|
||||
label: "Downloads",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("danger_color"),
|
||||
backgroundColor: color_by_name("danger_color"),
|
||||
borderColor: color_by_name("highlight_color"),
|
||||
backgroundColor: color_by_name("highlight_color"),
|
||||
},
|
||||
];
|
||||
graph_bandwidth.data().datasets = [
|
||||
graph_egress.data().datasets = [
|
||||
{
|
||||
label: "Free transfer",
|
||||
label: "Egress",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("highlight_color"),
|
||||
backgroundColor: color_by_name("highlight_color"),
|
||||
},
|
||||
{
|
||||
label: "Premium transfer",
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
borderColor: color_by_name("danger_color"),
|
||||
backgroundColor: color_by_name("danger_color"),
|
||||
}
|
||||
];
|
||||
|
||||
update_graphs(43200, 1440);
|
||||
@@ -171,15 +144,13 @@ onMount(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Chart bind:this={graph_bandwidth} data_type="bytes" height={chart_height} ticks={false}/>
|
||||
<Chart bind:this={graph_egress} data_type="bytes" height={chart_height} ticks={false}/>
|
||||
<div class="center">
|
||||
{formatDataVolume(total_bandwidth, 3)} free downloads and
|
||||
{formatDataVolume(total_transfer_paid, 3)} paid downloads
|
||||
{formatDataVolume(total_egress, 3)} egress used
|
||||
</div>
|
||||
|
||||
<Chart bind:this={graph_views_downloads} data_type="number" height={chart_height} ticks={false}/>
|
||||
<Chart bind:this={graph_downloads} data_type="number" height={chart_height} ticks={false}/>
|
||||
<div class="center">
|
||||
{formatThousands(total_views)} views and
|
||||
{formatThousands(total_downloads)} downloads
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { get_endpoint, get_user, type User } from "lib/PixeldrainAPI";
|
||||
import { onMount } from "svelte";
|
||||
import HotlinkProgressBar from "user_home/HotlinkProgressBar.svelte";
|
||||
import StorageProgressBar from "user_home/StorageProgressBar.svelte";
|
||||
import ProgressBar from "util/ProgressBar.svelte";
|
||||
|
||||
let transfer_cap = $state(0)
|
||||
let transfer_used = $state(0)
|
||||
let storage_limit = window.user.subscription.storage_space
|
||||
let fs_storage_limit = window.user.subscription.filesystem_storage_limit
|
||||
let storage_limit = $state(0)
|
||||
|
||||
let load_direct_bw = () => {
|
||||
let today = new Date()
|
||||
let start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
|
||||
fetch(
|
||||
window.api_endpoint + "/user/time_series/transfer_paid" +
|
||||
get_endpoint() + "/user/time_series/egress" +
|
||||
"?start=" + start.toISOString() +
|
||||
"&end=" + today.toISOString() +
|
||||
"&interval=60"
|
||||
@@ -29,34 +29,31 @@ let load_direct_bw = () => {
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (window.user.monthly_transfer_cap > 0) {
|
||||
transfer_cap = window.user.monthly_transfer_cap
|
||||
} else if (window.user.subscription.monthly_transfer_cap > 0) {
|
||||
transfer_cap = window.user.subscription.monthly_transfer_cap
|
||||
let user: User = $state()
|
||||
onMount(async () => {
|
||||
user = await get_user()
|
||||
|
||||
if (user.monthly_transfer_cap > 0) {
|
||||
transfer_cap = user.monthly_transfer_cap
|
||||
} else if (user.subscription.monthly_transfer_cap > 0) {
|
||||
transfer_cap = user.subscription.monthly_transfer_cap
|
||||
} else {
|
||||
transfer_cap = -1
|
||||
}
|
||||
|
||||
storage_limit = user.subscription.storage_space
|
||||
|
||||
load_direct_bw()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
Total storage space used:
|
||||
<StorageProgressBar used={window.user.storage_space_used} total={storage_limit}/>
|
||||
<br/>
|
||||
|
||||
{#if window.user.subscription.filesystem_access === true}
|
||||
Filesystem storage space used:
|
||||
<StorageProgressBar
|
||||
used={window.user.filesystem_storage_used}
|
||||
total={fs_storage_limit > 0 ? fs_storage_limit : storage_limit}
|
||||
disable_warnings
|
||||
/>
|
||||
{#if user !== undefined}
|
||||
Storage space used:
|
||||
<StorageProgressBar used={user.storage_space_used} total={storage_limit}/>
|
||||
<br/>
|
||||
{/if}
|
||||
|
||||
Premium data transfer:
|
||||
(<a href="/user/sharing/bandwidth">set custom limit</a>)
|
||||
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar>
|
||||
Egress used (30 days):
|
||||
(<a href="/user/sharing/bandwidth">set custom limit</a>)
|
||||
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar>
|
||||
{/if}
|
||||
|
||||
@@ -13,6 +13,10 @@ export const formatThousands = (amt: number) => {
|
||||
}
|
||||
|
||||
export const formatDataVolume = (amt: number, precision: number) => {
|
||||
if (amt === undefined) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (precision < 3) { precision = 3; }
|
||||
if (amt >= 1e18 - 1e15) {
|
||||
return (amt / 1e18).toPrecision(precision) + " EB";
|
||||
|
||||
@@ -50,11 +50,6 @@ const get_page = () => {
|
||||
|
||||
title = current_subpage === null ? current_page.title : current_subpage.title
|
||||
window.document.title = title+" / FNX"
|
||||
|
||||
console.debug("Page", current_page)
|
||||
console.debug("Subpage", current_subpage)
|
||||
|
||||
pages = pages
|
||||
}
|
||||
|
||||
let current_page: Tab = $state(null)
|
||||
|
||||
@@ -99,7 +99,6 @@ const load_page = (pathname: string, history: boolean): boolean => {
|
||||
}
|
||||
|
||||
window.document.title = current_page.title+" / FNX"
|
||||
console.debug("Page", current_page)
|
||||
|
||||
if(history) {
|
||||
window.history.pushState({}, window.document.title, pathname)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ComponentType } from "svelte";
|
||||
import type { Component } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type Tab = {
|
||||
path: string,
|
||||
prefix?: string,
|
||||
title: string,
|
||||
component?: ComponentType,
|
||||
component?: Component,
|
||||
footer?: boolean,
|
||||
login?: boolean,
|
||||
};
|
||||
|
||||
67
svelte/vite.config.ts
Normal file
67
svelte/vite.config.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import { sveltePreprocess } from 'svelte-preprocess';
|
||||
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
|
||||
const production = process.env.NODE_ENV === "production"
|
||||
console.log("Production mode:", production)
|
||||
|
||||
const builddir = "../res/static/svelte"
|
||||
const name = "wrap"
|
||||
|
||||
export default defineConfig({
|
||||
mode: production ? "production" : "development",
|
||||
build: {
|
||||
outDir: builddir,
|
||||
emptyOutDir: true,
|
||||
minify: production,
|
||||
lib: {
|
||||
entry: "src/wrap.js",
|
||||
name: "fnx_web",
|
||||
fileName: name,
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: {},
|
||||
emitCss: false,
|
||||
}),
|
||||
|
||||
|
||||
babel({
|
||||
extensions: [".js", ".ts", ".svelte"],
|
||||
babelHelpers: "bundled",
|
||||
}),
|
||||
|
||||
// If you have external dependencies installed from npm, you'll most
|
||||
// likely need these plugins. In some cases you'll need additional
|
||||
// configuration - consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
exportConditions: ['svelte'],
|
||||
modulePaths: [process.cwd() + "/src", process.cwd() + "/node_modules"],
|
||||
extensions: [".svelte", ".mjs", ".js", ".json", ".mts", ".ts"],
|
||||
}),
|
||||
commonjs(),
|
||||
typescript(),
|
||||
|
||||
// Watch the `public` directory and refresh the browser on changes when
|
||||
// not in production
|
||||
!production && livereload({
|
||||
watch: `${builddir}/${name}.*`,
|
||||
port: 5000,
|
||||
}),
|
||||
|
||||
// If we're building for production (npm run build instead of npm run
|
||||
// dev), minify
|
||||
production && terser(),
|
||||
]
|
||||
});
|
||||
Reference in New Issue
Block a user