mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-08-08 11:31:41 +00:00
Split up docs. Closes #122
This commit is contained in:
@@ -33,499 +33,17 @@ see [https://spothole.app/apidocs](https://spothole.app/apidocs) for the API det
|
|||||||
This is a Progressive Web App, so you can also "install" it to your Android or iOS device by accessing it in Chrome or
|
This is a Progressive Web App, so you can also "install" it to your Android or iOS device by accessing it in Chrome or
|
||||||
Safari respectively, and following the menu-driven process for installing PWAs.
|
Safari respectively, and following the menu-driven process for installing PWAs.
|
||||||
|
|
||||||
You are more than welcome to use the data and the API that Spothole provides to power your own software. There are many
|
You are more than welcome to use the data and the API that Spothole provides to power your own software, to run your
|
||||||
ways to do this; see below.
|
own Spothole server, or to modify it in any way. More details can be found in the following sections:
|
||||||
|
|
||||||
## Embedding Spothole in another website
|
* [Embedding Spothole in another website](docs/embedding.md)
|
||||||
|
* [Writing your own client](docs/clients.md)
|
||||||
You can embed Spothole's web interface in another website, e.g. for use as part of a ham radio custom dashboard.
|
* [Running your own copy](docs/running.md)
|
||||||
|
* [nginx reverse proxy configuration](docs/nginx.md)
|
||||||
URL parameters can be used to trigger an "embedded" mode which hides the headers, footers and settings. In this mode,
|
* [systemd service file](docs/systemd.md)
|
||||||
you provide configuration for the various filter and display options via additional URL parameters. Any settings that
|
* [Running in Docker](docs/docker.md)
|
||||||
the user has set for Spothole are ignored. This is so that the embedding site can select, for example, their choice of
|
* [Use of multiple cluster logins](docs/multicluster.md)
|
||||||
dark mode or SIG filters, which will not impact how Spothole appears when the user accesses it directly. Effectively, it
|
* [Modifying the source code](docs/modifying.md)
|
||||||
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 SIGs 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.
|
|
||||||
|
|
||||||
## 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/v1/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.
|
|
||||||
|
|
||||||
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/v1/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/v1/spots?sig=POTA,SOTA` rather than making two separate calls to
|
|
||||||
`https://spothole.app/api/v1/spots?sig=POTA` and `https://spothole.app/api/v1/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.)
|
|
||||||
|
|
||||||
## 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.8 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, mostly because it is downloading an updated file to match callsigns to
|
|
||||||
countries. This is normal, don't panic!
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Running using Docker
|
|
||||||
|
|
||||||
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:
|
|
||||||
spothole:
|
|
||||||
container_name: spothole
|
|
||||||
build:
|
|
||||||
context: https://git.ianrenton.com/ian/spothole.git#main
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
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
|
|
||||||
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).
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### 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`
|
|
||||||
|
|
||||||
### 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`.
|
|
||||||
|
|
||||||
### nginx Reverse Proxy configuration
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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 {
|
|
||||||
server_name spothole.app;
|
|
||||||
|
|
||||||
# Global proxy settings
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# Wellknown area for Lets Encrypt
|
|
||||||
location /.well-known/ {
|
|
||||||
alias /var/www/html/.well-known/;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load static assets directly from the Spothole static directory
|
|
||||||
location /static/ {
|
|
||||||
alias /home/spothole/spothole/static/;
|
|
||||||
expires 1h;
|
|
||||||
add_header Cache-Control "public, max-age=3600, must-revalidate";
|
|
||||||
}
|
|
||||||
|
|
||||||
# SSE endpoints
|
|
||||||
location ~ ^/api/v1/(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;
|
|
||||||
proxy_read_timeout 24h;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Templated pages
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
proxy_read_timeout 30s;
|
|
||||||
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.
|
|
||||||
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 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 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
|
|
||||||
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
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 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 scripts
|
|
||||||
* `/data` - Data storage classes
|
|
||||||
* `/spotproviders` - Classes providing spots by accessing the APIs of other services
|
|
||||||
* `/alertproviders` - Classes providing alerts by accessing the APIs of other services
|
|
||||||
* `/solarconditionsproviders` - Classes providing solar and propagation by accessing the APIs of other services
|
|
||||||
* `/server` - Classes for running Spothole's own web server
|
|
||||||
|
|
||||||
*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/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*
|
|
||||||
|
|
||||||
* `/` - Main script (`spothole.py`), pip `requirements.txt`, config, README, etc.
|
|
||||||
* `/images` - Image sources
|
|
||||||
* `/datafiles` - Local data sources (differentiated from the majority of data files which are loaded from URLs and
|
|
||||||
cached in `/cache`)
|
|
||||||
* `/cache` - Directory where static-ish data downloaded from the internet is cached to avoid rapid re-requests, and
|
|
||||||
where spot/alert data is cached so that it survives a software restart. 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 `spotproviders` 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 alert providers.
|
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
## 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/v1/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.
|
||||||
|
|
||||||
|
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/v1/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/v1/spots?sig=POTA,SOTA` rather than making two separate calls to
|
||||||
|
`https://spothole.app/api/v1/spots?sig=POTA` and `https://spothole.app/api/v1/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.)
|
||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
## Running using Docker
|
||||||
|
|
||||||
|
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:
|
||||||
|
spothole:
|
||||||
|
container_name: spothole
|
||||||
|
build:
|
||||||
|
context: https://git.ianrenton.com/ian/spothole.git#main
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
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
|
||||||
|
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).
|
||||||
|
|
||||||
|
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 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:
|
||||||
|
spothole:
|
||||||
|
container_name: spothole
|
||||||
|
build:
|
||||||
|
context: https://git.ianrenton.com/ian/spothole.git#main
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- docker-network
|
||||||
|
volumes:
|
||||||
|
- ./config.yml:/app/config.yml
|
||||||
|
- ./cache:/app/cache
|
||||||
|
|
||||||
|
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
|
||||||
|
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 {
|
||||||
|
server_name spothole.app;
|
||||||
|
|
||||||
|
# Global proxy settings
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Wellknown area for Lets Encrypt
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SSE endpoints
|
||||||
|
location ~ ^/api/v1/(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;
|
||||||
|
proxy_read_timeout 24h;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Templated pages and static assets
|
||||||
|
location / {
|
||||||
|
proxy_pass http://spothole:8080;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
add_header Cache-Control "no-cache, must-revalidate" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/spothole.app/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/spothole.app/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
server {
|
||||||
|
if ($host = spothole.app) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server_name spothole.app;
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
## 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 SIG 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 SIGs 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.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
## 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 scripts
|
||||||
|
* `/data` - Data storage classes
|
||||||
|
* `/spotproviders` - Classes providing spots by accessing the APIs of other services
|
||||||
|
* `/alertproviders` - Classes providing alerts by accessing the APIs of other services
|
||||||
|
* `/solarconditionsproviders` - Classes providing solar and propagation by accessing the APIs of other services
|
||||||
|
* `/server` - Classes for running Spothole's own web server
|
||||||
|
|
||||||
|
*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/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*
|
||||||
|
|
||||||
|
* `/` - Main script (`spothole.py`), pip `requirements.txt`, config, README, etc.
|
||||||
|
* `/docs` - Documentation
|
||||||
|
* `/images` - Image sources
|
||||||
|
* `/datafiles` - Local data sources (differentiated from the majority of data files which are loaded from URLs and
|
||||||
|
cached in `/cache`)
|
||||||
|
* `/cache` - Directory where static-ish data downloaded from the internet is cached to avoid rapid re-requests, and
|
||||||
|
where spot/alert data is cached so that it survives a software restart. 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 `spotproviders` 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 alert providers.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
## 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`
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
## nginx Reverse Proxy configuration
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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 {
|
||||||
|
server_name spothole.app;
|
||||||
|
|
||||||
|
# Global proxy settings
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Wellknown area for Lets Encrypt
|
||||||
|
location /.well-known/ {
|
||||||
|
alias /var/www/html/.well-known/;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load static assets directly from the Spothole static directory
|
||||||
|
location /static/ {
|
||||||
|
alias /home/spothole/spothole/static/;
|
||||||
|
expires 1h;
|
||||||
|
add_header Cache-Control "public, max-age=3600, must-revalidate";
|
||||||
|
}
|
||||||
|
|
||||||
|
# SSE endpoints
|
||||||
|
location ~ ^/api/v1/(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;
|
||||||
|
proxy_read_timeout 24h;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Templated pages
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
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.
|
||||||
|
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 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 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
|
||||||
|
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
|
||||||
|
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.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
## 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.8 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, mostly because it is downloading an updated file to match callsigns to
|
||||||
|
countries. This is normal, don't panic!
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
## 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`.
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/add-spot.js?v=1786173830"></script>
|
<script src="/static/js/add-spot.js?v=1786174949"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-add-spot").addClass("active");
|
$("#nav-link-add-spot").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/alerts.js?v=1786173830"></script>
|
<script src="/static/js/alerts.js?v=1786174950"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-alerts").addClass("active");
|
$("#nav-link-alerts").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -75,8 +75,8 @@
|
|||||||
<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="/static/js/spotsbandsandmap.js?v=1786173830"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1786174949"></script>
|
||||||
<script src="/static/js/bands.js?v=1786173830"></script>
|
<script src="/static/js/bands.js?v=1786174949"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-bands").addClass("active");
|
$("#nav-link-bands").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
{% extends "skeleton.html" %}
|
{% extends "skeleton.html" %}
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=1786173829" type="text/css">
|
<link rel="stylesheet" href="/static/css/style.css?v=1786174949" type="text/css">
|
||||||
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
|
<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/fontawesome-6.7.2.min.css" rel="stylesheet">
|
||||||
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
|
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
<script src="/static/vendor/js/bootstrap-5.3.8.bundle.min.js"></script>
|
<script src="/static/vendor/js/bootstrap-5.3.8.bundle.min.js"></script>
|
||||||
<script src="/static/vendor/js/tinycolor2-1.6.0.min.js"></script>
|
<script src="/static/vendor/js/tinycolor2-1.6.0.min.js"></script>
|
||||||
|
|
||||||
<script src="/static/js/utils.js?v=1786173829"></script>
|
<script src="/static/js/utils.js?v=1786174949"></script>
|
||||||
<script src="/static/js/ui-ham.js?v=1786173829"></script>
|
<script src="/static/js/ui-ham.js?v=1786174949"></script>
|
||||||
<script src="/static/js/geo.js?v=1786173829"></script>
|
<script src="/static/js/geo.js?v=1786174949"></script>
|
||||||
<script src="/static/js/common.js?v=1786173829"></script>
|
<script src="/static/js/common.js?v=1786174949"></script>
|
||||||
{% end %}
|
{% end %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
||||||
<script src="/static/js/conditions.js?v=1786173830"></script>
|
<script src="/static/js/conditions.js?v=1786174949"></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>
|
||||||
|
|||||||
+2
-2
@@ -108,8 +108,8 @@
|
|||||||
<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="/static/js/spotsbandsandmap.js?v=1786173830"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1786174950"></script>
|
||||||
<script src="/static/js/map.js?v=1786173830"></script>
|
<script src="/static/js/map.js?v=1786174950"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-map").addClass("active");
|
$("#nav-link-map").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -116,8 +116,8 @@
|
|||||||
<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="/static/js/spotsbandsandmap.js?v=1786173829"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1786174949"></script>
|
||||||
<script src="/static/js/spots.js?v=1786173829"></script>
|
<script src="/static/js/spots.js?v=1786174949"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-spots").addClass("active");
|
$("#nav-link-spots").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/status.js?v=1786173830"></script>
|
<script src="/static/js/status.js?v=1786174949"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$("#nav-link-status").addClass("active");
|
$("#nav-link-status").addClass("active");
|
||||||
|
|||||||
Reference in New Issue
Block a user