Fix stats reporting. Add Vite compatibility

This commit is contained in:
2025-11-04 16:16:50 +01:00
parent aa29de9029
commit e54dc2dbd7
21 changed files with 1060 additions and 361 deletions

View File

@@ -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,

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -32,10 +32,3 @@ import Euro from "util/Euro.svelte";
{:else}
<LoginRegister/>
{/if}
<style>
.page_content {
margin-top: 16px;
margin-bottom: 16px;
}
</style>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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"

View File

@@ -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>

View File

@@ -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}

View File

@@ -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";

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
View 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(),
]
});