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); color: var(--input_text);
text-decoration: none; text-decoration: none;
background: var(--input_hover_background); background: var(--input_hover_background);
box-shadow: 0px 0px 0px 1px var(--highlight_color);
} }
button:active, button:active,

View File

@@ -28,7 +28,7 @@
window.server_hostname = "{{.Hostname}}"; window.server_hostname = "{{.Hostname}}";
</script> </script>
<script defer src='/res/svelte/wrap.js?v{{cacheID}}'></script> <script defer src='/res/svelte/wrap.js?v{{cacheID}}' type="module"></script>
</head> </head>
<body></body> <body></body>
</html> </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", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "rollup -c", "rbuild": "rollup -c",
"dev": "rollup -c -w" "rdev": "rollup -c -w",
"vbuild": "vite build",
"vdev": "NODE_ENV=development vite build --watch"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",
@@ -15,18 +17,21 @@
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6", "@rollup/plugin-typescript": "^11.1.6",
"@types/jsmediatags": "^3.9.6", "@types/jsmediatags": "^3.9.6",
"@types/node": "^24.10.0",
"rollup": "^4.24.4", "rollup": "^4.24.4",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-svelte": "^7.2.2", "rollup-plugin-svelte": "^7.2.2",
"svelte": "^5.0.0" "svelte": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"behave-js": "^1.5.0", "behave-js": "^1.5.0",
"chart.js": "^4.4.6", "chart.js": "^4.4.6",
"country-data-list": "^1.4.0", "country-data-list": "^1.4.0",
"pure-color": "^1.3.0", "pure-color": "^1.3.0",
"rollup-plugin-includepaths": "^0.2.4", "rollup-plugin-includepaths": "^0.2.4",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1" "tslib": "^2.8.1",
"vite": "^7.1.12"
} }
} }

View File

@@ -1,22 +1,21 @@
<script> <script lang="ts">
import { onDestroy, onMount } from "svelte"; import { onMount } from "svelte";
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting"; import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting";
import Chart from "util/Chart.svelte"; import Chart from "util/Chart.svelte";
import { color_by_name } from "util/Util"; import { color_by_name } from "util/Util";
import ServerDiagnostics from "./ServerDiagnostics.svelte"; import ServerDiagnostics from "./ServerDiagnostics.svelte";
import PeerTable from "./PeerTable.svelte"; import PeerTable from "./PeerTable.svelte";
import { get_endpoint } from "lib/PixeldrainAPI";
let graphViews = $state() let graphEgress: Chart = $state()
let graphBandwidth = $state() let graphDownloads: Chart = $state()
let graphTimeout = null let graphTimeout: NodeJS.Timeout = null
let start_time = $state("") let start_time = $state("")
let end_time = $state("") let end_time = $state("")
let total_bandwidth = $state(0) let total_egress = $state(0)
let total_bandwidth_paid = $state(0)
let total_views = $state(0)
let total_downloads = $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 (graphTimeout !== null) { clearTimeout(graphTimeout) }
if (live) { if (live) {
graphTimeout = setTimeout(() => { loadGraph(minutes, interval, true) }, 10000) graphTimeout = setTimeout(() => { loadGraph(minutes, interval, true) }, 10000)
@@ -27,7 +26,7 @@ const loadGraph = (minutes, interval, live) => {
start.setMinutes(start.getMinutes() - minutes) start.setMinutes(start.getMinutes() - minutes)
fetch( fetch(
window.api_endpoint + "/admin/files/timeseries" + get_endpoint() + "/admin/files/timeseries" +
"?start=" + start.toISOString() + "?start=" + start.toISOString() +
"&end=" + today.toISOString() + "&end=" + today.toISOString() +
"&interval=" + interval "&interval=" + interval
@@ -35,37 +34,34 @@ const loadGraph = (minutes, interval, live) => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); } if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json(); return resp.json();
}).then(resp => { }).then(resp => {
resp.views.timestamps.forEach((val, idx) => { resp.egress.timestamps.forEach((val, idx) => {
let date = new Date(val); 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.getMonth() + 1)).slice(-2);
dateStr += "-" + ("00" + date.getDate()).slice(-2); dateStr += "-" + ("00" + date.getDate()).slice(-2);
dateStr += " " + ("00" + date.getHours()).slice(-2); dateStr += " " + ("00" + date.getHours()).slice(-2);
dateStr += ":" + ("00" + date.getMinutes()).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; graphEgress.data().labels = resp.egress.timestamps;
graphViews.data().datasets[0].data = resp.views.amounts; graphEgress.data().datasets[0].data = resp.egress.amounts;
graphViews.data().datasets[1].data = resp.downloads.amounts; graphDownloads.data().labels = resp.downloads.timestamps;
graphBandwidth.data().labels = resp.views.timestamps; graphDownloads.data().datasets[0].data = resp.downloads.amounts
graphBandwidth.data().datasets[0].data = resp.bandwidth.amounts graphEgress.update()
graphBandwidth.data().datasets[1].data = resp.bandwidth_paid.amounts graphDownloads.update()
graphViews.update()
graphBandwidth.update()
start_time = resp.views.timestamps[0] start_time = resp.egress.timestamps[0]
end_time = resp.views.timestamps.slice(-1)[0]; end_time = resp.egress.timestamps.slice(-1)[0];
total_bandwidth = resp.bandwidth.amounts.reduce((acc, val) => acc + val) total_egress = resp.egress.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)
total_downloads = resp.downloads.amounts.reduce((acc, val) => acc + val) total_downloads = resp.downloads.amounts.reduce((acc, val) => acc + val)
}) })
} }
// Load performance statistics // Load performance statistics
let lastOrder = $state(); let lastOrder: string = $state();
let status = $state({ let status = $state({
pid: 0,
cpu_profile_running_since: "", cpu_profile_running_since: "",
db_latency: 0, db_latency: 0,
db_time: "", db_time: "",
@@ -74,33 +70,32 @@ let status = $state({
local_read_size_per_sec: 0, local_read_size_per_sec: 0,
local_reads: 0, local_reads: 0,
local_reads_per_sec: 0, local_reads_per_sec: 0,
local_readers: 0,
neighbour_read_size: 0, neighbour_read_size: 0,
neighbour_read_size_per_sec: 0, neighbour_read_size_per_sec: 0,
neighbour_reads: 0, neighbour_reads: 0,
neighbour_reads_per_sec: 0, neighbour_reads_per_sec: 0,
peers: [], neighbour_readers: 0,
query_statistics: [],
remote_read_size: 0, remote_read_size: 0,
remote_read_size_per_sec: 0, remote_read_size_per_sec: 0,
remote_reads: 0, remote_reads: 0,
remote_reads_per_sec: 0, remote_reads_per_sec: 0,
remote_readers: 0,
peers: [],
query_statistics: [],
running_since: "", running_since: "",
stats_watcher_listeners: 0, stats_watcher_listeners: 0,
stats_watcher_threads: 0, stats_watcher_threads: 0,
filesystem_watcher_listeners: 0, filesystem_watcher_listeners: 0,
filesystem_watcher_threads: 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_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) 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 lastOrder = order
fetch(window.api_endpoint + "/status").then( fetch(get_endpoint() + "/status").then(
resp => resp.json() resp => resp.json()
).then(resp => { ).then(resp => {
// Sort all queries by the selected sort column // Sort all queries by the selected sort column
@@ -126,37 +121,25 @@ function getStats(order) {
let statsInterval = null let statsInterval = null
onMount(() => { onMount(() => {
// Prepare chart datasets // Prepare chart datasets
graphBandwidth.data().datasets = [ graphEgress.data().datasets = [
{ {
label: "Bandwidth (free)", label: "Egress",
data: [],
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
borderColor: color_by_name("highlight_color"), borderColor: color_by_name("highlight_color"),
backgroundColor: 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, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
borderColor: color_by_name("highlight_color"), borderColor: color_by_name("highlight_color"),
backgroundColor: 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); loadGraph(10080, 10, true);
@@ -164,13 +147,14 @@ onMount(() => {
statsInterval = setInterval(() => { statsInterval = setInterval(() => {
getStats(lastOrder) getStats(lastOrder)
}, 10000) }, 10000)
})
onDestroy(() => { return () => {
if (graphTimeout !== null) { if (graphTimeout !== null) {
clearTimeout(graphTimeout) clearTimeout(graphTimeout)
} }
if (statsInterval !== null) { if (statsInterval !== null) {
clearInterval(statsInterval) clearInterval(statsInterval)
}
} }
}) })
</script> </script>
@@ -188,14 +172,12 @@ onDestroy(() => {
<button onclick={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button> <button onclick={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button>
<button onclick={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button> <button onclick={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button>
</div> </div>
<Chart bind:this={graphBandwidth} data_type="bytes" /> <Chart bind:this={graphEgress} data_type="bytes" />
<Chart bind:this={graphViews} data_type="number" /> <Chart bind:this={graphDownloads} data_type="number" />
<div class="highlight_border"> <div class="highlight_border">
Total usage from {start_time} to {end_time}<br/> Total usage from {start_time} to {end_time}<br/>
{formatDataVolume(total_bandwidth, 3)} bandwidth, {formatDataVolume(total_egress, 3)} egress,
{formatDataVolume(total_bandwidth_paid, 3)} paid bandwidth, {formatThousands(total_downloads)} downloads
{formatThousands(total_views, 3)} views and
{formatThousands(total_downloads, 3)} downloads
</div> </div>
<br/> <br/>
@@ -222,6 +204,7 @@ onDestroy(() => {
<thead> <thead>
<tr> <tr>
<td>Source</td> <td>Source</td>
<td>Current</td>
<td>Reads</td> <td>Reads</td>
<td>Reads %</td> <td>Reads %</td>
<td>Reads / s</td> <td>Reads / s</td>
@@ -233,6 +216,7 @@ onDestroy(() => {
<tbody> <tbody>
<tr> <tr>
<td>Local cache</td> <td>Local cache</td>
<td>{status.local_readers}</td>
<td>{status.local_reads}</td> <td>{status.local_reads}</td>
<td>{((status.local_reads / total_reads) * 100).toPrecision(3)}%</td> <td>{((status.local_reads / total_reads) * 100).toPrecision(3)}%</td>
<td>{status.local_reads_per_sec.toPrecision(4)}/s</td> <td>{status.local_reads_per_sec.toPrecision(4)}/s</td>
@@ -242,6 +226,7 @@ onDestroy(() => {
</tr> </tr>
<tr> <tr>
<td>Neighbour</td> <td>Neighbour</td>
<td>{status.neighbour_readers}</td>
<td>{status.neighbour_reads}</td> <td>{status.neighbour_reads}</td>
<td>{((status.neighbour_reads / total_reads) * 100).toPrecision(3)}%</td> <td>{((status.neighbour_reads / total_reads) * 100).toPrecision(3)}%</td>
<td>{status.neighbour_reads_per_sec.toPrecision(4)}/s</td> <td>{status.neighbour_reads_per_sec.toPrecision(4)}/s</td>
@@ -251,6 +236,7 @@ onDestroy(() => {
</tr> </tr>
<tr> <tr>
<td>Reed-solomon</td> <td>Reed-solomon</td>
<td>{status.remote_readers}</td>
<td>{status.remote_reads}</td> <td>{status.remote_reads}</td>
<td>{((status.remote_reads / total_reads) * 100).toPrecision(3)}%</td> <td>{((status.remote_reads / total_reads) * 100).toPrecision(3)}%</td>
<td>{status.remote_reads_per_sec.toPrecision(4)}/s</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>{((status.remote_read_size / total_read_size) * 100).toPrecision(3)}%</td>
<td>{formatDataVolume(status.remote_read_size_per_sec, 4)}/s</td> <td>{formatDataVolume(status.remote_read_size_per_sec, 4)}/s</td>
</tr> </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> </tbody>
</table> </table>
</div> </div>
@@ -282,12 +278,6 @@ onDestroy(() => {
<td>{status.filesystem_watcher_listeners}</td> <td>{status.filesystem_watcher_listeners}</td>
<td>{(status.filesystem_watcher_listeners / status.filesystem_watcher_threads).toPrecision(3)}</td> <td>{(status.filesystem_watcher_listeners / status.filesystem_watcher_threads).toPrecision(3)}</td>
</tr> </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> </tbody>
</table> </table>

View File

@@ -3,7 +3,7 @@ import { preventDefault, stopPropagation } from 'svelte/legacy';
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "util/Formatting"; import { formatDate } from "util/Formatting";
import Expandable from "util/Expandable.svelte"; import Expandable from "util/Expandable.svelte";
import { loading_finish } from "lib/Loading"; import { loading_finish, loading_start } from "lib/Loading";
const abuse_types = [ const abuse_types = [
"copyright", "copyright",

View File

@@ -1,11 +1,39 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import { flip } from "svelte/animate"; import { flip } from "svelte/animate";
import { formatDataVolume } from "util/Formatting"; import { formatDataVolume } from "util/Formatting";
import SortButton from "layout/SortButton.svelte"; import SortButton from "layout/SortButton.svelte";
let { peers = $bindable([]) } = $props(); let {
let update_peers = (peers) => { 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) { for (let peer of peers) {
peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx peer.avg_network_total = peer.avg_network_tx + peer.avg_network_rx
peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100 peer.usage_percent = (peer.avg_network_tx / peer.port_speed) * 100
@@ -13,11 +41,11 @@ let update_peers = (peers) => {
} }
sort("") sort("")
} })
let sort_field = $state("hostname") let sort_field = $state("hostname")
let asc = $state(true) let asc = $state(true)
let sort = (field) => { let sort = (field: string) => {
if (field !== "" && field === sort_field) { if (field !== "" && field === sort_field) {
asc = !asc asc = !asc
} }
@@ -26,7 +54,7 @@ let sort = (field) => {
} }
sort_field = field sort_field = field
console.log("sorting by", field, "asc", asc) console.log("sorting by", field, "asc", $state.snapshot(asc))
peers.sort((a, b) => { peers.sort((a, b) => {
if (typeof (a[field]) === "number") { if (typeof (a[field]) === "number") {
// Sort ints from high to low // Sort ints from high to low
@@ -46,9 +74,6 @@ let sort = (field) => {
}) })
peers = peers peers = peers
} }
run(() => {
update_peers(peers)
});
</script> </script>
<div class="table_scroll"> <div class="table_scroll">
@@ -84,7 +109,7 @@ run(() => {
<td>{(peer.latency/1000).toFixed(3)}</td> <td>{(peer.latency/1000).toFixed(3)}</td>
<td>{formatDataVolume(peer.avg_network_tx, 3)}/s</td> <td>{formatDataVolume(peer.avg_network_tx, 3)}/s</td>
<td>{formatDataVolume(peer.avg_network_rx, 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>{formatDataVolume(peer.avg_network_total, 3)}/s</td>
<td>{Math.round(peer.usage_percent)}%</td> <td>{Math.round(peer.usage_percent)}%</td>
<td>{formatDataVolume(peer.cache_threshold, 3)}</td> <td>{formatDataVolume(peer.cache_threshold, 3)}</td>

View File

@@ -40,7 +40,7 @@ let chart_timespans = [
] ]
let total_downloads = $state(0) 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) => { let update_chart = async (base: FSNode, timespan: number, interval: number) => {
if (chart === undefined) { if (chart === undefined) {
@@ -106,10 +106,9 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
}); });
total_downloads = 0 total_downloads = 0
total_transfer = 0 total_egress = 0
resp.downloads.amounts.forEach(val => total_downloads += val); resp.downloads.amounts.forEach(val => total_downloads += val);
resp.transfer_free.amounts.forEach((val) => total_transfer += val); resp.egress.amounts.forEach((val) => total_egress += val);
resp.transfer_paid.amounts.forEach((val) => total_transfer += val);
c.data.labels = resp.downloads.timestamps c.data.labels = resp.downloads.timestamps
c.data.datasets = [ c.data.datasets = [
@@ -117,25 +116,17 @@ let update_chart = async (base: FSNode, timespan: number, interval: number) => {
label: "Downloads", label: "Downloads",
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
borderColor: color_by_name("chart_1_color"), borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("chart_1_color"), backgroundColor: color_by_name("highlight_color"),
data: resp.downloads.amounts, data: resp.downloads.amounts,
}, { }, {
label: "Free transfer", label: "Egress",
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
borderColor: color_by_name("chart_2_color"), borderColor: color_by_name("danger_color"),
backgroundColor: color_by_name("chart_2_color"), backgroundColor: color_by_name("danger_color"),
yAxisID: "y1", yAxisID: "y1",
data: resp.transfer_free.amounts, data: resp.egress.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,
}, },
]; ];
chart.update() chart.update()
@@ -201,11 +192,11 @@ run(() => {
<td>{formatThousands(total_downloads)} (unique, counted once per IP)</td> <td>{formatThousands(total_downloads)} (unique, counted once per IP)</td>
</tr> </tr>
<tr> <tr>
<td>Transfer used</td> <td>Egress bandwidth</td>
<td> <td>
{formatDataVolume(total_transfer, 4)} {formatDataVolume(total_egress, 4)}
( {formatThousands(total_transfer)} B ), ( {formatThousands(total_egress)} B ),
{(total_transfer/$nav.base.file_size).toFixed(1)}x file size {(total_egress/$nav.base.file_size).toFixed(1)}x file size
</td> </td>
</tr> </tr>
<tr><td>SHA256 sum</td><td>{$nav.base.sha256_sum}</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 loading = $state(true)
let downloads = $state(0) let downloads = $state(0)
let transfer_used = $state(0) let egress_used = $state(0)
let socket = null let socket = null
let error_msg = $state("") let error_msg = $state("")
@@ -48,6 +48,9 @@ const update_base = async () => {
} else if (connected_to === nav.base.path) { } else if (connected_to === nav.base.path) {
return // If we're already connected to the same path, don't reconnect 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 connected_to = nav.base.path
// If the socket is already active we need to close it // If the socket is already active we need to close it
@@ -68,7 +71,7 @@ const update_base = async () => {
error_msg = "" error_msg = ""
loading = false loading = false
downloads = j.downloads downloads = j.downloads
transfer_used = j.transfer_free + j.transfer_paid egress_used = j.egress
} }
socket.onerror = err => { socket.onerror = err => {
console.error("WS error", err) console.error("WS error", err)
@@ -133,7 +136,7 @@ const toggle_expand_keyboard = (e: KeyboardEvent) => {
<div class="group"> <div class="group">
<div class="label">Egress</div> <div class="label">Egress</div>
<div class="stat"> <div class="stat">
{loading ? "Loading..." : formatDataVolume(transfer_used, 3)} {loading ? "Loading..." : formatDataVolume(egress_used, 3)}
</div> </div>
</div> </div>
{/if} {/if}

View File

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

View File

@@ -22,45 +22,10 @@ let upload_widget
<div class="page_content"> <div class="page_content">
<section> <section>
<p> <p>
Pixeldrain offers services for efficiently moving and storing FNX.storage is a platform for cost-effective cloud storage and
digital files on the internet. content delivery. We will store and serve your files at an extremely
competitive rate.
</p> </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 class="prices">
<div> <div>
<div>Storage pricing</div> <div>Storage pricing</div>
@@ -70,16 +35,25 @@ let upload_widget
<div>Egress pricing</div> <div>Egress pricing</div>
<div>€ 1 / TB</div> <div>€ 1 / TB</div>
</div> </div>
<div>
<div>Minimum fee *</div>
<div>€1 / month</div>
</div>
</div> </div>
<p style="text-align: center;"> <h2>What FNX is good at</h2>
* The minimum fee is only charged when usage is less than €1 <ul>
</p> <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> <ul>
<li> <li>
<span class="bold">Unlimited</span> storage space <span class="bold">Unlimited</span> storage space
@@ -136,17 +110,16 @@ let upload_widget
</header> </header>
<GetStarted/> <GetStarted/>
<header id="pro"> <br/>
<h1>Subscription plans</h1> <br/>
</header> <br/>
<div class="page_content"> <br/>
<FeatureTable/> <br/>
</div>
<svelte:head> <svelte:head>
<style> <style>
body { body {
background-image: url("/res/img/catspaw.webp"); background-image: url("/res/img/northernlights.webp");
background-repeat: no-repeat; background-repeat: no-repeat;
background-attachment: fixed; background-attachment: fixed;
background-position: center; background-position: center;
@@ -164,10 +137,9 @@ header {
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
} }
header > h1, header > h1 {
header > span {
color: #ffffff; color: #ffffff;
text-shadow: 0 0 4px #000000; text-shadow: 0 0 6px #000000;
margin-top: 30px; margin-top: 30px;
margin-bottom: 30px; margin-bottom: 30px;
} }
@@ -209,17 +181,23 @@ header > span {
flex: 1 0 200px; flex: 1 0 200px;
min-width: 200px; min-width: 200px;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
text-align: center; text-align: center;
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
border: 2px solid var(--card_color); border: 2px solid var(--card_color);
} }
.prices > div > div { .prices > div > div {
flex: 1 1 auto;
padding: 4px; padding: 4px;
font-size: 1.3em;
} }
.prices > div > div:nth-child(2) { .prices > div > div:nth-child(2) {
background: var(--card_color); background: var(--card_color);
font-size: 1.3em; }
@media(max-width: 1000px) {
.prices > div {
flex-direction: column;
}
} }
</style> </style>

View File

@@ -1,31 +1,28 @@
<script> <script>
import { run } from 'svelte/legacy';
import { onMount } from "svelte";
import Euro from "util/Euro.svelte"; import Euro from "util/Euro.svelte";
import ProgressBar from "util/ProgressBar.svelte"; import ProgressBar from "util/ProgressBar.svelte";
let pixeldrain_storage = $state(0) let fnx_storage = $state(0)
let pixeldrain_egress = $state(0) let fnx_egress = $state(0)
let pixeldrain_total = $state(0) let fnx_total = $state(0)
let backblaze_storage = $state(0) let backblaze_storage = $state(0)
let backblaze_egress = $state(0) let backblaze_egress = $state(0)
let backblaze_api = $state(0) let backblaze_api = $state(0)
let backblaze_total = $state(0) let backblaze_total = $state(0)
let wasabi_storage = $state(0) let wasabi_storage = $state(0)
let wasabi_total = $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 price_max = $state(0)
let storage = $state(10) // TB let storage = $state(10) // TB
let egress = $state(10) // TB let egress = $state(10) // TB
let avg_file_size = $state(1000) // kB let avg_file_size = $state(1000) // kB
run(() => { $effect(() => {
pixeldrain_storage = storage * 4 // FNX has a minimum file size of 10kB. Calculate the number of files from
pixeldrain_egress = egress * 1 // storage and avg file size, then calculate storage usage based on that.
pixeldrain_total = pixeldrain_storage + pixeldrain_egress 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 // Egress at Backblaze is free up to three times the amount of storage, then
// it's $10/TB // it's $10/TB
@@ -34,45 +31,53 @@ run(() => {
backblaze_api = ((egress * 1e12) / (avg_file_size * 1e3)) * 0.0000004 backblaze_api = ((egress * 1e12) / (avg_file_size * 1e3)) * 0.0000004
backblaze_total = backblaze_storage + backblaze_egress + backblaze_api backblaze_total = backblaze_storage + backblaze_egress + backblaze_api
// Wasabi does not have egress fees // Wasabi does not have egress fees
wasabi_storage = storage * 6.99 wasabi_storage = storage * 6.99
wasabi_total = (egress > storage) ? 0 : wasabi_storage wasabi_total = (egress > storage) ? 0 : wasabi_storage
// price_amazon = (storage * 26) + (egress * 90) price_max = Math.max(fnx_total, backblaze_total, wasabi_total)
// 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)
}); });
onMount(() => {})
</script> </script>
<h2>Price calculator</h2> <h2>Price calculator</h2>
<div class="inputs"> <div class="inputs">
<div> <div>
<div>Storage</div> <div class="usage_label">Storage</div>
<div><input type="number" bind:value={storage}> TB / month</div> <div class="usage_input">
<input type="number" bind:value={storage}/>
<div>TB / month</div>
</div>
</div> </div>
<div> <div>
<div>Egress</div> <div class="usage_label">Egress</div>
<div><input type="number" bind:value={egress}> TB / month</div> <div class="usage_input">
<input type="number" bind:value={egress}/>
<div>TB / month</div>
</div>
</div> </div>
<div> <div>
<div>Average file size</div> <div class="usage_label">Average file size</div>
<div><input type="number" bind:value={avg_file_size}> kB</div> <div class="usage_input">
<input type="number" bind:value={avg_file_size}/>
<div>kB</div>
</div>
</div> </div>
</div> </div>
<div class="bars"> <div class="bars">
<div> <div>
<div> <div>
Pixeldrain - <Euro amount={pixeldrain_total*1e6}/> / month<br/> FNX.storage - <Euro amount={fnx_total*1e6}/> / month<br/>
<Euro amount={pixeldrain_storage*1e6}/> storage, <Euro amount={fnx_storage*1e6}/> storage,
<Euro amount={pixeldrain_egress*1e6}/> egress <Euro amount={fnx_egress*1e6}/> egress
<ProgressBar used={pixeldrain_total} total={price_max}/> <ProgressBar used={fnx_total} total={price_max}/>
</div> </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> <div>
<div> <div>
@@ -97,42 +102,20 @@ onMount(() => {})
</div> </div>
{/if} {/if}
</div> </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> </div>
<p> <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 cases, most cloud providers have extra hidden costs for API calls and
region-specific prices. This makes it very hard to accurately compare the 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. charge for storage and egress.
</p> </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> <style>
.inputs { .inputs {
@@ -142,14 +125,40 @@ onMount(() => {})
gap: 10px; gap: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
@media(max-width: 700px) {
.inputs {
flex-direction: column;
}
}
.inputs > div { .inputs > div {
flex: 1 1 auto; flex: 1 1 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
border: 2px solid var(--card_color); 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 { .bars {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -242,8 +242,7 @@ export type TimeSeries = {
} }
export type NodeTimeSeries = { export type NodeTimeSeries = {
downloads: TimeSeries, downloads: TimeSeries,
transfer_free: TimeSeries, egress: TimeSeries,
transfer_paid: TimeSeries,
} }
export const fs_timeseries = async (path: string, start: Date, end: Date, interval = 60) => { 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) start.setDate(start.getDate() - 30)
fetch( fetch(
window.api_endpoint + "/user/time_series/transfer_paid" + window.api_endpoint + "/user/time_series/egress" +
"?start=" + start.toISOString() + "?start=" + start.toISOString() +
"&end=" + today.toISOString() + "&end=" + today.toISOString() +
"&interval=60" "&interval=60"

View File

@@ -10,8 +10,8 @@ let { card_size = 1 }: {
} = $props(); } = $props();
let chart_height = $derived((80 + (card_size * 60)) + "px") let chart_height = $derived((80 + (card_size * 60)) + "px")
let graph_views_downloads = $state(null) let graph_downloads = $state(null)
let graph_bandwidth = $state(null) let graph_egress = $state(null)
let load_graphs = async (minutes, interval) => { let load_graphs = async (minutes, interval) => {
let end = new Date() let end = new Date()
@@ -19,34 +19,26 @@ let load_graphs = async (minutes, interval) => {
start.setMinutes(start.getMinutes() - minutes) start.setMinutes(start.getMinutes() - minutes)
try { try {
let views_req = get_graph_data("views", start, end, interval);
let downloads_req = get_graph_data("downloads", start, end, interval); let downloads_req = get_graph_data("downloads", start, end, interval);
let bandwidth_req = get_graph_data("bandwidth", start, end, interval); let egress_req = get_graph_data("egress", start, end, interval);
let transfer_paid_req = get_graph_data("transfer_paid", start, end, interval);
let views = await views_req
let downloads = await downloads_req let downloads = await downloads_req
let bandwidth = await bandwidth_req let egress = await egress_req
let transfer_paid = await transfer_paid_req
graph_views_downloads.data().labels = views.timestamps; graph_downloads.data().labels = downloads.timestamps;
graph_views_downloads.data().datasets[0].data = views.amounts graph_downloads.data().datasets[0].data = downloads.amounts
graph_views_downloads.data().datasets[1].data = downloads.amounts graph_egress.data().labels = egress.timestamps;
graph_bandwidth.data().labels = bandwidth.timestamps; graph_egress.data().datasets[0].data = egress.amounts
graph_bandwidth.data().datasets[0].data = bandwidth.amounts
graph_bandwidth.data().datasets[1].data = transfer_paid.amounts
graph_views_downloads.update() graph_downloads.update()
graph_bandwidth.update() graph_egress.update()
} catch (err) { } catch (err) {
console.error("Failed to update graphs", err) console.error("Failed to update graphs", err)
return return
} }
} }
let total_views = $state(0)
let total_downloads = $state(0) let total_downloads = $state(0)
let total_bandwidth = $state(0) let total_egress = $state(0)
let total_transfer_paid = $state(0)
let get_graph_data = async (stat: string, start: Date, end: Date, interval: number) => { let get_graph_data = async (stat: string, start: Date, end: Date, interval: number) => {
let resp = await fetch( 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 // Add up the total amount and save it in the correct place
let total = resp_json.amounts.reduce((acc, cur) => { return acc + cur }, 0) let total = resp_json.amounts.reduce((acc, cur) => { return acc + cur }, 0)
if (stat == "views") { if (stat == "downloads") {
total_views = total; total_downloads = total
} else if (stat == "downloads") { } else if (stat == "egress") {
total_downloads = total; total_egress = total
graph_views_downloads.update()
} else if (stat == "bandwidth") {
total_bandwidth = total;
} else if (stat == "transfer_paid") {
total_transfer_paid = total;
} }
return resp_json return resp_json
@@ -95,37 +82,23 @@ let update_graphs = (minutes, interval) => {
} }
onMount(() => { onMount(() => {
graph_views_downloads.data().datasets = [ graph_downloads.data().datasets = [
{
label: "Views",
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("highlight_color"),
},
{ {
label: "Downloads", label: "Downloads",
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
borderColor: color_by_name("danger_color"), borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("danger_color"), backgroundColor: color_by_name("highlight_color"),
}, },
]; ];
graph_bandwidth.data().datasets = [ graph_egress.data().datasets = [
{ {
label: "Free transfer", label: "Egress",
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
borderColor: color_by_name("highlight_color"), borderColor: color_by_name("highlight_color"),
backgroundColor: 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); update_graphs(43200, 1440);
@@ -171,15 +144,13 @@ onMount(() => {
</button> </button>
</div> </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"> <div class="center">
{formatDataVolume(total_bandwidth, 3)} free downloads and {formatDataVolume(total_egress, 3)} egress used
{formatDataVolume(total_transfer_paid, 3)} paid downloads
</div> </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"> <div class="center">
{formatThousands(total_views)} views and
{formatThousands(total_downloads)} downloads {formatThousands(total_downloads)} downloads
</div> </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 { onMount } from "svelte";
import HotlinkProgressBar from "user_home/HotlinkProgressBar.svelte"; import HotlinkProgressBar from "user_home/HotlinkProgressBar.svelte";
import StorageProgressBar from "user_home/StorageProgressBar.svelte"; import StorageProgressBar from "user_home/StorageProgressBar.svelte";
import ProgressBar from "util/ProgressBar.svelte";
let transfer_cap = $state(0) let transfer_cap = $state(0)
let transfer_used = $state(0) let transfer_used = $state(0)
let storage_limit = window.user.subscription.storage_space let storage_limit = $state(0)
let fs_storage_limit = window.user.subscription.filesystem_storage_limit
let load_direct_bw = () => { let load_direct_bw = () => {
let today = new Date() let today = new Date()
let start = new Date() let start = new Date()
start.setDate(start.getDate() - 30) start.setDate(start.getDate() - 30)
fetch( fetch(
window.api_endpoint + "/user/time_series/transfer_paid" + get_endpoint() + "/user/time_series/egress" +
"?start=" + start.toISOString() + "?start=" + start.toISOString() +
"&end=" + today.toISOString() + "&end=" + today.toISOString() +
"&interval=60" "&interval=60"
@@ -29,34 +29,31 @@ let load_direct_bw = () => {
}) })
} }
onMount(() => { let user: User = $state()
if (window.user.monthly_transfer_cap > 0) { onMount(async () => {
transfer_cap = window.user.monthly_transfer_cap user = await get_user()
} else if (window.user.subscription.monthly_transfer_cap > 0) {
transfer_cap = window.user.subscription.monthly_transfer_cap 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 { } else {
transfer_cap = -1 transfer_cap = -1
} }
storage_limit = user.subscription.storage_space
load_direct_bw() load_direct_bw()
}) })
</script> </script>
Total storage space used: {#if user !== undefined}
<StorageProgressBar used={window.user.storage_space_used} total={storage_limit}/> Storage space used:
<br/> <StorageProgressBar used={user.storage_space_used} total={storage_limit}/>
{#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
/>
<br/> <br/>
{/if}
Premium data transfer: Egress used (30 days):
(<a href="/user/sharing/bandwidth">set custom limit</a>) (<a href="/user/sharing/bandwidth">set custom limit</a>)
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar> <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) => { export const formatDataVolume = (amt: number, precision: number) => {
if (amt === undefined) {
return ""
}
if (precision < 3) { precision = 3; } if (precision < 3) { precision = 3; }
if (amt >= 1e18 - 1e15) { if (amt >= 1e18 - 1e15) {
return (amt / 1e18).toPrecision(precision) + " EB"; 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 title = current_subpage === null ? current_page.title : current_subpage.title
window.document.title = title+" / FNX" window.document.title = title+" / FNX"
console.debug("Page", current_page)
console.debug("Subpage", current_subpage)
pages = pages
} }
let current_page: Tab = $state(null) 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" window.document.title = current_page.title+" / FNX"
console.debug("Page", current_page)
if(history) { if(history) {
window.history.pushState({}, window.document.title, pathname) 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"; import { writable } from "svelte/store";
export type Tab = { export type Tab = {
path: string, path: string,
prefix?: string, prefix?: string,
title: string, title: string,
component?: ComponentType, component?: Component,
footer?: boolean, footer?: boolean,
login?: 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(),
]
});