5 Commits
14 changed files with 108 additions and 47 deletions
+39 -4
View File
@@ -246,8 +246,9 @@ To set up nginx as a reverse proxy that sits in front of Spothole, first ensure
Create a file at `/etc/nginx/sites-available/` called `spothole`. Give it the following contents, replacing `spothole.app` with the domain name on which you want to run Spothole. If you changed the port on which Spothole runs, update that on the "proxy_pass" line too. Create a file at `/etc/nginx/sites-available/` called `spothole`. Give it the following contents, replacing `spothole.app` with the domain name on which you want to run Spothole. If you changed the port on which Spothole runs, update that on the "proxy_pass" line too.
```nginx ```nginx
map $request_uri $xssorigin { map $request_uri $cors_origin {
~^/api *; ~^/api *;
default "";
} }
server { server {
@@ -258,12 +259,46 @@ server {
alias /var/www/html/.well-known/; alias /var/www/html/.well-known/;
} }
# SSE endpoints
location ~ ^/api/v1/(spots|alerts)/stream {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8081;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h;
proxy_connect_timeout 10s;
proxy_send_timeout 24h;
proxy_set_header X-Accel-Buffering no;
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Cache-Control no-store always;
add_header Content-Type text/event-stream always;
}
# Other API endpoints
location /api/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8081;
proxy_buffering on;
proxy_cache off;
proxy_read_timeout 30s;
proxy_connect_timeout 10s;
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Cache-Control no-store always;
}
# Static assets
location / { location / {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Connection ""; proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8080; proxy_pass http://127.0.0.1:8081;
proxy_hide_header Access-Control-Allow-Origin; proxy_buffering on;
add_header Access-Control-Allow-Origin $xssorigin; proxy_read_timeout 30s;
proxy_connect_timeout 10s;
add_header Cache-Control "public, max-age=3600, must-revalidate" always;
} }
} }
``` ```
+1 -1
View File
@@ -4,7 +4,7 @@ from data.sig import SIG
# General software # General software
SOFTWARE_NAME = "Spothole by M0TRT" SOFTWARE_NAME = "Spothole by M0TRT"
SOFTWARE_VERSION = "1.3" SOFTWARE_VERSION = "1.4-pre"
# HTTP headers used for spot providers that use HTTP # HTTP headers used for spot providers that use HTTP
HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"} HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"}
+19 -10
View File
@@ -98,8 +98,12 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._heartbeat = tornado.ioloop.PeriodicCallback(self._callback, SSE_HANDLER_QUEUE_CHECK_INTERVAL) self._heartbeat = tornado.ioloop.PeriodicCallback(self._callback, SSE_HANDLER_QUEUE_CHECK_INTERVAL)
self._heartbeat.start() self._heartbeat.start()
# Flush headers immediately so nginx doesn't time out waiting for a response
self.write_message("keepalive", "")
except Exception as e: except Exception as e:
logging.warning("Exception when serving SSE socket", e) logging.warning("Exception when serving SSE socket: %s", e, exc_info=True)
self.close()
def close(self): def close(self):
"""When the user closes the socket, empty our queue and remove it from the list so the server no longer fills it""" """When the user closes the socket, empty our queue and remove it from the list so the server no longer fills it"""
@@ -122,20 +126,25 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
try: try:
if self._alert_queue: if self._alert_queue:
while not self._alert_queue.empty(): if not self._alert_queue.empty():
alert = self._alert_queue.get() while not self._alert_queue.empty():
# If the new alert matches our param filters, send it to the client. If not, ignore it. alert = self._alert_queue.get()
if alert_allowed_by_query(alert, self._query_params): # If the new alert matches our param filters, send it to the client. If not, ignore it.
if self._credentials: if alert_allowed_by_query(alert, self._query_params):
alert = copy.deepcopy(alert) if self._credentials:
alert.infer_missing(self._credentials) alert = copy.deepcopy(alert)
self.write_message(msg=json.dumps(alert, default=serialize_everything)) alert.infer_missing(self._credentials)
self.write_message(msg=json.dumps(alert, default=serialize_everything))
else:
# Send a keepalive comment if the queue was empty
self.write_message("keepalive", "")
if self._alert_queue not in self._sse_alert_queues: if self._alert_queue not in self._sse_alert_queues:
logging.error("Web server cleared up a queue of an active connection!") logging.error("Web server cleared up a queue of an active connection!")
self.close() self.close()
except: except:
logging.warning("Exception in SSE callback, connection will be closed.") logging.warning("Exception in SSE callback, connection will be closed: %s", e, exc_info=True)
self.close() self.close()
+19 -10
View File
@@ -100,8 +100,12 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._heartbeat = tornado.ioloop.PeriodicCallback(self._callback, SSE_HANDLER_QUEUE_CHECK_INTERVAL) self._heartbeat = tornado.ioloop.PeriodicCallback(self._callback, SSE_HANDLER_QUEUE_CHECK_INTERVAL)
self._heartbeat.start() self._heartbeat.start()
# Flush headers immediately so nginx doesn't time out waiting for a response
self.write_message("keepalive", "")
except Exception as e: except Exception as e:
logging.warning("Exception when serving SSE socket", e) logging.warning("Exception when serving SSE socket: %s", e, exc_info=True)
self.close()
def close(self): def close(self):
"""When the user closes the socket, empty our queue and remove it from the list so the server no longer fills it""" """When the user closes the socket, empty our queue and remove it from the list so the server no longer fills it"""
@@ -124,20 +128,25 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
try: try:
if self._spot_queue: if self._spot_queue:
while not self._spot_queue.empty(): if not self._spot_queue.empty():
spot = self._spot_queue.get() while not self._spot_queue.empty():
# If the new spot matches our param filters, send it to the client. If not, ignore it. spot = self._spot_queue.get()
if spot_allowed_by_query(spot, self._query_params): # If the new spot matches our param filters, send it to the client. If not, ignore it.
if self._credentials: if spot_allowed_by_query(spot, self._query_params):
spot = copy.deepcopy(spot) if self._credentials:
spot.infer_missing(self._credentials) spot = copy.deepcopy(spot)
self.write_message(msg=json.dumps(spot, default=serialize_everything)) spot.infer_missing(self._credentials)
self.write_message(msg=json.dumps(spot, default=serialize_everything))
else:
# Send a keepalive comment if the queue was empty
self.write_message("keepalive", "")
if self._spot_queue not in self._sse_spot_queues: if self._spot_queue not in self._sse_spot_queues:
logging.error("Web server cleared up a queue of an active connection!") logging.error("Web server cleared up a queue of an active connection!")
self.close() self.close()
except: except:
logging.warning("Exception in SSE callback, connection will be closed.") logging.warning("Exception in SSE callback, connection will be closed: %s", e, exc_info=True)
self.close() self.close()
+1 -1
View File
@@ -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=1780424170"></script> <script src="/js/common.js?v=1780681952"></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 %}
+2 -2
View File
@@ -69,8 +69,8 @@
</div> </div>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780681952"></script>
<script src="/js/add-spot.js?v=1780424170"></script> <script src="/js/add-spot.js?v=1780681952"></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 %}
+2 -2
View File
@@ -70,8 +70,8 @@
</div> </div>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780681952"></script>
<script src="/js/alerts.js?v=1780424170"></script> <script src="/js/alerts.js?v=1780681952"></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 %}
+3 -3
View File
@@ -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=1780424170"></script> <script src="/js/common.js?v=1780681952"></script>
<script src="/js/spotsbandsandmap.js?v=1780424170"></script> <script src="/js/spotsbandsandmap.js?v=1780681952"></script>
<script src="/js/bands.js?v=1780424170"></script> <script src="/js/bands.js?v=1780681952"></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 %}
+4 -4
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1780424170" type="text/css"> <link rel="stylesheet" href="/css/style.css?v=1780681952" 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" />
@@ -19,9 +19,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=1780424170"></script> <script src="https://misc.ianrenton.com/jsutils/utils.js?v=1780681952"></script>
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1780424170"></script> <script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1780681952"></script>
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1780424170"></script> <script src="https://misc.ianrenton.com/jsutils/geo.js?v=1780681952"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
+2 -2
View File
@@ -271,8 +271,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=1780424170"></script> <script src="/js/common.js?v=1780681952"></script>
<script src="/js/conditions.js?v=1780424170"></script> <script src="/js/conditions.js?v=1780681952"></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>
+3 -3
View File
@@ -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=1780424170"></script> <script src="/js/common.js?v=1780681952"></script>
<script src="/js/spotsbandsandmap.js?v=1780424170"></script> <script src="/js/spotsbandsandmap.js?v=1780681952"></script>
<script src="/js/map.js?v=1780424170"></script> <script src="/js/map.js?v=1780681952"></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 %}
+3 -3
View File
@@ -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=1780424170"></script> <script src="/js/common.js?v=1780681952"></script>
<script src="/js/spotsbandsandmap.js?v=1780424170"></script> <script src="/js/spotsbandsandmap.js?v=1780681952"></script>
<script src="/js/spots.js?v=1780424170"></script> <script src="/js/spots.js?v=1780681952"></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 %}
+2 -2
View File
@@ -59,8 +59,8 @@
</div> </div>
</div> </div>
<script src="/js/common.js?v=1780424170"></script> <script src="/js/common.js?v=1780681952"></script>
<script src="/js/status.js?v=1780424170"></script> <script src="/js/status.js?v=1780681952"></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>
+8
View File
@@ -4,6 +4,14 @@ let restartSSEOnErrorTimeoutId;
// Table row count, to alternate shading // Table row count, to alternate shading
let rowCount = 0; let rowCount = 0;
// Set up a listener to close the SSE connection nicely when we navigate away from the page, to prevent console errors
// and keep things nice and tidy for the server.
window.addEventListener('beforeunload', function() {
if (evtSource != null) {
evtSource.close();
}
});
// Load spots and populate the table. // Load spots and populate the table.
function loadSpots() { function loadSpots() {
// If we have an ongoing SSE connection, stop it so it doesn't interfere with our reload // If we have an ongoing SSE connection, stop it so it doesn't interfere with our reload