Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2db7f30de7 | ||
|
|
5c7a6ab1a4 | ||
|
|
4b63328b74 | ||
|
|
ae1d004f60 | ||
|
|
7dd4c1dd24 | ||
|
|
1120247c99 | ||
|
|
c5c01e5450 | ||
|
|
0a65221905 | ||
|
|
9f9667c895 | ||
|
|
fda44e95fc | ||
|
|
945c12e6c6 | ||
|
|
0fde88cd8f | ||
|
|
c6af9a2913 | ||
|
|
11eba84cdb | ||
|
|
b9ead38015 | ||
|
|
3d0178faa1 | ||
|
|
8a2907e02c | ||
|
|
b870ce1c01 | ||
|
|
15107ffe1c | ||
|
|
efd4e0c66d | ||
|
|
652d7c5fb0 | ||
|
|
5a80f43f30 | ||
|
|
6d090cb1c7 | ||
|
|
b5fa3e49d6 | ||
|
|
ef0b60a0b8 | ||
|
|
dc13140cc4 | ||
|
|
5414b1f5bc | ||
|
|
1fdc3635e6 | ||
|
|
e2cc86cddd | ||
|
|
92181c716d | ||
|
|
208ca3d87f | ||
|
|
7167bb18fb | ||
|
|
daa81ebf94 | ||
|
|
543a8df9a2 | ||
|
|
b1347bcc3c | ||
|
|
c7e170b1a3 | ||
|
|
3d75384848 | ||
|
|
bf4819b241 | ||
|
|
7dd6050416 | ||
|
|
bb0ad8ff32 | ||
|
|
9eb192146a | ||
|
|
1b6e6ad142 | ||
|
|
f0d4a9e6f0 | ||
|
|
a2cbe7f5c8 | ||
|
|
57395b8dc7 | ||
|
|
51bb9696b0 | ||
|
|
a11e783cde | ||
|
|
e2e22517b6 | ||
|
|
d8e0399e92 | ||
|
|
f456897520 | ||
|
|
9303035bb9 | ||
|
|
3c40219003 | ||
|
|
6ff7122844 | ||
|
|
9f94ef83ba | ||
|
|
3236b2ecc3 | ||
|
|
2827913d42 | ||
|
|
1ac514293b | ||
|
|
392b339727 | ||
|
|
852eff8de6 | ||
|
|
c74a15c40c | ||
|
|
5419425834 | ||
|
|
f3a386079b | ||
|
|
aa43713943 | ||
|
|
1dece10679 | ||
|
|
c4f16d786a | ||
|
|
36b8adc019 | ||
|
|
bfe0b4757d | ||
|
|
2b61e55783 | ||
|
|
36c4f451b3 | ||
|
|
268d4ae7fa | ||
|
|
1b49e02cd8 | ||
|
|
9a55a6ec39 | ||
|
|
faaf6f770f | ||
|
|
79e4ed6e8b | ||
|
|
f956df1272 | ||
|
|
089ef56b10 | ||
|
|
c4e8721a2b | ||
|
|
a2efc2f767 | ||
|
|
c0e1c55453 | ||
|
|
860ca52e2d | ||
|
|
b891a1e3c0 | ||
|
|
70fb3b5dbe | ||
|
|
5bcc744867 | ||
|
|
da75226a63 | ||
|
|
cab4219740 | ||
|
|
9252275436 | ||
|
|
9d1c21d8ef | ||
|
|
6473f167a8 | ||
|
|
d280a5b3a9 | ||
|
|
b195ce042b | ||
|
|
974a061b44 | ||
|
|
7c50f5f1d7 | ||
|
|
4bf3f4d1e0 | ||
|
|
46da573715 |
@@ -1,2 +0,0 @@
|
||||
*.min.*
|
||||
server/scripts/vendor/*
|
||||
92
.eslintrc
@@ -1,92 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base"
|
||||
],
|
||||
"globals": {
|
||||
"TravelCities": "readonly",
|
||||
"RegionalCities": "readonly",
|
||||
"StationInfo": "readonly",
|
||||
"SunCalc": "readonly",
|
||||
"NoSleep": "readonly",
|
||||
"OVERRIDES": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab",
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"no-tabs": 0,
|
||||
"no-console": 0,
|
||||
"max-len": 0,
|
||||
"no-use-before-define": [
|
||||
"error",
|
||||
{
|
||||
"variables": false
|
||||
}
|
||||
],
|
||||
"no-param-reassign": [
|
||||
"error",
|
||||
{
|
||||
"props": false
|
||||
}
|
||||
],
|
||||
"no-mixed-operators": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
[
|
||||
"&",
|
||||
"|",
|
||||
"^",
|
||||
"~",
|
||||
"<<",
|
||||
">>",
|
||||
">>>"
|
||||
],
|
||||
[
|
||||
"==",
|
||||
"!=",
|
||||
"===",
|
||||
"!==",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<="
|
||||
],
|
||||
[
|
||||
"&&",
|
||||
"||"
|
||||
],
|
||||
[
|
||||
"in",
|
||||
"instanceof"
|
||||
]
|
||||
],
|
||||
"allowSamePrecedence": true
|
||||
}
|
||||
],
|
||||
"import/extensions": [
|
||||
"error",
|
||||
{
|
||||
"mjs": "always",
|
||||
"json": "always"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"*.min.js"
|
||||
]
|
||||
}
|
||||
23
.github/workflows/build-docker.yaml
vendored
@@ -1,5 +1,12 @@
|
||||
name: build-docker
|
||||
on: push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- 'v*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -13,7 +20,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/netbymatt/ws4kp
|
||||
@@ -27,23 +34,23 @@ jobs:
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and Push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
pull: true
|
||||
push: ${{ github.ref == 'refs/heads/main' }}
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
|
||||
19
Dockerfile
@@ -1,10 +1,19 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:24-alpine AS node-builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
CMD ["node", "index.mjs"]
|
||||
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN rm dist/playlist.json
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY static-env-handler.sh /docker-entrypoint.d/01-static-env-handler.sh
|
||||
RUN chmod +x /docker-entrypoint.d/01-static-env-handler.sh
|
||||
|
||||
COPY --from=node-builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
54
README.md
@@ -4,7 +4,7 @@ A live version of this project is available at https://weatherstar.netbymatt.com
|
||||
|
||||
## About
|
||||
|
||||
This project aims to bring back the feel of the 90's with a weather forecast that has the look and feel of The Weather Channel at that time but available in a modern way. This is by no means intended to be a perfect emulation of the WeatherStar 4000, the hardware that produced those wonderful blue and orange graphics you saw during the local forecast on The Weather Channel. If you would like a much more accurate project please see the [WS4000 Simulator](http://www.taiganet.com/). Instead, this project intends to create a simple to use interface with minimal configuration fuss. Some changes have been made to the screens available because either more or less forecast information is available today than was in the 90's. Most of these changes are captured in sections below.
|
||||
This project aims to bring back the feel of the 90s with a weather forecast that has the look and feel of The Weather Channel at that time but available in a modern way. This is by no means intended to be a perfect emulation of the WeatherStar 4000, the hardware that produced those wonderful blue and orange graphics you saw during the local forecast on The Weather Channel. If you would like a much more accurate project please see the [WS4000 Simulator](http://www.taiganet.com/). Instead, this project intends to create a simple to use interface with minimal configuration fuss. Some changes have been made to the screens available because either more or less forecast information is available today than was in the 90s. Most of these changes are captured in sections below.
|
||||
|
||||
## What's your motivation
|
||||
|
||||
@@ -76,10 +76,24 @@ services:
|
||||
### Serving static files
|
||||
The app can be served as a static set of files on any web server. Run the provided gulp task to create a set of static distribution files:
|
||||
```
|
||||
npm run buildDist
|
||||
npm run build
|
||||
```
|
||||
The resulting files will be in the /dist folder in the root of the project. These can then be uploaded to a web server for hosting, no server-side scripting is required.
|
||||
|
||||
When using the provided Docker image, the browser will generate `playlist.json`
|
||||
on the fly by scanning the `/music` directory served by nginx. The image
|
||||
intentionally omits this file so the page falls back to scanning the directory.
|
||||
Simply bind mount your music folder and the playlist will be created
|
||||
automatically. If no files are found in `/music`, the built in tracks located in
|
||||
`/music/default/` will be used instead.
|
||||
|
||||
The nginx configuration also sets the `X-Weatherstar: true` header on all
|
||||
responses. This uses `add_header ... always` so the header is sent even for
|
||||
404 responses. When `playlist.json` returns a 404 with this header present, the
|
||||
browser falls back to scanning the `/music` directory. If you host the static
|
||||
files elsewhere, be sure to include this header so the playlist can be generated
|
||||
automatically.
|
||||
|
||||
## What's different
|
||||
|
||||
I've made several changes to this Weather Star 4000 simulation compared to the original hardware unit and the code that this was forked from.
|
||||
@@ -88,13 +102,13 @@ I've made several changes to this Weather Star 4000 simulation compared to the o
|
||||
* A new hour-by-hour graph of the temperature, cloud cover and precipitation chances for the next 24 hours.
|
||||
* A new hourly forecast display for the next 24 hours is available, and is shown in the style of the travel cities forecast. (off by default because it duplicates the hourly graph)
|
||||
* The SPC Outlook is shown in the style of the old air quality screen. This shows the probability of severe weather over the next 3 days at your location.
|
||||
* The "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90's.
|
||||
* The "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90s.
|
||||
* The original music has been replaced. More info in [Music](#music).
|
||||
* Marine forecast (tides) is not available as it is not reliably part of the new API.
|
||||
* "Flavors" are not present in this simulation. Flavors refer to the order of the weather information that was shown on the original units. Instead, the order of the displays has been fixed and a checkboxes can be used to turn on and off individual displays. The travel forecast has been defaulted to off so only local information shows for new users.
|
||||
|
||||
## Sharing a permalink (bookmarking)
|
||||
Selected displays, the forecast city and widescreen setting are sticky from one session to the next. However if you would like to share your exact configuration or bookmark it, click the "Copy Permalink" (or get "Get Parmalink") near the bottom of the page. A URL will be copied to your clipboard with all of you selected displays and location (or copy it from the page if your browser doesn't support clipboard transfers directly). You can then share this link or add it to your bookmarks.
|
||||
Selected displays, the forecast city and widescreen setting are sticky from one session to the next. However if you would like to share your exact configuration or bookmark it, click the "Copy Permalink" (or get "Get Permalink") near the bottom of the page. A URL will be copied to your clipboard with all of you selected displays and location (or copy it from the page if your browser doesn't support clipboard transfers directly). You can then share this link or add it to your bookmarks.
|
||||
|
||||
Your permalink will be very long. Here is an example for the Orlando International Airport:
|
||||
```
|
||||
@@ -112,26 +126,31 @@ Kiosk mode can be activated by a checkbox on the page. Note that there is no way
|
||||
|
||||
It's also possible to enter kiosk mode using a permalink. First generate a [Permalink](#sharing-a-permalink-bookmarking), then to the end of it add `&kiosk=true`. Opening this link will load all of the selected displays included in the Permalink, enter kiosk mode immediately upon loading and start playing the forecast.
|
||||
|
||||
### Default query string paramaters (environment variables)
|
||||
When serving this via the built-in Express server, it's possible to define environment variables that direct the user to a default set of paramaters (like the [Permalink](#sharing-a-permalink-bookmarking) above). If a user requests the root page at `http://localhost:8080/` the query string provided by environment variables will be appended to the url thus providing a default configuration.
|
||||
### Default query string parameters (environment variables)
|
||||
When serving this via the built-in Express server, it's possible to define environment variables that direct the user to a default set of parameters (like the [Permalink](#sharing-a-permalink-bookmarking) above). If a user requests the root page at `http://localhost:8080/` the query string provided by environment variables will be appended to the url thus providing a default configuration.
|
||||
|
||||
Environment variables can be added to the command line as usual, or via a .env file which is parsed with [dotenv](https://github.com/motdotla/dotenv). Both methods have the same effect.
|
||||
|
||||
Environment variables that are to be added to the default query string are prefixed with `WSQS_` and then use the same key/value pairs generated by the [Permalink](#sharing-a-permalink-bookmarking) above, with the `- (dash)` character replaced by an `_ (underscore)`. For example, if you wanted to turn the travel forecast on, you would find `travel-checkbox=true` in the permalink, it's matching environment variable becomes `WSQS_travel_checkbox=true`.
|
||||
Environment variables that are to be added to the default query string are prefixed with `WSQS_` and then use the same key/value pairs generated by the [Permalink](#sharing-a-permalink-bookmarking) above, with the `- (dash)` character replaced by an `_ (underscore)`. For example, if you wanted to turn the travel forecast on, you would find `travel-checkbox=true` in the permalink, its matching environment variable becomes `WSQS_travel_checkbox=true`.
|
||||
|
||||
When using the Docker container, these environment variables are read on container start-up to generate the static redirect HTML.
|
||||
|
||||
## Music
|
||||
The WeatherStar had wonderful background music from the smooth jazz and new age genres by artists of the time. Lists of the music that played are available by searching online, but it's all copyrighted music and would be difficult to provide as part of this repository.
|
||||
|
||||
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks are in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
||||
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. To keep the size down, I've only included 4 tracks. Additional tracks are in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
||||
|
||||
If you're looking for the original music that played during forecasts [TWCClassics](https://twcclassics.com/audio/) has thurough documentation of playlists.
|
||||
If you're looking for the original music that played during forecasts [TWCClassics](https://twcclassics.com/audio/) has thorough documentation of playlists.
|
||||
|
||||
### Customizing the music
|
||||
Placing .mp3 files in the `/server/music` folder will override the default music included in the repo. Subdirectories will not be scanned. When weatherstar loads in the browser it will load a list if available files and randomize the order when it starts playing. On each loop through the available tracks the order will again be shuffled. If you're using the static files method to host your WeatherStar music is located in `/music`.
|
||||
|
||||
If using docker, you must pass a local accessible folder to the container in the `/app/server/music` directory.
|
||||
If using Docker, you can bind mount a local folder containing your music files.
|
||||
Mount the folder at `/usr/share/nginx/html/music` so the browser can read the
|
||||
directory listing and build the playlist automatically. If there are no `.mp3`
|
||||
files in `/music`, the built in tracks from `/music/default/` are used.
|
||||
```
|
||||
docker run -p 8080:8080 -v /path/to/local/music:/app/server/music ghcr.io/netbymatt/ws4kp
|
||||
docker run -p 8080:8080 -v /path/to/local/music:/usr/share/nginx/html/music ghcr.io/netbymatt/ws4kp
|
||||
```
|
||||
|
||||
### Music doesn't auto play
|
||||
@@ -139,6 +158,11 @@ Ws4kp is muted by default, but if it was unmuted on the last visit it is coded t
|
||||
|
||||
Chrome seems to be more lenient on auto play and will eventually let a site auto-play music if you're visited it enough recently and manually clicked to start playing music on each visit. It also has a flag you can add to the command line when launching Chrome: `chrome.exe --autoplay-policy=no-user-gesture-required`. This is the best solution when using Kiosk-style setup.
|
||||
|
||||
If you're unable to pre-set the play state before entering kiosk mode (such as with a home dashboard implementation) you can add the query string value below to the url. The browser will still follow the auto play rules outlined above.
|
||||
```
|
||||
?settings-mediaPlaying-boolean=true
|
||||
```
|
||||
|
||||
## Community Notes
|
||||
|
||||
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!
|
||||
@@ -148,7 +172,12 @@ Thanks to the WeatherStar community for providing these discussions to further e
|
||||
* [ws4channels](https://github.com/rice9797/ws4channels) A Dockerized Node.js application to stream WeatherStar 4000 data into Channels DVR using Puppeteer and FFmpeg.
|
||||
|
||||
## Customization
|
||||
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.
|
||||
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. A sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.
|
||||
|
||||
When using Docker, mount your `custom.js` file to `/usr/share/nginx/html/scripts/custom.js` to customize the static build.
|
||||
|
||||
### RSS feeds and custom scroll
|
||||
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, or show headlines from an rss feed turn on the setting for `Enable RSS Feed/Text` and then enter a URL or text in the resulting text box. Then press set.
|
||||
|
||||
## Issue reporting and feature requests
|
||||
|
||||
@@ -182,4 +211,3 @@ This project is based on the work of [Mike Battaglia](https://github.com/vbguyny
|
||||
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.
|
||||
|
||||
The WeatherSTAR 4000 unit and technology is owned by The Weather Channel. This web site is a free, non-profit work by fans. All of the back ground graphics of this web site were created from scratch. The icons were created by Charles Abel and Nick Smith (http://twcclassics.com/downloads/icons.html) as well as by Malek Masoud. The fonts were originally created by Nick Smith (http://twcclassics.com/downloads/fonts.html).
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// pass through api requests
|
||||
|
||||
// http(s) modules
|
||||
import https from 'https';
|
||||
|
||||
// url parsing
|
||||
import queryString from 'querystring';
|
||||
|
||||
// return an express router
|
||||
const cors = (req, res) => {
|
||||
// add out-going headers
|
||||
const headers = {};
|
||||
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
||||
headers.accept = req.headers.accept;
|
||||
|
||||
// get query paramaters if the exist
|
||||
const queryParams = Object.keys(req.query).reduce((acc, key) => {
|
||||
// skip the paramater 'u'
|
||||
if (key === 'u') return acc;
|
||||
// add the paramter to the resulting object
|
||||
acc[key] = req.query[key];
|
||||
return acc;
|
||||
}, {});
|
||||
let query = queryString.encode(queryParams);
|
||||
if (query.length > 0) query = `?${query}`;
|
||||
|
||||
// get the page
|
||||
https.get(`https://api.weather.gov${req.path}${query}`, {
|
||||
headers,
|
||||
}, (getRes) => {
|
||||
// pull some info
|
||||
const { statusCode } = getRes;
|
||||
// pass the status code through
|
||||
res.status(statusCode);
|
||||
|
||||
// set headers
|
||||
res.header('content-type', getRes.headers['content-type']);
|
||||
// pipe to response
|
||||
getRes.pipe(res);
|
||||
}).on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
export default cors;
|
||||
@@ -1,46 +0,0 @@
|
||||
// pass through api requests
|
||||
|
||||
// http(s) modules
|
||||
import https from 'https';
|
||||
|
||||
// url parsing
|
||||
import queryString from 'querystring';
|
||||
|
||||
// return an express router
|
||||
const outlook = (req, res) => {
|
||||
// add out-going headers
|
||||
const headers = {};
|
||||
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
||||
headers.accept = req.headers.accept;
|
||||
|
||||
// get query paramaters if the exist
|
||||
const queryParams = Object.keys(req.query).reduce((acc, key) => {
|
||||
// skip the paramater 'u'
|
||||
if (key === 'u') return acc;
|
||||
// add the paramter to the resulting object
|
||||
acc[key] = req.query[key];
|
||||
return acc;
|
||||
}, {});
|
||||
let query = queryString.encode(queryParams);
|
||||
if (query.length > 0) query = `?${query}`;
|
||||
|
||||
// get the page
|
||||
https.get(`https://www.cpc.ncep.noaa.gov/${req.path}${query}`, {
|
||||
headers,
|
||||
}, (getRes) => {
|
||||
// pull some info
|
||||
const { statusCode } = getRes;
|
||||
// pass the status code through
|
||||
res.status(statusCode);
|
||||
|
||||
// set headers
|
||||
res.header('content-type', getRes.headers['content-type']);
|
||||
res.header('last-modified', getRes.headers['last-modified']);
|
||||
// pipe to response
|
||||
getRes.pipe(res);
|
||||
}).on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
export default outlook;
|
||||
@@ -1,46 +0,0 @@
|
||||
// pass through api requests
|
||||
|
||||
// http(s) modules
|
||||
import https from 'https';
|
||||
|
||||
// url parsing
|
||||
import queryString from 'querystring';
|
||||
|
||||
// return an express router
|
||||
const radar = (req, res) => {
|
||||
// add out-going headers
|
||||
const headers = {};
|
||||
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
||||
headers.accept = req.headers.accept;
|
||||
|
||||
// get query paramaters if the exist
|
||||
const queryParams = Object.keys(req.query).reduce((acc, key) => {
|
||||
// skip the paramater 'u'
|
||||
if (key === 'u') return acc;
|
||||
// add the paramter to the resulting object
|
||||
acc[key] = req.query[key];
|
||||
return acc;
|
||||
}, {});
|
||||
let query = queryString.encode(queryParams);
|
||||
if (query.length > 0) query = `?${query}`;
|
||||
|
||||
// get the page
|
||||
https.get(`https://radar.weather.gov${req.path}${query}`, {
|
||||
headers,
|
||||
}, (getRes) => {
|
||||
// pull some info
|
||||
const { statusCode } = getRes;
|
||||
// pass the status code through
|
||||
res.status(statusCode);
|
||||
|
||||
// set headers
|
||||
res.header('content-type', getRes.headers['content-type']);
|
||||
res.header('last-modified', getRes.headers['last-modified']);
|
||||
// pipe to response
|
||||
getRes.pipe(res);
|
||||
}).on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
export default radar;
|
||||
105
eslint.config.mjs
Normal file
@@ -0,0 +1,105 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const compat = new FlatCompat({
|
||||
});
|
||||
|
||||
export default [{
|
||||
ignores: [
|
||||
'*.min.*',
|
||||
'server/scripts/vendor/*',
|
||||
'dist/**/*',
|
||||
],
|
||||
},
|
||||
...compat.config({
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
],
|
||||
globals: {
|
||||
TravelCities: 'readonly',
|
||||
RegionalCities: 'readonly',
|
||||
StationInfo: 'readonly',
|
||||
SunCalc: 'readonly',
|
||||
NoSleep: 'readonly',
|
||||
OVERRIDES: 'readonly',
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [],
|
||||
rules: {
|
||||
indent: [
|
||||
'error',
|
||||
'tab',
|
||||
{
|
||||
SwitchCase: 1,
|
||||
},
|
||||
],
|
||||
'no-tabs': 0,
|
||||
'no-console': 0,
|
||||
'max-len': 0,
|
||||
'no-use-before-define': [
|
||||
'error',
|
||||
{
|
||||
variables: false,
|
||||
},
|
||||
],
|
||||
'no-param-reassign': [
|
||||
'error',
|
||||
{
|
||||
props: false,
|
||||
},
|
||||
],
|
||||
'no-mixed-operators': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
[
|
||||
'&',
|
||||
'|',
|
||||
'^',
|
||||
'~',
|
||||
'<<',
|
||||
'>>',
|
||||
'>>>',
|
||||
],
|
||||
[
|
||||
'==',
|
||||
'!=',
|
||||
'===',
|
||||
'!==',
|
||||
'>',
|
||||
'>=',
|
||||
'<',
|
||||
'<=',
|
||||
],
|
||||
[
|
||||
'&&',
|
||||
'||',
|
||||
],
|
||||
[
|
||||
'in',
|
||||
'instanceof',
|
||||
],
|
||||
],
|
||||
allowSamePrecedence: true,
|
||||
},
|
||||
],
|
||||
'import/extensions': [
|
||||
'error',
|
||||
{
|
||||
mjs: 'always',
|
||||
json: 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
ignorePatterns: [
|
||||
'*.min.js',
|
||||
],
|
||||
}),
|
||||
];
|
||||
@@ -89,6 +89,7 @@ const mjsSources = [
|
||||
'server/scripts/modules/travelforecast.mjs',
|
||||
'server/scripts/modules/progress.mjs',
|
||||
'server/scripts/modules/media.mjs',
|
||||
'server/scripts/modules/custom-rss-feed.mjs',
|
||||
'server/scripts/index.mjs',
|
||||
];
|
||||
|
||||
@@ -137,10 +138,13 @@ const s3 = s3Upload({
|
||||
const uploadSources = [
|
||||
'dist/**',
|
||||
'!dist/**/*.map',
|
||||
'!dist/images/**/*',
|
||||
'!dist/fonts/**/*',
|
||||
];
|
||||
const upload = () => src(uploadSources, { base: './dist', encoding: false })
|
||||
|
||||
const uploadCreator = (bucket) => () => src(uploadSources, { base: './dist', encoding: false })
|
||||
.pipe(s3({
|
||||
Bucket: process.env.BUCKET,
|
||||
Bucket: bucket,
|
||||
StorageClass: 'STANDARD',
|
||||
maps: {
|
||||
CacheControl: (keyname) => {
|
||||
@@ -156,10 +160,14 @@ const imageSources = [
|
||||
'server/images/**',
|
||||
'!server/images/gimp/**',
|
||||
];
|
||||
const uploadImages = () => src(imageSources, { base: './server', encoding: false })
|
||||
|
||||
const upload = uploadCreator(process.env.BUCKET);
|
||||
const uploadPreview = uploadCreator(process.env.BUCKET_PREVIEW);
|
||||
|
||||
const uploadImagesCreator = (bucket) => () => src(imageSources, { base: './server', encoding: false })
|
||||
.pipe(
|
||||
s3({
|
||||
Bucket: process.env.BUCKET,
|
||||
Bucket: bucket,
|
||||
StorageClass: 'STANDARD',
|
||||
maps: {
|
||||
CacheControl: () => 'max-age=31536000',
|
||||
@@ -167,8 +175,14 @@ const uploadImages = () => src(imageSources, { base: './server', encoding: false
|
||||
}),
|
||||
);
|
||||
|
||||
const invalidate = () => cloudfront.send(new CreateInvalidationCommand({
|
||||
DistributionId: process.env.DISTRIBUTION_ID,
|
||||
const uploadImages = uploadImagesCreator(process.env.BUCKET);
|
||||
const uploadImagesPreview = uploadImagesCreator(process.env.BUCKET_PREVIEW);
|
||||
|
||||
const copyImageSources = () => src(imageSources, { base: './server', encoding: false })
|
||||
.pipe(dest('./dist'));
|
||||
|
||||
const invalidateCreator = (distributionId) => () => cloudfront.send(new CreateInvalidationCommand({
|
||||
DistributionId: distributionId,
|
||||
InvalidationBatch: {
|
||||
CallerReference: (new Date()).toLocaleString(),
|
||||
Paths: {
|
||||
@@ -178,21 +192,26 @@ const invalidate = () => cloudfront.send(new CreateInvalidationCommand({
|
||||
},
|
||||
}));
|
||||
|
||||
const invalidate = invalidateCreator(process.env.DISTRIBUTION_ID);
|
||||
const invalidatePreview = invalidateCreator(process.env.DISTRIBUTION_ID_PREVIEW);
|
||||
|
||||
const buildPlaylist = async () => {
|
||||
const availableFiles = await reader();
|
||||
const playlist = { availableFiles };
|
||||
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
|
||||
};
|
||||
|
||||
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles, buildPlaylist));
|
||||
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyImageSources, buildPlaylist));
|
||||
|
||||
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
|
||||
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
|
||||
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
|
||||
const stageFrontend = series(buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
||||
|
||||
export default publishFrontend;
|
||||
|
||||
export {
|
||||
buildDist,
|
||||
invalidate,
|
||||
stageFrontend,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import updateVendor from './gulp/update-vendor.mjs';
|
||||
import publishFrontend, { buildDist, invalidate } from './gulp/publish-frontend.mjs';
|
||||
import publishFrontend, { buildDist, invalidate, stageFrontend } from './gulp/publish-frontend.mjs';
|
||||
|
||||
export {
|
||||
updateVendor,
|
||||
publishFrontend,
|
||||
buildDist,
|
||||
invalidate,
|
||||
stageFrontend,
|
||||
};
|
||||
|
||||
14
index.mjs
@@ -1,9 +1,6 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import fs from 'fs';
|
||||
import corsPassThru from './cors/index.mjs';
|
||||
import radarPassThru from './cors/radar.mjs';
|
||||
import outlookPassThru from './cors/outlook.mjs';
|
||||
import playlist from './src/playlist.mjs';
|
||||
import OVERRIDES from './src/overrides.mjs';
|
||||
|
||||
@@ -13,12 +10,6 @@ const port = process.env.WS4KP_PORT ?? 8080;
|
||||
// template engine
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// cors pass-thru to api.weather.gov
|
||||
app.get('/stations/*station', corsPassThru);
|
||||
app.get('/Conus/*radar', radarPassThru);
|
||||
app.get('/products/*product', outlookPassThru);
|
||||
app.get('/playlist.json', playlist);
|
||||
|
||||
// version
|
||||
const { version } = JSON.parse(fs.readFileSync('package.json'));
|
||||
|
||||
@@ -81,8 +72,6 @@ const geoip = (req, res) => {
|
||||
// debugging
|
||||
if (process.env?.DIST === '1') {
|
||||
// distribution
|
||||
app.use('/images', express.static('./server/images'));
|
||||
app.use('/fonts', express.static('./server/fonts'));
|
||||
app.use('/scripts', express.static('./server/scripts'));
|
||||
app.use('/geoip', geoip);
|
||||
app.use('/', express.static('./dist'));
|
||||
@@ -90,8 +79,11 @@ if (process.env?.DIST === '1') {
|
||||
// debugging
|
||||
app.get('/index.html', index);
|
||||
app.use('/geoip', geoip);
|
||||
app.use('/resources', express.static('./server/scripts/modules'));
|
||||
app.get('/', index);
|
||||
app.get('*name', express.static('./server'));
|
||||
// cors pass-thru to api.weather.gov
|
||||
app.get('/playlist.json', playlist);
|
||||
}
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
|
||||
26
nginx.conf
Normal file
@@ -0,0 +1,26 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
include mime.types;
|
||||
types {
|
||||
text/javascript mjs;
|
||||
}
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
add_header X-Weatherstar true always;
|
||||
|
||||
location / {
|
||||
index redirect.html index.html index.htm;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location /music/ {
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
1380
package-lock.json
generated
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.21.14",
|
||||
"version": "5.27.1",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -23,15 +23,18 @@
|
||||
"homepage": "https://github.com/netbymatt/ws4kp#readme",
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-cloudfront": "^3.609.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"ajv": "^8.17.1",
|
||||
"del": "^8.0.0",
|
||||
"eslint": "^8.2.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-plugin-import": "^2.10.0",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-awspublish": "^8.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-file": "^0.4.0",
|
||||
"gulp-html-minifier-terser": "^7.1.0",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-s3-uploader": "^1.0.6",
|
||||
"gulp-sass": "^6.0.0",
|
||||
@@ -42,11 +45,11 @@
|
||||
"suncalc": "^1.8.0",
|
||||
"swiped-events": "^1.1.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack-stream": "^7.0.0",
|
||||
"gulp-html-minifier-terser": "^7.1.0"
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-stream": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.5.0",
|
||||
"dotenv": "^17.0.1",
|
||||
"ejs": "^3.1.5",
|
||||
"express": "^5.1.0"
|
||||
}
|
||||
|
||||
BIN
server/images/gimp/Radar Basemap5.xcf
Normal file
BIN
server/images/maps/radar-stretched-overlay.webp
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
server/images/maps/radar-stretched.webp
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
server/images/maps/radar-tiles/00-00.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
server/images/maps/radar-tiles/00-01.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
server/images/maps/radar-tiles/00-02.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
server/images/maps/radar-tiles/00-03.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
server/images/maps/radar-tiles/00-04.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
server/images/maps/radar-tiles/00-05.webp
Normal file
|
After Width: | Height: | Size: 464 B |
BIN
server/images/maps/radar-tiles/00-06.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
server/images/maps/radar-tiles/00-07.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
server/images/maps/radar-tiles/00-08.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
server/images/maps/radar-tiles/00-09.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
server/images/maps/radar-tiles/01-00.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
server/images/maps/radar-tiles/01-01.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
server/images/maps/radar-tiles/01-02.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
server/images/maps/radar-tiles/01-03.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
server/images/maps/radar-tiles/01-04.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
server/images/maps/radar-tiles/01-05.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
server/images/maps/radar-tiles/01-06.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
server/images/maps/radar-tiles/01-07.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
server/images/maps/radar-tiles/01-08.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
server/images/maps/radar-tiles/01-09.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
server/images/maps/radar-tiles/02-00.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
server/images/maps/radar-tiles/02-01.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
server/images/maps/radar-tiles/02-02.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
server/images/maps/radar-tiles/02-03.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
server/images/maps/radar-tiles/02-04.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
server/images/maps/radar-tiles/02-05.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
server/images/maps/radar-tiles/02-06.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
server/images/maps/radar-tiles/02-07.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
server/images/maps/radar-tiles/02-08.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
server/images/maps/radar-tiles/02-09.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
server/images/maps/radar-tiles/03-00.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
server/images/maps/radar-tiles/03-01.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
server/images/maps/radar-tiles/03-02.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
server/images/maps/radar-tiles/03-03.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
server/images/maps/radar-tiles/03-04.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
server/images/maps/radar-tiles/03-05.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
server/images/maps/radar-tiles/03-06.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
server/images/maps/radar-tiles/03-07.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
server/images/maps/radar-tiles/03-08.webp
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
server/images/maps/radar-tiles/03-09.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
server/images/maps/radar-tiles/04-00.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
server/images/maps/radar-tiles/04-01.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
server/images/maps/radar-tiles/04-02.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
server/images/maps/radar-tiles/04-03.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
server/images/maps/radar-tiles/04-04.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
server/images/maps/radar-tiles/04-05.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
server/images/maps/radar-tiles/04-06.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
server/images/maps/radar-tiles/04-07.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
server/images/maps/radar-tiles/04-08.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
server/images/maps/radar-tiles/04-09.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
server/images/maps/radar-tiles/05-00.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
server/images/maps/radar-tiles/05-01.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
server/images/maps/radar-tiles/05-02.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
server/images/maps/radar-tiles/05-03.webp
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
server/images/maps/radar-tiles/05-04.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
server/images/maps/radar-tiles/05-05.webp
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
server/images/maps/radar-tiles/05-06.webp
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
server/images/maps/radar-tiles/05-07.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
server/images/maps/radar-tiles/05-08.webp
Normal file
|
After Width: | Height: | Size: 462 B |
BIN
server/images/maps/radar-tiles/05-09.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
server/images/maps/radar-tiles/06-00.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
server/images/maps/radar-tiles/06-01.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
server/images/maps/radar-tiles/06-02.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
server/images/maps/radar-tiles/06-03.webp
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
server/images/maps/radar-tiles/06-04.webp
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
server/images/maps/radar-tiles/06-05.webp
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
server/images/maps/radar-tiles/06-06.webp
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
server/images/maps/radar-tiles/06-07.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
server/images/maps/radar-tiles/06-08.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
server/images/maps/radar-tiles/06-09.webp
Normal file
|
After Width: | Height: | Size: 482 B |
BIN
server/images/maps/radar-tiles/07-00.webp
Normal file
|
After Width: | Height: | Size: 604 B |
BIN
server/images/maps/radar-tiles/07-01.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
server/images/maps/radar-tiles/07-02.webp
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
server/images/maps/radar-tiles/07-03.webp
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
server/images/maps/radar-tiles/07-04.webp
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
server/images/maps/radar-tiles/07-05.webp
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
server/images/maps/radar-tiles/07-06.webp
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
server/images/maps/radar-tiles/07-07.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
server/images/maps/radar-tiles/07-08.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
server/images/maps/radar-tiles/07-09.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |