Compare commits

...

29 Commits

Author SHA1 Message Date
Matt Walsh
7900e59aab 6.2.7 2025-11-05 05:24:14 +00:00
Matt Walsh
9b422dd697 expose more data for scroll messages 2025-11-05 05:24:04 +00:00
Matt Walsh
e4ce0b6cc6 update bug template 2025-11-04 05:49:10 +00:00
Matt Walsh
b0e5018179 6.2.6 2025-10-22 00:22:44 +00:00
Matt Walsh
6422589b5c fix wind speed on hourly close #157 2025-10-22 00:22:29 +00:00
Matt Walsh
407da90f8a 6.2.5 2025-10-21 04:15:53 +00:00
Matt Walsh
3a0e6aa345 Merge branch 'geolookup-latlongquery' 2025-10-21 04:15:38 +00:00
Matt Walsh
650dda7b61 allow for latLon only in query string #154 2025-10-21 04:12:21 +00:00
Matt Walsh
8f1e8ffb74 Merge branch 'rmitchellscott-static-redirect-index' 2025-10-21 03:26:25 +00:00
Mitchell Scott
93af84cbd8 fix: geolookup if only latLonQuery is provided 2025-10-20 13:37:18 -06:00
Mitchell Scott
117f66e9d0 fix(static builds): duplicate query params 2025-10-20 13:28:45 -06:00
Mitchell Scott
bca9376edc fix: nginx query parameter redirect works like node.js 2025-10-20 11:42:42 -06:00
Matt Walsh
8b076db25d 6.2.4 2025-10-17 00:51:09 +00:00
Matt Walsh
807932fe3c Merge branch 'ios-regex' close #137 2025-10-17 00:49:59 +00:00
Matt Walsh
7bb024eff5 6.2.3 2025-10-17 00:36:14 +00:00
Matt Walsh
f4a1a3a1d8 add hazards before custom scroll options close #149 2025-10-17 00:35:26 +00:00
Matt Walsh
9a5efe9d48 update dependencies 2025-10-17 00:14:59 +00:00
Matt Walsh
58e0611a46 6.2.2 2025-10-16 19:00:30 -05:00
Matt Walsh
9ed496c892 better formatting for headend info 2025-10-16 19:00:20 -05:00
Matt Walsh
31315d1ace add com.chrome.devtools.json 2025-10-16 18:36:41 -05:00
Matt Walsh
77838e1a81 use locally stored weather parameters in spc outlook close #150 2025-10-15 00:29:23 +00:00
Matt Walsh
64d6484bd8 Merge pull request #151 from bparkin1283/patch-1
Update README.md clarifying displays if you're within one of the high…
2025-10-09 11:17:13 -05:00
bparkin1283
20cab8c25e Update README.md clarifying displays if you're within one of the highlight areas 2025-10-09 10:55:33 -05:00
Matt Walsh
b4de17ccd0 update dependencies 2025-10-02 21:50:28 -05:00
Matt Walsh
0fd90feb7a update community notes 2025-10-02 21:37:36 -05:00
Matt Walsh
8c3b596b69 add build script for travel cities #146 2025-10-02 21:26:45 -05:00
Matt Walsh
e57b9bcb20 6.2.1 2025-09-24 22:33:59 -05:00
Matt Walsh
e27750e915 fix load order on scroll when compiled 2025-09-24 22:33:47 -05:00
Matt Walsh
14b1891efd direct check of regex lookbehind capability 2025-09-11 08:47:16 -05:00
25 changed files with 1068 additions and 3326 deletions

View File

@@ -12,3 +12,4 @@ Please do not report issues with api.weather.gov being down. It's a new service
Please include:
* Web browser and OS
* Headend Information text block from the very bottom of the web page
* How you're running Weatherstar (Node, Dockerfile, Dockerfile.server, etc.)

View File

