Compare commits

..

16 Commits

Author SHA1 Message Date
Matt Walsh
392b339727 5.23.7 2025-06-12 20:53:28 -05:00
Matt Walsh
852eff8de6 add basic volume control #109 2025-06-12 20:53:23 -05:00
Matt Walsh
c74a15c40c 5.23.6 2025-06-12 13:17:26 -05:00
Matt Walsh
5419425834 fix for large font linux/win differences in extended forecast 2025-06-12 13:16:55 -05:00
Matt Walsh
f3a386079b fix for file didn't load in spc outlook 2025-06-12 13:13:19 -05:00
Matt Walsh
aa43713943 5.23.5 2025-06-12 12:15:10 -05:00
Matt Walsh
1dece10679 cache-busting for radar worker 2025-06-12 12:14:56 -05:00
Matt Walsh
c4f16d786a Merge remote-tracking branch 'origin/static-host' 2025-06-12 12:07:55 -05:00
Matt Walsh
36b8adc019 5.23.4 2025-06-12 09:30:54 -05:00
Matt Walsh
bfe0b4757d Merge branch 'main' of github.com:netbymatt/ws4kp 2025-06-12 09:30:38 -05:00
Matt Walsh
2b61e55783 get current conditions direct from api 2025-06-12 09:30:31 -05:00
Matt Walsh
36c4f451b3 Merge pull request #107 from rmitchellscott/fix-push-on-tag
chore(docker): fix pushing on tag
2025-06-09 15:12:38 -05:00
Mitchell Scott
268d4ae7fa chore(docker): fix pushing on tag 2025-06-07 11:46:00 -06:00
Matt Walsh
1b49e02cd8 5.23.3 2025-06-06 16:40:14 -05:00
Matt Walsh
9a55a6ec39 fix setting boolean-style query string parsing close #106 2025-06-06 16:40:08 -05:00
Matt Walsh
974a061b44 clean up paths for server and server-dist, remove cors #96 2025-06-02 20:59:35 -05:00
19 changed files with 74 additions and 162 deletions

View File

@@ -49,7 +49,7 @@ jobs:
with: with:
context: . context: .
pull: true pull: true
push: ${{ github.ref == 'refs/heads/main' }} push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -139,6 +139,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. 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 implemenation) 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 ## Community Notes
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts! Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -96,6 +96,19 @@ const buildJs = () => src(mjsSources)
.pipe(webpack(webpackOptions)) .pipe(webpack(webpackOptions))
.pipe(dest(RESOURCES_PATH)); .pipe(dest(RESOURCES_PATH));
const workerSources = [
'server/scripts/modules/radar-worker.mjs',
];
const buildWorkers = () => {
// update the file name in the webpack options
const output = { filename: 'radar-worker.mjs' };
const workerWebpackOptions = { ...webpackOptions, output };
return src(workerSources)
.pipe(webpack(workerWebpackOptions))
.pipe(dest(RESOURCES_PATH));
};
const cssSources = [ const cssSources = [
'server/styles/main.css', 'server/styles/main.css',
]; ];
@@ -137,6 +150,8 @@ const s3 = s3Upload({
const uploadSources = [ const uploadSources = [
'dist/**', 'dist/**',
'!dist/**/*.map', '!dist/**/*.map',
'!dist/images/**/*',
'!dist/fonts/**/*',
]; ];
const upload = () => src(uploadSources, { base: './dist', encoding: false }) const upload = () => src(uploadSources, { base: './dist', encoding: false })
.pipe(s3({ .pipe(s3({
@@ -167,6 +182,9 @@ const uploadImages = () => src(imageSources, { base: './server', encoding: false
}), }),
); );
const copyImageSources = () => src(imageSources, { base: './server', encoding: false })
.pipe(dest('./dist'));
const invalidate = () => cloudfront.send(new CreateInvalidationCommand({ const invalidate = () => cloudfront.send(new CreateInvalidationCommand({
DistributionId: process.env.DISTRIBUTION_ID, DistributionId: process.env.DISTRIBUTION_ID,
InvalidationBatch: { InvalidationBatch: {
@@ -184,7 +202,7 @@ const buildPlaylist = async () => {
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist')); 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, buildWorkers, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyImageSources, buildPlaylist));
// upload_images could be in parallel with upload, but _images logs a lot and has little changes // 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 // by running upload last the majority of the changes will be at the bottom of the log for easy viewing

View File

@@ -1,9 +1,6 @@
import 'dotenv/config'; import 'dotenv/config';
import express from 'express'; import express from 'express';
import fs from 'fs'; 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 playlist from './src/playlist.mjs';
import OVERRIDES from './src/overrides.mjs'; import OVERRIDES from './src/overrides.mjs';
@@ -13,12 +10,6 @@ const port = process.env.WS4KP_PORT ?? 8080;
// template engine // template engine
app.set('view engine', 'ejs'); 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 // version
const { version } = JSON.parse(fs.readFileSync('package.json')); const { version } = JSON.parse(fs.readFileSync('package.json'));
@@ -81,8 +72,6 @@ const geoip = (req, res) => {
// debugging // debugging
if (process.env?.DIST === '1') { if (process.env?.DIST === '1') {
// distribution // distribution
app.use('/images', express.static('./server/images'));
app.use('/fonts', express.static('./server/fonts'));
app.use('/scripts', express.static('./server/scripts')); app.use('/scripts', express.static('./server/scripts'));
app.use('/geoip', geoip); app.use('/geoip', geoip);
app.use('/', express.static('./dist')); app.use('/', express.static('./dist'));
@@ -90,8 +79,11 @@ if (process.env?.DIST === '1') {
// debugging // debugging
app.get('/index.html', index); app.get('/index.html', index);
app.use('/geoip', geoip); app.use('/geoip', geoip);
app.use('/resources', express.static('./server/scripts/modules'));
app.get('/', index); app.get('/', index);
app.get('*name', express.static('./server')); app.get('*name', express.static('./server'));
// cors pass-thru to api.weather.gov
app.get('/playlist.json', playlist);
} }
const server = app.listen(port, () => { const server = app.listen(port, () => {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "5.23.2", "version": "5.23.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ws4kp", "name": "ws4kp",
"version": "5.23.2", "version": "5.23.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dotenv": "^16.5.0", "dotenv": "^16.5.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "5.23.2", "version": "5.23.7",
"description": "Welcome to the WeatherStar 4000+ project page!", "description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View File

@@ -42,7 +42,6 @@ class CurrentWeather extends WeatherDisplay {
// station observations // station observations
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
observations = await json(`${station.id}/observations`, { observations = await json(`${station.id}/observations`, {
cors: true,
data: { data: {
limit: 2, limit: 2,
}, },

View File

@@ -47,6 +47,9 @@ const enableMediaPlayer = () => {
if (mediaPlaying.value === true) { if (mediaPlaying.value === true) {
startMedia(); startMedia();
} }
// add the volume control to the page
const settingsSection = document.querySelector('#settings');
settingsSection.append(mediaVolume.generate());
} }
}; };
@@ -123,6 +126,25 @@ const randomizePlaylist = () => {
playlist.availableFiles = randomPlaylist; playlist.availableFiles = randomPlaylist;
}; };
const setVolume = (newVolume) => {
if (player) {
player.volume = newVolume;
}
};
const mediaVolume = new Setting('mediaVolume', {
name: 'Volume',
type: 'select',
defaultValue: 0.75,
values: [
[1, '100%'],
[0.75, '75%'],
[0.50, '50%'],
[0.25, '25%'],
],
changeAction: setVolume,
});
const initializePlayer = () => { const initializePlayer = () => {
// basic sanity checks // basic sanity checks
if (!playlist.availableFiles || playlist?.availableFiles.length === 0) { if (!playlist.availableFiles || playlist?.availableFiles.length === 0) {
@@ -145,6 +167,7 @@ const initializePlayer = () => {
player.src = `music/${playlist.availableFiles[currentTrack]}`; player.src = `music/${playlist.availableFiles[currentTrack]}`;
setTrackName(playlist.availableFiles[currentTrack]); setTrackName(playlist.availableFiles[currentTrack]);
player.type = 'audio/mpeg'; player.type = 'audio/mpeg';
setVolume(mediaVolume.value);
}; };
const playerCanPlay = async () => { const playerCanPlay = async () => {

View File

@@ -92,7 +92,7 @@ const updateStatus = (value) => {
if (displays[0].status === STATUS.loading) return; if (displays[0].status === STATUS.loading) return;
// calculate first enabled display // calculate first enabled display
const firstDisplayIndex = displays.findIndex((display) => display.enabled && display.timing.totalScreens > 0); const firstDisplayIndex = displays.findIndex((display) => display?.enabled && display?.timing?.totalScreens > 0);
// value.id = 0 is hazards, if they fail to load hot-wire a new value.id to the current display to see if it needs to be loaded // value.id = 0 is hazards, if they fail to load hot-wire a new value.id to the current display to see if it needs to be loaded
// typically this plays out as current conditions loads, then hazards fails. // typically this plays out as current conditions loads, then hazards fails.

View File

@@ -14,7 +14,7 @@ class Progress extends WeatherDisplay {
// setup event listener for dom-required initialization // setup event listener for dom-required initialization
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
this.version = document.querySelector('#version').innerHTML; this.version = document.querySelector('#version').innerHTML.replace(/\s/g, '');
this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this)); this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this));
}); });
@@ -91,3 +91,10 @@ class Progress extends WeatherDisplay {
// register our own display // register our own display
const progress = new Progress(-1, 'progress'); const progress = new Progress(-1, 'progress');
registerProgress(progress); registerProgress(progress);
const version = () => progress.version;
export default progress;
export {
version,
};

View File

@@ -5,6 +5,7 @@ import { text } from './utils/fetch.mjs';
import WeatherDisplay from './weatherdisplay.mjs'; import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay, timeZone } from './navigation.mjs'; import { registerDisplay, timeZone } from './navigation.mjs';
import * as utils from './radar-utils.mjs'; import * as utils from './radar-utils.mjs';
import { version } from './progress.mjs';
// TEMPORARY fix to disable radar on ios safari. The same engine (webkit) is // TEMPORARY fix to disable radar on ios safari. The same engine (webkit) is
// used for all ios browers (chrome, brave, firefox, etc) so it's safe to skip // used for all ios browers (chrome, brave, firefox, etc) so it's safe to skip
@@ -87,7 +88,7 @@ class Radar extends WeatherDisplay {
const lists = (await Promise.all(baseUrls.map(async (url) => { const lists = (await Promise.all(baseUrls.map(async (url) => {
try { try {
// get a list of available radars // get a list of available radars
return text(url, { cors: true }); return text(url);
} catch (error) { } catch (error) {
console.log('Unable to get list of radars'); console.log('Unable to get list of radars');
console.error(error); console.error(error);
@@ -196,7 +197,7 @@ class Radar extends WeatherDisplay {
// create a radar worker with helper functions // create a radar worker with helper functions
const radarWorker = () => { const radarWorker = () => {
// create the worker // create the worker
const worker = new Worker(new URL('./radar-worker.mjs', import.meta.url), { type: 'module' }); const worker = new Worker(`/resources/radar-worker.mjs?_=${version()}`, { type: 'module' });
const processRadar = (url) => new Promise((resolve, reject) => { const processRadar = (url) => new Promise((resolve, reject) => {
// prepare for done message // prepare for done message

View File

@@ -18,6 +18,8 @@ const testAllPoints = (point, data) => {
data.forEach((day, index) => { data.forEach((day, index) => {
// initialize the result // initialize the result
result[index] = false; result[index] = false;
// if there's no data (file didn't load), exit early
if (day === undefined) return;
// loop through each category // loop through each category
day.features.forEach((feature) => { day.features.forEach((feature) => {
if (!feature.geometry.coordinates) return; if (!feature.geometry.coordinates) return;

View File

@@ -38,6 +38,9 @@ class Setting {
if (this.type === 'checkbox' && urlValue !== undefined) { if (this.type === 'checkbox' && urlValue !== undefined) {
urlState = urlValue === 'true'; urlState = urlValue === 'true';
} }
if (this.type === 'boolean' && urlValue !== undefined) {
urlState = urlValue === 'true';
}
if (this.type === 'select' && urlValue !== undefined) { if (this.type === 'select' && urlValue !== undefined) {
urlState = parseFloat(urlValue); urlState = parseFloat(urlValue);
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -30,7 +30,7 @@
.condition { .condition {
text-align: center; text-align: center;
height: 74px; height: 74px;
margin-top: 10px; margin-top: 5px;
} }
.icon { .icon {
@@ -44,7 +44,6 @@
.temperatures { .temperatures {
width: 100%; width: 100%;
margin-top: 5px;
.temperature-block { .temperature-block {
display: inline-block; display: inline-block;