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 %}