Convert admin panel to svelte
This commit is contained in:
135
svelte/src/admin_panel/AbuseReport.svelte
Normal file
135
svelte/src/admin_panel/AbuseReport.svelte
Normal file
@@ -0,0 +1,135 @@
|
||||
<script>
|
||||
import { formatDate } from "../util/Formatting.svelte";
|
||||
import Expandable from "../util/Expandable.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
export let report
|
||||
let expandable
|
||||
let preview = false
|
||||
|
||||
let set_status = async (action, report_type) => {
|
||||
const form = new FormData()
|
||||
form.append("action", action)
|
||||
if (action === "grant") {
|
||||
form.append("type", report_type)
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/admin/abuse_report/"+report.id,
|
||||
{ method: "POST", body: form }
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(resp.text())
|
||||
}
|
||||
|
||||
dispatch("refresh");
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Expandable bind:this={expandable} expanded={report.status === "pending" && report.reports.length > 1}>
|
||||
<div slot="header" class="header" on:click={expandable.toggle}>
|
||||
<div class="icon_cell">
|
||||
<img class="file_icon" src={"/api/file/"+report.file.id+"/thumbnail"} alt="File thumbnail"/>
|
||||
</div>
|
||||
|
||||
<div class="title">{report.file.name}</div>
|
||||
<div class="stats">Type<br/>
|
||||
{report.file.abuse_type === "" ? report.type : report.file.abuse_type}
|
||||
</div>
|
||||
{#if report.status !== "pending"}
|
||||
<div class="stats">Status<br/>{report.status}</div>
|
||||
{/if}
|
||||
<div class="stats">R<br/>{report.reports.length}</div>
|
||||
<div class="stats">V<br/>{report.file.views}</div>
|
||||
<div class="stats">DL<br/>{Math.round(report.file.bandwidth_used / report.file.size)}</div>
|
||||
</div>
|
||||
<div class="details">
|
||||
<div class="toolbar">
|
||||
<div style="flex: 1 1 auto">
|
||||
<a class="button" target="_blank" href={"/u/"+report.file.id}>
|
||||
<i class="icon">open_in_new</i> Open file
|
||||
</a>
|
||||
<button class:button_highlight={preview} on:click={() => {preview = !preview}}>
|
||||
<i class="icon">visibility</i> Preview
|
||||
</button>
|
||||
<button class="button_highlight" on:click={() => {set_status("grant", report.type)}}>
|
||||
<i class="icon">done</i> Block ({report.type})
|
||||
</button>
|
||||
<button class="button_red" on:click={() => {set_status("reject", "")}}>
|
||||
<i class="icon">delete</i> Ignore
|
||||
</button>
|
||||
</div>
|
||||
<div style="flex: 0 1 auto">
|
||||
<button on:click={() => {set_status("grant", "terrorism")}}>terrorism</button>
|
||||
<button on:click={() => {set_status("grant", "gore")}}>gore</button>
|
||||
<button on:click={() => {set_status("grant", "child_abuse")}}>child_abuse</button>
|
||||
<button on:click={() => {set_status("grant", "malware")}}>malware</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
{#if preview}
|
||||
<br/>
|
||||
<iframe
|
||||
title="File preview"
|
||||
src="/u/{report.file.id}?embed"
|
||||
style="border: none; width: 100%; height: 400px; border-radius: 6px;"
|
||||
></iframe>
|
||||
{/if}
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Time</td>
|
||||
<td>IP</td>
|
||||
<td>Type</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
{#each report.reports as user_report}
|
||||
<tr>
|
||||
<td>{formatDate(user_report.time, true, true, false)}</td>
|
||||
<td>{user_report.ip_address}</td>
|
||||
<td>{user_report.type}</td>
|
||||
<td>{user_report.status}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
</Expandable>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.icon_cell {
|
||||
flex: 0 0 auto;
|
||||
line-height: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.title {
|
||||
flex: 1 1 auto;
|
||||
align-self: center;
|
||||
}
|
||||
.stats {
|
||||
flex: 0 0 auto;
|
||||
padding: 3px 4px;
|
||||
border-left: 1px solid var(--layer_3_color_border);
|
||||
text-align: center;
|
||||
}
|
||||
.details {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.file_icon {
|
||||
width:48px;
|
||||
height:48px;
|
||||
}
|
||||
</style>
|
||||
180
svelte/src/admin_panel/AbuseReporters.svelte
Normal file
180
svelte/src/admin_panel/AbuseReporters.svelte
Normal file
@@ -0,0 +1,180 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { formatDate, formatDuration } from "../util/Formatting.svelte";
|
||||
import Spinner from "../util/Spinner.svelte";
|
||||
|
||||
let loading = true
|
||||
let reporters = []
|
||||
|
||||
let creating = false
|
||||
let new_reporter_email
|
||||
let new_reporter_name
|
||||
let new_reporter_type = "individual"
|
||||
|
||||
const get_reporters = async () => {
|
||||
loading = true;
|
||||
try {
|
||||
const resp = await fetch(window.api_endpoint+"/admin/abuse_reporter");
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(resp.text());
|
||||
}
|
||||
reporters = await resp.json();
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const create_reporter = async () => {
|
||||
if (!new_reporter_email.value) {
|
||||
alert("Please enter an e-mail address!")
|
||||
return
|
||||
} else if (!new_reporter_name.value) {
|
||||
alert("Please enter a name!")
|
||||
return
|
||||
} else if (!new_reporter_type) {
|
||||
alert("Please enter a type!")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const form = new FormData()
|
||||
form.append("email", new_reporter_email.value)
|
||||
form.append("name", new_reporter_name.value)
|
||||
form.append("type", new_reporter_type)
|
||||
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/admin/abuse_reporter",
|
||||
{ method: "POST", body: form }
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Failed to add abuse reporter! "+err)
|
||||
}
|
||||
|
||||
creating = false
|
||||
get_reporters();
|
||||
}
|
||||
|
||||
const delete_reporter = async (email) => {
|
||||
if (!confirm("Delete this reporter address?\n\n"+email)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/admin/abuse_reporter/"+encodeURI(email),
|
||||
{ method: "DELETE" }
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Failed to delete abuse reporter! "+err)
|
||||
}
|
||||
|
||||
get_reporters();
|
||||
}
|
||||
|
||||
onMount(get_reporters);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if loading}
|
||||
<div class="spinner_container">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="limit_width">
|
||||
<div class="toolbar" style="text-align: left;">
|
||||
<div class="toolbar_spacer"></div>
|
||||
<button class:button_highlight={creating} on:click={() => {creating = !creating}}>
|
||||
<i class="icon">create</i> Add abuse reporter
|
||||
</button>
|
||||
</div>
|
||||
{#if creating}
|
||||
<div class="highlight_light">
|
||||
<form on:submit|preventDefault={create_reporter}>
|
||||
<table class="form">
|
||||
<tr>
|
||||
<td>E-mail address</td>
|
||||
<td><input type="text" bind:this={new_reporter_email}/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td><input type="text" bind:this={new_reporter_name} value="Anonymous tip"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>
|
||||
<input id="reporter_type_individual" name="reporter_type" type="radio" bind:group={new_reporter_type} value="individual" />
|
||||
<label for="reporter_type_individual">Individual</label>
|
||||
<br/>
|
||||
<input id="reporter_type_org" name="reporter_type" type="radio" bind:group={new_reporter_type} value="org" />
|
||||
<label for="reporter_type_org">Organisation</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button class="button_highlight" type="submit" style="float: right;">
|
||||
<i class="icon">save</i> Save
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<table style="text-align: left;">
|
||||
<tr>
|
||||
<td>E-mail</td>
|
||||
<td>Name</td>
|
||||
<td>Blocked</td>
|
||||
<td>Type</td>
|
||||
<td>Last used</td>
|
||||
<td>Created</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{#each reporters as reporter}
|
||||
<tr>
|
||||
<td>{reporter.email}</td>
|
||||
<td>{reporter.name}</td>
|
||||
<td>{reporter.files_blocked}</td>
|
||||
<td>{reporter.type}</td>
|
||||
<td>{formatDate(reporter.last_used, true, true, false)}</td>
|
||||
<td>{formatDate(reporter.created, false, false, false)}</td>
|
||||
<td>
|
||||
<button on:click|preventDefault={() => {delete_reporter(reporter.email)}} class="button button_red">
|
||||
<i class="icon">delete</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spinner_container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
.toolbar > * { flex: 0 0 auto; }
|
||||
.toolbar_spacer { flex: 1 1 auto; }
|
||||
</style>
|
||||
145
svelte/src/admin_panel/AbuseReports.svelte
Normal file
145
svelte/src/admin_panel/AbuseReports.svelte
Normal file
@@ -0,0 +1,145 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import Spinner from "../util/Spinner.svelte";
|
||||
import AbuseReport from "./AbuseReport.svelte";
|
||||
|
||||
let loading = true
|
||||
let reports_pending = []
|
||||
let reports_processed = []
|
||||
|
||||
let startPicker
|
||||
let endPicker
|
||||
|
||||
const get_reports = async () => {
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+
|
||||
"/admin/abuse_report"+
|
||||
"?start="+(new Date(startPicker.value)).toISOString()+
|
||||
"&end="+(new Date(endPicker.value)).toISOString()
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(resp.text());
|
||||
}
|
||||
let reports = await resp.json();
|
||||
|
||||
// Sort files by number of reports. If the number of reports is equal we
|
||||
// sort by number of views. If the number of views is equal we sort by
|
||||
// date of the first report received
|
||||
reports.sort((a, b) => {
|
||||
if (a.reports.length > b.reports.length) {
|
||||
return -1
|
||||
} else if (a.reports.length < b.reports.length) {
|
||||
return 1
|
||||
} else if (a.file.views > b.file.views) {
|
||||
return -1
|
||||
} else if (a.file.views < b.file.views) {
|
||||
return 1
|
||||
} else if (a.first_report_time > b.first_report_time) {
|
||||
return -1
|
||||
} else if (a.first_report_time < b.first_report_time) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
reports_pending = []
|
||||
reports_processed = []
|
||||
|
||||
// Sort individual reports of each file from old to new, then separate
|
||||
// pending reports and processed reports
|
||||
reports.forEach(v => {
|
||||
v.reports.sort((a, b) => {
|
||||
if (a.time > b.time) {
|
||||
return 1
|
||||
} else if (a.time < b.time) {
|
||||
return -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
if (v.status === "pending") {
|
||||
reports_pending.push(v)
|
||||
} else {
|
||||
reports_processed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
// Update svelte views
|
||||
reports_processed = reports_processed
|
||||
reports_pending = reports_pending
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
let start = new Date()
|
||||
start.setDate(start.getDate() - 14)
|
||||
let end = new Date()
|
||||
|
||||
startPicker.valueAsNumber = start.getTime()
|
||||
endPicker.valueAsNumber = end.getTime()
|
||||
|
||||
get_reports()
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if loading}
|
||||
<div class="spinner_container">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="limit_width">
|
||||
<div class="toolbar" style="text-align: left;">
|
||||
<div class="toolbar_spacer"></div>
|
||||
<div>Start:</div>
|
||||
<input type="date" bind:this={startPicker}/>
|
||||
<div>End:</div>
|
||||
<input type="date" bind:this={endPicker}/>
|
||||
<button on:click={get_reports}>Go</button>
|
||||
</div>
|
||||
|
||||
<h2>Pending</h2>
|
||||
{#each reports_pending as report}
|
||||
{#if report.status === "pending"}
|
||||
<AbuseReport report={report} on:refresh={get_reports}/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<h2>Resolved</h2>
|
||||
{#each reports_processed as report}
|
||||
{#if report.status !== "pending"}
|
||||
<AbuseReport report={report} on:refresh={get_reports}/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spinner_container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
.toolbar > * { flex: 0 0 auto; }
|
||||
.toolbar_spacer { flex: 1 1 auto; }
|
||||
|
||||
</style>
|
||||
75
svelte/src/admin_panel/AdminPanel.svelte
Normal file
75
svelte/src/admin_panel/AdminPanel.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<script>
|
||||
import AbuseReporters from "./AbuseReporters.svelte"
|
||||
import AbuseReports from "./AbuseReports.svelte"
|
||||
import IpBans from "./IPBans.svelte"
|
||||
import Home from "./Home.svelte"
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let page = ""
|
||||
|
||||
let navigate = (path, title) => {
|
||||
page = path
|
||||
window.document.title = title+" ~ pixeldrain"
|
||||
window.history.pushState(
|
||||
{}, window.document.title, "/admin/"+path
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1)
|
||||
if (newpage === "admin") {
|
||||
newpage = ""
|
||||
}
|
||||
page = newpage
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<a class="button"
|
||||
href="/admin"
|
||||
class:button_highlight={page === ""}
|
||||
on:click|preventDefault={() => {navigate("", "Status")}}>
|
||||
<i class="icon">home</i>
|
||||
Status
|
||||
</a>
|
||||
<a class="button" href="/admin/abuse">
|
||||
<i class="icon">block</i>
|
||||
Block files
|
||||
</a>
|
||||
<a class="button"
|
||||
href="/admin/abuse_reports"
|
||||
class:button_highlight={page === "abuse_reports"}
|
||||
on:click|preventDefault={() => {navigate("abuse_reports", "Abuse reports")}}>
|
||||
<i class="icon">flag</i>
|
||||
User abuse reports
|
||||
</a>
|
||||
<a class="button"
|
||||
href="/admin/abuse_reporters"
|
||||
class:button_highlight={page === "abuse_reporters"}
|
||||
on:click|preventDefault={() => {navigate("abuse_reporters", "Abuse reporters")}}>
|
||||
<i class="icon">report</i>
|
||||
E-mail abuse reporters
|
||||
</a>
|
||||
<a class="button"
|
||||
href="/admin/ip_bans"
|
||||
class:button_highlight={page === "ip_bans"}
|
||||
on:click|preventDefault={() => {navigate("ip_bans", "IP bans")}}>
|
||||
<i class="icon">remove_circle</i>
|
||||
IP bans
|
||||
</a>
|
||||
<a class="button" href="/admin/globals">
|
||||
<i class="icon">edit</i>
|
||||
Update global settings
|
||||
</a>
|
||||
<hr/>
|
||||
|
||||
{#if page === ""}
|
||||
<Home></Home>
|
||||
{:else if page === "abuse_reports"}
|
||||
<AbuseReports></AbuseReports>
|
||||
{:else if page === "abuse_reporters"}
|
||||
<AbuseReporters></AbuseReporters>
|
||||
{:else if page === "ip_bans"}
|
||||
<IpBans></IpBans>
|
||||
{/if}
|
||||
</div>
|
||||
272
svelte/src/admin_panel/Home.svelte
Normal file
272
svelte/src/admin_panel/Home.svelte
Normal file
@@ -0,0 +1,272 @@
|
||||
<script>
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "../util/Formatting.svelte";
|
||||
import Chart from "../util/Chart.svelte";
|
||||
|
||||
let graphViews
|
||||
let graphBandwidth
|
||||
let graphTimeout = null
|
||||
|
||||
let start_time = ""
|
||||
let end_time = ""
|
||||
let total_bandwidth = 0
|
||||
let total_views = 0
|
||||
const loadGraph = (minutes, interval, live) => {
|
||||
if (graphTimeout !== null) { clearTimeout(graphTimeout) }
|
||||
if (live) {
|
||||
graphTimeout = setTimeout(() => { loadGraph(minutes, interval, true) }, 10000)
|
||||
}
|
||||
|
||||
let today = new Date()
|
||||
let start = new Date()
|
||||
start.setMinutes(start.getMinutes() - minutes)
|
||||
|
||||
fetch(
|
||||
window.api_endpoint + "/admin/files/timeseries" +
|
||||
"?start=" + start.toISOString() +
|
||||
"&end=" + today.toISOString() +
|
||||
"&interval=" + interval
|
||||
).then(resp => {
|
||||
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
|
||||
return resp.json();
|
||||
}).then(resp => {
|
||||
resp.views.timestamps.forEach((val, idx) => {
|
||||
let date = new Date(val);
|
||||
let 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
|
||||
});
|
||||
graphViews.chart().data.labels = resp.views.timestamps;
|
||||
graphViews.chart().data.datasets[0].data = resp.views.amounts;
|
||||
graphBandwidth.chart().data.labels = resp.views.timestamps;
|
||||
graphBandwidth.chart().data.datasets[0].data = resp.bandwidth.amounts;
|
||||
graphViews.update()
|
||||
graphBandwidth.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_views = resp.views.amounts.reduce((acc, val) => acc + val)
|
||||
})
|
||||
}
|
||||
|
||||
// Load performance statistics
|
||||
|
||||
let lastOrder;
|
||||
let status = {
|
||||
db_latency: 0,
|
||||
db_time: "",
|
||||
local_read_size: 0,
|
||||
local_read_size_per_sec: 0,
|
||||
local_reads: 0,
|
||||
local_reads_per_sec: 0,
|
||||
peers: [],
|
||||
query_statistics: [],
|
||||
remote_read_size: 0,
|
||||
remote_read_size_per_sec: 0,
|
||||
remote_reads: 0,
|
||||
remote_reads_per_sec: 0,
|
||||
running_since: "",
|
||||
stats_watcher_listeners: 0,
|
||||
stats_watcher_threads: 0
|
||||
}
|
||||
|
||||
function getStats(order) {
|
||||
lastOrder = order
|
||||
|
||||
fetch(window.api_endpoint + "/status").then(
|
||||
resp => resp.json()
|
||||
).then(resp => {
|
||||
// Sort all queries by the selected sort column
|
||||
resp.query_statistics.sort((a, b) => {
|
||||
if (typeof (a[order]) === "number") {
|
||||
// Sort ints from high to low
|
||||
return b[order] - a[order]
|
||||
} else {
|
||||
// Sort strings alphabetically
|
||||
return a[order].localeCompare(b[order])
|
||||
}
|
||||
})
|
||||
|
||||
// Sort individual query callers by frequency
|
||||
resp.query_statistics.forEach((v) => {
|
||||
v.callers.sort((a, b) => b.count - a.count)
|
||||
})
|
||||
|
||||
status = resp
|
||||
})
|
||||
}
|
||||
|
||||
let statsInterval = null
|
||||
onMount(() => {
|
||||
loadGraph(10080, 10, false);
|
||||
getStats("calls")
|
||||
statsInterval = setInterval(() => {
|
||||
getStats(lastOrder)
|
||||
}, 10000)
|
||||
})
|
||||
onDestroy(() => {
|
||||
if (graphTimeout !== null) {
|
||||
clearTimeout(graphTimeout)
|
||||
}
|
||||
if (statsInterval !== null) {
|
||||
clearInterval(statsInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="limit_width">
|
||||
<h3>Bandwidth and views</h3>
|
||||
</div>
|
||||
<div class="highlight_dark" style="margin-bottom: 6px;">
|
||||
<button on:click={() => { loadGraph(1440, 1, true) }}>Day</button>
|
||||
<button on:click={() => { loadGraph(10080, 10, false) }}>Week</button>
|
||||
<button on:click={() => { loadGraph(20160, 60, false) }}>Two Weeks</button>
|
||||
<button on:click={() => { loadGraph(43200, 60, false) }}>Month</button>
|
||||
<button on:click={() => { loadGraph(131400, 1440, false) }}>Quarter</button>
|
||||
<button on:click={() => { loadGraph(262800, 1440, false) }}>Half-year</button>
|
||||
<button on:click={() => { loadGraph(525600, 1440, false) }}>Year</button>
|
||||
<button on:click={() => { loadGraph(1051200, 1440, false) }}>Two Years</button>
|
||||
</div>
|
||||
<Chart bind:this={graphBandwidth} dataType="bytes" label="Bandwidth" />
|
||||
<hr/>
|
||||
<Chart bind:this={graphViews} dataType="number" label="Views" />
|
||||
<div class="highlight_dark">
|
||||
Total usage from {start_time} to {end_time}<br/>
|
||||
{formatDataVolume(total_bandwidth, 3)} bandwidth and {formatThousands(total_views, 3)} views
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<a class="button" href="/api/admin/call_stack">Call stack</a>
|
||||
<a class="button" href="/api/admin/heap_profile">Heap profile</a>
|
||||
<a class="button" href="/api/admin/cpu_profile">CPU profile (wait 1 min)</a>
|
||||
<br/>
|
||||
|
||||
<div class="limit_width">
|
||||
<table>
|
||||
<tr>
|
||||
<td>DB Time</td>
|
||||
<td>{formatDate(new Date(status.db_time), true, true, true)}</td>
|
||||
<td>DB Latency</td>
|
||||
<td>{formatNumber(status.db_latency / 1000, 3)} ms</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>Pixelstore peers</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Address</td>
|
||||
<td>Pos</td>
|
||||
<td>Alive</td>
|
||||
<td>Err</td>
|
||||
<td>1m</td>
|
||||
<td>5m</td>
|
||||
<td>15m</td>
|
||||
<td>Ping</td>
|
||||
<td>Free</td>
|
||||
<td>Min free</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each status.peers as peer}
|
||||
<tr class="peer_row"
|
||||
class:highlight_red={peer.free_space < peer.min_free_space / 2 || !peer.reachable}
|
||||
class:highlight_blue={peer.free_space < peer.min_free_space}
|
||||
class:highlight_green={peer.reachable}
|
||||
>
|
||||
<td>{peer.address}</td>
|
||||
<td>{peer.position}</td>
|
||||
<td>{peer.reachable}</td>
|
||||
<td>{peer.unreachable_count}</td>
|
||||
<td>{peer.load_1_min}</td>
|
||||
<td>{peer.load_5_min}</td>
|
||||
<td>{peer.load_15_min}</td>
|
||||
<td>{formatDuration(peer.latency)}</td>
|
||||
<td>{formatDataVolume(peer.free_space, 3)}</td>
|
||||
<td>{formatDataVolume(peer.min_free_space, 3)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Pixelstore stats</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Local reads</td>
|
||||
<td>Local read size</td>
|
||||
<td>Remote reads</td>
|
||||
<td>Remote read size</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{status.local_reads}</td>
|
||||
<td>{formatDataVolume(status.local_read_size, 4)}</td>
|
||||
<td>{status.remote_reads}</td>
|
||||
<td>{formatDataVolume(status.remote_read_size, 4)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{status.local_reads_per_sec.toPrecision(4)} / s</td>
|
||||
<td>{formatDataVolume(status.local_read_size_per_sec, 4)} / s</td>
|
||||
<td>{status.remote_reads_per_sec.toPrecision(4)} / s</td>
|
||||
<td>{formatDataVolume(status.remote_read_size_per_sec, 4)} /s</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Websocket statistics</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Watcher</td>
|
||||
<td>Threads</td>
|
||||
<td>Listeners</td>
|
||||
<td>Avg</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>File statistics</td>
|
||||
<td>{status.stats_watcher_threads}</td>
|
||||
<td>{status.stats_watcher_listeners}</td>
|
||||
<td>{(status.stats_watcher_listeners / status.stats_watcher_threads).toPrecision(3)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Query statistics</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td style="cursor: pointer;" on:click={() => { getStats('query_name') }}>Query</td>
|
||||
<td style="cursor: pointer;" on:click={() => { getStats('calls') }}>Calls</td>
|
||||
<td style="cursor: pointer;" on:click={() => { getStats('average_duration') }}>Average Duration</td>
|
||||
<td style="cursor: pointer;" on:click={() => { getStats('total_duration') }}>Total Duration</td>
|
||||
<td>Callers</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tstat_body">
|
||||
{#each status.query_statistics as q}
|
||||
<tr>
|
||||
<td>{q.query_name}</td>
|
||||
<td>{q.calls}</td>
|
||||
<td>{formatDuration(q.average_duration)}</td>
|
||||
<td>{formatDuration(q.total_duration)}</td>
|
||||
<td>
|
||||
{#each q.callers as caller}
|
||||
{caller.count}x {caller.name}<br/>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.peer_row {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
181
svelte/src/admin_panel/IPBans.svelte
Normal file
181
svelte/src/admin_panel/IPBans.svelte
Normal file
@@ -0,0 +1,181 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { formatDate } from "../util/Formatting.svelte";
|
||||
import Spinner from "../util/Spinner.svelte";
|
||||
|
||||
let loading = true
|
||||
let rows = []
|
||||
|
||||
let creating = false
|
||||
let new_ban_address
|
||||
let new_ban_reason = "unknown"
|
||||
|
||||
const get_bans = async () => {
|
||||
loading = true;
|
||||
try {
|
||||
const resp = await fetch(window.api_endpoint+"/admin/ip_ban");
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(resp.text());
|
||||
}
|
||||
rows = await resp.json();
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const create_ban = async () => {
|
||||
if (!new_ban_address.value) {
|
||||
alert("Please enter an IP address!")
|
||||
return
|
||||
} else if (!new_ban_reason) {
|
||||
alert("Please enter a reason!")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const form = new FormData()
|
||||
form.append("address", new_ban_address.value)
|
||||
form.append("reason", new_ban_reason)
|
||||
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/admin/ip_ban",
|
||||
{ method: "POST", body: form }
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Failed to add IP ban! "+err)
|
||||
}
|
||||
|
||||
creating = false
|
||||
get_bans();
|
||||
}
|
||||
|
||||
const delete_ban = async (addr) => {
|
||||
if (!confirm("Delete this banned address?\n\n"+addr)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
window.api_endpoint+"/admin/ip_ban/"+encodeURI(addr),
|
||||
{ method: "DELETE" }
|
||||
);
|
||||
if(resp.status >= 400) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Failed to delete ban! "+err)
|
||||
}
|
||||
|
||||
get_bans();
|
||||
}
|
||||
|
||||
onMount(get_bans);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if loading}
|
||||
<div class="spinner_container">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="limit_width">
|
||||
<div class="toolbar" style="text-align: left;">
|
||||
<div class="toolbar_spacer"></div>
|
||||
<button class:button_highlight={creating} on:click={() => {creating = !creating}}>
|
||||
<i class="icon">create</i> Add IP ban
|
||||
</button>
|
||||
</div>
|
||||
{#if creating}
|
||||
<div class="highlight_light">
|
||||
<form on:submit|preventDefault={create_ban}>
|
||||
<table class="form">
|
||||
<tr>
|
||||
<td>IP address</td>
|
||||
<td><input type="text" bind:this={new_ban_address}/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reason</td>
|
||||
<td>
|
||||
<input id="reason_unknown" name="reporter_type" type="radio" bind:group={new_ban_reason} value="unknown" />
|
||||
<label for="reason_unknown">unknown</label>
|
||||
<br/>
|
||||
<input id="reason_copyright" name="reporter_type" type="radio" bind:group={new_ban_reason} value="copyright" />
|
||||
<label for="reason_copyright">copyright</label>
|
||||
<br/>
|
||||
<input id="reason_child_abuse" name="reporter_type" type="radio" bind:group={new_ban_reason} value="child_abuse" />
|
||||
<label for="reason_child_abuse">child_abuse</label>
|
||||
<br/>
|
||||
<input id="reason_terrorism" name="reporter_type" type="radio" bind:group={new_ban_reason} value="terorrism" />
|
||||
<label for="reason_terrorism">terrorism</label>
|
||||
<br/>
|
||||
<input id="reason_gore" name="reporter_type" type="radio" bind:group={new_ban_reason} value="gore" />
|
||||
<label for="reason_gore">gore</label>
|
||||
<br/>
|
||||
<input id="reason_malware" name="reporter_type" type="radio" bind:group={new_ban_reason} value="malware" />
|
||||
<label for="reason_malware">malware</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button class="button_highlight" type="submit" style="float: right;">
|
||||
<i class="icon">save</i> Save
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<table style="text-align: left;">
|
||||
<tr>
|
||||
<td>Address</td>
|
||||
<td>Reason</td>
|
||||
<td>Ban time</td>
|
||||
<td>Expire time</td>
|
||||
<td>Offences</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{#each rows as row}
|
||||
<tr>
|
||||
<td>{row.address}</td>
|
||||
<td>{row.reason}</td>
|
||||
<td>{formatDate(row.ban_time, true, true, false)}</td>
|
||||
<td>{formatDate(row.expire_time, true, true, false)}</td>
|
||||
<td>{row.offences}</td>
|
||||
<td>
|
||||
<button on:click|preventDefault={() => {delete_ban(row.address)}} class="button button_red">
|
||||
<i class="icon">delete</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spinner_container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
.toolbar > * { flex: 0 0 auto; }
|
||||
.toolbar_spacer { flex: 1 1 auto; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user