mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-06-23 21:25:12 +00:00
Add LUF to ionosonde data API & chart
This commit is contained in:
@@ -161,7 +161,7 @@ class SolarConditions:
|
|||||||
blackout_forecast_r1r2: dict = None
|
blackout_forecast_r1r2: dict = None
|
||||||
# NOAA Radio Blackout (R3 or greater) probability forecast, keyed by UNIX timestamp of start of day UTC
|
# NOAA Radio Blackout (R3 or greater) probability forecast, keyed by UNIX timestamp of start of day UTC
|
||||||
blackout_forecast_r3_or_greater: dict = None
|
blackout_forecast_r3_or_greater: dict = None
|
||||||
# Ionosonde measurements from LGDC, dict keyed by URSI code, values are dicts with keys: ursi, name, fof2, muf
|
# Ionosonde measurements from LGDC, dict keyed by URSI code, values are dicts with keys: ursi, name, fof2, muf, luf
|
||||||
ionosonde_data: dict = None
|
ionosonde_data: dict = None
|
||||||
|
|
||||||
# Derived values (populated by infer_descriptions())
|
# Derived values (populated by infer_descriptions())
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ HISTORY_HOURS = 24
|
|||||||
|
|
||||||
class GIROIonosonde(SolarConditionsProvider):
|
class GIROIonosonde(SolarConditionsProvider):
|
||||||
"""Solar conditions provider using ionosonde data from the GIRO Data Center.
|
"""Solar conditions provider using ionosonde data from the GIRO Data Center.
|
||||||
Queries foF2 and MUF measurements for all stations in datafiles/didbase-stations.csv."""
|
Queries foF2, MUF, and LUF measurements for all stations in datafiles/didbase-stations.csv."""
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(provider_config)
|
super().__init__(provider_config)
|
||||||
@@ -41,7 +41,7 @@ class GIROIonosonde(SolarConditionsProvider):
|
|||||||
|
|
||||||
super().setup(solar_conditions, solar_conditions_cache)
|
super().setup(solar_conditions, solar_conditions_cache)
|
||||||
self.update_data({"ionosonde_data": {
|
self.update_data({"ionosonde_data": {
|
||||||
s["ursi"]: {"ursi": s["ursi"], "name": s["name"], "fof2": None, "muf": None}
|
s["ursi"]: {"ursi": s["ursi"], "name": s["name"], "fof2": None, "muf": None, "luf": None}
|
||||||
for s in self._stations
|
for s in self._stations
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -73,9 +73,9 @@ class GIROIonosonde(SolarConditionsProvider):
|
|||||||
ursi = station["ursi"]
|
ursi = station["ursi"]
|
||||||
name = station["name"]
|
name = station["name"]
|
||||||
try:
|
try:
|
||||||
fof2, muf = self._fetch_station_data(ursi, from_time, now)
|
fof2, muf, luf = self._fetch_station_data(ursi, from_time, now)
|
||||||
if fof2 and muf:
|
if fof2 and muf:
|
||||||
ionosonde_data[ursi] = {"ursi": ursi, "name": name, "fof2": fof2, "muf": muf}
|
ionosonde_data[ursi] = {"ursi": ursi, "name": name, "fof2": fof2, "muf": muf, "luf": luf or None}
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.warning(f"Could not fetch ionosonde data for {ursi} ({name})")
|
logging.warning(f"Could not fetch ionosonde data for {ursi} ({name})")
|
||||||
@@ -91,27 +91,28 @@ class GIROIonosonde(SolarConditionsProvider):
|
|||||||
self._stop_event.wait(timeout=1)
|
self._stop_event.wait(timeout=1)
|
||||||
|
|
||||||
def _fetch_station_data(self, ursi, from_time, to_time):
|
def _fetch_station_data(self, ursi, from_time, to_time):
|
||||||
"""Fetch foF2 and MUF readings for a station. Returns (fof2_dict, muf_dict) keyed by UNIX timestamp."""
|
"""Fetch foF2, MUF and LUF readings for a station. Returns (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp."""
|
||||||
|
|
||||||
from_str = from_time.strftime("%Y.%m.%d+%H:%M:%S")
|
from_str = from_time.strftime("%Y.%m.%d+%H:%M:%S")
|
||||||
to_str = to_time.strftime("%Y.%m.%d+%H:%M:%S")
|
to_str = to_time.strftime("%Y.%m.%d+%H:%M:%S")
|
||||||
url = f"{LGDC_URL}?ursiCode={ursi}&charName=foF2,MUFD&DMUF=3000&fromDate={from_str}&toDate={to_str}"
|
url = f"{LGDC_URL}?ursiCode={ursi}&charName=foF2,MUFD,fmin&DMUF=3000&fromDate={from_str}&toDate={to_str}"
|
||||||
response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15))
|
response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15))
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return None, None
|
return None, None, None
|
||||||
return self._parse_all(response.text)
|
return self._parse_all(response.text)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_all(text):
|
def _parse_all(text):
|
||||||
"""Parse web server response and return (fof2_dict, muf_dict) keyed by UNIX timestamp."""
|
"""Parse web server response and return (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp."""
|
||||||
|
|
||||||
fof2_data = {}
|
fof2_data = {}
|
||||||
muf_data = {}
|
muf_data = {}
|
||||||
|
luf_data = {}
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith('#'):
|
if not line or line.startswith('#'):
|
||||||
continue
|
continue
|
||||||
# Data rows have the following format: timestamp CS foF2 QD MUFD QD
|
# Data rows have the following format: timestamp CS foF2 QD MUFD QD fmin QD
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) >= 5:
|
if len(parts) >= 5:
|
||||||
try:
|
try:
|
||||||
@@ -127,4 +128,9 @@ class GIROIonosonde(SolarConditionsProvider):
|
|||||||
muf_data[ts] = float(parts[4])
|
muf_data[ts] = float(parts[4])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return fof2_data, muf_data
|
if len(parts) >= 7:
|
||||||
|
try:
|
||||||
|
luf_data[ts] = float(parts[6])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return fof2_data, muf_data, luf_data
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1779390551"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1779390551"></script>
|
||||||
<script src="/js/add-spot.js?v=1778927183"></script>
|
<script src="/js/add-spot.js?v=1779390551"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1779390551"></script>
|
||||||
<script src="/js/alerts.js?v=1778927183"></script>
|
<script src="/js/alerts.js?v=1779390551"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -76,9 +76,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1779390551"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1778927183"></script>
|
<script src="/js/spotsbandsandmap.js?v=1779390551"></script>
|
||||||
<script src="/js/bands.js?v=1778927183"></script>
|
<script src="/js/bands.js?v=1779390551"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<title>Spothole</title>
|
<title>Spothole</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/style.css?v=1778927183" type="text/css">
|
<link rel="stylesheet" href="/css/style.css?v=1779390551" type="text/css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||||
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
|
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
|
||||||
@@ -52,9 +52,9 @@
|
|||||||
integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn"
|
integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1778927183"></script>
|
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1779390551"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1778927183"></script>
|
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1779390551"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1778927183"></script>
|
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1779390551"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -249,8 +249,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1779390551"></script>
|
||||||
<script src="/js/conditions.js?v=1778927183"></script>
|
<script src="/js/conditions.js?v=1779390551"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-conditions").addClass("active");
|
$("#nav-link-conditions").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -94,9 +94,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1779390551"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1778927183"></script>
|
<script src="/js/spotsbandsandmap.js?v=1779390551"></script>
|
||||||
<script src="/js/map.js?v=1778927183"></script>
|
<script src="/js/map.js?v=1779390551"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1779390551"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1778927183"></script>
|
<script src="/js/spotsbandsandmap.js?v=1779390551"></script>
|
||||||
<script src="/js/spots.js?v=1778927183"></script>
|
<script src="/js/spots.js?v=1779390551"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1778927183"></script>
|
<script src="/js/common.js?v=1779390551"></script>
|
||||||
<script src="/js/status.js?v=1778927183"></script>
|
<script src="/js/status.js?v=1779390551"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1726,6 +1726,15 @@ components:
|
|||||||
example:
|
example:
|
||||||
"1747267201.0": 21.66
|
"1747267201.0": 21.66
|
||||||
"1747267501.0": 21.80
|
"1747267501.0": 21.80
|
||||||
|
luf:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
description: Lowest Usable Frequency (LUF, reported as fmin) in MHz, keyed by UNIX timestamp (UTC seconds since epoch) of each measurement. Can be null if there is no data.
|
||||||
|
additionalProperties:
|
||||||
|
type: number
|
||||||
|
example:
|
||||||
|
"1747267201.0": 2.10
|
||||||
|
"1747267501.0": 2.05
|
||||||
|
|
||||||
SolarConditionsProviderStatus:
|
SolarConditionsProviderStatus:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -389,11 +389,12 @@ function renderIonosondeData() {
|
|||||||
const station = ionosondeData[ursi];
|
const station = ionosondeData[ursi];
|
||||||
if (!station) return;
|
if (!station) return;
|
||||||
|
|
||||||
// Set up some styles, matching the k-index chart. We use Bootstrap's "primary" and "danger" colours not for any
|
// Set up some styles, matching the k-index chart. We use Bootstrap's "primary", "danger", and "success" colours
|
||||||
// real reason but just to get a suitable blue and red that match the other colours Spothole uses
|
// not for any real reason but just to get a suitable blue, red, and green that match the other colours Spothole uses
|
||||||
const style = getComputedStyle(document.documentElement);
|
const style = getComputedStyle(document.documentElement);
|
||||||
const fof2Color = style.getPropertyValue('--bs-primary').trim();
|
const fof2Color = style.getPropertyValue('--bs-primary').trim();
|
||||||
const mufColor = style.getPropertyValue('--bs-danger').trim();
|
const mufColor = style.getPropertyValue('--bs-success').trim();
|
||||||
|
const lufColor = style.getPropertyValue('--bs-danger').trim();
|
||||||
const textColor = style.getPropertyValue('--bs-body-color').trim() || '#666';
|
const textColor = style.getPropertyValue('--bs-body-color').trim() || '#666';
|
||||||
const gridColor = style.getPropertyValue('--bs-border-color').trim() || 'rgba(128,128,128,0.3)';
|
const gridColor = style.getPropertyValue('--bs-border-color').trim() || 'rgba(128,128,128,0.3)';
|
||||||
|
|
||||||
@@ -407,7 +408,8 @@ function renderIonosondeData() {
|
|||||||
|
|
||||||
const fof2Entries = toSeries(station.fof2);
|
const fof2Entries = toSeries(station.fof2);
|
||||||
const mufEntries = toSeries(station.muf);
|
const mufEntries = toSeries(station.muf);
|
||||||
const allTs = [...fof2Entries, ...mufEntries].map(e => e.ts);
|
const lufEntries = toSeries(station.luf);
|
||||||
|
const allTs = [...fof2Entries, ...mufEntries, ...lufEntries].map(e => e.ts);
|
||||||
if (allTs.length === 0) {
|
if (allTs.length === 0) {
|
||||||
$('#ionosonde-latest').html('<div class="alert alert-warning mt-2 mb-0 py-2">No data available for this station.</div>');
|
$('#ionosonde-latest').html('<div class="alert alert-warning mt-2 mb-0 py-2">No data available for this station.</div>');
|
||||||
$('#ionosonde-chart').hide();
|
$('#ionosonde-chart').hide();
|
||||||
@@ -418,6 +420,7 @@ function renderIonosondeData() {
|
|||||||
// Populate latest values summary (visible on all screen sizes)
|
// Populate latest values summary (visible on all screen sizes)
|
||||||
const latestFof2 = fof2Entries.length ? fof2Entries[fof2Entries.length - 1].val : null;
|
const latestFof2 = fof2Entries.length ? fof2Entries[fof2Entries.length - 1].val : null;
|
||||||
const latestMuf = mufEntries.length ? mufEntries[mufEntries.length - 1].val : null;
|
const latestMuf = mufEntries.length ? mufEntries[mufEntries.length - 1].val : null;
|
||||||
|
const latestLuf = lufEntries.length ? lufEntries[lufEntries.length - 1].val : null;
|
||||||
const minTs = allTs.length ? Math.min(...allTs) : null;
|
const minTs = allTs.length ? Math.min(...allTs) : null;
|
||||||
const maxTs = allTs.length ? Math.max(...allTs) : null;
|
const maxTs = allTs.length ? Math.max(...allTs) : null;
|
||||||
let latestTimeStr = '';
|
let latestTimeStr = '';
|
||||||
@@ -429,9 +432,11 @@ function renderIonosondeData() {
|
|||||||
? '<div class="alert alert-warning mt-2 mb-0 py-2">Data is more than 12 hours old!</div>'
|
? '<div class="alert alert-warning mt-2 mb-0 py-2">Data is more than 12 hours old!</div>'
|
||||||
: '';
|
: '';
|
||||||
$('#ionosonde-latest').html(
|
$('#ionosonde-latest').html(
|
||||||
|
'<div class="row align-items-center me-0">' +
|
||||||
|
'<div class="col-12 py-2 text-muted">Latest values as of ' + latestTimeStr + '</div></div>' +
|
||||||
'<div class="row border-bottom align-items-center me-0">' +
|
'<div class="row border-bottom align-items-center me-0">' +
|
||||||
'<div class="col-12 col-md-6 py-2 text-muted">Latest values as of ' + latestTimeStr + '</div>' +
|
'<div class="col-12 col-md-4 py-2">LUF: <strong>' + (latestLuf !== null ? latestLuf.toFixed(2) + ' MHz' : 'N/A') + '</strong></div>' +
|
||||||
'<div class="col-12 col-md-2 py-2">foF2: <strong>' + (latestFof2 !== null ? latestFof2.toFixed(2) + ' MHz' : 'N/A') + '</strong></div>' +
|
'<div class="col-12 col-md-4 py-2">foF2: <strong>' + (latestFof2 !== null ? latestFof2.toFixed(2) + ' MHz' : 'N/A') + '</strong></div>' +
|
||||||
'<div class="col-12 col-md-4 py-2">MUF (3000 km): <strong>' + (latestMuf !== null ? latestMuf.toFixed(2) + ' MHz' : 'N/A') + '</strong></div>' +
|
'<div class="col-12 col-md-4 py-2">MUF (3000 km): <strong>' + (latestMuf !== null ? latestMuf.toFixed(2) + ' MHz' : 'N/A') + '</strong></div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
staleWarning +
|
staleWarning +
|
||||||
@@ -546,6 +551,14 @@ function renderIonosondeData() {
|
|||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
datasets: [
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'LUF',
|
||||||
|
data: lufEntries.map(e => ({x: e.ts, y: e.val})),
|
||||||
|
borderColor: lufColor,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.2,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'foF2',
|
label: 'foF2',
|
||||||
data: fof2Entries.map(e => ({x: e.ts, y: e.val})),
|
data: fof2Entries.map(e => ({x: e.ts, y: e.val})),
|
||||||
|
|||||||
Reference in New Issue
Block a user