From 97056ebc5cd7cbd72f7f96c9ea38f723ce30bf1f Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sun, 21 Dec 2025 01:43:19 +0100 Subject: [PATCH] u --- manage-users.js | 195 +++++++ package-lock.json | 1086 +++++++++++++++++++++++++++++++++++++ package.json | 5 + server.js | 66 +++ src/client/App.js | 84 ++- src/client/AuthContext.js | 79 +++ src/client/LoginDialog.js | 140 +++++ src/client/RuleCard.js | 161 ++++++ src/client/RuleEditor.js | 346 ++++++++++++ src/client/RuleManager.js | 173 ++++++ 10 files changed, 2322 insertions(+), 13 deletions(-) create mode 100644 manage-users.js create mode 100644 src/client/AuthContext.js create mode 100644 src/client/LoginDialog.js create mode 100644 src/client/RuleCard.js create mode 100644 src/client/RuleEditor.js create mode 100644 src/client/RuleManager.js diff --git a/manage-users.js b/manage-users.js new file mode 100644 index 0000000..8f34cc7 --- /dev/null +++ b/manage-users.js @@ -0,0 +1,195 @@ +#!/usr/bin/env node +import { input, password, select, confirm } from '@inquirer/prompts'; +import Database from 'better-sqlite3'; +import bcrypt from 'bcrypt'; + +const DB_FILE = 'ac_data.db'; + +const db = new Database(DB_FILE); +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'user', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) +`); + +const insertUser = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'); +const getAllUsers = db.prepare('SELECT id, username, role, created_at FROM users ORDER BY id'); +const getUserById = db.prepare('SELECT * FROM users WHERE id = ?'); +const updateUserRole = db.prepare('UPDATE users SET role = ? WHERE id = ?'); +const updateUserPassword = db.prepare('UPDATE users SET password_hash = ? WHERE id = ?'); +const deleteUser = db.prepare('DELETE FROM users WHERE id = ?'); + +console.log('\n╔════════════════════════════════════╗'); +console.log('║ 🔐 User Manager - AC Dashboard ║'); +console.log('╚════════════════════════════════════╝\n'); + +async function listUsers() { + const users = getAllUsers.all(); + if (users.length === 0) { + console.log(' No users found.\n'); + return; + } + console.log(' ID │ Username │ Role │ Created'); + console.log(' ────┼────────────────┼────────┼─────────────────────'); + users.forEach(u => { + const id = String(u.id).padStart(3); + const name = u.username.padEnd(14); + const role = u.role.padEnd(6); + const date = u.created_at?.slice(0, 16) || 'N/A'; + const roleColor = u.role === 'admin' ? '\x1b[35m' : '\x1b[33m'; + console.log(` ${id} │ ${name} │ ${roleColor}${role}\x1b[0m │ ${date}`); + }); + console.log(''); +} + +async function createUser() { + const username = await input({ + message: 'Username:', + validate: v => v.length >= 3 || 'Min 3 characters' + }); + + const pwd = await password({ + message: 'Password:', + mask: '*', + validate: v => v.length >= 4 || 'Min 4 characters' + }); + + const role = await select({ + message: 'Role:', + choices: [ + { name: '👤 user', value: 'user' }, + { name: '👑 admin', value: 'admin' } + ] + }); + + try { + const hash = await bcrypt.hash(pwd, 10); + insertUser.run(username, hash, role); + console.log(`\n✅ User "${username}" created as ${role}\n`); + } catch (e) { + if (e.code === 'SQLITE_CONSTRAINT_UNIQUE') { + console.log(`\n❌ User "${username}" already exists\n`); + } else throw e; + } +} + +async function editUser() { + const users = getAllUsers.all(); + if (users.length === 0) { + console.log(' No users to edit.\n'); + return; + } + + const userId = await select({ + message: 'Select user to edit:', + choices: users.map(u => ({ + name: `${u.username} (${u.role})`, + value: u.id + })) + }); + + const action = await select({ + message: 'What to change?', + choices: [ + { name: '🔑 Change password', value: 'password' }, + { name: '👤 Change role', value: 'role' }, + { name: '← Back', value: 'back' } + ] + }); + + if (action === 'back') return; + + if (action === 'password') { + const pwd = await password({ + message: 'New password:', + mask: '*', + validate: v => v.length >= 4 || 'Min 4 characters' + }); + const hash = await bcrypt.hash(pwd, 10); + updateUserPassword.run(hash, userId); + console.log('\n✅ Password updated\n'); + } + + if (action === 'role') { + const user = getUserById.get(userId); + const newRole = await select({ + message: 'New role:', + choices: [ + { name: '👤 user', value: 'user' }, + { name: '👑 admin', value: 'admin' } + ], + default: user.role + }); + updateUserRole.run(newRole, userId); + console.log(`\n✅ Role changed to ${newRole}\n`); + } +} + +async function removeUser() { + const users = getAllUsers.all(); + if (users.length === 0) { + console.log(' No users to delete.\n'); + return; + } + + const userId = await select({ + message: 'Select user to delete:', + choices: users.map(u => ({ + name: `${u.username} (${u.role})`, + value: u.id + })) + }); + + const user = getUserById.get(userId); + const confirmed = await confirm({ + message: `Delete user "${user.username}"?`, + default: false + }); + + if (confirmed) { + deleteUser.run(userId); + console.log(`\n✅ User "${user.username}" deleted\n`); + } else { + console.log('\n❌ Cancelled\n'); + } +} + +async function main() { + while (true) { + try { + const action = await select({ + message: 'What would you like to do?', + choices: [ + { name: '📋 List users', value: 'list' }, + { name: '➕ Create user', value: 'create' }, + { name: '✏️ Edit user', value: 'edit' }, + { name: '🗑️ Delete user', value: 'delete' }, + { name: '🚪 Exit', value: 'exit' } + ] + }); + + if (action === 'exit') { + console.log('Bye!\n'); + break; + } + + if (action === 'list') await listUsers(); + if (action === 'create') await createUser(); + if (action === 'edit') await editUser(); + if (action === 'delete') await removeUser(); + + } catch (e) { + if (e.name === 'ExitPromptError') { + console.log('\nBye!\n'); + break; + } + throw e; + } + } +} + +main(); diff --git a/package-lock.json b/package-lock.json index 169dd71..69db587 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,14 +14,19 @@ "@babel/preset-react": "^7.28.5", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@inquirer/prompts": "^8.1.0", "@mui/material": "^7.3.6", "babel-loader": "^10.0.0", + "bcrypt": "^6.0.0", "better-sqlite3": "^12.5.0", "chart.js": "^4.5.1", "css-loader": "^7.1.2", "dotenv": "^16.4.5", "express": "^5.2.1", "html-webpack-plugin": "^5.6.5", + "ink": "^6.5.1", + "ink-text-input": "^6.0.0", + "jsonwebtoken": "^9.0.3", "react": "^19.2.3", "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.3", @@ -31,6 +36,19 @@ "webpack-dev-middleware": "^7.4.5" } }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.2.tgz", + "integrity": "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1771,6 +1789,346 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@inquirer/ansi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.2.tgz", + "integrity": "sha512-SYLX05PwJVnW+WVegZt1T4Ip1qba1ik+pNJPDiqvk6zS5Y/i8PhRzLpGEtVd7sW0G8cMtkD8t4AZYhQwm8vnww==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.3.tgz", + "integrity": "sha512-xtQP2eXMFlOcAhZ4ReKP2KZvDIBb1AnCfZ81wWXG3DXLVH0f0g4obE0XDPH+ukAEMRcZT0kdX2AS1jrWGXbpxw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.2", + "@inquirer/core": "^11.1.0", + "@inquirer/figures": "^2.0.2", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.3.tgz", + "integrity": "sha512-lyEvibDFL+NA5R4xl8FUmNhmu81B+LDL9L/MpKkZlQDJZXzG8InxiqYxiAlQYa9cqLLhYqKLQwZqXmSTqCLjyw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.0", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.0.tgz", + "integrity": "sha512-+jD/34T1pK8M5QmZD/ENhOfXdl9Zr+BrQAUc5h2anWgi7gggRq15ZbiBeLoObj0TLbdgW7TAIQRU2boMc9uOKQ==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.2", + "@inquirer/figures": "^2.0.2", + "@inquirer/type": "^4.0.2", + "cli-width": "^4.1.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^9.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.3.tgz", + "integrity": "sha512-wYyQo96TsAqIciP/r5D3cFeV8h4WqKQ/YOvTg5yOfP2sqEbVVpbxPpfV3LM5D0EP4zUI3EZVHyIUIllnoIa8OQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.0", + "@inquirer/external-editor": "^2.0.2", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.3.tgz", + "integrity": "sha512-2oINvuL27ujjxd95f6K2K909uZOU2x1WiAl7Wb1X/xOtL8CgQ1kSxzykIr7u4xTkXkXOAkCuF45T588/YKee7w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.0", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.2.tgz", + "integrity": "sha512-X/fMXK7vXomRWEex1j8mnj7s1mpnTeP4CO/h2gysJhHLT2WjBnLv4ZQEGpm/kcYI8QfLZ2fgW+9kTKD+jeopLg==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.2.tgz", + "integrity": "sha512-qXm6EVvQx/FmnSrCWCIGtMHwqeLgxABP8XgcaAoywsL0NFga9gD5kfG0gXiv80GjK9Hsoz4pgGwF/+CjygyV9A==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.3.tgz", + "integrity": "sha512-4R0TdWl53dtp79Vs6Df2OHAtA2FVNqya1hND1f5wjHWxZJxwDMSNB1X5ADZJSsQKYAJ5JHCTO+GpJZ42mK0Otw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.0", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.3.tgz", + "integrity": "sha512-TjQLe93GGo5snRlu83JxE38ZPqj5ZVggL+QqqAF2oBA5JOJoxx25GG3EGH/XN/Os5WOmKfO8iLVdCXQxXRZIMQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.0", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.3.tgz", + "integrity": "sha512-rCozGbUMAHedTeYWEN8sgZH4lRCdgG/WinFkit6ZPsp8JaNg2T0g3QslPBS5XbpORyKP/I+xyBO81kFEvhBmjA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.2", + "@inquirer/core": "^11.1.0", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.1.0.tgz", + "integrity": "sha512-LsZMdKcmRNF5LyTRuZE5nWeOjganzmN3zwbtNfcs6GPh3I2TsTtF1UYZlbxVfhxd+EuUqLGs/Lm3Xt4v6Az1wA==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.0.3", + "@inquirer/confirm": "^6.0.3", + "@inquirer/editor": "^5.0.3", + "@inquirer/expand": "^5.0.3", + "@inquirer/input": "^5.0.3", + "@inquirer/number": "^4.0.3", + "@inquirer/password": "^5.0.3", + "@inquirer/rawlist": "^5.1.0", + "@inquirer/search": "^4.0.3", + "@inquirer/select": "^5.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.1.0.tgz", + "integrity": "sha512-yUCuVh0jW026Gr2tZlG3kHignxcrLKDR3KBp+eUgNz+BAdSeZk0e18yt2gyBr+giYhj/WSIHCmPDOgp1mT2niQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.0", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.0.3.tgz", + "integrity": "sha512-lzqVw0YwuKYetk5VwJ81Ba+dyVlhseHPx9YnRKQgwXdFS0kEavCz2gngnNhnMIxg8+j1N/rUl1t5s1npwa7bqg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.0", + "@inquirer/figures": "^2.0.2", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.3.tgz", + "integrity": "sha512-M+ynbwS0ecQFDYMFrQrybA0qL8DV0snpc4kKevCCNaTpfghsRowRY7SlQBeIYNzHqXtiiz4RG9vTOeb/udew7w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.2", + "@inquirer/core": "^11.1.0", + "@inquirer/figures": "^2.0.2", + "@inquirer/type": "^4.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.2.tgz", + "integrity": "sha512-cae7mzluplsjSdgFA6ACLygb5jC8alO0UUnFPyu0E7tNRPrL+q/f8VcSXp+cjZQ7l5CMpDpi2G1+IQvkOiL1Lw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2525,6 +2883,21 @@ "ajv": "^8.8.2" } }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2534,6 +2907,30 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/babel-loader": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", @@ -2642,6 +3039,20 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/better-sqlite3": { "version": "12.5.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz", @@ -2763,6 +3174,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2846,6 +3263,24 @@ ], "license": "CC-BY-4.0" }, + "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==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, "node_modules/chart.js": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", @@ -2894,6 +3329,58 @@ "node": ">=0.10.0" } }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -2917,6 +3404,18 @@ "node": ">=6" } }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -2960,6 +3459,15 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -3270,6 +3778,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3282,6 +3799,12 @@ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3334,6 +3857,18 @@ "node": ">=4" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -3379,6 +3914,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", + "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3659,6 +4204,18 @@ "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3983,6 +4540,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3995,6 +4564,70 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/ink": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-6.5.1.tgz", + "integrity": "sha512-wF3j/DmkM8q5E+OtfdQhCRw8/0ahkc8CUTgEddxZzpEWPslu7YPL3t64MWRoI9m6upVGpfAg4ms2BBvxCdKRLQ==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.2.1", + "ansi-escapes": "^7.2.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.6.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^5.1.1", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.39.10", + "indent-string": "^5.0.0", + "is-in-ci": "^2.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.33.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^8.1.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/react": ">=19.0.0", + "react": ">=19.0.0", + "react-devtools-core": "^6.1.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -4034,6 +4667,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -4123,6 +4786,49 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4178,6 +4884,48 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4287,6 +5035,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -4320,6 +5077,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4381,6 +5147,26 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -4441,6 +5227,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4539,6 +5340,15 @@ "tslib": "^2.0.3" } }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4934,6 +5744,21 @@ "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", "license": "MIT" }, + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -5110,6 +5935,22 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -5354,6 +6195,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -5399,6 +6246,22 @@ "simple-concat": "^1.0.0" } }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5436,6 +6299,27 @@ "node": ">=0.10.0" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -5454,6 +6338,49 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5682,6 +6609,18 @@ "node": "*" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -6002,18 +6941,159 @@ "node": ">= 8" } }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6040,6 +7120,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" } } } diff --git a/package.json b/package.json index e83bfe1..f7194b2 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,19 @@ "@babel/preset-react": "^7.28.5", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@inquirer/prompts": "^8.1.0", "@mui/material": "^7.3.6", "babel-loader": "^10.0.0", + "bcrypt": "^6.0.0", "better-sqlite3": "^12.5.0", "chart.js": "^4.5.1", "css-loader": "^7.1.2", "dotenv": "^16.4.5", "express": "^5.2.1", "html-webpack-plugin": "^5.6.5", + "ink": "^6.5.1", + "ink-text-input": "^6.0.0", + "jsonwebtoken": "^9.0.3", "react": "^19.2.3", "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.3", diff --git a/server.js b/server.js index 027aef7..073b846 100644 --- a/server.js +++ b/server.js @@ -5,6 +5,8 @@ import path from 'path'; import { fileURLToPath } from 'url'; import webpack from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; import config from './webpack.config.js'; const __filename = fileURLToPath(import.meta.url); @@ -17,6 +19,7 @@ const USER_AGENT = 'ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS const POLL_INTERVAL_MS = 60000; // 60 seconds const DB_FILE = 'ac_data.db'; const PORT = 3905; +const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production'; // Device Type Mapping const DEVICE_TYPES = { @@ -52,11 +55,23 @@ db.exec(` ) `); +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'user', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) +`); + const insertStmt = db.prepare(` INSERT INTO readings (dev_id, dev_name, port, port_name, temp_c, humidity, vpd, fan_speed, on_speed, off_speed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); +const getUserByUsername = db.prepare('SELECT * FROM users WHERE username = ?'); + // --- AC INFINITY API LOGIC --- let token = null; @@ -201,7 +216,58 @@ async function poll() { // --- EXPRESS SERVER --- const app = express(); +app.use(express.json()); +// Auth: Login +app.post('/api/auth/login', async (req, res) => { + try { + const { username, password } = req.body; + if (!username || !password) { + return res.status(400).json({ error: 'Username and password required' }); + } + + const user = getUserByUsername.get(username); + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const token = jwt.sign( + { id: user.id, username: user.username, role: user.role }, + JWT_SECRET, + { expiresIn: '7d' } + ); + + res.json({ token, user: { username: user.username, role: user.role } }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Auth: Get current user +app.get('/api/auth/me', (req, res) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'No token provided' }); + } + + const token = authHeader.split(' ')[1]; + const decoded = jwt.verify(token, JWT_SECRET); + + res.json({ user: { username: decoded.username, role: decoded.role } }); + } catch (error) { + if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') { + return res.status(401).json({ error: 'Invalid or expired token' }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}); // API: Devices app.get('/api/devices', (req, res) => { diff --git a/src/client/App.js b/src/client/App.js index d5b505b..d4c588e 100644 --- a/src/client/App.js +++ b/src/client/App.js @@ -1,6 +1,9 @@ import React from 'react'; -import { ThemeProvider, createTheme, CssBaseline, AppBar, Toolbar, Typography, Container, Box } from '@mui/material'; +import { ThemeProvider, createTheme, CssBaseline, AppBar, Toolbar, Typography, Container, Box, Button, Chip } from '@mui/material'; import Dashboard from './Dashboard'; +import RuleManager from './RuleManager'; +import LoginDialog from './LoginDialog'; +import { AuthProvider, useAuth } from './AuthContext'; // Gruvbox Dark color palette const gruvboxDark = { @@ -39,22 +42,77 @@ const darkTheme = createTheme({ }, }); +function AppContent() { + const { user, loading, logout, isAuthenticated, isAdmin } = useAuth(); + + if (loading) { + return ( + + Loading... + + ); + } + + if (!isAuthenticated) { + return ; + } + + return ( + + + + + Tischlerei Dashboard + + + + {isAdmin && ( + + )} + + + + + + + {isAdmin && } + + + ); +} + function App() { return ( - - - - - Tischlerei Dashboard - - - - - - - + + + ); } diff --git a/src/client/AuthContext.js b/src/client/AuthContext.js new file mode 100644 index 0000000..50549a0 --- /dev/null +++ b/src/client/AuthContext.js @@ -0,0 +1,79 @@ +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; + +const AuthContext = createContext(null); + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // Check for existing session on mount + useEffect(() => { + const checkAuth = async () => { + const token = localStorage.getItem('authToken'); + if (token) { + try { + const res = await fetch('api/auth/me', { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + setUser(data.user); + } else { + localStorage.removeItem('authToken'); + } + } catch (error) { + console.error('Auth check failed:', error); + localStorage.removeItem('authToken'); + } + } + setLoading(false); + }; + checkAuth(); + }, []); + + const login = useCallback(async (username, password) => { + const res = await fetch('api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'Login failed'); + } + + const data = await res.json(); + localStorage.setItem('authToken', data.token); + setUser(data.user); + return data.user; + }, []); + + const logout = useCallback(() => { + localStorage.removeItem('authToken'); + setUser(null); + }, []); + + const value = { + user, + loading, + login, + logout, + isAuthenticated: !!user, + isAdmin: user?.role === 'admin' + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/src/client/LoginDialog.js b/src/client/LoginDialog.js new file mode 100644 index 0000000..7f7fe05 --- /dev/null +++ b/src/client/LoginDialog.js @@ -0,0 +1,140 @@ +import React, { useState } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Button, + Alert, + Box, + CircularProgress, + InputAdornment, + IconButton, + Typography +} from '@mui/material'; +import { useAuth } from './AuthContext'; + +// Simple eye icons using unicode +const VisibilityIcon = () => 👁; +const VisibilityOffIcon = () => 👁‍🗨; + +export default function LoginDialog({ open }) { + const { login } = useAuth(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + await login(username, password); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( + + + + 🔐 Dashboard Login + + + Tischlerei Automation Control + + + +
+ + {error && ( + + {error} + + )} + + setUsername(e.target.value)} + disabled={loading} + sx={{ mb: 2 }} + /> + + setPassword(e.target.value)} + disabled={loading} + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + edge="end" + size="small" + > + {showPassword ? : } + + + ) + }} + /> + + + + + +
+
+ ); +} diff --git a/src/client/RuleCard.js b/src/client/RuleCard.js new file mode 100644 index 0000000..522c0ce --- /dev/null +++ b/src/client/RuleCard.js @@ -0,0 +1,161 @@ +import React from 'react'; +import { + Box, + Paper, + Typography, + Switch, + IconButton, + Chip, + Tooltip +} from '@mui/material'; + +// Simple icons using unicode/emoji +const EditIcon = () => ✏️; +const DeleteIcon = () => 🗑️; + +const dayLabels = { + mon: 'M', tue: 'T', wed: 'W', thu: 'T', fri: 'F', sat: 'S', sun: 'S' +}; + +const dayOrder = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; + +function TriggerSummary({ trigger }) { + if (trigger.type === 'time') { + const days = trigger.days || []; + const isEveryDay = days.length === 7; + const isWeekdays = days.length === 5 && + ['mon', 'tue', 'wed', 'thu', 'fri'].every(d => days.includes(d)); + + let dayText = isEveryDay ? 'Every day' : + isWeekdays ? 'Weekdays' : + dayOrder.filter(d => days.includes(d)).map(d => dayLabels[d]).join(' '); + + return ( + + + + At {trigger.time} • {dayText} + + + ); + } + + if (trigger.type === 'sensor') { + return ( + + + + When {trigger.sensor} {trigger.operator} {trigger.value} + + + ); + } + + return null; +} + +function ActionSummary({ action }) { + if (action.type === 'toggle') { + return ( + + + + Turn {action.target} {action.state ? 'on' : 'off'} + + + ); + } + + if (action.type === 'keepOn') { + return ( + + + + Keep {action.target} on for {action.duration} min + + + ); + } + + return null; +} + +export default function RuleCard({ rule, onEdit, onDelete, onToggle }) { + return ( + + + + + + {rule.name} + + {!rule.enabled && ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/client/RuleEditor.js b/src/client/RuleEditor.js new file mode 100644 index 0000000..11c24b7 --- /dev/null +++ b/src/client/RuleEditor.js @@ -0,0 +1,346 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Button, + Box, + FormControl, + InputLabel, + Select, + MenuItem, + ToggleButton, + ToggleButtonGroup, + Typography, + Divider, + Slider, + Switch, + FormControlLabel +} from '@mui/material'; + +const DAYS = [ + { key: 'mon', label: 'Mon' }, + { key: 'tue', label: 'Tue' }, + { key: 'wed', label: 'Wed' }, + { key: 'thu', label: 'Thu' }, + { key: 'fri', label: 'Fri' }, + { key: 'sat', label: 'Sat' }, + { key: 'sun', label: 'Sun' } +]; + +const SENSORS = ['Temperature', 'Humidity', 'CO2', 'VPD', 'Light Level']; +const OPERATORS = [ + { value: '>', label: 'Greater than (>)' }, + { value: '<', label: 'Less than (<)' }, + { value: '>=', label: 'Greater or equal (≥)' }, + { value: '<=', label: 'Less or equal (≤)' }, + { value: '==', label: 'Equal to (=)' } +]; + +const OUTPUTS = [ + 'Workshop Light', + 'Exhaust Fan', + 'Heater', + 'Humidifier', + 'All Outlets', + 'Grow Light', + 'Circulation Fan' +]; + +export default function RuleEditor({ open, rule, onSave, onClose }) { + const [name, setName] = useState(''); + const [triggerType, setTriggerType] = useState('time'); + + // Time trigger state + const [time, setTime] = useState('08:00'); + const [days, setDays] = useState(['mon', 'tue', 'wed', 'thu', 'fri']); + + // Sensor trigger state + const [sensor, setSensor] = useState('Temperature'); + const [operator, setOperator] = useState('>'); + const [sensorValue, setSensorValue] = useState(25); + + // Action state + const [actionType, setActionType] = useState('toggle'); + const [target, setTarget] = useState('Workshop Light'); + const [toggleState, setToggleState] = useState(true); + const [duration, setDuration] = useState(15); + + // Reset form when rule changes + useEffect(() => { + if (rule) { + setName(rule.name); + setTriggerType(rule.trigger.type); + + if (rule.trigger.type === 'time') { + setTime(rule.trigger.time); + setDays(rule.trigger.days || []); + } else { + setSensor(rule.trigger.sensor); + setOperator(rule.trigger.operator); + setSensorValue(rule.trigger.value); + } + + setActionType(rule.action.type); + setTarget(rule.action.target); + if (rule.action.type === 'toggle') { + setToggleState(rule.action.state); + } else { + setDuration(rule.action.duration); + } + } else { + // Reset to defaults for new rule + setName(''); + setTriggerType('time'); + setTime('08:00'); + setDays(['mon', 'tue', 'wed', 'thu', 'fri']); + setSensor('Temperature'); + setOperator('>'); + setSensorValue(25); + setActionType('toggle'); + setTarget('Workshop Light'); + setToggleState(true); + setDuration(15); + } + }, [rule, open]); + + const handleDaysChange = (event, newDays) => { + if (newDays.length > 0) { + setDays(newDays); + } + }; + + const handleSave = () => { + const ruleData = { + name, + trigger: triggerType === 'time' + ? { type: 'time', time, days } + : { type: 'sensor', sensor, operator, value: sensorValue }, + action: actionType === 'toggle' + ? { type: 'toggle', target, state: toggleState } + : { type: 'keepOn', target, duration } + }; + onSave(ruleData); + }; + + const isValid = name.trim().length > 0 && + (triggerType !== 'time' || days.length > 0); + + return ( + + + {rule ? '✏️ Edit Rule' : '➕ Create New Rule'} + + + + + {/* Rule Name */} + setName(e.target.value)} + fullWidth + placeholder="e.g., Morning Lights" + /> + + {/* Trigger Section */} + + + TRIGGER (When to activate) + + + + + Trigger Type + + + + {triggerType === 'time' && ( + + setTime(e.target.value)} + InputLabelProps={{ shrink: true }} + fullWidth + /> + + + Days of Week + + + {DAYS.map(day => ( + + {day.label} + + ))} + + + + )} + + {triggerType === 'sensor' && ( + + + Sensor + + + + + Condition + + + setSensorValue(Number(e.target.value))} + fullWidth + /> + + + )} + + + {/* Action Section */} + + + ACTION (What to do) + + + + + Action Type + + + + + Target Output + + + + {actionType === 'toggle' && ( + setToggleState(e.target.checked)} + color="primary" + /> + } + label={toggleState ? 'Turn ON' : 'Turn OFF'} + /> + )} + + {actionType === 'keepOn' && ( + + + Duration: {duration} minutes + + setDuration(val)} + min={1} + max={120} + marks={[ + { value: 1, label: '1m' }, + { value: 30, label: '30m' }, + { value: 60, label: '1h' }, + { value: 120, label: '2h' } + ]} + valueLabelDisplay="auto" + /> + + )} + + + + + + + + + + ); +} diff --git a/src/client/RuleManager.js b/src/client/RuleManager.js new file mode 100644 index 0000000..6d9aecf --- /dev/null +++ b/src/client/RuleManager.js @@ -0,0 +1,173 @@ +import React, { useState } from 'react'; +import { + Box, + Typography, + Button, + Paper, + Divider +} from '@mui/material'; +import RuleCard from './RuleCard'; +import RuleEditor from './RuleEditor'; + +// Initial mock rules for demonstration +const initialRules = [ + { + id: 1, + name: 'Morning Light', + enabled: true, + trigger: { + type: 'time', + time: '06:30', + days: ['mon', 'tue', 'wed', 'thu', 'fri'] + }, + action: { + type: 'toggle', + target: 'Workshop Light', + state: true + } + }, + { + id: 2, + name: 'High Humidity Fan', + enabled: true, + trigger: { + type: 'sensor', + sensor: 'Humidity', + operator: '>', + value: 70 + }, + action: { + type: 'keepOn', + target: 'Exhaust Fan', + duration: 15 + } + }, + { + id: 3, + name: 'Evening Shutdown', + enabled: false, + trigger: { + type: 'time', + time: '18:00', + days: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] + }, + action: { + type: 'toggle', + target: 'All Outlets', + state: false + } + } +]; + +export default function RuleManager() { + const [rules, setRules] = useState(initialRules); + const [editorOpen, setEditorOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + + const handleAddRule = () => { + setEditingRule(null); + setEditorOpen(true); + }; + + const handleEditRule = (rule) => { + setEditingRule(rule); + setEditorOpen(true); + }; + + const handleDeleteRule = (ruleId) => { + setRules(rules.filter(r => r.id !== ruleId)); + }; + + const handleToggleRule = (ruleId) => { + setRules(rules.map(r => + r.id === ruleId ? { ...r, enabled: !r.enabled } : r + )); + }; + + const handleSaveRule = (ruleData) => { + if (editingRule) { + // Update existing rule + setRules(rules.map(r => + r.id === editingRule.id ? { ...r, ...ruleData } : r + )); + } else { + // Add new rule + const newRule = { + ...ruleData, + id: Math.max(0, ...rules.map(r => r.id)) + 1, + enabled: true + }; + setRules([...rules, newRule]); + } + setEditorOpen(false); + setEditingRule(null); + }; + + const handleCloseEditor = () => { + setEditorOpen(false); + setEditingRule(null); + }; + + return ( + + + + + ⚙️ Automation Rules + + + Configure triggers and actions for home automation + + + + + + + + {rules.length === 0 ? ( + + + No rules configured. Click "Add Rule" to create one. + + + ) : ( + + {rules.map(rule => ( + handleEditRule(rule)} + onDelete={() => handleDeleteRule(rule.id)} + onToggle={() => handleToggleRule(rule.id)} + /> + ))} + + )} + + + + ); +}