From f08ff030e39343e4a5c21d877c0a47c40eb06b6f Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Sat, 8 Aug 2026 08:42:29 +0100 Subject: [PATCH] Split up docs. Closes #122 --- README.md | 504 +------------------------------------- docs/clients.md | 39 +++ docs/docker.md | 150 ++++++++++++ docs/embedding.md | 41 ++++ docs/modifying.md | 69 ++++++ docs/multicluster.md | 96 ++++++++ docs/nginx.md | 123 ++++++++++ docs/running.md | 53 ++++ docs/systemd.md | 34 +++ templates/add_spot.html | 2 +- templates/alerts.html | 2 +- templates/bands.html | 4 +- templates/base.html | 10 +- templates/conditions.html | 2 +- templates/map.html | 4 +- templates/spots.html | 4 +- templates/status.html | 2 +- 17 files changed, 631 insertions(+), 508 deletions(-) create mode 100644 docs/clients.md create mode 100644 docs/docker.md create mode 100644 docs/embedding.md create mode 100644 docs/modifying.md create mode 100644 docs/multicluster.md create mode 100644 docs/nginx.md create mode 100644 docs/running.md create mode 100644 docs/systemd.md diff --git a/README.md b/README.md index 714adc9..f6acbb1 100644 --- a/README.md +++ b/README.md @@ -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 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 -ways to do this; see below. - -## 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. - -## 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)
-`UNSET/ANN` (stop announce messages)
-`UNSET/WCY` (stop wcy messages)
-`UNSET/WWV` (stop wwv messages)
-`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)
-`UNSET/ANN` (stop announce messages)
-`UNSET/WCY` (stop wcy messages)
-`UNSET/WWV` (stop wwv messages)
-`UNSET/DX` (stop human DX spots)
-`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)
-`UNSET/ANN` (stop announce messages)
-`UNSET/WCY` (stop wcy messages)
-`UNSET/WWV` (stop wwv messages)
-`UNSET/DX` (stop human DX spots)
-`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)
-`UNSET/ANN` (stop announce messages)
-`UNSET/WCY` (stop wcy messages)
-`UNSET/WWV` (stop wwv messages)
-`UNSET/DX` (stop human DX spots)
-`SET/SKIMMER FT` (enable FT RBN spots) - -For each callsign-SSID, we also specify our basic information with commands: - -`SET/NAME Spothole10`, Spothole11... etc.
-`SET/QTH Cerkno`
-`SET/QRA JN66XD`
-`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. +You are more than welcome to use the data and the API that Spothole provides to power your own software, to run your +own Spothole server, or to modify it in any way. More details can be found in the following sections: + +* [Embedding Spothole in another website](docs/embedding.md) +* [Writing your own client](docs/clients.md) +* [Running your own copy](docs/running.md) + * [nginx reverse proxy configuration](docs/nginx.md) + * [systemd service file](docs/systemd.md) + * [Running in Docker](docs/docker.md) + * [Use of multiple cluster logins](docs/multicluster.md) +* [Modifying the source code](docs/modifying.md) ## Thanks diff --git a/docs/clients.md b/docs/clients.md new file mode 100644 index 0000000..95e0c46 --- /dev/null +++ b/docs/clients.md @@ -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.) \ No newline at end of file diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..2ac47f4 --- /dev/null +++ b/docs/docker.md @@ -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. \ No newline at end of file diff --git a/docs/embedding.md b/docs/embedding.md new file mode 100644 index 0000000..f897655 --- /dev/null +++ b/docs/embedding.md @@ -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. \ No newline at end of file diff --git a/docs/modifying.md b/docs/modifying.md new file mode 100644 index 0000000..e332b7d --- /dev/null +++ b/docs/modifying.md @@ -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. \ No newline at end of file diff --git a/docs/multicluster.md b/docs/multicluster.md new file mode 100644 index 0000000..3632eae --- /dev/null +++ b/docs/multicluster.md @@ -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)
+`UNSET/ANN` (stop announce messages)
+`UNSET/WCY` (stop wcy messages)
+`UNSET/WWV` (stop wwv messages)
+`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)
+`UNSET/ANN` (stop announce messages)
+`UNSET/WCY` (stop wcy messages)
+`UNSET/WWV` (stop wwv messages)
+`UNSET/DX` (stop human DX spots)
+`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)
+`UNSET/ANN` (stop announce messages)
+`UNSET/WCY` (stop wcy messages)
+`UNSET/WWV` (stop wwv messages)
+`UNSET/DX` (stop human DX spots)
+`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)
+`UNSET/ANN` (stop announce messages)
+`UNSET/WCY` (stop wcy messages)
+`UNSET/WWV` (stop wwv messages)
+`UNSET/DX` (stop human DX spots)
+`SET/SKIMMER FT` (enable FT RBN spots) + +For each callsign-SSID, we also specify our basic information with commands: + +`SET/NAME Spothole10`, Spothole11... etc.
+`SET/QTH Cerkno`
+`SET/QRA JN66XD`
+`SET/HOME S50CLX` \ No newline at end of file diff --git a/docs/nginx.md b/docs/nginx.md new file mode 100644 index 0000000..8f3b753 --- /dev/null +++ b/docs/nginx.md @@ -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. \ No newline at end of file diff --git a/docs/running.md b/docs/running.md new file mode 100644 index 0000000..54ae3bc --- /dev/null +++ b/docs/running.md @@ -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. \ No newline at end of file diff --git a/docs/systemd.md b/docs/systemd.md new file mode 100644 index 0000000..a4c9189 --- /dev/null +++ b/docs/systemd.md @@ -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`. \ No newline at end of file diff --git a/templates/add_spot.html b/templates/add_spot.html index 0b6d5d6..4093d04 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -76,7 +76,7 @@ - + diff --git a/templates/alerts.html b/templates/alerts.html index 6b13226..adfb03d 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -75,7 +75,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index eb052c7..2223e5b 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -75,8 +75,8 @@ - - + + diff --git a/templates/base.html b/templates/base.html index 4cb364a..061c44c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -10,10 +10,10 @@ - - - - + + + + {% end %} {% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html index 3dca22e..a5fa06f 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -284,7 +284,7 @@
- + diff --git a/templates/map.html b/templates/map.html index e78371e..d6e9daa 100644 --- a/templates/map.html +++ b/templates/map.html @@ -108,8 +108,8 @@ - - + + diff --git a/templates/spots.html b/templates/spots.html index 9b7416c..782f996 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -116,8 +116,8 @@ - - + + diff --git a/templates/status.html b/templates/status.html index 344ec87..bc44c68 100644 --- a/templates/status.html +++ b/templates/status.html @@ -59,7 +59,7 @@ - +