From 3fcf2e43cde21de4f6bf80b1f0321ba64ef74aa1 Mon Sep 17 00:00:00 2001 From: mrkmntal Date: Fri, 21 Aug 2026 13:34:26 -0400 Subject: [PATCH] Add npm run init-db to auto-create database and hazard_history table --- README.md | 29 +++------------ package-lock.json | 12 +++--- package.json | 5 ++- src/init-db.mjs | 95 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 src/init-db.mjs diff --git a/README.md b/README.md index 2813b6d..4cb6437 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ `ws4kp-linhanced` is a Linux-focused fork of [`netbymatt/ws4kp`](https://github.com/netbymatt/ws4kp) by `markmental`. -Current release: `v0.2.1` (Security Update released on 7/30/2026) +Current release: `v0.2.2` It keeps the stronger, more stable foundation of the original `ws4kp` project while selectively incorporating international weather support and global map ideas from [`mwood77/ws4kp-international`](https://github.com/mwood77/ws4kp-international). The goal is not to become a kitchen-sink weather platform. The goal is a leaner, maintainable, Linux-oriented WeatherStar fork with a clear identity. @@ -142,31 +142,14 @@ WS4KP_MYSQL_DATABASE=ws4kp_linhanced If your local MariaDB/MySQL instance is socket-only, set `WS4KP_MYSQL_SOCKET_PATH` and omit `WS4KP_MYSQL_HOST` / `WS4KP_MYSQL_PORT`. -Create the required table: +The database and `hazard_history` table are created automatically when you run `npm run start`. You can also run the setup manually at any time: -```sql -CREATE TABLE hazard_history ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - location_label VARCHAR(255) NOT NULL, - location_key VARCHAR(128) NOT NULL, - hazard_type VARCHAR(128) NOT NULL, - source VARCHAR(64) NOT NULL, - severity VARCHAR(64) DEFAULT NULL, - latest_hazard_id VARCHAR(255) DEFAULT NULL, - encountered_at DATETIME NOT NULL, - last_seen_at DATETIME NOT NULL, - ongoing TINYINT(1) NOT NULL DEFAULT 1, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - PRIMARY KEY (id), - UNIQUE KEY uq_logical_hazard (location_key, hazard_type, source), - KEY idx_last_seen_at (last_seen_at), - KEY idx_location_key (location_key), - KEY idx_ongoing (ongoing) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +```bash +npm run init-db ``` +This script is idempotent, so it is safe to run repeatedly. Database initialization is skipped when running in static mode (`STATIC=1`). + The database retains full hazard history. The `Hazard List` UI shows only the latest 7 entries and only the most recent row per stored location. Nearby same-label locations are reconciled during updates to reduce duplicate location spam. In the current browser session, `Hazard List` updates immediately after new hazard history is synced. diff --git a/package-lock.json b/package-lock.json index 63d1f6a..b65dbcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3039,9 +3039,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -4828,9 +4828,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 292c045..0a58555 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "ws4kp-linhanced", - "version": "0.2.1", + "version": "0.2.2", "description": "WeatherStar 4000+: Linhanced - A Linux-focused fork of the WeatherStar 4000+ project", "main": "index.mjs", "type": "module", "scripts": { - "start": "npm run build && node index.mjs", + "init-db": "node src/init-db.mjs", + "start": "npm run init-db && npm run build && 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", diff --git a/src/init-db.mjs b/src/init-db.mjs new file mode 100644 index 0000000..b310ff6 --- /dev/null +++ b/src/init-db.mjs @@ -0,0 +1,95 @@ +import 'dotenv/config'; +import mysql from 'mysql2/promise'; + +const getDbConfig = () => { + const { + WS4KP_MYSQL_HOST = '127.0.0.1', + WS4KP_MYSQL_PORT = '3306', + WS4KP_MYSQL_USER, + WS4KP_MYSQL_PASSWORD, + WS4KP_MYSQL_DATABASE, + WS4KP_MYSQL_SOCKET_PATH, + } = process.env; + + if (!WS4KP_MYSQL_USER || !WS4KP_MYSQL_PASSWORD || !WS4KP_MYSQL_DATABASE) { + throw new Error('Missing MySQL configuration. Set WS4KP_MYSQL_USER, WS4KP_MYSQL_PASSWORD, and WS4KP_MYSQL_DATABASE.'); + } + + const config = { + user: WS4KP_MYSQL_USER, + password: WS4KP_MYSQL_PASSWORD, + }; + + if (WS4KP_MYSQL_SOCKET_PATH) { + config.socketPath = WS4KP_MYSQL_SOCKET_PATH; + } else { + config.host = WS4KP_MYSQL_HOST; + config.port = Number(WS4KP_MYSQL_PORT); + } + + return { ...config, database: WS4KP_MYSQL_DATABASE }; +}; + +const createDatabase = async (connection, database) => { + await connection.execute( + `CREATE DATABASE IF NOT EXISTS \`${database}\` + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci`, + ); +}; + +const createHazardHistoryTable = async (connection) => { + await connection.execute(` + CREATE TABLE IF NOT EXISTS \`hazard_history\` ( + \`id\` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + \`location_label\` VARCHAR(255) NOT NULL, + \`location_key\` VARCHAR(128) NOT NULL, + \`hazard_type\` VARCHAR(128) NOT NULL, + \`source\` VARCHAR(64) NOT NULL, + \`severity\` VARCHAR(64) DEFAULT NULL, + \`latest_hazard_id\` VARCHAR(255) DEFAULT NULL, + \`encountered_at\` DATETIME NOT NULL, + \`last_seen_at\` DATETIME NOT NULL, + \`ongoing\` TINYINT(1) NOT NULL DEFAULT 1, + \`created_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updated_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + PRIMARY KEY (\`id\`), + UNIQUE KEY \`uq_logical_hazard\` (\`location_key\`, \`hazard_type\`, \`source\`), + KEY \`idx_last_seen_at\` (\`last_seen_at\`), + KEY \`idx_location_key\` (\`location_key\`), + KEY \`idx_ongoing\` (\`ongoing\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); +}; + +const initDatabase = async () => { + if (process.env.STATIC) { + console.log('Skipping database initialization (STATIC mode).'); + return; + } + + const { database, ...baseConfig } = getDbConfig(); + + let connection; + try { + connection = await mysql.createConnection(baseConfig); + await createDatabase(connection, database); + console.log(`Database "${database}" is ready.`); + } finally { + if (connection) await connection.end(); + } + + try { + connection = await mysql.createConnection({ ...baseConfig, database }); + await createHazardHistoryTable(connection); + console.log('Table "hazard_history" is ready.'); + } finally { + if (connection) await connection.end(); + } +}; + +initDatabase().catch((error) => { + console.error('Database initialization failed:', error.message); + process.exit(1); +});