Compare commits

..

5 Commits

Author SHA1 Message Date
Matt Walsh
107fa26da8 add daily rain and timestamp #132 2025-11-12 15:31:08 -06:00
Matt Walsh
afad953ed7 Merge branch 'main' into personal-weather 2025-11-12 15:16:42 -06:00
Matt Walsh
410880833b fix include location in build 2025-10-21 15:38:43 -05:00
Matt Walsh
539e7663d6 Format and populate personal weather data 2025-10-21 15:18:01 -05:00
Matt Walsh
5b5b313786 add personal-weather and rearrange pages 2025-10-21 03:08:48 +00:00
47 changed files with 1860 additions and 2213 deletions

View File

@@ -1,11 +0,0 @@
---
name: Screen Enhancement
about: Items and tasks related to the screen enhancement project
title: '[Project]: '
labels: screen-enhance
projects: ['netbymatt/5']
assignees: ''
---
Describe the task, how it affects the overall project and what is considered complete.

View File

@@ -4,8 +4,6 @@ on:
push:
branches:
- '**'
- '!screen-enhance'
- '!screen-enhance/**'
tags:
- 'v*.*.*'
- 'v*.*'

View File

@@ -2,7 +2,7 @@
"liveSassCompile.settings.formats": [
{
"format": "compressed",
"extensionName": ".min.css",
"extensionName": ".css",
"savePath": "/server/styles",
}
],
@@ -17,4 +17,4 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
}
}

View File

@@ -1,5 +1,3 @@
![Weatherstar 4000+ Current Conditions](https://github.com/netbymatt/ws4kp/blob/main/server/images/social/1200x600.png)
# WeatherStar 4000+
A live version of this project is available at https://weatherstar.netbymatt.com
@@ -34,7 +32,7 @@ From a learning standpoint, this codebase make use of a lot of different methods
* Hand written CSS made easier to mange with SASS
* A linting library to keep code style consistent
## Quick Start
## Quck Start
Ensure you have Node installed.
```bash
@@ -202,9 +200,7 @@ https://weatherstar.netbymatt.com/?settings-units-select=metric
```
### Kiosk mode
Kiosk mode can be activated by a checkbox on the page. This will start Weatherstar in a fullscreen-like view without the play/volume/etc toolbar and scaled to fill the entire space. This does not activate the browser's fullscreen or kiosk mode. Those can only be activated by user interaction or by launching the browser with specific parameters such as `--start-fullscreen` or `--kiosk`.
When using kiosk mode (via the checkbox), there will be no way to exit the fullscreen-like view of weatherstar. Reloading the page should remove the kiosk checkbox and return you to the normal view. This is deliberate as a browser's kiosk mode it intended not to be exited or significantly modified. A separate full-screen icon is available in the tool bar to go full-screen on a laptop or mobile browser.
Kiosk mode can be activated by a checkbox on the page. Note that there is no way out of kiosk mode (except refresh or closing the browser), and the play/pause and other controls will not be available. This is deliberate as a browser's kiosk mode it intended not to be exited or significantly modified. A separate full-screen icon is available in the tool bar to go full-screen on a laptop or mobile browser.
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.
@@ -336,10 +332,8 @@ When using Docker:
* **Static deployment**: Mount your `custom.js` file to `/usr/share/nginx/html/scripts/custom.js`
* **Server deployment**: Mount your `custom.js` file to `/app/server/scripts/custom.js`
### Custom text scroll
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, turn on the setting for `Enable RSS Feed/Text` and then enter text in the resulting text box. Then press set.
Tip: You can have Weatherstar select randomly between several text strings on each pass through the current conditions. Use a pipe character to separate string. `Welcome to Weatherstar|Thanks for watching`.
### 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

View File

@@ -729,16 +729,6 @@
"wfo": "LMK"
}
},
{
"city": "Lubbock",
"lat": 33.5836,
"lon": -101.8549,
"point": {
"x": 49,
"y": 34,
"wfo": "LUB"
}
},
{
"city": "Manchester",
"lat": 42.9956,

View File

@@ -364,11 +364,6 @@
"lat": 38.2542,
"lon": -85.7594
},
{
"city": "Lubbock",
"lat": 33.5836,
"lon": -101.8549
},
{
"city": "Manchester",
"lat": 42.9956,

View File

@@ -8,11 +8,13 @@ import states from './stations-states.mjs';
import chunk from './chunk.mjs';
import overrides from './stations-overrides.mjs';
import postProcessor from './stations-postprocessor.mjs';
import { stationFilter } from '../server/scripts/modules/utils/string.mjs';
// check for cached flag
const USE_CACHE = process.argv.includes('--use-cache');
// skip stations starting with these letters
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
// chunk the list of states
const chunkStates = chunk(states, 3);
@@ -39,8 +41,10 @@ if (!USE_CACHE) {
// eslint-disable-next-line no-await-in-loop
const stationsRaw = await https(next);
stations = JSON.parse(stationsRaw);
// filter stations for 4 letter identifiers
const stationsFiltered4 = stations.features.filter((station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/));
// filter against starting letter
const stationsFiltered = stations.filter(stationFilter);
const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// add each resulting station to the output
stationsFiltered.forEach((station) => {
const id = station.properties.stationIdentifier;

View File

@@ -1,4 +1,4 @@
import 'dotenv/config';
import { config } from 'dotenv';
import {
src, dest, series, parallel,
} from 'gulp';
@@ -15,15 +15,14 @@ import { readFile } from 'fs/promises';
import file from 'gulp-file';
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
import log from 'fancy-log';
import dartSass from 'sass';
import gulpSass from 'gulp-sass';
import sourceMaps from 'gulp-sourcemaps';
import OVERRIDES from '../src/overrides.mjs';
// get cloudfront
import reader from '../src/playlist-reader.mjs';
const sass = gulpSass(dartSass);
config({
path: ['gulp/.env', '.env'],
});
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
@@ -41,7 +40,6 @@ const webpackOptions = {
resolve: {
roots: ['./'],
},
devtool: 'source-map',
optimization: {
minimize: true,
minimizer: [
@@ -86,22 +84,23 @@ const mjsSources = [
'server/scripts/modules/travelforecast.mjs',
'server/scripts/modules/progress.mjs',
'server/scripts/modules/media.mjs',
'server/scripts/modules/custom-scroll-text.mjs',
'server/scripts/modules/custom-rss-feed.mjs',
'server/scripts/index.mjs',
];
if (!process.env.DISABLE_PERSONAL) {
mjsSources.push('server/scripts/modules/personal-weather.mjs');
}
const buildJs = () => src(mjsSources)
.pipe(webpack(webpackOptions))
.pipe(dest(RESOURCES_PATH));
const cssSources = [
'server/styles/scss/**/*.scss',
'server/styles/main.css',
];
const buildCss = () => src(cssSources)
.pipe(sourceMaps.init())
.pipe(sass({ style: 'compressed' }).on('error', sass.logError))
.pipe(rename({ suffix: '.min' }))
.pipe(sourceMaps.write('./'))
const copyCss = () => src(cssSources)
.pipe(concat('ws.min.css'))
.pipe(dest(RESOURCES_PATH));
const htmlSources = [
@@ -123,6 +122,7 @@ const compressHtml = async () => src(htmlSources)
version,
OVERRIDES,
query: {},
DISABLE_PERSONAL: process.env.DISABLE_PERSONAL === '1',
}))
.pipe(rename({ extname: '.html' }))
.pipe(htmlmin({ collapseWhitespace: true }))
@@ -150,6 +150,7 @@ const s3 = s3Upload({
});
const uploadSources = [
'dist/**',
'!dist/**/*.map',
'!dist/images/**/*',
'!dist/fonts/**/*',
];
@@ -217,7 +218,7 @@ const logVersion = async () => {
log(`Version Published: ${version}`);
};
const buildDist = series(clean, parallel(buildJs, compressJsVendor, buildCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
const buildDist = series(clean, parallel(buildJs, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyDataFiles, 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

View File

@@ -9,6 +9,7 @@ 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';
import ambientRelay from "./src/personal-weather.mjs";
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
@@ -59,6 +60,7 @@ const renderIndex = (req, res, production = false) => {
version,
OVERRIDES,
query: req.query,
DISABLE_PERSONAL: process.env.DISABLE_PERSONAL === '1'
});
};
@@ -158,7 +160,6 @@ if (process.env?.DIST === '1') {
// 'npm run build' and then 'DIST=1 npm start'
app.use('/scripts', express.static('./server/scripts', staticOptions));
app.use('/geoip', geoip);
app.use('/music', express.static('./server/music', staticOptions));
// render the EJS template in production mode (serve compressed files from dist directory)
app.get('/', (req, res) => { renderIndex(req, res, true); });
@@ -171,6 +172,7 @@ if (process.env?.DIST === '1') {
app.use('/resources', express.static('./server/scripts/modules'));
app.get('/', index);
app.get('/.well-known/appspecific/com.chrome.devtools.json', devTools);
app.get('/ambient-relay/api/latest', ambientRelay);
app.get('*name', express.static('./server', staticOptions));
}

2920
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "6.5.7",
"version": "6.3.1",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs",
"type": "module",
@@ -31,7 +31,7 @@
"@eslint/eslintrc": "^3.3.1",
"ajv": "^8.17.1",
"del": "^8.0.0",
"eslint": "^10.0.3",
"eslint": "^9.0.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-plugin-import": "^2.10.0",
"fancy-log": "^2.0.0",
@@ -40,11 +40,10 @@
"gulp-concat": "^2.6.1",
"gulp-ejs": "^5.1.0",
"gulp-file": "^0.4.0",
"gulp-html-minifier-terser": "^8.0.0",
"gulp-html-minifier-terser": "^7.1.0",
"gulp-rename": "^2.0.0",
"gulp-s3-uploader": "^1.0.6",
"gulp-sass": "^6.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-terser": "^2.0.0",
"luxon": "^3.0.0",
"metar-taf-parser": "^9.0.0",
@@ -58,7 +57,7 @@
},
"dependencies": {
"dotenv": "^17.0.1",
"ejs": "^5.0.1",
"ejs": "^3.1.5",
"express": "^5.1.0"
}
}

View File

@@ -205,7 +205,7 @@ const formatTimesForColumn = (times) => {
};
// register display
const display = new Almanac(9, 'almanac');
const display = new Almanac(10, 'almanac');
registerDisplay(display);
export default display.getSun.bind(display);

View File

@@ -15,6 +15,9 @@ import { debugFlag } from './utils/debug.mjs';
import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
// some stations prefixed do not provide all the necessary data
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
class CurrentWeather extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Current Conditions', true);
@@ -26,8 +29,8 @@ class CurrentWeather extends WeatherDisplay {
// note: current weather does not use old data on a silent refresh
// this is deliberate because it can pull data from more than one station in sequence
// get the available stations
const { stations } = this.weatherParameters;
// filter for 4-letter observation stations, only those contain sky conditions and thus an icon
const filteredStations = this.weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// Load the observations
let observations;
@@ -35,9 +38,9 @@ class CurrentWeather extends WeatherDisplay {
// station number counter
let stationNum = 0;
while (!observations && stationNum < stations.length) {
while (!observations && stationNum < filteredStations.length) {
// get the station
station = stations[stationNum];
station = filteredStations[stationNum];
const stationId = station.properties.stationIdentifier;
stationNum += 1;
@@ -101,11 +104,7 @@ class CurrentWeather extends WeatherDisplay {
debugContext: 'currentweather',
});
// copy enhanced data and restore the timestamp if it was overwritten by older data from mapclick
const { timestamp } = candidateObservation.features[0].properties;
candidateObservation.features[0].properties = enhancedResult.data;
candidateObservation.features[0].properties.timestamp = timestamp;
const { missingFields } = enhancedResult;
const missingRequired = missingFields.filter((fieldName) => {
const field = requiredFields.find((f) => f.name === fieldName && f.required);
@@ -325,7 +324,7 @@ const backfill = (data) => {
// backfill each property
Object.keys(sortedData[0].properties).forEach((key) => {
// qualify the key (must have value)
if (Object.hasOwn(sortedData[0].properties?.[key] ?? {}, 'value')) {
if (Object.hasOwn(sortedData[0].properties[key], 'value')) {
// backfill this property
result[key] = backfillProperty(sortedData, key);
} else {

View File

@@ -261,7 +261,6 @@ const parseMessage = (event) => {
if (event?.data?.type === 'current-weather-scroll') {
if (event.data?.method === 'start') start();
if (event.data?.method === 'reload') stop(true);
if (event.data?.method === 'non-display') nonDisplay();
if (event.data?.method === 'show') show();
if (event.data?.method === 'hide') hide();
}
@@ -275,20 +274,6 @@ const hide = () => {
mainScroll.style.display = 'none';
};
const nonDisplay = () => {
if (interval) {
clearInterval(interval);
interval = null;
stop();
// if greater than default update (typically long scroll) skip to the next weather screen
if (nextUpdate > DEFAULT_UPDATE) {
screenIndex = (screenIndex + 1) % (workingScreens.length);
sinceLastUpdate = 0;
nextUpdate = DEFAULT_UPDATE;
}
}
};
const screenCount = () => workingScreens.length;
const atDefault = () => defaultScreensLoaded;

View File

@@ -0,0 +1,135 @@
import Setting from './utils/setting.mjs';
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
import { json } from './utils/fetch.mjs';
let firstRun = true;
const parser = new DOMParser();
// change of enable handler
const changeEnable = (newValue) => {
let newDisplay;
if (newValue) {
// add the feed to the scroll
parseFeed(customFeed.value);
// show the string box
newDisplay = 'block';
} else {
// set scroll back to original
resetScroll();
// hide the string entry
newDisplay = 'none';
}
const stringEntry = document.getElementById('settings-customFeed-label');
if (stringEntry) {
stringEntry.style.display = newDisplay;
}
};
// parse the feed/text provided
const parseFeed = (textInput) => {
// skip getting the feed on first run
if (firstRun) return;
// test validity
if (textInput === undefined || textInput === '') {
resetScroll();
}
// test for url
if (textInput.match(/https?:\/\//)) {
getFeed(textInput);
return;
}
// add single text scroll after hazards if present
resetScroll();
addScroll(hazards);
addScroll(
() => (
{
type: 'scroll',
text: textInput,
}),
// keep the existing scroll
true,
);
};
// get the rss feed and then swap out the current weather scroll
const getFeed = async (url) => {
// get the text as a string
// it needs to be proxied, use a free service
const rssResponse = await json(`https://api.allorigins.win/get?url=${url}`);
// this returns a data url
// a few sanity checks
if (rssResponse.status.content_type.indexOf('xml') < 0) return;
// determine return type
const isBase64 = rssResponse.status.content_type.substring(0, 8) !== 'text/xml';
// base 64 decode everything after the comma
const rss = isBase64 ? atob(rssResponse.contents.split('base64,')[1]) : rssResponse.contents;
// parse the rss
const doc = parser.parseFromString(rss, 'text/xml');
// get the title
const rssTitle = doc.querySelector('channel title').textContent;
// get each item
const titles = [...doc.querySelectorAll('item title')].map((t) => t.textContent);
// 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(
() => ({
header: rssTitle,
type: 'scroll',
text: title,
}),
// false parameter does not include the default weather scrolls
false,
);
});
};
// change the feed source and re-load if necessary
const changeFeed = (newValue) => {
// first pass through won't have custom feed enable ready
if (firstRun) return;
if (customFeedEnable.value) {
parseFeed(newValue);
}
};
const customFeed = new Setting('customFeed', {
name: 'Custom RSS Feed',
defaultValue: '',
type: 'string',
changeAction: changeFeed,
placeholder: 'Text or URL',
});
const customFeedEnable = new Setting('customFeedEnable', {
name: 'Enable RSS Feed/Text',
defaultValue: false,
changeAction: changeEnable,
});
// initialize the custom feed inputs on the page
document.addEventListener('DOMContentLoaded', () => {
// add the controls to the page
const settingsSection = document.querySelector('#settings');
settingsSection.append(customFeedEnable.generate(), customFeed.generate());
// clear the first run value
firstRun = false;
// call change enable with the current value to show/hide the url box
// and make the call to get the feed if enabled
changeEnable(customFeedEnable.value);
});

View File

@@ -1,89 +0,0 @@
import Setting from './utils/setting.mjs';
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
let firstRun = true;
// change of enable handler
const changeEnable = (newValue) => {
let newDisplay;
if (newValue) {
// add the text to the scroll
parseText(customText.value);
// show the string box
newDisplay = 'block';
} else {
// set scroll back to original
resetScroll();
// hide the string entry
newDisplay = 'none';
}
const stringEntry = document.getElementById('settings-customText-label');
if (stringEntry) {
stringEntry.style.display = newDisplay;
}
};
// parse the text provided
const parseText = (textInput) => {
// skip updating text on first run
if (firstRun) return;
// test validity
if (textInput === undefined || textInput === '') {
resetScroll();
}
// split the text at pipe characters
const texts = textInput.split('|');
// add single text scroll after hazards if present
resetScroll();
addScroll(hazards);
addScroll(
() => {
// pick a random string from the available list
const randInt = Math.floor(Math.random() * texts.length);
return {
type: 'scroll',
text: texts[randInt],
};
},
// keep the existing scroll
true,
);
};
// change the text
const changeText = (newValue) => {
// first pass through won't have custom text enable ready
if (firstRun) return;
if (customTextEnable.value) {
parseText(newValue);
}
};
const customText = new Setting('customText', {
name: 'Custom Text',
defaultValue: '',
type: 'string',
changeAction: changeText,
placeholder: 'Text to scroll',
});
const customTextEnable = new Setting('customTextEnable', {
name: 'Enable Custom Text',
defaultValue: false,
changeAction: changeEnable,
});
// initialize the custom text inputs on the page
document.addEventListener('DOMContentLoaded', () => {
// add the controls to the page
const settingsSection = document.querySelector('#settings');
settingsSection.append(customTextEnable.generate(), customText.generate());
// clear the first run value
firstRun = false;
// call change enable with the current value to show/hide the url box
changeEnable(customTextEnable.value);
});

View File

@@ -97,9 +97,11 @@ const parse = (fullForecast, forecastUrl) => {
// Skip the first period if it's nighttime (like "Tonight") since extended forecast
// should focus on upcoming full days, not the end of the current day
let startIndex = 0;
let dateOffset = 0; // offset for date labels when we skip periods
if (activePeriods.length > 0 && !activePeriods[0].isDaytime) {
startIndex = 1;
dateOffset = 1; // start date labels from tomorrow since we're skipping tonight
if (debugFlag('extendedforecast')) {
console.log(`ExtendedForecast: Skipping first period "${activePeriods[0].name}" because it's nighttime`);
}
@@ -109,14 +111,25 @@ const parse = (fullForecast, forecastUrl) => {
}
}
// create a list of days starting with the appropriate day
const Days = [0, 1, 2, 3, 4, 5, 6];
const dates = Days.map((shift) => {
const date = DateTime.local().startOf('day').plus({ days: shift + dateOffset });
return date.toLocaleString({ weekday: 'short' });
});
if (debugFlag('extendedforecast')) {
console.log(`ExtendedForecast: Generated date labels: [${dates.join(', ')}]`);
}
// track the destination forecast index
let destIndex = 0;
const forecast = [];
// if the first period is nighttime it is skipped above via startIndex
for (let i = startIndex; i < activePeriods.length; i += 1) {
const period = activePeriods[i];
// create the destination object if necessary
if (!forecast[destIndex]) {
forecast.push({
dayName: '', low: undefined, high: undefined, text: undefined, icon: undefined,
@@ -125,14 +138,15 @@ const parse = (fullForecast, forecastUrl) => {
// get the object to modify/populate
const fDay = forecast[destIndex];
// preload the icon
preloadImg(fDay.icon);
if (period.isDaytime) {
// day time is the high temperature
fDay.high = period.temperature;
fDay.icon = getLargeIcon(period.icon);
fDay.text = shortenExtendedForecastText(period.shortForecast);
fDay.dayName = DateTime.fromISO(period.startTime).startOf('day').toLocaleString({ weekday: 'short' });
// preload the icon
preloadImg(fDay.icon);
fDay.dayName = dates[destIndex];
// Wait for the corresponding night period to increment
} else {
// low temperature
@@ -195,4 +209,4 @@ const shortenExtendedForecastText = (long) => {
};
// register display
registerDisplay(new ExtendedForecast(8, 'extended-forecast'));
registerDisplay(new ExtendedForecast(9, 'extended-forecast'));

View File

@@ -40,10 +40,9 @@ class HourlyGraph extends WeatherDisplay {
const temperature = data.map((d) => d.temperature);
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
const skyCover = data.map((d) => d.skyCover);
const dewpoint = data.map((d) => d.dewpoint);
this.data = {
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint,
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
};
this.setStatus(STATUS.loaded);
@@ -64,16 +63,12 @@ class HourlyGraph extends WeatherDisplay {
// calculate time scale
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
const timeStep = this.data.temperature.length / 4;
const startTime = DateTime.now().startOf('hour');
let prevTime = startTime;
Array(5).fill().forEach((val, idx) => {
// track the previous label so a day of week can be added when it changes
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
prevTime = label.ts;
// write to page
document.querySelector(`.x-axis .l-${idx + 1}`).innerHTML = label.formatted;
});
document.querySelector('.x-axis .l-1').innerHTML = formatTime(startTime);
document.querySelector('.x-axis .l-2').innerHTML = formatTime(startTime.plus({ hour: 6 }));
document.querySelector('.x-axis .l-3').innerHTML = formatTime(startTime.plus({ hour: 12 }));
document.querySelector('.x-axis .l-4').innerHTML = formatTime(startTime.plus({ hour: 18 }));
document.querySelector('.x-axis .l-5').innerHTML = formatTime(startTime.plus({ hour: 24 }));
// order is important last line drawn is on top
// clouds
@@ -91,22 +86,11 @@ class HourlyGraph extends WeatherDisplay {
lineWidth: 3,
});
// calculate temperature scale for min and max of dewpoint and temperature
const minScale = Math.min(...this.data.dewpoint, ...this.data.temperature);
const maxScale = Math.max(...this.data.dewpoint, ...this.data.temperature);
const thirdScale = (maxScale - minScale) / 3;
const midScale1 = Math.round(minScale + thirdScale);
const midScale2 = Math.round(minScale + (thirdScale * 2));
const tempScale = calcScale(minScale, availableHeight - 10, maxScale, 10);
// dewpoint
const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale);
drawPath(dewpointPath, ctx, {
strokeStyle: 'green',
lineWidth: 3,
});
// temperature
const minTemp = Math.min(...this.data.temperature);
const maxTemp = Math.max(...this.data.temperature);
const midTemp = Math.round((minTemp + maxTemp) / 2);
const tempScale = calcScale(minTemp, availableHeight - 10, maxTemp, 10);
const tempPath = createPath(this.data.temperature, timeScale, tempScale);
drawPath(tempPath, ctx, {
strokeStyle: 'red',
@@ -116,17 +100,15 @@ class HourlyGraph extends WeatherDisplay {
// temperature axis labels
// limited to 3 characters, sacraficing degree character
const degree = String.fromCharCode(176);
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxScale + degree).substring(0, 3);
this.elem.querySelector('.y-axis .l-2').innerHTML = (midScale2 + degree).substring(0, 3);
this.elem.querySelector('.y-axis .l-3').innerHTML = (midScale1 + degree).substring(0, 3);
this.elem.querySelector('.y-axis .l-4').innerHTML = (minScale + degree).substring(0, 3);
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxTemp + degree).substring(0, 3);
this.elem.querySelector('.y-axis .l-2').innerHTML = (midTemp + degree).substring(0, 3);
this.elem.querySelector('.y-axis .l-3').innerHTML = (minTemp + degree).substring(0, 3);
// set the image source
this.image.src = canvas.toDataURL();
// change the units in the header
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
this.elem.querySelector('.dewpoint').innerHTML = `Dewpoint ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
super.drawCanvas();
this.finishDraw();
@@ -163,18 +145,7 @@ const drawPath = (path, ctx, options) => {
};
// format as 1p, 12a, etc.
const formatTime = (time, prev) => {
// if the day of the week changes, show the day of the week in the label
let format = 'ha';
if (prev.weekday !== time.weekday) format = 'ccc ha';
const ts = time.setZone(timeZone());
return {
ts,
formatted: ts.toFormat(format).slice(0, -1),
};
};
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
// register display
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
registerDisplay(new HourlyGraph(5, 'hourly-graph'));

View File

@@ -75,10 +75,7 @@ class Hourly extends WeatherDisplay {
const startingHour = DateTime.local().setZone(timeZone());
// shorten to 24 hours
const shortData = this.data.slice(0, 24);
const lines = shortData.map((data, index) => {
const lines = this.data.map((data, index) => {
const fillValues = {};
// hour
const hour = startingHour.plus({ hours: index });
@@ -105,7 +102,7 @@ class Hourly extends WeatherDisplay {
const filledRow = this.fillTemplate('hourly-row', fillValues);
// alter the color of the feels like column to reflect wind chill or heat index
if (data.apparentTemperature < data.temperature) {
if (feelsLike < temperature) {
filledRow.querySelector('.like').classList.add('wind-chill');
} else if (feelsLike > temperature) {
filledRow.querySelector('.like').classList.add('heat-index');
@@ -206,7 +203,6 @@ const parseForecast = async (data) => {
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
const dewpoint = expand(data.dewpoint.values);
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
@@ -220,7 +216,6 @@ const parseForecast = async (data) => {
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],
icon: icons[idx],
dewpoint: temperatureConverter(dewpoint[idx]),
}));
};
@@ -238,7 +233,7 @@ const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPr
};
// expand a set of values with durations to an hour-by-hour array
const expand = (data, maxHours = 36) => {
const expand = (data, maxHours = 24) => {
const startOfHour = DateTime.utc().startOf('hour').toMillis();
const result = []; // resulting expanded values
data.forEach((item) => {
@@ -260,7 +255,7 @@ const expand = (data, maxHours = 36) => {
};
// register display
const display = new Hourly(3, 'hourly', false);
const display = new Hourly(4, 'hourly', false);
registerDisplay(display);
export default display.getHourlyData.bind(display);

View File

@@ -13,7 +13,7 @@ const largeIcon = (link, _isNightTime) => {
} catch (error) {
console.warn(`largeIcon: ${error.message}`);
// Return a fallback icon to prevent downstream errors
return addPath(`No-Data-Large.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
}
// find the icon
@@ -102,8 +102,6 @@ const largeIcon = (link, _isNightTime) => {
case 'snow_fzra':
case 'snow_fzra-n':
case 'winter_mix':
case 'winter_mix-n':
return addPath('Freezing-Rain-Snow.gif');
case 'fzra':
@@ -143,8 +141,6 @@ const largeIcon = (link, _isNightTime) => {
return addPath('Thunderstorm.gif');
case 'wind_skc':
case 'wind_':
case 'wind_-n':
return addPath('Windy.gif');
case 'wind_skc-n':
@@ -173,7 +169,7 @@ const largeIcon = (link, _isNightTime) => {
default: {
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
// Return a reasonable fallback instead of false to prevent downstream errors
return addPath(`No-Data-Large.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
}
}
};

View File

@@ -133,7 +133,6 @@ const smallIcon = (link, _isNightTime) => {
case 'wind_few':
case 'wind_few-n':
case 'wind_':
return addPath('Wind.gif');
case 'wind_sct':
@@ -171,7 +170,7 @@ const smallIcon = (link, _isNightTime) => {
case 'blizzard':
case 'blizzard-n':
return addPath('Blowing-Snow.gif');
return addPath('Blowing Snow.gif');
default:
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);

View File

@@ -205,4 +205,4 @@ const shortenCurrentConditions = (_condition) => {
return condition;
};
// register display
registerDisplay(new LatestObservations(2, 'latest-observations'));
registerDisplay(new LatestObservations(3, 'latest-observations'));

View File

@@ -262,4 +262,4 @@ const parse = (forecast, forecastUrl) => {
}));
};
// register display
registerDisplay(new LocalForecast(7, 'local-forecast'));
registerDisplay(new LocalForecast(8, 'local-forecast'));

View File

@@ -6,7 +6,6 @@ import { safeJson } from './utils/fetch.mjs';
import { getPoint } from './utils/weather.mjs';
import { debugFlag } from './utils/debug.mjs';
import settings from './settings.mjs';
import { stationFilter } from './utils/string.mjs';
document.addEventListener('DOMContentLoaded', () => {
init();
@@ -86,15 +85,7 @@ const getWeather = async (latLon, haveDataCallback) => {
return;
}
// filter stations for proper format
const stationsFiltered = stations.features.filter(stationFilter);
// check for stations available after filtering
if (stationsFiltered.length === 0) {
console.warn('No observation stations left for location after filtering');
return;
}
const StationId = stationsFiltered[0].properties.stationIdentifier;
const StationId = stations.features[0].properties.stationIdentifier;
let { city } = point.properties.relativeLocation.properties;
const { state } = point.properties.relativeLocation.properties;
@@ -117,7 +108,7 @@ const getWeather = async (latLon, haveDataCallback) => {
weatherParameters.timeZone = point.properties.timeZone;
weatherParameters.forecast = point.properties.forecast;
weatherParameters.forecastGridData = point.properties.forecastGridData;
weatherParameters.stations = stationsFiltered;
weatherParameters.stations = stations.features;
weatherParameters.relativeLocation = point.properties.relativeLocation.properties;
// update the main process for display purposes

View File

@@ -0,0 +1,114 @@
// current weather conditions display
import STATUS from './status.mjs';
import { safeJson } from './utils/fetch.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import {
temperature, pressure, distanceMm, windSpeed,
} from './utils/units.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
class PersonalWeather extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Personal Weather Station', true);
}
async getData(weatherParameters, refresh) {
// always load the data for use in the lower scroll
const superResult = super.getData(weatherParameters, refresh);
const dataUrl = '/ambient-relay/api/latest';
let personalData;
try {
personalData = await safeJson(dataUrl, {
retryCount: 3,
stillWaiting: () => this.stillWaiting(),
});
} catch (error) {
console.error(`Unexpected error getting personal weather station data from: ${dataUrl}: ${error.message}`);
}
// test for data received
if (!personalData) {
if (this.isEnabled) this.setStatus(STATUS.failed);
// send failed to subscribers
this.getDataCallback(undefined);
return;
}
// we only get here if there was no error above
this.data = parseData(personalData);
this.getDataCallback();
// stop here if we're disabled
if (!superResult) return;
// Data is available, ensure we're enabled for display
this.timing.totalScreens = 1;
this.setStatus(STATUS.loaded);
}
async drawCanvas() {
super.drawCanvas();
const fill = {
temp: this.data.Temperature + String.fromCharCode(176),
wind: `${this.data.WindSpeed} ${this.data.WindUnit}`,
deviceName: this.data.device_name,
deviceLocation: this.data.device_location,
humidity: `${this.data.Humidity}%`,
pressure: `${this.data.Pressure} ${this.data.PressureUnit}`,
dailyRain: `${this.data.DailyRain} ${this.data.DailyRainUnit}`,
timestamp: `At ${this.data.timestamp}`,
};
const area = this.elem.querySelector('.main');
area.innerHTML = '';
area.append(this.fillTemplate('weather', fill));
this.finishDraw();
}
// make data available outside this class
// promise allows for data to be requested before it is available
async getCurrentWeather(stillWaiting) {
// an external caller has requested data, set up auto reload
this.setAutoReload();
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
return new Promise((resolve) => {
if (this.data) resolve(this.data);
// data not available, put it into the data callback queue
this.getDataCallbacks.push(() => resolve(this.data));
});
}
}
// format the received data
const parseData = (data) => {
// get the unit converters
const temperatureConverter = temperature('us');
const pressureConverter = pressure('us');
const inConverter = distanceMm('us');
const windConverter = windSpeed('us');
data.Pressure = pressureConverter(data.baromrelin * 10000) / 100;
data.PressureUnit = pressureConverter.units;
data.Humidity = data.humidity;
data.Temperature = temperatureConverter(data.tempf);
data.WindSpeed = windConverter(data.windspeedmph);
data.WindUnit = windConverter.units;
data.DailyRain = inConverter(data.dailyrainin);
data.DailyRainUnit = inConverter.units;
data.timestamp = DateTime.fromISO(data.date).toFormat('H:mm:ss a EEE MMM d').toUpperCase();
// set wind speed of 0 as calm
if (data.WindSpeed === 0) data.WindSpeed = 'Calm';
return data;
};
const display = new PersonalWeather(2, 'personal-weather');
registerDisplay(display);
// export default display.getPersonalWeather.bind(display);

View File

@@ -9,12 +9,10 @@ const pixelToFile = (xPixel, yPixel) => {
return `${yTile}-${xTile}`;
};
// convert a pixel location in the overall map to a pixel location on the tile set
// convert a pixel location in the overall map to a pixel location on the tile
const modTile = (xPixel, yPixel) => {
// adjust for additional 1 tile when odd
const x = (Math.floor(xPixel) % (TILE_SIZE.x));
const y = (Math.floor(yPixel) % (TILE_SIZE.y));
const x = Math.round(xPixel) % TILE_SIZE.x;
const y = Math.round(yPixel) % TILE_SIZE.y;
return { x, y };
};
@@ -48,8 +46,8 @@ const setTiles = (data) => {
// determine which tiles are used
const usedTiles = [
true,
tileShift.x + TILE_SIZE.x > RADAR_FINAL_SIZE.width,
tileShift.y + TILE_SIZE.y > RADAR_FINAL_SIZE.height,
TILE_SIZE.x - tileShift.x < RADAR_FINAL_SIZE.width,
TILE_SIZE.y - tileShift.y < RADAR_FINAL_SIZE.width,
];
// if we need t[1] and t[2] then we also need t[3]
usedTiles.push(usedTiles[1] && usedTiles[2]);

View File

@@ -231,4 +231,4 @@ class Radar extends WeatherDisplay {
}
// register display
registerDisplay(new Radar(11, 'radar'));
registerDisplay(new Radar(12, 'radar'));

View File

@@ -23,7 +23,7 @@ const buildForecast = (forecast, city, cityXY) => {
const getRegionalObservation = async (point, city) => {
try {
// get stations using centralized safe handling
const stations = await safeJson(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations?limit=10`);
const stations = await safeJson(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations?limit=1`);
if (!stations || !stations.features || stations.features.length === 0) {
if (debugFlag('verbose-failures')) {
@@ -32,13 +32,9 @@ const getRegionalObservation = async (point, city) => {
return false;
}
// get the first station with a 4-letter id (generally has appropriate data)
const station4Letter = stations.features.find((station) => {
if (station.properties.stationIdentifier.length === 4) return station.properties;
return false;
});
const station = station4Letter.id;
const stationId = station4Letter.properties.stationIdentifier;
// get the first station
const station = stations.features[0].id;
const stationId = stations.features[0].properties.stationIdentifier;
// get the observation data using centralized safe handling
const observation = await safeJson(`${station}/observations/latest`);

View File

@@ -51,7 +51,7 @@ class RegionalForecast extends WeatherDisplay {
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
// get a target distance
let targetDistance = 2.4;
let targetDistance = 2.5;
if (this.weatherParameters.state === 'HI') targetDistance = 1;
// make station info into an array
@@ -235,4 +235,4 @@ const getAndFormatPoint = async (lat, lon) => {
};
// register display
registerDisplay(new RegionalForecast(6, 'regional-forecast'));
registerDisplay(new RegionalForecast(7, 'regional-forecast'));

View File

@@ -146,4 +146,4 @@ class SpcOutlook extends WeatherDisplay {
}
// register display
registerDisplay(new SpcOutlook(10, 'spc-outlook'));
registerDisplay(new SpcOutlook(11, 'spc-outlook'));

View File

@@ -222,4 +222,4 @@ const getTravelCitiesDayName = (cities) => cities.reduce((dayName, city) => {
}, '');
// register display, not active by default
registerDisplay(new TravelForecast(5, 'travel', false));
registerDisplay(new TravelForecast(6, 'travel', false));

View File

@@ -13,12 +13,7 @@ const locationCleanup = (input) => {
return regexes.reduce((value, regex) => value.replace(regex, ''), input);
};
// stations must be 4 alpha characters and not start with the provided list
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
const stationFilter = (station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/) && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1));
export {
// eslint-disable-next-line import/prefer-default-export
locationCleanup,
stationFilter,
};

View File

@@ -13,6 +13,7 @@ const fahrenheitToCelsius = (Fahrenheit) => Math.round((Fahrenheit - 32) * 5 / 9
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
const mmToIn = (mm) => round2(mm / 25.4);
// each module/page/slide creates it's own unit converter as needed by providing the base units available
// the factory function then returns an appropriate converter or pass-thru function for use on the page
@@ -98,6 +99,23 @@ const distanceKilometers = (defaultUnit = 'si') => {
return converter;
};
// millimeters (annoying with camel case)
const distanceMm = (defaultUnit = 'si') => {
// default to passthru
let converter = passthru();
// change the converter if there is a mismatch
if (defaultUnit !== settings.units.value) {
converter = convert((value) => Math.round(mmToIn(value)));
}
// append units
if (settings.units.value === 'si') {
converter.units = ' mm.';
} else {
converter.units = ' in.';
}
return converter;
};
const pressure = (defaultUnit = 'si') => {
// default to passthru (millibar)
let converter = passthru(100);
@@ -121,6 +139,7 @@ export {
distanceMeters,
distanceKilometers,
pressure,
distanceMm,
// formatter
round2,

View File

@@ -172,7 +172,6 @@ class WeatherDisplay {
if (this.screenIndex < 0) this.screenIndex = 0;
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
if (this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'start' });
if (!this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'non-display' });
if (this.okToDrawCurrentConditions === false) postMessage({ type: 'current-weather-scroll', method: 'hide' });
}

View File

@@ -346,7 +346,7 @@ var TimeIndicator;
TimeIndicator["TL"] = "TL";
})(TimeIndicator || (TimeIndicator = {}));
/**
* https://web.archive.org/web/20230318235549/https://aviationweather.gov/taf/decoder
* https://www.aviationweather.gov/taf/decoder
*/
var WeatherChangeType;
(function (WeatherChangeType) {
@@ -2535,8 +2535,7 @@ class MetarParser extends AbstractParser {
while (i < trendParts.length &&
trendParts[i] !== this.TEMPO &&
trendParts[i] !== this.INTER &&
trendParts[i] !== this.BECMG &&
trendParts[i] !== this.RMK) {
trendParts[i] !== this.BECMG) {
if (trendParts[i].startsWith(this.FM) ||
trendParts[i].startsWith(this.TL) ||
trendParts[i].startsWith(this.AT)) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
@use 'shared/_colors' as c;
@use 'shared/_utils' as u;
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
// also shared with personal weather
.weather-display .main.current-weather {
&.main {
@@ -92,4 +93,4 @@
text-wrap: nowrap;
}
}
}
}

View File

@@ -11,7 +11,7 @@
right: 60px;
width: 360px;
font-family: 'Star4000 Small';
font-size: 28px;
font-size: 32px;
@include u.text-shadow();
text-align: right;
@@ -23,10 +23,6 @@
color: red;
}
.dewpoint {
color: green;
}
.cloud {
color: lightgrey;
}
@@ -56,33 +52,32 @@
.x-axis {
bottom: 0px;
left: 54px;
width: 532px;
left: 0px;
width: 640px;
height: 20px;
.label {
text-align: center;
transform: translateX(-50%);
white-space: nowrap;
width: 50px;
&.l-1 {
left: 0px;
left: 25px;
}
&.l-2 {
left: calc(532px / 4 * 1);
left: 158px;
}
&.l-3 {
left: calc(532px / 4 * 2);
left: 291px;
}
&.l-4 {
left: calc(532px / 4 * 3);
left: 424px;
}
&.l-5 {
left: calc(532px / 4 * 4);
left: 557px;
}
}
@@ -115,14 +110,10 @@
}
&.l-2 {
top: calc(280px / 3);
top: 140px;
}
&.l-3 {
bottom: calc(280px / 3 - 11px);
}
&.l-4 {
bottom: 0px;
}
}

View File

@@ -0,0 +1,68 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
// also shared with personal weather
.weather-display .main.personal-weather {
&.main {
@include u.text-shadow();
font-family: "Star4000 Large";
font-size: 20px;
font-weight: bold;
line-height: 24px;
top: 20px;
height: 290px;
.row {
margin-bottom: 12px;
.label,
.value {
display: inline-block;
}
.label {
margin-left: 20px;
}
.value {
float: right;
margin-right: 10px;
}
}
.center {
text-align: center;
}
.temp {
font-family: 'Star4000 Large';
font-size: 24pt;
position: absolute;
top: 20px;
right: 0px;
}
.deviceName,
.deviceLocation {
color: c.$title-color;
max-height: 32px;
margin-bottom: 10px;
padding-top: 4px;
overflow: hidden;
text-wrap: nowrap;
}
.timestamp {
position: absolute;
bottom: 15px;
right: 10px;
text-align: right;
font-family: "Star4000 Small";
font-size: 24pt;
font-weight: normal;
}
}
}

View File

@@ -1,6 +1,7 @@
@use 'page';
@use 'weather-display';
@use 'current-weather';
@use 'personal-weather';
@use 'extended-forecast';
@use 'hourly';
@use 'hourly-graph';

File diff suppressed because one or more lines are too long

20
src/personal-weather.mjs Normal file
View File

@@ -0,0 +1,20 @@
// testing data for use with personal weather stations via
// ambient-relay https://github.com/jasonkonen/ambient-relay
const ambientRelay = (req, res) => {
res.json({
"id": 123,
"mac_address": "00:00:00:00:00:00",
"device_name": "My Weather Station",
"device_location": "Backyard",
"dateutc": 1515436500000,
"date": "2018-01-08T18:35:00.000Z",
"tempf": 66.9,
"humidity": 30,
"windspeedmph": 0.9,
"baromrelin": 30.05,
"dailyrainin": 0,
"raw_data": {}
})
}
export default ambientRelay;

327
tests/package-lock.json generated
View File

@@ -14,12 +14,12 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -28,26 +28,26 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@puppeteer/browsers": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz",
"integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==",
"version": "2.10.5",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.3",
"debug": "^4.4.1",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.4",
"tar-fs": "^3.1.1",
"semver": "^7.7.2",
"tar-fs": "^3.0.8",
"yargs": "^17.7.2"
},
"bin": {
@@ -64,13 +64,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"version": "22.15.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"undici-types": "~7.18.0"
"undici-types": "~6.21.0"
}
},
"node_modules/@types/yauzl": {
@@ -84,9 +84,9 @@
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -135,44 +135,28 @@
}
},
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
"integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
"license": "Apache-2.0"
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
"optional": true
},
"node_modules/bare-fs": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz",
"integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==",
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz",
"integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
"bare-stream": "^2.6.4"
},
"engines": {
"bare": ">=1.16.0"
@@ -187,10 +171,11 @@
}
},
"node_modules/bare-os": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz",
"integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
@@ -200,28 +185,25 @@
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.11.0.tgz",
"integrity": "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw==",
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"streamx": "^2.25.0",
"teex": "^1.0.1"
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-abort-controller": "*",
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
},
"bare-buffer": {
"optional": true
},
@@ -230,19 +212,10 @@
}
}
},
"node_modules/bare-url": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
"integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
"integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -267,9 +240,9 @@
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -279,9 +252,9 @@
}
},
"node_modules/chromium-bidi": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz",
"integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
"integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
@@ -324,9 +297,9 @@
"license": "MIT"
},
"node_modules/cosmiconfig": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
"integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"license": "MIT",
"dependencies": {
"env-paths": "^2.2.1",
@@ -359,9 +332,9 @@
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -390,9 +363,9 @@
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1581282",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
"version": "0.0.1452169",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
"integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
"license": "BSD-3-Clause"
},
"node_modules/emoji-regex": {
@@ -402,9 +375,9 @@
"license": "MIT"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
@@ -420,9 +393,9 @@
}
},
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
@@ -489,15 +462,6 @@
"node": ">=0.10.0"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -558,9 +522,9 @@
}
},
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
"integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
@@ -614,10 +578,14 @@
}
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"license": "MIT",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
},
"engines": {
"node": ">= 12"
}
@@ -644,9 +612,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -655,6 +623,12 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -815,9 +789,9 @@
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
@@ -825,18 +799,18 @@
}
},
"node_modules/puppeteer": {
"version": "24.40.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz",
"integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==",
"version": "24.10.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.10.0.tgz",
"integrity": "sha512-Oua9VkGpj0S2psYu5e6mCer6W9AU9POEQh22wRgSXnLXASGH+MwLUVWgLCLeP9QPHHcJ7tySUlg4Sa9OJmaLpw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.13.0",
"chromium-bidi": "14.0.0",
"@puppeteer/browsers": "2.10.5",
"chromium-bidi": "5.1.0",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1581282",
"puppeteer-core": "24.40.0",
"typed-query-selector": "^2.12.1"
"devtools-protocol": "0.0.1452169",
"puppeteer-core": "24.10.0",
"typed-query-selector": "^2.12.0"
},
"bin": {
"puppeteer": "lib/cjs/puppeteer/node/cli.js"
@@ -846,18 +820,17 @@
}
},
"node_modules/puppeteer-core": {
"version": "24.40.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz",
"integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==",
"version": "24.10.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.0.tgz",
"integrity": "sha512-xX0QJRc8t19iAwRDsAOR38Q/Zx/W6WVzJCEhKCAwp2XMsaWqfNtQ+rBfQW9PlF+Op24d7c8Zlgq9YNmbnA7hdQ==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.13.0",
"chromium-bidi": "14.0.0",
"debug": "^4.4.3",
"devtools-protocol": "0.0.1581282",
"typed-query-selector": "^2.12.1",
"webdriver-bidi-protocol": "0.4.1",
"ws": "^8.19.0"
"@puppeteer/browsers": "2.10.5",
"chromium-bidi": "5.1.0",
"debug": "^4.4.1",
"devtools-protocol": "0.0.1452169",
"typed-query-selector": "^2.12.0",
"ws": "^8.18.2"
},
"engines": {
"node": ">=18"
@@ -882,9 +855,9 @@
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -904,12 +877,12 @@
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"ip-address": "^9.0.5",
"smart-buffer": "^4.2.0"
},
"engines": {
@@ -941,15 +914,23 @@
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/streamx": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
},
"optionalDependencies": {
"bare-events": "^2.2.0"
}
},
"node_modules/string-width": {
@@ -979,9 +960,9 @@
}
},
"node_modules/tar-fs": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
"integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
@@ -993,30 +974,20 @@
}
},
"node_modules/tar-stream": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
@@ -1029,24 +1000,18 @@
"license": "0BSD"
},
"node_modules/typed-query-selector": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz",
"integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==",
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT",
"optional": true
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz",
"integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==",
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -1071,9 +1036,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -1138,9 +1103,9 @@
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"version": "3.25.49",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.49.tgz",
"integrity": "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@@ -36,7 +36,7 @@
const OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
</script>
<% } else { %>
<link rel="stylesheet" type="text/css" href="styles/ws.min.css" />
<link rel="stylesheet" type="text/css" href="styles/main.css" />
<!--<script type="text/javascript">const OVERRIDES={};</script>-->
<script type="text/javascript">
OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
@@ -62,7 +62,10 @@
<script type="module" src="scripts/modules/radar.mjs"></script>
<script type="module" src="scripts/modules/settings.mjs"></script>
<script type="module" src="scripts/modules/media.mjs"></script>
<script type="module" src="scripts/modules/custom-scroll-text.mjs"></script>
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
<% if (!DISABLE_PERSONAL) { %>
<script type="module" src="scripts/modules/personal-weather.mjs"></script>
<% } %>
<script type="module" src="scripts/index.mjs"></script>
<% } %>
@@ -109,6 +112,11 @@
<div id="current-weather-html" class="weather-display">
<%- include('partials/current-weather.ejs') %>
</div>
<% if (!DISABLE_PERSONAL) { %>
<div id="personal-weather-html" class="weather-display">
<%- include('partials/personal-weather.ejs') %>
</div>
<% } %>
<div id="local-forecast-html" class="weather-display">
<%- include('partials/local-forecast.ejs') %>
</div>

View File

@@ -2,7 +2,6 @@
<div class="main has-scroll hourly-graph">
<div class="top-right template ">
<div class="temperature">Temperature</div>
<div class="dewpoint">Dewpoint</div>
<div class="cloud">Cloud %</div>
<div class="rain">Precip %</div>
</div>
@@ -10,7 +9,6 @@
<div class="label l-1">75</div>
<div class="label l-2">65</div>
<div class="label l-3">55</div>
<div class="label l-4">45</div>
</div>
<div class="chart">
<img id="chart-area"></img>

View File

@@ -0,0 +1,25 @@
<%- include('header.ejs', {titleDual:{ top: 'Personal' , bottom: 'Weather Station' }, noaaLogo: false, hasTime: true}) %>
<div class="main has-scroll has-box personal-weather">
<div class="weather template">
<div class="deviceName value"></div>
<div class="deviceLocation value"></div>
<div class="temp value"></div>
<div class="row">
<div class="label">Humidity:</div>
<div class="humidity value"></div>
</div>
<div class="row">
<div class="label">Wind:</div>
<div class="wind value"></div>
</div>
<div class="row">
<div class="label">Pressure:</div>
<div class="pressure value"></div>
</div>
<div class="row">
<div class="label">Daily Rain:</div>
<div class="dailyRain value"></div>
</div>
<div class="timestamp value">At 12:34:55 PM</div>
</div>
</div>