From 413e7ce4cf8265a7f4809e0f2f1433422967b7c6 Mon Sep 17 00:00:00 2001 From: cpu Date: Thu, 8 May 2025 15:36:17 +0200 Subject: [PATCH] PWA fixed added systemd service howto traefik nginix set_real_ip_from improved readme visuals fixed on mobile labels removed updated readme fixed visuals overlay for the hotkey disable screen lock clean up git precommit hooks clean up clean up update check for update feature added build-time information fixed date clean up added hook script fix fix fix hooks fixed webhook setup players stay in run all timers mode mqtt mqtt allways connected mqtt messages work capturing mqtt in edit player mqtt in Setup updated readme --- .dockerignore | 40 + .gitignore | 146 ++ Dockerfile | 17 + README.md | 146 +- docker/traefik.labels | 8 + docs/architecture.md | 62 + docs/deployment.md | 169 ++ docs/development.md | 28 + docs/remote-control.md | 78 + hooks/redeploy.sh | 44 + hooks/webhook.conf | 38 + index.html | 30 + nginx.conf | 47 + package-lock.json | 2732 +++++++++++++++++++++ package.json | 25 + postcss.config.js | 6 + public/favicon.ico | Bin 0 -> 15406 bytes public/icons/icon-192x192.png | Bin 0 -> 6035 bytes public/icons/icon-512x512.png | Bin 0 -> 17887 bytes public/icons/maskable-icon-192x192.png | Bin 0 -> 6035 bytes public/icons/maskable-icon-512x512.png | Bin 0 -> 17887 bytes public/icons/shortcut-info-96x96.png | Bin 0 -> 2143 bytes public/icons/shortcut-setup-96x96.png | Bin 0 -> 4956 bytes public/manifest.json | 55 + src/App.vue | 291 +++ src/assets/default-avatar.png | Bin 0 -> 458 bytes src/assets/tailwind.css | 54 + src/components/DefaultAvatarIcon.vue | 9 + src/components/HotkeyCaptureOverlay.vue | 79 + src/components/MqttCharCaptureOverlay.vue | 65 + src/components/PlayerDisplay.vue | 145 ++ src/components/PlayerForm.vue | 260 ++ src/components/PlayerListItem.vue | 80 + src/components/TimerDisplay.vue | 27 + src/main.js | 26 + src/router/index.js | 59 + src/services/AudioService.js | 133 + src/services/CameraService.js | 69 + src/services/MqttService.js | 146 ++ src/services/StorageService.js | 23 + src/services/WakeLockService.js | 65 + src/store/index.js | 495 ++++ src/sw.js | 150 ++ src/utils/timeFormatter.js | 32 + src/views/GameView.vue | 263 ++ src/views/InfoView.vue | 127 + src/views/SetupView.vue | 362 +++ systemd/virt-nexus-timer.service | 28 + systemd/webhook.service | 35 + tailwind.config.js | 29 + vite.config.js | 61 + 51 files changed, 6782 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 docker/traefik.labels create mode 100644 docs/architecture.md create mode 100644 docs/deployment.md create mode 100644 docs/development.md create mode 100644 docs/remote-control.md create mode 100755 hooks/redeploy.sh create mode 100644 hooks/webhook.conf create mode 100644 index.html create mode 100644 nginx.conf create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/favicon.ico create mode 100644 public/icons/icon-192x192.png create mode 100644 public/icons/icon-512x512.png create mode 100644 public/icons/maskable-icon-192x192.png create mode 100644 public/icons/maskable-icon-512x512.png create mode 100644 public/icons/shortcut-info-96x96.png create mode 100644 public/icons/shortcut-setup-96x96.png create mode 100644 public/manifest.json create mode 100644 src/App.vue create mode 100644 src/assets/default-avatar.png create mode 100644 src/assets/tailwind.css create mode 100644 src/components/DefaultAvatarIcon.vue create mode 100644 src/components/HotkeyCaptureOverlay.vue create mode 100644 src/components/MqttCharCaptureOverlay.vue create mode 100644 src/components/PlayerDisplay.vue create mode 100644 src/components/PlayerForm.vue create mode 100644 src/components/PlayerListItem.vue create mode 100644 src/components/TimerDisplay.vue create mode 100644 src/main.js create mode 100644 src/router/index.js create mode 100644 src/services/AudioService.js create mode 100644 src/services/CameraService.js create mode 100644 src/services/MqttService.js create mode 100644 src/services/StorageService.js create mode 100644 src/services/WakeLockService.js create mode 100644 src/store/index.js create mode 100644 src/sw.js create mode 100644 src/utils/timeFormatter.js create mode 100644 src/views/GameView.vue create mode 100644 src/views/InfoView.vue create mode 100644 src/views/SetupView.vue create mode 100644 systemd/virt-nexus-timer.service create mode 100644 systemd/webhook.service create mode 100644 tailwind.config.js create mode 100644 vite.config.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6aa9de4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,40 @@ +# Git files +.git +.gitignore + +# Node modules - these are installed within the Docker build context +node_modules + +# Docker specific files (if any, other than Dockerfile itself) +# .dockerignore (to avoid including itself if context changes) +docker +systemd + +# Local development environment files +.env +.env*.local + +# Logs and temporary files +logs +*.log +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* + +# OS-specific files +.DS_Store +Thumbs.db + +# IDE configuration +.idea/ +.vscode/ +*.sublime-workspace +*.sublime-project + +# Build output (if you ever build locally before Docker) +dist/ +# If your build output is different, change the line above + +# Coverage reports +coverage/ +.nyc_output/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b4db6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,146 @@ +# docker labels +labels +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarnclean + +# dotenv environment variables file +.env +.env*.local +.env.development.local +.env.test.local +.env.production.local + +# parcel-bundler cache files +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build output +.nuxt +dist + +# Svelte Sapper build output +__sapper__ + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Vite local development server cache +.vite + +# Vite build output directory +/dist +# If your build output is different, change the line above to /your-build-output-dir + +# Mac OS system files +.DS_Store +Thumbs.db + +# IDE specific +.idea/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sublime-workspace +*.sublime-project + +# Editor directories and files +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36b0b91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Stage 1: Build the Vue.js application +FROM node:24-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Stage 2: Serve the application with Nginx +FROM nginx:stable-alpine +COPY ./nginx.conf /etc/nginx/conf.d/default.conf + +RUN find /usr/share/nginx/html -mindepth 1 -delete +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README.md b/README.md index ff454c2..c352420 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,145 @@ -# OrreryTimer +# Nexus Timer 🕰️✨ -OrreryTimer is a dynamic multi-player timer designed for games, workshops, or any activity where turns pass sequentially in a circular fashion. It provides a clear visual focus on the current participant, their immediate predecessor and successor, and the direction of play, ensuring everyone stays engaged and aware of the flow. \ No newline at end of file +Nexus Timer is a dynamic multi-player timer designed for games, workshops, or any activity where turns pass sequentially. It focuses on the current participant and their immediate successor, ensuring everyone stays engaged and aware of who's next. + +## Table of Contents + +1. [Core Concept](#core-concept) +2. [Target Audience](#target-audience) +3. [Key Features at a Glance](#key-features-at-a-glance) +4. [Getting Started (User Guide)](#getting-started-user-guide) +5. [Remote Control Options](#remote-control-options) +6. [Development, Deployment, and Architecture](#development-deployment-and-architecture) + +## Core Concept + +Nexus Timer visualizes players in a circular sequence. The **Current Player** is prominently displayed in the top half of the screen, and the **Next Player** is in the bottom half. This clear visual pairing indicates the flow of turns, perfect for board games, RPGs, timed presentations, or any scenario needing structured turn management with individual countdowns. + +## Target Audience + +Game enthusiasts who play turn-based games (board games, tabletop RPGs, card games) and need a visually clear and customizable timer solution. + +## Key Features at a Glance + +* **Dynamic Player Display:** Clear focus on Current & Next player (Normal Mode) or a list of active timers (All Timers Mode). +* **Individual Timers:** Customizable (MM:SS), supports negative time, auto-skips players at max negative time. +* **Two Game Modes:** + * **Normal Mode:** Only the current player's timer runs. + * **All Timers Mode:** All active players' timers run simultaneously. +* **Flexible Player Setup:** Add, edit, delete, and reorder up to 99 players. Use camera for avatars or defaults. +* **Intuitive Controls:** Swipe up to pass turns, tap to pause/resume. +* **Customizable Triggers:** + * **Keyboard Hotkeys:** Assign unique keys for player turns and global actions (Stop/Pause All, Run All Timers). *This will allow the connected Bluetooth HID device to act like a remote buttons. See [HID Smart Buttons](docs/remote-control.md#hid-smart-buttons)* + * **MQTT Remote Triggers:** Assign unique characters for remote activation of player turns and global actions via an MQTT broker. *This will allow the players use their own smartphone to remote control their timers. See [Android Shortcut Setup](docs/remote-control.md#android-shortcut-setup)* +* **PWA Ready:** Installable on desktop and mobile, offline capable, with update notifications and screen wake lock. +* **Audio & Visual Cues:** Ticking sounds, alerts, light/dark themes, and clear timer states. +* **Persistent Settings:** Your setup and game state are saved locally. + +## Getting Started (User Guide) + +This guide walks you through setting up and using Nexus Timer. + +### 1. Installation (PWA) + +Nexus Timer is a Progressive Web App (PWA), meaning you can install it on your device for a native-like experience. +* **Mobile:** Open Nexus Timer in a compatible browser (e.g., Chrome on Android, Safari on iOS). Look for an "Add to Home Screen" or "Install App" option in the browser menu. +* **Desktop:** In browsers like Chrome or Edge, look for an install icon in the address bar or an "Install App" option in the browser menu. + +### 2. Initial Setup (First Use) + +When you first open Nexus Timer (or after a full reset), it starts with two predefined players to get you going quickly. +* If you have at least two players defined (either predefined or previously saved), the app will **automatically start in Normal Mode**. +* If no players are defined, you'll be taken to the **Setup Screen**. + +### 3. The Setup Screen + +This is where you configure your game: + +* **Players Section:** + * **View Players:** Shows the current number of players (e.g., "Players (3)"). + * **Add Player:** Click to add a new player (up to 99). You can set their: + * **Name** + * **Avatar:** Use your device camera or a default icon. + * **Remaining Time:** Set their starting timer (MM:SS). This can also be negative (e.g., -02:30). + * **"Pass Turn / My Pause" Hotkey:** Click the display area (shows "-") and press a single keyboard key. This key will be used by this player to pass their turn (Normal Mode) or pause their own timer (All Timers Mode). + * **"Pass Turn / My Pause" MQTT Character:** Click the display area (shows "-") and enter a single character. This character, when received via MQTT, will trigger the same action as the player's hotkey. (See [Remote Control Options](#remote-control-options)). + * **Edit Player:** Click the pencil icon next to a player to modify their details. + * **Delete Player:** Click the trash icon to remove a player. + * **Reorder Players:** Use the up/down arrow icons to change player turn order. + * **Shuffle/Reverse Order:** Buttons to quickly randomize or reverse the player list. +* **MQTT Broker (for Remote Control):** + * **URL:** Enter the WebSocket URL of your MQTT broker (e.g., `ws://your-broker-ip:9001`). + * **Connect/Disconnect:** Toggle the connection to your broker. The status (Connected, Disconnected, Error) will be displayed. +* **Global Triggers (Hotkeys & MQTT):** + * Assign a unique keyboard **Hotkey** and/or a unique **MQTT Character** for: + * **Global "Stop/Pause All":** Pauses the current player's timer (Normal Mode) or all running timers (All Timers Mode). If all are paused in All Timers Mode, this resumes them. + * **Global "Run All Timers":** Switches from Normal Mode to All Timers Mode and starts all active player timers. + * Click the display area (shows "-") next to "Hotkey" or "MQTT" to set the trigger. +* **Other Settings:** + * **Dark Mode:** Toggle between light and dark themes. + * **Mute Audio:** Turn all app sounds on or off. +* **Action Buttons:** + * **Save & Close:** Saves your current setup and starts the game (if at least 2 players are configured), taking you to the Game View. + * **Reset Player Timers:** Resets all current players' timers back to their initially set values and pauses the game. Player list and other settings remain. + * **Reset Entire App Data:** Clears *all* data (players, settings, timer states) and resets the app to its default state (with 2 predefined players). + +### 4. Game View: Playing the Game + +#### Normal Mode (Default) + +* **Display:** Shows the **Current Player** (top half) and **Next Player** (bottom half). +* **Timer:** Only the Current Player's timer runs. + * Timers count down. If they reach 00:00, they continue into **negative time** (e.g., -00:01, -00:02...). + * If a player reaches the maximum negative time (default -59:59), they are **skipped** automatically until their timer is edited or reset. Skipped players are visually distinct. +* **Passing the Turn:** + 1. **Swipe Up** on the Next Player's area (bottom half). + 2. Or, the Current Player presses their assigned "Pass Turn / My Pause" **Hotkey** or sends their **MQTT Character**. + * *Behavior:* The current player's timer pauses, the next non-skipped player becomes "Current," and their timer starts. + * *Sound:* A 3-second ticking sound alerts players when a new turn starts. +* **Pausing/Resuming the Current Player's Timer (without passing):** + 1. **Tap** on the Current Player's area (top half). + 2. Or, use the "Global Stop/Pause All" **Hotkey** or **MQTT Character**. + * *Behavior:* If the current player's timer was paused when the turn was passed to them, it will remain paused. They need to tap their area or use a global resume trigger to start it. +* **Switching to All Timers Mode:** Click the "Run All Timers" button in the header. + +#### All Timers Mode + +* **Display:** Shows a list of all non-skipped players with their timers. +* **Timers:** All active (non-skipped) players' timers run simultaneously. + * A continuous ticking sound plays while any timer is active in this mode. +* **Entering this Mode:** + * Click "Run All Timers" in the Game View header (when in Normal Mode). + * Or, use the "Global Run All Timers" **Hotkey** or **MQTT Character**. + * All non-skipped players' timers will start. +* **Pausing/Resuming Individual Timers:** + * **Tap** on a player in the list to pause/resume their specific timer. + * A player can press their own "Pass Turn / My Pause" **Hotkey** or send their **MQTT Character** to pause their *own* timer. +* **Visuals:** + * Players with running timers are clearly indicated (e.g., green/red border, pulsing background). + * Players with paused timers are visually distinct (e.g., dimmer, yellowish border) but remain in the list. +* **Global Pause/Resume:** + * The "Global Stop/Pause All" **Hotkey** or **MQTT Character** will: + * Pause all currently running timers if any are active. + * Resume all previously running (and still active, non-skipped) timers if all were paused. +* **Reverting to Normal Mode:** + * Click "Back to Normal Mode" in the Game View header. + * Automatically reverts if all player timers in this mode are paused (and at least one non-skipped player exists). + * All timers will be paused when switching back to Normal Mode. The player who was "Current" before switching to All Timers mode (or the first player if starting fresh) will be the new Current Player. + +### 5. Info Screen + +* Accessible via the "Info" icon (ℹ️) in the Game View header. +* Displays "About" information, key features, and the app's build time. +* Includes a "Check for Update" button to manually trigger a PWA update check. +* Provides a link to the source code. + +## Remote Control Options + +Nexus Timer supports remote control via physical HID buttons or MQTT messages. +For detailed setup instructions, please see the [Remote Control Options Guide](docs/remote-control.md). + +## Development, Deployment, and Architecture + +* For Developer Setup, see [Developer Setup Guide](docs/development.md). +* For Deployment Setup, see [Deployment Setup Guide](docs/deployment.md). +* For Architecture Docs, see [Architecture](docs/architecture.md). \ No newline at end of file diff --git a/docker/traefik.labels b/docker/traefik.labels new file mode 100644 index 0000000..6550fc8 --- /dev/null +++ b/docker/traefik.labels @@ -0,0 +1,8 @@ +traefik.enable=true +traefik.docker.network=traefik +traefik.http.routers.virt-nexus-timer.rule=Host("nexus-timer.virtonline.eu") +traefik.http.routers.virt-nexus-timer.service=virt-nexus-timer +traefik.http.routers.virt-nexus-timer.tls=true +traefik.http.routers.virt-nexus-timer.tls.certResolver=default +traefik.http.routers.virt-nexus-timer.entrypoints=web-secure +traefik.http.services.virt-nexus-timer.loadbalancer.server.port=80 \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..8ac2fa5 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,62 @@ +# Table of Contents +1. [UI/UX Considerations](#uiux-considerations) +2. [Tech Stack](#tech-stack) +3. [Data Model (For AI Generation)](#data-model-for-ai-generation) +4. [Build-time Information & Service Worker Versioning](#build-time-information--service-worker-versioning) + +## UI/UX Considerations +* **Minimalist Design:** Focus on clarity and ease of use. Avoid clutter. +* **Large, Clear Timers:** Timers should be easily readable at a glance. +* **Color Coding:** Use color to indicate timer state (e.g., green for running, red for negative time, grey for skipped). +* **Responsive Layout:** The UI should adapt to different (mobile phone) screen sizes. +* **Touch-Friendly:** Buttons and interactive elements should be large enough for easy tapping. + +## Tech Stack +* **HTML5:** For structuring the user interface. +* **CSS3:** For styling and visual presentation, including animations. Consider a CSS framework like Tailwind CSS for rapid prototyping. +* **JavaScript:** For application logic, timer functionality, and event handling. +* **MQTT.js** for MQTT communication +* **Web Audio API:** For audio feedback (ticking sounds, alerts). +* **Browser API:** For capturing Players' photo. +* **Screen Wake Lock API:** For preventing of the screen lock in a PWA. +* **Local Storage/IndexedDB:** For persistent storage of player data, timer states, and settings. +* **Service Worker:** Essential for PWA functionality (offline access, push notifications - potential future feature). +* **Manifest File:** Defines the PWA's metadata (name, icons, theme color). +* **Web Framework:** Vith Vite (Vue.js) +* **Tailwind CSS:** Utility-first CSS framework for rapid UI development. + +## Data Model (Conceptual for Setup) +```json +{ + "players": [ + { + "id": "1", "name": "Alice", "avatar": "image_data_or_default", + "initialTimerSec": 3600, "currentTimerSec": 3600, + "hotkey": "a", "mqttChar": "x", "isSkipped": false + } + ], + "globalHotkeyStopPause": "s", + "globalMqttStopPause": "p", + "globalHotkeyRunAll": "r", + "globalMqttRunAll": "t", + "mqttBrokerUrl": "ws://localhost:9001", + "currentPlayerIndex": 0, + "gameMode": "normal", + "isMuted": false, + "theme": "light" +} +``` + +## Build-time Information & Service Worker Versioning +The application incorporates build-time information and a mechanism for service worker updates: + +1. **Build Timestamp:** + * The build date and time are automatically injected into the application during the Vite build process. + * This is configured in `vite.config.js` using Vite's `define` feature, making `import.meta.env.VITE_APP_BUILD_TIME` available in the Vue components. + * The timestamp (formatted for the `sk-SK` locale) is displayed on the "About" screen (`src/views/InfoView.vue`). + +2. **Service Worker Cache Versioning:** + * The `CACHE_VERSION` constant within the service worker (`src/sw.js`) is also dynamically generated during the Vite build. + * `vite.config.js` uses the `define` feature to replace a placeholder (`__APP_CACHE_VERSION__`) in `src/sw.js` with a unique version string. This string typically incorporates the application's version from `package.json` and a build timestamp (`Date.now()`) to ensure uniqueness. + * The `src/sw.js` file is configured as a separate Rollup entry point in `vite.config.js` so that Vite processes it and performs this replacement, outputting the final `service-worker.js` to the `dist` directory root. + * When a new version of the app is deployed with a changed `service-worker.js` (due to this new `CACHE_VERSION`), the browser detects the difference. The updated service worker installs, and upon activation, it clears out old caches associated with previous versions. This mechanism is key to how the PWA updates and provides users with the latest assets. The "Check for Update" feature on the "About" screen manually triggers the browser to check for a new `service-worker.js` file. \ No newline at end of file diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..cd131cb --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,169 @@ +# Deployment Guide: Nexus Timer + +This guide outlines the steps to deploy and manage the Nexus Timer application on a server using Docker, Traefik (as a reverse proxy), and systemd, with an optional automated deployment via Gitea webhooks. + +## Table of Contents +1. [Initial Server Setup](#initial-server-setup) +2. [Exposing the App Behind Traefik](#exposing-the-app-behind-traefik-reverse-proxy) +3. [Automating Updates with Webhooks (Gitea)](#automating-updates-with-webhooks-gitea) +4. [Manual Updates (Fallback)](#manual-updates-fallback) +5. [Troubleshooting & Logs](#troubleshooting--logs) + +## Initial Server Setup + +### On the Server +Navigate to your preferred service directory on the server (e.g., `/virt`). +```bash +cd /virt +``` +Clone the repository (only the latest commit for faster cloning). +```bash +git clone --depth 1 https://gitea.virtonline.eu/2HoursProject/nexus-timer.git +cd nexus-timer +``` +If you will run the container on the Docker network `traefik` (or any other pre-existing network), find its IP subnet to allow Nginx inside the container to correctly identify the real client IP. +```bash +docker network inspect traefik --format '{{(index .IPAM.Config 0).Subnet}}' +``` +You'll get an output like `172.22.0.0/16`. + +Set this subnet in the `nginx.conf` file within your cloned repository before building the image. For example: +```bash +set_real_ip_from 172.22.0.0/16; +``` +Build the Docker image for the application. +```bash +docker build -t virt-nexus-timer . +``` +## Exposing the App Behind Traefik (Reverse Proxy) +This setup assumes you have Traefik running and configured to watch for Docker labels. + +### Review the provided docker labels and systemd service file + +The `docker/traefik.labels` file contains Docker labels that Traefik uses for routing and HTTPS. Review and adjust them if necessary. +Copy the example label file to its destination (one level up, to be read by systemd). +```bash +cp docker/traefik.labels labels +``` +View the example systemd service definition to understand how the Docker container will be managed. +```bash +cat systemd/virt-nexus-timer.service +``` +### Create the systemd service +Use `systemctl edit` to create or overwrite the service file. This is the recommended way to manage custom systemd units. +```bash +sudo systemctl edit --force --full virt-nexus-timer.service +``` +The editor will open. Paste the content from your `systemd/virt-nexus-timer.service` file into the editor, then save and exit (e.g., Ctrl+X, then Y, then Enter in nano). + +Enable the service to start on system boot and start it immediately. +```bash +sudo systemctl enable --now virt-nexus-timer.service +``` +Check the service status to ensure it's running correctly. +```bash +systemctl status virt-nexus-timer.service +``` +Look for "active (running)". + +### Test the web application +Verify that the application is accessible via HTTPS through Traefik: +```bash +curl https://nexus-timer.virtonline.eu +``` +Or open it in your browser: +[https://nexus-timer.virtonline.eu](https://nexus-timer.virtonline.eu) + +## Automating Updates with Webhooks (Gitea) +Instead of manually pulling, building, and restarting on the server, you can automate this process using a webhook. Gitea will notify a webhook service running on your server, which will then execute a deployment script. + +Install the `webhook` service +```bash +sudo install webhook +``` +Allow your Gitea instance to reach the webhook service on your server (e.g., `10.0.0.1:9000`). Ensure Gitea's `ALLOWED_HOST_LIST` in its `app.ini` includes this IP. +### The Redeployment Script +The `webhook` service will execute the script `hooks/redeploy.sh`. If webhook runs as a non-root user (recommended), that user will need passwordless sudo permission to restart the `virt-nexus-timer.service`. +You can grant this by editing the sudoers file: +```bash +sudo visudo +``` + and adding a line like: +```bash +webhooksvc ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart virt-nexus-timer.service +``` +Replace `webhooksvc` with the actual user webhook runs as. If webhook runs as root, this is not necessary but less secure overall. +### Configure the `webhook` Service +Create or edit the main webhook configuration file, typically at `/etc/webhook.conf`. Add the JSON object from `hooks/webhook.conf` to the array in the file (or create the file if it's new). +**Note:** Replace `YOUR_VERY_STRONG_SECRET_TOKEN_HERE_REPLACE_ME` with a strong, unique secret. +### Set Up webhook as a Systemd Service +Use `systemctl edit` to create or overwrite the service file. This is the recommended way to manage custom systemd units. +```bash +sudo systemctl edit --force --full webhook.service +``` +The editor will open. Paste the content from your `systemd/webhook.service` file into the editor, then save and exit (e.g., Ctrl+X, then Y, then Enter in nano). + +Enable, and start the webhook service: +```bash +sudo systemctl enable --now webhook.service +sudo systemctl status webhook.service +``` +### Configure Webhook in Gitea +1. Navigate to your Gitea repository: `https://gitea.virtonline.eu/2HoursProject/nexus-timer` +2. Go to `Settings -> Webhooks`. +3. Click Add Webhook and choose `Gitea`. +4. **Target URL**: `http://10.0.0.1:9000/hooks/redeploy-nexus-timer` +*(The redeploy-nexus-timer part must match the id in your /etc/webhook.conf)* +5. **HTTP Method**: POST +6. **POST Content Type**: application/json +7. **Secret**: Enter the exact same strong secret token you used in `/etc/webhook.conf` (e.g., `YOUR_VERY_STRONG_SECRET_TOKEN_HERE_REPLACE_ME`). +8. **Trigger On**: +Select `Push Events`. +You can further refine this to specific branches if your `webhook.conf` doesn't already filter by branch (though redundant filtering is fine). +9. Ensure `Enable this webhook` is checked. +10. Click `Add Webhook`. +### Test the Webhook +* In Gitea, on the Webhooks settings page for the webhook you just created, click `Test Delivery`. +* Check the Gitea UI for the response. It should show a `200 OK` and include the output from your `redeploy.sh` script. +* Push a small change to your `main` (or configured) branch to trigger a real deployment. +### Firewall Considerations +If your server has a firewall (e.g., `ufw`), ensure that port `9000` (or whichever port you configured for `webhook`) is allowed for incoming connections from your Gitea server's IP address or network. + +Example for `ufw` allowing any connection to `10.0.0.1:9000` (restrict source IP if possible): +```bash +sudo ufw allow to 10.0.0.1 port 9000 proto tcp comment 'Gitea Webhook' +``` +If Gitea is external, use its specific source IP instead of 'any' for better security. +```bash +sudo ufw allow from to 10.0.0.1 port 9000 proto tcp comment 'Gitea Webhook' +``` +Reload `ufw` if changes are made: +```bash +sudo ufw reload +``` + +## Manual Updates (Fallback) +Navigate to the application directory on your server. +```bash +cd /virt/nexus-timer +``` +Pull the latest changes from the repository, rebuild the Docker image, and restart the systemd service. +```bash +git pull && docker build -t virt-nexus-timer . && sudo systemctl restart virt-nexus-timer.service +``` +The previously installed Progressive Web App (PWA) should update automatically upon next launch or offer an upgrade prompt. + +## Troubleshooting & Logs +View real-time logs for the application service: +```bash +journalctl -fu virt-nexus-timer.service +``` +View real-time logs for the `webhook` service: +```bash +journalctl -fu webhook.service +``` +Check the custom log file for the redeployment script (if configured): +```bash +tail -f /var/log/webhook-redeploy-nexus-timer.log +``` +Check Gitea's webhook delivery logs for request/response details and errors. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..46f0719 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,28 @@ +# Table of Contents +1. [Developer Setup](#developer-setup) +2. [Modify the app](#modify-the-app) +3. [Test the PWA locally](#test-the-pwa-locally) +4. [Commit & Push](#commit--push) + +## Developer Setup +Clone the repository +```bash +git clone https://gitea.virtonline.eu/2HoursProject/nexus-timer.git +cd nexus-timer +``` +Run the live update server locally +```bash +npm run dev +``` +### Modify the app +Make code changes... +### Test the PWA locally +Open it in your browser: +[http://localhost:8080/](http://localhost:8080/) +### Commit & Push +Stage changes, commit and push +```bash +git add . +git commit -m 'My cool feature' +git push +``` \ No newline at end of file diff --git a/docs/remote-control.md b/docs/remote-control.md new file mode 100644 index 0000000..2d12c8f --- /dev/null +++ b/docs/remote-control.md @@ -0,0 +1,78 @@ +# Remote Control Options + +## Table of Contents +1. [HID Smart Buttons](#hid-smart-buttons) +2. [MQTT Remote Control](#mqtt-remote-control) + - [Mosquitto Installation Guide](#mosquitto-installation-guide) + - [Android Shortcut Setup](#android-shortcut-setup) + - [Nexus Timer Setup](#nexus-timer-setup) + +## HID Smart Buttons +For an enhanced tactile experience, Nexus Timer supports Smart Buttons based on Bluetooth-connected microcontroller (e.g., XIAO nRF52840) implementing HID (Human Interface Device) protocol. + +* **Buttons:** Connect 3 physical buttons, potentially extended (e.g., via 1.5m wires) for easy player access. +* **Configuration:** + * **Player 1's Button:** Single Click: Emulates a key press (e.g., 'a'). Configure this as Player 1's "Pass Turn / My Pause" hotkey in the app. + * **Player 2's Button:** Single Click: Emulates a key press (e.g., 'b'). Configure as Player 2's "Pass Turn / My Pause" hotkey. + * **Player 3's Button:** Single Click: Emulates a key press (e.g., 'c'). Configure as Player 3's "Pass Turn / My Pause" hotkey. + * **If Player 3 is Game Admin:** + * **Player 3's Button:** Long Press: Emulates a key press (e.g., 's'). Configure as the "Global Stop/Pause All" hotkey in the app. + * **Player 3's Button:** Double Click: Emulates a key press (e.g., 'x'). Configure as the "Global Run All Timers" hotkey in the app. + +## MQTT Remote Control +Players can use their smartphones to send commands to Nexus Timer via MQTT. This requires an MQTT broker (like Mosquitto) on the same network. + +### Mosquitto Installation Guide + +**On Android (using Termux):** + +1. **Install Termux** from the Play Store and run. +2. **Update packages and install Mosquitto in Termux:** + ```bash + pkg update && pkg upgrade + pkg install mosquitto + ``` +3. **Configure the MQTT Broker:** + ```bash + nano ~/.termux/etc/mosquitto/mosquitto.conf + ``` + Add the following configuration, then save and exit: + ``` + listener 1883 0.0.0.0 + protocol mqtt + + listener 8080 0.0.0.0 + protocol websockets + + allow_anonymous true + ``` +4. **Run Mosquitto with the configuration:** + ```bash + mosquitto -c ~/.termux/etc/mosquitto/mosquitto.conf + ``` + +* **MQTT Broker:** Needs to be accessible on the local network and configured with a WebSocket listener (e.g., `ws://:`). Nexus Timer defaults to `ws://localhost:9001`. + +### Android Shortcut Setup +* Install the "HTTP Shortcuts" app from the Play Store. +* Create a new shortcut. +* **Shortcut Type:** Choose "Execute Script" or a type that allows MQTT publishing (some versions might have direct MQTT support, or you can use Termux with `mosquitto_pub` called by the script). +* **If HTTP Shortcuts has direct MQTT publish action:** + * Configure Broker URL (e.g., `tcp://:`). + * Set Topic to: `game` + * Set Payload to: The single character (e.g., `a`). + * Set QoS, Retain as needed (usually 0 and false are fine). +* **If using scripting with `mosquitto_pub` (via Termux):** + * Install Termux and `mosquitto-clients` package (`pkg install mosquitto`). + * In HTTP Shortcuts, create a shortcut that executes a script. + * The script would be something like: `mosquitto_pub -h -p -t game -m "X"` + * Replace `` with your Mosquitto broker's IP. + * Replace `` with Mosquitto's TCP port (e.g., 1883). Note: `mosquitto_pub` uses TCP, while the PWA uses WebSockets to connect to the *same* broker. + * `-t game`: The topic Nexus Timer listens on. + * `-m "X"`: The single character message (e.g., "a", "b", "s"). This "X" should match the MQTT char configured in Nexus Timer for the desired action. +* **Customize Shortcut:** Give the shortcut a name (e.g., "Pass My Turn") and an icon on the Android home screen. + +### Nexus Timer Setup +* Enter the MQTT Broker URL in the Setup screen and connect. +* Assign unique single characters as **MQTT Triggers** for each player's "Pass Turn / My Pause" action. +* Assign unique single characters as **MQTT Triggers** for "Global Stop/Pause All" and "Global Run All Timers". \ No newline at end of file diff --git a/hooks/redeploy.sh b/hooks/redeploy.sh new file mode 100755 index 0000000..91e9aaf --- /dev/null +++ b/hooks/redeploy.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -eo pipefail # Exit on error, treat unset variables as an error, and propagate pipeline errors + +APP_DIR="/virt/nexus-timer" +IMAGE_NAME="virt-nexus-timer" +SERVICE_NAME="virt-nexus-timer.service" +LOG_FILE="/var/log/webhook-redeploy-nexus-timer.log" # Optional: for script-specific logging + +# Redirect stdout and stderr to a log file and also to the console (for webhook response) +exec > >(tee -a "$LOG_FILE") 2>&1 + +echo "----------------------------------------------------" +echo "Webhook redeploy-nexus-timer triggered at $(date)" +echo "Branch/Ref: $1" +echo "Repository: $2" +echo "----------------------------------------------------" + +# Ensure script is run from the app directory +cd "$APP_DIR" || { echo "ERROR: Failed to cd to $APP_DIR. Exiting."; exit 1; } + +# Optional: Check if the trigger is for the correct branch (e.g., main or master) +# TARGET_BRANCH="refs/heads/main" # Adjust to your primary branch name +# if [ "$1" != "$TARGET_BRANCH" ]; then +# echo "Webhook triggered for branch $1, but only deploying $TARGET_BRANCH. Exiting." +# exit 0 # Exit successfully to not show an error in Gitea for non-target branches +# fi + +echo "Pulling latest changes from Git (main branch)..." +# Ensure you are on the correct branch first or specify it in the pull +# git checkout main # Uncomment if your repo might be on other branches +git pull origin main || { echo "ERROR: git pull failed. Exiting."; exit 1; } # Adjust 'main' if needed + +echo "Building Docker image $IMAGE_NAME..." +docker build -t "$IMAGE_NAME" . || { echo "ERROR: docker build failed. Exiting."; exit 1; } + +echo "Restarting systemd service $SERVICE_NAME..." +# This command might require sudo privileges. See note below. +sudo systemctl restart "$SERVICE_NAME" || { echo "ERROR: systemctl restart failed. Exiting."; exit 1; } + +echo "Deployment finished successfully at $(date)." +echo "----------------------------------------------------" + +exit 0 \ No newline at end of file diff --git a/hooks/webhook.conf b/hooks/webhook.conf new file mode 100644 index 0000000..8b0d542 --- /dev/null +++ b/hooks/webhook.conf @@ -0,0 +1,38 @@ +[ + { + "id": "redeploy-nexus-timer", + "execute-command": "/virt/nexus-timer/hooks/redeploy.sh", + "command-working-directory": "/virt/nexus-timer", + "pass-arguments-to-command": [ + { "source": "payload", "name": "ref" }, + { "source": "payload", "name": "repository.full_name" } + ], + "trigger-rule": { + "and": [ + { + "match": { + "type": "payload-hmac-sha256", + "secret": "YOUR_VERY_STRONG_SECRET_TOKEN_HERE_REPLACE_ME", + "parameter": { + "source": "header", + "name": "X-Gitea-Signature" + } + } + }, + { + "match": { + "type": "value", + "value": "refs/heads/main", + "parameter": { + "source": "payload", + "name": "ref" + } + } + } + ] + }, + "include-command-output-in-response": true, + "include-command-output-in-response-on-error": true, + "response-message": "Webhook processed." + } +] \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..8be2b52 --- /dev/null +++ b/index.html @@ -0,0 +1,30 @@ + + + + + + + Nexus Timer + + + + + +
+ + + + \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..92f0884 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,47 @@ +# Existing log format often uses $remote_addr by default +# log_format main '$remote_addr - $remote_user [$time_local] "$request" ' +# '$status $body_bytes_sent "$http_referer" ' +# '"$http_user_agent" "$http_x_forwarded_for"'; + +# Add these lines within the http {} block, OR server {} block +# (http block is generally preferred for these directives) +# Make sure they are *before* the access_log directive if possible. + +# --- Real IP Configuration --- +# Replace with the ACTUAL IP range(s) of your Traefik Docker network(s) +set_real_ip_from 172.22.0.0/16; # Example: Trust IPs from this subnet +# You can add multiple set_real_ip_from lines if needed +# set_real_ip_from 192.168.1.0/24; # Example if Traefik was also on another network + +# Which header contains the real client IP? +# X-Forwarded-For handles multiple proxies better. X-Real-IP is simpler if only Traefik. +real_ip_header X-Real-IP; + +# If using X-Forwarded-For, tell Nginx how to process it. +# 'on' means find the *last* IP address that is NOT from a trusted proxy. +# This is usually correct when behind one or more trusted proxies. +real_ip_recursive off; +# --- End Real IP Configuration --- + + +server { + listen 80; + listen [::]:80; + server_name localhost; + + # Use the 'realip' processed $remote_addr in logs + # Ensure your access_log format uses $remote_addr (like 'combined' or 'main') + access_log /var/log/nginx/access.log combined; # Or your preferred format using $remote_addr + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} + +# It's common practice to put realip config in the http block +# If your default.conf is included inside an existing http block in nginx.conf, +# placing the realip directives *outside* the server block but *inside* http is standard. +# If this file IS your entire http block, place them just before the server block. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..05048c0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2732 @@ +{ + "name": "nexus-timer", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nexus-timer", + "version": "0.0.1", + "dependencies": { + "mqtt": "^5.13.0", + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "vuex": "^4.0.2" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.16", + "@vitejs/plugin-vue": "^4.2.3", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.27", + "tailwindcss": "^3.3.3", + "vite": "^4.4.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/node": { + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz", + "integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz", + "integrity": "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.150", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz", + "integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "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/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mqtt": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.13.0.tgz", + "integrity": "sha512-pR+z+ChxFl3n8AKLQbTONVOOg/jl4KiKQRBAi78tjd6PksOWvl1nl9L8ZHOZ3MiavZfrUOjok2ddwc1VymGWRg==", + "dependencies": { + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.0", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.3", + "split2": "^4.2.0", + "worker-timers": "^7.1.8", + "ws": "^8.18.0" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "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/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vuex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", + "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==", + "dependencies": { + "@vue/devtools-api": "^6.0.0-beta.11" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f32b8be --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "nexus-timer", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "mqtt": "^5.13.0", + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "vuex": "^4.0.2" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.16", + "@vitejs/plugin-vue": "^4.2.3", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.27", + "tailwindcss": "^3.3.3", + "vite": "^4.4.5" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..70d778e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, + } \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d8045d922e571db84ff130ce2bf1309d55f3c119 GIT binary patch literal 15406 zcmeHOYitx%6doV{#UK8~Em6d%JbVE0iDF{Jx6#BzOo&esAMvgDXd0tGF#5-+3DA~T zEm&G;iwG@HD32nK2sTZrJfyrzsqH*=W_EXGcjkD`-OjW-yRe8CZ2I)!_merU<2sI;f^Col zDU#2z%28ox!a z9dF2qUf%i!;9Ota~6Q`p6$& zGkSUJ>tm;9^IgW;6JG!mB8R>oGriRK1Q^N%(2lPMb@w7L!e@ZSA-BE?^7c1bolOSe z=4T*2aF}7w^e6oLi(|qg*3M=+OnNLeJqc3d;~;IA2g0h`ob(yjKsKDi@AFxMj5ecD zaPg4rObD+zc#(a&RDlZa%E6`Ae}TCB#yB>ooO1cyM?4wtO2#{vKJum+O!D?~+)m8J z#^1s8I=cC>E4wUu}L8!Uicf5(AZrk*j(IK=M(|pBXsQos7x*@yjI=U$B z@B7MMSTz-OAZ|m5b}sRiKds~Vh5a^v+D8{+ZM*419rhsz1nukH(7I_${w802V_N^> zn!C{jF>D!R*t@6ExZ!KH)q7i+uSWi&+EEVTulL#gT7DRT@aj7(uDyrJ*B&U|&^MUA zL-GED_G9f-3&gr>Ks#0s+CRU6hPd@J+aY@R$DI94O9mwC(Y$P?W4s}s+v+Rlx~dG6 z9q*w3_tzM?jx;OuH-h9rHi-0r)`7b3E4(W3_m`mV{T!4%i$K}^2`IZh!hPxo5Z?G) z{^NHa0}2w)V){VL56xLG^PCrl5A3HL`pEYs|NByLf&a^Mij!c7mRo?=<>Dmxn&X#D zx@K_M#QCT%9eh?eL%D3+BzPrWW!{|qZ)w&Wfp?3N4> zv#m4Mv*6XGt?$|7S;}iQ8}{+{LE6h)601-%1GE#3kQh1&dfz^Xp&u<_JfmQ?(qlc5 zTV4h;CXQ^Z@y?8^c%JiV-advOGN3pC;dK`BmxPtKmi#=BUqR=F&O*K*^OHTE|9t02 zK|L`OQ73%R&+W!o{*2O>|IYXD>^5D0$Tt>$xwGW)%dM}0sYz+_PYiYgor&fSF=_MH$S&eq&Qxt z;wl%Sh;}x5}YKu@C#@sN5`G5!RQ!|_uBpys((W|ZNau{ zBRYuqlleV`;^S36vA^qV+#@$82(>cYeiOuw)}-N2@I4vN$_mN5P<>Gzfzy{UWZcJ%I$tXk zFE9F$nw|nfy_hlwCfd;OrWXRCLpKl(Nb`Z!+5%ZonM1B+Aeqe7t#`WEpQ z`8!rU8N_uDWz7u}YiHqBLQfe(ta~-^%O@9g4oHVdmnp{~e(?}QaBnF!Jd*Tr9oV#! z%_;b`Q(JJ{Q;UjUSa}PcgEyq$qPP#)e3CDn+sa!OfVBQmP}<(cm{7!S%Y6B|IT(+) z1H?76(H`9oQvF;ue#&!^8ZloOWkKu53!S$5qR@q1{aicx_-(j$wt;@S74u-WU_Q(y&`xe-v3L6agBa>0`4Evmf55oc zEZ^~8ZfxW4T>r0AaON+_XOP4B1Oe91WG+OI^9L{oBFOn0Rt}1t3sK7JVOZZs=g$!S L@iH!fTqW=yU4u4Y literal 0 HcmV?d00001 diff --git a/public/icons/icon-192x192.png b/public/icons/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..7fabb3868741e613c459cce621525e917d36b76e GIT binary patch literal 6035 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 W6@G)1C5CvH9iXAAt5U9H6Z#)KUH5hX literal 0 HcmV?d00001 diff --git a/public/icons/icon-512x512.png b/public/icons/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..6c47bef641900744a3fb17630b5af64549b71cd2 GIT binary patch literal 17887 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 V4oQ0eez678xU6%j=z`U~{{<5pF}nZ& literal 0 HcmV?d00001 diff --git a/public/icons/maskable-icon-192x192.png b/public/icons/maskable-icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..7fabb3868741e613c459cce621525e917d36b76e GIT binary patch literal 6035 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 W6@G)1C5CvH9iXAAt5U9H6Z#)KUH5hX literal 0 HcmV?d00001 diff --git a/public/icons/maskable-icon-512x512.png b/public/icons/maskable-icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..6c47bef641900744a3fb17630b5af64549b71cd2 GIT binary patch literal 17887 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 V4oQ0eez678xU6%j=z`U~{{<5pF}nZ& literal 0 HcmV?d00001 diff --git a/public/icons/shortcut-info-96x96.png b/public/icons/shortcut-info-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..2fb587768c589d5e912e81b6fdd774fbfe554190 GIT binary patch literal 2143 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 +
+ +
+ A new version is available! + + +
+ + + + + + +
+ + + + + \ No newline at end of file diff --git a/src/assets/default-avatar.png b/src/assets/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..6812f6f0367fed6024925b347ef95bded4da56b1 GIT binary patch literal 458 zcmV;*0X6=KP)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv|Nj)LG0gw~00(qQ zO+^Rk1qTuJrUyEfa%rqKPG?@Al<=AM*W&?~e&VB;7@e zhX9N0P=AgM^+h(o6c^e~)cVO)>)VOnzXH6tmfG*I|DH23q=T_4Eg|e<_2JB zAh|->mx|@I-u z1Az7@))%W>Yfk{xY1X8MlMSGHw@4 zFLNBx&sWHe!Y4U5@-oMBaMChPT7>H4$D%$nKc?k>GA=9dT>t<807*qoM6N<$f*Y*8 AmjD0& literal 0 HcmV?d00001 diff --git a/src/assets/tailwind.css b/src/assets/tailwind.css new file mode 100644 index 0000000..d4e13c1 --- /dev/null +++ b/src/assets/tailwind.css @@ -0,0 +1,54 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 antialiased select-none; + overscroll-behavior-y: contain; /* Prevents pull-to-refresh on mobile */ +} + +/* Safe area insets for mobile devices (status bar, home indicator) */ +.safe-area-padding { + padding-top: env(safe-area-inset-top); + /* padding-bottom: env(safe-area-inset-bottom); */ +} + +.safe-area-height { + height: calc(100dvh - env(safe-area-inset-top)); + min-height: 0; +} + +/* Basic button styling */ +.btn { + @apply px-4 py-2 rounded font-semibold focus:outline-none focus:ring-2 focus:ring-opacity-50; +} +.btn-primary { + @apply bg-blue-500 hover:bg-blue-600 text-white focus:ring-blue-400; +} +.btn-secondary { + @apply bg-gray-500 hover:bg-gray-600 text-white focus:ring-gray-400; +} +.btn-danger { + @apply bg-red-500 hover:bg-red-600 text-white focus:ring-red-400; +} +.btn-warning { + @apply bg-yellow-500 hover:bg-yellow-600 text-black focus:ring-yellow-400; +} +.btn-icon { + @apply p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700; +} + +/* Input styling */ +.input-base { + @apply mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm; +} + +/* For preventing text selection during swipes/taps */ +.no-select { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ +} \ No newline at end of file diff --git a/src/components/DefaultAvatarIcon.vue b/src/components/DefaultAvatarIcon.vue new file mode 100644 index 0000000..a67623e --- /dev/null +++ b/src/components/DefaultAvatarIcon.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/components/HotkeyCaptureOverlay.vue b/src/components/HotkeyCaptureOverlay.vue new file mode 100644 index 0000000..66e920a --- /dev/null +++ b/src/components/HotkeyCaptureOverlay.vue @@ -0,0 +1,79 @@ + + + + + \ No newline at end of file diff --git a/src/components/MqttCharCaptureOverlay.vue b/src/components/MqttCharCaptureOverlay.vue new file mode 100644 index 0000000..8e69569 --- /dev/null +++ b/src/components/MqttCharCaptureOverlay.vue @@ -0,0 +1,65 @@ + + + \ No newline at end of file diff --git a/src/components/PlayerDisplay.vue b/src/components/PlayerDisplay.vue new file mode 100644 index 0000000..823d519 --- /dev/null +++ b/src/components/PlayerDisplay.vue @@ -0,0 +1,145 @@ + + + \ No newline at end of file diff --git a/src/components/PlayerForm.vue b/src/components/PlayerForm.vue new file mode 100644 index 0000000..108e6ee --- /dev/null +++ b/src/components/PlayerForm.vue @@ -0,0 +1,260 @@ + + + \ No newline at end of file diff --git a/src/components/PlayerListItem.vue b/src/components/PlayerListItem.vue new file mode 100644 index 0000000..16de6d2 --- /dev/null +++ b/src/components/PlayerListItem.vue @@ -0,0 +1,80 @@ + + + \ No newline at end of file diff --git a/src/components/TimerDisplay.vue b/src/components/TimerDisplay.vue new file mode 100644 index 0000000..f705405 --- /dev/null +++ b/src/components/TimerDisplay.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..50523cb --- /dev/null +++ b/src/main.js @@ -0,0 +1,26 @@ +// src/main.js +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' // router will be initialized here +import store from './store' // store will be initialized here +import './assets/tailwind.css' + +const app = createApp(App) + +// Dispatch loadState immediately after store is created and before app is mounted +// and before router is fully used by the app. +store.dispatch('loadState').then(() => { + // Now that the state is loaded (or attempted to be loaded), + // we can safely use the router and mount the app. + app.use(router) + app.use(store) // Using store here is fine, it's already created. + app.mount('#app') +}).catch(error => { + console.error("Failed to load initial state for the store:", error); + // Fallback: Mount the app even if state loading fails, guards should handle it. + // Or display an error message to the user. + // For now, let's still try to mount. + app.use(router) + app.use(store) + app.mount('#app') +}); \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..1dd1032 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,59 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import SetupView from '../views/SetupView.vue'; +import GameView from '../views/GameView.vue'; +import InfoView from '../views/InfoView.vue'; +import store from '../store'; + +const routes = [ + { + path: '/', + name: 'Setup', + component: SetupView, + beforeEnter: (to, from, next) => { + // Check if we are navigating FROM the Game view. + // If so, the user explicitly clicked the Setup button, so allow it. + if (from.name === 'Game') { + console.log('Router Guard: Allowing navigation from Game to Setup.'); + next(); // Allow navigation to Setup + return; // Stop further processing of this guard + } + + // Original logic for initial load or other navigations TO Setup: + if (store.state.players && store.state.players.length >= 2) { + // If 2 or more players exist (and not coming from Game), redirect to Game. + console.log('Router Guard: Players found, redirecting to Game (not coming from Game).'); + next({ name: 'Game', replace: true }); + } else { + // Otherwise (fewer than 2 players), allow navigation to the Setup view. + console.log('Router Guard: Not enough players, proceeding to Setup.'); + next(); + } + } + }, + { + path: '/game', + name: 'Game', + component: GameView, + beforeEnter: (to, from, next) => { + // Keep this guard: prevent direct access to /game without enough players + if (!store.state.players || store.state.players.length < 2) { + console.log('Router Guard: Attempted to access Game without enough players, redirecting to Setup.'); + next({ name: 'Setup' }); + } else { + next(); + } + } + }, + { + path: '/info', + name: 'Info', + component: InfoView + } +]; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes +}); + +export default router; \ No newline at end of file diff --git a/src/services/AudioService.js b/src/services/AudioService.js new file mode 100644 index 0000000..ed4ab17 --- /dev/null +++ b/src/services/AudioService.js @@ -0,0 +1,133 @@ +let audioContext; +let tickSoundBuffer; // For short tick +let passTurnSoundBuffer; // For 3s pass turn alert +let isMutedGlobally = false; +let continuousTickInterval = null; // For setInterval based continuous ticking +let passTurnSoundTimeout = null; + +function getAudioContext() { + if (!audioContext && (window.AudioContext || window.webkitAudioContext)) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + return audioContext; +} + +function createBeepBuffer(frequency = 440, duration = 0.1, type = 'sine') { + const ctx = getAudioContext(); + if (!ctx) return null; + const sampleRate = ctx.sampleRate; + const numFrames = duration * sampleRate; + const buffer = ctx.createBuffer(1, numFrames, sampleRate); + const data = buffer.getChannelData(0); + const gain = 0.1; // Reduce gain to make beeps softer + + for (let i = 0; i < numFrames; i++) { + // Simple fade out + const currentGain = gain * (1 - (i / numFrames)); + if (type === 'square') { + data[i] = (Math.sin(2 * Math.PI * frequency * (i / sampleRate)) >= 0 ? 1 : -1) * currentGain; + } else { // sine + data[i] = Math.sin(2 * Math.PI * frequency * (i / sampleRate)) * currentGain; + } + } + return buffer; +} + +async function initSounds() { + const ctx = getAudioContext(); + if (!ctx) return; + // Tick sound (shorter, slightly different pitch) + if (!tickSoundBuffer) { + // Using a square wave for a more 'digital' tick, short duration + tickSoundBuffer = createBeepBuffer(1000, 0.03, 'square'); + } + // Pass turn alert sound (3 beeps) + if (!passTurnSoundBuffer) { + passTurnSoundBuffer = createBeepBuffer(660, 0.08, 'sine'); + } +} +initSounds(); + +function playSoundBuffer(buffer) { + if (isMutedGlobally || !buffer || !audioContext || audioContext.state === 'suspended') return; + const source = audioContext.createBufferSource(); + source.buffer = buffer; + source.connect(audioContext.destination); + source.start(); +} + +export const AudioService = { + setMuted(muted) { + isMutedGlobally = muted; + if (muted) { + this.stopContinuousTick(); + this.cancelPassTurnSound(); + } + }, + + // This is the single, short tick sound for "All Timers Running" mode. + _playSingleTick() { + playSoundBuffer(tickSoundBuffer); + }, + + playPassTurnAlert() { + if (isMutedGlobally || !audioContext || audioContext.state === 'suspended') return; + this.cancelPassTurnSound(); + + let count = 0; + const playAndSchedule = () => { + if (count < 3 && !isMutedGlobally && audioContext.state !== 'suspended') { + playSoundBuffer(passTurnSoundBuffer); + count++; + passTurnSoundTimeout = setTimeout(playAndSchedule, 1000); // Beep every second for 3s + } else { + passTurnSoundTimeout = null; + } + }; + playAndSchedule(); + }, + + cancelPassTurnSound() { + if (passTurnSoundTimeout) { + clearTimeout(passTurnSoundTimeout); + passTurnSoundTimeout = null; + } + }, + + startContinuousTick() { + this.stopContinuousTick(); // Clear any existing interval + if (isMutedGlobally || !audioContext || audioContext.state === 'suspended') return; + + // Play immediately once, then set interval + this._playSingleTick(); + continuousTickInterval = setInterval(() => { + if (!isMutedGlobally && audioContext.state !== 'suspended') { + this._playSingleTick(); + } else { + this.stopContinuousTick(); // Stop if muted or context suspended during interval + } + }, 1000); // Tick every second + }, + + stopContinuousTick() { + if (continuousTickInterval) { + clearInterval(continuousTickInterval); + continuousTickInterval = null; + } + // Ensure no rogue oscillators are playing. + // If an oscillator was ever used directly and not disconnected, it could persist. + // The current implementation relies on BufferSource which stops automatically. + }, + + resumeContext() { + const ctx = getAudioContext(); + if (ctx && ctx.state === 'suspended') { + ctx.resume().then(() => { + console.log("AudioContext resumed successfully."); + initSounds(); // Re-initialize sounds if context was suspended for long + }).catch(e => console.error("Error resuming AudioContext:", e)); + } else if (ctx && !tickSoundBuffer) { // If context was fine but sounds not loaded + initSounds(); + } + } +}; \ No newline at end of file diff --git a/src/services/CameraService.js b/src/services/CameraService.js new file mode 100644 index 0000000..b15a32a --- /dev/null +++ b/src/services/CameraService.js @@ -0,0 +1,69 @@ +export const CameraService = { + async getPhoto() { + return new Promise(async (resolve, reject) => { + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + reject(new Error('Camera API not available.')); + return; + } + + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false }); + + // Create a modal or an overlay to show the video stream and a capture button + const videoElement = document.createElement('video'); + videoElement.srcObject = stream; + videoElement.setAttribute('playsinline', ''); // Required for iOS + videoElement.style.cssText = "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 90%; max-height: 70vh; z-index: 1001; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.2);"; + + const captureButton = document.createElement('button'); + captureButton.textContent = 'Capture'; + captureButton.style.cssText = "position: fixed; bottom: 10%; left: 50%; transform: translateX(-50%); z-index: 1002; padding: 12px 24px; background-color: #3b82f6; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer;"; + + const closeButton = document.createElement('button'); + closeButton.textContent = 'Cancel'; + closeButton.style.cssText = "position: fixed; top: 10px; right: 10px; z-index: 1002; padding: 8px 12px; background-color: #ef4444; color: white; border: none; border-radius: 8px; font-size: 14px; cursor: pointer;"; + + + const overlay = document.createElement('div'); + overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 1000;"; + + document.body.appendChild(overlay); + document.body.appendChild(videoElement); + document.body.appendChild(captureButton); + document.body.appendChild(closeButton); + + videoElement.onloadedmetadata = () => { + videoElement.play(); + }; + + const cleanup = () => { + stream.getTracks().forEach(track => track.stop()); + document.body.removeChild(videoElement); + document.body.removeChild(captureButton); + document.body.removeChild(closeButton); + document.body.removeChild(overlay); + }; + + captureButton.onclick = () => { + const canvas = document.createElement('canvas'); + canvas.width = videoElement.videoWidth; + canvas.height = videoElement.videoHeight; + const context = canvas.getContext('2d'); + context.drawImage(videoElement, 0, 0, canvas.width, canvas.height); + const dataUrl = canvas.toDataURL('image/png'); + cleanup(); + resolve(dataUrl); + }; + + closeButton.onclick = () => { + cleanup(); + reject(new Error('User cancelled photo capture.')); + }; + + } catch (err) { + console.error("Error accessing camera: ", err); + reject(err); + } + }); + } + }; \ No newline at end of file diff --git a/src/services/MqttService.js b/src/services/MqttService.js new file mode 100644 index 0000000..5ac8599 --- /dev/null +++ b/src/services/MqttService.js @@ -0,0 +1,146 @@ +// src/services/MqttService.js +import mqtt from 'mqtt'; +import { ref } from 'vue'; + +const client = ref(null); +const connectionStatus = ref('disconnected'); +const error = ref(null); +const receivedMessages = ref([]); // For debugging +const MQTT_TOPIC_GAME = 'game'; + +let generalMessageHandlerCallback = null; // For App.vue to handle game commands + +// --- MQTT Character Capture State --- +const isCapturingMqttChar = ref(false); +const mqttCharCaptureCallback = ref(null); // Function to call when a char is captured + +const startMqttCharCapture = (onCapturedCallback) => { + console.log("MQTT Service: Starting character capture mode."); + isCapturingMqttChar.value = true; + mqttCharCaptureCallback.value = onCapturedCallback; +}; + +const stopMqttCharCapture = () => { + console.log("MQTT Service: Stopping character capture mode."); + isCapturingMqttChar.value = false; + mqttCharCaptureCallback.value = null; +}; +// --- End MQTT Character Capture State --- + +const connect = async (brokerUrl = 'ws://localhost:9001') => { + // ... (existing connect logic) + if (client.value && client.value.connected) { return; } + if (connectionStatus.value === 'connecting') { return; } + console.log(`MQTT: Attempting to connect to ${brokerUrl}...`); + connectionStatus.value = 'connecting'; + error.value = null; + let fullBrokerUrl = brokerUrl; + if (!brokerUrl.startsWith('ws://') && !brokerUrl.startsWith('wss://')) { + if (brokerUrl.includes(':1883')) { + fullBrokerUrl = `ws://${brokerUrl}`; + } else if (!brokerUrl.includes(':')) { + fullBrokerUrl = `ws://${brokerUrl}:9001`; + } else { + fullBrokerUrl = `ws://${brokerUrl}`; + } + } + + try { + const connectFn = typeof mqtt === 'function' ? mqtt : (mqtt.connect || (mqtt.default && mqtt.default.connect)); + if (typeof connectFn !== 'function') { + throw new Error("MQTT connect function not found."); + } + client.value = connectFn(fullBrokerUrl, { + reconnectPeriod: 5000, + connectTimeout: 10000, + }); + + client.value.on('connect', () => { + console.log('MQTT: Connected!'); + connectionStatus.value = 'connected'; + error.value = null; + client.value.subscribe(MQTT_TOPIC_GAME, (err) => { + if (!err) { console.log(`MQTT: Subscribed to "${MQTT_TOPIC_GAME}"`); } + else { + console.error('MQTT: Subscr. error:', err); + connectionStatus.value = 'error'; error.value = 'Subscription failed.'; + } + }); + }); + + client.value.on('message', (topic, message) => { + const msgString = message.toString(); + const char = msgString.charAt(0).toLowerCase(); // Process as lowercase single char + console.log(`MQTT: Received on "${topic}": '${msgString}' -> processed char: '${char}'`); + receivedMessages.value.push({ topic, message: msgString, time: new Date() }); + + if (topic === MQTT_TOPIC_GAME && char.length === 1) { + if (isCapturingMqttChar.value && mqttCharCaptureCallback.value) { + console.log(`MQTT Service: Captured char '${char}' for registration.`); + mqttCharCaptureCallback.value(char); // Pass char to the specific capture handler + stopMqttCharCapture(); // Stop capture mode after one char + } else if (generalMessageHandlerCallback) { + // Normal message processing if not in capture mode + generalMessageHandlerCallback(char); + } + } + }); + + client.value.on('error', (err) => { /* ... existing error handling ... */ + console.error('MQTT: Connection error:', err); + connectionStatus.value = 'error'; + error.value = err.message || 'Connection error'; + }); + client.value.on('reconnect', () => { /* ... existing reconnect handling ... */ + console.log('MQTT: Reconnecting...'); + connectionStatus.value = 'connecting'; + }); + client.value.on('offline', () => { /* ... existing offline handling ... */ + console.log('MQTT: Client offline.'); + }); + client.value.on('close', () => { /* ... existing close handling ... */ + console.log('MQTT: Connection closed.'); + if (connectionStatus.value !== 'error' && connectionStatus.value !== 'connecting') { + connectionStatus.value = 'disconnected'; + } + }); + + } catch (err) { /* ... existing catch ... */ + console.error('MQTT: Setup error during connect call:', err); + connectionStatus.value = 'error'; + error.value = err.message || 'Setup failed.'; + if(client.value && typeof client.value.end === 'function') client.value.end(true); + client.value = null; + } +}; + +const disconnect = () => { /* ... existing disconnect logic ... */ + if (client.value && typeof client.value.end === 'function') { + console.log('MQTT: Disconnecting...'); + connectionStatus.value = 'disconnected'; + client.value.end(true, () => { + console.log('MQTT: Disconnected callback triggered.'); + }); + } else { + connectionStatus.value = 'disconnected'; + } +}; + +const setGeneralMessageHandler = (handler) => { // Renamed for clarity + generalMessageHandlerCallback = handler; +}; + +export const MqttService = { + connect, + disconnect, + setGeneralMessageHandler, // Use this for App.vue game commands + connectionStatus, + error, + receivedMessages, + MQTT_TOPIC_GAME, + getClient: () => client.value, + // New methods for capture mode + startMqttCharCapture, + stopMqttCharCapture, + isCapturingMqttChar // Expose reactive state if needed elsewhere, though not directly used by components +}; \ No newline at end of file diff --git a/src/services/StorageService.js b/src/services/StorageService.js new file mode 100644 index 0000000..7dbf34d --- /dev/null +++ b/src/services/StorageService.js @@ -0,0 +1,23 @@ +const STORAGE_KEY = 'nexusTimerState'; + +export const StorageService = { + getState() { + const savedState = localStorage.getItem(STORAGE_KEY); + if (savedState) { + try { + return JSON.parse(savedState); + } catch (e) { + console.error("Error parsing saved state from localStorage", e); + localStorage.removeItem(STORAGE_KEY); // Clear corrupted data + return null; + } + } + return null; + }, + saveState(state) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + }, + clearState() { + localStorage.removeItem(STORAGE_KEY); + } +}; \ No newline at end of file diff --git a/src/services/WakeLockService.js b/src/services/WakeLockService.js new file mode 100644 index 0000000..b66d8c4 --- /dev/null +++ b/src/services/WakeLockService.js @@ -0,0 +1,65 @@ +let wakeLock = null; +let wakeLockActive = false; + +const requestWakeLock = async () => { + if ('wakeLock' in navigator && !wakeLockActive) { + try { + wakeLock = await navigator.wakeLock.request('screen'); + wakeLockActive = true; + console.log('Screen Wake Lock activated.'); + + wakeLock.addEventListener('release', () => { + console.log('Screen Wake Lock was released.'); + wakeLockActive = false; + wakeLock = null; // Clear the reference + // Optionally, re-request if it was released unexpectedly and should be active + // For now, we'll let it be re-requested manually by the app logic + }); + } catch (err) { + console.error(`Failed to acquire Screen Wake Lock: ${err.name}, ${err.message}`); + wakeLock = null; + wakeLockActive = false; + } + } else { + console.warn('Screen Wake Lock API not supported or already active.'); + } +}; + +const releaseWakeLock = async () => { + if (wakeLock && wakeLockActive) { + try { + await wakeLock.release(); + // The 'release' event listener on wakeLock itself will set wakeLockActive = false and wakeLock = null + } catch (err) { + console.error(`Failed to release Screen Wake Lock: ${err.name}, ${err.message}`); + // Even if release fails, mark as inactive to allow re-request + wakeLock = null; + wakeLockActive = false; + } + } else { + // console.log('No active Screen Wake Lock to release or already released.'); + } +}; + +// Handle visibility changes to re-acquire lock if necessary +const handleVisibilityChange = () => { + if (wakeLock !== null && document.visibilityState === 'visible') { + // If we had a wake lock and the page became visible again, + // it might have been released by the browser. Try to re-acquire. + // This behavior is usually handled automatically by the browser with the 'release' event + // but can be a fallback. For now, we rely on manual re-request. + // console.log('Page visible, checking wake lock status.'); + } else if (document.visibilityState === 'hidden' && wakeLockActive) { + // The browser usually releases the wake lock when tab is hidden. + // Our 'release' event listener should handle this. + } +}; + +document.addEventListener('visibilitychange', handleVisibilityChange); +// document.addEventListener('fullscreenchange', handleVisibilityChange); // Also useful for fullscreen + +export const WakeLockService = { + request: requestWakeLock, + release: releaseWakeLock, + isActive: () => wakeLockActive, +}; \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..3d8a111 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,495 @@ +import { createStore } from 'vuex'; +import { StorageService } from '../services/StorageService'; +import { parseTime, formatTime } from '../utils/timeFormatter'; + +const MAX_NEGATIVE_SECONDS = -(59 * 60 + 59); // -59:59 +const DEFAULT_AVATAR_MARKER = null; // Ensure this is defined + +// Define predefined players +const predefinedPlayers = [ + { + id: 'predefined-1', // Unique ID for predefined player 1 + name: 'Player 1', + avatar: DEFAULT_AVATAR_MARKER, // Or a specific avatar path if you have one + initialTimerSec: 60 * 60, // 60:00 + currentTimerSec: 60 * 60, + hotkey: '1', // Hotkey '1' + isSkipped: false, + isTimerRunning: false, + }, + { + id: 'predefined-2', // Unique ID for predefined player 2 + name: 'Player 2', + avatar: DEFAULT_AVATAR_MARKER, // Or a specific avatar path + initialTimerSec: 60 * 60, // 60:00 + currentTimerSec: 60 * 60, + hotkey: '2', // Hotkey '2' + isSkipped: false, + isTimerRunning: false, + } +]; + +const initialState = { + players: JSON.parse(JSON.stringify(predefinedPlayers)), // Start with predefined players (deep copy) + globalHotkeyStopPause: null, + globalHotkeyRunAll: null, + globalMqttStopPause: null, // New + globalMqttRunAll: null, // New + mqttBrokerUrl: 'ws://localhost:9001', // Default, user can change + currentPlayerIndex: 0, + gameMode: 'normal', + isMuted: false, + theme: 'dark', + gameRunning: false, +}; + +// Helper function to create a new player object +const createPlayerObject = (playerData = {}) => ({ + id: playerData.id || Date.now().toString() + Math.random(), + name: playerData.name || `Player ${store.state.players.length + 1}`, // Access store carefully here or pass length + avatar: playerData.avatar === undefined ? DEFAULT_AVATAR_MARKER : playerData.avatar, + initialTimerSec: playerData.initialTimerSec || 3600, + currentTimerSec: playerData.currentTimerSec || playerData.initialTimerSec || 3600, + hotkey: playerData.hotkey || null, + mqttChar: playerData.mqttChar || null, // New + isSkipped: playerData.isSkipped || false, + isTimerRunning: playerData.isTimerRunning || false, +}); + +export default createStore({ + state: () => { + const persistedState = StorageService.getState(); + if (persistedState) { + let playersToUse = persistedState.players; + if (!playersToUse || (playersToUse.length === 0 && !persistedState.hasOwnProperty('players'))) { + playersToUse = JSON.parse(JSON.stringify(predefinedPlayers.map(p => createPlayerObject(p)))); + } else if (persistedState.hasOwnProperty('players') && playersToUse.length === 0) { + playersToUse = []; + } + + playersToUse = playersToUse.map(p_persisted => { + const p_base = predefinedPlayers.find(p_def => p_def.id === p_persisted.id) || {}; + return createPlayerObject({ ...p_base, ...p_persisted, isTimerRunning: false }); + }); + + return { + ...initialState, // Start with all initial state defaults + ...persistedState, // Override with persisted values + players: playersToUse, // Specifically set processed players + // Ensure new MQTT fields from initialState are used if not in persistedState + globalMqttStopPause: persistedState.globalMqttStopPause || initialState.globalMqttStopPause, + globalMqttRunAll: persistedState.globalMqttRunAll || initialState.globalMqttRunAll, + mqttBrokerUrl: persistedState.mqttBrokerUrl || initialState.mqttBrokerUrl, + gameRunning: false, // Always start non-running + currentPlayerIndex: persistedState.currentPlayerIndex !== undefined && persistedState.currentPlayerIndex < playersToUse.length ? persistedState.currentPlayerIndex : 0, + }; + } + // If no persisted state, deep copy initialState which includes new MQTT fields + const newInitialState = JSON.parse(JSON.stringify(initialState)); + newInitialState.players = newInitialState.players.map(p => createPlayerObject(p)); + return newInitialState; + }, + mutations: { + ADD_PLAYER(state, playerConfig) { + if (state.players.length < 99) { + const newPlayer = createPlayerObject({ + name: playerConfig.name, + avatar: playerConfig.avatar, + initialTimerSec: playerConfig.initialTimerSec, + currentTimerSec: playerConfig.currentTimerSec, // Set from form + hotkey: playerConfig.hotkey, + mqttChar: playerConfig.mqttChar // From form + }); + state.players.push(newPlayer); + } else { + alert("Maximum player limit (99) reached."); + } + }, + UPDATE_PLAYER(state, updatedPlayer) { + const index = state.players.findIndex(p => p.id === updatedPlayer.id); + if (index !== -1) { + state.players[index] = { ...state.players[index], ...updatedPlayer }; + } + }, + SET_MQTT_BROKER_URL(state, url) { + state.mqttBrokerUrl = url; + }, + SET_GLOBAL_MQTT_STOP_PAUSE(state, char) { + state.globalMqttStopPause = char; + }, + SET_GLOBAL_MQTT_RUN_ALL(state, char) { + state.globalMqttRunAll = char; + }, + // ... (SET_PLAYERS, SET_THEME, etc.) + SET_PLAYERS(state, playersData) { // Used by fullResetApp and potentially reorder + state.players = playersData.map(p => createPlayerObject(p)); + }, + SET_THEME(state, theme) { state.theme = theme; }, + DELETE_PLAYER(state, playerId) { + state.players = state.players.filter(p => p.id !== playerId); + if (state.currentPlayerIndex >= state.players.length && state.players.length > 0) { + state.currentPlayerIndex = state.players.length - 1; + } else if (state.players.length === 0) { + state.currentPlayerIndex = 0; + } + }, + REORDER_PLAYERS(state, players) { + state.players = players; + }, + SHUFFLE_PLAYERS(state) { + for (let i = state.players.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [state.players[i], state.players[j]] = [state.players[j], state.players[i]]; + } + }, + REVERSE_PLAYERS(state) { + state.players.reverse(); + }, + SET_CURRENT_PLAYER_INDEX(state, index) { + state.currentPlayerIndex = index; + }, + SET_GAME_MODE(state, mode) { + state.gameMode = mode; + }, + SET_IS_MUTED(state, muted) { + state.isMuted = muted; + }, + TOGGLE_THEME(state) { + state.theme = state.theme === 'light' ? 'dark' : 'light'; + }, + SET_GLOBAL_HOTKEY_STOP_PAUSE(state, key) { + state.globalHotkeyStopPause = key; + }, + SET_GLOBAL_HOTKEY_RUN_ALL(state, key) { + state.globalHotkeyRunAll = key; + }, + SET_THEME(state, theme) { + state.theme = theme; + }, + DECREMENT_TIMER(state, { playerIndex }) { + const player = state.players[playerIndex]; + if (player && player.isTimerRunning && !player.isSkipped) { + player.currentTimerSec--; + if (player.currentTimerSec < MAX_NEGATIVE_SECONDS) { + player.currentTimerSec = MAX_NEGATIVE_SECONDS; + player.isSkipped = true; // Auto-skip if max negative time reached + player.isTimerRunning = false; + } + } + }, + RESET_PLAYER_TIMER(state, playerIndex) { + if (state.players[playerIndex]) { + state.players[playerIndex].currentTimerSec = state.players[playerIndex].initialTimerSec; + state.players[playerIndex].isSkipped = false; + state.players[playerIndex].isTimerRunning = false; + } + }, + RESET_ALL_TIMERS(state) { + // When resetting, decide if you want to go back to *only* predefined players + // or reset existing players' timers. The current spec "restores all timers to initial values" + // implies resetting existing players. If it meant reverting to the initial player set, + // this logic would need to change to: + // state.players = JSON.parse(JSON.stringify(predefinedPlayers)); + // For now, sticking to resetting current players' timers: + state.players.forEach(player => { + player.currentTimerSec = player.initialTimerSec; + player.isSkipped = false; + player.isTimerRunning = false; + }); + state.currentPlayerIndex = 0; + state.gameMode = 'normal'; + state.gameRunning = false; + }, + START_PLAYER_TIMER(state, playerIndex) { + if(state.players[playerIndex] && !state.players[playerIndex].isSkipped) { + state.players[playerIndex].isTimerRunning = true; + state.gameRunning = true; + } + }, + PAUSE_PLAYER_TIMER(state, playerIndex) { + if(state.players[playerIndex]) { + state.players[playerIndex].isTimerRunning = false; + } + if (!state.players.some(p => p.isTimerRunning)) { + state.gameRunning = false; + } + }, + PAUSE_ALL_TIMERS(state) { + state.players.forEach(p => p.isTimerRunning = false); + state.gameRunning = false; + }, + SET_GAME_RUNNING(state, isRunning) { + state.gameRunning = isRunning; + }, + }, + actions: { + loadState({ commit, state }) { + // The state initializer already did the main loading from localStorage. + // This action can be used for any *additional* setup after initial hydration + // or to re-apply certain defaults if needed. + // For now, it's mainly a confirmation that persisted state is used. + + // Example: ensure theme is applied if it was loaded + // This is already handled by App.vue's watcher, but could be centralized. + // if (state.theme === 'dark') { + // document.documentElement.classList.add('dark'); + // } else { + // document.documentElement.classList.remove('dark'); + // } + console.log("Store state loaded/initialized."); + // It's good practice for actions to return a Promise if they are async + // or if other parts of the app expect to chain .then() + return Promise.resolve(); // Resolve immediately + }, + saveState({ state }) { + StorageService.saveState({ + players: state.players.map(p => ({ // Persist mqttChar for players + id: p.id, name: p.name, avatar: p.avatar, + initialTimerSec: p.initialTimerSec, currentTimerSec: p.currentTimerSec, + hotkey: p.hotkey, mqttChar: p.mqttChar, // Save mqttChar + isSkipped: p.isSkipped, + })), + globalHotkeyStopPause: state.globalHotkeyStopPause, + globalHotkeyRunAll: state.globalHotkeyRunAll, + globalMqttStopPause: state.globalMqttStopPause, // Save + globalMqttRunAll: state.globalMqttRunAll, // Save + mqttBrokerUrl: state.mqttBrokerUrl, // Save + currentPlayerIndex: state.currentPlayerIndex, + gameMode: state.gameMode, + isMuted: state.isMuted, + theme: state.theme, + }); + }, + addPlayer({ commit, dispatch }, player) { + commit('ADD_PLAYER', player); + dispatch('saveState'); + }, + setMqttBrokerUrl({ commit, dispatch }, url) { + commit('SET_MQTT_BROKER_URL', url); + dispatch('saveState'); + }, + setGlobalMqttStopPause({ commit, dispatch }, char) { + commit('SET_GLOBAL_MQTT_STOP_PAUSE', char); + dispatch('saveState'); + }, + setGlobalMqttRunAll({ commit, dispatch }, char) { + commit('SET_GLOBAL_MQTT_RUN_ALL', char); + dispatch('saveState'); + }, + updatePlayer({ commit, dispatch }, player) { + commit('UPDATE_PLAYER', player); + dispatch('saveState'); + }, + deletePlayer({ commit, dispatch }, playerId) { + commit('DELETE_PLAYER', playerId); + dispatch('saveState'); + }, + reorderPlayers({commit, dispatch}, players) { + commit('REORDER_PLAYERS', players); + dispatch('saveState'); + }, + shufflePlayers({commit, dispatch}) { + commit('SHUFFLE_PLAYERS'); + dispatch('saveState'); + }, + reversePlayers({commit, dispatch}) { + commit('REVERSE_PLAYERS'); + dispatch('saveState'); + }, + toggleTheme({ commit, dispatch }) { + commit('TOGGLE_THEME'); + dispatch('saveState'); + }, + setGlobalHotkey({ commit, dispatch }, key) { + commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', key); + dispatch('saveState'); + }, + setMuted({ commit, dispatch }, muted) { + commit('SET_IS_MUTED', muted); + dispatch('saveState'); + }, + resetGame({ commit, dispatch }) { // This is for resetting timers during a game session + commit('RESET_ALL_TIMERS'); + dispatch('saveState'); + }, + setGlobalHotkeyStopPause({ commit, dispatch }, key) { // <-- New action + commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', key); + dispatch('saveState'); + }, + setGlobalHotkeyRunAll({ commit, dispatch }, key) { // <-- New action + commit('SET_GLOBAL_HOTKEY_RUN_ALL', key); + dispatch('saveState'); + }, + fullResetApp({ commit, dispatch, state: currentGlobalState }) { + StorageService.clearState(); + const freshInitialState = JSON.parse(JSON.stringify(initialState)); + freshInitialState.players = freshInitialState.players.map(p => createPlayerObject(p)); + + + commit('SET_PLAYERS', freshInitialState.players); + commit('SET_CURRENT_PLAYER_INDEX', freshInitialState.currentPlayerIndex); + commit('SET_GAME_MODE', freshInitialState.gameMode); + commit('SET_IS_MUTED', freshInitialState.isMuted); + commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', freshInitialState.globalHotkeyStopPause); + commit('SET_GLOBAL_HOTKEY_RUN_ALL', freshInitialState.globalHotkeyRunAll); + commit('SET_MQTT_BROKER_URL', freshInitialState.mqttBrokerUrl); // Reset + commit('SET_GLOBAL_MQTT_STOP_PAUSE', freshInitialState.globalMqttStopPause); // Reset + commit('SET_GLOBAL_MQTT_RUN_ALL', freshInitialState.globalMqttRunAll); // Reset + + if (currentGlobalState.theme !== freshInitialState.theme) { + commit('SET_THEME', freshInitialState.theme); + } + commit('SET_GAME_RUNNING', false); + dispatch('saveState'); + }, + tick({ commit, state }) { + if (state.gameMode === 'normal') { + if (state.players[state.currentPlayerIndex]?.isTimerRunning) { + commit('DECREMENT_TIMER', { playerIndex: state.currentPlayerIndex }); + } + } else if (state.gameMode === 'allTimers') { + state.players.forEach((player, index) => { + if (player.isTimerRunning) { + commit('DECREMENT_TIMER', { playerIndex: index }); + } + }); + } + }, + passTurn({ commit, state, dispatch }) { + const numPlayers = state.players.length; + if (numPlayers === 0) return; + + const currentIdx = state.currentPlayerIndex; + const currentPlayerTimerWasRunning = state.players[currentIdx]?.isTimerRunning; + + commit('PAUSE_PLAYER_TIMER', currentIdx); + + let nextPlayerIndex = (currentIdx + 1) % numPlayers; + let skippedCount = 0; + while(state.players[nextPlayerIndex]?.isSkipped && skippedCount < numPlayers) { + nextPlayerIndex = (nextPlayerIndex + 1) % numPlayers; + skippedCount++; + } + + if (skippedCount === numPlayers) { + commit('PAUSE_ALL_TIMERS'); + dispatch('saveState'); + return; + } + + commit('SET_CURRENT_PLAYER_INDEX', nextPlayerIndex); + + if (currentPlayerTimerWasRunning && !state.players[nextPlayerIndex].isSkipped) { + commit('START_PLAYER_TIMER', nextPlayerIndex); + } else { + if (state.players[nextPlayerIndex] && !state.players[nextPlayerIndex].isSkipped) { + commit('PAUSE_PLAYER_TIMER', nextPlayerIndex); + } + } + + dispatch('saveState'); + }, + toggleCurrentPlayerTimerNormalMode({ commit, state, dispatch }) { + const player = state.players[state.currentPlayerIndex]; + if (!player) return; + + if (player.isTimerRunning) { + commit('PAUSE_PLAYER_TIMER', state.currentPlayerIndex); + } else if (!player.isSkipped) { + commit('START_PLAYER_TIMER', state.currentPlayerIndex); + } + dispatch('saveState'); + }, + togglePlayerTimerAllTimersMode({ commit, state, dispatch }, playerIndex) { + const player = state.players[playerIndex]; + if (!player) return; + + if (player.isTimerRunning) { + commit('PAUSE_PLAYER_TIMER', playerIndex); + } else if (!player.isSkipped) { + commit('START_PLAYER_TIMER', playerIndex); + } + + const anyTimerRunning = state.players.some(p => p.isTimerRunning && !p.isSkipped); + if (!anyTimerRunning && state.players.length > 0 && state.gameMode === 'allTimers') { + // This auto-revert logic is now in GameView.vue watcher for better control over timing + } + dispatch('saveState'); + }, + globalStopPauseAll({ commit, state, dispatch }) { + if (state.gameMode === 'normal') { + dispatch('toggleCurrentPlayerTimerNormalMode'); + } else { + const anyTimerRunning = state.players.some(p => p.isTimerRunning && !p.isSkipped); + if (anyTimerRunning) { + commit('PAUSE_ALL_TIMERS'); + } else { + state.players.forEach((player, index) => { + if (!player.isSkipped) { + commit('START_PLAYER_TIMER', index); + } + }); + } + } + dispatch('saveState'); + }, + switchToAllTimersMode({ commit, state, dispatch }) { + commit('SET_GAME_MODE', 'allTimers'); + let anyStarted = false; + state.players.forEach((player, index) => { + if (!player.isSkipped) { + commit('START_PLAYER_TIMER', index); + anyStarted = true; + } + }); + if(anyStarted) commit('SET_GAME_RUNNING', true); + else commit('SET_GAME_RUNNING', false); + dispatch('saveState'); + }, + switchToNormalMode({commit, state, dispatch}) { + commit('PAUSE_ALL_TIMERS'); + commit('SET_GAME_MODE', 'normal'); + // Determine current player for normal mode, respecting skips + let currentIdx = state.currentPlayerIndex; + let skippedCount = 0; + while(state.players[currentIdx]?.isSkipped && skippedCount < state.players.length) { + currentIdx = (currentIdx + 1) % state.players.length; + skippedCount++; + } + if (skippedCount < state.players.length) { + commit('SET_CURRENT_PLAYER_INDEX', currentIdx); + // Timer for this player should remain paused as per PAUSE_ALL_TIMERS + } else { + // All players skipped, game is effectively paused. + commit('SET_GAME_RUNNING', false); + } + dispatch('saveState'); + } + }, + getters: { + players: state => state.players, + currentPlayer: state => state.players[state.currentPlayerIndex], + nextPlayer: state => { + if (!state.players || state.players.length < 1) return null; + let nextIndex = (state.currentPlayerIndex + 1) % state.players.length; + let count = 0; + while(state.players[nextIndex]?.isSkipped && count < state.players.length) { + nextIndex = (nextIndex + 1) % state.players.length; + count++; + } + return state.players[nextIndex]; + }, + getPlayerById: (state) => (id) => state.players.find(p => p.id === id), + gameMode: state => state.gameMode, + isMuted: state => state.isMuted, + theme: state => state.theme, + globalHotkeyStopPause: state => state.globalHotkeyStopPause, + globalHotkeyRunAll: state => state.globalHotkeyRunAll, + mqttBrokerUrl: state => state.mqttBrokerUrl, + globalMqttStopPause: state => state.globalMqttStopPause, + globalMqttRunAll: state => state.globalMqttRunAll, + totalPlayers: state => state.players.length, + gameRunning: state => state.gameRunning, + maxNegativeTimeReached: () => MAX_NEGATIVE_SECONDS, + } +}); \ No newline at end of file diff --git a/src/sw.js b/src/sw.js new file mode 100644 index 0000000..0cc7aac --- /dev/null +++ b/src/sw.js @@ -0,0 +1,150 @@ +// This global constant __APP_CACHE_VERSION__ will be replaced by Vite +// during the build process due to the `define` config in vite.config.js. +const CACHE_VERSION = typeof __APP_CACHE_VERSION__ !== 'undefined' + ? __APP_CACHE_VERSION__ + : 'nexus-timer-cache-fallback-dev-vManual'; // Fallback for dev or if define fails + +const APP_SHELL_URLS = [ + // Note: '/' (index.html) is handled by NetworkFirst strategy, no need to precache explicitly here. + '/manifest.json', // Will be served from public, copied to dist root + '/favicon.ico', // Will be served from public, copied to dist root + // Icons from public/icons, will be copied to dist/icons + '/icons/icon-192x192.png', + '/icons/icon-512x512.png', + '/icons/maskable-icon-192x192.png', + '/icons/maskable-icon-512x512.png', + '/icons/shortcut-setup-96x96.png', + '/icons/shortcut-info-96x96.png', + // Any other critical static assets from the public folder that should be part of the app shell +]; + +self.addEventListener('install', event => { + console.log(`[SW ${CACHE_VERSION}] Install`); + event.waitUntil( + caches.open(CACHE_VERSION) + .then(cache => { + console.log(`[SW ${CACHE_VERSION}] Caching app shell essentials`); + return cache.addAll(APP_SHELL_URLS); + }) + .then(() => { + console.log(`[SW ${CACHE_VERSION}] Skip waiting on install.`); + return self.skipWaiting(); + }) + ); +}); + +self.addEventListener('activate', event => { + console.log(`[SW ${CACHE_VERSION}] Activate`); + event.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheName !== CACHE_VERSION) { + console.log(`[SW ${CACHE_VERSION}] Deleting old cache: ${cacheName}`); + return caches.delete(cacheName); + } + }) + ); + }).then(() => { + console.log(`[SW ${CACHE_VERSION}] Clients claimed.`); + return self.clients.claim(); + }) + ); +}); + +self.addEventListener('fetch', event => { + const { request } = event; + const url = new URL(request.url); + + if (request.method !== 'GET' || !url.protocol.startsWith('http')) { + // console.log(`[SW ${CACHE_VERSION}] Ignoring non-GET or non-http(s) request: ${request.url}`); + return; + } + + // Strategy 1: Network First for HTML (navigations or direct / request) + if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) { + // console.log(`[SW ${CACHE_VERSION}] NetworkFirst for: ${request.url}`); + event.respondWith( + fetch(request) + .then(response => { + if (response.ok) { + const responseClone = response.clone(); + caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); + } + return response; + }) + .catch(async () => { + // console.warn(`[SW ${CACHE_VERSION}] Network fetch failed for ${request.url}, trying cache.`); + const cachedResponse = await caches.match(request); + if (cachedResponse) return cachedResponse; + // Fallback to root /index.html from cache if specific page not found offline + const rootCache = await caches.match('/'); + if (rootCache) return rootCache; + // console.error(`[SW ${CACHE_VERSION}] Network and cache miss for navigation: ${request.url}`); + return new Response('Network error: You are offline and this page is not cached.', { + status: 404, + statusText: 'Not Found', + headers: { 'Content-Type': 'text/html' } // Important for SPA offline fallback + }); + }) + ); + return; + } + + // Strategy 2: Stale-While-Revalidate for assets (CSS, JS, images, fonts) + if (request.destination === 'style' || + request.destination === 'script' || + request.destination === 'worker' || + request.destination === 'image' || + request.destination === 'font') { + // console.log(`[SW ${CACHE_VERSION}] StaleWhileRevalidate for: ${request.url}`); + event.respondWith( + caches.open(CACHE_VERSION).then(cache => { + return cache.match(request).then(cachedResponse => { + const fetchPromise = fetch(request).then(networkResponse => { + if (networkResponse.ok) { + const responseToCache = networkResponse.clone(); + cache.put(request, responseToCache); + } else { + // console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch for ${request.url} failed with status ${networkResponse.status}`); + } + return networkResponse; + }).catch(err => { + // console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch error for ${request.url}:`, err); + // If fetch fails, and we already served from cache, that's okay. + // If cache also missed (i.e., cachedResponse was null), then this error will propagate. + throw err; + }); + return cachedResponse || fetchPromise; + }).catch(err => { + // This catch block handles errors from cache.match() or if fetchPromise was returned and rejected + // console.error(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Error for ${request.url}. Trying network fallback.`, err); + return fetch(request); // Final fallback to network if cache interactions fail + }); + }) + ); + return; + } + + // Strategy 3: Cache First for other types of requests (e.g., manifest.json if not in APP_SHELL_URLS) + // console.log(`[SW ${CACHE_VERSION}] CacheFirst for: ${request.url}`); + event.respondWith( + caches.match(request) + .then(response => { + return response || fetch(request).then(networkResponse => { + if(networkResponse.ok) { + const responseClone = networkResponse.clone(); + caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); + } + return networkResponse; + }); + }) + ); +}); + +self.addEventListener('message', event => { + if (event.data && event.data.action === 'skipWaiting') { + console.log(`[SW ${CACHE_VERSION}] Received skipWaiting message, activating new SW.`); + self.skipWaiting(); + } +}); \ No newline at end of file diff --git a/src/utils/timeFormatter.js b/src/utils/timeFormatter.js new file mode 100644 index 0000000..e6d5300 --- /dev/null +++ b/src/utils/timeFormatter.js @@ -0,0 +1,32 @@ +export function formatTime(totalSeconds) { + const isNegative = totalSeconds < 0; + if (isNegative) { + totalSeconds = -totalSeconds; + } + + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + const paddedMinutes = String(minutes).padStart(2, '0'); + const paddedSeconds = String(seconds).padStart(2, '0'); + + return `${isNegative ? '-' : ''}${paddedMinutes}:${paddedSeconds}`; + } + + export function parseTime(timeString) { // MM:SS or -MM:SS + if (!timeString || typeof timeString !== 'string') return 0; + const isNegative = timeString.startsWith('-'); + if (isNegative) { + timeString = timeString.substring(1); + } + const parts = timeString.split(':'); + if (parts.length !== 2) return 0; + + const minutes = parseInt(parts[0], 10); + const seconds = parseInt(parts[1], 10); + + if (isNaN(minutes) || isNaN(seconds)) return 0; + + let totalSeconds = (minutes * 60) + seconds; + return isNegative ? -totalSeconds : totalSeconds; + } \ No newline at end of file diff --git a/src/views/GameView.vue b/src/views/GameView.vue new file mode 100644 index 0000000..172c53f --- /dev/null +++ b/src/views/GameView.vue @@ -0,0 +1,263 @@ + + + \ No newline at end of file diff --git a/src/views/InfoView.vue b/src/views/InfoView.vue new file mode 100644 index 0000000..b7cce85 --- /dev/null +++ b/src/views/InfoView.vue @@ -0,0 +1,127 @@ + + + \ No newline at end of file diff --git a/src/views/SetupView.vue b/src/views/SetupView.vue new file mode 100644 index 0000000..8fbcb8d --- /dev/null +++ b/src/views/SetupView.vue @@ -0,0 +1,362 @@ + + + \ No newline at end of file diff --git a/systemd/virt-nexus-timer.service b/systemd/virt-nexus-timer.service new file mode 100644 index 0000000..f977e26 --- /dev/null +++ b/systemd/virt-nexus-timer.service @@ -0,0 +1,28 @@ +[Unit] +Description=nexus-timer (virt-nexus-timer) +Requires=docker.service +After=network.target docker.service +DefaultDependencies=no + +[Service] +Type=simple +Environment="HOME=/root" +ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker stop -t 3 virt-nexus-timer 2>/dev/null || true' +ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-nexus-timer 2>/dev/null || true' + +ExecStart=/usr/bin/env docker run \ + --rm \ + --name=virt-nexus-timer \ + --network=traefik \ + --label-file /virt/nexus-timer/labels \ + virt-nexus-timer + +ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker stop -t 3 virt-nexus-timer 2>/dev/null || true' +ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-nexus-timer 2>/dev/null || true' + +Restart=always +RestartSec=30 +SyslogIdentifier=virt-nexus-timer + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd/webhook.service b/systemd/webhook.service new file mode 100644 index 0000000..59c0ad9 --- /dev/null +++ b/systemd/webhook.service @@ -0,0 +1,35 @@ +[Unit] +Description=Small server for creating HTTP endpoints (hooks) +Documentation=https://github.com/adnanh/webhook/ +After=network-online.target +Wants=network-online.target +ConditionPathExists=/etc/webhook.conf + + +[Service] +Type=simple + +# Clear any existing ExecStart from a base unit file, if any +ExecStart= +# Path to webhook, IP, port, path to hooks config, verbose logging, hot-reloading config +ExecStart=/usr/bin/webhook -ip 10.0.0.1 -port 9000 -verbose -nopanic -hooks /etc/webhook.conf + +# --- Security & User (Recommended) --- +# 1. Create a dedicated user: +# sudo useradd --system --no-create-home --shell /bin/false webhooksvc +# 2. Ensure this user can read /etc/webhook.conf and execute redeploy.sh +# sudo chown webhooksvc:webhooksvc /etc/webhook.conf && sudo chmod 640 /etc/webhook.conf +# Also grant sudo rights for systemctl restart as mentioned in Step 3.1. +# Uncomment and use if you created the 'webhooksvc' user: +# User=webhooksvc +# Group=webhooksvc +# If running as root (less secure), leave User/Group commented. + +Restart=on-failure +RestartSec=5s +TimeoutStopSec=30s +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..a7eeb71 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,29 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + darkMode: 'class', // or 'media' or 'class' + theme: { + extend: { + animation: { + pulsePositive: 'pulsePositive 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', + pulseNegative: 'pulseNegative 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', + }, + keyframes: { + pulsePositive: { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '.7' }, + }, + pulseNegative: { // For text, maybe a color change or slight scale + '0%, 100%': { opacity: '1', transform: 'scale(1)' }, + '50%': { opacity: '.8', transform: 'scale(1.02)' }, + } + } + }, + }, + plugins: [ + require('@tailwindcss/typography'), // Add this line + ], + } \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..d08cf7f --- /dev/null +++ b/vite.config.js @@ -0,0 +1,61 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import fs from 'fs'; +import { resolve } from 'path'; + +const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +const appVersion = packageJson.version; + +const now = new Date(); +const dateTimeFormat = new Intl.DateTimeFormat('sk-SK', { + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + hour12: false +}); + +const parts = dateTimeFormat.formatToParts(now); +let day = '', month = '', year = '', hour = '', minute = '', second = ''; + +parts.forEach(part => { + switch (part.type) { + case 'day': day = part.value; break; + case 'month': month = part.value; break; + case 'year': year = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + } +}); + +// Assemble the date part without unwanted spaces +const appBuildTime = `${day}.${month}.${year} ${hour}:${minute}:${second}`; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 8080 + }, + define: { + 'import.meta.env.VITE_APP_BUILD_TIME': JSON.stringify(appBuildTime), + '__APP_CACHE_VERSION__': JSON.stringify(`nexus-timer-cache-v${appVersion}-${Date.now()}`) + }, + build: { + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + sw: resolve(__dirname, 'src/sw.js') + }, + output: { + entryFileNames: assetInfo => { + if (assetInfo.name === 'sw') { + return 'service-worker.js'; + } + return 'assets/[name]-[hash].js'; + }, + chunkFileNames: 'assets/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash].[ext]', + } + }, + emptyOutDir: true, + } +}); \ No newline at end of file