mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-24 08:14:32 +00:00
Rationalise docs part 4 #151
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
## Writing your own client
|
||||
|
||||
One of the key strengths of Spothole is that the API is well-defined and open to anyone to use. This means you can build
|
||||
your own software that uses data from Spothole.
|
||||
|
||||
As well as the main API endpoints to fetch spots and alerts, with various possible query parameters, there are also
|
||||
Server-Sent Events (SSE) API endpoints to receive a live feed, plus various utility lookup endpoints for things like
|
||||
callsign and park data.
|
||||
|
||||
Various approaches exist to writing your own client, but in general:
|
||||
|
||||
* Refer to the API docs. These are built on an OpenAPI definition file (`/static/apidocs/openapi.yml`), which you can
|
||||
automatically use to generate a client skeleton using various software.
|
||||
* Call the main "spots" or "alerts" API endpoints to get the data you want. For example, your app could call
|
||||
`https://spothole.app/api/v2/spots` once every few minutes. Apply filters if necessary.
|
||||
* Call the "options" API to get an idea of which bands, modes etc. the server knows about. You might want to do that
|
||||
first before calling the spots/alerts APIs, to allow you to populate your filters correctly.
|
||||
* Refer to the provided HTML/JS interface for a reference on different approaches. For example, the "alerts"/"upcoming"
|
||||
page simply query the main spot API on a timer, whereas the spots, map and bands pages combine this approach with
|
||||
using the Server-Sent Events (SSE) endpoint to update live.
|
||||
* Let me know if you get stuck, I'm happy to help.
|
||||
* Caveat: If you have vibe-coded a client, do not contact me until you have read and understood the code using your actual Human Intelligence.
|
||||
|
||||
Please don't hammer the API with an unnecessarily high request rate. For example, Spothole only queries the POTA API
|
||||
once every two minutes, so if your client is interested in POTA data there's no need to poll Spothole any more often
|
||||
than that.
|
||||
|
||||
If you absolutely must be informed within seconds of a spot arriving in Spothole, please use the SSE endpoints instead,
|
||||
e.g. `https://spothole.app/api/v2/spots/stream`.
|
||||
|
||||
If you want to handle different types of spot or alert differently within your client, please consider making a single
|
||||
request to the Spothole API to retrieve all the data, then filtering on your side. For example, call
|
||||
`https://spothole.app/api/v2/spots?sig=POTA,SOTA` rather than making two separate calls to
|
||||
`https://spothole.app/api/v2/spots?sig=POTA` and `https://spothole.app/api/v2/spots?sig=SOTA`.
|
||||
|
||||
Remember, here at Spothole Inc. we offer an industry-standard "five nines" uptime on our server, with our own unique
|
||||
twist: we don't tell you which side of the decimal point the nines start! (Translation: This is a hobby project.
|
||||
`spothole.app` runs on the same server as my blog and other stuff. It might go down without warning. By all means base
|
||||
your own project on data from the main server if you like, but if you want any control over reliability and downtime,
|
||||
please run your own copy instead.)
|
||||
@@ -1,41 +0,0 @@
|
||||
## Embedding Spothole in another website
|
||||
|
||||
You can embed Spothole's web interface in another website, e.g. for use as part of a ham radio custom dashboard.
|
||||
|
||||
URL parameters can be used to trigger an "embedded" mode which hides the headers, footers and settings. In this mode,
|
||||
you provide configuration for the various filter and display options via additional URL parameters. Any settings that
|
||||
the user has set for Spothole are ignored. This is so that the embedding site can select, for example, their choice of
|
||||
dark mode or activity filters, which will not impact how Spothole appears when the user accesses it directly. Effectively, it
|
||||
becomes separate to their normal Spothole settings.
|
||||
|
||||
Setting `embedded` to true is important for the rest of the settings to be applied; otherwise, the user's defaults will
|
||||
be used in preference to the URL params.
|
||||
|
||||
These are supplied with the URL to the page you want to embed, for example for an embedded version of the band map in
|
||||
dark mode, use `https://spothole.app/bands?embedded=true&dark-mode=true`. For an embedded version of the main spots/home
|
||||
page in the system light/dark mode, use `https://spothole.app/?embedded=true`. For dark mode showing 70cm TOTA spots
|
||||
only, use `https://spothole.app/?embedded=true&dark-mode=true&sig=TOTA&band=70cm`. Providing no URL params causes the
|
||||
page to be loaded in the normal way it would when accessed directly in the user's browser.
|
||||
|
||||
The supported parameters are as follows. Generally these match the equivalent parameters in the real Spothole API, where
|
||||
a mapping exists.
|
||||
|
||||
| Name | Allowed Values | Default | Example | Description |
|
||||
|------------------|-------------------------|---------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `embedded` | `true`, `false` | `false` | `?embedded=true` | Enables embedded mode. |
|
||||
| `color_scheme` | `light`, `dark`, `auto` | `auto` | `?color_scheme=dark` | Forces light or dark mode in preference to the operating system default. |
|
||||
| `time_zone` | `UTC`, `local` | `UTC` | `?time_zone=local` | Sets times to be in UTC or local time. |
|
||||
| `limit` | 10, 25, 50, 100 | 50 | `?limit=50` | Sets the number of spots that will be displayed on the main spots page |
|
||||
| `limit` | 25, 50, 100, 200, 500 | 100 | `?limit=100` | Sets the number of alerts that will be displayed on the alerts page |
|
||||
| `max_age` | 300, 600, 1800, 3600 | 1800 | `?max_age=1800` | Sets the maximum age of spots displayed on the map and bands pages, in seconds. |
|
||||
| `band` | Comma-separated list | (all) | `?band=20m,40m` | Sets the list of bands that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `sig` | Comma-separated list | (all) | `?sig=POTA,SOTA,NO_SIG` | Sets the list of activities that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `source` | Comma-separated list | (all) | `?source=Cluster` | Sets the list of sources that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `mode_type` | Comma-separated list | (all) | `?mode_type=PHONE,CW` | Sets the list of mode types that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `dx_continent` | Comma-separated list | (all) | `?dx_continent=NA,SA` | Sets the list of DX Continents that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `de_continent` | Comma-separated list | (all) | `?de_continent=EU` | Sets the list of DE Continents that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. |
|
||||
| `map-center-lat` | Numeric (decimal) | (auto) | `?map-center-lat=51.5` | Sets the initial latitude of the map centre on the map page. If omitted, the map auto-fits to the loaded spots. |
|
||||
| `map-center-lon` | Numeric (decimal) | (auto) | `?map-center-lon=-0.1` | Sets the initial longitude of the map centre on the map page. If omitted, the map auto-fits to the loaded spots. |
|
||||
| `map-zoom` | Numeric (integer) | (auto) | `?map-zoom=6` | Sets the initial zoom level of the map on the map page. If omitted, the map auto-fits to the loaded spots. |
|
||||
|
||||
See the comment at the end of the next section regarding reliability and uptime of the "main" server.
|
||||
@@ -1,79 +0,0 @@
|
||||
## Modifying the source code
|
||||
|
||||
Spothole is Public Domain licenced, so you can grab the source code and start modifying it for your own needs.
|
||||
Contributions of code back to the main repository are encouraged, but completely optional.
|
||||
|
||||
### Code structure
|
||||
|
||||
To navigate your way around the source code, this list may help.
|
||||
|
||||
*Python back-end code*
|
||||
|
||||
* `/core` - Core classes and utilities
|
||||
* `/data` - Data storage classes
|
||||
* `/providers/spot` - Classes providing spots by accessing the APIs of other services
|
||||
* `/providers/alert` - Classes providing alerts by accessing the APIs of other services
|
||||
* `/providers/solarconditions` - Classes providing solar and propagation by accessing the APIs of other services
|
||||
* `/providers/staticdata` - Classes providing static lookup data by accessing bundled data files or the APIs of other
|
||||
services
|
||||
* `/providers/callsign` - Classes providing callsign lookup data by accessing bundled data files or the APIs of other
|
||||
services
|
||||
* `/providers/activityrefdata` - Classes providing activity reference lookup data by accessing bundled data files or
|
||||
the APIs of other services
|
||||
* `/webserver` - Classes for running Spothole's own web server
|
||||
* `/telnetserver` - Classes for running Spothole's telnet server
|
||||
* `spothole.py` - Main application script
|
||||
|
||||
*Templates*
|
||||
|
||||
* `/templates` - Templates used for constructing Spothole's user-targeted HTML pages
|
||||
|
||||
*HTML/JS/CSS front-end code*
|
||||
|
||||
* `/static` - Root for static files served by the web server. These are all served from a path starting `/static/`.
|
||||
* `/static/apidocs` - Contains the OpenAPI spec (`openapi.yml`)
|
||||
* `/static/audio` - Audio files used by the web front-end
|
||||
* `/static/css` - CSS files used by the web front-end
|
||||
* `/static/img` - image files used by the web front-end
|
||||
* `/static/js` - JavaScript used by the web front-end
|
||||
* `/static/vendor` - Third-party libraries (CSS, JS, fonts and images)
|
||||
|
||||
*Miscellaneous*
|
||||
|
||||
* `/` - pip `requirements.txt`, config, README, etc.
|
||||
* `/docs` - Documentation
|
||||
* `/images` - Image sources
|
||||
* `/datafiles` - Local data files, used by some providers when the data will never change and/or is not easily available
|
||||
online in a format Spothole can handle
|
||||
* `/cache` - Directory where Spothole stores all the data it uses that should be persisted to disk. Created on first
|
||||
run.
|
||||
|
||||
### Extending the server
|
||||
|
||||
Spothole is designed to be easily extensible. If you want to write your own spot provider, for example, simply add a
|
||||
module to the `providers.spot` package containing your class. (Currently, in order to be loaded correctly, the module
|
||||
(file) name should be the same as the class name, but lower case.)
|
||||
|
||||
Your class should extend "SpotProvider"; if it operates by polling an HTTP Server on a timer, it can instead extend "
|
||||
HTTPSpotProvider" where some of the work is done for you.
|
||||
|
||||
The class will need to implement a constructor that takes in the `provider_config` and provides it to the superclass
|
||||
constructor, while also taking any other config parameters it needs.
|
||||
|
||||
If you're extending the base `SpotProvider` class, you will need to implement `start()` and `stop()` methods that start
|
||||
and stop a separate thread which handles the provider's processing needs. The thread should call `submit()` or
|
||||
`submit_batch()` when it has one or more spots to report.
|
||||
|
||||
If you're extending the `HTTPSpotProvider` class, you will need to provide a URI to query and an interval to the
|
||||
superclass constructor. You'll then need to implement the `http_response_to_spots()` method which is called when new
|
||||
data is retrieved. Your implementation should then call `submit()` or `submit_batch()` when it has one or more spots to
|
||||
report.
|
||||
|
||||
When constructing spots, use the comments in the Spot class and the existing implementations as an example. All
|
||||
parameters are optional, but you will at least want to provide a `time` (which must be timezone-aware) and a `dx_call`.
|
||||
|
||||
Finally, simply add the appropriate config to the `spot_providers` section of `config.yml`, and your provider should be
|
||||
instantiated on startup.
|
||||
|
||||
The same approach as above is also used for alerts, and other types of providers. Give me a shout if you need any
|
||||
advice.
|
||||
@@ -1,96 +0,0 @@
|
||||
## Multiple cluster nodes with different settings
|
||||
|
||||
Dan, S50U has written in with his Spothole cluster settings. He is using a cluster node which provides RBN spots, and
|
||||
uses different SSIDs on his callsign to get different settings when logged into the same cluster node. For example:
|
||||
|
||||
```
|
||||
-
|
||||
class: "DXCluster"
|
||||
name: "S50CLX"
|
||||
enabled: true
|
||||
host: "s50clx.si"
|
||||
port: 41112
|
||||
login_prompt: "login: "
|
||||
login_callsign: "callsign-10"
|
||||
```
|
||||
|
||||
Telnet to DXSpider and log in with "callsign-10" and execute the following commands:
|
||||
|
||||
`CLEAR/SPOTS ALL` (delete all previous filters)<br/>
|
||||
`UNSET/ANN` (stop announce messages)<br/>
|
||||
`UNSET/WCY` (stop wcy messages)<br/>
|
||||
`UNSET/WWV` (stop wwv messages)<br/>
|
||||
`SET/DX` (enable human DX spots)
|
||||
|
||||
```
|
||||
-
|
||||
class: "DXCluster"
|
||||
name: "RBN CW"
|
||||
enabled: true
|
||||
host: "s50clx.si"
|
||||
port: 41112
|
||||
login_prompt: "login: "
|
||||
login_callsign: "callsign-11"
|
||||
allow_rbn_spots: true
|
||||
enabled_by_default_in_web_ui: false
|
||||
```
|
||||
|
||||
Telnet to DXSpider and log in with "callsign-11" and execute the following commands:
|
||||
|
||||
`CLEAR/SPOTS ALL` (delete all previous filters)<br/>
|
||||
`UNSET/ANN` (stop announce messages)<br/>
|
||||
`UNSET/WCY` (stop wcy messages)<br/>
|
||||
`UNSET/WWV` (stop wwv messages)<br/>
|
||||
`UNSET/DX` (stop human DX spots)<br/>
|
||||
`SET/SKIMMER CW` (enable CW RBN spots)
|
||||
|
||||
```
|
||||
-
|
||||
class: "DXCluster"
|
||||
name: "RBN RTTY"
|
||||
enabled: true
|
||||
host: "s50clx.si"
|
||||
port: 41112
|
||||
login_prompt: "login: "
|
||||
login_callsign: "callsign-12"
|
||||
allow_rbn_spots: true
|
||||
enabled_by_default_in_web_ui: false
|
||||
```
|
||||
|
||||
Telnet to DXSpider and log in with "callsign-12" and execute the following commands:
|
||||
|
||||
`CLEAR/SPOTS ALL` (delete all previous filters)<br/>
|
||||
`UNSET/ANN` (stop announce messages)<br/>
|
||||
`UNSET/WCY` (stop wcy messages)<br/>
|
||||
`UNSET/WWV` (stop wwv messages)<br/>
|
||||
`UNSET/DX` (stop human DX spots)<br/>
|
||||
`SET/SKIMMER RTTY` (enable RTTY RBN spots)
|
||||
|
||||
```
|
||||
-
|
||||
class: "DXCluster"
|
||||
name: "RBN FT4/8"
|
||||
enabled: true
|
||||
host: "s50clx.si"
|
||||
port: 41112
|
||||
login_prompt: "login: "
|
||||
login_callsign: "callsign-13"
|
||||
allow_rbn_spots: true
|
||||
enabled_by_default_in_web_ui: false
|
||||
```
|
||||
|
||||
Telnet to DXSpider and log in with "callsign-13" and execute the following commands:
|
||||
|
||||
`CLEAR/SPOTS ALL` (delete all previous filters)<br/>
|
||||
`UNSET/ANN` (stop announce messages)<br/>
|
||||
`UNSET/WCY` (stop wcy messages)<br/>
|
||||
`UNSET/WWV` (stop wwv messages)<br/>
|
||||
`UNSET/DX` (stop human DX spots)<br/>
|
||||
`SET/SKIMMER FT` (enable FT RBN spots)
|
||||
|
||||
For each callsign-SSID, we also specify our basic information with commands:
|
||||
|
||||
`SET/NAME Spothole10`, Spothole11... etc.<br/>
|
||||
`SET/QTH Cerkno`<br/>
|
||||
`SET/QRA JN66XD`<br/>
|
||||
`SET/HOME S50CLX`
|
||||
@@ -1,54 +0,0 @@
|
||||
## Running your own copy
|
||||
|
||||
If you want to run a copy of Spothole with different configuration settings than the main instance, you can download it
|
||||
and run it on your own local machine or server.
|
||||
|
||||
You will require Python version 3.10 or later. If you encounter an error about `gdal-config` during the following
|
||||
process, you will also need `libgdal-dev` installed.
|
||||
|
||||
To download and set up Spothole on a Debian server, run the following commands. Other operating systems will likely be
|
||||
similar.
|
||||
|
||||
```bash
|
||||
git clone ssh://git@git.ianrenton.com/ian/spothole.git
|
||||
cd spothole
|
||||
python3 -m venv ./.venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
deactivate
|
||||
cp config-example.yml config.yml
|
||||
```
|
||||
|
||||
Then edit `config.yml` in your text editor of choice to set up the software as you like it. Mostly, this will involve
|
||||
enabling or disabling the various providers of spot and alert data.
|
||||
|
||||
By default, all outdoor programme providers are enabled, as is one cluster node and the NG3K DXpedition data. The RBN
|
||||
spot providers are turned off by default due to the volume of traffic from CW/RTTY/FT8 skimmers, and the APRS and Packet
|
||||
spot providers are off by default on the assumption that Spothole users want a spot with a human at the other end of it,
|
||||
but all can be easily re-enabled.
|
||||
|
||||
Other parameters you will want to update include the base URL to your instance, and whether you want to serve a full
|
||||
web-based DX cluster interface or just the API endpoints for client software to use.
|
||||
|
||||
`config.yml` has an entry for a Clublog API key. If provided, this will allow Spothole to retrieve some more information
|
||||
about DX spots. The software will work just fine without it, but you may find a few country flags etc. are less accurate
|
||||
or missing. Clublog API keys are free, but you'll need to get your own by submitting a helpdesk ticket and explaining
|
||||
what you'll use it for. The admin team are happy with the rate of requests made by my Spothole server, so unless you
|
||||
change the source code of yours to radically increase the rate of querying Clublog, I'm sure they will be fine with your
|
||||
server too.
|
||||
|
||||
Once you're happy with the content of `config.yml`, you can proceed to running the software.
|
||||
|
||||
To run the software this time and any future times you want to run it directly from the command line:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
python3 spothole.py
|
||||
```
|
||||
|
||||
The software can take a few seconds to start up, particularly if it's been run previously and has a large amount of
|
||||
cache data to sort through. This is normal, don't panic! Once you see `You can access your copy of Spothole at
|
||||
http://localhost:8080` in the log, your server is good to go.
|
||||
|
||||
If you see some errors on startup, check your configuration, e.g. in case you have specified a port for the web server
|
||||
that is already in use by something else.
|
||||
@@ -1,34 +0,0 @@
|
||||
## systemd configuration
|
||||
|
||||
If you want Spothole to run automatically on startup on a Linux distribution that uses `systemd`, follow the
|
||||
instructions here. For distros that don't use `systemd`, or Windows/OSX/etc., you can find generic instructions for your
|
||||
OS online.
|
||||
|
||||
Create a file at `/etc/systemd/system/spothole.service`. Give it the following content, adjusting for the user you want
|
||||
to run it as and the directory in which you have installed it:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Spothole
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spothole
|
||||
WorkingDirectory=/home/spothole/spothole
|
||||
ExecStart=/home/spothole/spothole/.venv/bin/python /home/spothole/spothole/spothole.py --serve-in-foreground
|
||||
Restart=on-abort
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Run the following:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable spothole
|
||||
sudo systemctl start spothole
|
||||
```
|
||||
|
||||
Check the service has started up correctly with `sudo journalctl -u spothole -f`.
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/add-spot.js?v=1790182205"></script>
|
||||
<script src="/static/js/add-spot.js?v=1790184305"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-add-spot").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/alerts.js?v=1790182205"></script>
|
||||
<script src="/static/js/alerts.js?v=1790184306"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-alerts").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790182205"></script>
|
||||
<script src="/static/js/bands.js?v=1790182205"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790184305"></script>
|
||||
<script src="/static/js/bands.js?v=1790184305"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-bands").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1790182205" type="text/css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1790184305" type="text/css">
|
||||
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
|
||||
@@ -16,10 +16,10 @@
|
||||
window.fetchEventSource = fetchEventSource;
|
||||
</script>
|
||||
|
||||
<script src="/static/js/utils.js?v=1790182205"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1790182205"></script>
|
||||
<script src="/static/js/geo.js?v=1790182205"></script>
|
||||
<script src="/static/js/common.js?v=1790182205"></script>
|
||||
<script src="/static/js/utils.js?v=1790184305"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1790184305"></script>
|
||||
<script src="/static/js/geo.js?v=1790184305"></script>
|
||||
<script src="/static/js/common.js?v=1790184305"></script>
|
||||
{% end %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
||||
<script src="/static/js/conditions.js?v=1790182205"></script>
|
||||
<script src="/static/js/conditions.js?v=1790184305"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-conditions").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -8,7 +8,20 @@
|
||||
<p>In the sections below, you can find more information about what Spothole is, and how to use it.</p>
|
||||
<ul>
|
||||
<li><a href="/help/about">About Spothole</a></li>
|
||||
<li><a href="/help/using">How To Use Spothole</a></li>
|
||||
<li><a href="/help/using">How To Use Spothole</a>
|
||||
<ul>
|
||||
<li><a href="/help/usage/clients">Writing Your Own Client</a></li>
|
||||
<li><a href="/help/usage/embedding">Embedding Spothole In Another Website</a></li>
|
||||
<li><a href="/help/usage/running">Running Your Own Copy</a></li>
|
||||
<ul>
|
||||
<li><a href="/help/usage/systemd">systemd Configuration</a></li>
|
||||
<li><a href="/help/usage/nginx">nginx Reverse Proxy Configuration</a></li>
|
||||
<li><a href="/help/usage/docker">Running Using Docker</a></li>
|
||||
<li><a href="/help/usage/multicluster">Multiple Cluster Nodes With Different Settings</a></li>
|
||||
</ul>
|
||||
<li><a href="/help/usage/modifying">Modifying The Source Code</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/help/sources">Supported Data Sources</a></li>
|
||||
<li><a href="/help/faq">Frequently Asked Questions (FAQ)</a></li>
|
||||
<li><a href="/apidocs">API Documentation</a></li>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends "../help_page.html" %}
|
||||
{% block help_content %}
|
||||
|
||||
<h2 class="mt-4 mb-4">Writing your own client</h2>
|
||||
<p>One of the key strengths of Spothole is that the API is well-defined and open to anyone to use. This means you can build
|
||||
your own software that uses data from Spothole.</p>
|
||||
<p>As well as the main API endpoints to fetch spots and alerts, with various possible query parameters, there are also
|
||||
Server-Sent Events (SSE) API endpoints to receive a live feed, plus various utility lookup endpoints for things like
|
||||
callsign and park data.</p>
|
||||
<p>Various approaches exist to writing your own client, but in general:</p>
|
||||
<ul>
|
||||
<li>Refer to the API docs. These are built on an OpenAPI definition file (<code>/static/apidocs/openapi.yml</code>), which you can
|
||||
automatically use to generate a client skeleton using various software.</li>
|
||||
<li>Call the main "spots" or "alerts" API endpoints to get the data you want. For example, your app could call
|
||||
<code>https://spothole.app/api/v2/spots</code> once every few minutes. Apply filters if necessary.</li>
|
||||
<li>Call the "options" API to get an idea of which bands, modes etc. the server knows about. You might want to do that
|
||||
first before calling the spots/alerts APIs, to allow you to populate your filters correctly.</li>
|
||||
<li>Refer to the provided HTML/JS interface for a reference on different approaches. For example, the "alerts"/"upcoming"
|
||||
page simply query the main spot API on a timer, whereas the spots, map and bands pages combine this approach with
|
||||
using the Server-Sent Events (SSE) endpoint to update live.</li>
|
||||
<li>Let me know if you get stuck, I'm happy to help.
|
||||
<ul>
|
||||
<li>Caveat: If you have vibe-coded a client, do not contact me until you have read and understood the code using your actual Human Intelligence.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Please don't hammer the API with an unnecessarily high request rate. For example, Spothole only queries the POTA API
|
||||
once every two minutes, so if your client is interested in POTA data there's no need to poll Spothole any more often
|
||||
than that.</p>
|
||||
<p>If you absolutely must be informed within seconds of a spot arriving in Spothole, please use the SSE endpoints instead,
|
||||
e.g. <code>https://spothole.app/api/v2/spots/stream</code>.</p>
|
||||
<p>If you want to handle different types of spot or alert differently within your client, please consider making a single
|
||||
request to the Spothole API to retrieve all the data, then filtering on your side. For example, call
|
||||
<code>https://spothole.app/api/v2/spots?sig=POTA,SOTA</code> rather than making two separate calls to
|
||||
<code>https://spothole.app/api/v2/spots?sig=POTA</code> and <code>https://spothole.app/api/v2/spots?sig=SOTA</code>.</p>
|
||||
<p>Remember, here at Spothole Inc. we offer an industry-standard "five nines" uptime on our server, with our own unique
|
||||
twist: we don't tell you which side of the decimal point the nines start! (Translation: This is a hobby project.
|
||||
<code>spothole.app</code> runs on the same server as my blog and other stuff. It might go down without warning. By all means base
|
||||
your own project on data from the main server if you like, but if you want any control over reliability and downtime,
|
||||
please run your own copy instead.)</p>
|
||||
|
||||
{% end %}
|
||||
@@ -1,11 +1,11 @@
|
||||
## Running using Docker
|
||||
{% extends "../help_page.html" %}
|
||||
{% block help_content %}
|
||||
|
||||
Spothole comes with a Docker configuration to make it easy to run it in a containerised environment. To set it up using
|
||||
Docker, the easiest way is to use a Docker Compose file. Create a new directory such as `/opt/docker/spothole` and
|
||||
create a `compose.yaml` file inside it with the following contents:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
<h2 class="mt-4 mb-4">Running using Docker</h2>
|
||||
<p>Spothole comes with a Docker configuration to make it easy to run it in a containerised environment. To set it up using
|
||||
Docker, the easiest way is to use a Docker Compose file. Create a new directory such as <code>/opt/docker/spothole</code> and
|
||||
create a <code>compose.yaml</code> file inside it with the following contents:</p>
|
||||
<pre><code>services:
|
||||
spothole:
|
||||
container_name: spothole
|
||||
build:
|
||||
@@ -17,34 +17,28 @@ services:
|
||||
volumes:
|
||||
- ./config.yml:/app/config.yml
|
||||
- ./cache:/app/cache
|
||||
```
|
||||
|
||||
You can replace `#main` with any other branch or tag reference, for example `#1.5` to pin the build to tagged version
|
||||
1.5.
|
||||
|
||||
Save the file. You will still need to create a copy of `config-example.yml` and name it `config.yml`, though with the
|
||||
</code></pre>
|
||||
<p>You can replace <code>#main</code> with any other branch or tag reference, for example <code>#1.5</code> to pin the build to tagged version
|
||||
1.5.</p>
|
||||
<p>Save the file. You will still need to create a copy of <code>config-example.yml</code> and name it <code>config.yml</code>, though with the
|
||||
Docker setup nothing has actually been downloaded yet, so you will have to copy the example from the repository some
|
||||
other way,
|
||||
e.g. [from the repo in a web browser](https://git.ianrenton.com/ian/spothole/src/branch/main/config-example.yml).
|
||||
e.g. <a href="https://git.ianrenton.com/ian/spothole/src/branch/main/config-example.yml">from the repo in a web browser</a>.</p>
|
||||
<p>With that in place, run <code>docker compose up</code> and you should be good to go. To detach, press <code>d</code> or run the command with
|
||||
the <code>-d</code> flag.</p>
|
||||
|
||||
With that in place, run `docker compose up` and you should be good to go. To detach, press `d` or run the command with
|
||||
the `-d` flag.
|
||||
|
||||
### nginx Reverse Proxy with Docker
|
||||
|
||||
In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal of
|
||||
<h3 class="mt-4">nginx Reverse Proxy with Docker</h3>
|
||||
<p>In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal of
|
||||
HTTPS certificates, and then applications like Spothole in a separate container. In this case, there are a couple of
|
||||
variations of the docker compose file above, and the nginx reverse proxy configuration covered [here](./nginx.md), that
|
||||
you will want to make.
|
||||
|
||||
1. A port mapping is no longer required in the docker compose file; nginx will access into the docker container directly
|
||||
on e.g. `http://spothole:8080`
|
||||
2. Spothole and nginx will need to be on the same docker network.
|
||||
|
||||
So your `compose.yaml` might look like this:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
variations of the docker compose file above, and the nginx reverse proxy configuration covered <a href="/help/usage/nginx">here</a>, that
|
||||
you will want to make.</p>
|
||||
<ol>
|
||||
<li>A port mapping is no longer required in the docker compose file; nginx will access into the docker container directly
|
||||
on e.g. <code>http://spothole:8080</code></li>
|
||||
<li>Spothole and nginx will need to be on the same docker network.</li>
|
||||
</ol>
|
||||
<p>So your <code>compose.yaml</code> might look like this:</p>
|
||||
<pre><code>services:
|
||||
spothole:
|
||||
container_name: spothole
|
||||
build:
|
||||
@@ -59,14 +53,11 @@ services:
|
||||
networks:
|
||||
docker-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
In your nginx site configuration, you'll want to refer to the Spothole container directly, and drop the block that
|
||||
</code></pre>
|
||||
<p>In your nginx site configuration, you'll want to refer to the Spothole container directly, and drop the block that
|
||||
allows nginx to access static files directly, as these will be inaccessible in another container. So you may end up with
|
||||
something like:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
something like:</p>
|
||||
<pre><code>server {
|
||||
server_name spothole.app;
|
||||
|
||||
# Global proxy settings
|
||||
@@ -74,7 +65,7 @@ server {
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_buffering on;
|
||||
|
||||
|
||||
# Pass on IP address and host information to Spothole, in case logging this information is required
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@@ -85,11 +76,11 @@ server {
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
|
||||
# SSE endpoints
|
||||
location ~ ^/api/v\d*/(spots|alerts)/stream/? {
|
||||
proxy_pass http://spothole:8080;
|
||||
|
||||
|
||||
# Remove buffering, remove caching, add suitable timeouts for SSE API calls
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
@@ -97,22 +88,22 @@ server {
|
||||
proxy_send_timeout 24h;
|
||||
proxy_set_header X-Accel-Buffering no;
|
||||
add_header Cache-Control no-store always;
|
||||
|
||||
|
||||
# Allow cross-origin requests to API
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
}
|
||||
|
||||
# Other API endpoints
|
||||
location /api/ {
|
||||
proxy_pass http://spothole:8080;
|
||||
|
||||
|
||||
# Remove buffering, remove caching, add suitable timeouts for API calls
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 30s;
|
||||
add_header Cache-Control no-store always;
|
||||
|
||||
|
||||
# Allow cross-origin requests to API
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
@@ -124,7 +115,7 @@ server {
|
||||
proxy_read_timeout 30s;
|
||||
add_header Cache-Control "no-cache, must-revalidate" always;
|
||||
}
|
||||
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
|
||||
@@ -145,19 +136,15 @@ server {
|
||||
listen [::]:80;
|
||||
return 404;
|
||||
}
|
||||
```
|
||||
</code></pre>
|
||||
<p>If desired, you could even change the port on which Spothole runs from 8080 to a plain 80, in which case your
|
||||
<code>proxy_pass</code> statements could drop the <code>:8080</code> suffix. Since Spothole is in a container, it can serve HTTP on port 80 if
|
||||
desired, because it doesn't conflict with the host system.</p>
|
||||
|
||||
If desired, you could even change the port on which Spothole runs from 8080 to a plain 80, in which case your
|
||||
`proxy_pass` statements could drop the `:8080` suffix. Since Spothole is in a container, it can serve HTTP on port 80 if
|
||||
desired, because it doesn't conflict with the host system.
|
||||
|
||||
### Restoring the static files bypass
|
||||
|
||||
If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can do. The
|
||||
easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
<h3 class="mt-4">Restoring the static files bypass</h3>
|
||||
<p>If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can do. The
|
||||
easiest way is to run another nginx container to serve the files, so your Spothole <code>compose.yaml</code> becomes:</p>
|
||||
<pre><code>services:
|
||||
spothole:
|
||||
container_name: spothole
|
||||
build:
|
||||
@@ -183,16 +170,15 @@ services:
|
||||
networks:
|
||||
docker-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time point it
|
||||
at the new container rather than at a filesystem path:
|
||||
|
||||
```nginx
|
||||
# Load static assets from the spothole-static-nginx container
|
||||
</code></pre>
|
||||
<p>Then you can re-add the block that handles the <code>/static</code> path in your nginx reverse proxy config, but this time point it
|
||||
at the new container rather than at a filesystem path:</p>
|
||||
<pre><code> # Load static assets from the spothole-static-nginx container
|
||||
location /static/ {
|
||||
proxy_pass http://spothole-static-nginx/;
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, max-age=3600, must-revalidate";
|
||||
}
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
{% end %}
|
||||
@@ -0,0 +1,142 @@
|
||||
{% extends "../help_page.html" %}
|
||||
{% block help_content %}
|
||||
|
||||
<h2 class="mt-4 mb-4">Embedding Spothole in another website</h2>
|
||||
<p>You can embed Spothole's web interface in another website, e.g. for use as part of a ham radio custom dashboard.</p>
|
||||
<p>URL parameters can be used to trigger an "embedded" mode which hides the headers, footers and settings. In this mode,
|
||||
you provide configuration for the various filter and display options via additional URL parameters. Any settings that
|
||||
the user has set for Spothole are ignored. This is so that the embedding site can select, for example, their choice of
|
||||
dark mode or activity filters, which will not impact how Spothole appears when the user accesses it directly. Effectively, it
|
||||
becomes separate to their normal Spothole settings.</p>
|
||||
<p>Setting <code>embedded</code> to true is important for the rest of the settings to be applied; otherwise, the user's defaults will
|
||||
be used in preference to the URL params.</p>
|
||||
<p>These are supplied with the URL to the page you want to embed, for example for an embedded version of the band map in
|
||||
dark mode, use <code>https://spothole.app/bands?embedded=true&dark-mode=true</code>. For an embedded version of the main spots/home
|
||||
page in the system light/dark mode, use <code>https://spothole.app/?embedded=true</code>. For dark mode showing 70cm TOTA spots
|
||||
only, use <code>https://spothole.app/?embedded=true&dark-mode=true&sig=TOTA&band=70cm</code>. Providing no URL params causes the
|
||||
page to be loaded in the normal way it would when accessed directly in the user's browser.</p>
|
||||
<p>The supported parameters are as follows. Generally these match the equivalent parameters in the real Spothole API, where
|
||||
a mapping exists.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Allowed Values</th>
|
||||
<th>Default</th>
|
||||
<th>Example</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>embedded</code></td>
|
||||
<td><code>true</code>, <code>false</code></td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>?embedded=true</code></td>
|
||||
<td>Enables embedded mode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>color_scheme</code></td>
|
||||
<td><code>light</code>, <code>dark</code>, <code>auto</code></td>
|
||||
<td><code>auto</code></td>
|
||||
<td><code>?color_scheme=dark</code></td>
|
||||
<td>Forces light or dark mode in preference to the operating system default.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>time_zone</code></td>
|
||||
<td><code>UTC</code>, <code>local</code></td>
|
||||
<td><code>UTC</code></td>
|
||||
<td><code>?time_zone=local</code></td>
|
||||
<td>Sets times to be in UTC or local time.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>10, 25, 50, 100</td>
|
||||
<td>50</td>
|
||||
<td><code>?limit=50</code></td>
|
||||
<td>Sets the number of spots that will be displayed on the main spots page</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>25, 50, 100, 200, 500</td>
|
||||
<td>100</td>
|
||||
<td><code>?limit=100</code></td>
|
||||
<td>Sets the number of alerts that will be displayed on the alerts page</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>max_age</code></td>
|
||||
<td>300, 600, 1800, 3600</td>
|
||||
<td>1800</td>
|
||||
<td><code>?max_age=1800</code></td>
|
||||
<td>Sets the maximum age of spots displayed on the map and bands pages, in seconds.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>band</code></td>
|
||||
<td>Comma-separated list</td>
|
||||
<td>(all)</td>
|
||||
<td><code>?band=20m,40m</code></td>
|
||||
<td>Sets the list of bands that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sig</code></td>
|
||||
<td>Comma-separated list</td>
|
||||
<td>(all)</td>
|
||||
<td><code>?sig=POTA,SOTA,NO_SIG</code></td>
|
||||
<td>Sets the list of activities that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>source</code></td>
|
||||
<td>Comma-separated list</td>
|
||||
<td>(all)</td>
|
||||
<td><code>?source=Cluster</code></td>
|
||||
<td>Sets the list of sources that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>mode_type</code></td>
|
||||
<td>Comma-separated list</td>
|
||||
<td>(all)</td>
|
||||
<td><code>?mode_type=PHONE,CW</code></td>
|
||||
<td>Sets the list of mode types that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>dx_continent</code></td>
|
||||
<td>Comma-separated list</td>
|
||||
<td>(all)</td>
|
||||
<td><code>?dx_continent=NA,SA</code></td>
|
||||
<td>Sets the list of DX Continents that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>de_continent</code></td>
|
||||
<td>Comma-separated list</td>
|
||||
<td>(all)</td>
|
||||
<td><code>?de_continent=EU</code></td>
|
||||
<td>Sets the list of DE Continents that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>map-center-lat</code></td>
|
||||
<td>Numeric (decimal)</td>
|
||||
<td>(auto)</td>
|
||||
<td><code>?map-center-lat=51.5</code></td>
|
||||
<td>Sets the initial latitude of the map centre on the map page. If omitted, the map auto-fits to the loaded spots.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>map-center-lon</code></td>
|
||||
<td>Numeric (decimal)</td>
|
||||
<td>(auto)</td>
|
||||
<td><code>?map-center-lon=-0.1</code></td>
|
||||
<td>Sets the initial longitude of the map centre on the map page. If omitted, the map auto-fits to the loaded spots.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>map-zoom</code></td>
|
||||
<td>Numeric (integer)</td>
|
||||
<td>(auto)</td>
|
||||
<td><code>?map-zoom=6</code></td>
|
||||
<td>Sets the initial zoom level of the map on the map page. If omitted, the map auto-fits to the loaded spots.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>See the comment at the end of the next section regarding reliability and uptime of the "main" server.</p>
|
||||
|
||||
{% end %}
|
||||
@@ -0,0 +1,78 @@
|
||||
{% extends "../help_page.html" %}
|
||||
{% block help_content %}
|
||||
|
||||
<h2 class="mt-4 mb-4">Modifying the source code</h2>
|
||||
<p>Spothole is Public Domain licenced, so you can grab the source code and start modifying it for your own needs.
|
||||
Contributions of code back to the main repository are encouraged, but completely optional.</p>
|
||||
|
||||
<h3 class="mt-4">Code structure</h3>
|
||||
<p>To navigate your way around the source code, this list may help.</p>
|
||||
|
||||
<p><em>Python back-end code</em></p>
|
||||
<ul>
|
||||
<li><code>/core</code> - Core classes and utilities</li>
|
||||
<li><code>/data</code> - Data storage classes</li>
|
||||
<li><code>/providers/spot</code> - Classes providing spots by accessing the APIs of other services</li>
|
||||
<li><code>/providers/alert</code> - Classes providing alerts by accessing the APIs of other services</li>
|
||||
<li><code>/providers/solarconditions</code> - Classes providing solar and propagation by accessing the APIs of other services</li>
|
||||
<li><code>/providers/staticdata</code> - Classes providing static lookup data by accessing bundled data files or the APIs of other
|
||||
services</li>
|
||||
<li><code>/providers/callsign</code> - Classes providing callsign lookup data by accessing bundled data files or the APIs of other
|
||||
services</li>
|
||||
<li><code>/providers/activityrefdata</code> - Classes providing activity reference lookup data by accessing bundled data files or
|
||||
the APIs of other services</li>
|
||||
<li><code>/webserver</code> - Classes for running Spothole's own web server</li>
|
||||
<li><code>/telnetserver</code> - Classes for running Spothole's telnet server</li>
|
||||
<li><code>spothole.py</code> - Main application script</li>
|
||||
</ul>
|
||||
|
||||
<p><em>Templates</em></p>
|
||||
<ul>
|
||||
<li><code>/templates</code> - Templates used for constructing Spothole's user-targeted HTML pages</li>
|
||||
</ul>
|
||||
|
||||
<p><em>HTML/JS/CSS front-end code</em></p>
|
||||
<ul>
|
||||
<li><code>/static</code> - Root for static files served by the web server. These are all served from a path starting <code>/static/</code>.</li>
|
||||
<li><code>/static/apidocs</code> - Contains the OpenAPI spec (<code>openapi.yml</code>)</li>
|
||||
<li><code>/static/audio</code> - Audio files used by the web front-end</li>
|
||||
<li><code>/static/css</code> - CSS files used by the web front-end</li>
|
||||
<li><code>/static/img</code> - image files used by the web front-end</li>
|
||||
<li><code>/static/js</code> - JavaScript used by the web front-end</li>
|
||||
<li><code>/static/vendor</code> - Third-party libraries (CSS, JS, fonts and images)</li>
|
||||
</ul>
|
||||
|
||||
<p><em>Miscellaneous</em></p>
|
||||
<ul>
|
||||
<li><code>/</code> - pip <code>requirements.txt</code>, config, README, etc.</li>
|
||||
<li><code>/docs</code> - Documentation</li>
|
||||
<li><code>/images</code> - Image sources</li>
|
||||
<li><code>/datafiles</code> - Local data files, used by some providers when the data will never change and/or is not easily available
|
||||
online in a format Spothole can handle</li>
|
||||
<li><code>/cache</code> - Directory where Spothole stores all the data it uses that should be persisted to disk. Created on first
|
||||
run.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="mt-4">Extending the server</h3>
|
||||
<p>Spothole is designed to be easily extensible. If you want to write your own spot provider, for example, simply add a
|
||||
module to the <code>providers.spot</code> package containing your class. (Currently, in order to be loaded correctly, the module
|
||||
(file) name should be the same as the class name, but lower case.)</p>
|
||||
<p>Your class should extend "SpotProvider"; if it operates by polling an HTTP Server on a timer, it can instead extend "
|
||||
HTTPSpotProvider" where some of the work is done for you.</p>
|
||||
<p>The class will need to implement a constructor that takes in the <code>provider_config</code> and provides it to the superclass
|
||||
constructor, while also taking any other config parameters it needs.</p>
|
||||
<p>If you're extending the base <code>SpotProvider</code> class, you will need to implement <code>start()</code> and <code>stop()</code> methods that start
|
||||
and stop a separate thread which handles the provider's processing needs. The thread should call <code>submit()</code> or
|
||||
<code>submit_batch()</code> when it has one or more spots to report.</p>
|
||||
<p>If you're extending the <code>HTTPSpotProvider</code> class, you will need to provide a URI to query and an interval to the
|
||||
superclass constructor. You'll then need to implement the <code>http_response_to_spots()</code> method which is called when new
|
||||
data is retrieved. Your implementation should then call <code>submit()</code> or <code>submit_batch()</code> when it has one or more spots to
|
||||
report.</p>
|
||||
<p>When constructing spots, use the comments in the Spot class and the existing implementations as an example. All
|
||||
parameters are optional, but you will at least want to provide a <code>time</code> (which must be timezone-aware) and a <code>dx_call</code>.</p>
|
||||
<p>Finally, simply add the appropriate config to the <code>spot_providers</code> section of <code>config.yml</code>, and your provider should be
|
||||
instantiated on startup.</p>
|
||||
<p>The same approach as above is also used for alerts, and other types of providers. Give me a shout if you need any
|
||||
advice.</p>
|
||||
|
||||
{% end %}
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "../help_page.html" %}
|
||||
{% block help_content %}
|
||||
|
||||
<h2 class="mt-4 mb-4">Multiple cluster nodes with different settings</h2>
|
||||
<p>Dan, S50U has written in with his Spothole cluster settings. He is using a cluster node which provides RBN spots, and
|
||||
uses different SSIDs on his callsign to get different settings when logged into the same cluster node. For example:</p>
|
||||
<pre><code> -
|
||||
class: "DXCluster"
|
||||
name: "S50CLX"
|
||||
enabled: true
|
||||
host: "s50clx.si"
|
||||
port: 41112
|
||||
login_prompt: "login: "
|
||||
login_callsign: "callsign-10"
|
||||
</code></pre>
|
||||
<p>Telnet to DXSpider and log in with "callsign-10" and execute the following commands:</p>
|
||||
<p><code>CLEAR/SPOTS ALL</code> (delete all previous filters)<br/>
|
||||
<code>UNSET/ANN</code> (stop announce messages)<br/>
|
||||
<code>UNSET/WCY</code> (stop wcy messages)<br/>
|
||||
<code>UNSET/WWV</code> (stop wwv messages)<br/>
|
||||
<code>SET/DX</code> (enable human DX spots)</p>
|
||||
<pre><code> -
|
||||
class: "DXCluster"
|
||||
name: "RBN CW"
|
||||
enabled: true
|
||||
host: "s50clx.si"
|
||||
port: 41112
|
||||
login_prompt: "login: "
|
||||
login_callsign: "callsign-11"
|
||||
allow_rbn_spots: true
|
||||
enabled_by_default_in_web_ui: false
|
||||
</code></pre>
|
||||
<p>Telnet to DXSpider and log in with "callsign-11" and execute the following commands:</p>
|
||||
<p><code>CLEAR/SPOTS ALL</code> (delete all previous filters)<br/>
|
||||
<code>UNSET/ANN</code> (stop announce messages)<br/>
|
||||
<code>UNSET/WCY</code> (stop wcy messages)<br/>
|
||||
<code>UNSET/WWV</code> (stop wwv messages)<br/>
|
||||
<code>UNSET/DX</code> (stop human DX spots)<br/>
|
||||
<code>SET/SKIMMER CW</code> (enable CW RBN spots)</p>
|
||||
<pre><code> -
|
||||
class: "DXCluster"
|
||||
name: "RBN RTTY"
|
||||
enabled: true
|
||||
host: "s50clx.si"
|
||||
port: 41112
|
||||
login_prompt: "login: "
|
||||
login_callsign: "callsign-12"
|
||||
allow_rbn_spots: true
|
||||
enabled_by_default_in_web_ui: false
|
||||
</code></pre>
|
||||
<p>Telnet to DXSpider and log in with "callsign-12" and execute the following commands:</p>
|
||||
<p><code>CLEAR/SPOTS ALL</code> (delete all previous filters)<br/>
|
||||
<code>UNSET/ANN</code> (stop announce messages)<br/>
|
||||
<code>UNSET/WCY</code> (stop wcy messages)<br/>
|
||||
<code>UNSET/WWV</code> (stop wwv messages)<br/>
|
||||
<code>UNSET/DX</code> (stop human DX spots)<br/>
|
||||
<code>SET/SKIMMER RTTY</code> (enable RTTY RBN spots)</p>
|
||||
<pre><code> -
|
||||
class: "DXCluster"
|
||||
name: "RBN FT4/8"
|
||||
enabled: true
|
||||
host: "s50clx.si"
|
||||
port: 41112
|
||||
login_prompt: "login: "
|
||||
login_callsign: "callsign-13"
|
||||
allow_rbn_spots: true
|
||||
enabled_by_default_in_web_ui: false
|
||||
</code></pre>
|
||||
<p>Telnet to DXSpider and log in with "callsign-13" and execute the following commands:</p>
|
||||
<p><code>CLEAR/SPOTS ALL</code> (delete all previous filters)<br/>
|
||||
<code>UNSET/ANN</code> (stop announce messages)<br/>
|
||||
<code>UNSET/WCY</code> (stop wcy messages)<br/>
|
||||
<code>UNSET/WWV</code> (stop wwv messages)<br/>
|
||||
<code>UNSET/DX</code> (stop human DX spots)<br/>
|
||||
<code>SET/SKIMMER FT</code> (enable FT RBN spots)</p>
|
||||
<p>For each callsign-SSID, we also specify our basic information with commands:</p>
|
||||
<p><code>SET/NAME Spothole10</code>, Spothole11... etc.<br/>
|
||||
<code>SET/QTH Cerkno</code><br/>
|
||||
<code>SET/QRA JN66XD</code><br/>
|
||||
<code>SET/HOME S50CLX</code></p>
|
||||
|
||||
{% end %}
|
||||
@@ -1,26 +1,23 @@
|
||||
## nginx Reverse Proxy configuration
|
||||
{% extends "../help_page.html" %}
|
||||
{% block help_content %}
|
||||
|
||||
Web servers generally serve their pages from port 80. However, it's best not to serve Spothole's web interface directly
|
||||
<h2 class="mt-4 mb-4">nginx Reverse Proxy configuration</h2>
|
||||
<p>Web servers generally serve their pages from port 80. However, it's best not to serve Spothole's web interface directly
|
||||
on port 80, as that requires root privileges on a Linux system. It also and prevents us using HTTPS to serve a secure
|
||||
site, since Spothole itself doesn't directly support acting as an HTTPS server. The normal solution to this is to use a
|
||||
"reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then
|
||||
"reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then
|
||||
passes on the request to the back-end application (in this case Spothole). nginx is a common choice for this general web
|
||||
server.
|
||||
|
||||
To set up nginx as a reverse proxy that sits in front of Spothole, first ensure it's installed e.g.
|
||||
`sudo apt install nginx`, and enabled e.g. `sudo systemd enable nginx`.
|
||||
|
||||
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, and if you installed Spothole somewhere other than `/home/spothole/spothole`,
|
||||
adjust the alias location for serving static files.
|
||||
|
||||
(The latter section, configuring the nginx server to serve static files directly, improves efficiency because it saves
|
||||
server.</p>
|
||||
<p>To set up nginx as a reverse proxy that sits in front of Spothole, first ensure it's installed e.g.
|
||||
<code>sudo apt install nginx</code>, and enabled e.g. <code>sudo systemd enable nginx</code>.</p>
|
||||
<p>Create a file at <code>/etc/nginx/sites-available/</code> called <code>spothole</code>. Give it the following contents, replacing
|
||||
<code>spothole.app</code> 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, and if you installed Spothole somewhere other than <code>/home/spothole/spothole</code>,
|
||||
adjust the alias location for serving static files.</p>
|
||||
<p>(The latter section, configuring the nginx server to serve static files directly, improves efficiency because it saves
|
||||
Spothole itself from serving JS, CSS etc. files. If you can't do this for some reason, e.g. your nginx and spothole are
|
||||
on different computers, you can omit the `location /static/ {}` block.)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
on different computers, you can omit the <code>location /static/ {}</code> block.)</p>
|
||||
<pre><code>server {
|
||||
server_name spothole.app;
|
||||
|
||||
# Global proxy settings
|
||||
@@ -28,7 +25,7 @@ server {
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_buffering on;
|
||||
|
||||
|
||||
# Pass on IP address and host information to Spothole, in case logging this information is required
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@@ -46,11 +43,11 @@ server {
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, max-age=3600, must-revalidate";
|
||||
}
|
||||
|
||||
|
||||
# SSE endpoints
|
||||
location ~ ^/api/v\d*/(spots|alerts)/stream/? {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
|
||||
|
||||
# Remove buffering, remove caching, add suitable timeouts for SSE API calls
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
@@ -58,22 +55,22 @@ server {
|
||||
proxy_send_timeout 24h;
|
||||
proxy_set_header X-Accel-Buffering no;
|
||||
add_header Cache-Control no-store always;
|
||||
|
||||
|
||||
# Allow cross-origin requests to API
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
}
|
||||
|
||||
# Other API endpoints
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
|
||||
|
||||
# Remove buffering, remove caching, add suitable timeouts for API calls
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 30s;
|
||||
add_header Cache-Control no-store always;
|
||||
|
||||
|
||||
# Allow cross-origin requests to API
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
@@ -86,33 +83,27 @@ server {
|
||||
add_header Cache-Control "no-cache, must-revalidate" always;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
One further change you might want to make to the file above is the `add_header Access-Control-Allow-Origin` statements.
|
||||
</code></pre>
|
||||
<p>One further change you might want to make to the file above is the <code>add_header Access-Control-Allow-Origin</code> statements.
|
||||
These are what's used on my own Spothole server to make sure that other third-party web-based software can get the data
|
||||
from my instance, and applies to any endpoint underneath `/api`. If you want *your* Spothole instance to be set up the
|
||||
from my instance, and applies to any endpoint underneath <code>/api</code>. If you want <em>your</em> Spothole instance to be set up the
|
||||
same way, so that others can write software in JavaScript that can access it, leave this intact. But if you want your
|
||||
Spothole instance to only be usable by scripts running on the web server you write, you can remove these lines. (Note
|
||||
that this doesn't stop other people writing *non-web-based* software that accesses your Spothole API—the
|
||||
that this doesn't stop other people writing <em>non-web-based</em> software that accesses your Spothole API—the
|
||||
enforcement of cross-origin headers only happens within the user's browser. If you need to lock your instance down so
|
||||
that no-one else can access it with *any* software, that's an aspect of nginx or firewall config that you will need to
|
||||
find help with elsewhere.)
|
||||
|
||||
Now, make a symbolic link to enable the site:
|
||||
|
||||
```bash
|
||||
cd /etc/nginx/sites-enabled
|
||||
that no-one else can access it with <em>any</em> software, that's an aspect of nginx or firewall config that you will need to
|
||||
find help with elsewhere.)</p>
|
||||
<p>Now, make a symbolic link to enable the site:</p>
|
||||
<pre><code>cd /etc/nginx/sites-enabled
|
||||
sudo ln -sf ../sites-available/spothole
|
||||
```
|
||||
|
||||
Test that your nginx config isn't broken using `nginx -t`. If it works, restart nginx with
|
||||
`sudo systemctl restart nginx`.
|
||||
|
||||
If you haven't already done so, set up a DNS entry to make sure requests for your domain name end up at the server
|
||||
that's running Spothole.
|
||||
|
||||
You should now be able to access the web interface by going to the domain from your browser.
|
||||
|
||||
Once that's working, [install certbot](https://certbot.eff.org/instructions?ws=nginx&os=snap) onto your server. Run it
|
||||
</code></pre>
|
||||
<p>Test that your nginx config isn't broken using <code>nginx -t</code>. If it works, restart nginx with
|
||||
<code>sudo systemctl restart nginx</code>.</p>
|
||||
<p>If you haven't already done so, set up a DNS entry to make sure requests for your domain name end up at the server
|
||||
that's running Spothole.</p>
|
||||
<p>You should now be able to access the web interface by going to the domain from your browser.</p>
|
||||
<p>Once that's working, <a href="https://certbot.eff.org/instructions?ws=nginx&os=snap">install certbot</a> onto your server. Run it
|
||||
as root, and when prompted pick your domain name from the list. After a few seconds, it should successfully provision a
|
||||
certificate and modify your nginx config files automatically. You should then be able to access the site via HTTPS.
|
||||
certificate and modify your nginx config files automatically. You should then be able to access the site via HTTPS.</p>
|
||||
|
||||
{% end %}
|
||||
@@ -0,0 +1,44 @@
|
||||
{% extends "../help_page.html" %}
|
||||
{% block help_content %}
|
||||
|
||||
<h2 class="mt-4 mb-4">Running your own copy</h2>
|
||||
<p>If you want to run a copy of Spothole with different configuration settings than the main instance, you can download it
|
||||
and run it on your own local machine or server.</p>
|
||||
<p>You will require Python version 3.10 or later. If you encounter an error about <code>gdal-config</code> during the following
|
||||
process, you will also need <code>libgdal-dev</code> installed.</p>
|
||||
<p>To download and set up Spothole on a Debian server, run the following commands. Other operating systems will likely be
|
||||
similar.</p>
|
||||
<pre><code>git clone ssh://git@git.ianrenton.com/ian/spothole.git
|
||||
cd spothole
|
||||
python3 -m venv ./.venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
deactivate
|
||||
cp config-example.yml config.yml
|
||||
</code></pre>
|
||||
<p>Then edit <code>config.yml</code> in your text editor of choice to set up the software as you like it. Mostly, this will involve
|
||||
enabling or disabling the various providers of spot and alert data.</p>
|
||||
<p>By default, all outdoor programme providers are enabled, as is one cluster node and the NG3K DXpedition data. The RBN
|
||||
spot providers are turned off by default due to the volume of traffic from CW/RTTY/FT8 skimmers, and the APRS and Packet
|
||||
spot providers are off by default on the assumption that Spothole users want a spot with a human at the other end of it,
|
||||
but all can be easily re-enabled.</p>
|
||||
<p>Other parameters you will want to update include the base URL to your instance, and whether you want to serve a full
|
||||
web-based DX cluster interface or just the API endpoints for client software to use.</p>
|
||||
<p><code>config.yml</code> has an entry for a Clublog API key. If provided, this will allow Spothole to retrieve some more information
|
||||
about DX spots. The software will work just fine without it, but you may find a few country flags etc. are less accurate
|
||||
or missing. Clublog API keys are free, but you'll need to get your own by submitting a helpdesk ticket and explaining
|
||||
what you'll use it for. The admin team are happy with the rate of requests made by my Spothole server, so unless you
|
||||
change the source code of yours to radically increase the rate of querying Clublog, I'm sure they will be fine with your
|
||||
server too.</p>
|
||||
<p>Once you're happy with the content of <code>config.yml</code>, you can proceed to running the software.</p>
|
||||
<p>To run the software this time and any future times you want to run it directly from the command line:</p>
|
||||
<pre><code>source .venv/bin/activate
|
||||
python3 spothole.py
|
||||
</code></pre>
|
||||
<p>The software can take a few seconds to start up, particularly if it's been run previously and has a large amount of
|
||||
cache data to sort through. This is normal, don't panic! Once you see <code>You can access your copy of Spothole at
|
||||
http://localhost:8080</code> in the log, your server is good to go.</p>
|
||||
<p>If you see some errors on startup, check your configuration, e.g. in case you have specified a port for the web server
|
||||
that is already in use by something else.</p>
|
||||
|
||||
{% end %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "../help_page.html" %}
|
||||
{% block help_content %}
|
||||
|
||||
<h2 class="mt-4 mb-4">systemd configuration</h2>
|
||||
<p>If you want Spothole to run automatically on startup on a Linux distribution that uses <code>systemd</code>, follow the
|
||||
instructions here. For distros that don't use <code>systemd</code>, or Windows/OSX/etc., you can find generic instructions for your
|
||||
OS online.</p>
|
||||
<p>Create a file at <code>/etc/systemd/system/spothole.service</code>. Give it the following content, adjusting for the user you want
|
||||
to run it as and the directory in which you have installed it:</p>
|
||||
<pre><code>[Unit]
|
||||
Description=Spothole
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spothole
|
||||
WorkingDirectory=/home/spothole/spothole
|
||||
ExecStart=/home/spothole/spothole/.venv/bin/python /home/spothole/spothole/spothole.py --serve-in-foreground
|
||||
Restart=on-abort
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
</code></pre>
|
||||
<p>Run the following:</p>
|
||||
<pre><code>sudo systemctl daemon-reload
|
||||
sudo systemctl enable spothole
|
||||
sudo systemctl start spothole
|
||||
</code></pre>
|
||||
<p>Check the service has started up correctly with <code>sudo journalctl -u spothole -f</code>.</p>
|
||||
|
||||
{% end %}
|
||||
+2
-2
@@ -113,8 +113,8 @@
|
||||
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
|
||||
</script>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790182204"></script>
|
||||
<script src="/static/js/map.js?v=1790182204"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790184306"></script>
|
||||
<script src="/static/js/map.js?v=1790184306"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-map").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -125,8 +125,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790182204"></script>
|
||||
<script src="/static/js/spots.js?v=1790182204"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790184305"></script>
|
||||
<script src="/static/js/spots.js?v=1790184305"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-spots").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/status.js?v=1790182205"></script>
|
||||
<script src="/static/js/status.js?v=1790184305"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#nav-link-status").addClass("active");
|
||||
|
||||
+64
-43
@@ -193,49 +193,70 @@ class WebServer:
|
||||
)
|
||||
]
|
||||
else:
|
||||
ui_routes = [
|
||||
(r"/", PageTemplateHandler, {"template_name": "spots"}),
|
||||
(
|
||||
r"/map",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "map"},
|
||||
),
|
||||
(
|
||||
r"/bands",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "bands"},
|
||||
),
|
||||
(
|
||||
r"/alerts",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "alerts"},
|
||||
),
|
||||
(
|
||||
r"/conditions",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "conditions"},
|
||||
),
|
||||
(
|
||||
r"/status",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "status"},
|
||||
),
|
||||
(
|
||||
r"/help",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "help/index"},
|
||||
),
|
||||
] + [
|
||||
(
|
||||
rf"/help/{page}",
|
||||
PageTemplateHandler,
|
||||
{"template_name": f"help/{page}"},
|
||||
)
|
||||
for page in ["about", "using", "sources", "faq", "accuracy", "privacy", "thanks"]
|
||||
] + [
|
||||
# Old "About" page URL, redirect for the benefit of existing links & bookmarks
|
||||
(r"/about", RedirectHandler, {"url": "/help"}),
|
||||
]
|
||||
ui_routes = (
|
||||
[
|
||||
(r"/", PageTemplateHandler, {"template_name": "spots"}),
|
||||
(
|
||||
r"/map",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "map"},
|
||||
),
|
||||
(
|
||||
r"/bands",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "bands"},
|
||||
),
|
||||
(
|
||||
r"/alerts",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "alerts"},
|
||||
),
|
||||
(
|
||||
r"/conditions",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "conditions"},
|
||||
),
|
||||
(
|
||||
r"/status",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "status"},
|
||||
),
|
||||
(
|
||||
r"/help",
|
||||
PageTemplateHandler,
|
||||
{"template_name": "help/index"},
|
||||
),
|
||||
]
|
||||
+ [
|
||||
(
|
||||
rf"/help/{page}",
|
||||
PageTemplateHandler,
|
||||
{"template_name": f"help/{page}"},
|
||||
)
|
||||
for page in ["about", "using", "sources", "faq", "accuracy", "privacy", "thanks"]
|
||||
]
|
||||
+ [
|
||||
(
|
||||
rf"/help/usage/{page}",
|
||||
PageTemplateHandler,
|
||||
{"template_name": f"help/usage/{page}"},
|
||||
)
|
||||
for page in [
|
||||
"running",
|
||||
"docker",
|
||||
"systemd",
|
||||
"nginx",
|
||||
"multicluster",
|
||||
"clients",
|
||||
"embedding",
|
||||
"modifying",
|
||||
]
|
||||
]
|
||||
+ [
|
||||
# Old "About" page URL, redirect for the benefit of existing links & bookmarks
|
||||
(r"/about", RedirectHandler, {"url": "/help"}),
|
||||
]
|
||||
)
|
||||
# Only allow the Add Spot page if spotting is allowed
|
||||
if ALLOW_SPOTTING:
|
||||
ui_routes += [
|
||||
|
||||
Reference in New Issue
Block a user