@@ -179,7 +179,7 @@ I've made several changes to this Weather Star 4000 simulation compared to the o
* Radar displays the timestamp of the image.
* 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 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. SPC outlook only displays if you're within one of the highlight areas over the next 3 day. You can view the [maps](https://www.weather.gov/crh/outlooks) and pick a location within one of the risk categories to see if the screen is working for you.
* 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.
@@ -321,6 +321,7 @@ Thanks to the WeatherStar+ community for providing these discussions to further
* [ws4channels](https://github.com/rice9797/ws4channels) A Dockerized Node.js application to stream WeatherStar 4000 data into Channels DVR using Puppeteer and FFmpeg.
* [SSL Certificates](https://github.com/netbymatt/ws4kp/issues/135) Discussion about how to host with an SSL certificate (enables geolocation).
* [Changing playlists](https://github.com/netbymatt/ws4kp/issues/138) Possible ways to automatically change the playlist on a schedule.
* [Customize Travel Forecast Cities](https://github.com/netbymatt/ws4kp/issues/146#issuecomment-3363940202)
## Customization

View File

@@ -84,8 +84,8 @@
"Latitude": 29.7633,
"Longitude": -95.3633,
"point": {
"x": 65,
"y": 97,
"x": 63,
"y": 95,
"wfo": "HGX"
}
},

View File

@@ -5,8 +5,8 @@ import rename from 'gulp-rename';
const clean = () => deleteAsync(['./server/scripts/vendor/auto/**']);
const vendorFiles = [
'./node_modules/luxon/build/es6/luxon.js',
'./node_modules/luxon/build/es6/luxon.js.map',
'./node_modules/luxon/build/es6/luxon.mjs',
'./node_modules/luxon/build/es6/luxon.mjs.map',
'./node_modules/nosleep.js/dist/NoSleep.js',
'./node_modules/suncalc/suncalc.js',
'./node_modules/swiped-events/src/swiped-events.js',
@@ -23,7 +23,6 @@ const copy = () => src(vendorFiles)
path.dirname = path.dirname.toLowerCase();
path.basename = path.basename.toLowerCase();
path.extname = path.extname.toLowerCase();
if (path.basename === 'luxon') path.extname = '.mjs';
}))
.pipe(dest('./server/scripts/vendor/auto'));

View File

@@ -8,6 +8,7 @@ import {
import playlist from './src/playlist.mjs';
import OVERRIDES from './src/overrides.mjs';
import cache from './proxy/cache.mjs';
import devTools from './src/com.chrome.devtools.mjs';
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
@@ -168,6 +169,7 @@ if (process.env?.DIST === '1') {
app.use('/geoip', geoip);
app.use('/resources', express.static('./server/scripts/modules'));
app.get('/', index);
app.get('/.well-known/appspecific/com.chrome.devtools.json', devTools);
app.get('*name', express.static('./server', staticOptions));
}

View File

@@ -10,8 +10,10 @@ server {
add_header X-Weatherstar true always;
include /etc/nginx/includes/wsqs_redirect.conf;
location / {
index redirect.html index.html index.htm;
index index.html index.htm;
try_files $uri $uri/ =404;
}

4150
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "6.2.0",
"version": "6.2.7",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs",
"type": "module",
@@ -8,6 +8,7 @@
"start": "node index.mjs",
"stop": "pkill -f 'node index.mjs' || echo 'No process found'",
"test": "echo \"Error: no test specified\" && exit 1",
"build:travelcities": "node datagenerators/travelcities.mjs",
"build:css": "sass --style=compressed ./server/styles/scss/main.scss ./server/styles/main.css",
"build": "gulp buildDist",
"lint": "eslint ./server/scripts/**/*.mjs ./proxy/**/*.mjs ./src/**/*.mjs *.mjs",
@@ -50,13 +51,12 @@
"swiped-events": "^1.1.4",
"terser-webpack-plugin": "^5.3.6",
"webpack": "^5.99.9",
"webpack-stream": "^7.0.0"
"webpack-stream": "^7.0.0",
"metar-taf-parser": "^9.0.0"
},
"dependencies": {
"dotenv": "^17.0.1",
"ejs": "^3.1.5",
"express": "^5.1.0",
"metar-taf-parser": "^9.0.0",
"npm": "^11.6.0"
"express": "^5.1.0"
}
}

View File

@@ -106,17 +106,34 @@ const init = async () => {
// attempt to parse the url parameters
const parsedParameters = parseQueryString();
const loadFromParsed = parsedParameters.latLonQuery && parsedParameters.latLon;
const loadFromParsed = !!parsedParameters.latLon;
// Auto load the parsed parameters and fall back to the previous query
const query = parsedParameters.latLonQuery ?? localStorage.getItem('latLonQuery');
const latLon = parsedParameters.latLon ?? localStorage.getItem('latLon');
const fromGPS = localStorage.getItem('latLonFromGPS') && !loadFromParsed;
if (query && latLon && !fromGPS) {
if (parsedParameters.latLonQuery && !parsedParameters.latLon) {
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
txtAddress.value = query;
loadData(JSON.parse(latLon));
txtAddress.value = parsedParameters.latLonQuery;
const geometry = await geocodeLatLonQuery(parsedParameters.latLonQuery);
if (geometry) {
doRedirectToGeometry(geometry);
}
} else if (latLon && !fromGPS) {
// update in-page search box if using cached data, or parsed parameter
if ((query && !loadFromParsed) || (parsedParameters.latLonQuery && loadFromParsed)) {
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
txtAddress.value = query;
}
// use lat-long lookup if that's all that was provided in the query string
if (loadFromParsed && parsedParameters.latLon && !parsedParameters.latLonQuery) {
const { lat, lon } = JSON.parse(latLon);
getForecastFromLatLon(lat, lon, true);
} else {
// otherwise use pre-stored data
loadData(JSON.parse(latLon));
}
}
if (fromGPS) {
btnGetGpsClick();
@@ -162,6 +179,26 @@ const init = async () => {
document.querySelector('#container').addEventListener('swiped-right', () => swipeCallBack('right'));
};
const geocodeLatLonQuery = async (query) => {
try {
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
data: {
text: query,
f: 'json',
},
});
const loc = data.locations?.[0];
if (loc) {
return loc.feature.geometry;
}
return null;
} catch (error) {
console.error('Geocoding failed:', error);
return null;
}
};
const autocompleteOnSelect = async (suggestion) => {
// Note: it's fine that this uses json instead of safeJson since it's infrequent and user-initiated
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {

View File

@@ -231,7 +231,7 @@ class CurrentWeather extends WeatherDisplay {
this.setAutoReload();
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
return new Promise((resolve) => {
if (this.data) resolve(this.data);
if (this.data) resolve({ data: this.data, parameters: this.weatherParameters });
// data not available, put it into the data callback queue
this.getDataCallbacks.push(() => resolve(this.data));
});

View File

@@ -12,9 +12,14 @@ const secondsToTicks = (seconds) => Math.ceil((seconds * 1000) / TICK_INTERVAL_M
const DEFAULT_UPDATE = secondsToTicks(4.0); // 4 second default for each current conditions
// items on page
const mainScroll = document.querySelector('#container>.scroll');
const fixedScroll = document.querySelector('#container>.scroll .fixed');
const header = document.querySelector('#container>.scroll .scroll-header');
let mainScroll;
let fixedScroll;
let header;
document.addEventListener('DOMContentLoaded', () => {
mainScroll = document.querySelector('#container>.scroll');
fixedScroll = document.querySelector('#container>.scroll .fixed');
header = document.querySelector('#container>.scroll .scroll-header');
});
// local variables
let interval;
@@ -79,7 +84,7 @@ const incrementInterval = (force) => {
const drawScreen = async () => {
// get the conditions
const data = await getCurrentWeather();
const { data, parameters } = await getCurrentWeather();
// create a data object (empty if no valid current weather conditions)
const scrollData = data || {};
@@ -95,7 +100,7 @@ const drawScreen = async () => {
// if we have no current weather and no hazards, there's nothing to display
if (!data && (!scrollData.hazards || scrollData.hazards.length === 0)) return;
const thisScreen = workingScreens[screenIndex](scrollData);
const thisScreen = workingScreens[screenIndex](scrollData, parameters);
// update classes on the scroll area
mainScroll.classList.forEach((cls) => { if (cls !== 'scroll') mainScroll.classList.remove(cls); });
@@ -293,4 +298,5 @@ export {
hide,
screenCount,
atDefault,
hazards,
};

View File

@@ -1,5 +1,5 @@
import Setting from './utils/setting.mjs';
import { reset as resetScroll, addScreen as addScroll } from './currentweatherscroll.mjs';
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
import { json } from './utils/fetch.mjs';
let firstRun = true;
@@ -42,8 +42,9 @@ const parseFeed = (textInput) => {
return;
}
// add single text scroll
// add single text scroll after hazards if present
resetScroll();
addScroll(hazards);
addScroll(
() => (
{
@@ -81,6 +82,8 @@ const getFeed = async (url) => {
// reset the scroll, then add the screens
resetScroll();
// add the hazards scroll first
addScroll(hazards);
titles.forEach((title) => {
// data is provided to the screen handler, so we return a function
addScroll(

View File

@@ -3,7 +3,7 @@
import STATUS from './status.mjs';
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
import { safeJson } from './utils/fetch.mjs';
import { temperature as temperatureUnit, distanceKilometers } from './utils/units.mjs';
import { temperature as temperatureUnit, windSpeed as windUnit } from './utils/units.mjs';
import { getHourlyIcon } from './icons.mjs';
import { directionToNSEW } from './utils/calc.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
@@ -191,7 +191,7 @@ class Hourly extends WeatherDisplay {
const parseForecast = async (data) => {
// get unit converters
const temperatureConverter = temperatureUnit();
const distanceConverter = distanceKilometers();
const windConverter = windUnit();
// parse data
const temperature = expand(data.temperature.values);
@@ -210,8 +210,8 @@ const parseForecast = async (data) => {
temperature: temperatureConverter(temperature[idx]),
temperatureUnit: temperatureConverter.units,
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
windSpeed: distanceConverter(windSpeed[idx]),
windUnit: distanceConverter.units,
windSpeed: windConverter(windSpeed[idx]),
windUnit: windConverter.units,
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],

View File

@@ -109,6 +109,7 @@ const getWeather = async (latLon, haveDataCallback) => {
weatherParameters.forecast = point.properties.forecast;
weatherParameters.forecastGridData = point.properties.forecastGridData;
weatherParameters.stations = stations.features;
weatherParameters.relativeLocation = point.properties.relativeLocation.properties;
// update the main process for display purposes
populateWeatherParameters(weatherParameters, point.properties);

View File

@@ -57,6 +57,7 @@ class SpcOutlook extends WeatherDisplay {
}
async getData(weatherParameters, refresh) {
if (weatherParameters) this.weatherParameters = weatherParameters;
if (!super.getData(weatherParameters, refresh)) return;
// SPC outlook data does not need to be reloaded on a location change, only during silent refresh
@@ -93,7 +94,7 @@ class SpcOutlook extends WeatherDisplay {
}
}
// parse the data
this.data = testAllPoints([weatherParameters.longitude, weatherParameters.latitude], this.rawOutlookData);
this.data = testAllPoints([this.weatherParameters.longitude, this.weatherParameters.latitude], this.rawOutlookData);
// check if there's a "risk" for any of the three days, otherwise skip the SPC Outlook screen
if (this.data.reduce((prev, cur) => prev || !!cur, false)) {

View File

@@ -5,21 +5,22 @@ import en from '../../vendor/auto/locale/en.js';
// metar-taf-parser requires regex lookbehind
// this does not work in iOS < 16.4
// this is a detection algorithm for iOS versions
const isIos = /iP(ad|od|hone)/i.test(window.navigator.userAgent);
let iosVersionOk = false;
if (isIos) {
// regex match the version string
const iosVersionRaw = /OS (\d+)_(\d+)/.exec(window.navigator.userAgent);
// check for match
if (iosVersionRaw) {
// break into parts
const iosVersionMajor = parseInt(iosVersionRaw[1], 10);
const iosVersionMinor = parseInt(iosVersionRaw[2], 10);
if (iosVersionMajor > 16) iosVersionOk = true;
if (iosVersionMajor === 16 && iosVersionMinor >= 4) iosVersionOk = true;
// this is a detection algorithm for missing lookbehind support
const supportsRegexLookAheadLookBehindCheck = () => {
try {
return (
// deliberately using RegExp for broader browser support during check
/* eslint-disable prefer-regex-literals */
'hibyehihi'
.replace(new RegExp('(?<=hi)hi', 'g'), 'hello')
.replace(new RegExp('hi(?!bye)', 'g'), 'hey') === 'hibyeheyhello'
/* eslint-enable prefer-regex-literals */
);
} catch {
return false;
}
}
};
const supportsRegexLookAheadLookBehind = supportsRegexLookAheadLookBehindCheck();
/**
* Augment observation data by parsing METAR when API fields are missing
@@ -27,8 +28,8 @@ if (isIos) {
* @returns {Object} - Augmented observation with parsed METAR data filled in
*/
const augmentObservationWithMetar = (observation) => {
// check for a metar message and for unusable ios versions
if (!observation?.rawMessage || (isIos && !iosVersionOk)) {
// check for a metar message and for regex lookbehind support
if (!observation?.rawMessage || (!supportsRegexLookAheadLookBehind)) {
return observation;
}

File diff suppressed because one or more lines are too long

View File

@@ -8127,7 +8127,7 @@ function friendlyDateTime(dateTimeish) {
}
}
const VERSION = "3.7.1";
const VERSION = "3.7.2";
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
//# sourceMappingURL=luxon.js.map
//# sourceMappingURL=luxon.mjs.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
import path from 'path';
// get values for devtools json
const uuid = 'd2bd1130-560f-4c8e-b2c5-e91073784964';
const root = path.resolve('server');
const DEVTOOLS_CONFIG = {
workspace: {
uuid,
root,
},
};
const devTools = (req, res) => {
// test for localhost
if (['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.ip)) {
console.log(DEVTOOLS_CONFIG);
res.json(DEVTOOLS_CONFIG);
} else {
// not localhost
res.status(404).send('File not found');
}
};
export default devTools;

View File

@@ -12,6 +12,9 @@ url_encode() {
# build query string from WSQS_ env vars
while IFS='=' read -r key val; do
# Skip empty lines
[ -z "$key" ] && continue
# Remove WSQS_ prefix and convert underscores to hyphens
key="${key#WSQS_}"
key="${key//_/-}"
@@ -23,11 +26,16 @@ while IFS='=' read -r key val; do
QS="${key}=${encoded_val}"
fi
done << EOF
$(env | grep '^WSQS_')
$(env | grep '^WSQS_' || true)
EOF
mkdir -p /etc/nginx/includes
if [ -n "$QS" ]; then
# Escape the query string for use in JavaScript (escape backslashes and single quotes)
QS_ESCAPED=$(printf '%s' "$QS" | sed "s/\\\\/\\\\\\\\/g; s/'/\\\'/g")
# Generate redirect.html with JavaScript logic
cat > "$ROOT/redirect.html" <<EOF
<!DOCTYPE html>
<html>
@@ -35,10 +43,36 @@ if [ -n "$QS" ]; then
<meta charset="utf-8" />
<title>Redirecting</title>
<meta http-equiv="refresh" content="0;url=/index.html?$QS" />
<script>
(function() {
var wsqsParams = '$QS_ESCAPED';
var currentParams = window.location.search.substring(1);
var targetParams = currentParams || wsqsParams;
window.location.replace('/index.html?' + targetParams);
})();
</script>
</head>
<body></body>
</html>
EOF
# Generate nginx config for conditional redirects
cat > /etc/nginx/includes/wsqs_redirect.conf <<'EOF'
location = / {
if ($args = '') {
rewrite ^ /redirect.html last;
}
rewrite ^/$ /index.html?$args? redirect;
}
location = /index.html {
if ($args = '') {
rewrite ^ /redirect.html last;
}
}
EOF
else
touch /etc/nginx/includes/wsqs_redirect.conf
fi
exec nginx -g 'daemon off;'

View File

@@ -188,14 +188,22 @@
<div class='heading'>Headend Information</div>
<div id="divInfo">
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
Station Id: <span id="spanStationId"></span><br />
Radar Id: <span id="spanRadarId"></span><br />
Zone Id: <span id="spanZoneId"></span><br />
Office Id: <span id="spanOfficeId"></span><br />
Grid X,Y: <span id="spanGridPoint"></span><br />
Music: <span id="musicTrack">Not playing</span><br />
Ws4kp Version: <span><%- version %></span>
<div class="header">Location:</div>
<div class="header"><span id="spanCity"></span> <span id="spanState"></span></div>
<div class="header">Station Id:</div>
<div class="header"><span id="spanStationId"></span></div>
<div class="header">Radar Id:</div>
<div class="header"><span id="spanRadarId"></span></div>
<div class="header">Zone Id:</div>
<div class="header"><span id="spanZoneId"></span></div>
<div class="header">Office Id:</div>
<div class="header"><span id="spanOfficeId"></span></div>
<div class="header">Grid X,Y:</div>
<div class="header"><span id="spanGridPoint"></span></div>
<div class="header">Music:</div>
<div class="header"><span id="musicTrack">Not playing</span></div>
<div class="header">Ws4kp Version:</div>
<div class="header"><span><%- version %></span></div>
</div>
</div>

View File

@@ -73,7 +73,10 @@
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"cSpell.words": [
"hibyehihi"
]
},
"extensions": {
"recommendations": [