diff --git a/README.md b/README.md index 095ac1f..554b48e 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,39 @@ server { } ``` +### Приклад конфігурації для балонсування серверів + +```nginx +upstream backend { + server 5.58.145.96 max_fails=3 fail_timeout=10s; + server 95.47.167.120 max_fails=3 fail_timeout=10s; +} + +server { + listen 80; + listen [::]:80; + + server_name sheep-service.com www.sheep-service.com; + + location / { + proxy_pass http://backend; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 5s; + proxy_read_timeout 30s; + proxy_send_timeout 30s; + } +} +``` + ### Активація сайту ```bash diff --git a/api/controllers/homestead.joint.controller.js b/api/controllers/homestead.joint.controller.js new file mode 100644 index 0000000..fc76447 --- /dev/null +++ b/api/controllers/homestead.joint.controller.js @@ -0,0 +1,91 @@ +const HomesteadJointService = require('../services/homestead.joint.service'); + +class HomesteadJointController { + async getList(req, res) { + const { homestead_id } = req.params; + + if (homestead_id) { + if (req.possibilities.can_joint_territory) { + let result = await HomesteadJointService.getList(homestead_id); + if (result) { + return res + .status(200) + .send(result); + } else { + return res + .status(500) + .send({ message: 'Internal server error.' }); + } + } else { + return res + .status(403) + .send({ message: 'The user does not have enough rights.' }); + } + } else { + return res + .status(404) + .send({ message: 'Users not found.' }); + } + } + + async createJoint(req, res) { + const { homestead_id } = req.params; + const data = req.body; + + if (homestead_id) { + if (req.possibilities.can_joint_territory) { + let result = await HomesteadJointService.createJoint( + homestead_id, + data + ); + + if (result) { + return res.status(200).send(result); + } else { + return res.status(500).send({ + message: 'Unable create joint homestead.', + }); + } + } else { + return res + .status(404) + .send({ message: 'User not found.' }); + } + } else { + return res + .status(404) + .send({ message: 'Users not found.' }); + } + } + + async deleteJoint(req, res) { + const { homestead_id } = req.params; + const data = req.body; + + if (homestead_id) { + if (req.possibilities.can_joint_territory) { + let result = await HomesteadJointService.deleteJoint( + homestead_id, + data + ); + if (result) { + return res.status(200).send(result); + } else { + return res.status(500).send({ + message: 'Unable delete joint homestead.', + }); + } + } else { + return res + .status(404) + .send({ message: 'User not found.' }); + } + } else { + return res + .status(404) + .send({ message: 'Users not found.' }); + } + } +} + +module.exports = new HomesteadJointController(); \ No newline at end of file diff --git a/api/controllers/homesteads.controller.js b/api/controllers/homesteads.controller.js index 16349d2..67a04ce 100644 --- a/api/controllers/homesteads.controller.js +++ b/api/controllers/homesteads.controller.js @@ -13,6 +13,8 @@ class HomesteadsController { id = req.sheepId; } else if (mode == "group") { id = req.group_id; + } else if (mode == "joint") { + id = req.sheepId; } let result = await HomesteadsService.getList(mode, id); diff --git a/api/controllers/sheeps.controller.js b/api/controllers/sheeps.controller.js index 3121e7b..ef6635e 100644 --- a/api/controllers/sheeps.controller.js +++ b/api/controllers/sheeps.controller.js @@ -45,7 +45,7 @@ class SheepsController { } async getListStand(req, res) { - if (req.possibilities.can_view_stand) { + if (req.possibilities.can_view_stand) { const result = await SheepsService.getListStand(req.mode); if (result) { @@ -86,7 +86,7 @@ class SheepsController { const data = req.body; if (req.mode == 2) { - let result = await SheepsService.updateSheep(data); + let result = await SheepsService.updateSheep(data, 2); if (result) { return res.status(200).send(result); @@ -95,10 +95,24 @@ class SheepsController { message: 'Unable update sheep.', }); } + } if (req.possibilities.can_manager_sheeps) { + if (Number(data.mode) == 2) { + return res.status(403).send({ message: 'The sheep does not have enough rights.' }); + } else { + let result = await SheepsService.updateSheep(data, 1); + + if (result) { + return res.status(200).send(result); + } else { + return res.status(500).send({ + message: 'Unable update sheep.', + }); + } + } } else { return res .status(403) - .send({ message: 'Sheep not foundThe sheep does not have enough rights.' }); + .send({ message: 'The sheep does not have enough rights.' }); } } diff --git a/api/middleware/auth.js b/api/middleware/auth.js index 66a8514..6a3d5d2 100644 --- a/api/middleware/auth.js +++ b/api/middleware/auth.js @@ -9,9 +9,11 @@ const authenticate = (req, res, next) => { sheeps.*, possibilities.can_add_sheeps AS can_add_sheeps, possibilities.can_view_sheeps AS can_view_sheeps, + possibilities.can_manager_sheeps AS can_manager_sheeps, possibilities.can_add_territory AS can_add_territory, possibilities.can_view_territory AS can_view_territory, possibilities.can_manager_territory AS can_manager_territory, + possibilities.can_joint_territory AS can_joint_territory, possibilities.can_add_stand AS can_add_stand, possibilities.can_view_stand AS can_view_stand, possibilities.can_manager_stand AS can_manager_stand, @@ -33,9 +35,11 @@ const authenticate = (req, res, next) => { req.possibilities = { can_add_sheeps: moderator.can_add_sheeps == 1 ? true : false, can_view_sheeps: moderator.can_view_sheeps == 1 ? true : false, + can_manager_sheeps: moderator.can_manager_sheeps == 1 ? true : false, can_add_territory: moderator.can_add_territory == 1 ? true : false, can_view_territory: moderator.can_view_territory == 1 ? true : false, can_manager_territory: moderator.can_manager_territory == 1 ? true : false, + can_joint_territory: moderator.can_joint_territory == 1 ? true : false, can_add_stand: moderator.can_add_stand == 1 ? true : false, can_view_stand: moderator.can_view_stand == 1 ? true : false, can_manager_stand: moderator.can_manager_stand == 1 ? true : false, @@ -51,6 +55,7 @@ const authenticate = (req, res, next) => { sheeps.*, possibilities.can_add_sheeps AS can_add_sheeps, possibilities.can_view_sheeps AS can_view_sheeps, + possibilities.can_manager_sheeps AS can_manager_sheeps, possibilities.can_add_territory AS can_add_territory, possibilities.can_view_territory AS can_view_territory, possibilities.can_manager_territory AS can_manager_territory, @@ -76,8 +81,10 @@ const authenticate = (req, res, next) => { req.possibilities = { can_add_sheeps: false, can_view_sheeps: false, + can_manager_sheeps: false, can_add_territory: false, can_manager_territory: false, + can_joint_territory: false, can_add_stand: false, can_manager_stand: false, can_add_schedule: false, diff --git a/api/middleware/metrics.js b/api/middleware/metrics.js deleted file mode 100644 index ddc8671..0000000 --- a/api/middleware/metrics.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = (req, res, next) => { - const start = performance.now(); - - res.on("finish", () => { - const duration = performance.now() - start; - - fetch("http://metrics:4005/push", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - type: "rest", - path: req.originalUrl, - method: req.method, - status: res.statusCode, - time: duration, - timestamp: Date.now() - }) - }).catch(err => console.error(err)); - }); - - next(); -}; \ No newline at end of file diff --git a/api/routes/homestead.joint.routes.js b/api/routes/homestead.joint.routes.js new file mode 100644 index 0000000..8a062ae --- /dev/null +++ b/api/routes/homestead.joint.routes.js @@ -0,0 +1,12 @@ +const express = require('express'); +const router = express.Router({ mergeParams: true }); +const HomesteadJointController = require('../controllers/homestead.joint.controller'); +const authenticate = require("../middleware/auth"); + +router + .route('/') + .get(authenticate, HomesteadJointController.getList) + .post(authenticate, HomesteadJointController.createJoint) + .delete(authenticate, HomesteadJointController.deleteJoint); + +module.exports = router; \ No newline at end of file diff --git a/api/routes/index.js b/api/routes/index.js index 88c09e5..c0e3b4f 100644 --- a/api/routes/index.js +++ b/api/routes/index.js @@ -1,13 +1,12 @@ const express = require('express'); const router = express.Router(); -const metrics = require('../middleware/metrics'); - const authRoutes = require('./auth.routes'); const sheepsRoutes = require('./sheeps.routes'); const constructorRoutes = require('./constructor.routes'); const housesRoutes = require('./houses.routes'); const homesteadsRoutes = require('./homesteads.routes'); +const homesteadJointRoutes = require('./homestead.joint.routes'); const buildingsRoutes = require('./buildings.routes'); const entrancesRoutes = require('./entrances.routes'); const apartmentsRoutes = require('./apartments.routes'); @@ -20,13 +19,11 @@ const pushRoutes = require('./push.routes'); const generatorCardsRoutes = require('./generator.cards.routes'); const generatorReportTerritoriesRoutes = require('./generator.report.territories.routes'); - -router.use(metrics); - router.use('/auth', authRoutes); router.use('/sheeps?', sheepsRoutes); router.use('/constructor', constructorRoutes); router.use('/houses?', housesRoutes); +router.use('/homestead/joint/:homestead_id', homesteadJointRoutes); router.use('/homesteads?', homesteadsRoutes); router.use('/buildings?', buildingsRoutes); router.use('/house/:house_id/entrances', entrancesRoutes); diff --git a/api/services/auth.service.js b/api/services/auth.service.js index 481ef70..87f9a92 100644 --- a/api/services/auth.service.js +++ b/api/services/auth.service.js @@ -8,9 +8,11 @@ class AuthService { sheeps.*, possibilities.can_add_sheeps AS can_add_sheeps, possibilities.can_view_sheeps AS can_view_sheeps, + possibilities.can_manager_sheeps AS can_manager_sheeps, possibilities.can_add_territory AS can_add_territory, possibilities.can_view_territory AS can_view_territory, possibilities.can_manager_territory AS can_manager_territory, + possibilities.can_joint_territory AS can_joint_territory, possibilities.can_add_stand AS can_add_stand, possibilities.can_view_stand AS can_view_stand, possibilities.can_manager_stand AS can_manager_stand, @@ -42,8 +44,10 @@ class AuthService { possibilities: { can_add_sheeps: false, can_view_sheeps: false, + can_manager_sheeps: false, can_add_territory: false, can_manager_territory: false, + can_joint_territory: false, can_add_stand: false, can_manager_stand: false, can_add_schedule: false, @@ -55,8 +59,10 @@ class AuthService { if (mode && (mode == 1 || mode == 2)) { data.possibilities.can_add_sheeps = sheep.can_add_sheeps == 1 ? true : false; data.possibilities.can_view_sheeps = sheep.can_view_sheeps == 1 ? true : false; + data.possibilities.can_manager_sheeps = sheep.can_manager_sheeps == 1 ? true : false; data.possibilities.can_add_territory = sheep.can_add_territory == 1 ? true : false; data.possibilities.can_manager_territory = sheep.can_manager_territory == 1 ? true : false; + data.possibilities.can_joint_territory = sheep.can_joint_territory == 1 ? true : false; data.possibilities.can_add_stand = sheep.can_add_stand == 1 ? true : false; data.possibilities.can_manager_stand = sheep.can_manager_stand == 1 ? true : false; data.possibilities.can_add_schedule = sheep.can_add_schedule == 1 ? true : false; diff --git a/api/services/entrances.service.js b/api/services/entrances.service.js index f2a9b43..be496a2 100644 --- a/api/services/entrances.service.js +++ b/api/services/entrances.service.js @@ -6,6 +6,8 @@ class EntrancesService { let sql = ` SELECT entrance.*, + (SELECT house.title FROM house WHERE house.id = entrance.house_id) AS address_title, + (SELECT house.number FROM house WHERE house.id = entrance.house_id) AS address_number, COALESCE((SELECT entrance_history.working FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1), 0) AS working, (SELECT entrance_history.name FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1) AS entrance_history_name, (SELECT entrance_history.group_id FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1) AS entrance_history_group_id, @@ -30,6 +32,11 @@ class EntrancesService { "house_id": Number(row.house_id), "entrance_number": Number(row.entrance_number), "title": row.title, + "address": { + "title": row.address_title, + "number": row.address_number, + "points_number": JSON.parse(row.points_number) + }, "description": row.description, "created_at": Number(row.created_at), "updated_at": Number(row.updated_at), diff --git a/api/services/history.homestead.service.js b/api/services/history.homestead.service.js index e5921ec..d243172 100644 --- a/api/services/history.homestead.service.js +++ b/api/services/history.homestead.service.js @@ -1,4 +1,5 @@ const db = require("../config/db"); +const Notification = require("../utils/notification.js"); class HistoryHomesteadService { getHistoryHomestead(homestead_id) { diff --git a/api/services/homestead.joint.service.js b/api/services/homestead.joint.service.js new file mode 100644 index 0000000..bd7f031 --- /dev/null +++ b/api/services/homestead.joint.service.js @@ -0,0 +1,87 @@ +const db = require("../config/db"); +const Notification = require("../utils/notification.js"); + +class HomesteadJointService { + getList(homestead_id) { + return new Promise((res, rej) => { + let sql = ` + SELECT + * + FROM + homestead_joint + WHERE + homestead_joint.homestead_id = '${homestead_id}' + ORDER BY + homestead_joint.created_at + `; + + db.all(sql, (err, rows) => { + if (err) { + console.error(err.message); + return res(false); + } else { + let data = rows.map((row) => { + return { + "id": Number(row.id), + "homestead_id": Number(row.homestead_id), + "sheep_id": Number(row.sheep_id), + "created_at": Number(row.created_at) + } + }) + + return res(data); + } + }); + }); + } + + createJoint(homestead_id, data) { + return new Promise((res, rej) => { + let sql = 'INSERT INTO homestead_joint(homestead_id, sheep_id, created_at) VALUES (?, ?, ?)'; + + db.run(sql, [ + Number(homestead_id), + Number(data.sheep_id), + Date.now() + ], function (err) { + if (err) { + console.error(err.message); + return res(false); + } else if (this.changes === 0) { + return res(false); + } else { + Notification.sendSheep({ + sheep_id: Number(data.sheep_id), + title: "Тимчасова територія", + body: "Вам надали спільний доступ до території" + }); + + res({ "create": "ok", "id": this.lastID }); + } + }); + }); + } + + deleteJoint(homestead_id, data) { + return new Promise((res, rej) => { + db.run('DELETE FROM homestead_joint WHERE homestead_id = ? AND sheep_id = ?', [Number(homestead_id), Number(data.sheep_id)], function (err) { + if (err) { + console.error(err.message); + return res(false); + } else if (this.changes === 0) { + return res(false); + } else { + Notification.sendSheep({ + sheep_id: Number(data.sheep_id), + title: "Тимчасова територія", + body: "Вам відкликанно спільний доступ до території" + }); + + res({ "delete": "ok", "homestead_id": Number(homestead_id), "sheep_id": Number(data.sheep_id)}); + } + }); + }); + } +} + +module.exports = new HomesteadJointService(); \ No newline at end of file diff --git a/api/services/homesteads.service.js b/api/services/homesteads.service.js index cb92125..fd53dfb 100644 --- a/api/services/homesteads.service.js +++ b/api/services/homesteads.service.js @@ -1,4 +1,5 @@ const db = require("../config/db"); +const genCards = require("../middleware/genCards"); class HomesteadsService { getList(mode, id) { @@ -63,6 +64,27 @@ class HomesteadsService { AND homestead_history.sheep_id = '${id}'; `; + } else if (mode == "joint") { + sql = ` + SELECT + homestead.*, + homestead_history.id AS homestead_history_id, + homestead_history.name AS homestead_history_name, + homestead_history.group_id AS homestead_history_group_id, + homestead_history.sheep_id AS homestead_history_sheep_id, + homestead_history.date_start AS homestead_history_date_start, + homestead_history.date_end AS homestead_history_date_end + FROM + homestead + JOIN + homestead_history ON homestead.id = homestead_history.homestead_id + JOIN + homestead_joint ON homestead.id = homestead_joint.homestead_id + WHERE + homestead_joint.sheep_id = '${id}' + ORDER BY + homestead_history.date_start DESC; + `; } db.all(sql, (err, rows) => { @@ -88,7 +110,7 @@ class HomesteadsService { "id": row.homestead_history_id ? Number(row.homestead_history_id) : null, "name": row.homestead_history_name, "group_id": row.homestead_history_group_id ? Number(row.homestead_history_group_id) : null, - "sheep_id": row.entrance_history_sheep_id ? Number(row.entrance_history_sheep_id) : null, + "sheep_id": row.homestead_history_sheep_id ? Number(row.homestead_history_sheep_id) : null, "date": { "start": row.homestead_history_date_start ? Number(row.homestead_history_date_start) : null, "end": row.homestead_history_date_end ? Number(row.homestead_history_date_end) : null @@ -111,6 +133,7 @@ class HomesteadsService { COALESCE((SELECT homestead_history.working FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1), 0) AS working, (SELECT homestead_history.name FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_name, (SELECT homestead_history.group_id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_group_id, + (SELECT homestead_history.sheep_id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_sheep_id, (SELECT homestead_history.id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_id, (SELECT homestead_history.date_start FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_date_start, (SELECT homestead_history.date_end FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_date_end @@ -144,6 +167,7 @@ class HomesteadsService { "id": row.homestead_history_id ? Number(row.homestead_history_id) : null, "name": row.homestead_history_name, "group_id": row.homestead_history_group_id ? Number(row.homestead_history_group_id) : null, + "sheep_id": row.homestead_history_sheep_id ? Number(row.homestead_history_sheep_id) : null, "date": { "start": row.homestead_history_date_start ? Number(row.homestead_history_date_start) : null, "end": row.homestead_history_date_end ? Number(row.homestead_history_date_end) : null @@ -192,6 +216,7 @@ class HomesteadsService { return res(false); } else { res({ "status": "ok", "id": this.lastID }); + genCards({type: "homestead", id: this.lastID}); } }); }); @@ -234,6 +259,7 @@ class HomesteadsService { return res(false); } else { res({ "status": "ok", "id": homestead_id }); + genCards({type: "homestead", id: homestead_id}); } }); }); diff --git a/api/services/houses.service.js b/api/services/houses.service.js index 62b4e30..32e867f 100644 --- a/api/services/houses.service.js +++ b/api/services/houses.service.js @@ -1,4 +1,5 @@ const db = require("../config/db"); +const genCards = require("../middleware/genCards"); class HousesService { getListEntrance() { @@ -228,6 +229,7 @@ class HousesService { return res(false); } else { res({ "status": "ok", "id": this.lastID }); + genCards({type: "house", id: this.lastID}); } }); }); @@ -272,6 +274,8 @@ class HousesService { return res(false); } else { res({ "status": "ok", "id": house_id }); + + genCards({type: "house", id: house_id}); } }); }); diff --git a/api/services/sheeps.service.js b/api/services/sheeps.service.js index ea43201..72ed36e 100644 --- a/api/services/sheeps.service.js +++ b/api/services/sheeps.service.js @@ -9,9 +9,11 @@ class SheepService { sheeps.*, possibilities.can_add_sheeps, possibilities.can_view_sheeps, + possibilities.can_manager_sheeps, possibilities.can_add_territory, possibilities.can_view_territory, possibilities.can_manager_territory, + possibilities.can_joint_territory, possibilities.can_add_stand, possibilities.can_view_stand, possibilities.can_manager_stand, @@ -38,9 +40,11 @@ class SheepService { const fields = [ "can_add_sheeps", "can_view_sheeps", + "can_manager_sheeps", "can_add_territory", "can_view_territory", "can_manager_territory", + "can_joint_territory", "can_add_stand", "can_view_stand", "can_manager_stand", @@ -83,9 +87,11 @@ class SheepService { sheeps.*, possibilities.can_add_sheeps, possibilities.can_view_sheeps, + possibilities.can_manager_sheeps, possibilities.can_add_territory, possibilities.can_view_territory, possibilities.can_manager_territory, + possibilities.can_joint_territory, possibilities.can_add_stand, possibilities.can_view_stand, possibilities.can_manager_stand, @@ -108,9 +114,11 @@ class SheepService { const fields = [ "can_add_sheeps", "can_view_sheeps", + "can_manager_sheeps", "can_add_territory", "can_view_territory", "can_manager_territory", + "can_joint_territory", "can_add_stand", "can_view_stand", "can_manager_stand", @@ -156,9 +164,11 @@ class SheepService { sheeps.*, possibilities.can_add_sheeps, possibilities.can_view_sheeps, + possibilities.can_manager_sheeps, possibilities.can_add_territory, possibilities.can_view_territory, possibilities.can_manager_territory, + possibilities.can_joint_territory, possibilities.can_add_stand, possibilities.can_view_stand, possibilities.can_manager_stand, @@ -183,9 +193,11 @@ class SheepService { const fields = [ "can_add_sheeps", "can_view_sheeps", + "can_manager_sheeps", "can_add_territory", "can_view_territory", "can_manager_territory", + "can_joint_territory", "can_add_stand", "can_view_stand", "can_manager_stand", @@ -253,71 +265,120 @@ class SheepService { }); } - updateSheep(data) { - const stmt1 = db.prepare(` - UPDATE - sheeps - SET - name = ?, - group_id = ?, - mode = ?, - mode_title = ?, - uuid_manager = ? - WHERE - uuid = ? - `); + updateSheep(data, mode) { + if (mode == 2) { + const stmt1 = db.prepare(` + UPDATE + sheeps + SET + name = ?, + group_id = ?, + mode = ?, + mode_title = ?, + uuid_manager = ? + WHERE + uuid = ? + `); - const stmt2 = db.prepare(` - UPDATE - possibilities - SET - can_add_sheeps = ?, - can_view_sheeps = ?, - can_add_territory = ?, - can_view_territory = ?, - can_manager_territory = ?, - can_add_stand = ?, - can_view_stand = ?, - can_manager_stand = ?, - can_add_schedule = ?, - can_view_schedule = ? - WHERE - sheep_id = (SELECT id FROM sheeps WHERE uuid = ? LIMIT 1) - `); + const stmt2 = db.prepare(` + UPDATE + possibilities + SET + can_add_sheeps = ?, + can_view_sheeps = ?, + can_manager_sheeps = ?, + can_add_territory = ?, + can_view_territory = ?, + can_manager_territory = ?, + can_joint_territory = ?, + can_add_stand = ?, + can_view_stand = ?, + can_manager_stand = ?, + can_add_schedule = ?, + can_view_schedule = ? + WHERE + sheep_id = (SELECT id FROM sheeps WHERE uuid = ? LIMIT 1) + `); - return new Promise((res, rej) => { - db.serialize(() => { - let uuid_manager = crypto.randomUUID(); + return new Promise((res, rej) => { + db.serialize(() => { + let uuid_manager = crypto.randomUUID(); - stmt1.run([ - data.name, - Number(data.group_id), - Number(data.mode), - data.mode_title, - Number(data.mode) == 0 ? null : (data.uuid_manager ? data.uuid_manager : uuid_manager), - data.uuid - ], (err) => { - if (err) return rej(err); - - stmt2.run([ - data.possibilities.can_add_sheeps, - data.possibilities.can_view_sheeps, - data.possibilities.can_add_territory, - data.possibilities.can_view_territory, - data.possibilities.can_manager_territory, - data.possibilities.can_add_stand, - data.possibilities.can_view_stand, - data.possibilities.can_manager_stand, - data.possibilities.can_add_schedule, - data.possibilities.can_view_schedule, + stmt1.run([ + data.name, + Number(data.group_id), + Number(data.mode), + data.mode_title, + Number(data.mode) == 0 ? null : (data.uuid_manager ? data.uuid_manager : uuid_manager), data.uuid - ], (err2) => { - if (err2) return rej(err2); - res({ status: "ok", id: data.id }); + ], (err) => { + if (err) return rej(err); + + stmt2.run([ + data.possibilities.can_add_sheeps, + data.possibilities.can_view_sheeps, + data.possibilities.can_manager_sheeps, + data.possibilities.can_add_territory, + data.possibilities.can_view_territory, + data.possibilities.can_manager_territory, + data.possibilities.can_joint_territory, + data.possibilities.can_add_stand, + data.possibilities.can_view_stand, + data.possibilities.can_manager_stand, + data.possibilities.can_add_schedule, + data.possibilities.can_view_schedule, + data.uuid + ], (err2) => { + if (err2) return rej(err2); + res({ status: "ok", id: data.id }); + }); }); }); }); - }); + } else if(mode == 1){ + const stmt1 = db.prepare(` + UPDATE + sheeps + SET + name = ?, + group_id = ? + WHERE + uuid = ? + `); + + const stmt2 = db.prepare(` + UPDATE + possibilities + SET + can_view_territory = ?, + can_view_stand = ?, + can_view_schedule = ? + WHERE + sheep_id = (SELECT id FROM sheeps WHERE uuid = ? LIMIT 1) + `); + + return new Promise((res, rej) => { + db.serialize(() => { + stmt1.run([ + data.name, + Number(data.group_id), + data.uuid + ], (err) => { + if (err) return rej(err); + + stmt2.run([ + data.possibilities.can_view_territory, + data.possibilities.can_view_stand, + data.possibilities.can_view_schedule, + data.uuid + ], (err2) => { + if (err2) return rej(err2); + res({ status: "ok", id: data.id }); + }); + }); + }); + }); + } } deleteSheep(data) { diff --git a/api/services/stand.service.js b/api/services/stand.service.js index 8ce973c..ede7032 100644 --- a/api/services/stand.service.js +++ b/api/services/stand.service.js @@ -221,7 +221,7 @@ class StandService { } const normalized = normalizeTs(row && row.max_date ? row.max_date : null); - if (normalized) { + if (normalized && normalized > Date.now()) { date_start = getNextMonday(normalized); } else { date_start = getNextMonday(Date.now()); @@ -272,9 +272,26 @@ class StandService { console.error(err.message); return res(false); } + let text = [ + `Стенд «${stand.title}» отримав новий графік. Можна сміливо записуватися 🙂`, + `Для «${stand.title}» відкрито новий розклад. Хто планував — саме час.`, + `Новий графік для «${stand.title}» вже доступний. Обирайте зручний час 👍`, + `Стенд «${stand.title}» оновив розклад. Запис розпочато.`, + `З’явилися нові дати у «${stand.title}». Встигніть обрати свою 😉`, + `«${stand.title}» відкрив новий період запису. Плануємо заздалегідь 🙂`, + `Оновлення для «${stand.title}». Додано новий графік.`, + `Новий розклад для «${stand.title}» вже чекає на охочих 📋`, + `Стенд «${stand.title}» додав нові години для запису ⏰`, + `Графік «${stand.title}» поповнено. Можна бронювати час 😊`, + `У «${stand.title}» з’явилися нові можливості для запису`, + `Свіжий графік для «${stand.title}» уже доступний 🚀` + ]; + + let randomMessage = text[Math.floor(Math.random() * text.length)]; + Notification.sendStand({ - title: "Додан новий день служіння", - body: `Стенд «${stand.title}» поповнився, встигніть записатися.`, + title: "Додано новий день служіння", + body: randomMessage, page: `/stand/card/${stand.id}` }); @@ -301,7 +318,7 @@ class StandService { WHERE ss.stand_id = ? AND - date(ss.date / 1000, 'unixepoch') >= date('now') + date(ss.date / 1000, 'unixepoch', 'localtime') >= date('now', 'localtime') ORDER BY ss.id; `; diff --git a/api/utils/notification.js b/api/utils/notification.js index 81351ed..eb76106 100644 --- a/api/utils/notification.js +++ b/api/utils/notification.js @@ -1,5 +1,9 @@ const db = require("../config/db"); const webpush = require('web-push'); +const TelegramBot = require("node-telegram-bot-api"); + +const util = require('util'); +const dbRun = util.promisify(db.run).bind(db); const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY; const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY; @@ -10,6 +14,11 @@ webpush.setVapidDetails( VAPID_PRIVATE_KEY ); +const TOKEN = process.env.TELEGRAM_TOKEN; +const STAND_CHAT_ID = process.env.STAND_CHAT_ID; + +const bot = new TelegramBot(TOKEN, { polling: false }); + class Notification { async sendSheep({ sheep_id, title, body, page }) { const sql = ` @@ -123,7 +132,7 @@ class Notification { } if (!rows.length) { - console.log(`🐑 No subscriptions found for sheep_id: ${sheep_id}`); + console.log(`🐑 No subscriptions`); return; } @@ -146,6 +155,25 @@ class Notification { const failed = results.filter(r => r.status === 'rejected').length; console.log(`✅ Sent: ${rows.length - failed}, ❌ Failed: ${failed}`); }); + + // Формуємо повне повідомлення + const fullMessage = `📢 ${title}\n\n${body.replace('«', '«').replace('»', '»')}`; + + try { + const sentMessage = await bot.sendMessage(STAND_CHAT_ID, fullMessage, { + parse_mode: 'HTML' + }); + + // Зберігаємо ID нового повідомлення у базі + await dbRun( + `INSERT INTO sent_messages (last_message_id, created_at) VALUES (?, ?)`, [sentMessage.message_id, Date.now()] + ); + + console.log(`✅ Сповіщення надіслано для стенду: ${stand.title}`); + } catch (err) { + console.error('❌ Помилка відправки тексту:', err.message); + } + } }; diff --git a/cron/Dockerfile b/cron/Dockerfile index 40a4f30..9d4e158 100644 --- a/cron/Dockerfile +++ b/cron/Dockerfile @@ -8,4 +8,8 @@ RUN npm install COPY . . +COPY ./fonts/ /usr/share/fonts/truetype/roboto/ + +RUN fc-cache -f -v + CMD npm start diff --git a/cron/cron.js b/cron/cron.js index c2a7097..8acfc3a 100644 --- a/cron/cron.js +++ b/cron/cron.js @@ -1,13 +1,57 @@ const cron = require("node-cron"); const Backup = require("./tasks/backup"); +const Stand = require("./tasks/stands"); +const Messages = require('./tasks/messages'); +const Rept = require('./tasks/rept'); -// Завдання: резервна копія БД кожен день в 22:30 +// 1. Резервна копія БД щодня о 22:35 cron.schedule("30 22 * * *", () => { Backup.database(); - - const now = new Date().toLocaleString(); - console.log(`[${now}] Завдання «Backup» виконане!`); + console.log(`[${new Date().toLocaleString()}] Завдання «Backup» виконане!`); }); +// 2. Перевірка стендів без графіку щосуботи о 18:00 +cron.schedule("0 18 * * 6", async () => { + console.log(`[${new Date().toLocaleString()}] Запуск перевірки стендів без графіку...`); + try { + await Stand.check_add(); + console.log(`[${new Date().toLocaleString()}] Завдання «Stand check_add» виконане!`); + } catch (err) { + console.error("Помилка Stand check_add:", err.message); + } +}); + +// 3. Відправка графіку стендів щодня о 18:00 +cron.schedule("0 18 * * *", async () => { + console.log(`[${new Date().toLocaleString()}] Запуск відправки графіків...`); + try { + await Stand.check_entries(); + console.log(`[${new Date().toLocaleString()}] Завдання «Stand check_entries» виконане!`); + } catch (err) { + console.error("Помилка Stand check_entries:", err.message); + } +}); + +// 4. Очищення старих повідомлень щодня о 17:59 +cron.schedule('59 17 * * *', async () => { + console.log(`[${new Date().toLocaleString()}] Запуск очищення старих повідомлень...`); + try { + await Messages.cleanup_old(); + console.log(`[${new Date().toLocaleString()}] Завдання «Messages cleanup_old» виконане успішно!`); + } catch (err) { + console.error(`[${new Date().toLocaleString()}] Помилка Messages cleanup_old:`, err.message); + } +}); + +// 5. Нагадування про звіт щомісяця о 10:00 +cron.schedule('00 10 1 * *', async () => { + console.log(`[${new Date().toLocaleString()}] Запуск відправки повідомлень...`); + try { + await Rept.send_notification(); + console.log(`[${new Date().toLocaleString()}] Завдання Rept send_notification виконане успішно!`); + } catch (err) { + console.error(`[${new Date().toLocaleString()}] Помилка Rept send_notification:`, err.message); + } +}); console.log("Cron-завдання запущено."); \ No newline at end of file diff --git a/cron/font/RobotoMono-Bold.ttf b/cron/font/RobotoMono-Bold.ttf new file mode 100644 index 0000000..bef439f Binary files /dev/null and b/cron/font/RobotoMono-Bold.ttf differ diff --git a/cron/font/RobotoMono-Light.ttf b/cron/font/RobotoMono-Light.ttf new file mode 100644 index 0000000..b6fb475 Binary files /dev/null and b/cron/font/RobotoMono-Light.ttf differ diff --git a/cron/font/RobotoMono-Medium.ttf b/cron/font/RobotoMono-Medium.ttf new file mode 100644 index 0000000..53fdd40 Binary files /dev/null and b/cron/font/RobotoMono-Medium.ttf differ diff --git a/cron/font/RobotoMono-Regular.ttf b/cron/font/RobotoMono-Regular.ttf new file mode 100644 index 0000000..3806bfb Binary files /dev/null and b/cron/font/RobotoMono-Regular.ttf differ diff --git a/cron/fonts/RobotoMono-Bold.ttf b/cron/fonts/RobotoMono-Bold.ttf new file mode 100644 index 0000000..bef439f Binary files /dev/null and b/cron/fonts/RobotoMono-Bold.ttf differ diff --git a/cron/fonts/RobotoMono-Light.ttf b/cron/fonts/RobotoMono-Light.ttf new file mode 100644 index 0000000..b6fb475 Binary files /dev/null and b/cron/fonts/RobotoMono-Light.ttf differ diff --git a/cron/fonts/RobotoMono-Medium.ttf b/cron/fonts/RobotoMono-Medium.ttf new file mode 100644 index 0000000..53fdd40 Binary files /dev/null and b/cron/fonts/RobotoMono-Medium.ttf differ diff --git a/cron/fonts/RobotoMono-Regular.ttf b/cron/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000..3806bfb Binary files /dev/null and b/cron/fonts/RobotoMono-Regular.ttf differ diff --git a/cron/package-lock.json b/cron/package-lock.json index b515d3d..0e8492a 100644 --- a/cron/package-lock.json +++ b/cron/package-lock.json @@ -9,13 +9,816 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-s3": "^3.1004.0", "dotenv": "^17.2.0", "node-cron": "^4.2.1", "node-telegram-bot-api": "^0.66.0", + "sharp": "^0.34.5", "sqlite3": "^5.1.7", "web-push": "^3.6.7" } }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1004.0.tgz", + "integrity": "sha512-m0zNfpsona9jQdX1cHtHArOiuvSGZPsgp/KRZS2YjJhKah96G2UN3UNGZQ6aVjXIQjCY6UanCJo0uW9Xf2U41w==", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-node": "^3.972.18", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.7", + "@aws-sdk/middleware-expect-continue": "^3.972.7", + "@aws-sdk/middleware-flexible-checksums": "^3.973.4", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-location-constraint": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-sdk-s3": "^3.972.18", + "@aws-sdk/middleware-ssec": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/signature-v4-multi-region": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/eventstream-serde-browser": "^4.2.11", + "@smithy/eventstream-serde-config-resolver": "^4.3.11", + "@smithy/eventstream-serde-node": "^4.2.11", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-blob-browser": "^4.2.12", + "@smithy/hash-node": "^4.2.11", + "@smithy/hash-stream-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/md5-js": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.18.tgz", + "integrity": "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/xml-builder": "^3.972.10", + "@smithy/core": "^3.23.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.4.tgz", + "integrity": "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.16.tgz", + "integrity": "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.18.tgz", + "integrity": "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.17.tgz", + "integrity": "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-env": "^3.972.16", + "@aws-sdk/credential-provider-http": "^3.972.18", + "@aws-sdk/credential-provider-login": "^3.972.17", + "@aws-sdk/credential-provider-process": "^3.972.16", + "@aws-sdk/credential-provider-sso": "^3.972.17", + "@aws-sdk/credential-provider-web-identity": "^3.972.17", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.17.tgz", + "integrity": "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.18.tgz", + "integrity": "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.16", + "@aws-sdk/credential-provider-http": "^3.972.18", + "@aws-sdk/credential-provider-ini": "^3.972.17", + "@aws-sdk/credential-provider-process": "^3.972.16", + "@aws-sdk/credential-provider-sso": "^3.972.17", + "@aws-sdk/credential-provider-web-identity": "^3.972.17", + "@aws-sdk/types": "^3.973.5", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.16.tgz", + "integrity": "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.17.tgz", + "integrity": "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/token-providers": "3.1004.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.17.tgz", + "integrity": "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.7.tgz", + "integrity": "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.7.tgz", + "integrity": "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.973.4.tgz", + "integrity": "sha512-7CH2jcGmkvkHc5Buz9IGbdjq1729AAlgYJiAvGq7qhCHqYleCsriWdSnmsqWTwdAfXHMT+pkxX3w6v5tJNcSug==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/crc64-nvme": "^3.972.4", + "@aws-sdk/types": "^3.973.5", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz", + "integrity": "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.7.tgz", + "integrity": "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz", + "integrity": "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz", + "integrity": "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.18.tgz", + "integrity": "sha512-5E3XxaElrdyk6ZJ0TjH7Qm6ios4b/qQCiLr6oQ8NK7e4Kn6JBTJCaYioQCQ65BpZ1+l1mK5wTAac2+pEz0Smpw==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.7.tgz", + "integrity": "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.19.tgz", + "integrity": "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@smithy/core": "^3.23.8", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-retry": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.7.tgz", + "integrity": "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz", + "integrity": "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/config-resolver": "^4.4.10", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.6.tgz", + "integrity": "sha512-NnsOQsVmJXy4+IdPFUjRCWPn9qNH1TzS/f7MiWgXeoHs903tJpAWQWQtoFvLccyPoBgomKP9L89RRr2YsT/L0g==", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1004.0.tgz", + "integrity": "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", + "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz", + "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-endpoints": "^3.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz", + "integrity": "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.4.tgz", + "integrity": "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/types": "^3.973.5", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", + "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", + "dependencies": { + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@cypress/request": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", @@ -75,12 +878,461 @@ "node": ">=6" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -105,6 +1357,687 @@ "node": ">=10" } }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz", + "integrity": "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.10.tgz", + "integrity": "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.9", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.9.tgz", + "integrity": "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==", + "dependencies": { + "@smithy/middleware-serde": "^4.2.12", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.11.tgz", + "integrity": "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.11.tgz", + "integrity": "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.11.tgz", + "integrity": "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.11.tgz", + "integrity": "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.11.tgz", + "integrity": "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.11.tgz", + "integrity": "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.13.tgz", + "integrity": "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.12.tgz", + "integrity": "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ==", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.11.tgz", + "integrity": "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.11.tgz", + "integrity": "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ==", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.11.tgz", + "integrity": "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.11.tgz", + "integrity": "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng==", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.11.tgz", + "integrity": "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.23", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.23.tgz", + "integrity": "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw==", + "dependencies": { + "@smithy/core": "^3.23.9", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-middleware": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.40", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.40.tgz", + "integrity": "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/service-error-classification": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.12.tgz", + "integrity": "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.11.tgz", + "integrity": "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.11.tgz", + "integrity": "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==", + "dependencies": { + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.14.tgz", + "integrity": "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==", + "dependencies": { + "@smithy/abort-controller": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.11.tgz", + "integrity": "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.11.tgz", + "integrity": "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.11.tgz", + "integrity": "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.11.tgz", + "integrity": "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.11.tgz", + "integrity": "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==", + "dependencies": { + "@smithy/types": "^4.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.6.tgz", + "integrity": "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.11.tgz", + "integrity": "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.3.tgz", + "integrity": "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==", + "dependencies": { + "@smithy/core": "^3.23.9", + "@smithy/middleware-endpoint": "^4.4.23", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.11.tgz", + "integrity": "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==", + "dependencies": { + "@smithy/querystring-parser": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.39", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.39.tgz", + "integrity": "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ==", + "dependencies": { + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.42.tgz", + "integrity": "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A==", + "dependencies": { + "@smithy/config-resolver": "^4.4.10", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.2.tgz", + "integrity": "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.11.tgz", + "integrity": "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.11.tgz", + "integrity": "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==", + "dependencies": { + "@smithy/service-error-classification": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.17", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.17.tgz", + "integrity": "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.11.tgz", + "integrity": "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A==", + "dependencies": { + "@smithy/abort-controller": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -387,6 +2320,11 @@ "version": "4.12.2", "license": "MIT" }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -957,6 +2895,35 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "peer": true }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", @@ -2815,6 +4782,49 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -3161,6 +5171,17 @@ "node": ">=0.10.0" } }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -3275,6 +5296,11 @@ "node": ">=16" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/cron/package.json b/cron/package.json index 53ece9a..2242824 100644 --- a/cron/package.json +++ b/cron/package.json @@ -9,10 +9,12 @@ "license": "ISC", "description": "", "dependencies": { - "node-cron": "^4.2.1", - "sqlite3": "^5.1.7", - "web-push": "^3.6.7", + "@aws-sdk/client-s3": "^3.1004.0", "dotenv": "^17.2.0", - "node-telegram-bot-api": "^0.66.0" + "node-cron": "^4.2.1", + "node-telegram-bot-api": "^0.66.0", + "sharp": "^0.34.5", + "sqlite3": "^5.1.7", + "web-push": "^3.6.7" } } diff --git a/cron/tasks/backup.js b/cron/tasks/backup.js index 7ecbee7..64337a4 100644 --- a/cron/tasks/backup.js +++ b/cron/tasks/backup.js @@ -1,6 +1,7 @@ const fs = require("fs"); const path = require("path"); const TelegramBot = require("node-telegram-bot-api"); +const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); const TOKEN = process.env.TELEGRAM_TOKEN; const CHAT_ID = process.env.CHAT_ID; @@ -9,6 +10,16 @@ const FILE = path.join(DB_PATH, "database.sqlite"); const bot = new TelegramBot(TOKEN, { polling: false }); +// Настройки S3 / R2 +const s3Client = new S3Client({ + region: "auto", + endpoint: process.env.S3_ENDPOINT, // Напр: https://.r2.cloudflarestorage.com + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY_ID, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, + }, +}); + class Backup { async database() { try { @@ -17,13 +28,25 @@ class Backup { return; } - console.log(`📤 Надсилаю файл: ${FILE}`); + const fileName = `backup_${new Date().toISOString().replace(/[:.]/g, "-")}.sqlite`; + // 1. Отправка в Telegram await bot.sendDocument(CHAT_ID, fs.createReadStream(FILE), { - caption: "📦 Резервна копія бази даних", + caption: `📦 Резервна копія бази даних`, }); + console.log("✅ Telegram: успішно надіслано"); - console.log("✅ Файл успішно надіслано!"); + // 2. Отправка в S3 (R2) + const fileBuffer = fs.readFileSync(FILE); + const uploadParams = { + Bucket: process.env.S3_BUCKET_NAME, + Key: `sheep-service.com/database/${fileName}`, // Путь внутри бакета + Body: fileBuffer, + ContentType: "application/x-sqlite3", + }; + + await s3Client.send(new PutObjectCommand(uploadParams)); + console.log("✅ S3: успішно завантажено в хмару"); } catch (err) { console.error("❌ Помилка при надсиланні файлу:", err.message); } diff --git a/cron/tasks/messages.js b/cron/tasks/messages.js new file mode 100644 index 0000000..e3e4b48 --- /dev/null +++ b/cron/tasks/messages.js @@ -0,0 +1,55 @@ +const db = require("../config/db"); +const TelegramBot = require("node-telegram-bot-api"); + +const util = require('util'); +const dbAll = util.promisify(db.all).bind(db); +const dbRun = util.promisify(db.run).bind(db); + +const TOKEN = process.env.TELEGRAM_TOKEN; +const STAND_CHAT_ID = process.env.STAND_CHAT_ID; + +const bot = new TelegramBot(TOKEN, { polling: false }); + +class Messages { + async cleanup_old() { + try { + // "Зараз мінус 24 години" + const oneDayAgo = Date.now() - (24 * 60 * 55 * 1000); + + // 1. Отримуємо повідомлення, які старші за добу + const oldMessages = await dbAll( + `SELECT last_message_id FROM sent_messages WHERE created_at < ?`, + [oneDayAgo] + ); + + if (!oldMessages || oldMessages.length === 0) { + console.log('🧹 Застарілих повідомлень не знайдено.'); + return; + } + + console.log(`🧹 Знайдено ${oldMessages.length} повідомлень для видалення...`); + + // 2. Видаляємо повідомлення з Telegram по черзі + for (const msg of oldMessages) { + try { + await bot.deleteMessage(STAND_CHAT_ID, msg.last_message_id); + await new Promise(resolve => setTimeout(resolve, 50)); // пауза 50мс + } catch (e) { + // Код помилки 400 зазвичай означає, що повідомлення вже видалено вручну + // або минуло понад 48 годин + console.log(`⚠️ Повідомлення ${msg.last_message_id} не знайдено в Telegram або застаре.`); + } + } + + // 3. Видаляємо записи з бази даних одним махом + await dbRun(`DELETE FROM sent_messages WHERE created_at < ?`, [oneDayAgo]); + + console.log(`✅ Очистка завершена.`); + + } catch (err) { + console.error('❌ Помилка під час виконання cleanup_old:', err.message); + } + } +} + +module.exports = new Messages(); \ No newline at end of file diff --git a/cron/tasks/rept.js b/cron/tasks/rept.js new file mode 100644 index 0000000..7692bc3 --- /dev/null +++ b/cron/tasks/rept.js @@ -0,0 +1,28 @@ +const Notification = require("../utils/notification"); + +class Rept { + async send_notification() { + let text = [ + 'Час підбивати підсумки! Не забудьте здати свій звіт про служіння за минулий місяць 📋', + 'Нагадуємо про щомісячний звіт. Будь ласка, надішліть його сьогодні 😊', + 'Кінець місяця вже тут, а це значить — пора здавати звіти про служіння 🕒', + 'Ваш звіт дуже важливий! Не забудьте поділитися результатами служіння за цей місяць.', + 'Маленьке нагадування: пора заповнити звіт про служіння за місяць. Дякуємо за вашу працю!', + 'Ще не здали звіт? Саме час це зробити! 😉', + 'Звітність — це порядок. Будь ласка, надішліть дані про своє служіння за минулий місяць.', + 'Місяць завершився, тож не забудьте прозвітувати про ваші успіхи в служінні 📝', + 'Чекаємо на ваш звіт! Це допоможе нам мати загальну картину нашого спільного служіння.', + 'Пора здавати звіти! Дякуємо кожному за активність у цьому місяці ✨', + 'Не відкладайте на потім — здайте звіт про служіння вже зараз 📋' + ]; + let randomMessage = text[Math.floor(Math.random() * text.length)]; + + Notification.sendAll({ + title: "Звіт про служіння", + body: randomMessage, + page: `/` + }); + } +} + +module.exports = new Rept(); \ No newline at end of file diff --git a/cron/tasks/stands.js b/cron/tasks/stands.js new file mode 100644 index 0000000..d4ac68c --- /dev/null +++ b/cron/tasks/stands.js @@ -0,0 +1,260 @@ +const db = require("../config/db"); +const Notification = require("../utils/notification"); +const sharp = require('sharp'); +const TelegramBot = require("node-telegram-bot-api"); + +const util = require('util'); +const dbAll = util.promisify(db.all).bind(db); +const dbRun = util.promisify(db.run).bind(db); + +const TOKEN = process.env.TELEGRAM_TOKEN; +const STAND_CHAT_ID = process.env.STAND_CHAT_ID; + +const bot = new TelegramBot(TOKEN, { polling: false }); + + +class Stands { + async check_add() { + const sqlStands = ` + SELECT id + FROM stand_list + WHERE status = '1' + ORDER BY id + `; + + db.all(sqlStands, (err, stands) => { + if (err) { + console.error('DB error:', err.message); + return; + } + + if (!stands.length) { + console.log('There are no active stands'); + return; + } + + const dateNow = Date.now(); + + const sqlSchedule = ` + SELECT 1 + FROM stand_schedule + WHERE stand_id = ? + AND date >= ? + LIMIT 1 + `; + + let checked = 0; + let emptyStands = 0; + + stands.forEach(stand => { + db.get(sqlSchedule, [stand.id, dateNow], (err, row) => { + if (err) { + console.error('Schedule error:', err.message); + return; + } + + if (!row) emptyStands++; + + checked++; + + if (checked === stands.length) { + console.log('Empty stands:', emptyStands); + + if (emptyStands > 0) { + Notification.sendStandAdd({ + title: 'Додайте нові дні служіння', + body: `${emptyStands} з ${stands.length} стендів потребують додавання днів служіння.`, + page: '/stand' + }); + } + } + }); + }); + }); + } + + async check_entries() { + try { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // 1. Отримуємо вісників і одразу робимо Map для швидкого пошуку + const sheeps = await dbAll(`SELECT id, name FROM sheeps`); + if (!sheeps.length) return console.log('There are no sheeps'); + + const sheepMap = new Map(sheeps.map(s => [s.id, s.name])); + + const startTomorrow = today.getTime() + 86400000; + const startDayAfterTomorrow = today.getTime() + 86400000 * 2; + + const sqlStands = ` + SELECT + stand_schedule.*, + (SELECT stand_list.title FROM stand_list WHERE stand_list.id = stand_schedule.stand_id) AS title + FROM + stand_schedule + WHERE + stand_schedule.date >= ? AND stand_schedule.date < ? + ORDER BY + stand_schedule.stand_id + `; + + const schedule = await dbAll(sqlStands, [startTomorrow, startDayAfterTomorrow]); + if (!schedule.length) return console.log('No active schedule'); + + // 2. Угруповання даних (Transform) + const standsData = schedule.reduce((acc, item) => { + if (!acc[item.stand_id]) { + acc[item.stand_id] = { id: item.stand_id, title: item.title || `Стенд ${sId}`, hours: {}, maxSheepIdx: 0, date: item.date }; + } + if (!acc[item.stand_id].hours[item.hour]) { + acc[item.stand_id].hours[item.hour] = {}; + } + acc[item.stand_id].hours[item.hour][item.number_sheep] = item.sheep_id; + acc[item.stand_id].maxSheepIdx = Math.max(acc[item.stand_id].maxSheepIdx, item.number_sheep); + return acc; + }, {}); + + // 3. Генерація + for (const standId in standsData) { + const data = standsData[standId]; + const title = data.title; + const sortedHours = Object.keys(data.hours).map(Number).sort((a, b) => a - b); + + // Визначаємо крок (мінімальна різниця між годинником або 1) + let step = 1; + if (sortedHours.length > 1) { + step = sortedHours[1] - sortedHours[0]; + } + + const sheepIdsMatrix = sortedHours.map(h => { + const row = []; + for (let i = 0; i <= data.maxSheepIdx; i++) { + row.push(data.hours[h][i] || null); + } + return row; + }); + + const formattedDate = new Date(data.date).toLocaleDateString('ru-RU'); + + const result = await generateSchedule({ + title: title, + date: formattedDate, + options: { + start: sortedHours[0], + quantity: data.maxSheepIdx + 1, + step: step + }, + sheep_ids: sheepIdsMatrix, + sheepMap // Передаємо Map для імен + }); + + if (result && result.buffer) { + try { + let isNotFull = result.isNotFull ? '\n\n✏️ Ще є вільні місця, встигніть записатись' : ''; + + // 1. Надсилаємо нове фото + const sentMessage = await bot.sendPhoto(STAND_CHAT_ID, result.buffer, { + caption: `📍 *${title}*\n📅 ${formattedDate}${isNotFull}`, + parse_mode: 'Markdown' + }, { + filename: result.fileName, + contentType: 'image/png' + }); + + // 2. Зберігаємо ID нового повідомлення у базі + await dbRun( + `INSERT INTO sent_messages (last_message_id, created_at) VALUES (?, ?)`, [sentMessage.message_id, Date.now()] + ); + + console.log(`✅ Обновлено: ${title}`); + + } catch (err) { + console.error('Помилка відправки в Telegram:', err.message); + } + } + } + } catch (err) { + console.error('Logic error:', err); + } + } +} + + +// Генерування зображення графіка +async function generateSchedule({ + title, date, options, sheep_ids, sheepMap +}) { + const { start, step, quantity } = options; + const rowsCount = sheep_ids.length; + + const rowHeight = 50; + const timeWidth = 120; + const gap = 10; + const padding = 10; + + const totalWidth = 200 + (200 * quantity); + const headerHeight = 85; + const totalHeight = 136 + (rowHeight * (rowsCount - 1)); + + const mainRectWidth = totalWidth - (padding * 2); + const availableForBlocks = mainRectWidth - timeWidth - 5; + const blockWidth = (availableForBlocks - (gap * (quantity - 1))) / quantity; + + let isNotFull = false; + + const formatTime = (h) => { + const hh = Math.floor(h).toString().padStart(2, '0'); + const mm = (h % 1 * 60).toString().padStart(2, '0'); + return `${hh}:${mm}`; + }; + + let rowsSvg = ''; + sheep_ids.forEach((rowSheepIds, i) => { + const y = headerHeight + (i * rowHeight); + const hStart = formatTime(start + (i * step)); + const hEnd = formatTime(start + ((i + 1) * step)); + const isEven = i % 2 === 0; + + // Фон рядка + rowsSvg += ``; + + // Текст часу + rowsSvg += `${hStart}-${hEnd}`; + + // Блоки з іменами + rowSheepIds.forEach((id, j) => { + const x = padding + timeWidth + (j * (blockWidth + gap)); + const name = sheepMap.get(id) || ''; + if(!sheepMap.get(id)) isNotFull = true; // Якщо є вільне місце + + rowsSvg += ` + + + ${name} + `; + }); + }); + + const finalSvg = ` + + + + ${date} • ${title} + + ${rowsSvg} + `; + + try { + const buffer = await sharp(Buffer.from(finalSvg)) + .png() + .toBuffer(); + + return { buffer, fileName: `${title}_${date}.png`, isNotFull }; + } catch (e) { + console.error('Sharp error:', e); + return null; + } +} + +module.exports = new Stands(); \ No newline at end of file diff --git a/cron/utils/notification.js b/cron/utils/notification.js index 81351ed..8b5cbed 100644 --- a/cron/utils/notification.js +++ b/cron/utils/notification.js @@ -11,6 +11,44 @@ webpush.setVapidDetails( ); class Notification { + async sendAll({ title, body, page }) { + const sql = ` + SELECT * FROM subscription + ORDER BY id + `; + + db.all(sql, async (err, rows) => { + if (err) { + console.error('DB error:', err.message); + return; + } + + if (!rows.length) { + console.log(`🐑 No subscriptions`); + return; + } + + console.log(`📨 Sending notification to ${rows.length} subscriptions...`); + + const payload = JSON.stringify({ + title: title ?? "Тестове повідомлення", + body: body ?? "Ви успішно підписалися на отримання push повідомлень!", + url: `https://${process.env.DOMAIN}${page ?? ""}` + }); + + const results = await Promise.allSettled(rows.map(row => { + const subscription = { + endpoint: row.endpoint, + keys: JSON.parse(row.keys), + }; + return webpush.sendNotification(subscription, payload); + })); + + const failed = results.filter(r => r.status === 'rejected').length; + console.log(`✅ Sent: ${rows.length - failed}, ❌ Failed: ${failed}`); + }); + } + async sendSheep({ sheep_id, title, body, page }) { const sql = ` SELECT * FROM subscription @@ -73,7 +111,7 @@ class Notification { } if (!rows.length) { - console.log(`🐑 No subscriptions found for sheep_id: ${sheep_id}`); + console.log(`🐑 No subscriptions found for group_id: ${group_id}`); return; } @@ -98,7 +136,7 @@ class Notification { }); } - async sendStand({ title, body, page }) { + async sendStandAdd({title, body, page }) { const sql = ` SELECT subscription.* @@ -111,7 +149,7 @@ class Notification { possibilities ON possibilities.sheep_id = sheeps.id WHERE - possibilities.can_view_stand = '1' + possibilities.can_add_stand = '1' ORDER BY subscription.id; `; @@ -147,6 +185,22 @@ class Notification { console.log(`✅ Sent: ${rows.length - failed}, ❌ Failed: ${failed}`); }); } + + async sendEndpoint({ endpoint, keys, title, body, page }) { + const payload = JSON.stringify({ + title: title ?? "Тестове повідомлення", + body: body ?? "Ви успішно підписалися на отримання push повідомлень!", + url: `https://${process.env.DOMAIN}${page ?? ""}` + }); + + const subscription = { + endpoint, + keys: typeof keys === "string" ? JSON.parse(keys) : keys + }; + + await webpush.sendNotification(subscription, payload); + console.log("✅ Push sent"); + } }; module.exports = new Notification(); \ No newline at end of file diff --git a/dash.json b/dash.json deleted file mode 100644 index fb3e92e..0000000 --- a/dash.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "datasource", - "uid": "prometheus" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "target": { - "limit": 100, - "matchAny": false, - "tags": [], - "type": "dashboard" - }, - "type": "dashboard" - } - ] - }, - "editable": true, - "graphTooltip": 1, - "fiscalYearStartMonth": 0, - "id": null, - "links": [], - "panels": [ - - - { - "datasource": { "type": "datasource", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "custom": { - "axisPlacement": "auto", - "drawStyle": "line", - "fillBorderColor": "rgba(255, 255, 255, 1)", - "gradientMode": "none", - "lineStyle": "solid", - "lineWidth": 1, - "scaleDistribution": { "type": "linear" }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { "group": "A", "mode": "none" }, - "thresholdsStyle": { "mode": "off" } - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { "h": 8, "w": 8, "x": 0, "y": 0 }, - "id": 2, - "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "list", "placement": "bottom" }, - "tooltip": { "mode": "single", "sort": "none" } - }, - "targets": [ - { "expr": "frontend_load_time", "legendFormat": "Завантаження: {{instance}}", "refId": "A" } - ], - "title": "Час завантаження Frontend (ms)", - "type": "timeseries" - }, - - { - "datasource": { "type": "datasource", "uid": "prometheus" }, - "fieldConfig": { - "defaults": { - "color": { "mode": "palette-classic" }, - "unit": "short" - } - }, - "gridPos": { "h": 8, "w": 8, "x": 8, "y": 0 }, - "id": 4, - "options": { - "legend": { - "calcs": ["last"], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { "mode": "single", "sort": "none" } - }, - "targets": [ - { "expr": "users_online", "legendFormat": "Онлайн користувачі", "refId": "A" } - ], - "title": "Онлайн користувачі (Gauge)", - "type": "timeseries" - }, - - { - "datasource": { "type": "datasource", "uid": "prometheus" }, - "gridPos": { "h": 8, "w": 8, "x": 16, "y": 0 }, - "id": 10, - "targets": [ - { - "expr": "rest_request_duration_ms", - "legendFormat": "{{path}} ({{status}})", - "refId": "A" - } - ], - "title": "REST Запити (Duration)", - "type": "timeseries" - }, - - { - "datasource": { "type": "datasource", "uid": "prometheus" }, - "gridPos": { "h": 8, "w": 8, "x": 0, "y": 8 }, - "id": 6, - "targets": [ - { - "expr": "rate(ws_in_bytes_total[5m])", - "legendFormat": "Вхідний WS-трафік (байт/сек)", - "refId": "A" - }, - { - "expr": "rate(ws_out_bytes_total[5m])", - "legendFormat": "Вихідний WS-трафік (байт/сек)", - "refId": "B" - } - ], - "type": "timeseries", - "title": "WS трафік (Rate)" - }, - - { - "datasource": { "type": "datasource", "uid": "prometheus" }, - "gridPos": { "h": 8, "w": 8, "x": 8, "y": 8 }, - "id": 16, - "targets": [ - { "expr": "frontend_resource_count", "legendFormat": "Кількість ресурсів", "refId": "A" } - ], - "title": "Кількість завантажених ресурсів", - "type": "timeseries" - }, - - { - "datasource": { "type": "datasource", "uid": "prometheus" }, - "gridPos": { "h": 8, "w": 8, "x": 16, "y": 8 }, - "id": 14, - "targets": [ - { - "expr": "frontend_memory_used", - "legendFormat": "Використано JS Heap (MB)", - "refId": "A" - } - ], - "title": "Використання JS Heap (MB)", - "type": "timeseries" - } - ], - - "refresh": "5s", - "schemaVersion": 38, - "style": "dark", - "tags": ["Sheep Service", "pushgateway"], - - "time": { "from": "now-30m", "to": "now" }, - "timezone": "browser", - "title": "Sheep Service Metrics", - "version": 1 -} \ No newline at end of file diff --git a/data/prometheus.yml b/data/prometheus.yml deleted file mode 100644 index fa41064..0000000 --- a/data/prometheus.yml +++ /dev/null @@ -1,7 +0,0 @@ -global: - scrape_interval: 5s - -scrape_configs: - - job_name: 'pushgateway' - static_configs: - - targets: ['pushgateway:9091'] diff --git a/dock/Інструкції/Інструкція. Записи на стенд.mp4 b/dock/Інструкції/Інструкція. Записи на стенд.mp4 new file mode 100644 index 0000000..43e5093 Binary files /dev/null and b/dock/Інструкції/Інструкція. Записи на стенд.mp4 differ diff --git a/docker-compose.yml b/docker-compose.yml index 1f36d9c..19c9795 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,11 +31,11 @@ services: - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY} - DOMAIN=${DOMAIN} - ADMIN_TOKEN=${ADMIN_TOKEN} + - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} + - STAND_CHAT_ID=${STAND_CHAT_ID} shm_size: '1gb' networks: - network - depends_on: - - metrics ws: image: sheep-service/ws @@ -48,57 +48,12 @@ services: environment: - TZ=${TZ} - DATABASE_PATH=/app/data/ + - VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY} + - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY} + - DOMAIN=${DOMAIN} networks: - network - metrics: - image: sheep-service/metrics - build: ./metrics - ports: - - "4005:4005" # HTTP push - - "4006:4006" # WebSocket - networks: - - network - depends_on: - - pushgateway - - pushgateway: - image: prom/pushgateway - ports: - - "4007:9091" - networks: - - network - - prometheus: - image: prom/prometheus - volumes: - - "${DATA_PATH:-./data}/prometheus.yml:/etc/prometheus/prometheus.yml" - ports: - - "4008:9090" - networks: - - network - depends_on: - - pushgateway - command: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - "--storage.tsdb.retention.time=7d" - - grafana: - image: grafana/grafana - ports: - - "4009:3000" - networks: - - network - depends_on: - - prometheus - environment: - - GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s/grafana/ - - GF_SECURITY_ADMIN_USER=${ADMIN_USER} - - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_TOKEN} - volumes: - - ${DATA_PATH:-./data}/grafana:/var/lib/grafana - nginx: image: nginx:latest restart: always @@ -116,8 +71,6 @@ services: command: /bin/sh -c "envsubst '\$DOMAIN' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" networks: - network - depends_on: - - grafana cron: image: sheep-service/cron @@ -129,7 +82,11 @@ services: - TZ=${TZ} - DATABASE_PATH=/app/data/ - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} + - STAND_CHAT_ID=${STAND_CHAT_ID} - CHAT_ID=${CHAT_ID} + - VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY} + - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY} + - DOMAIN=${DOMAIN} networks: - network diff --git a/metrics/Dockerfile b/metrics/Dockerfile deleted file mode 100644 index fc9b222..0000000 --- a/metrics/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM node:20.18 - -WORKDIR /app - -COPY package.json package-lock.json ./ - -RUN npm install - -COPY . . - -EXPOSE 4005 -EXPOSE 4006 - -CMD npm start diff --git a/metrics/metrics.js b/metrics/metrics.js deleted file mode 100644 index ca9d111..0000000 --- a/metrics/metrics.js +++ /dev/null @@ -1,133 +0,0 @@ -const express = require("express"); -const cors = require("cors"); -const WebSocket = require("ws"); -const app = express(); - -const PORT_HTTP = 4005; -const PORT_WS = 4006; -const PUSHGATEWAY = "http://pushgateway:9091"; - -// --- HTTP server --- -app.use(cors({ origin: "*", methods: ["GET", "POST"], allowedHeaders: ["Content-Type"], credentials: true })); -app.use(express.json({ limit: "50mb" })); - -app.post("/push", (req, res) => { - const metric = req.body; - if (metric) pushToProm(metric); - res.send("ok"); -}); - -// --- WS server --- -const wss = new WebSocket.Server({ port: PORT_WS }); -wss.on("connection", ws => { - console.log("New WS connection. Online users:", wss.clients.size); - pushOnlineCount(); - - ws.on("message", msg => { - try { - const m = JSON.parse(msg); - pushToProm(m); - } catch (e) { - console.error("Invalid WS message:", e); - } - }); - - ws.on("close", () => pushOnlineCount()); -}); - -// --- Push metrics to Pushgateway --- -async function pushToProm(metric) { - if (!metric || !metric.type) return; - - const lines = []; - let job = "webapp"; // default job - let groupingKey = {}; - - switch (metric.type) { - case "frontend_metrics": - job = "webapp_frontend"; - - lines.push(`# HELP frontend_load_time Frontend page load time in ms`); - lines.push(`# TYPE frontend_load_time gauge`); - lines.push(`frontend_load_time{instance="${metric.id || 'default'}"} ${metric.navigation?.loadEventEnd || 0}`); - - if (metric.memory?.usedJSHeapSize) { - lines.push(`# HELP frontend_memory_used JS heap used in MB`); - lines.push(`# TYPE frontend_memory_used gauge`); - lines.push(`frontend_memory_used{instance="${metric.id || 'default'}"} ${metric.memory.usedJSHeapSize / 1024 / 1024}`); - } - - if (metric.resources?.length) { - lines.push(`# HELP frontend_resource_count Number of resources loaded`); - lines.push(`# TYPE frontend_resource_count gauge`); - lines.push(`frontend_resource_count{instance="${metric.id || 'default'}"} ${metric.resources.length}`); - } - - break; - - case "rest": - job = "webapp_api"; - lines.push(`# HELP rest_request_duration_ms REST request duration in ms`); - lines.push(`# TYPE rest_request_duration_ms gauge`); - lines.push(`rest_request_duration_ms{path="${metric.path}",status="${metric.status}"} ${metric.time}`); - break; - - case "ws_in": - job = "webapp_ws"; - lines.push(`# HELP ws_in_bytes_total WS bytes in`); - lines.push(`# TYPE ws_in_bytes_total counter`); - lines.push(`ws_in_bytes_total ${metric.length}`); - break; - - case "ws_out": - job = "webapp_ws"; - lines.push(`# HELP ws_out_bytes_total WS bytes out`); - lines.push(`# TYPE ws_out_bytes_total counter`); - lines.push(`ws_out_bytes_total ${metric.length}`); - break; - - case "connection_status": - job = "webapp_connection"; - lines.push(`# HELP users_online Users online`); - lines.push(`# TYPE users_online gauge`); - lines.push(`users_online ${metric.count || wss.clients.size}`); - - const putBody = lines.join("\n") + "\n"; - - // Отправляем как PUT для Gauge, чтобы сбросить старые значения - return await pushToPrometheus(`${PUSHGATEWAY}/metrics/job/${job}`, putBody, "PUT"); - } - - const body = lines.join("\n") + "\n"; - console.log(`Pushing metric for job=${job}:\n${body}\n`); - - // Используем POST по умолчанию для инкремента/добавления - await pushToPrometheus(`${PUSHGATEWAY}/metrics/job/${job}`, body, "POST"); -} - -async function pushToPrometheus(url, body, method) { - try { - const response = await fetch(url, { - method: method, - headers: { "Content-Type": "text/plain" }, - body - }); - if (response.status !== 200) { - console.error(`PushGateway error (${method} ${url}):`, response.status, await response.text()); - } - } catch (err) { - console.error("PushGateway network error:", err.message); - } -} - - -function pushOnlineCount() { - pushToProm({ type: "connection_status", count: wss.clients.size }); -} - -// --- Start HTTP server --- -app.listen(PORT_HTTP, () => console.log("Metrics HTTP listening on", PORT_HTTP)); -console.log("Metrics WS listening on", PORT_WS); - -// Запуск интервала после инициализации wss -setInterval(pushOnlineCount, 5000); \ No newline at end of file diff --git a/metrics/package-lock.json b/metrics/package-lock.json deleted file mode 100644 index 9a8b170..0000000 --- a/metrics/package-lock.json +++ /dev/null @@ -1,796 +0,0 @@ -{ - "name": "metrics", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "metrics", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "cors": "^2.8.5", - "express": "^5.2.1", - "ws": "^8.18.3" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "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==" - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "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 - } - } - } - } -} diff --git a/metrics/package.json b/metrics/package.json deleted file mode 100644 index cbd543c..0000000 --- a/metrics/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "metrics", - "version": "1.0.0", - "main": "metrics.js", - "scripts": { - "start": "node metrics.js" - }, - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "cors": "^2.8.5", - "express": "^5.2.1", - "ws": "^8.18.3" - } -} diff --git a/nginx/default.conf.template b/nginx/default.conf.template index 65041b4..2c0b94a 100644 --- a/nginx/default.conf.template +++ b/nginx/default.conf.template @@ -10,10 +10,6 @@ upstream ws_backend { server ws:4004; } -upstream metrics_backend { - server metrics:4006; -} - # Загальні CORS-заголовки map $request_method $cors_preflight { OPTIONS 1; @@ -58,25 +54,6 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - # Metrics - location /metrics { - proxy_pass http://metrics_backend$request_uri; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - # Grafana - location /grafana/ { - proxy_pass http://grafana:3000/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - # Frontend location / { proxy_pass http://frontend$request_uri; diff --git a/web/config.js b/web/config.js index 79e425e..3286ac1 100644 --- a/web/config.js +++ b/web/config.js @@ -1,6 +1,5 @@ const CONFIG = { "web": "https://test.sheep-service.com/", "api": "https://test.sheep-service.com/api/", - "wss": "wss://test.sheep-service.com/ws", - "metrics": "wss://test.sheep-service.com/metrics" + "wss": "wss://test.sheep-service.com/ws" } \ No newline at end of file diff --git a/web/css/main.css b/web/css/main.css index f038f02..c8287fd 100644 --- a/web/css/main.css +++ b/web/css/main.css @@ -1,4 +1,42 @@ -@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap'); +@font-face { + font-family: 'Roboto'; + src: url('../fonts/Roboto/Roboto-Light.ttf') format('truetype'); + font-weight: 300; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Roboto'; + src: url('../fonts/Roboto/Roboto-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Roboto'; + src: url('../fonts/Roboto/Roboto-Medium.ttf') format('truetype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Roboto'; + src: url('../fonts/Roboto/Roboto-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Roboto'; + src: url('../fonts/Roboto/Roboto-Black.ttf') format('truetype'); + font-weight: 900; + font-style: normal; + font-display: swap; +} :root { --FontSize1: 12px; @@ -28,13 +66,14 @@ /* BGColor */ --ColorThemes0: #fbfbfb; --ColorThemes1: #f3f3f3; - --ColorThemes2: #dbdbd1; + --ColorThemes2: #e7e7e1; /* TextColor */ --ColorThemes3: #313131; --ColorAnimation: linear-gradient(90deg, #f3f3f3, #efefef, #f3f3f3); - --shadow-l1: 0px 2px 4px rgba(0, 0, 0, 0.02), 0px 0px 2px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04); + /* --shadow-l1: 0px 2px 4px rgba(0, 0, 0, 0.02), 0px 0px 2px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04); */ + --shadow-l1: 0; --border-radius: 15px; @@ -51,13 +90,14 @@ /* BGColor */ --ColorThemes0: #1c1c19; --ColorThemes1: #21221d; - --ColorThemes2: #525151; + --ColorThemes2: #3d3c3c; /* TextColor */ --ColorThemes3: #f3f3f3; --ColorAnimation: linear-gradient(90deg, #21221d, #242520, #21221d); - --shadow-l1: 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04); + /* --shadow-l1: 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04); */ + --shadow-l1: 0; --border-radius: 15px; @@ -81,6 +121,16 @@ cursor: no-drop !important; } +@media(hover: hover) { + a:hover, + button:hover, + select:hover, + input:hover, + textarea:hover { + opacity: 0.8; + } +} + @media (min-width: 800px) { * { scroll-snap-type: none !important; @@ -238,314 +288,6 @@ body.modal-open { overflow: hidden; } -/* Банер з прохання встановлення PWA */ -#blur-backdrop { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.3); - backdrop-filter: blur(8px); - z-index: 9998; -} - -.pwa-overlay { - position: fixed; - inset: 0; - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; -} - -.pwa-overlay>.popup { - background: var(--ColorThemes0); - padding: 24px 32px; - border-radius: var(--border-radius); - max-width: 90%; - width: 320px; - text-align: center; - font-family: sans-serif; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); - animation: fadeIn 0.3s ease-out; - display: flex; - flex-direction: column; - align-items: center; -} - -.pwa-overlay>.popup h2 { - margin-bottom: 12px; - color: var(--ColorThemes3); - opacity: 0.8; -} - -.pwa-overlay>.popup p { - margin-bottom: 10px; - color: var(--ColorThemes3); - opacity: 0.6; -} - -.pwa-overlay>.popup ol { - text-align: justify; - font-size: var(--FontSize4); - margin-bottom: 10px; - max-width: 290px; -} - -.pwa-overlay>.popup li { - list-style-type: none; - font-size: var(--FontSize3); -} - -.pwa-overlay>.popup li span { - vertical-align: middle; - display: inline-block; - width: 22px; - height: 22px; -} - -.pwa-overlay>.popup li span svg { - fill: var(--PrimaryColor); -} - -.pwa-overlay>.popup>div { - margin-top: 10px; - display: flex; - justify-content: center; - gap: 10px; -} - -.pwa-overlay>.popup>div>button { - padding: 8px 16px; - border: none; - border-radius: calc(var(--border-radius) - 8px); - cursor: pointer; - font-size: var(--FontSize3); -} - -#pwa-install-button { - background-color: var(--PrimaryColor); - color: var(--PrimaryColorText); -} - -#pwa-close-button, -#pwa-ios-close-button { - background-color: #ccc; - color: #333; -} - -.pwa-hidden { - display: none; -} - -@media (max-width: 450px) { - .pwa-overlay>.popup { - padding: 17px 10px; - } - - .pwa-overlay>.popup h2 { - font-size: 22px; - } - - .pwa-overlay>.popup p { - font-size: var(--FontSize4); - } -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: scale(0.95); - } - - to { - opacity: 1; - transform: scale(1); - } -} - - - - -#swipe_updater { - position: absolute; - top: 0px; - width: 100%; -} - -#swipe_block { - width: calc(100% - 252px); - margin-left: 252px; - height: 50px; - display: flex; - justify-content: center; - align-items: flex-end; - position: relative; -} - -#swipe_icon { - width: 20px; - fill: var(--ColorThemes3); - transform: rotate(0deg); - position: absolute; - margin-top: -45px; - top: -45px; - background: var(--ColorThemes2); - border: 2px solid var(--ColorThemes3); - border-radius: 50%; - padding: 10px; - display: flex; - overflow: hidden; - height: 0; - opacity: 0; - transition: height 0ms 400ms, opacity 400ms 0ms; -} - -#swipe_icon[data-state="active"] { - height: 20px; - opacity: 1; - transition: height 0ms 0ms, opacity 400ms 0ms; -} - -@media (max-width: 1100px) { - #swipe_block { - width: calc(100% - 122px); - margin-left: 122px; - } -} - -@media (max-width: 700px) { - #swipe_block { - width: 100%; - margin-left: 0; - } -} - - -/* Стили для меню */ -#navigation { - position: fixed; - width: 230px; - height: calc(100vh - 60px); - min-height: 510px; - background: var(--ColorThemes2); - padding: 36px 10px; - -webkit-transition: width .2s ease 0s; - -o-transition: width .2s ease 0s; - transition: width .2s ease 0s; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -#navigation>nav { - display: flex; - flex-direction: column; - height: 290px; - align-items: center; - justify-content: flex-start; -} - -#navigation>nav>li { - width: 180px; - height: 50px; - list-style-type: none; - position: relative; - -webkit-transition: all .2s ease 0s; - -o-transition: all .2s ease 0s; - transition: all .2s ease 0s; - z-index: 1; - margin: 5px; -} - -#navigation>nav>li>div { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-items: center; - justify-content: flex-start; - height: 50px; - padding: 0 10px; - border-radius: var(--border-radius); - -webkit-transition: all .2s ease 0s; - -o-transition: all .2s ease 0s; - transition: all .2s ease 0s; - opacity: 0.8; - cursor: pointer; - border: 2px; - border: 2px solid var(--ColorThemes2); - overflow: hidden; -} - -#navigation>nav>li>div[data-state="active"] { - background: var(--ColorThemes3); - border: 2px solid var(--ColorThemes3); - box-shadow: var(--shadow-l1); -} - -#navigation>nav>li:has(div[data-state="active"]) { - z-index: 10; -} - -#navigation>nav>li>div>svg { - width: 25px; - height: 25px; - min-width: 25px; - min-height: 25px; - fill: var(--ColorThemes3); -} - -#navigation>nav>li>div[data-state="active"] svg { - fill: var(--ColorThemes2); -} - -#navigation>nav>li>div>b { - margin-left: 15px; - font-size: var(--FontSize3); - font-weight: 300; - color: var(--ColorThemes3); - white-space: nowrap; -} - -#navigation>nav>li>div[data-state="active"] b { - color: var(--ColorThemes2); -} - -#navigation>nav>li>a { - position: absolute; - width: 100%; - height: 100%; - top: 2px; -} - - -@media (hover: hover) { - #navigation>nav>li:hover { - transform: scale(1.01); - } - - #navigation>nav>li:hover>div { - border: 2px solid var(--ColorThemes3); - } -} - -@media (max-width: 1100px) { - #navigation { - width: 100px; - } - - #navigation>nav>li { - width: 50px; - } - - #navigation>nav>li>div { - width: 30px; - justify-content: center; - } - - #navigation>nav>li>div>b { - display: none; - } -} - #app { background: var(--ColorThemes0); position: absolute; @@ -659,7 +401,7 @@ body.modal-open { } .leaflet-popup-content { - margin: 8px 10px !important; + margin: 10px 10px !important; padding: 0 !important; } diff --git a/web/fonts/PT_Mono/OFL.txt b/web/fonts/PT_Mono/OFL.txt deleted file mode 100644 index fcbaa96..0000000 --- a/web/fonts/PT_Mono/OFL.txt +++ /dev/null @@ -1,94 +0,0 @@ -Copyright (c) 2011, ParaType Ltd. (http://www.paratype.com/public), -with Reserved Font Names "PT Sans", "PT Serif", "PT Mono" and "ParaType". - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/web/fonts/PT_Mono/PTMono-Regular.ttf b/web/fonts/PT_Mono/PTMono-Regular.ttf deleted file mode 100644 index b198383..0000000 Binary files a/web/fonts/PT_Mono/PTMono-Regular.ttf and /dev/null differ diff --git a/web/fonts/Roboto/Roboto-Black.ttf b/web/fonts/Roboto/Roboto-Black.ttf new file mode 100644 index 0000000..d51221a Binary files /dev/null and b/web/fonts/Roboto/Roboto-Black.ttf differ diff --git a/web/fonts/Roboto/Roboto-Bold.ttf b/web/fonts/Roboto/Roboto-Bold.ttf new file mode 100644 index 0000000..4658f9a Binary files /dev/null and b/web/fonts/Roboto/Roboto-Bold.ttf differ diff --git a/web/fonts/Roboto/Roboto-Light.ttf b/web/fonts/Roboto/Roboto-Light.ttf new file mode 100644 index 0000000..6fcd5f9 Binary files /dev/null and b/web/fonts/Roboto/Roboto-Light.ttf differ diff --git a/web/fonts/Roboto/Roboto-Medium.ttf b/web/fonts/Roboto/Roboto-Medium.ttf new file mode 100644 index 0000000..d629e98 Binary files /dev/null and b/web/fonts/Roboto/Roboto-Medium.ttf differ diff --git a/web/fonts/Roboto/Roboto-Regular.ttf b/web/fonts/Roboto/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/web/fonts/Roboto/Roboto-Regular.ttf differ diff --git a/web/fonts/Roboto_Mono/RobotoMono-Bold.ttf b/web/fonts/Roboto_Mono/RobotoMono-Bold.ttf new file mode 100644 index 0000000..bef439f Binary files /dev/null and b/web/fonts/Roboto_Mono/RobotoMono-Bold.ttf differ diff --git a/web/fonts/Roboto_Mono/RobotoMono-Light.ttf b/web/fonts/Roboto_Mono/RobotoMono-Light.ttf new file mode 100644 index 0000000..b6fb475 Binary files /dev/null and b/web/fonts/Roboto_Mono/RobotoMono-Light.ttf differ diff --git a/web/fonts/Roboto_Mono/RobotoMono-Medium.ttf b/web/fonts/Roboto_Mono/RobotoMono-Medium.ttf new file mode 100644 index 0000000..53fdd40 Binary files /dev/null and b/web/fonts/Roboto_Mono/RobotoMono-Medium.ttf differ diff --git a/web/fonts/Roboto_Mono/RobotoMono-Regular.ttf b/web/fonts/Roboto_Mono/RobotoMono-Regular.ttf new file mode 100644 index 0000000..3806bfb Binary files /dev/null and b/web/fonts/Roboto_Mono/RobotoMono-Regular.ttf differ diff --git a/web/fonts/UbuntuMono/UFL.txt b/web/fonts/UbuntuMono/UFL.txt deleted file mode 100644 index ae78a8f..0000000 --- a/web/fonts/UbuntuMono/UFL.txt +++ /dev/null @@ -1,96 +0,0 @@ -------------------------------- -UBUNTU FONT LICENCE Version 1.0 -------------------------------- - -PREAMBLE -This licence allows the licensed fonts to be used, studied, modified and -redistributed freely. The fonts, including any derivative works, can be -bundled, embedded, and redistributed provided the terms of this licence -are met. The fonts and derivatives, however, cannot be released under -any other licence. The requirement for fonts to remain under this -licence does not require any document created using the fonts or their -derivatives to be published under this licence, as long as the primary -purpose of the document is not to be a vehicle for the distribution of -the fonts. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this licence and clearly marked as such. This may -include source files, build scripts and documentation. - -"Original Version" refers to the collection of Font Software components -as received under this licence. - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to -a new environment. - -"Copyright Holder(s)" refers to all individuals and companies who have a -copyright ownership of the Font Software. - -"Substantially Changed" refers to Modified Versions which can be easily -identified as dissimilar to the Font Software by users of the Font -Software comparing the Original Version with the Modified Version. - -To "Propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification and with or without charging -a redistribution fee), making available to the public, and in some -countries other activities as well. - -PERMISSION & CONDITIONS -This licence does not grant any rights under trademark law and all such -rights are reserved. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of the Font Software, to propagate the Font Software, subject to -the below conditions: - -1) Each copy of the Font Software must contain the above copyright -notice and this licence. These can be included either as stand-alone -text files, human-readable headers or in the appropriate machine- -readable metadata fields within text or binary files as long as those -fields can be easily viewed by the user. - -2) The font name complies with the following: -(a) The Original Version must retain its name, unmodified. -(b) Modified Versions which are Substantially Changed must be renamed to -avoid use of the name of the Original Version or similar names entirely. -(c) Modified Versions which are not Substantially Changed must be -renamed to both (i) retain the name of the Original Version and (ii) add -additional naming elements to distinguish the Modified Version from the -Original Version. The name of such Modified Versions must be the name of -the Original Version, with "derivative X" where X represents the name of -the new work, appended to that name. - -3) The name(s) of the Copyright Holder(s) and any contributor to the -Font Software shall not be used to promote, endorse or advertise any -Modified Version, except (i) as required by this licence, (ii) to -acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with -their explicit written permission. - -4) The Font Software, modified or unmodified, in part or in whole, must -be distributed entirely under this licence, and must not be distributed -under any other licence. The requirement for fonts to remain under this -licence does not affect any document created using the Font Software, -except any version of the Font Software extracted from a document -created using the Font Software may only be distributed under this -licence. - -TERMINATION -This licence becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER -DEALINGS IN THE FONT SOFTWARE. diff --git a/web/fonts/UbuntuMono/UbuntuMono-Bold.ttf b/web/fonts/UbuntuMono/UbuntuMono-Bold.ttf deleted file mode 100644 index 01ad81b..0000000 Binary files a/web/fonts/UbuntuMono/UbuntuMono-Bold.ttf and /dev/null differ diff --git a/web/fonts/UbuntuMono/UbuntuMono-BoldItalic.ttf b/web/fonts/UbuntuMono/UbuntuMono-BoldItalic.ttf deleted file mode 100644 index 731884e..0000000 Binary files a/web/fonts/UbuntuMono/UbuntuMono-BoldItalic.ttf and /dev/null differ diff --git a/web/fonts/UbuntuMono/UbuntuMono-Italic.ttf b/web/fonts/UbuntuMono/UbuntuMono-Italic.ttf deleted file mode 100644 index b89338d..0000000 Binary files a/web/fonts/UbuntuMono/UbuntuMono-Italic.ttf and /dev/null differ diff --git a/web/fonts/UbuntuMono/UbuntuMono-Regular.ttf b/web/fonts/UbuntuMono/UbuntuMono-Regular.ttf deleted file mode 100644 index 4977028..0000000 Binary files a/web/fonts/UbuntuMono/UbuntuMono-Regular.ttf and /dev/null differ diff --git a/web/index.html b/web/index.html index 719a6bd..0fee8d9 100644 --- a/web/index.html +++ b/web/index.html @@ -41,12 +41,16 @@ - - - - + + + + + + + + @@ -63,12 +67,11 @@ - + - @@ -130,148 +133,28 @@ -
-
- -
-
- -
- - - + + -
-
- - - -
-
+ - + + +
diff --git a/web/lib/app.js b/web/lib/app.js index cb7e3eb..e6d95e9 100644 --- a/web/lib/app.js +++ b/web/lib/app.js @@ -4,6 +4,7 @@ let swRegistration = null; // Реєструємо CustomElements const Notifier = document.getElementById('notif-manager'); +const Updater = document.querySelector('swipe-updater'); // Определение ID главного блока let app = document.getElementById('app'); @@ -13,32 +14,23 @@ Router.config({ mode: 'history' }); async function appReload() { - location.reload(); + // location.reload(); - // Router.navigate(window.location.pathname, false).check(); + // Router.check().listen().delegateLinks(); // // Закрытие старого соединения WebSocket - // if (socket) socket.close(1000, "Перезапуск соединения"); + // if (Cloud.socket) Cloud.socket.close(1000, "Перезапуск з'єднання"); + // Cloud.start(); // listEntrances = [] // listApartment = [] } +// Перевизначення функції, яка викликається при "свайпі" +// Updater.setReloadFunction(() => appReload()); + // Функция загрузки приложения window.addEventListener('load', async function () { console.log('[OS] ', detectOS()); - if (window.matchMedia('(display-mode: standalone)').matches) { - if (detectOS() == 'Android') { - document.getElementById('navigation').dataset.state = ''; - } else if (detectOS() == 'iOS') { - document.getElementById('navigation').dataset.state = 'ios'; - localStorage.setItem('backToTop', 'false'); - } else if (detectOS() == 'MacOS') { - document.getElementById('navigation').dataset.state = 'ios'; - localStorage.setItem('backToTop', 'false'); - } else { - document.getElementById('navigation').dataset.state = ''; - } - } if (Router.getParams().uuid) { localStorage.setItem("uuid", Router.getParams().uuid) @@ -65,14 +57,47 @@ window.addEventListener('load', async function () { console.log("[APP] USER Info: ", USER); - - if (USER.possibilities.can_view_sheeps) document.getElementById("li-sheeps").style.display = ""; - if (USER.possibilities.can_view_schedule) document.getElementById("li-schedule").style.display = ""; - if (USER.possibilities.can_manager_territory) document.getElementById("li-territory").style.display = ""; - if (USER.possibilities.can_view_stand) document.getElementById("li-stand").style.display = ""; - document.getElementById("li-options").style.display = ""; - - if (USER.possibilities.can_view_sheeps) await Sheeps.sheeps_list.loadAPI(); + if (USER.possibilities.can_view_stand) { + newMenuItems({ + id: 'menu-stand', + title: 'Графік стенду', + icon: ``, + href: '/stand' + }); + } + if (USER.possibilities.can_view_schedule) { + newMenuItems({ + id: 'menu-schedule', + title: 'Графіки зібрань', + icon: ``, + href: '/schedule' + }); + } + if (USER.possibilities.can_view_sheeps) { + newMenuItems({ + id: 'menu-sheeps', + title: 'Вісники', + icon: ``, + href: '/sheeps', + hidden: true + }); + await Sheeps.sheeps_list.loadAPI(); + } + if (USER.possibilities.can_manager_territory) { + newMenuItems({ + id: 'menu-territory', + title: 'Території', + icon: ``, + href: '/territory', + hidden: true + }); + } + newMenuItems({ + id: 'menu-options', + title: 'Опції', + icon: ``, + href: '/options' + }); if (Cloud.socket) Cloud.socket.close(1000, "Перезапуск з'єднання"); Cloud.start(); @@ -80,11 +105,19 @@ window.addEventListener('load', async function () { editFontStyle(); Router.check().listen().delegateLinks(); - - setupFrontendMetrics(); } }); +function newMenuItems({ id, title, icon, href, hidden=false }) { + const newItem = document.createElement('nav-item'); + newItem.setAttribute('id', id); + newItem.setAttribute('title', title); + newItem.setAttribute('icon', icon); + newItem.setAttribute('href', href); + newItem.setAttribute('data-hidden', hidden); + document.querySelector('navigation-container').appendChild(newItem); +} + let offlineNode = null; window.addEventListener("offline", () => { console.log("[APP] Інтернет зник"); @@ -104,9 +137,6 @@ window.addEventListener("online", () => { title: 'Онлайн', text: 'Інтернет знову працює' }, { timeout: 3000 }); - - if (Cloud.socket) Cloud.socket.close(1000, "Перезапуск з'єднання"); - Cloud.start(); }); function editFontStyle() { @@ -154,52 +184,6 @@ function applyFontMode(mode) { }); } - -// Банер з прохання встановлення PWA -let deferredPrompt; -const isInStandaloneMode = () => - ('standalone' in window.navigator && window.navigator.standalone === true); - -if (detectOS() == 'iOS' && !isInStandaloneMode()) { - setTimeout(() => { - document.getElementById('blur-backdrop').classList.remove('pwa-hidden'); - document.getElementById('pwa-ios-overlay').classList.remove('pwa-hidden'); - document.body.classList.add('modal-open'); - }, 1000); -} - -window.addEventListener("beforeinstallprompt", (e) => { - if (localStorage.getItem('modal') != "false") { - e.preventDefault(); - deferredPrompt = e; - - document.getElementById("blur-backdrop").classList.remove("pwa-hidden"); - document.getElementById("pwa-install-overlay").classList.remove("pwa-hidden"); - document.body.classList.add("modal-open"); - } -}); -document.getElementById("pwa-install-button").addEventListener("click", async () => { - if (!deferredPrompt) return; - - deferredPrompt.prompt(); - const { outcome } = await deferredPrompt.userChoice; - console.log(`[APP] Результат встановлення PWA: ${outcome}`); - - closePopup(); -}); -document.getElementById("pwa-close-button").addEventListener("click", closePopup); -document.getElementById('pwa-ios-close-button').addEventListener('click', closePopup); - -function closePopup() { - document.getElementById("pwa-install-overlay").classList.add("pwa-hidden"); - document.getElementById("blur-backdrop").classList.add("pwa-hidden"); - document.getElementById('pwa-ios-overlay').classList.add('pwa-hidden'); - document.body.classList.remove("modal-open"); - deferredPrompt = null; -} - - - if ('serviceWorker' in navigator) { let refreshing = false; let updateNode = null; diff --git a/web/lib/components/clipboard.js b/web/lib/components/clipboard.js index 63054f8..9dffc0e 100644 --- a/web/lib/components/clipboard.js +++ b/web/lib/components/clipboard.js @@ -1,5 +1,5 @@ clipboard = (text) => { navigator.clipboard.writeText(text) - .then(() => alert("Посилання скопійовано!")) + .then(() => Notifier.success("Посилання скопійовано!", {timeout: 2000})) .catch(err => console.error(err)) } \ No newline at end of file diff --git a/web/lib/components/cloud.js b/web/lib/components/cloud.js index 8164139..dedf355 100644 --- a/web/lib/components/cloud.js +++ b/web/lib/components/cloud.js @@ -9,8 +9,6 @@ const Cloud = { Cloud.status = 'sync'; const uuid = localStorage.getItem("uuid"); - if(!navigator.onLine) alert("[APP] Інтернет з'єднання відсутнє!") - if (Cloud.socket && Cloud.socket.readyState <= 1) return; const ws = new WebSocket(CONFIG.wss, uuid); @@ -79,13 +77,18 @@ const Cloud = { } else { Cloud.reconnecting = false; - if (confirm("З'єднання розірвано! Перепідключитись?")) { - Cloud.reconnecting = true; - Cloud.reconnectAttempts = 0; - Cloud.start(); - } else { - console.warn("[WebSocket] Перепідключення відмінено користувачем"); - } + Notifier.click({ + title: `З'єднання розірвано!`, + text: `Натисніть, щоб перепідключитись!` + }, { + type: 'warn', + f: () => { + Cloud.reconnecting = true; + Cloud.reconnectAttempts = 0; + Cloud.start(); + }, + timeout: 0 + }); } }; diff --git a/web/lib/components/metrics.js b/web/lib/components/metrics.js index 0a9d93e..425ae4a 100644 --- a/web/lib/components/metrics.js +++ b/web/lib/components/metrics.js @@ -3,7 +3,6 @@ const RECONNECT_INTERVAL = 3000; let isConnectedMetrics = false; function setupFrontendMetrics() { - console.log("[Metrics] Спроба підключення до метрик..."); mws = new WebSocket(CONFIG.metrics); mws.onopen = () => { diff --git a/web/lib/components/swipeUpdater.js b/web/lib/components/swipeUpdater.js deleted file mode 100644 index e6b94f4..0000000 --- a/web/lib/components/swipeUpdater.js +++ /dev/null @@ -1,40 +0,0 @@ -// Скрипт перезагрузки страници свайпом. -let app_scroll = false; -let animID = document.getElementById('swipe_updater'); -let animIconID = document.getElementById('swipe_icon'); - - -window.addEventListener('scroll', function(e) { - if (window.matchMedia('(display-mode: standalone)').matches) { - let a = window.scrollY; - let b = 50; - let c = 125; - a = -a; - a = a; - - animIconID.style.top = a/1.5; - - console.log(window.scrollY); - - if(window.scrollY <= -10){ - animID.style.zIndex = 115; - } else { - animID.style.zIndex = 0; - } - - if(window.scrollY <= -120){ - if(app_scroll == false){ - app_scroll = true; - animIconID.style.transform = 'rotate(180deg)'; - animIconID.setAttribute('data-state', '') - } - } else if(window.scrollY >= 0){ - if(app_scroll == true){ - appReload(); - app_scroll = false; - animIconID.style.transform = 'rotate(0deg)'; - animIconID.setAttribute('data-state', 'active') - } - } - } -}); \ No newline at end of file diff --git a/web/lib/customElements/menuContainer.js b/web/lib/customElements/menuContainer.js new file mode 100644 index 0000000..90ddeb5 --- /dev/null +++ b/web/lib/customElements/menuContainer.js @@ -0,0 +1,511 @@ +// ========================================================= +// Клас 1: Пункт Меню (nav-item) +// ========================================================= + +class NavigationItem extends HTMLElement { + + // 1. Відстежувані атрибути + static get observedAttributes() { + return ['title', 'icon', 'href', 'click']; + } + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + // Створюємо базову структуру елементів + this.renderBaseStructure(); + } + + /** + * Створює базовий DOM та стилі. + */ + renderBaseStructure() { + const shadow = this.shadowRoot; + shadow.innerHTML = ''; + + const style = document.createElement('style'); + style.textContent = ` + .item-wrapper{ + width: 184px; + height: 54px; + list-style-type: none; + position: relative; + -webkit-transition: width .2s ease 0s; + -o-transition: width .2s ease 0s; + transition: width .2s ease 0s; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + } + .nav-item { + width: 100%; + height: 50px; + padding: 0 12px; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + border-radius: var(--border-radius, 15px); + -webkit-transition: width .2s ease 0s; + -o-transition: width .2s ease 0s; + transition: width .2s ease 0s; + opacity: 0.8; + cursor: pointer; + border: 2px; + border: 2px solid var(--ColorThemes2, #525151); + color: var(--ColorThemes3, #f3f3f3); + overflow: hidden; + text-decoration: none; + gap: 15px; + } + + .nav-icon-img, .nav-icon-wrapper { + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + } + .nav-icon-wrapper svg { + width: 25px; + height: 25px; + min-width: 25px; + min-height: 25px; + fill: currentColor; + } + .nav-title { + font-size: var(--FontSize3, 14px); + font-weight: 300; + white-space: nowrap; + } + + + :host([data-state="active"]) .nav-item { + color: var(--ColorThemes2, #525151); + background: var(--ColorThemes3, #f3f3f3); + border: 2px solid var(--ColorThemes3, #f3f3f3); + box-shadow: var(--shadow-l1, 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04)); + } + @media (hover: hover) { + .nav-item:hover { + border: 2px solid var(--ColorThemes3); + } + } + @media (max-width: 1100px) { + .item-wrapper{ + width: 54px; + } + .nav-item{ + width: 50px; + } + .nav-title { + display: none; + } + } + @media (max-width: 700px), (max-height: 540px) { + .item-wrapper{ + width: 40px; + height: 40px; + } + .nav-item { + width: 40px; + height: 40px; + padding: 0; + border: 0; + justify-content: center; + background: transparent; + color: var(--ColorThemes0, #1c1c19); + border-radius: 50%; + } + :host([data-state="active"]) .nav-item { + color: var(--PrimaryColor, #cb9e44); + background: transparent; + border: 0; + box-shadow: none; + } + .nav-title { + display: none; + } + @media (hover: hover) { + .nav-item:hover { + border: 0; + } + } + } + `; + + shadow.appendChild(style); + + // Створюємо порожній контейнер, який буде замінено на
або + this.containerWrapper = document.createElement('div'); + this.containerWrapper.setAttribute('class', 'item-wrapper'); + shadow.appendChild(this.containerWrapper); + + // Первинне заповнення контентом + this.updateContent(); + } + + connectedCallback() { + // Обробник кліку додається після того, як this.itemElement створено в updateContent + this.containerWrapper.addEventListener('click', this.handleItemClick.bind(this)); + } + + disconnectedCallback() { + this.containerWrapper.removeEventListener('click', this.handleItemClick.bind(this)); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue !== newValue) { + this.updateContent(); + } + } + + /** + * Зчитує атрибути та оновлює внутрішній HTML і тег-контейнер. + */ + updateContent() { + const title = this.getAttribute('title') || 'Без назви'; + const iconContent = this.getAttribute('icon'); + const href = this.getAttribute('href'); + + // 1. Формуємо HTML для іконки та заголовка + let iconHTML = ''; + if (iconContent) { + const trimmedIcon = iconContent.trim(); + const isSVG = trimmedIcon.startsWith(''); + + if (isSVG) { + iconHTML = `${trimmedIcon}`; + } else { + iconHTML = `${title} icon`; + } + } + const innerContent = `${iconHTML}${title}`; + + // 2. Визначаємо, який тег використовувати ( чи
) + const currentTag = this.containerWrapper.firstChild ? this.containerWrapper.firstChild.tagName.toLowerCase() : null; + const requiredTag = href ? 'a' : 'div'; + + // Якщо тип тега потрібно змінити, створюємо новий елемент + if (currentTag !== requiredTag) { + // Створюємо новий елемент або
+ this.itemElement = document.createElement(requiredTag); + this.itemElement.setAttribute('class', 'nav-item'); + + // Замінюємо старий елемент новим + this.containerWrapper.innerHTML = ''; + this.containerWrapper.appendChild(this.itemElement); + } else { + // Елемент вже правильний, використовуємо його + this.itemElement = this.containerWrapper.firstChild; + } + + // 3. Встановлюємо атрибути + this.itemElement.innerHTML = innerContent; + + if (href) { + // Це посилання: встановлюємо href та data-route + this.itemElement.setAttribute('href', href); + this.itemElement.setAttribute('data-route', href); // <-- ДОДАНО data-route + this.itemElement.removeAttribute('role'); + this.itemElement.removeAttribute('tabindex'); + } else { + // Це кнопка: видаляємо посилальні атрибути та встановлюємо роль + this.itemElement.removeAttribute('href'); + this.itemElement.removeAttribute('data-route'); + this.itemElement.setAttribute('role', 'button'); + this.itemElement.setAttribute('tabindex', '0'); + } + } + + /** + * Обробляє клік, виконуючи код з атрибута 'click' для не-посилань. + */ + handleItemClick(event) { + const clickAction = this.getAttribute('click'); + const href = this.getAttribute('href'); + + if (href) { + // Якщо це тег , дозволяємо браузеру обробляти клік (або JS-роутеру) + return; + } + + if (clickAction) { + try { + event.preventDefault(); // Запобігаємо стандартній дії (якщо була встановлена роль кнопки) + console.log(`Executing click action: ${clickAction}`); + eval(clickAction); + } catch (e) { + console.error(`Error executing click action "${clickAction}":`, e); + } + } + } +} + +// ========================================================= +// Клас 2: Контейнер Меню (navigation-container) +// ========================================================= + +class NavigationContainer extends HTMLElement { + /** + * Обробляє клік на документі, щоб приховати меню, якщо клік був за його межами. + */ + handleOutsideClick = (event) => { + // Перевіряємо, чи містить наш компонент елемент, на який клікнули + // (this.shadowRoot.host - це сам ) + // або this.menuContainer.contains(event.target) + + if (!this.contains(event.target) && !this.shadowRoot.contains(event.target)) { + // Клік був за межами компонента + this.hideHiddenMenu(); + } + } + handleScroll = () => { + this.hideHiddenMenu(); + } + + + constructor() { + super(); + const shadow = this.attachShadow({ mode: 'open' }); + this.standalone = window.matchMedia('(display-mode: standalone)').matches; + this.os = this.detectOS(); + + // Стилі контейнера + const style = document.createElement('style'); + style.textContent = ` + .navigation-menu{ + position: fixed; + width: 230px; + height: calc(100vh - 60px); + min-height: 510px; + background: var(--ColorThemes2, #525151); + margin: 0; + padding: 40px 10px; + -webkit-transition: width .2s ease 0s; + -o-transition: width .2s ease 0s; + transition: width .2s ease 0s; + display: flex; + flex-direction: column; + justify-content: flex-start; + } + .navigation-items, + .navigation-items-hidden { + position: relative; + display: flex; + flex-direction: column; + min-height: 55px; + align-items: center; + justify-content: flex-start; + gap: 6px; + border-radius: 30px; + } + .navigation-items-hidden { + margin-top: 5px; + } + + .more-button-item { + display: none; + } + + @media (max-width: 1100px) { + .navigation-menu { + width: 100px; + } + } + @media (max-width: 700px), (max-height: 540px) { + .navigation-menu { + width: calc(100% - 30px); + height: 60px; + min-height: 60px; + padding: 0; + z-index: 9991; + bottom: 0px; + background: transparent; + left: 15px; + border: 0; + margin: 0; + bottom: 0px; + } + .navigation-items { + display: flex; + flex-direction: row; + height: 100%; + justify-content: space-around; + align-items: center; + z-index: 9998; + bottom: 10px; + box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.2); + } + .navigation-items::before { + content: ""; + position: absolute; + inset: 0; + background: var(--ColorThemes2, #525151); + background: var(--ColorThemes3, #f3f3f3); + opacity: 0.97; + z-index: 0; + border-radius: 30px; + } + .navigation-items-hidden{ + flex-direction: row; + height: fit-content; + justify-content: space-around; + align-items: center; + position: absolute; + bottom: 12px; + left: 2px; + width: calc(100% - 4px); + margin: 0; + -webkit-transition: .2s ease 0s; + -o-transition: .2s ease 0s; + transition: .2s ease 0s; + z-index: 9992; + opacity: 0; + } + .navigation-items-hidden::before { + content: ""; + position: absolute; + inset: 0; + background: var(--ColorThemes2, #525151); + background: var(--ColorThemes3, #f3f3f3); + opacity: 0.98; + z-index: 0; + border-radius: 30px; + } + + .more-button-item { + display: flex; + } + + .navigation-menu.expanded .navigation-items-hidden { + bottom: 75px; + opacity: 1; + box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.2); + } + .navigation-menu[data-os="iOS"] .navigation-items { + bottom: 15px; + } + .navigation-menu[data-os="iOS"] .navigation-items-hidden { + bottom: 17px; + } + .navigation-menu[data-os="iOS"].expanded .navigation-items-hidden { + bottom: 80px; + opacity: 1; + } + } + @media (max-width: 700px) { + .navigation-menu { + -webkit-transition: 0s ease 0s; + -o-transition: 0s ease 0s; + transition: 0s ease 0s; + } + } + `; + shadow.appendChild(style); + + this.menuContainer = document.createElement('menu'); + this.menuContainer.setAttribute('class', 'navigation-menu'); + if (this.standalone) this.menuContainer.setAttribute('data-os', this.os); + + this.itemsContainer = document.createElement('items'); + this.itemsContainer.setAttribute('class', 'navigation-items'); + + this.itemsHiddenContainer = document.createElement('items-hidden'); + this.itemsHiddenContainer.setAttribute('class', 'navigation-items-hidden'); + + // Слот дозволяє відображати дочірні елементи + const slot = document.createElement('slot'); + this.itemsContainer.appendChild(slot); + + this.menuContainer.appendChild(this.itemsContainer); + this.menuContainer.appendChild(this.itemsHiddenContainer); + shadow.appendChild(this.menuContainer); + + // MutationObserver для відстеження динамічного додавання/видалення пунктів + this.observer = new MutationObserver(this.handleMutations.bind(this)); + // Спостерігаємо за зміною дочірніх елементів + this.observer.observe(this, { childList: true, subtree: false }); + } + + connectedCallback() { + // Додаємо обробник кліків до документа + document.addEventListener('click', this.handleOutsideClick); + // Додаємо обробник прокручування до вікна + window.addEventListener('scroll', this.handleScroll); + + // Повторна перевірка елементів на випадок, якщо вони вже були в DOM до реєстрації + this.reassignItems(); + } + + disconnectedCallback() { + this.observer.disconnect(); + // Видаляємо обробник кліків з документа + document.removeEventListener('click', this.handleOutsideClick); + // Видаляємо обробник прокручування з вікна + window.removeEventListener('scroll', this.handleScroll); + } + + handleMutations(mutationsList, observer) { + // Логіка оновлення при додаванні/видаленні дочірніх елементів (наприклад, для логування) + this.reassignItems(); + } + + reassignItems() { + // Отримуємо всі дочірні елементи (які можуть бути nav-item) + const allItems = Array.from(this.children); + allItems.forEach(item => { + // Перевіряємо, чи має елемент атрибут data-hidden + if (item.getAttribute('data-hidden') == 'true') { + this.itemsHiddenContainer.appendChild(item); + this.createMoreButton(); + } else if (item.parentNode !== this) { + this.appendChild(item); + } + }); + } + + // --- Утиліти --- + hideHiddenMenu() { + this.menuContainer.classList.remove('expanded'); + } + + toggleHiddenMenu() { + this.menuContainer.classList.toggle('expanded'); + } + + createMoreButton() { + let moreButton = this.itemsContainer.querySelector('.more-button-item'); + + if (!moreButton) { + const button = document.createElement('nav-item'); + button.setAttribute('class', 'more-button-item'); + button.setAttribute('title', 'Більше'); + button.setAttribute('icon', + `` + ); + button.addEventListener('click', this.toggleHiddenMenu.bind(this)); + this.itemsContainer.appendChild(button); + + return button; + } + } + + detectOS() { + const userAgent = navigator.userAgent || navigator.vendor || window.opera; + if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { + return 'iOS'; + } + return 'Other'; + } +} + +// ========================================================= +// Реєстрація компонентів у браузері +// ========================================================= + +customElements.define('nav-item', NavigationItem); +customElements.define('navigation-container', NavigationContainer); \ No newline at end of file diff --git a/web/lib/customElements/notification.js b/web/lib/customElements/notifManager.js similarity index 70% rename from web/lib/customElements/notification.js rename to web/lib/customElements/notifManager.js index 8e1103f..d1c4656 100644 --- a/web/lib/customElements/notification.js +++ b/web/lib/customElements/notifManager.js @@ -1,20 +1,28 @@ -class AppNotificationContainer extends HTMLElement { +/** + * Клас NotificationContainer + * Веб-компонент для відображення системних сповіщень. + * Використовує Shadow DOM для інкапсуляції стилів. + */ +class NotificationContainer extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); - // Настройки по умолчанию this._timeout = 4000; this._maxVisible = 5; this._position = 'top-right'; this._mobileBottomEnabled = false; + // *** Вдосконалення: Відстежуємо ноди, що видаляються *** + this._removingNodes = new Set(); + this._container = document.createElement('div'); this._container.className = 'app-notification-container'; this.shadowRoot.appendChild(this._container); this._insertStyles(); + // SVG іконки для різних типів сповіщень (залишаємо як є) this._icons = { info: ``, success: ``, @@ -43,7 +51,6 @@ class AppNotificationContainer extends HTMLElement { this._timeout = parseInt(this.getAttribute('timeout')) || 4000; const mobilePosAttr = this.getAttribute('mobile-position'); - // Если атрибут установлен в 'bottom' или присутствует (как пустая строка, если это булевый атрибут) this._mobileBottomEnabled = mobilePosAttr === 'bottom' || mobilePosAttr === ''; this._container.setAttribute('data-position', this._position); @@ -51,9 +58,6 @@ class AppNotificationContainer extends HTMLElement { this._applyMobileStyles(); } - /** - * Динамически применяет класс, который активирует мобильные стили "только снизу". - */ _applyMobileStyles() { if (this._mobileBottomEnabled) { this._container.classList.add('mobile-bottom'); @@ -62,15 +66,14 @@ class AppNotificationContainer extends HTMLElement { } } - /** - * Публичный метод для изменения настройки мобильной позиции во время выполнения. - * @param {boolean} enable - true, чтобы принудительно устанавливать позицию снизу на мобильных, false, чтобы использовать обычные @media стили. - */ setMobileBottom(enable) { this._mobileBottomEnabled = !!enable; this._applyMobileStyles(); } + /** + * Показує нове сповіщення. + */ show(message, options = {}) { const { type = 'info', @@ -84,15 +87,28 @@ class AppNotificationContainer extends HTMLElement { ? { title: title || '', text: message } : message; - while (this._container.children.length >= this._maxVisible) { - const first = this._container.firstElementChild; - if (first) first.remove(); - else break; - } + // **Оновлена логіка обмеження кількості:** + // Визначаємо кількість "видимих" нод (які не перебувають у процесі видалення) + const totalChildren = this._container.children.length; + const visibleNodesCount = totalChildren - this._removingNodes.size; + while (visibleNodesCount >= this._maxVisible) { + // Шукаємо найстаріший елемент, який ще НЕ видаляється + const first = Array.from(this._container.children).find(n => !this._removingNodes.has(n)); + + if (first) { + this._removeNode(first); + } else { + // Якщо всі елементи в процесі видалення, виходимо + break; + } + } + // Кінець оновленої логіки обмеження + + // Створення DOM елементів (залишаємо як є) const node = document.createElement('div'); node.className = `app-notification ${type}`; - if (onClick) node.style.cursor = "pointer" + if (onClick) node.style.cursor = "pointer"; const icon = document.createElement('div'); icon.className = 'icon'; @@ -114,6 +130,7 @@ class AppNotificationContainer extends HTMLElement { node.appendChild(icon); node.appendChild(body); + // Додаємо кнопку закриття if (!onClick && !lock) { const closeDiv = document.createElement('div'); closeDiv.className = 'blockClose'; @@ -121,28 +138,35 @@ class AppNotificationContainer extends HTMLElement { const closeBtn = document.createElement('button'); closeBtn.className = 'close'; - closeBtn.setAttribute('aria-label', 'Закрыть уведомление'); + closeBtn.setAttribute('aria-label', 'Закрити повідомлення'); closeBtn.innerHTML = ''; closeDiv.appendChild(closeBtn); - closeBtn.addEventListener('click', () => this._removeNode(node)); + closeBtn.addEventListener('click', (e) => { + e.stopPropagation(); // Запобігаємо спрацьовуванню onClick на самій ноді + this._removeNode(node); + }); } this._container.appendChild(node); + // Запускаємо анімацію появи через requestAnimationFrame requestAnimationFrame(() => node.classList.add('show')); let timer = null; const startTimer = () => { - if (timeout === 0) return; + if (timeout === 0 || lock) return; timer = setTimeout(() => this._removeNode(node), timeout); }; const clearTimer = () => { if (timer) { clearTimeout(timer); timer = null; } }; + // Зупинка таймауту при наведенні node.addEventListener('mouseenter', clearTimer); node.addEventListener('mouseleave', startTimer); + // Обробка кліку на сповіщенні if (typeof onClick === 'function') { node.addEventListener('click', () => { - try { onClick(); } catch (e) { } + clearTimer(); // Зупиняємо таймаут, якщо він був + try { onClick(); } catch (e) { console.error(e); } this._removeNode(node); }); } @@ -152,29 +176,50 @@ class AppNotificationContainer extends HTMLElement { return node; } + /** + * Приватний метод для видалення ноди з анімацією. + * **Тепер перевіряє, чи нода вже видаляється.** + */ _removeNode(node) { - if (!node || !node.parentElement) return; + if (!node || !node.parentElement || this._removingNodes.has(node)) return; + + this._removingNodes.add(node); // Позначаємо як ту, що видаляється + node.classList.remove('show'); + // Чекаємо завершення анімації зникнення (200мс) setTimeout(() => { - if (node && node.parentElement) node.parentElement.removeChild(node); + if (node && node.parentElement) { + node.parentElement.removeChild(node); + } + this._removingNodes.delete(node); // Видаляємо зі списку після фізичного видалення }, 200); } + /** + * Видаляє всі видимі сповіщення. + */ clearAll() { if (!this._container) return; + // Використовуємо _removeNode, який тепер безпечно обробляє повторні виклики Array.from(this._container.children).forEach(n => this._removeNode(n)); } + // Допоміжні методи з фіксованим типом сповіщення info(message, opts = {}) { return this.show(message, { ...opts, type: 'info' }); } success(message, opts = {}) { return this.show(message, { ...opts, type: 'success' }); } warn(message, opts = {}) { return this.show(message, { ...opts, type: 'warn' }); } error(message, opts = {}) { return this.show(message, { ...opts, type: 'error' }); } + // Метод для сповіщень, що реагують на клік (псевдонім 'click' для 'show' з 'onClick') click(message, opts = {}) { return this.show(message, { ...opts, onClick: opts.f }); } + /** + * Вставляє необхідні CSS стилі в Shadow DOM (залишаємо як є). + */ _insertStyles() { const style = document.createElement('style'); style.textContent = ` + /* Контейнер */ .app-notification-container { position: fixed; z-index: 9999; @@ -184,11 +229,13 @@ class AppNotificationContainer extends HTMLElement { gap: 10px; padding: 12px; } + /* Позиціонування контейнера */ .app-notification-container[data-position="top-right"] { top: 8px; right: 8px; align-items: flex-end; } .app-notification-container[data-position="top-left"] { top: 8px; left: 8px; align-items: flex-start; } .app-notification-container[data-position="bottom-right"] { bottom: 8px; right: 8px; align-items: flex-end; } .app-notification-container[data-position="bottom-left"] { bottom: 8px; left: 8px; align-items: flex-start; } + /* Одне сповіщення */ .app-notification { pointer-events: auto; min-width: 220px; @@ -230,6 +277,8 @@ class AppNotificationContainer extends HTMLElement { } .app-notification .body { flex:1; } .app-notification .title { font-weight: 600; margin-bottom: 4px; font-size: 13px; } + + /* Кнопка закриття */ .app-notification .blockClose { width: 20px; height: 20px; @@ -253,6 +302,7 @@ class AppNotificationContainer extends HTMLElement { display: block; } + /* Стилі за типами */ .app-notification.info { background: var(--ColorThemes3, #2196F3); color: var(--ColorThemes0, #ffffff); @@ -273,6 +323,7 @@ class AppNotificationContainer extends HTMLElement { .app-notification.error .icon { background: #c45050; } .app-notification.error .close svg{fill: #fff;} + /* Адаптивність для мобільних пристроїв */ @media (max-width: 700px) { .app-notification-container { left: 0; @@ -281,10 +332,10 @@ class AppNotificationContainer extends HTMLElement { align-items: center !important; } .app-notification-container .app-notification { - max-width: 95%; - min-width: 95%; + max-width: calc(100% - 30px); + min-width: calc(100% - 30px); } - + /* Спеціальна мобільна позиція знизу */ .app-notification-container.mobile-bottom { top: auto; bottom: 0; @@ -295,32 +346,62 @@ class AppNotificationContainer extends HTMLElement { } } -customElements.define('app-notification-container', AppNotificationContainer); +// Реєструємо веб-компонент у браузері +customElements.define('notification-container', NotificationContainer); -/* - */ -// const Notifier = document.getElementById('notif-manager'); + -// 💡 Включить принудительную позицию снизу для мобильных -// Notifier.setMobileBottom(true); +2. Отримайте посилання на компонент у JS: +const Notifier = document.getElementById('notif-manager'); -// 💡 Отключить принудительную позицию снизу (вернется к поведению @media или position) -// Notifier.setMobileBottom(false); +3. Приклади викликів: +💡 Базові сповіщення +Notifier.info('Налаштування мобільної позиції змінено.'); +Notifier.success('Успішна операція.'); +Notifier.warn('Увага: низький рівень заряду батареї.'); +Notifier.error('Критична помилка!'); -// Пример использования +💡 Сповіщення із заголовком +Notifier.info('Це повідомлення має чіткий заголовок.', { + title: 'Важлива інформація' +}); -// Notifier.info('Настройки мобильной позиции изменены.'); -// Notifier.info('Привет! Это ваше первое уведомление через Web Component.', { -// title: 'Успешная инициализация', -// onClick: () => alert('Вы кликнули!'), -// lock: false -// }); -// Notifier.success('Успешная операция.'); -// Notifier.error('Критическая ошибка!', { timeout: 0, lock: true }); -// Notifier.warn({ title: `Metrics`, text: `З'єднання встановлено` }, { timeout: 0 }); \ No newline at end of file +💡 Сповіщення з об'єктом (заголовок та текст) +Notifier.warn({ + title: `Metrics`, + text: `З'єднання встановлено` +}); + +💡 Сповіщення, яке не зникає (timeout: 0 або lock: true) +Notifier.error('Критична помилка! Необхідне втручання.', { + timeout: 0, + lock: true +}); + +💡 Сповіщення з обробником кліку (автоматично закривається після кліку) +Notifier.info('Натисніть тут, щоб побачити деталі.', { + onClick: () => alert('Ви клацнули! Дякую.'), + lock: false +}); + +💡 Програмне керування +Notifier.setMobileBottom(true); // Включити примусову позицію знизу для мобільних +Notifier.setMobileBottom(false); // Вимкнути примусову позицію знизу +Notifier.clearAll(); // Видалити всі сповіщення +*/ \ No newline at end of file diff --git a/web/lib/customElements/pwaInstallBanner.js b/web/lib/customElements/pwaInstallBanner.js new file mode 100644 index 0000000..d113a85 --- /dev/null +++ b/web/lib/customElements/pwaInstallBanner.js @@ -0,0 +1,301 @@ +class PwaInstallBanner extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.deferredPrompt = null; + this.STORAGE_KEY = 'PwaInstallBanner'; // Визначаємо ключ localStorage + this.isInStandaloneMode = () => ('standalone' in window.navigator && window.navigator.standalone === true); + this.os = this.detectOS(); + } + + connectedCallback() { + // Додаємо стилі та розмітку до Shadow DOM + this.shadowRoot.innerHTML = this.getStyles() + this.getTemplate(); + + this.elements = { + backdrop: this.shadowRoot.getElementById('blur-backdrop'), + installOverlay: this.shadowRoot.getElementById('pwa-install-overlay'), + iosOverlay: this.shadowRoot.getElementById('pwa-ios-overlay'), + installButton: this.shadowRoot.getElementById('pwa-install-button'), + closeButton: this.shadowRoot.getElementById('pwa-close-button'), + iosCloseButton: this.shadowRoot.getElementById('pwa-ios-close-button'), + }; + + this.setupListeners(); + this.checkInitialDisplay(); + } + + // --- Утиліти --- + + detectOS() { + const userAgent = navigator.userAgent || navigator.vendor || window.opera; + if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { + return 'iOS'; + } + // ... (можна додати Android, Windows, але для PWA нас цікавить в першу чергу iOS) + return 'Other'; + } + + shouldShowBanner() { + return localStorage.getItem(this.STORAGE_KEY) !== 'false'; + } + + checkInitialDisplay() { + if (!this.shouldShowBanner()) { + return; // Не показуємо, якщо localStorage = 'false' + } + + // Логіка для iOS + if (this.os === 'iOS' && !this.isInStandaloneMode()) { + // Затримка відображення, як у вихідному коді + setTimeout(() => { + this.openPopup(this.elements.iosOverlay); + }, 1000); + } + } + + openPopup(overlayElement) { + this.elements.backdrop.classList.remove('pwa-hidden'); + overlayElement.classList.remove('pwa-hidden'); + document.body.classList.add('modal-open'); + } + + closePopup = () => { + this.elements.installOverlay.classList.add('pwa-hidden'); + this.elements.iosOverlay.classList.add('pwa-hidden'); + this.elements.backdrop.classList.add('pwa-hidden'); + document.body.classList.remove('modal-open'); + this.deferredPrompt = null; + } + + // --- Обробники подій --- + + setupListeners() { + window.addEventListener("beforeinstallprompt", this.handleBeforeInstallPrompt); + + // Обробники кнопок + this.elements.installButton.addEventListener("click", this.handleInstallClick); + this.elements.closeButton.addEventListener("click", this.closePopup); + this.elements.iosCloseButton.addEventListener('click', this.closePopup); + } + + handleBeforeInstallPrompt = (e) => { + // Вихідний код перевіряв localStorage, але для простоти прикладу я її пропускаю. + if (!this.shouldShowBanner()) { + return; // Не показуємо, якщо localStorage = 'false' + } + + e.preventDefault(); + this.deferredPrompt = e; + + // Показуємо стандартний банер, якщо доступно і не в режимі iOS + if (this.os !== 'iOS') { + this.openPopup(this.elements.installOverlay); + } + } + + handleInstallClick = async () => { + if (!this.deferredPrompt) return; + + this.deferredPrompt.prompt(); + const { outcome } = await this.deferredPrompt.userChoice; + console.log(`[APP] Результат встановлення PWA: ${outcome}`); + + this.closePopup(); + } + + // --- Шаблон (Template) та Стилі (Styles) --- + + getTemplate() { + // HTML розмітка з вихідного коду + return ` +
+
+ +
+
+ +
+ `; + } + + getStyles() { + // CSS стилі, які були у вихідному коді, але адаптовані для Shadow DOM + // Примітки: + // 1. Змінні CSS (наприклад, --ColorThemes0) мають бути визначені в основному документі + // або передані через властивості, інакше вони не працюватимуть в Shadow DOM. + // Я залишаю їх як є, припускаючи, що вони глобально доступні. + // 2. Стилі для body.modal-open потрібно додати в основний CSS. + + return ` + + `; + } +} + +// Реєстрація веб-компонента +customElements.define('pwa-install-banner', PwaInstallBanner); \ No newline at end of file diff --git a/web/lib/customElements/smartSelect.js b/web/lib/customElements/smartSelect.js new file mode 100644 index 0000000..73bd3f0 --- /dev/null +++ b/web/lib/customElements/smartSelect.js @@ -0,0 +1,292 @@ +const SMART_SELECT_STYLES_CSS = ` + :host { display: block; position: relative; width: 100%; font-family: system-ui, sans-serif; } + :host ::-webkit-scrollbar { height: 5px; width: 8px; } + :host ::-webkit-scrollbar-track { background: transparent; } + :host ::-webkit-scrollbar-thumb { background: var(--smart-select-chip-background, #475569); border-radius: 4px; } + @media (hover: hover) { + :host ::-webkit-scrollbar-thumb:hover { opacity: 0.7; } + } + + /* Стили для скролла */ + .trigger::-webkit-scrollbar { height: 4px; } + .trigger::-webkit-scrollbar-thumb { background: var(--smart-select-chip-background, #475569); border-radius: 4px; } + + .wrapper { + min-height: 35px; + border: 1px solid var(--smart-select-border-color, #ccc); + border-radius: var(--smart-select-border-radius-1, 6px); + display: flex; + padding: 0 6px; + flex-direction: column; + justify-content: center; + background: var(--smart-select-background, #fff); + } + + .trigger { + display: flex; + gap: 6px; + width: 100%; + overflow-x: auto; + align-items: center; + cursor: pointer; + } + + .placeholder-text { + color: var(--smart-select-chip-color); + opacity: 0.4; + pointer-events: none; + user-select: none; + font-size: var(--smart-select-font-size-2); + } + + .chip { + background: var(--smart-select-chip-background, #dbe3ea); + color: var(--smart-select-chip-color, #000); + padding: 4px 6px; + border-radius: var(--smart-select-border-radius-2, 4px); + font-size: var(--smart-select-font-size-1, 12px); + display: flex; + align-items: center; + gap: 3px; + white-space: nowrap; + cursor: pointer; + } + + .chip button { + display: flex; + position: relative; + background: none; + border: none; + cursor: pointer; + padding: 0; + width: 20px; + height: 15px; + justify-content: flex-end; + fill: var(--smart-select-chip-fill, #e91e63); + } + + .chip button svg{ width: 15px; height: 15px; } + + .dropdown { + display: none; position: absolute; top: 100%; left: 0; right: 0; + background: var(--smart-select-background, #ffffff); + border: 1px solid var(--smart-select-border-color, #ccc); + border-radius: var(--smart-select-border-radius-1, 6px); z-index: 9999; margin-top: 4px; + flex-direction: column; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); + } + + :host([open]) .dropdown { display: flex; } + + input[type="search"] { + margin: 10px; + padding: 8px 10px; + border-radius: var(--smart-select-border-radius-2, 4px); + border: 1px solid var(--smart-select-search-border, #ccc); + background: transparent; + color: var(--smart-select-search-color, #000); + } + + .options-list {max-height: 200px; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; } + + ::slotted([slot="option"]) { + padding: 8px 12px !important; + cursor: pointer; + border-radius: var(--smart-select-border-radius-2, 4px); + display: block; + color: var(--smart-select-option-color, #000); + font-size: var(--smart-select-font-size-2, 14px); + } + + @media (hover: hover) { + ::slotted([slot="option"]:hover) { + background: var(--smart-select-hover-background, #475569); + color: var(--smart-select-hover-color, #fff); + } + } + + ::slotted([slot="option"].selected) { + background: var(--smart-select-selected-background, #dbe3eb) !important; + color: var(--smart-select-selected-color, #000) !important; + } +`; + +// Створення об'єкта CSSStyleSheet (якщо підтримується) +let SmartSelectStyles = null; +if (typeof CSSStyleSheet !== 'undefined' && CSSStyleSheet.prototype.replaceSync) { + SmartSelectStyles = new CSSStyleSheet(); // (2) Визначення об'єкта тут + SmartSelectStyles.replaceSync(SMART_SELECT_STYLES_CSS); +} + +class SmartSelect extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this._selectedValues = new Set(); + + // Додаємо стилі в конструкторі, якщо це adoptable + if (this.shadowRoot.adoptedStyleSheets) { + this.shadowRoot.adoptedStyleSheets = [SmartSelectStyles]; + } else { + // FALLBACK для старих браузерів (наприклад, iOS < 16.4) + const style = document.createElement('style'); + style.textContent = SMART_SELECT_STYLES_CSS; + this.shadowRoot.appendChild(style); + } + } + + connectedCallback() { + this.render(); + + const searchInput = this.shadowRoot.querySelector('input[type="search"]'); + searchInput.addEventListener('input', (e) => this.handleSearch(e)); + + const wrapper = this.shadowRoot.querySelector('.wrapper'); + wrapper.addEventListener('click', (e) => { + if (e.target.closest('button')) return; + this.toggleAttribute('open'); + if (this.hasAttribute('open')) { + setTimeout(() => searchInput.focus(), 50); + } + }); + + // Слушаем клики по элементам в слоте + this.addEventListener('click', (e) => { + const opt = e.target.closest('[slot="option"]'); + if (opt) { + this.toggleValue(opt.getAttribute('data-value'), opt); + } + }); + + // Слушаем изменение слота для инициализации (фикс Safari) + this.shadowRoot.querySelector('slot').addEventListener('slotchange', () => { + this.syncOptions(); + }); + + this.syncOptions(); + } + + _formatValue(val) { + const isNumber = this.getAttribute('type') === 'number'; + return isNumber ? Number(val) : String(val); + } + + syncOptions() { + const options = Array.from(this.querySelectorAll('[slot="option"]')); + options.forEach(opt => { + // Используем data-selected вместо атрибута selected + if (opt.hasAttribute('data-selected')) { + // Зберігаємо вже у потрібному форматі + const val = this._formatValue(opt.getAttribute('data-value')); + this._selectedValues.add(val); + } + }); + this.updateDisplay(); + } + + handleSearch(e) { + const term = e.target.value.toLowerCase().trim(); + const options = this.querySelectorAll('[slot="option"]'); + options.forEach(opt => { + const text = opt.textContent.toLowerCase(); + opt.style.display = text.includes(term) ? '' : 'none'; + }); + } + + toggleValue(val) { + const max = this.getAttribute('max') ? parseInt(this.getAttribute('max')) : null; + + const formattedVal = this._formatValue(val); + + if (this._selectedValues.has(formattedVal)) { + this._selectedValues.delete(formattedVal); + this._click = { + value: formattedVal, + state: "delete" + }; + } else { + if (max && this._selectedValues.size >= max) return; + this._selectedValues.add(formattedVal); + this._click = { + value: formattedVal, + state: "add" + }; + } + + this.updateDisplay(); + this.dispatchEvent(new Event('change', { bubbles: true })); + } + + updateDisplay() { + const container = this.shadowRoot.getElementById('tags'); + const placeholder = this.shadowRoot.getElementById('placeholder'); + const optionsElements = this.querySelectorAll('[slot="option"]'); + + // Керування видимістю плейсхолдера + if (this._selectedValues.size > 0) { + placeholder.style.display = 'none'; + } else { + placeholder.style.display = 'block'; + } + + container.innerHTML = ''; + this._selectedValues.forEach(val => { + // Важливо: при пошуку елемента в DOM атрибут data-value завжди рядок, + // тому використовуємо == для порівняння числа з рядком + const opt = Array.from(optionsElements).find(o => o.getAttribute('data-value') == val); + if (opt) { + const chip = document.createElement('div'); + chip.className = 'chip'; + chip.innerHTML = `${opt.textContent} `; + chip.querySelector('button').onclick = (e) => { + e.stopPropagation(); + this.toggleValue(val); + }; + container.appendChild(chip); + } + }); + + const max = this.getAttribute('max') ? parseInt(this.getAttribute('max')) : null; + const isFull = max && this._selectedValues.size >= max; + + optionsElements.forEach(opt => { + const optVal = this._formatValue(opt.getAttribute('data-value')); + const isSelected = this._selectedValues.has(optVal); + opt.classList.toggle('selected', isSelected); + + // Если лимит исчерпан, делаем невыбранные опции полупрозрачными + if (isFull && !isSelected) { + opt.style.opacity = '0.5'; + opt.style.cursor = 'not-allowed'; + } else { + opt.style.opacity = '1'; + opt.style.cursor = 'pointer'; + } + }); + } + + get value() { + return Array.from(this._selectedValues); + } + + get getClick() { + return this._click; + } + + render() { + this.shadowRoot.innerHTML = ` +
+
+
+ ${this.getAttribute('placeholder') || 'Оберіть значення...'} +
+
+ + `; + } +} +customElements.define('smart-select', SmartSelect); \ No newline at end of file diff --git a/web/lib/customElements/swipeUpdater.js b/web/lib/customElements/swipeUpdater.js new file mode 100644 index 0000000..27f45fe --- /dev/null +++ b/web/lib/customElements/swipeUpdater.js @@ -0,0 +1,234 @@ +/** + * Вебкомпонент для ініціації оновлення сторінки (Pull-to-Refresh) + * за допомогою свайпу вниз на пристроях з iOS/iPadOS у режимі PWA. + */ +class SwipeUpdater extends HTMLElement { + constructor() { + super(); + + // 1. Створення Shadow DOM + // Використовуємо тіньовий DOM для інкапсуляції стилів та структури + const shadow = this.attachShadow({ mode: 'open' }); + + // 2. Внутрішня функція оновлення за замовчуванням + this._appReload = () => { + console.log('Стандартна функція: Перезавантаження сторінки'); + // Стандартна дія - перезавантаження сторінки + window.location.reload(); + }; + + // 3. Внутрішній стан + this._isReadyToReload = false; // Прапорець, що вказує на готовність до оновлення + + // 4. Створення елементів (Внутрішній HTML) + shadow.innerHTML = ` +
+
+ + + +
+
+ + + `; + + // 5. Збереження посилань на елементи Shadow DOM + this._animID = shadow.getElementById('swipe_updater'); + this._animIconID = shadow.getElementById('swipe_icon'); + + // 6. Прив'язка контексту `this` для обробника подій (важливо для коректної роботи `this.handleScroll`) + this.handleScroll = this.handleScroll.bind(this); + } + + /** + * Метод для встановлення користувацької функції оновлення. + * Замінює стандартне перезавантаження сторінки. + * @param {function} func - Користувацька функція, що буде викликана при свайпі. + */ + setReloadFunction(func) { + if (typeof func === 'function') { + this._appReload = func; + } else { + console.error('setReloadFunction вимагає передати функцію.'); + } + } + + /** + * Обробник події скролу (головна логіка Pull-to-Refresh). + * Відстежує прокручування вище верхньої межі сторінки (`window.scrollY < 0`). + */ + handleScroll() { + // Перевірка на режим Standalone (PWA) - функціональність актуальна переважно тут + const isStandalone = window.matchMedia('(display-mode: standalone)').matches; + + if (isStandalone) { + let scrollY = window.scrollY; + + // 1. Анімація іконки під час прокручування за верхню межу (scrollY < 0) + if (scrollY <= -10) { + // Зміщення іконки разом із прокруткою + this._animIconID.style.top = `${scrollY / 1.5}px`; + this._animID.style.zIndex = '115'; // Піднімаємо z-index для видимості над контентом + } else { + this._animID.style.zIndex = '0'; // Повертаємо базовий z-index + } + + const threshold = -125; // Поріг прокрутки (наприклад, -125px) для активації оновлення + + // 2. Логіка активації "готовий до оновлення" + if (scrollY <= threshold) { + if (!this._isReadyToReload) { + this._isReadyToReload = true; + // Поворот іконки на 180 градусів + this._animIconID.style.transform = 'rotate(180deg)'; + this._animIconID.setAttribute('data-state', ''); // Деактивація стану "active" + } + } + // 3. Логіка виклику оновлення та скидання стану + // Якщо користувач відпускає свайп (scrollY повертається до >= 0) І був готовий до оновлення + else if (scrollY >= 0) { + if (this._isReadyToReload) { + // Виклик користувацької функції (або стандартного window.location.reload()) + this._appReload(); + + // Скидання стану та анімації + this._isReadyToReload = false; + this._animIconID.style.transform = 'rotate(0deg)'; + this._animIconID.setAttribute('data-state', 'active'); + } + } + } + } + + + /** + * Lifecycle hook: викликається при додаванні елемента в DOM. + * Додаємо обробник події прокручування. + */ + connectedCallback() { + // Прослуховування глобальної події скролу + window.addEventListener('scroll', this.handleScroll); + } + + /** + * Lifecycle hook: викликається при видаленні елемента з DOM. + * Видаляємо обробник події прокручування для запобігання витоку пам'яті. + */ + disconnectedCallback() { + window.removeEventListener('scroll', this.handleScroll); + } +} + +// Реєстрація веб-компонента +customElements.define('swipe-updater', SwipeUpdater); + + +/* + ============================ + ПРИКЛАД ВИКОРИСТАННЯ + ============================ +*/ + +/* +1. Додайте цей елемент у свій HTML: + + +2. Отримайте посилання на компонент у JS: +const Updater = document.querySelector('swipe-updater'); + +3. Користувацька функція оновлення +function customReload() { + const now = new Date().toLocaleTimeString(); + console.log(`Користувацьке оновлення: оновлено о ${now}`); + document.querySelector('h1').textContent = `Сторінка оновлена о ${now}`; + // Тут можна виконати AJAX-запит, оновити DOM тощо. +} + +4. Перевизначення функції оновлення компонента +if (Updater && Updater.setReloadFunction) { + Updater.setReloadFunction(customReload); +} else { + console.error('Компонент SwipeUpdater не знайдено або не готовий.'); +} + + +💡 Приклад стандартного використання (якщо не викликати setReloadFunction): + +При свайпі буде викликано window.location.reload(); + +*/ \ No newline at end of file diff --git a/web/lib/customElements/territoryCard.js b/web/lib/customElements/territoryCard.js index 199237c..62dc955 100644 --- a/web/lib/customElements/territoryCard.js +++ b/web/lib/customElements/territoryCard.js @@ -1,5 +1,5 @@ -const appTerritoryCardStyles = new CSSStyleSheet(); -appTerritoryCardStyles.replaceSync(` +// Заміна вмісту таблиці стилів на надані CSS-правила. +const CARD_STYLES_CSS = ` :host { display: inline-block; box-sizing: border-box; @@ -54,14 +54,15 @@ appTerritoryCardStyles.replaceSync(` width: 100%; height: 100%; object-fit: cover; + position: relative; + z-index: 1; + filter: blur(3px); + border-radius: calc(var(--border-radius, 15px) - 5px); + } + .contents { position: absolute; top: 0; left: 0; - z-index: 1; - filter: blur(3px); - } - .contents { - position: relative; z-index: 2; background: rgb(64 64 64 / 0.7); width: 100%; @@ -89,7 +90,7 @@ appTerritoryCardStyles.replaceSync(` - /* Стили для режима 'sheep' */ + /* Стилі для режиму 'sheep' */ .sheep { margin: 10px; max-height: 50px; @@ -116,7 +117,7 @@ appTerritoryCardStyles.replaceSync(` } - /* Стили для режима 'info' (прогресс) */ + /* Стилі для режиму 'info' (прогресс) */ .info { margin: 10px; } @@ -133,7 +134,7 @@ appTerritoryCardStyles.replaceSync(` } .info span { z-index: 2; - font-size: var(--FontSize1, 12px); + font-size: var(--FontSize3, 14px); color: var(--ColorThemes3, #f3f3f3); } .info p { @@ -158,24 +159,45 @@ appTerritoryCardStyles.replaceSync(` width: 100%; height: 100%; z-index: 10; + border-radius: calc(var(--border-radius, 15px) - 5px); } -`); +`; +// Створення об'єкта CSSStyleSheet (якщо підтримується) +let appTerritoryCardStyles = null; +if (typeof CSSStyleSheet !== 'undefined' && CSSStyleSheet.prototype.replaceSync) { + appTerritoryCardStyles = new CSSStyleSheet(); // (2) Визначення об'єкта тут + appTerritoryCardStyles.replaceSync(CARD_STYLES_CSS); +} + +/** + * Веб-компонент AppTerritoryCard. + * Відображає картку території з фоновим зображенням та різними режимами відображення. + */ class AppTerritoryCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); + // Додаємо стилі в конструкторі, якщо це adoptable if (this.shadowRoot.adoptedStyleSheets) { this.shadowRoot.adoptedStyleSheets = [appTerritoryCardStyles]; + } else { + // FALLBACK для старих браузерів (наприклад, iOS < 16.4) + const style = document.createElement('style'); + style.textContent = CARD_STYLES_CSS; + this.shadowRoot.appendChild(style); } } - // Определяем, какие атрибуты будем отслеживать + // Вказуємо, які атрибути ми хочемо відстежувати static get observedAttributes() { - return ['image', 'address', 'sheep', 'link', 'atWork', 'quantity']; + return ['image', 'address', 'sheep', 'link', 'atWork', 'quantity', 'overdue']; } + // Геттери та Сеттери для атрибутів + // Вони спрощують роботу з атрибутами як з властивостями DOM-елемента + get image() { return this.getAttribute('image'); } @@ -198,6 +220,11 @@ class AppTerritoryCard extends HTMLElement { } } + /** * Атрибут 'sheep' може приймати три стани: + * 1. null / відсутній: відключення блоку sheep та info + * 2. порожній рядок ('') / присутній без значення: Режим "Територія не опрацьовується" + * 3. рядок зі значенням: Режим "Територію опрацьовує: [значення]" + */ get sheep() { return this.getAttribute('sheep'); } @@ -228,6 +255,7 @@ class AppTerritoryCard extends HTMLElement { if (newValue === null) { this.removeAttribute('atWork'); } else { + // Приводимо до рядка, оскільки атрибути завжди є рядками this.setAttribute('atWork', String(newValue)); } } @@ -239,41 +267,74 @@ class AppTerritoryCard extends HTMLElement { if (newValue === null) { this.removeAttribute('quantity'); } else { + // Приводимо до рядка this.setAttribute('quantity', String(newValue)); } } - // Вызывается при добавлении элемента в DOM + get overdue() { + return this.getAttribute('address'); + } + set overdue(newValue) { + if (newValue === null) { + this.removeAttribute('overdue'); + } else { + this.setAttribute('overdue', newValue); + } + } + + /** + * connectedCallback викликається, коли елемент додається в DOM. + * Тут ми викликаємо початкове рендеринг. + */ connectedCallback() { this.render(); } - // Вызывается при изменении одного из отслеживаемых атрибутов + /** + * attributeChangedCallback викликається при зміні одного зі спостережуваних атрибутів. + */ attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { - this.render(); + this.render(); // Перерендеринг при зміні атрибута } } + /** + * Логіка рендерингу (відображення) вмісту компонента. + */ render() { const image = this.getAttribute('image') || ''; const address = this.getAttribute('address') || ''; - const sheep = this.getAttribute('sheep'); // Может быть null или "" + const sheep = this.getAttribute('sheep'); const link = this.getAttribute('link') || '#'; - const atWork = this.getAttribute('atWork'); // Может быть null - const quantity = this.getAttribute('quantity'); // Может быть null + const atWork = this.getAttribute('atWork'); + const quantity = this.getAttribute('quantity'); + const overdue = this.getAttribute('overdue') == 'true' ? true : false; + + this.shadowRoot.innerHTML = ``; + + // Додаємо стилі для старих браузерів + if (!this.shadowRoot.adoptedStyleSheets) { + const style = document.createElement('style'); + style.textContent = CARD_STYLES_CSS; + this.shadowRoot.appendChild(style); + } + - // --- Логика определения контента --- let contentHTML = ''; + + // Перевіряємо, чи має бути увімкнений режим прогресу ('info'): + // обидва атрибути 'atWork' та 'quantity' присутні і є коректними числами. const isProgressMode = atWork !== null && quantity !== null && !isNaN(parseInt(atWork)) && !isNaN(parseInt(quantity)); - const hasSheep = sheep !== null && sheep !== ''; if (isProgressMode) { - // Режим прогресса (свободные подъезды) + // Режим прогресу (вільні під'їзди) const atWorkNum = parseInt(atWork); const quantityNum = parseInt(quantity); const free = quantityNum - atWorkNum; + // Обчислення відсотка прогресу. Уникнення ділення на нуль. const progressPercent = quantityNum > 0 ? (atWorkNum / quantityNum) * 100 : 100; contentHTML = ` @@ -286,15 +347,15 @@ class AppTerritoryCard extends HTMLElement {
`; } else if (sheep !== null && sheep !== '') { - // Режим ответственного + // Режим опрацювання (значення атрибута 'sheep' є ім'ям опрацювача) contentHTML = ` -
+
Територію опрацьовує:

${sheep}

`; - } else if (sheep !== null) { - // Режим "не опрацьовується" + } else if (sheep !== null && sheep === '') { + // Режим "не опрацьовується" (атрибут 'sheep' присутній, але порожній) contentHTML = `
Територія не опрацьовується @@ -302,9 +363,9 @@ class AppTerritoryCard extends HTMLElement { `; } - // --- Сборка всего шаблона --- - this.shadowRoot.innerHTML = ` -
+ // --- Складання всього шаблону --- + this.shadowRoot.innerHTML += ` +
${address}

${address}

@@ -316,8 +377,42 @@ class AppTerritoryCard extends HTMLElement { } } -// Регистрируем веб-компонент +// Реєструємо веб-компонент у браузері customElements.define('app-territory-card', AppTerritoryCard); -// document.getElementById('app-territory-card-1').setAttribute('sheep', 'test') +/* + ============================ + ПРИКЛАД ВИКОРИСТАННЯ + ============================ +*/ + +/* + + + + + + + +*/ \ No newline at end of file diff --git a/web/lib/pages/auth/index.html b/web/lib/pages/auth/index.html index 72d44b4..24e38f6 100644 --- a/web/lib/pages/auth/index.html +++ b/web/lib/pages/auth/index.html @@ -2,7 +2,7 @@
`, + href: '/stand' + }); + } + if (USER.possibilities.can_view_schedule) { + newMenuItems({ + id: 'menu-schedule', + title: 'Графіки зібрань', + icon: ``, + href: '/schedule' + }); + } + if (USER.possibilities.can_view_sheeps) { + newMenuItems({ + id: 'menu-sheeps', + title: 'Вісники', + icon: ``, + href: '/sheeps', + hidden: true + }); + await Sheeps.sheeps_list.loadAPI(); + } + if (USER.possibilities.can_manager_territory) { + newMenuItems({ + id: 'menu-territory', + title: 'Території', + icon: ``, + href: '/territory', + hidden: true + }); + } + newMenuItems({ + id: 'menu-options', + title: 'Опції', + icon: ``, + href: '/options' + }); }); } } \ No newline at end of file diff --git a/web/lib/pages/home/index.html b/web/lib/pages/home/index.html index 42eb954..10dd58b 100644 --- a/web/lib/pages/home/index.html +++ b/web/lib/pages/home/index.html @@ -28,4 +28,12 @@
+ +
diff --git a/web/lib/pages/home/script.js b/web/lib/pages/home/script.js index a3aae0a..814fd3f 100644 --- a/web/lib/pages/home/script.js +++ b/web/lib/pages/home/script.js @@ -9,6 +9,8 @@ const Home = { Home.group.house.setHTML(); Home.group.homestead.setHTML(); + + Home.joint.homestead.setHTML(); } }, personal: { @@ -119,6 +121,34 @@ const Home = { } } }, + joint: { + homestead: { + list: [], + loadAPI: async () => { + const uuid = localStorage.getItem("uuid"); + const URL = `${CONFIG.api}homestead/list?mode=joint`; + const res = await fetch(URL, { + headers: { + "Content-Type": "application/json", + "Authorization": uuid + } + }); + Home.joint.homestead.list = await res.json(); + return Home.joint.homestead.list; + }, + setHTML: async () => { + const list = Home.joint.homestead.list.length > 0 + ? Home.joint.homestead.list + : await Home.joint.homestead.loadAPI(); + + if (USER.possibilities.can_view_territory && list.length) + document.getElementById('details-joint-territory').style.display = ""; + + list.sort((a, b) => b.id - a.id); + Home.renderCards(list, "homestead", "joint"); + } + } + }, renderCards: (list, type, block) => { const container = document.getElementById(`home-${block}-territory-list`); const fragment = document.createDocumentFragment(); @@ -126,7 +156,7 @@ const Home = { for (const el of list) { const card = document.createElement('app-territory-card'); card.image = `${CONFIG.web}cards/${type}/${type === "house" ? "T" : "H"}${el.id}.webp`; - card.address = `${el.title} ${el.number})`; + card.address = `${el.title} ${el.number}`; card.link = `/territory/card/${type}/${el.id}`; fragment.appendChild(card); } diff --git a/web/lib/pages/home/style.css b/web/lib/pages/home/style.css index 77c68d1..16eb007 100644 --- a/web/lib/pages/home/style.css +++ b/web/lib/pages/home/style.css @@ -91,8 +91,9 @@ .page-home #home-personal-territory-list, -.page-home #home-group-territory-list { - width: 100%; +.page-home #home-group-territory-list, +.page-home #home-joint-territory-list { + width: calc(100% - 20px); margin: 0; display: flex; flex-wrap: wrap; diff --git a/web/lib/pages/schedule/constructor/index.html b/web/lib/pages/schedule/constructor/index.html index 615ba76..b13f0a1 100644 --- a/web/lib/pages/schedule/constructor/index.html +++ b/web/lib/pages/schedule/constructor/index.html @@ -1,3 +1,3 @@
-
+
\ No newline at end of file diff --git a/web/lib/pages/schedule/constructor/script.js b/web/lib/pages/schedule/constructor/script.js index de36022..98f3bd0 100644 --- a/web/lib/pages/schedule/constructor/script.js +++ b/web/lib/pages/schedule/constructor/script.js @@ -3,6 +3,12 @@ const Schedule_constructor = { let html = await fetch('/lib/pages/schedule/constructor/index.html').then((response) => response.text()); app.innerHTML = html; - + const newItem = document.createElement('nav-item'); + const uniqueId = `nav-dynamic-${Date.now()}`; + newItem.setAttribute('id', uniqueId); + newItem.setAttribute('title', 'Динамічний Пункт'); + newItem.setAttribute('icon', '/img/0.svg'); + newItem.setAttribute('click', `alert('${uniqueId} clicked!')`); + document.querySelector('navigation-container').appendChild(newItem); } } \ No newline at end of file diff --git a/web/lib/pages/sheeps/index.html b/web/lib/pages/sheeps/index.html index 080891e..54abf32 100644 --- a/web/lib/pages/sheeps/index.html +++ b/web/lib/pages/sheeps/index.html @@ -121,7 +121,7 @@ id="sheep-editor-can_view_sheeps" type="checkbox" /> - +
- + +
+
+ +
@@ -151,7 +160,18 @@ type="checkbox" /> +
+
+ +
@@ -161,7 +181,7 @@ id="sheep-editor-can_add_stand" type="checkbox" /> - +
- +
- +
@@ -194,7 +214,7 @@ id="sheep-editor-can_view_schedule" type="checkbox" /> - +
- +
diff --git a/web/lib/pages/sheeps/script.js b/web/lib/pages/sheeps/script.js index c6b3bd1..ceb58e7 100644 --- a/web/lib/pages/sheeps/script.js +++ b/web/lib/pages/sheeps/script.js @@ -40,6 +40,8 @@ const SheepsEvents = { const sheepEditorButton = document.getElementById('sheep-editor-button'); const form = event.target; const formData = new FormData(form); + console.log(formData); + const uuidValue = form.elements["uuid"].value; const sheep = Sheeps.sheeps_list.list.find(item => item.uuid === uuidValue); @@ -50,7 +52,7 @@ const SheepsEvents = { sheep.name = form.elements["name"].value; sheep.group_id = Number(formData.get("group_id")); - sheep.mode = formData.get("mode"); + sheep.mode = formData.get("mode") || sheep.mode; sheep.mode_title = ["Користувач", "Модератор", "Адміністратор"][sheep.mode] || "Користувач"; const permKeys = [ @@ -59,8 +61,10 @@ const SheepsEvents = { "can_view_stand", "can_view_territory", "can_add_sheeps", + "can_manager_sheeps", "can_add_territory", "can_manager_territory", + "can_joint_territory", "can_add_stand", "can_manager_stand", "can_add_schedule" @@ -73,8 +77,7 @@ const SheepsEvents = { try { const uuid = localStorage.getItem('uuid'); - const URL = `${CONFIG.api}sheep`; - const response = await fetch(URL, { + const response = await fetch(`${CONFIG.api}sheep`, { method: 'PUT', headers: { "Content-Type": "application/json", @@ -84,26 +87,23 @@ const SheepsEvents = { }); if (response.ok) { - sheepEditorButton.innerText = "Успішно збережено"; + sheepEditorButton.innerText = "Зберегти"; + Notifier.success("Успішно збережено!", {timeout: 2000}) const data = await response.json(); console.log(data); Sheeps.sheeps_list.list = []; await Sheeps.sheeps_list.setHTML(); - - setTimeout(() => { - sheepEditorButton.innerText = "Зберегти"; - }, 3000); } else { + sheepEditorButton.innerText = "Зберегти"; console.error('Помилка збереження'); - sheepEditorButton.innerText = "Помилка збереження"; + Notifier.error("Помилка збереження!", {timeout: 3000}); } } catch (err) { console.error(err); - sheepEditorButton.innerText = "Помилка збереження"; + Notifier.error("Помилка збереження!", {timeout: 3000}); } - // тот же код, что был в _onSheepEditorSubmit, но обращаемся к editorForm return; } @@ -120,8 +120,7 @@ const SheepsEvents = { try { const uuid = localStorage.getItem('uuid'); - const URL = `${CONFIG.api}sheep`; - const response = await fetch(URL, { + const response = await fetch(`${CONFIG.api}sheep`, { method: 'POST', headers: { "Content-Type": "application/json", @@ -131,7 +130,7 @@ const SheepsEvents = { }); if (response.ok) { - sheepAddedsButton.innerText = "Вісника додано"; + sheepAddedsButton.innerText = "Додати"; const data = await response.json(); console.log(data); @@ -141,17 +140,14 @@ const SheepsEvents = { Sheeps.addeds.close(); await Sheeps.editor.setHTML(data.id, randomNumber); - - setTimeout(() => { - sheepAddedsButton.innerText = "Додати"; - }, 3000); } else { + sheepEditorButton.innerText = "Додати"; console.error('Помилка додавання'); - sheepAddedsButton.innerText = "Помилка додавання"; + Notifier.error("Помилка додавання!", {timeout: 3000}); } } catch (err) { console.error(err); - sheepAddedsButton.innerText = "Помилка додавання"; + Notifier.error("Помилка додавання!", {timeout: 3000}); } return; } @@ -176,9 +172,7 @@ const Sheeps = { loadAPI: async () => { let uuid = localStorage.getItem("uuid"); - const URL = `${CONFIG.api}sheeps/list`; - - Sheeps.sheeps_list.list = await fetch(URL, { + Sheeps.sheeps_list.list = await fetch(`${CONFIG.api}sheeps/list`, { method: 'GET', headers: { "Content-Type": "application/json", @@ -238,9 +232,11 @@ const Sheeps = { if (p.can_view_territory) perms.push("View Territory"); if (p.can_add_sheeps) perms.push("Create Sheeps"); if (p.can_add_territory) perms.push("Create Territory"); + if (p.can_manager_sheeps) perms.push("Manager Sheeps"); if (p.can_manager_territory) perms.push("Manager Territory"); - if (p.can_add_stand) perms.push("Create Stand"); if (p.can_manager_stand) perms.push("Manager Stand"); + if (p.can_joint_territory) perms.push("Joint Territory"); + if (p.can_add_stand) perms.push("Create Stand"); if (p.can_add_schedule) perms.push("Create Schedule"); return perms.map(p => `${p}`).join(''); }; @@ -274,8 +270,7 @@ const Sheeps = { loadAPI: async (id) => { let uuid = localStorage.getItem("uuid"); - const URL = `${CONFIG.api}sheep?id=${id}`; - return await fetch(URL, { + return await fetch(`${CONFIG.api}sheep?id=${id}`, { method: 'GET', headers: { "Content-Type": "application/json", @@ -327,8 +322,11 @@ const Sheeps = { let sheep_editor_can_view_sheeps = document.getElementById('sheep-editor-can_view_sheeps'); let sheep_editor_can_add_sheeps = document.getElementById('sheep-editor-can_add_sheeps'); + let sheep_editor_can_manager_sheeps = document.getElementById('sheep-editor-can_manager_sheeps'); let sheep_editor_can_add_territory = document.getElementById('sheep-editor-can_add_territory'); let sheep_editor_can_manager_territory = document.getElementById('sheep-editor-can_manager_territory'); + let sheep_editor_can_joint_territory = document.getElementById('sheep-editor-can_joint_territory'); + let sheep_editor_can_add_stand = document.getElementById('sheep-editor-can_add_stand'); let sheep_editor_can_manager_stand = document.getElementById('sheep-editor-can_manager_stand'); let sheep_editor_can_add_schedule = document.getElementById('sheep-editor-can_add_schedule'); @@ -367,8 +365,10 @@ const Sheeps = { sheep_editor_can_view_sheeps.checked = sheep.possibilities.can_view_sheeps; sheep_editor_can_add_sheeps.checked = sheep.possibilities.can_add_sheeps; + sheep_editor_can_manager_sheeps.checked = sheep.possibilities.can_manager_sheeps; sheep_editor_can_add_territory.checked = sheep.possibilities.can_add_territory; sheep_editor_can_manager_territory.checked = sheep.possibilities.can_manager_territory; + sheep_editor_can_joint_territory.checked = sheep.possibilities.can_joint_territory; sheep_editor_can_add_stand.checked = sheep.possibilities.can_add_stand; sheep_editor_can_manager_stand.checked = sheep.possibilities.can_manager_stand; sheep_editor_can_add_schedule.checked = sheep.possibilities.can_add_schedule; @@ -387,6 +387,9 @@ const Sheeps = { if (USER.mode == 2) { document.getElementById('sheep-editor-button').style.display = ""; sheep_editor_mode.disabled = false; + } else if (USER.possibilities.can_manager_sheeps) { + document.getElementById('sheep-editor-button').style.display = ""; + sheep_editor_mode.disabled = true; } else { sheep_editor_mode.disabled = true; } @@ -458,9 +461,9 @@ const Sheeps = { } }, territory: { - async loadAPI(URL) { + async loadAPI(url) { const uuid = localStorage.getItem("uuid"); - const res = await fetch(URL, { + const res = await fetch(url, { headers: { "Content-Type": "application/json", "Authorization": uuid @@ -470,8 +473,7 @@ const Sheeps = { }, async house(id) { - const URL = `${CONFIG.api}house/list?mode=admin&sheep_id=${id}`; - const list = await Sheeps.territory.loadAPI(URL); + const list = await Sheeps.territory.loadAPI(`${CONFIG.api}house/list?mode=admin&sheep_id=${id}`); if ((USER.possibilities.can_view_territory || USER.mode == 2) && list.length > 0) { document.getElementById('editor-blocks-territory').style.display = ""; @@ -480,8 +482,7 @@ const Sheeps = { }, async homestead(id) { - const URL = `${CONFIG.api}homestead/list?mode=admin&sheep_id=${id}`; - const list = await Sheeps.territory.loadAPI(URL); + const list = await Sheeps.territory.loadAPI(`${CONFIG.api}homestead/list?mode=admin&sheep_id=${id}`); if ((USER.possibilities.can_view_territory || USER.mode == 2) && list.length > 0) { document.getElementById('editor-blocks-territory').style.display = ""; diff --git a/web/lib/pages/stand/card/style.css b/web/lib/pages/stand/card/style.css index d9bec85..76aae1b 100644 --- a/web/lib/pages/stand/card/style.css +++ b/web/lib/pages/stand/card/style.css @@ -54,15 +54,15 @@ } #stand-info div span { - opacity: 0.8; - font-weight: 400; - font-size: var(--FontSize2); + font-weight: 500; + font-size: var(--FontSize3); margin-right: 5px; } #stand-info div p { - font-weight: 300; - font-size: var(--FontSize4); + opacity: 0.9; + font-weight: 400; + font-size: var(--FontSize3); } #stand-info img { @@ -143,11 +143,11 @@ } #stand-schedule>.block-day h3 { - font-size: var(--FontSize5); + font-size: var(--FontSize6); font-weight: 500; - padding: 10px; + padding: 20px; text-transform: capitalize; - width: calc(100% - 20px); + width: calc(100% - 40px); text-align: center; } @@ -181,6 +181,7 @@ #stand-schedule>.block-day div select:disabled { opacity: 0.9 !important; + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%2217%2010%2038%2050%22%3E%3Cpath%20fill%3D%22%23F2BD53%22%20d%3D%22M%2036%2010%20C%2028.28%2010%2022%2016.28%2022%2024%20L%2022%2028.587891%20C%2019.069798%2029.775473%2017%2032.643974%2017%2036%20L%2017%2052%20C%2017%2056.418%2020.582%2060%2025%2060%20L%2047%2060%20C%2051.418%2060%2055%2056.418%2055%2052%20L%2055%2036%20C%2055%2032.643974%2052.930202%2029.775473%2050%2028.587891%20L%2050%2024%20C%2050%2016.28%2043.72%2010%2036%2010%20z%20M%2036%2018%20C%2039.309%2018%2042%2020.691%2042%2024%20L%2042%2028%20L%2030%2028%20L%2030%2024%20C%2030%2020.691%2032.691%2018%2036%2018%20z%22%2F%3E%3C%2Fsvg%3E); } #stand-schedule>.block-day div:nth-child(2n) { diff --git a/web/lib/pages/stand/constructor/script.js b/web/lib/pages/stand/constructor/script.js index 44a5d8d..b757988 100644 --- a/web/lib/pages/stand/constructor/script.js +++ b/web/lib/pages/stand/constructor/script.js @@ -52,6 +52,9 @@ const Stand_constructor = { button.innerText = "Стенд додано"; Notifier.success('Стенд створено'); + Stand_list.list = []; + Stand_list.loadAPI(); + return response.json() } else { console.log('err'); diff --git a/web/lib/pages/stand/editor/script.js b/web/lib/pages/stand/editor/script.js index 1286a4f..7ebe71d 100644 --- a/web/lib/pages/stand/editor/script.js +++ b/web/lib/pages/stand/editor/script.js @@ -85,6 +85,9 @@ const Stand_editor = { button.innerText = "Стенд відредаговано"; Notifier.success('Стенд відредаговано'); + Stand_list.list = []; + Stand_list.loadAPI(); + return response.json() } else { console.log('err'); diff --git a/web/lib/pages/territory/card/index.html b/web/lib/pages/territory/card/index.html index d8d40ca..80157f8 100644 --- a/web/lib/pages/territory/card/index.html +++ b/web/lib/pages/territory/card/index.html @@ -1,206 +1,11 @@ - -
@@ -403,4 +210,4 @@
-
\ No newline at end of file + diff --git a/web/lib/pages/territory/card/script.js b/web/lib/pages/territory/card/script.js index 3905995..742f8bb 100644 --- a/web/lib/pages/territory/card/script.js +++ b/web/lib/pages/territory/card/script.js @@ -32,7 +32,6 @@ const Territory_card = { // Застосовуємо режим сортування this.sort(localStorage.getItem('territory_card_sort'), false); - this.getEntrances({ update: false }); } else if (type === "homestead") { this.getHomestead.map({}); } @@ -40,7 +39,7 @@ const Territory_card = { const ids = ['cloud_1', 'cloud_2', 'cloud_3']; ids.forEach((id, idx) => { const el = document.getElementById(id); - if(!el) return; + if (!el) return; el.setAttribute('data-state', ['sync', 'ok', 'err'].indexOf(Cloud.status) === idx ? 'active' : ''); }); @@ -134,10 +133,18 @@ const Territory_card = { if (navigator.onLine && Cloud.socket?.readyState === WebSocket.OPEN) { Cloud.socket.send(JSON.stringify(message)); } else { - if (confirm("З'єднання розірвано! Перепідключитись?")) { - Cloud.start(); - Territory_card.getEntrances({ update: true }); - } + Notifier.click({ + title: `Запис не додано!`, + text: `Натисніть, щоб перепідключитись!` + }, { + type: 'warn', + f: () => { + Cloud.reconnecting = true; + Cloud.reconnectAttempts = 0; + Cloud.start(); + }, + timeout: 0 + }); } }, @@ -181,9 +188,18 @@ const Territory_card = { if (navigator.onLine && Cloud.socket?.readyState === WebSocket.OPEN) { Cloud.socket.send(JSON.stringify(message)); } else { - if (confirm("З'єднання розірвано! Перепідключитись?")) { - Territory_card.cloud.start(); - } + Notifier.click({ + title: `Запис не додано!`, + text: `Натисніть, щоб перепідключитись!` + }, { + type: 'warn', + f: () => { + Cloud.reconnecting = true; + Cloud.reconnectAttempts = 0; + Cloud.start(); + }, + timeout: 0 + }); } } }, @@ -211,12 +227,14 @@ const Territory_card = { return; } + Territory_card.info(data); + const fragment = document.createDocumentFragment(); const canManage = USER.mode === 2 || (USER.mode === 1 && USER.possibilities.can_manager_territory); for (const element of data) { - const { id, entrance_number, title, history, working } = element; + const { id, entrance_number, title, history, working, address } = element; const isMy = ((history.name === "Групова" && history.group_id == USER.group_id) || history.name === USER.name); const show = (isMy && working) ? "open" : canManage ? "close" : null; @@ -310,8 +328,9 @@ const Territory_card = { div.style = style; div.innerHTML = ` + Квартира ${apt.title} +
- кв.${apt.title} @@ -350,9 +369,6 @@ const Territory_card = { async map({ homestead_id = Territory_card.id }) { let data = await this.loadAPI({ url: `${CONFIG.api}homestead/${homestead_id}` }); - console.log(data); - - let lat = data.geo?.lat ?? data.points?.[0]?.[0]?.[0]?.lat ?? 49.5629016; let lng = data.geo?.lng ?? data.points?.[0]?.[0]?.[0]?.lng ?? 25.6145625; let zoom = 15; @@ -483,10 +499,116 @@ const Territory_card = { Territory_card.getHomestead.markers[element.id] = marker; // сохраним ссылку на маркер } + + if((USER.possibilities.can_joint_territory && data.history.sheep_id == USER.id) || USER.mode == 2){ + this.joint.setHTML(homestead_id); + } }, + + joint: { + async setHTML(homestead_id){ + let lest = await this.getJoint(homestead_id); + + let block_info = document.getElementById('page-card-info'); + + block_info.style.display = "flex"; + block_info.innerHTML = ` +

Надати спільний доступ:

+ + ${Sheeps.sheeps_list.list.map(p => { + const isSelected = lest.some(item => item.sheep_id === p.id); + if(USER.id === Number(p.id) && USER.mode != 2) return + return `
+ ${p.name} +
`; + }).join('')} +
+ `; + }, + + setJoint(homestead_id){ + const select = document.getElementById(`joint-${homestead_id}`); + if (!select) return; + console.log(select.getClick); + + if(select.getClick.state == "add"){ + this.addSheep(homestead_id, select.getClick.value); + } else if(select.getClick.state == "delete"){ + this.delSheep(homestead_id, select.getClick.value); + } + }, + + async getJoint(homestead_id){ + let uuid = localStorage.getItem("uuid"); + + return await fetch(`${CONFIG.api}homestead/joint/${homestead_id}`, { + method: 'GET', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + } + }).then((response) => response.json()); + }, + + async addSheep(homestead_id, sheep_id){ + const uuid = localStorage.getItem('uuid'); + + if (!homestead_id) { + console.warn("Невірні дані для наданя доступу."); + return; + } + + try { + const response = await fetch(`${CONFIG.api}homestead/joint/${homestead_id}`, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({"sheep_id": sheep_id}) + }); + + if (!response.ok) throw new Error("Failed to assign"); + + Notifier.success('Віснику успішно надано доступ.'); + } catch (err) { + console.error('❌ Error:', err); + Notifier.error('Помилка надання доступу.'); + } + }, + async delSheep(homestead_id, sheep_id){ + const uuid = localStorage.getItem('uuid'); + + if (!homestead_id) { + console.warn("Невірні дані для відкликання доступу."); + return; + } + + try { + const response = await fetch(`${CONFIG.api}homestead/joint/${homestead_id}`, { + method: 'DELETE', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({"sheep_id": sheep_id}) + }); + + if (!response.ok) throw new Error("Failed to assign"); + + Notifier.success('Доступ успішно відкликанно.'); + } catch (err) { + console.error('❌ Error:', err); + Notifier.error('Помилка при відкликанні доступу.'); + } + } + } }, - async reload(){ + async reload() { Territory_card.getEntrances({ update: true }); }, @@ -547,5 +669,39 @@ const Territory_card = { } this.close(); } + }, + + info(data) { + let block_info = document.getElementById('page-card-info'); + + block_info.style.display = "flex"; + block_info.innerHTML = ` +
${data[0].address.title} ${data[0].address.number} +
+

Терміни опрацювання:

+ ` + + for (let index = 0; index < data.length; index++) { + const element = data[index]; + + const canManage = USER.mode === 2 || (USER.mode === 1 && USER.possibilities.can_manager_territory); + const isMy = ((element.history.name === "Групова" && element.history.group_id == USER.group_id) || element.history.name === USER.name); + + let date_start = element.history.date.start; + let date_end = date_start + (1000 * 2629743 * 4); + let red = () => { + if(Date.now() > date_end) return `color: #ec2d2d;` + return + } + + if (element.working && (isMy || canManage)) { + block_info.innerHTML += ` +
+

${element.title}

+

${formattedDate(date_start)} — ${formattedDate(date_end)}

+
+ `; + } + } } } \ No newline at end of file diff --git a/web/lib/pages/territory/card/style.css b/web/lib/pages/territory/card/style.css index ddacce2..bb1c115 100644 --- a/web/lib/pages/territory/card/style.css +++ b/web/lib/pages/territory/card/style.css @@ -88,6 +88,48 @@ } +#page-card-info { + padding: 10px; + margin: 0px 0 10px 0; + background: var(--ColorThemes1); + color: var(--ColorThemes3); + border: 1px solid var(--ColorThemes2); + box-shadow: var(--shadow-l1); + border-radius: var(--border-radius); + /* overflow: auto; */ + display: flex; + justify-content: space-between; + flex-direction: column; + gap: 10px; +} + +#page-card-info>a { + font-size: var(--FontSize5); + font-weight: 500; +} + +#page-card-info>h2 { + font-size: var(--FontSize4); +} + +#page-card-info>div { + display: flex; + align-items: flex-end; + gap: 10px; +} + +#page-card-info>div>h3 { + font-size: var(--FontSize3); + font-weight: 500; + opacity: 0.9; +} + +#page-card-info>div>h4 { + font-size: var(--FontSize2); + font-weight: 400; + opacity: 0.8; +} + #card-new-date { display: flex; width: 100%; @@ -149,4 +191,221 @@ font-weight: 400; -webkit-appearance: none; -moz-appearance: none; +} + +.page-card *[disabled] { + opacity: 0.5; +} + +.page-card #list { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.page-card details { + color: var(--ColorThemes3); + width: 100%; + min-width: 320px; + background: var(--ColorThemes1); + border-radius: var(--border-radius); + overflow: hidden; + border: 1px solid var(--ColorThemes2); + box-shadow: var(--shadow-l1); +} + +@media (min-width: 900px) { + .page-card #list { + align-items: flex-start; + justify-content: space-between; + flex-direction: row; + flex-wrap: wrap; + } + + .page-card details { + width: calc(50% - 10px); + } +} + +.page-card details[disabled] .page-card details summary, +.page-card details.disabled .page-card details summary { + pointer-events: none; + user-select: none; +} + +.page-card details .page-card details summary::-webkit-.page-card details-marker, +.page-card details .page-card details summary::marker { + display: none; + content: ""; +} + +.page-card details summary { + cursor: pointer; + height: 45px; + font-weight: 500; + display: flex; + align-items: center; + justify-content: space-between; +} + +.page-card details summary p { + padding: 0 10px; + font-size: var(--FontSize3); + font-weight: 500; +} + +.page-card details summary svg { + width: 25px; + height: 25px; + padding: 0 10px; + fill: currentColor; +} + +.page-card .apartments_list { + padding: 10px; + gap: 15px; + display: flex; + flex-direction: column; +} + +.page-card .apartments_list>p { + font-size: var(--FontSize5); + text-align: center; + color: var(--ColorThemes3); + padding: 10px; +} + +.page-card .card_info { + display: flex; + font-size: var(--FontSize3); + border-radius: 8px; + flex-direction: column; + align-items: stretch; + border: 1px solid var(--ColorThemes3); + background-color: var(--ColorThemes2); + gap: 6px; + padding: 6px; +} + +.card_info_homestead { + border-radius: 6px !important; + width: calc(100% - 15px); + margin: 0; +} + +.page-card .card_info>span { + font-size: var(--FontSize3); + font-weight: 500; + position: relative; +} + +.page-card .card_info>hr { + height: 1px; + background-color: currentColor; + opacity: 0.1; + margin-bottom: 4px; +} + +.page-card .card_info>.info { + display: flex; + font-size: var(--FontSize3); + align-items: center; + justify-content: space-between; + border-radius: 8px; + gap: 5px; +} + +.page-card .card_info>.info>select { + color: #3d3d3d; + border-radius: 6px; + border: 1px solid #eaebef; + background-color: var(--ColorThemes3); + min-width: 110px; + width: 50%; + padding: 4px; + height: 30px; +} + +.page-card .card_info>.info>input, +.page-card .card_info>.info>button { + font-size: var(--FontSize2); + font-weight: 400; + -webkit-appearance: none; + -moz-appearance: none; + border-radius: 6px; + background-color: var(--ColorThemes0); + border: 1px solid var(--ColorThemes1); + color: var(--ColorThemes3); + width: 50%; + min-width: 70px; + padding: 0 4px; + height: 30px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.page-card .card_info>.info>button>svg { + width: 15px; + height: 15px; + fill: var(--ColorThemes3); +} + +.page-card .card_info>textarea { + border-radius: 6px; + font-size: var(--FontSize3); + background-color: var(--ColorThemes0); + border: 0; + color: var(--ColorThemes3); + width: calc(100% - 10px); + min-width: 70px; + padding: 5px; + min-height: 40px; + appearance: none; + resize: vertical; + -webkit-appearance: none; +} + +.page-card .card_info>textarea::placeholder { + color: var(--ColorThemes3); + opacity: 0.5; +} + +.page-card .card_info>textarea::-webkit-input-placeholder { + color: var(--ColorThemes3); + opacity: 0.5; +} + +.page-card #map_card { + display: none; + width: 100%; + height: calc(100vh - 100px); + background: var(--ColorThemes1); + color: var(--ColorThemes3); + border: 1px solid var(--ColorThemes2); + box-shadow: var(--shadow-l1); + border-radius: var(--border-radius); + margin-bottom: 15px; +} + + +smart-select { + --smart-select-option-color: var(--ColorThemes3); + --smart-select-border-color: var(--ColorThemes2); + --smart-select-background: var(--ColorThemes0); + --smart-select-color: var(--PrimaryColor); + --smart-select-chip-fill: var(--PrimaryColorText); + --smart-select-chip-background: var(--PrimaryColor); + --smart-select-chip-color: var(--ColorThemes3); + --smart-select-search-color: var(--ColorThemes3); + --smart-select-hover-background: var(--ColorThemes2); + --smart-select-hover-color: var(--ColorThemes3); + --smart-select-selected-background: var(--PrimaryColor); + --smart-select-selected-color: var(--PrimaryColorText); + --smart-select-font-size-1: var(--FontSize1); + --smart-select-font-size-2: var(--FontSize3); + --smart-select-border-radius-1: 8px; + --smart-select-border-radius-2: 4px; } \ No newline at end of file diff --git a/web/lib/pages/territory/editor/index.html b/web/lib/pages/territory/editor/index.html index 3317989..c54f48f 100644 --- a/web/lib/pages/territory/editor/index.html +++ b/web/lib/pages/territory/editor/index.html @@ -13,12 +13,20 @@ name="address" required value="" + onchange="Territory_editor.info.title=this.value" />
- +
@@ -29,6 +37,7 @@ name="settlement" required value="Тернопіль" + onchange="Territory_editor.info.settlement=this.value" />
@@ -66,14 +75,18 @@ або - +
- +