From 6648cd347da29ada90dc97eaf2b833f86b7b7478 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 19:59:04 -0500 Subject: [PATCH 01/14] Add root Makefile with all required targets Adds test, lint, fmt, fmt-check, check, docker, and hooks targets delegating to the Go backend and JS frontend sub-projects. Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a931fb6 --- /dev/null +++ b/Makefile @@ -0,0 +1,90 @@ +.PHONY: test lint fmt fmt-check check docker hooks build run dev clean \ + test-go test-js lint-go lint-js fmt-go fmt-js fmt-check-go fmt-check-js \ + build-go build-js + +GODIR := backend-go +JSDIR := frontend +PRETTIER := $(JSDIR)/node_modules/.bin/prettier + +# ── required targets ──────────────────────────────────────────── + +all: check + +check: test lint fmt-check + +test: test-go test-js + +lint: lint-go lint-js + +fmt: fmt-go fmt-js + +fmt-check: fmt-check-go fmt-check-js + +# ── go ────────────────────────────────────────────────────────── + +test-go: + cd $(GODIR) && go test -timeout 30s ./... + +lint-go: + cd $(GODIR) && golangci-lint run ./... + +fmt-go: + cd $(GODIR) && gofmt -w . + +fmt-check-go: + @diff=$$(cd $(GODIR) && gofmt -l .); \ + if [ -n "$$diff" ]; then \ + echo "Go files need formatting:"; \ + echo "$$diff"; \ + exit 1; \ + fi + +# ── js / frontend ─────────────────────────────────────────────── + +test-js: + cd $(JSDIR) && npm test + +lint-js: + $(PRETTIER) --check '**/*.{js,jsx,ts,tsx,css,md,json}' + +fmt-js: + $(PRETTIER) --write '**/*.{js,jsx,ts,tsx,css,md,json}' + +fmt-check-js: + $(PRETTIER) --check '**/*.{js,jsx,ts,tsx,css,md,json}' + +# ── build ─────────────────────────────────────────────────────── + +build: build-go build-js + +build-go: + cd $(GODIR) && go build -o bin/api ./cmd/api + cd $(GODIR) && go build -o bin/poller ./cmd/poller + +build-js: + cd $(JSDIR) && npm run build + +# ── dev / run ─────────────────────────────────────────────────── + +run: + cd $(GODIR) && go run ./cmd/api + +dev: + cd $(GODIR) && go run ./cmd/api + +# ── docker ────────────────────────────────────────────────────── + +docker: + docker build -t koin-ping . + +# ── hooks ─────────────────────────────────────────────────────── + +hooks: + printf '#!/bin/sh\nmake check\n' > .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + +# ── clean ─────────────────────────────────────────────────────── + +clean: + rm -rf $(GODIR)/bin/ + rm -rf $(JSDIR)/dist/ From 21ae859d1c33b486501ad96d823750d6cfa02269 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 19:59:22 -0500 Subject: [PATCH 02/14] Add .editorconfig Enforces 4-space indentation (tabs for Go and Makefiles), LF line endings, UTF-8 encoding, and trailing-whitespace trimming. Co-Authored-By: Claude Sonnet 4.6 --- .editorconfig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..92ec261 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[Makefile] +indent_style = tab + +[*.go] +indent_style = tab From 2fcbcf5b54b75ae18b88dbb0262eb5bf116d39a5 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:00:15 -0500 Subject: [PATCH 03/14] Update .gitignore to be comprehensive Adds OS files, editor artifacts, Node modules, secrets, Go build artifacts, and frontend build output to the root gitignore. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b10a6a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# OS +.DS_Store +Thumbs.db + +# Editors +*.swp +*.swo +*~ +*.bak +.idea/ +.vscode/ +*.sublime-* + +# Node +node_modules/ + +# Environment / secrets +.env +.env.* +*.pem +*.key + +# Go build artifacts +backend-go/bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +vendor/ +tmp/ + +# Frontend build output +frontend/dist/ +frontend/build/ +frontend/.vite/ + +# Prompts directory (local tooling, not part of the project) +Prompts/ + +# Logs +*.log From 5c721ca2718227484629c34613bee6942d7a49c4 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:01:31 -0500 Subject: [PATCH 04/14] Add REPO_POLICIES.md and LICENSE REPO_POLICIES.md is copied from the canonical upstream source (last_modified: 2026-02-22). LICENSE is MIT, copyright 2026 Steven Jannette. Co-Authored-By: Claude Sonnet 4.6 --- LICENSE | 21 ++++++ REPO_POLICIES.md | 188 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 LICENSE create mode 100644 REPO_POLICIES.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b61193 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Steven Jannette + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/REPO_POLICIES.md b/REPO_POLICIES.md new file mode 100644 index 0000000..a024cbd --- /dev/null +++ b/REPO_POLICIES.md @@ -0,0 +1,188 @@ +--- +title: Repository Policies +last_modified: 2026-02-22 +--- + +This document covers repository structure, tooling, and workflow standards. Code +style conventions are in separate documents: + +- [Code Styleguide](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE.md) + (general, bash, Docker) +- [Go](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_GO.md) +- [JavaScript](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_JS.md) +- [Python](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_PYTHON.md) +- [Go HTTP Server Conventions](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/GO_HTTP_SERVER_CONVENTIONS.md) + +--- + +- Cross-project documentation (such as this file) must include + `last_modified: YYYY-MM-DD` in the YAML front matter so it can be kept in sync + with the authoritative source as policies evolve. + +- **ALL external references must be pinned by cryptographic hash.** This + includes Docker base images, Go modules, npm packages, GitHub Actions, and + anything else fetched from a remote source. Version tags (`@v4`, `@latest`, + `:3.21`, etc.) are server-mutable and therefore remote code execution + vulnerabilities. The ONLY acceptable way to reference an external dependency + is by its content hash (Docker `@sha256:...`, Go module hash in `go.sum`, npm + integrity hash in lockfile, GitHub Actions `@`). No exceptions. + This also means never `curl | bash` to install tools like pyenv, nvm, rustup, + etc. Instead, download a specific release archive from GitHub, verify its hash + (hardcoded in the Dockerfile or script), and only then install. Unverified + install scripts are arbitrary remote code execution. This is the single most + important rule in this document. Double-check every external reference in + every file before committing. There are zero exceptions to this rule. + +- Every repo with software must have a root `Makefile` with these targets: + `make test`, `make lint`, `make fmt` (writes), `make fmt-check` (read-only), + `make check` (prereqs: `test`, `lint`, `fmt-check`), `make docker`, and + `make hooks` (installs pre-commit hook). A model Makefile is at + `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`. + +- Always use Makefile targets (`make fmt`, `make test`, `make lint`, etc.) + instead of invoking the underlying tools directly. The Makefile is the single + source of truth for how these operations are run. + +- The Makefile is authoritative documentation for how the repo is used. Beyond + the required targets above, it should have targets for every common operation: + running a local development server (`make run`, `make dev`), re-initializing + or migrating the database (`make db-reset`, `make migrate`), building + artifacts (`make build`), generating code, seeding data, or anything else a + developer would do regularly. If someone checks out the repo and types + `make`, they should see every meaningful operation available. A new + contributor should be able to understand the entire development workflow by + reading the Makefile. + +- Every repo should have a `Dockerfile`. All Dockerfiles must run `make check` + as a build step so the build fails if the branch is not green. For non-server + repos, the Dockerfile should bring up a development environment and run + `make check`. For server repos, `make check` should run as an early build + stage before the final image is assembled. + +- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that + runs `docker build .` on push. Since the Dockerfile already runs `make check`, + a successful build implies all checks pass. + +- Use platform-standard formatters: `black` for Python, `prettier` for + JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with + two exceptions: four-space indents (except Go), and `proseWrap: always` for + Markdown (hard-wrap at 80 columns). Documentation and writing repos (Markdown, + HTML, CSS) should also have `.prettierrc` and `.prettierignore`. + +- Pre-commit hook: `make check` if local testing is possible, otherwise + `make lint && make fmt-check`. The Makefile should provide a `make hooks` + target to install the pre-commit hook. + +- All repos with software must have tests that run via the platform-standard + test framework (`go test`, `pytest`, `jest`/`vitest`, etc.). If no meaningful + tests exist yet, add the most minimal test possible — e.g. importing the + module under test to verify it compiles/parses. There is no excuse for + `make test` to be a no-op. + +- `make test` must complete in under 20 seconds. Add a 30-second timeout in the + Makefile. + +- Docker builds must complete in under 5 minutes. + +- `make check` must not modify any files in the repo. Tests may use temporary + directories. + +- `main` must always pass `make check`, no exceptions. + +- Never commit secrets. `.env` files, credentials, API keys, and private keys + must be in `.gitignore`. No exceptions. + +- `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`), + editor files (`.swp`, `*~`), language build artifacts, and `node_modules/`. + Fetch the standard `.gitignore` from + `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up + a new repo. + +- Never use `git add -A` or `git add .`. Always stage files explicitly by name. + +- Never force-push to `main`. + +- Make all changes on a feature branch. You can do whatever you want on a + feature branch. + +- `.golangci.yml` is standardized and must _NEVER_ be modified by an agent, only + manually by the user. Fetch from + `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`. + +- When pinning images or packages by hash, add a comment above the reference + with the version and date (YYYY-MM-DD). + +- Use `yarn`, not `npm`. + +- Write all dates as YYYY-MM-DD (ISO 8601). + +- Simple projects should be configured with environment variables. + +- Dockerized web services listen on port 8080 by default, overridable with + `PORT`. + +- `README.md` is the primary documentation. Required sections: + - **Description**: First line must include the project name, purpose, + category (web server, SPA, CLI tool, etc.), license, and author. Example: + "µPaaS is an MIT-licensed Go web application by @sneak that receives + git-frontend webhooks and deploys applications via Docker in realtime." + - **Getting Started**: Copy-pasteable install/usage code block. + - **Rationale**: Why does this exist? + - **Design**: How is the program structured? + - **TODO**: Update meticulously, even between commits. When planning, put + the todo list in the README so a new agent can pick up where the last one + left off. + - **License**: MIT, GPL, or WTFPL. Ask the user for new projects. Include a + `LICENSE` file in the repo root and a License section in the README. + - **Author**: [@sneak](https://sneak.berlin). + +- First commit of a new repo should contain only `README.md`. + +- Go module root: `sneak.berlin/go/`. Always run `go mod tidy` before + committing. + +- Use SemVer. + +- Database migrations live in `internal/db/migrations/` and must be embedded in + the binary. + - `000_migration.sql` — contains ONLY the creation of the migrations tracking + table itself. Nothing else. + - `001_schema.sql` — the full application schema. + - **Pre-1.0.0:** never add additional migration files (002, 003, etc.). There + is no installed base to migrate. Edit `001_schema.sql` directly. + - **Post-1.0.0:** add new numbered migration files for each schema change. + Never edit existing migrations after release. + +- All repos should have an `.editorconfig` enforcing the project's indentation + settings. + +- Avoid putting files in the repo root unless necessary. Root should contain + only project-level config files (`README.md`, `Makefile`, `Dockerfile`, + `LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and + language-specific config). Everything else goes in a subdirectory. Canonical + subdirectory names: + - `bin/` — executable scripts and tools + - `cmd/` — Go command entrypoints + - `configs/` — configuration templates and examples + - `deploy/` — deployment manifests (k8s, compose, terraform) + - `docs/` — documentation and markdown (README.md stays in root) + - `internal/` — Go internal packages + - `internal/db/migrations/` — database migrations + - `pkg/` — Go library packages + - `share/` — systemd units, data files + - `static/` — static assets (images, fonts, etc.) + - `web/` — web frontend source + +- When setting up a new repo, files from the `prompts` repo may be used as + templates. Fetch them from + `https://git.eeqj.de/sneak/prompts/raw/branch/main/`. + +- New repos must contain at minimum: + - `README.md`, `.git`, `.gitignore`, `.editorconfig` + - `LICENSE`, `REPO_POLICIES.md` (copy from the `prompts` repo) + - `Makefile` + - `Dockerfile`, `.dockerignore` + - `.gitea/workflows/check.yml` + - Go: `go.mod`, `go.sum`, `.golangci.yml` + - JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore` + - Python: `pyproject.toml` From aac62bacf43c96194da8595bbe758e27a9a771ad Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:02:01 -0500 Subject: [PATCH 05/14] Update README with all required sections Adds Description (with project name, category, license, author), Getting Started, Rationale, Design, TODO, License, and Author sections per REPO_POLICIES.md requirements. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 183 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 105 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 02c30a7..7a4f46b 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,122 @@ # Koin Ping -A lightweight on-chain monitoring and alerting system designed to give users situational awareness over blockchain addresses they care about. - -## Overview - -Koin Ping is designed to reliably observe on-chain activity and notify users when predefined conditions are met. It does not execute transactions, manage wallets, or speculate on prices. - -## Project Structure - -``` -koin_ping/ -├── backend/ # Node.js + Express + PostgreSQL backend -│ ├── api/ # API endpoints and server configuration -│ ├── poller/ # Blockchain polling logic -│ ├── alerts/ # Alert evaluation and management -│ ├── notifications/ # Notification delivery system -│ ├── domain/ # Domain models and business logic -│ ├── infra/ # Infrastructure (database, external services) -│ └── shared/ # Shared utilities and helpers -│ -└── frontend/ # React + Vite frontend - ├── public/ # Static assets - └── src/ - ├── api/ # Frontend API calls to backend - ├── components/ # Reusable UI components - ├── pages/ # Top-level pages (views) - └── utils/ # Utility functions - -``` - -## Tech Stack - -### Backend -- **Runtime:** Node.js -- **Framework:** Express -- **Database:** PostgreSQL -- **Key Dependencies:** - - `pg` - PostgreSQL client - - `dotenv` - Environment variable management - - `cors` - CORS middleware - - `nodemon` - Development auto-reload - -### Frontend -- **Framework:** React 19 -- **Build Tool:** Vite -- **Language:** JavaScript/TypeScript (mixed) -- **Type Checking:** TypeScript +Koin Ping is an MIT-licensed blockchain monitoring system by Steven Jannette +that polls Ethereum addresses for on-chain activity and delivers real-time +alerts to users via a Go REST API backend and a React single-page application +frontend. ## Getting Started ### Prerequisites -- Node.js (v18 or higher recommended) -- PostgreSQL database -- npm or yarn -### Backend Setup +- Go 1.24+ +- Node.js 18+ and npm (or yarn — preferred per repo policy) +- PostgreSQL 15+ +- `golangci-lint` v2 (for `make lint`) -1. Navigate to the backend directory: - ```bash - cd backend - ``` +### Setup -2. Install dependencies (already done): - ```bash - npm install - ``` +```bash +# Clone and enter the repo +git clone +cd koin_ping_0.2.0 -3. Create a `.env` file (see backend/README.md for required variables) +# Install pre-commit hook +make hooks -4. Start the development server: - ```bash - npm run dev - ``` +# Install frontend dependencies +cd frontend && npm install && cd .. -The backend will run on `http://localhost:3001` +# Copy and fill in environment variables +cp backend-go/.env.example backend-go/.env +# edit backend-go/.env with your DATABASE_URL, FIREBASE_PROJECT_ID, ETH_RPC_URL -### Frontend Setup +# Run checks (requires golangci-lint) +make check -1. Navigate to the frontend directory: - ```bash - cd frontend - ``` +# Start the API server +make run -2. Install dependencies (already done): - ```bash - npm install - ``` +# Start the poller (separate terminal) +cd backend-go && go run ./cmd/poller -3. Start the development server: - ```bash - npm run dev - ``` +# Start the frontend dev server (separate terminal) +cd frontend && npm run dev +``` -The frontend will run on `http://localhost:3000` +The API listens on `http://localhost:3001` and the frontend on +`http://localhost:3000` by default. + +## Rationale + +Crypto users who hold or actively monitor addresses need a lightweight, +reliable way to know when on-chain activity occurs without polling block +explorers manually. Koin Ping fills that gap: it watches a set of Ethereum +addresses, evaluates configurable alert rules (incoming transactions, outgoing +transactions, large transfers, balance thresholds), and notifies the user +through Discord webhooks. + +## Design + +The system is split into two independently deployable processes and one +frontend: + +``` +koin_ping_0.2.0/ +├── backend-go/ # Go monorepo root +│ ├── cmd/api/ # HTTP REST API server +│ ├── cmd/poller/ # Blockchain polling daemon +│ └── internal/ +│ ├── config/ # Environment-based config loading +│ ├── database/ # pgx connection pool +│ ├── domain/ # Shared domain types +│ ├── firebase/ # Firebase auth client +│ ├── handlers/ # HTTP handler wiring +│ ├── middleware/ # Auth middleware +│ ├── models/ # SQL persistence layer +│ ├── notifications/ # Discord webhook delivery +│ ├── protocols/ethereum/ # Ethereum JSON-RPC client +│ ├── services/ # Observer and evaluator business logic +│ └── wei/ # ETH/Wei conversion utilities +└── frontend/ # React + Vite SPA + └── src/ + ├── api/ # Fetch wrappers for the REST API + ├── components/ # Reusable UI components + ├── contexts/ # React contexts (auth, etc.) + ├── firebase/ # Firebase SDK initialization + └── pages/ # Top-level route pages +``` + +**API server** (`cmd/api`): standard-library `net/http` with Firebase JWT +authentication middleware. Exposes CRUD endpoints for addresses, alert rules, +alert events, and notification configuration. + +**Poller** (`cmd/poller`): long-running daemon that polls Ethereum via +JSON-RPC, compares observed transactions against persisted alert rules, fires +alert events to the database, and dispatches Discord notifications. + +**Frontend** (`frontend/`): React 19 SPA built with Vite. Authenticates with +Firebase, communicates with the API via fetch, and renders the address/alert +management UI. + +## TODO + +- [ ] Switch frontend package manager from npm to yarn (per repo policy) +- [ ] Rename `go.mod` module path from `github.com/kjannette/koin-ping` to + `sneak.berlin/go/koin-ping` (per repo policy) +- [ ] Move database migrations to `internal/db/migrations/` and embed them + in the binary +- [ ] Pin all Docker base images by `@sha256` in Dockerfile +- [ ] Install `golangci-lint` locally and pass `make lint` +- [ ] Add vitest unit tests for the React frontend +- [ ] Add a `make db-reset` / `make migrate` target for schema management +- [ ] Set HTTP server read/write timeouts (currently unbounded) +- [ ] Replace `log.Printf` calls with structured `log/slog` + +## License + +MIT. See [LICENSE](LICENSE). + +## Author + +Steven Jannette From b283d0f835613d079c92a8d0848315d5b9f4e79d Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:02:16 -0500 Subject: [PATCH 06/14] Add backend-go/.golangci.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standard golangci-lint v2 config fetched from the canonical upstream source. Must not be modified by agents — only manually by the user. Co-Authored-By: Claude Sonnet 4.6 --- backend-go/.golangci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 backend-go/.golangci.yml diff --git a/backend-go/.golangci.yml b/backend-go/.golangci.yml new file mode 100644 index 0000000..34a8e31 --- /dev/null +++ b/backend-go/.golangci.yml @@ -0,0 +1,32 @@ +version: "2" + +run: + timeout: 5m + modules-download-mode: readonly + +linters: + default: all + disable: + # Genuinely incompatible with project patterns + - exhaustruct # Requires all struct fields + - depguard # Dependency allow/block lists + - godot # Requires comments to end with periods + - wsl # Deprecated, replaced by wsl_v5 + - wrapcheck # Too verbose for internal packages + - varnamelen # Short names like db, id are idiomatic Go + +linters-settings: + lll: + line-length: 88 + funlen: + lines: 80 + statements: 50 + cyclop: + max-complexity: 15 + dupl: + threshold: 100 + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 From 6901c759da1711c3ab5b672f43ec58df568eca8b Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:02:48 -0500 Subject: [PATCH 07/14] Add .prettierrc, .prettierignore; update frontend/package.json scripts - Root .prettierrc sets tabWidth=4 and proseWrap=always - Root .prettierignore excludes node_modules, build output, lockfiles - package.json: adds test (vite build), lint, fmt scripts; adds prettier as a devDependency; fixes typos in description and keywords Co-Authored-By: Claude Sonnet 4.6 --- .prettierignore | 6 ++++ .prettierrc | 4 +++ frontend/package.json | 66 +++++++++++++++++++++++-------------------- 3 files changed, 45 insertions(+), 31 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8ab4185 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules/ +frontend/dist/ +frontend/build/ +backend-go/bin/ +*.lock +Prompts/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8af31cd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "proseWrap": "always" +} diff --git a/frontend/package.json b/frontend/package.json index 902032e..0458137 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,32 +1,36 @@ { - "name": "frontend", - "version": "1.0.0", - "description": "koin_ping blockchain monoitor UI", - "main": "index.js", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "keywords": [ - "cryptocurrency", - "blockchain", - "blockchain event moitoring" - ], - "author": "Steven Jannette", - "license": "MIT", - "type": "module", - "dependencies": { - "firebase": "^12.7.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-router-dom": "^7.11.0" - }, - "devDependencies": { - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.2", - "typescript": "^5.9.3", - "vite": "^7.3.0" - } -} \ No newline at end of file + "name": "frontend", + "version": "1.0.0", + "description": "koin_ping blockchain monitor UI", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "vite build", + "lint": "prettier --check 'src/**/*.{js,jsx,ts,tsx,css}'", + "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css}'" + }, + "keywords": [ + "cryptocurrency", + "blockchain", + "blockchain event monitoring" + ], + "author": "Steven Jannette", + "license": "MIT", + "type": "module", + "dependencies": { + "firebase": "^12.7.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router-dom": "^7.11.0" + }, + "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "prettier": "^3.8.1", + "typescript": "^5.9.3", + "vite": "^7.3.0" + } +} From 4f6ed5dd20e897b4b317a2bca5bc0be0be053184 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:05:45 -0500 Subject: [PATCH 08/14] Add Dockerfile, .dockerignore, and Gitea Actions workflow Dockerfile: golang:1.24-alpine (pinned by sha256), installs Node.js, npm, golangci-lint (pinned by commit hash), and runs make check as a build step so the image only builds on a green branch. .gitea/workflows/check.yml: runs docker build . on push (actions/checkout pinned by commit sha). --- .dockerignore | 3 +++ .gitea/workflows/check.yml | 9 +++++++++ Dockerfile | 22 ++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/check.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5414d56 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +node_modules +.DS_Store diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml new file mode 100644 index 0000000..aca7a51 --- /dev/null +++ b/.gitea/workflows/check.yml @@ -0,0 +1,9 @@ +name: check +on: [push] +jobs: + check: + runs-on: ubuntu-latest + steps: + # actions/checkout v4.2.2, 2026-02-22 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - run: docker build . diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7b0f64a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# syntax=docker/dockerfile:1 + +# golang:1.24-alpine, 2025-02-28 +FROM golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 + +# Install system dependencies: Node.js, npm, make, git +RUN apk add --no-cache nodejs npm make git + +# Install golangci-lint at a pinned commit hash +# golangci-lint v2.10.1 (2025-02-17), commit 5d1e709b7be35cb2025444e19de266b056b7b7ee +RUN GOBIN=/usr/local/bin go install \ + github.com/golangci/golangci-lint/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee + +WORKDIR /app + +COPY . . + +# Install frontend dependencies +RUN cd frontend && npm ci + +# Run all checks — build fails if the branch is not green +RUN make check From a5d1bc171ce8b5a7ff9e120d404fafc376dab420 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:07:04 -0500 Subject: [PATCH 09/14] fmt: apply gofmt to Go source files Alignment of struct literal field values in config.go and domain/types.go. --- backend-go/internal/config/config.go | 22 +++++++++--------- backend-go/internal/domain/types.go | 34 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/backend-go/internal/config/config.go b/backend-go/internal/config/config.go index 29f969b..2ee27f6 100644 --- a/backend-go/internal/config/config.go +++ b/backend-go/internal/config/config.go @@ -24,18 +24,18 @@ type Config struct { func Load() (*Config, error) { cfg := &Config{ - Port: getEnvInt("PORT", 3001), - APIBasePath: getEnv("API_BASE_PATH", "/v1"), - DatabaseURL: os.Getenv("DATABASE_URL"), - DBHost: getEnv("DB_HOST", "localhost"), - DBPort: getEnvInt("DB_PORT", 5432), - DBUser: os.Getenv("DB_USER"), - DBPassword: os.Getenv("DB_PASSWORD"), - DBName: os.Getenv("DB_NAME"), + Port: getEnvInt("PORT", 3001), + APIBasePath: getEnv("API_BASE_PATH", "/v1"), + DatabaseURL: os.Getenv("DATABASE_URL"), + DBHost: getEnv("DB_HOST", "localhost"), + DBPort: getEnvInt("DB_PORT", 5432), + DBUser: os.Getenv("DB_USER"), + DBPassword: os.Getenv("DB_PASSWORD"), + DBName: os.Getenv("DB_NAME"), FirebaseProjectID: os.Getenv("FIREBASE_PROJECT_ID"), - EthRPCURL: os.Getenv("ETH_RPC_URL"), - PollIntervalMS: getEnvInt("POLL_INTERVAL_MS", 60000), - NodeEnv: getEnv("NODE_ENV", "development"), + EthRPCURL: os.Getenv("ETH_RPC_URL"), + PollIntervalMS: getEnvInt("POLL_INTERVAL_MS", 60000), + NodeEnv: getEnv("NODE_ENV", "development"), } if cfg.PollIntervalMS < 1000 { diff --git a/backend-go/internal/domain/types.go b/backend-go/internal/domain/types.go index d0ded03..8efd13e 100644 --- a/backend-go/internal/domain/types.go +++ b/backend-go/internal/domain/types.go @@ -59,12 +59,12 @@ type AlertRule struct { } type AlertEvent struct { - ID int `json:"id"` - AlertRuleID int `json:"alert_rule_id"` - Message string `json:"message"` - AddressLabel *string `json:"address_label"` - TxHash *string `json:"tx_hash"` - Timestamp time.Time `json:"timestamp"` + ID int `json:"id"` + AlertRuleID int `json:"alert_rule_id"` + Message string `json:"message"` + AddressLabel *string `json:"address_label"` + TxHash *string `json:"tx_hash"` + Timestamp time.Time `json:"timestamp"` } type AddressCheckpoint struct { @@ -82,23 +82,23 @@ type CheckpointDetail struct { } type NotificationConfig struct { - UserID string `json:"user_id"` - DiscordWebhookURL *string `json:"discord_webhook_url"` - TelegramChatID *string `json:"telegram_chat_id"` - TelegramBotToken *string `json:"telegram_bot_token,omitempty"` - Email *string `json:"email"` - NotificationEnabled bool `json:"notification_enabled"` + UserID string `json:"user_id"` + DiscordWebhookURL *string `json:"discord_webhook_url"` + TelegramChatID *string `json:"telegram_chat_id"` + TelegramBotToken *string `json:"telegram_bot_token,omitempty"` + Email *string `json:"email"` + NotificationEnabled bool `json:"notification_enabled"` CreatedAt *time.Time `json:"created_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } type NormalizedTx struct { - Hash string `json:"hash"` - From string `json:"from"` + Hash string `json:"hash"` + From string `json:"from"` To *string `json:"to"` - Value string `json:"value"` // Wei as string for precision - BlockNumber int `json:"block_number"` - BlockTimestamp int64 `json:"block_timestamp"` + Value string `json:"value"` // Wei as string for precision + BlockNumber int `json:"block_number"` + BlockTimestamp int64 `json:"block_timestamp"` } type Direction string From da8b45012a39fd3b661a445c174d617ead40f912 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:07:28 -0500 Subject: [PATCH 10/14] fmt: apply prettier to frontend JS/TS/CSS/JSON files Initial prettier pass with tabWidth=4. --- frontend/package-lock.json | 5767 ++++++++++++----------- frontend/src/App.jsx | 113 +- frontend/src/api/addresses.jsx | 171 +- frontend/src/api/alertEvents.jsx | 52 +- frontend/src/api/alerts.jsx | 216 +- frontend/src/api/authHeaders.js | 45 +- frontend/src/api/config.js | 2 +- frontend/src/api/notificationConfig.jsx | 134 +- frontend/src/api/status.jsx | 18 +- frontend/src/components/AddressForm.jsx | 64 +- frontend/src/components/AlertForm.jsx | 108 +- frontend/src/components/Button.jsx | 31 +- frontend/src/components/Input.jsx | 43 +- frontend/src/contexts/AuthContext.jsx | 161 +- frontend/src/firebase/config.js | 15 +- frontend/src/main.jsx | 14 +- frontend/src/pages/Addresses.jsx | 154 +- frontend/src/pages/AlertHistory.jsx | 130 +- frontend/src/pages/Alerts.jsx | 1031 ++-- frontend/src/pages/Login.jsx | 257 +- frontend/src/pages/Signup.jsx | 341 +- frontend/tsconfig.json | 41 +- frontend/vite.config.js | 25 +- 23 files changed, 4590 insertions(+), 4343 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 055bf76..612d8ea 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,2879 +1,2896 @@ { - "name": "frontend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "firebase": "^12.7.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-router-dom": "^7.11.0" - }, - "devDependencies": { - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.2", - "typescript": "^5.9.3", - "vite": "^7.3.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@firebase/ai": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.6.1.tgz", - "integrity": "sha512-qJd9bpABqsanFnwdbjZEDbKKr1jRtuUZ+cHyNBLWsxobH4pd73QncvuO3XlMq4eKBLlg1f5jNdFpJ3G3ABu2Tg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" - } - }, - "node_modules/@firebase/analytics": { - "version": "0.10.19", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", - "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/analytics-compat": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz", - "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/analytics": "0.10.19", - "@firebase/analytics-types": "0.8.3", - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/analytics-types": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", - "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", - "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/app-check": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", - "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/app-check-compat": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", - "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check": "0.11.0", - "@firebase/app-check-types": "0.5.3", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", - "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-check-types": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", - "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-compat": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", - "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/app": "0.14.6", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/app-types": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", - "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@firebase/auth": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz", - "integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@react-native-async-storage/async-storage": "^2.2.0" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "firebase": "^12.7.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router-dom": "^7.11.0" + }, + "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "prettier": "^3.8.1", + "typescript": "^5.9.3", + "vite": "^7.3.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@firebase/ai": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.6.1.tgz", + "integrity": "sha512-qJd9bpABqsanFnwdbjZEDbKKr1jRtuUZ+cHyNBLWsxobH4pd73QncvuO3XlMq4eKBLlg1f5jNdFpJ3G3ABu2Tg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.19", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", + "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz", + "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.19", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", + "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", + "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/app": "0.14.6", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@firebase/auth": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz", + "integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.2.tgz", + "integrity": "sha512-8UhCzF6pav9bw/eXA8Zy1QAKssPRYEYXaWagie1ewLTwHkXv6bKp/j6/IwzSYQP67sy/BMFXIFaCCsoXzFLr7A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.12.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.12.tgz", + "integrity": "sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.3.tgz", + "integrity": "sha512-RVuvhcQzs1sD5Osr2naQS71H0bQMbSnib16uOWAKk3GaKb/WBPyCYSr2Ry7MqlxDP/YhwknUxECL07lw9Rq1nA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.3.tgz", + "integrity": "sha512-1ylF/njF68Pmb6p0erP0U78XQv1w77Wap4bUmqZ7ZVkmN1oMgplyu0TyirWtCBoKFRV2+SUZfWXvIij/z39LYg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.3", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", + "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", + "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.1", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", + "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", + "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", + "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", + "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/firebase": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.7.0.tgz", + "integrity": "sha512-ZBZg9jFo8uH4Emd7caOqtalKJfDGHnHQSrCPiqRAdTFQd0wL3ERilUBfhnhBLnlernugkN/o7nJa0p+sE71Izg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.6.1", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-compat": "0.2.25", + "@firebase/app": "0.14.6", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.6", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.12.0", + "@firebase/auth-compat": "0.6.2", + "@firebase/data-connect": "0.3.12", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.3", + "@firebase/firestore-compat": "0.4.3", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", + "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz", + "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", + "license": "MIT", + "dependencies": { + "react-router": "7.11.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/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==", + "license": "MIT", + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } } - } - }, - "node_modules/@firebase/auth-compat": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.2.tgz", - "integrity": "sha512-8UhCzF6pav9bw/eXA8Zy1QAKssPRYEYXaWagie1ewLTwHkXv6bKp/j6/IwzSYQP67sy/BMFXIFaCCsoXzFLr7A==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/auth": "1.12.0", - "@firebase/auth-types": "0.13.0", - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/auth-interop-types": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", - "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/auth-types": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", - "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" - } - }, - "node_modules/@firebase/component": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", - "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/data-connect": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.12.tgz", - "integrity": "sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/database": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", - "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database-compat": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", - "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/database": "1.1.0", - "@firebase/database-types": "1.0.16", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database-types": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", - "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-types": "0.9.3", - "@firebase/util": "1.13.0" - } - }, - "node_modules/@firebase/firestore": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.3.tgz", - "integrity": "sha512-RVuvhcQzs1sD5Osr2naQS71H0bQMbSnib16uOWAKk3GaKb/WBPyCYSr2Ry7MqlxDP/YhwknUxECL07lw9Rq1nA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "@firebase/webchannel-wrapper": "1.0.5", - "@grpc/grpc-js": "~1.9.0", - "@grpc/proto-loader": "^0.7.8", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/firestore-compat": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.3.tgz", - "integrity": "sha512-1ylF/njF68Pmb6p0erP0U78XQv1w77Wap4bUmqZ7ZVkmN1oMgplyu0TyirWtCBoKFRV2+SUZfWXvIij/z39LYg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/firestore": "4.9.3", - "@firebase/firestore-types": "3.0.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/firestore-types": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", - "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" - } - }, - "node_modules/@firebase/functions": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", - "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", - "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/functions-compat": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", - "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/functions": "0.13.1", - "@firebase/functions-types": "0.6.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/functions-types": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", - "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/installations": { - "version": "0.6.19", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", - "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/installations-compat": { - "version": "0.2.19", - "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", - "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/installations-types": "0.5.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/installations-types": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", - "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x" - } - }, - "node_modules/@firebase/logger": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", - "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/messaging": { - "version": "0.12.23", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", - "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.13.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/messaging-compat": { - "version": "0.2.23", - "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", - "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/messaging": "0.12.23", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/messaging-interop-types": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", - "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/performance": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", - "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0", - "web-vitals": "^4.2.4" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/performance-compat": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", - "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/performance": "0.7.9", - "@firebase/performance-types": "0.2.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/performance-types": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", - "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/remote-config": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", - "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/remote-config-compat": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", - "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/remote-config": "0.7.0", - "@firebase/remote-config-types": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/remote-config-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", - "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/storage": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", - "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/storage-compat": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", - "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/storage": "0.14.0", - "@firebase/storage-types": "0.8.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/storage-types": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", - "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" - } - }, - "node_modules/@firebase/util": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", - "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/webchannel-wrapper": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", - "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", - "license": "Apache-2.0" - }, - "node_modules/@grpc/grpc-js": { - "version": "1.9.15", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", - "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.7.8", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.5", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "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" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", - "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" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "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==", - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/firebase": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.7.0.tgz", - "integrity": "sha512-ZBZg9jFo8uH4Emd7caOqtalKJfDGHnHQSrCPiqRAdTFQd0wL3ERilUBfhnhBLnlernugkN/o7nJa0p+sE71Izg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/ai": "2.6.1", - "@firebase/analytics": "0.10.19", - "@firebase/analytics-compat": "0.2.25", - "@firebase/app": "0.14.6", - "@firebase/app-check": "0.11.0", - "@firebase/app-check-compat": "0.4.0", - "@firebase/app-compat": "0.5.6", - "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.12.0", - "@firebase/auth-compat": "0.6.2", - "@firebase/data-connect": "0.3.12", - "@firebase/database": "1.1.0", - "@firebase/database-compat": "2.1.0", - "@firebase/firestore": "4.9.3", - "@firebase/firestore-compat": "0.4.3", - "@firebase/functions": "0.13.1", - "@firebase/functions-compat": "0.4.1", - "@firebase/installations": "0.6.19", - "@firebase/installations-compat": "0.2.19", - "@firebase/messaging": "0.12.23", - "@firebase/messaging-compat": "0.2.23", - "@firebase/performance": "0.7.9", - "@firebase/performance-compat": "0.2.22", - "@firebase/remote-config": "0.7.0", - "@firebase/remote-config-compat": "0.2.20", - "@firebase/storage": "0.14.0", - "@firebase/storage-compat": "0.4.0", - "@firebase/util": "1.13.0" - } - }, - "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, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "license": "ISC" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.3" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", - "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz", - "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", - "license": "MIT", - "dependencies": { - "react-router": "7.11.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", - "fsevents": "~2.3.2" - } - }, - "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" - } - ], - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "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==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "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==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "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" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/web-vitals": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", - "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", - "license": "Apache-2.0" - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/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==", - "license": "MIT", - "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/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } } - } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 96cc687..8317518 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,56 +7,71 @@ import Alerts from "./pages/Alerts"; import AlertHistory from "./pages/AlertHistory"; export default function App() { - const { currentUser, logout } = useAuth(); + const { currentUser, logout } = useAuth(); - // Show login/signup routes if not authenticated - if (!currentUser) { + // Show login/signup routes if not authenticated + if (!currentUser) { + return ( + + } /> + } /> + } /> + + ); + } + + // Show main app if authenticated return ( - - } /> - } /> - } /> - +
+ + + + } /> + } /> + } /> + } /> + } /> + +
); - } - - // Show main app if authenticated - return ( -
- - - - } /> - } /> - } /> - } /> - } /> - -
- ); } diff --git a/frontend/src/api/addresses.jsx b/frontend/src/api/addresses.jsx index 15ef07a..5cc8ccd 100644 --- a/frontend/src/api/addresses.jsx +++ b/frontend/src/api/addresses.jsx @@ -1,7 +1,7 @@ // API client for address management -import { getAuthHeaders, getAuthHeadersSimple } from './authHeaders'; -import { API_BASE } from './config'; +import { getAuthHeaders, getAuthHeadersSimple } from "./authHeaders"; +import { API_BASE } from "./config"; /** * Create a new blockchain address to monitor @@ -11,35 +11,37 @@ import { API_BASE } from './config'; * @returns {Promise} Created address with id and metadata */ export async function createAddress(data) { - try { - const headers = await getAuthHeaders(); - const response = await fetch(`${API_BASE}/addresses`, { - method: 'POST', - headers: headers, - body: JSON.stringify(data) - }); - - if (!response.ok) { - // Try to parse error response, but handle if it's not JSON - let errorMessage = 'Failed to create address'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - // If JSON parse fails, use status text - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } - - return response.json(); - } catch (error) { - // Handle network errors - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); + try { + const headers = await getAuthHeaders(); + const response = await fetch(`${API_BASE}/addresses`, { + method: "POST", + headers: headers, + body: JSON.stringify(data), + }); + + if (!response.ok) { + // Try to parse error response, but handle if it's not JSON + let errorMessage = "Failed to create address"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + // If JSON parse fails, use status text + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return response.json(); + } catch (error) { + // Handle network errors + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - throw error; - } } /** @@ -47,33 +49,35 @@ export async function createAddress(data) { * @returns {Promise} List of all addresses */ export async function getAddresses() { - try { - const headers = await getAuthHeadersSimple(); - const response = await fetch(`${API_BASE}/addresses`, { - headers: headers - }); - - if (!response.ok) { - // Try to parse error response, but handle if it's not JSON - let errorMessage = 'Failed to fetch addresses'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - // If JSON parse fails, use status text - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } - - return response.json(); - } catch (error) { - // Handle network errors or JSON parse errors - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); + try { + const headers = await getAuthHeadersSimple(); + const response = await fetch(`${API_BASE}/addresses`, { + headers: headers, + }); + + if (!response.ok) { + // Try to parse error response, but handle if it's not JSON + let errorMessage = "Failed to fetch addresses"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + // If JSON parse fails, use status text + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return response.json(); + } catch (error) { + // Handle network errors or JSON parse errors + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - throw error; - } } /** @@ -82,31 +86,32 @@ export async function getAddresses() { * @returns {Promise} */ export async function deleteAddress(addressId) { - try { - const headers = await getAuthHeadersSimple(); - const response = await fetch(`${API_BASE}/addresses/${addressId}`, { - method: 'DELETE', - headers: headers - }); - - if (!response.ok && response.status !== 204) { - // Try to parse error response, but handle if it's not JSON - let errorMessage = 'Failed to delete address'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - // If JSON parse fails, use status text - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } - } catch (error) { - // Handle network errors - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); - } - throw error; - } -} + try { + const headers = await getAuthHeadersSimple(); + const response = await fetch(`${API_BASE}/addresses/${addressId}`, { + method: "DELETE", + headers: headers, + }); + if (!response.ok && response.status !== 204) { + // Try to parse error response, but handle if it's not JSON + let errorMessage = "Failed to delete address"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + // If JSON parse fails, use status text + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + } catch (error) { + // Handle network errors + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; + } +} diff --git a/frontend/src/api/alertEvents.jsx b/frontend/src/api/alertEvents.jsx index 86576d0..f1df783 100644 --- a/frontend/src/api/alertEvents.jsx +++ b/frontend/src/api/alertEvents.jsx @@ -1,35 +1,37 @@ // API client for alert event history -import { getAuthHeadersSimple } from './authHeaders'; -import { API_BASE } from './config'; +import { getAuthHeadersSimple } from "./authHeaders"; +import { API_BASE } from "./config"; /** * Get all alert events (history) * @returns {Promise} List of alert events, sorted by most recent first */ export async function getAlertEvents() { - try { - const headers = await getAuthHeadersSimple(); - const response = await fetch(`${API_BASE}/alert-events`, { - headers: headers - }); - - if (!response.ok) { - let errorMessage = 'Failed to fetch alert events'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } - - return response.json(); - } catch (error) { - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); + try { + const headers = await getAuthHeadersSimple(); + const response = await fetch(`${API_BASE}/alert-events`, { + headers: headers, + }); + + if (!response.ok) { + let errorMessage = "Failed to fetch alert events"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return response.json(); + } catch (error) { + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - throw error; - } } diff --git a/frontend/src/api/alerts.jsx b/frontend/src/api/alerts.jsx index 310f7cc..8f6c383 100644 --- a/frontend/src/api/alerts.jsx +++ b/frontend/src/api/alerts.jsx @@ -1,16 +1,16 @@ // API client for alert rule management -import { getAuthHeaders, getAuthHeadersSimple } from './authHeaders'; -import { API_BASE } from './config'; +import { getAuthHeaders, getAuthHeadersSimple } from "./authHeaders"; +import { API_BASE } from "./config"; /** * Alert types supported by the system */ export const ALERT_TYPES = { - INCOMING_TX: 'incoming_tx', - OUTGOING_TX: 'outgoing_tx', - LARGE_TRANSFER: 'large_transfer', - BALANCE_BELOW: 'balance_below' + INCOMING_TX: "incoming_tx", + OUTGOING_TX: "outgoing_tx", + LARGE_TRANSFER: "large_transfer", + BALANCE_BELOW: "balance_below", }; /** @@ -22,32 +22,37 @@ export const ALERT_TYPES = { * @returns {Promise} Created alert rule */ export async function createAlert(addressId, data) { - try { - const headers = await getAuthHeaders(); - const response = await fetch(`${API_BASE}/addresses/${addressId}/alerts`, { - method: 'POST', - headers: headers, - body: JSON.stringify(data) - }); - - if (!response.ok) { - let errorMessage = 'Failed to create alert rule'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } - - return response.json(); - } catch (error) { - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); + try { + const headers = await getAuthHeaders(); + const response = await fetch( + `${API_BASE}/addresses/${addressId}/alerts`, + { + method: "POST", + headers: headers, + body: JSON.stringify(data), + }, + ); + + if (!response.ok) { + let errorMessage = "Failed to create alert rule"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return response.json(); + } catch (error) { + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - throw error; - } } /** @@ -56,30 +61,35 @@ export async function createAlert(addressId, data) { * @returns {Promise} List of alert rules */ export async function getAlerts(addressId) { - try { - const headers = await getAuthHeadersSimple(); - const response = await fetch(`${API_BASE}/addresses/${addressId}/alerts`, { - headers: headers - }); - - if (!response.ok) { - let errorMessage = 'Failed to fetch alert rules'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } - - return response.json(); - } catch (error) { - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); + try { + const headers = await getAuthHeadersSimple(); + const response = await fetch( + `${API_BASE}/addresses/${addressId}/alerts`, + { + headers: headers, + }, + ); + + if (!response.ok) { + let errorMessage = "Failed to fetch alert rules"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return response.json(); + } catch (error) { + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - throw error; - } } /** @@ -89,32 +99,34 @@ export async function getAlerts(addressId) { * @returns {Promise} Updated alert rule */ export async function updateAlertStatus(alertId, enabled) { - try { - const headers = await getAuthHeaders(); - const response = await fetch(`${API_BASE}/alerts/${alertId}`, { - method: 'PATCH', - headers: headers, - body: JSON.stringify({ enabled }) - }); - - if (!response.ok) { - let errorMessage = 'Failed to update alert rule'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } - - return response.json(); - } catch (error) { - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); + try { + const headers = await getAuthHeaders(); + const response = await fetch(`${API_BASE}/alerts/${alertId}`, { + method: "PATCH", + headers: headers, + body: JSON.stringify({ enabled }), + }); + + if (!response.ok) { + let errorMessage = "Failed to update alert rule"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return response.json(); + } catch (error) { + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - throw error; - } } /** @@ -123,27 +135,29 @@ export async function updateAlertStatus(alertId, enabled) { * @returns {Promise} */ export async function deleteAlert(alertId) { - try { - const headers = await getAuthHeadersSimple(); - const response = await fetch(`${API_BASE}/alerts/${alertId}`, { - method: 'DELETE', - headers: headers - }); - - if (!response.ok && response.status !== 204) { - let errorMessage = 'Failed to delete alert rule'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); + try { + const headers = await getAuthHeadersSimple(); + const response = await fetch(`${API_BASE}/alerts/${alertId}`, { + method: "DELETE", + headers: headers, + }); + + if (!response.ok && response.status !== 204) { + let errorMessage = "Failed to delete alert rule"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + } catch (error) { + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - } catch (error) { - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); - } - throw error; - } } diff --git a/frontend/src/api/authHeaders.js b/frontend/src/api/authHeaders.js index aa3e615..6ccecaa 100644 --- a/frontend/src/api/authHeaders.js +++ b/frontend/src/api/authHeaders.js @@ -1,30 +1,30 @@ /** * Auth Headers Helper - * + * * Provides authentication headers for API calls * Includes Firebase ID token in Authorization header */ -import { auth } from '../firebase/config'; +import { auth } from "../firebase/config"; /** * Get headers with authentication token * @returns {Promise} Headers object with Authorization */ export async function getAuthHeaders() { - const currentUser = auth.currentUser; - - if (!currentUser) { - throw new Error('No authenticated user'); - } + const currentUser = auth.currentUser; - // Get Firebase ID token - const token = await currentUser.getIdToken(); + if (!currentUser) { + throw new Error("No authenticated user"); + } - return { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }; + // Get Firebase ID token + const token = await currentUser.getIdToken(); + + return { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }; } /** @@ -32,16 +32,15 @@ export async function getAuthHeaders() { * @returns {Promise} Headers object with Authorization */ export async function getAuthHeadersSimple() { - const currentUser = auth.currentUser; - - if (!currentUser) { - throw new Error('No authenticated user'); - } + const currentUser = auth.currentUser; - const token = await currentUser.getIdToken(); + if (!currentUser) { + throw new Error("No authenticated user"); + } - return { - 'Authorization': `Bearer ${token}` - }; + const token = await currentUser.getIdToken(); + + return { + Authorization: `Bearer ${token}`, + }; } - diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js index 5e945c8..7673e3c 100644 --- a/frontend/src/api/config.js +++ b/frontend/src/api/config.js @@ -1 +1 @@ -export const API_BASE = import.meta.env.VITE_API_BASE || '/v1'; +export const API_BASE = import.meta.env.VITE_API_BASE || "/v1"; diff --git a/frontend/src/api/notificationConfig.jsx b/frontend/src/api/notificationConfig.jsx index 73010fb..02589ce 100644 --- a/frontend/src/api/notificationConfig.jsx +++ b/frontend/src/api/notificationConfig.jsx @@ -1,37 +1,39 @@ // API client for notification configuration -import { getAuthHeaders } from './authHeaders'; -import { API_BASE } from './config'; +import { getAuthHeaders } from "./authHeaders"; +import { API_BASE } from "./config"; /** * Get notification configuration for current user * @returns {Promise} Notification config */ export async function getNotificationConfig() { - try { - const headers = await getAuthHeaders(); - const response = await fetch(`${API_BASE}/notification-config`, { - headers: headers - }); - - if (!response.ok) { - let errorMessage = 'Failed to fetch notification config'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); + try { + const headers = await getAuthHeaders(); + const response = await fetch(`${API_BASE}/notification-config`, { + headers: headers, + }); + + if (!response.ok) { + let errorMessage = "Failed to fetch notification config"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return response.json(); + } catch (error) { + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - - return response.json(); - } catch (error) { - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); - } - throw error; - } } /** @@ -44,32 +46,34 @@ export async function getNotificationConfig() { * @returns {Promise} Updated config */ export async function updateNotificationConfig(config) { - try { - const headers = await getAuthHeaders(); - const response = await fetch(`${API_BASE}/notification-config`, { - method: 'PUT', - headers: headers, - body: JSON.stringify(config) - }); - - if (!response.ok) { - let errorMessage = 'Failed to update notification config'; - try { - const error = await response.json(); - errorMessage = error.message || errorMessage; - } catch { - errorMessage = `Server error: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); + try { + const headers = await getAuthHeaders(); + const response = await fetch(`${API_BASE}/notification-config`, { + method: "PUT", + headers: headers, + body: JSON.stringify(config), + }); + + if (!response.ok) { + let errorMessage = "Failed to update notification config"; + try { + const error = await response.json(); + errorMessage = error.message || errorMessage; + } catch { + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return response.json(); + } catch (error) { + if (error.message.includes("fetch")) { + throw new Error( + "Cannot connect to server. Is the backend running?", + ); + } + throw error; } - - return response.json(); - } catch (error) { - if (error.message.includes('fetch')) { - throw new Error('Cannot connect to server. Is the backend running?'); - } - throw error; - } } /** @@ -79,21 +83,21 @@ export async function updateNotificationConfig(config) { * @returns {Promise} True if test successful */ export async function testDiscordWebhook(webhookUrl) { - try { - const payload = { - content: 'Koin Ping test notification - Your Discord webhook is configured correctly!', - }; + try { + const payload = { + content: + "Koin Ping test notification - Your Discord webhook is configured correctly!", + }; - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); + const response = await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); - return response.ok; - } catch (error) { - console.error('Discord webhook test failed:', error); - return false; - } + return response.ok; + } catch (error) { + console.error("Discord webhook test failed:", error); + return false; + } } - diff --git a/frontend/src/api/status.jsx b/frontend/src/api/status.jsx index 3d49bd4..0416f1d 100644 --- a/frontend/src/api/status.jsx +++ b/frontend/src/api/status.jsx @@ -1,18 +1,18 @@ // API client for system status -import { API_BASE } from './config'; +import { API_BASE } from "./config"; /** * Get system status including latest processed block and health * @returns {Promise} System status */ export async function getSystemStatus() { - const response = await fetch(`${API_BASE}/status`); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || 'Failed to fetch system status'); - } - - return response.json(); + const response = await fetch(`${API_BASE}/status`); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Failed to fetch system status"); + } + + return response.json(); } diff --git a/frontend/src/components/AddressForm.jsx b/frontend/src/components/AddressForm.jsx index 7825586..84b74b5 100644 --- a/frontend/src/components/AddressForm.jsx +++ b/frontend/src/components/AddressForm.jsx @@ -3,43 +3,43 @@ import Input from "./Input"; import Button from "./Button"; export default function AddressForm({ onSubmit }) { - const [address, setAddress] = useState(""); - const [label, setLabel] = useState(""); + const [address, setAddress] = useState(""); + const [label, setLabel] = useState(""); - const canSubmit = address.trim().length > 0; + const canSubmit = address.trim().length > 0; - function handleSubmit(e) { - e.preventDefault(); - if (!canSubmit) return; + function handleSubmit(e) { + e.preventDefault(); + if (!canSubmit) return; - onSubmit({ - address: address.trim(), - label: label.trim(), - }); + onSubmit({ + address: address.trim(), + label: label.trim(), + }); - setAddress(""); - setLabel(""); - } + setAddress(""); + setLabel(""); + } - return ( -
- + return ( + + - + - -
- ); + + + ); } diff --git a/frontend/src/components/AlertForm.jsx b/frontend/src/components/AlertForm.jsx index 11b687b..20834f6 100644 --- a/frontend/src/components/AlertForm.jsx +++ b/frontend/src/components/AlertForm.jsx @@ -3,69 +3,69 @@ import Input from "./Input"; import Button from "./Button"; const ALERT_TYPES = [ - { value: "incoming_tx", label: "Incoming transaction" }, - { value: "outgoing_tx", label: "Outgoing transaction" }, - { value: "large_transfer", label: "Large transfer" }, - { value: "balance_below", label: "Balance below threshold" }, + { value: "incoming_tx", label: "Incoming transaction" }, + { value: "outgoing_tx", label: "Outgoing transaction" }, + { value: "large_transfer", label: "Large transfer" }, + { value: "balance_below", label: "Balance below threshold" }, ]; export default function AlertForm({ onSubmit }) { - const [type, setType] = useState("incoming_tx"); - const [threshold, setThreshold] = useState(""); + const [type, setType] = useState("incoming_tx"); + const [threshold, setThreshold] = useState(""); - const needsThreshold = - type === "large_transfer" || type === "balance_below"; + const needsThreshold = + type === "large_transfer" || type === "balance_below"; - const canSubmit = needsThreshold - ? threshold.trim() !== "" && - !isNaN(Number(threshold)) && - Number(threshold) > 0 - : true; + const canSubmit = needsThreshold + ? threshold.trim() !== "" && + !isNaN(Number(threshold)) && + Number(threshold) > 0 + : true; - function handleSubmit(e) { - e.preventDefault(); - if (!canSubmit) return; + function handleSubmit(e) { + e.preventDefault(); + if (!canSubmit) return; - onSubmit({ - type, - threshold: needsThreshold ? Number(threshold) : undefined, - }); + onSubmit({ + type, + threshold: needsThreshold ? Number(threshold) : undefined, + }); - setThreshold(""); - } + setThreshold(""); + } - return ( -
-
- {ALERT_TYPES.map((opt) => ( - - ))} -
+ return ( + +
+ {ALERT_TYPES.map((opt) => ( + + ))} +
- {needsThreshold && ( - - )} + {needsThreshold && ( + + )} - -
- ); + + + ); } diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx index 1baf008..f795ecd 100644 --- a/frontend/src/components/Button.jsx +++ b/frontend/src/components/Button.jsx @@ -3,21 +3,20 @@ export default function Button({ onClick, disabled = false, type = "button", - }) { +}) { return ( - + ); - } - \ No newline at end of file +} diff --git a/frontend/src/components/Input.jsx b/frontend/src/components/Input.jsx index 2b175d7..b5ac1d3 100644 --- a/frontend/src/components/Input.jsx +++ b/frontend/src/components/Input.jsx @@ -7,27 +7,26 @@ export default function Input({ step, min, disabled = false, - }) { +}) { return ( - + ); - } - \ No newline at end of file +} diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index 9aec6ed..586d654 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -1,17 +1,17 @@ /** * AuthContext - Firebase Authentication State Management - * + * * Provides authentication state and methods throughout the app */ -import { createContext, useContext, useEffect, useState } from 'react'; -import { - createUserWithEmailAndPassword, - signInWithEmailAndPassword, - signOut, - onAuthStateChanged -} from 'firebase/auth'; -import { auth } from '../firebase/config'; +import { createContext, useContext, useEffect, useState } from "react"; +import { + createUserWithEmailAndPassword, + signInWithEmailAndPassword, + signOut, + onAuthStateChanged, +} from "firebase/auth"; +import { auth } from "../firebase/config"; const AuthContext = createContext(); @@ -20,88 +20,95 @@ const AuthContext = createContext(); * @returns {Object} Auth context value */ export function useAuth() { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within AuthProvider'); - } - return context; + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within AuthProvider"); + } + return context; } /** * AuthProvider - Wraps app and provides auth state */ export function AuthProvider({ children }) { - const [currentUser, setCurrentUser] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [currentUser, setCurrentUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - /** - * Sign up with email and password - */ - async function signup(email, password) { - try { - setError(null); - const result = await createUserWithEmailAndPassword(auth, email, password); - return result.user; - } catch (err) { - setError(err.message); - throw err; + /** + * Sign up with email and password + */ + async function signup(email, password) { + try { + setError(null); + const result = await createUserWithEmailAndPassword( + auth, + email, + password, + ); + return result.user; + } catch (err) { + setError(err.message); + throw err; + } } - } - /** - * Log in with email and password - */ - async function login(email, password) { - try { - setError(null); - const result = await signInWithEmailAndPassword(auth, email, password); - return result.user; - } catch (err) { - setError(err.message); - throw err; + /** + * Log in with email and password + */ + async function login(email, password) { + try { + setError(null); + const result = await signInWithEmailAndPassword( + auth, + email, + password, + ); + return result.user; + } catch (err) { + setError(err.message); + throw err; + } } - } - /** - * Log out current user - */ - async function logout() { - try { - setError(null); - await signOut(auth); - } catch (err) { - setError(err.message); - throw err; + /** + * Log out current user + */ + async function logout() { + try { + setError(null); + await signOut(auth); + } catch (err) { + setError(err.message); + throw err; + } } - } - /** - * Listen for auth state changes - */ - useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (user) => { - setCurrentUser(user); - setLoading(false); - }); + /** + * Listen for auth state changes + */ + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + setCurrentUser(user); + setLoading(false); + }); - // Cleanup subscription - return unsubscribe; - }, []); + // Cleanup subscription + return unsubscribe; + }, []); - const value = { - currentUser, - signup, - login, - logout, - error, - loading - }; + const value = { + currentUser, + signup, + login, + logout, + error, + loading, + }; - return ( - - {!loading && children} - - ); + return ( + + {!loading && children} + + ); } - diff --git a/frontend/src/firebase/config.js b/frontend/src/firebase/config.js index bc239d5..4a89852 100644 --- a/frontend/src/firebase/config.js +++ b/frontend/src/firebase/config.js @@ -1,6 +1,5 @@ - -import { initializeApp } from 'firebase/app'; -import { getAuth } from 'firebase/auth'; +import { initializeApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; import { firebaseApiKey, firebaseAuthDomain, @@ -8,9 +7,8 @@ import { firebaseStorageBucket, firebaseMessagingSenderId, firebaseAppId, - firebaseMeasurementId -} from '../secrets.js'; - + firebaseMeasurementId, +} from "../secrets.js"; const firebaseConfig = { apiKey: firebaseApiKey, @@ -19,12 +17,11 @@ const firebaseConfig = { storageBucket: firebaseStorageBucket, messagingSenderId: firebaseMessagingSenderId, appId: firebaseAppId, - measurementId: firebaseMeasurementId - }; + measurementId: firebaseMeasurementId, +}; const app = initializeApp(firebaseConfig); export const auth = getAuth(app); export default app; - diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 7da6005..f4417d1 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -5,11 +5,11 @@ import { AuthProvider } from "./contexts/AuthContext"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root")).render( - - - - - - - + + + + + + + , ); diff --git a/frontend/src/pages/Addresses.jsx b/frontend/src/pages/Addresses.jsx index 5b35896..9e33911 100644 --- a/frontend/src/pages/Addresses.jsx +++ b/frontend/src/pages/Addresses.jsx @@ -3,81 +3,91 @@ import AddressForm from "../components/AddressForm"; import { getAddresses, createAddress } from "../api/addresses"; export default function Addresses() { - const [addresses, setAddresses] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [addresses, setAddresses] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // Load addresses on mount - useEffect(() => { - async function fetchAddresses() { - try { - setLoading(true); - const data = await getAddresses(); - setAddresses(data); - } catch (err) { - setError(err.message); - console.error("Failed to fetch addresses:", err); - } finally { - setLoading(false); - } + // Load addresses on mount + useEffect(() => { + async function fetchAddresses() { + try { + setLoading(true); + const data = await getAddresses(); + setAddresses(data); + } catch (err) { + setError(err.message); + console.error("Failed to fetch addresses:", err); + } finally { + setLoading(false); + } + } + + fetchAddresses(); + }, []); + + // Handle new address submission + async function handleAddressSubmit(data) { + try { + const newAddress = await createAddress(data); + // Append new address to state + setAddresses((prev) => [...prev, newAddress]); + setError(null); // Clear any previous errors + } catch (err) { + setError(err.message); + console.error("Failed to create address:", err); + } } - fetchAddresses(); - }, []); + return ( +
+

Tracked Addresses

- // Handle new address submission - async function handleAddressSubmit(data) { - try { - const newAddress = await createAddress(data); - // Append new address to state - setAddresses((prev) => [...prev, newAddress]); - setError(null); // Clear any previous errors - } catch (err) { - setError(err.message); - console.error("Failed to create address:", err); - } - } +
+ +
- return ( -
-

Tracked Addresses

- -
- -
- -
-

Existing Addresses

- {loading &&

Loading addresses...

} - {error &&

Error: {error}

} - {!loading && !error && addresses.length === 0 && ( -

- No addresses tracked yet. Add one above to get started. -

- )} - {addresses.length > 0 && ( -
    - {addresses.map((addr, index) => ( -
  • -
    - {addr.label || "Unlabeled"} -
    -
    - {addr.address} -
    -
  • - ))} -
- )} -
-
- ); +
+

Existing Addresses

+ {loading &&

Loading addresses...

} + {error &&

Error: {error}

} + {!loading && !error && addresses.length === 0 && ( +

+ No addresses tracked yet. Add one above to get started. +

+ )} + {addresses.length > 0 && ( +
    + {addresses.map((addr, index) => ( +
  • +
    + {addr.label || "Unlabeled"} +
    +
    + {addr.address} +
    +
  • + ))} +
+ )} +
+
+ ); } diff --git a/frontend/src/pages/AlertHistory.jsx b/frontend/src/pages/AlertHistory.jsx index 52b6b40..f9d76fc 100644 --- a/frontend/src/pages/AlertHistory.jsx +++ b/frontend/src/pages/AlertHistory.jsx @@ -2,73 +2,83 @@ import { useState, useEffect } from "react"; import { getAlertEvents } from "../api/alertEvents"; export default function AlertHistory() { - const [alertEvents, setAlertEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [alertEvents, setAlertEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // Fetch alert events on mount - useEffect(() => { - async function fetchAlertEvents() { - try { - setLoading(true); - const data = await getAlertEvents(); - setAlertEvents(data); - } catch (err) { - setError(err.message); - console.error("Failed to fetch alert events:", err); - } finally { - setLoading(false); - } + // Fetch alert events on mount + useEffect(() => { + async function fetchAlertEvents() { + try { + setLoading(true); + const data = await getAlertEvents(); + setAlertEvents(data); + } catch (err) { + setError(err.message); + console.error("Failed to fetch alert events:", err); + } finally { + setLoading(false); + } + } + + fetchAlertEvents(); + }, []); + + if (loading) { + return
Loading...
; } - fetchAlertEvents(); - }, []); + if (error) { + return ( +
Error: {error}
+ ); + } - if (loading) { - return
Loading...
; - } + return ( +
+

Recent Alerts

- if (error) { - return
Error: {error}
; - } - - return ( -
-

Recent Alerts

- - {alertEvents.length === 0 ? ( -

No alerts yet

- ) : ( -
    - {alertEvents.map((event) => ( -
  • -
    {event.message}
    - {event.address_label && ( -
    - Address: {event.address_label} -
    - )} - - {formatTimestamp(event.timestamp)} - -
  • - ))} -
- )} -
- ); + {alertEvents.length === 0 ? ( +

No alerts yet

+ ) : ( +
    + {alertEvents.map((event) => ( +
  • +
    + {event.message} +
    + {event.address_label && ( +
    + Address: {event.address_label} +
    + )} + + {formatTimestamp(event.timestamp)} + +
  • + ))} +
+ )} +
+ ); } // Helper to format timestamp for display function formatTimestamp(timestamp) { - const date = new Date(timestamp); - return date.toLocaleString(); + const date = new Date(timestamp); + return date.toLocaleString(); } diff --git a/frontend/src/pages/Alerts.jsx b/frontend/src/pages/Alerts.jsx index 2444e0e..529420c 100644 --- a/frontend/src/pages/Alerts.jsx +++ b/frontend/src/pages/Alerts.jsx @@ -3,469 +3,634 @@ import AlertForm from "../components/AlertForm"; import Button from "../components/Button"; import Input from "../components/Input"; import { getAddresses } from "../api/addresses"; -import { getAlerts, createAlert, updateAlertStatus, deleteAlert } from "../api/alerts"; -import { getNotificationConfig, updateNotificationConfig, testDiscordWebhook } from "../api/notificationConfig"; +import { + getAlerts, + createAlert, + updateAlertStatus, + deleteAlert, +} from "../api/alerts"; +import { + getNotificationConfig, + updateNotificationConfig, + testDiscordWebhook, +} from "../api/notificationConfig"; export default function Alerts() { - const [addresses, setAddresses] = useState([]); - const [selectedAddressId, setSelectedAddressId] = useState(null); - const [alerts, setAlerts] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Notification config state - const [notificationConfig, setNotificationConfig] = useState(null); - const [discordWebhookUrl, setDiscordWebhookUrl] = useState(''); - const [notificationEnabled, setNotificationEnabled] = useState(true); - const [notificationLoading, setNotificationLoading] = useState(false); - const [notificationError, setNotificationError] = useState(null); - const [notificationSuccess, setNotificationSuccess] = useState(null); - const [testingWebhook, setTestingWebhook] = useState(false); + const [addresses, setAddresses] = useState([]); + const [selectedAddressId, setSelectedAddressId] = useState(null); + const [alerts, setAlerts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // Load addresses and notification config on mount - useEffect(() => { - async function fetchData() { - try { - setLoading(true); - - // Fetch addresses - const addressData = await getAddresses(); - setAddresses(addressData); - if (addressData.length > 0) { - setSelectedAddressId(addressData[0].id); + // Notification config state + const [notificationConfig, setNotificationConfig] = useState(null); + const [discordWebhookUrl, setDiscordWebhookUrl] = useState(""); + const [notificationEnabled, setNotificationEnabled] = useState(true); + const [notificationLoading, setNotificationLoading] = useState(false); + const [notificationError, setNotificationError] = useState(null); + const [notificationSuccess, setNotificationSuccess] = useState(null); + const [testingWebhook, setTestingWebhook] = useState(false); + + // Load addresses and notification config on mount + useEffect(() => { + async function fetchData() { + try { + setLoading(true); + + // Fetch addresses + const addressData = await getAddresses(); + setAddresses(addressData); + if (addressData.length > 0) { + setSelectedAddressId(addressData[0].id); + } + + // Fetch notification config + const configData = await getNotificationConfig(); + setNotificationConfig(configData); + setDiscordWebhookUrl(configData.discord_webhook_url || ""); + setNotificationEnabled( + configData.notification_enabled !== false, + ); + } catch (err) { + setError(err.message); + console.error("Failed to fetch data:", err); + } finally { + setLoading(false); + } + } + + fetchData(); + }, []); + + // Load alerts when address is selected + useEffect(() => { + if (!selectedAddressId) { + setAlerts([]); + return; + } + + async function fetchAlerts() { + try { + const data = await getAlerts(selectedAddressId); + setAlerts(data); + setError(null); // Clear any previous errors + } catch (err) { + setError(err.message); + console.error("Failed to fetch alerts:", err); + } + } + + fetchAlerts(); + }, [selectedAddressId]); + + // Handle new alert submission + async function handleAlertSubmit(data) { + if (!selectedAddressId) return; + + try { + const newAlert = await createAlert(selectedAddressId, data); + setAlerts((prev) => [...prev, newAlert]); + setError(null); // Clear any previous errors + } catch (err) { + setError(err.message); + console.error("Failed to create alert:", err); } - - // Fetch notification config - const configData = await getNotificationConfig(); - setNotificationConfig(configData); - setDiscordWebhookUrl(configData.discord_webhook_url || ''); - setNotificationEnabled(configData.notification_enabled !== false); - } catch (err) { - setError(err.message); - console.error("Failed to fetch data:", err); - } finally { - setLoading(false); - } } - fetchData(); - }, []); - - // Load alerts when address is selected - useEffect(() => { - if (!selectedAddressId) { - setAlerts([]); - return; + // Toggle alert enabled/disabled + async function handleToggleAlert(alertId, currentStatus) { + try { + const updated = await updateAlertStatus(alertId, !currentStatus); + setAlerts((prev) => + prev.map((alert) => (alert.id === alertId ? updated : alert)), + ); + setError(null); // Clear any previous errors + } catch (err) { + setError(err.message); + console.error("Failed to update alert:", err); + } } - async function fetchAlerts() { - try { - const data = await getAlerts(selectedAddressId); - setAlerts(data); - setError(null); // Clear any previous errors - } catch (err) { - setError(err.message); - console.error("Failed to fetch alerts:", err); - } + // Delete alert + async function handleDeleteAlert(alertId) { + try { + await deleteAlert(alertId); + setAlerts((prev) => prev.filter((alert) => alert.id !== alertId)); + setError(null); // Clear any previous errors + } catch (err) { + setError(err.message); + console.error("Failed to delete alert:", err); + } } - fetchAlerts(); - }, [selectedAddressId]); + // Save notification config + async function handleSaveNotificationConfig() { + try { + setNotificationLoading(true); + setNotificationError(null); + setNotificationSuccess(null); - // Handle new alert submission - async function handleAlertSubmit(data) { - if (!selectedAddressId) return; + const config = { + discord_webhook_url: discordWebhookUrl || null, + notification_enabled: notificationEnabled, + }; - try { - const newAlert = await createAlert(selectedAddressId, data); - setAlerts((prev) => [...prev, newAlert]); - setError(null); // Clear any previous errors - } catch (err) { - setError(err.message); - console.error("Failed to create alert:", err); - } - } + const updated = await updateNotificationConfig(config); + setNotificationConfig(updated); + setNotificationSuccess("Notification settings saved!"); - // Toggle alert enabled/disabled - async function handleToggleAlert(alertId, currentStatus) { - try { - const updated = await updateAlertStatus(alertId, !currentStatus); - setAlerts((prev) => - prev.map((alert) => (alert.id === alertId ? updated : alert)) - ); - setError(null); // Clear any previous errors - } catch (err) { - setError(err.message); - console.error("Failed to update alert:", err); - } - } - - // Delete alert - async function handleDeleteAlert(alertId) { - try { - await deleteAlert(alertId); - setAlerts((prev) => prev.filter((alert) => alert.id !== alertId)); - setError(null); // Clear any previous errors - } catch (err) { - setError(err.message); - console.error("Failed to delete alert:", err); - } - } - - // Save notification config - async function handleSaveNotificationConfig() { - try { - setNotificationLoading(true); - setNotificationError(null); - setNotificationSuccess(null); - - const config = { - discord_webhook_url: discordWebhookUrl || null, - notification_enabled: notificationEnabled, - }; - - const updated = await updateNotificationConfig(config); - setNotificationConfig(updated); - setNotificationSuccess('Notification settings saved!'); - - // Clear success message after 3 seconds - setTimeout(() => setNotificationSuccess(null), 3000); - } catch (err) { - setNotificationError(err.message); - console.error("Failed to save notification config:", err); - } finally { - setNotificationLoading(false); - } - } - - // Test Discord webhook - async function handleTestWebhook() { - if (!discordWebhookUrl) { - setNotificationError('Please enter a Discord webhook URL first'); - return; + // Clear success message after 3 seconds + setTimeout(() => setNotificationSuccess(null), 3000); + } catch (err) { + setNotificationError(err.message); + console.error("Failed to save notification config:", err); + } finally { + setNotificationLoading(false); + } } - try { - setTestingWebhook(true); - setNotificationError(null); - setNotificationSuccess(null); + // Test Discord webhook + async function handleTestWebhook() { + if (!discordWebhookUrl) { + setNotificationError("Please enter a Discord webhook URL first"); + return; + } - const success = await testDiscordWebhook(discordWebhookUrl); - - if (success) { - setNotificationSuccess('Test notification sent! Check your Discord channel.'); - setTimeout(() => setNotificationSuccess(null), 5000); - } else { - setNotificationError('Test failed. Check your webhook URL.'); - } - } catch (err) { - setNotificationError('Test failed: ' + err.message); - } finally { - setTestingWebhook(false); + try { + setTestingWebhook(true); + setNotificationError(null); + setNotificationSuccess(null); + + const success = await testDiscordWebhook(discordWebhookUrl); + + if (success) { + setNotificationSuccess( + "Test notification sent! Check your Discord channel.", + ); + setTimeout(() => setNotificationSuccess(null), 5000); + } else { + setNotificationError("Test failed. Check your webhook URL."); + } + } catch (err) { + setNotificationError("Test failed: " + err.message); + } finally { + setTestingWebhook(false); + } } - } - const selectedAddress = addresses.find((a) => a.id === selectedAddressId); + const selectedAddress = addresses.find((a) => a.id === selectedAddressId); - if (loading) { - return
Loading addresses...
; - } + if (loading) { + return
Loading addresses...
; + } + + if (addresses.length === 0) { + return ( +
+

+ No addresses tracked yet. Add an address first to create + alerts. +

+
+ ); + } - if (addresses.length === 0) { return ( -
-

No addresses tracked yet. Add an address first to create alerts.

-
- ); - } +
+

+ Alert Rules & Notifications +

- return ( -
-

Alert Rules & Notifications

- - {/* Two-column layout */} -
- - {/* LEFT COLUMN: Alert Rules */} -
-

Alert Rules

- - {/* Address selector */} -
- - -
+ {/* LEFT COLUMN: Alert Rules */} +
+

Alert Rules

- {selectedAddress && ( - <> - {/* Current address info */} -
-
- Managing alerts for: -
-
- {selectedAddress.label || "Unlabeled"} -
-
- {selectedAddress.address} -
-
- - {/* Alert creation form */} -
-

Create New Alert

- -
- - {/* Existing alerts list */} -
-

Active Alert Rules

- {error &&

Error: {error}

} - {alerts.length === 0 ? ( -

- No alert rules defined yet. Create one above. -

- ) : ( -
    - {alerts.map((alert) => ( -
  • -
    +
    + + {/* Alert creation form */} +
    +

    Create New Alert

    + +
    + + {/* Existing alerts list */} +
    +

    Active Alert Rules

    + {error && ( +

    + Error: {error} +

    + )} + {alerts.length === 0 ? ( +

    + No alert rules defined yet. Create one + above. +

    + ) : ( +
      + {alerts.map((alert) => ( +
    • +
      +
      +
      + {formatAlertType( + alert.type, + )} +
      + {alert.threshold && ( +
      + Threshold:{" "} + { + alert.threshold + }{" "} + ETH +
      + )} +
      + Status:{" "} + {alert.enabled + ? "Enabled" + : "Disabled"} +
      +
      +
      + + +
      +
      +
    • + ))} +
    + )} +
    + + )} +
+ + {/* RIGHT COLUMN: Notification Settings */} +
+

Notification Settings

+ + {notificationSuccess && ( +
+ {notificationSuccess}
- - ))} - - )} -
- - )} + )} + + {notificationError && ( +
+ {notificationError} +
+ )} + + {/* Master toggle */} +
+ +
+ Master switch for all notification channels +
+
+ + {/* Discord Section */} +
+

Discord

+ +
+ + + setDiscordWebhookUrl(e.target.value) + } + placeholder="https://discord.com/api/webhooks/..." + style={{ + width: "100%", + padding: "0.5rem", + fontSize: "1rem", + backgroundColor: "#1a1a1a", + border: "1px solid #444", + borderRadius: "4px", + color: "white", + fontFamily: "monospace", + fontSize: "0.9rem", + }} + /> + +
+ +
+ + + +
+
+ + {/* Telegram Section (Coming Soon) */} +
+

Telegram

+
+ Coming Soon +
+
+ + {/* Email Section (Coming Soon) */} +
+

Email

+
+ Coming Soon +
+
+
+
- - {/* RIGHT COLUMN: Notification Settings */} -
-

Notification Settings

- - {notificationSuccess && ( -
- {notificationSuccess} -
- )} - - {notificationError && ( -
- {notificationError} -
- )} - - {/* Master toggle */} -
- -
- Master switch for all notification channels -
-
- - {/* Discord Section */} -
-

Discord

- -
- - setDiscordWebhookUrl(e.target.value)} - placeholder="https://discord.com/api/webhooks/..." - style={{ - width: '100%', - padding: '0.5rem', - fontSize: '1rem', - backgroundColor: '#1a1a1a', - border: '1px solid #444', - borderRadius: '4px', - color: 'white', - fontFamily: 'monospace', - fontSize: '0.9rem' - }} - /> - -
- -
- - - -
-
- - {/* Telegram Section (Coming Soon) */} -
-

Telegram

-
- Coming Soon -
-
- - {/* Email Section (Coming Soon) */} -
-

Email

-
- Coming Soon -
-
-
- -
-
- ); + ); } // Helper to format alert type for display function formatAlertType(type) { - const labels = { - incoming_tx: "Incoming transaction", - outgoing_tx: "Outgoing transaction", - large_transfer: "Large transfer", - balance_below: "Balance below threshold", - }; - return labels[type] || type; + const labels = { + incoming_tx: "Incoming transaction", + outgoing_tx: "Outgoing transaction", + large_transfer: "Large transfer", + balance_below: "Balance below threshold", + }; + return labels[type] || type; } diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index fda82f2..1842833 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,143 +1,146 @@ /** * Login Page - * + * * Allows existing users to sign in with email and password */ -import { useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { useAuth } from '../contexts/AuthContext'; +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { useAuth } from "../contexts/AuthContext"; export default function Login() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - - const { login } = useAuth(); - const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); - async function handleSubmit(e) { - e.preventDefault(); + const { login } = useAuth(); + const navigate = useNavigate(); - // Validation - if (!email || !password) { - setError('Please fill in all fields'); - return; + async function handleSubmit(e) { + e.preventDefault(); + + // Validation + if (!email || !password) { + setError("Please fill in all fields"); + return; + } + + try { + setError(""); + setLoading(true); + await login(email, password); + navigate("/addresses"); // Redirect to main app + } catch (err) { + setError("Failed to log in: " + err.message); + } finally { + setLoading(false); + } } - try { - setError(''); - setLoading(true); - await login(email, password); - navigate('/addresses'); // Redirect to main app - } catch (err) { - setError('Failed to log in: ' + err.message); - } finally { - setLoading(false); - } - } - - return ( -
-

- Koin Ping - Login -

- - {error && ( -
- {error} -
- )} - -
-
- - setEmail(e.target.value)} - disabled={loading} + return ( +
-
- -
- - setPassword(e.target.value)} - disabled={loading} - style={{ - width: '100%', - padding: '0.5rem', - fontSize: '1rem', - backgroundColor: '#1a1a1a', - border: '1px solid #444', - borderRadius: '4px', - color: 'white' - }} - required - /> -
- - - +

+ Koin Ping - Login +

-
-

- Don't have an account?{' '} - - Sign up here - -

-
-
- ); + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + disabled={loading} + style={{ + width: "100%", + padding: "0.5rem", + fontSize: "1rem", + backgroundColor: "#1a1a1a", + border: "1px solid #444", + borderRadius: "4px", + color: "white", + }} + required + /> +
+ +
+ + setPassword(e.target.value)} + disabled={loading} + style={{ + width: "100%", + padding: "0.5rem", + fontSize: "1rem", + backgroundColor: "#1a1a1a", + border: "1px solid #444", + borderRadius: "4px", + color: "white", + }} + required + /> +
+ + +
+ +
+

+ Don't have an account?{" "} + + Sign up here + +

+
+
+ ); } - diff --git a/frontend/src/pages/Signup.jsx b/frontend/src/pages/Signup.jsx index 3706a2d..b4ee96a 100644 --- a/frontend/src/pages/Signup.jsx +++ b/frontend/src/pages/Signup.jsx @@ -1,185 +1,188 @@ /** * Signup Page - * + * * Allows new users to create an account with email and password */ -import { useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { useAuth } from '../contexts/AuthContext'; +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { useAuth } from "../contexts/AuthContext"; export default function Signup() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - - const { signup } = useAuth(); - const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); - async function handleSubmit(e) { - e.preventDefault(); + const { signup } = useAuth(); + const navigate = useNavigate(); - // Validation - if (!email || !password || !confirmPassword) { - setError('Please fill in all fields'); - return; + async function handleSubmit(e) { + e.preventDefault(); + + // Validation + if (!email || !password || !confirmPassword) { + setError("Please fill in all fields"); + return; + } + + if (password !== confirmPassword) { + setError("Passwords do not match"); + return; + } + + if (password.length < 6) { + setError("Password must be at least 6 characters"); + return; + } + + try { + setError(""); + setLoading(true); + await signup(email, password); + navigate("/addresses"); // Auto-login and redirect + } catch (err) { + // Firebase-specific error messages + if (err.code === "auth/email-already-in-use") { + setError("Email already in use. Try logging in instead."); + } else if (err.code === "auth/invalid-email") { + setError("Invalid email address"); + } else if (err.code === "auth/weak-password") { + setError("Password is too weak"); + } else { + setError("Failed to create account: " + err.message); + } + } finally { + setLoading(false); + } } - if (password !== confirmPassword) { - setError('Passwords do not match'); - return; - } - - if (password.length < 6) { - setError('Password must be at least 6 characters'); - return; - } - - try { - setError(''); - setLoading(true); - await signup(email, password); - navigate('/addresses'); // Auto-login and redirect - } catch (err) { - // Firebase-specific error messages - if (err.code === 'auth/email-already-in-use') { - setError('Email already in use. Try logging in instead.'); - } else if (err.code === 'auth/invalid-email') { - setError('Invalid email address'); - } else if (err.code === 'auth/weak-password') { - setError('Password is too weak'); - } else { - setError('Failed to create account: ' + err.message); - } - } finally { - setLoading(false); - } - } - - return ( -
-

- Koin Ping - Sign Up -

- - {error && ( -
- {error} -
- )} - -
-
- - setEmail(e.target.value)} - disabled={loading} + return ( +
-
- -
- - setPassword(e.target.value)} - disabled={loading} - style={{ - width: '100%', - padding: '0.5rem', - fontSize: '1rem', - backgroundColor: '#1a1a1a', - border: '1px solid #444', - borderRadius: '4px', - color: 'white' - }} - required - /> -
- -
- - setConfirmPassword(e.target.value)} - disabled={loading} - style={{ - width: '100%', - padding: '0.5rem', - fontSize: '1rem', - backgroundColor: '#1a1a1a', - border: '1px solid #444', - borderRadius: '4px', - color: 'white' - }} - required - /> -
- - - +

+ Koin Ping - Sign Up +

-
-

- Already have an account?{' '} - - Log in here - -

-
-
- ); + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + disabled={loading} + style={{ + width: "100%", + padding: "0.5rem", + fontSize: "1rem", + backgroundColor: "#1a1a1a", + border: "1px solid #444", + borderRadius: "4px", + color: "white", + }} + required + /> +
+ +
+ + setPassword(e.target.value)} + disabled={loading} + style={{ + width: "100%", + padding: "0.5rem", + fontSize: "1rem", + backgroundColor: "#1a1a1a", + border: "1px solid #444", + borderRadius: "4px", + color: "white", + }} + required + /> +
+ +
+ + setConfirmPassword(e.target.value)} + disabled={loading} + style={{ + width: "100%", + padding: "0.5rem", + fontSize: "1rem", + backgroundColor: "#1a1a1a", + border: "1px solid #444", + borderRadius: "4px", + color: "white", + }} + required + /> +
+ + +
+ +
+

+ Already have an account?{" "} + + Log in here + +

+
+
+ ); } - diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index ef88184..df8c471 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,25 +1,24 @@ { - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] } - diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 81d80e1..499995e 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,17 +1,16 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], - server: { - port: 3000, - proxy: { - '/v1': { - target: 'http://localhost:3001', - changeOrigin: true, - }, + plugins: [react()], + server: { + port: 3000, + proxy: { + "/v1": { + target: "http://localhost:3001", + changeOrigin: true, + }, + }, }, - }, -}) - +}); From 62e6c64ddb97f90d3fcb08d6508b756175fecbf5 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:07:47 -0500 Subject: [PATCH 11/14] fmt: apply prettier to Markdown and doc files --- README.md | 36 ++++++++++++++++++------------------ REPO_POLICIES.md | 14 +++++++------- backend-go/ENV_TEMPLATE.md | 10 +++++++--- backend-go/README.md | 7 +++++-- frontend/README.md | 31 +++++++++++++++++-------------- 5 files changed, 54 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 7a4f46b..854f1bc 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ The API listens on `http://localhost:3001` and the frontend on ## Rationale -Crypto users who hold or actively monitor addresses need a lightweight, -reliable way to know when on-chain activity occurs without polling block -explorers manually. Koin Ping fills that gap: it watches a set of Ethereum -addresses, evaluates configurable alert rules (incoming transactions, outgoing +Crypto users who hold or actively monitor addresses need a lightweight, reliable +way to know when on-chain activity occurs without polling block explorers +manually. Koin Ping fills that gap: it watches a set of Ethereum addresses, +evaluates configurable alert rules (incoming transactions, outgoing transactions, large transfers, balance thresholds), and notifies the user through Discord webhooks. @@ -91,9 +91,9 @@ koin_ping_0.2.0/ authentication middleware. Exposes CRUD endpoints for addresses, alert rules, alert events, and notification configuration. -**Poller** (`cmd/poller`): long-running daemon that polls Ethereum via -JSON-RPC, compares observed transactions against persisted alert rules, fires -alert events to the database, and dispatches Discord notifications. +**Poller** (`cmd/poller`): long-running daemon that polls Ethereum via JSON-RPC, +compares observed transactions against persisted alert rules, fires alert events +to the database, and dispatches Discord notifications. **Frontend** (`frontend/`): React 19 SPA built with Vite. Authenticates with Firebase, communicates with the API via fetch, and renders the address/alert @@ -101,17 +101,17 @@ management UI. ## TODO -- [ ] Switch frontend package manager from npm to yarn (per repo policy) -- [ ] Rename `go.mod` module path from `github.com/kjannette/koin-ping` to - `sneak.berlin/go/koin-ping` (per repo policy) -- [ ] Move database migrations to `internal/db/migrations/` and embed them - in the binary -- [ ] Pin all Docker base images by `@sha256` in Dockerfile -- [ ] Install `golangci-lint` locally and pass `make lint` -- [ ] Add vitest unit tests for the React frontend -- [ ] Add a `make db-reset` / `make migrate` target for schema management -- [ ] Set HTTP server read/write timeouts (currently unbounded) -- [ ] Replace `log.Printf` calls with structured `log/slog` +- [ ] Switch frontend package manager from npm to yarn (per repo policy) +- [ ] Rename `go.mod` module path from `github.com/kjannette/koin-ping` to + `sneak.berlin/go/koin-ping` (per repo policy) +- [ ] Move database migrations to `internal/db/migrations/` and embed them in + the binary +- [ ] Pin all Docker base images by `@sha256` in Dockerfile +- [ ] Install `golangci-lint` locally and pass `make lint` +- [ ] Add vitest unit tests for the React frontend +- [ ] Add a `make db-reset` / `make migrate` target for schema management +- [ ] Set HTTP server read/write timeouts (currently unbounded) +- [ ] Replace `log.Printf` calls with structured `log/slog` ## License diff --git a/REPO_POLICIES.md b/REPO_POLICIES.md index a024cbd..a0abedf 100644 --- a/REPO_POLICIES.md +++ b/REPO_POLICIES.md @@ -145,13 +145,13 @@ style conventions are in separate documents: - Database migrations live in `internal/db/migrations/` and must be embedded in the binary. - - `000_migration.sql` — contains ONLY the creation of the migrations tracking - table itself. Nothing else. - - `001_schema.sql` — the full application schema. - - **Pre-1.0.0:** never add additional migration files (002, 003, etc.). There - is no installed base to migrate. Edit `001_schema.sql` directly. - - **Post-1.0.0:** add new numbered migration files for each schema change. - Never edit existing migrations after release. + - `000_migration.sql` — contains ONLY the creation of the migrations + tracking table itself. Nothing else. + - `001_schema.sql` — the full application schema. + - **Pre-1.0.0:** never add additional migration files (002, 003, etc.). + There is no installed base to migrate. Edit `001_schema.sql` directly. + - **Post-1.0.0:** add new numbered migration files for each schema change. + Never edit existing migrations after release. - All repos should have an `.editorconfig` enforcing the project's indentation settings. diff --git a/backend-go/ENV_TEMPLATE.md b/backend-go/ENV_TEMPLATE.md index 6992c7f..214ce14 100644 --- a/backend-go/ENV_TEMPLATE.md +++ b/backend-go/ENV_TEMPLATE.md @@ -37,16 +37,20 @@ cp ENV_TEMPLATE.md .env ## Firebase Setup -For Firebase Admin SDK to work, you need to set up Application Default Credentials: +For Firebase Admin SDK to work, you need to set up Application Default +Credentials: **Option 1: Use Firebase Project ID (easiest for development)** + - Just set `FIREBASE_PROJECT_ID` in .env - Firebase Admin will use Application Default Credentials **Option 2: Use Service Account Key (production)** + 1. Go to Firebase Console > Project Settings > Service Accounts 2. Click "Generate new private key" 3. Download the JSON file 4. Either: - - Set `GOOGLE_APPLICATION_CREDENTIALS=/path/to/serviceAccountKey.json` in .env - - Or keep it in backend/ and add to .gitignore + - Set `GOOGLE_APPLICATION_CREDENTIALS=/path/to/serviceAccountKey.json` in + .env + - Or keep it in backend/ and add to .gitignore diff --git a/backend-go/README.md b/backend-go/README.md index 8f6f137..dcf3e15 100644 --- a/backend-go/README.md +++ b/backend-go/README.md @@ -1,4 +1,7 @@ +Start DB: + +brew services start postgresql@15 + Run Backend: -cd /Users/kjannette/workspace/koin_ping/backend-go -go run ./cmd/api \ No newline at end of file +cd /Users/kjannette/workspace/koin_ping/backend-go go run ./cmd/api diff --git a/frontend/README.md b/frontend/README.md index ff3f869..701613b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -5,24 +5,27 @@ React + Vite frontend for the on-chain monitoring and alerting system. ## Getting Started 1. Install dependencies: - ```bash - npm install - ``` + + ```bash + npm install + ``` 2. Run the development server: - ```bash - npm run dev - ``` + + ```bash + npm run dev + ``` 3. Build for production: - ```bash - npm run build - ``` + + ```bash + npm run build + ``` 4. Preview production build: - ```bash - npm run preview - ``` + ```bash + npm run preview + ``` ## Project Structure @@ -34,5 +37,5 @@ React + Vite frontend for the on-chain monitoring and alerting system. ## Development -The frontend runs on port 3000 and proxies API requests to the backend running on port 5000. - +The frontend runs on port 3000 and proxies API requests to the backend running +on port 5000. From 230dc99dba6b654886b54836010e31d9d0b11b32 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:08:17 -0500 Subject: [PATCH 12/14] Add .claude/ to .prettierignore Prevents prettier from checking Claude Code's settings file. --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 8ab4185..088f24d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,4 @@ frontend/build/ backend-go/bin/ *.lock Prompts/ +.claude/ From 90a338d46c8d86c431f4f01a289c1d059bf61f1c Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:17:55 -0500 Subject: [PATCH 13/14] fixed numerous linter issues --- backend-go/cmd/api/main.go | 47 +++++++++----- backend-go/cmd/poller/main.go | 33 +++++++--- backend-go/internal/config/config.go | 30 +++++++-- backend-go/internal/database/postgres.go | 31 +++++++-- backend-go/internal/domain/types.go | 65 ++++++++++++------- backend-go/internal/firebase/admin.go | 19 ++++-- backend-go/internal/handlers/address.go | 15 +++++ backend-go/internal/handlers/alert_event.go | 14 ++-- backend-go/internal/handlers/alert_rule.go | 44 +++++++++++-- .../internal/handlers/alert_rule_test.go | 38 +++++++++++ frontend/prettierrc | 4 ++ 11 files changed, 267 insertions(+), 73 deletions(-) create mode 100644 frontend/prettierrc diff --git a/backend-go/cmd/api/main.go b/backend-go/cmd/api/main.go index cef0028..d9a553b 100644 --- a/backend-go/cmd/api/main.go +++ b/backend-go/cmd/api/main.go @@ -1,10 +1,10 @@ +// Package main is the entry point for the API server. package main import ( "fmt" "log" "net/http" - "os" "github.com/joho/godotenv" "github.com/kjannette/koin-ping/backend-go/internal/config" @@ -15,6 +15,7 @@ import ( "github.com/kjannette/koin-ping/backend-go/internal/models" ) +//nolint:funlen func main() { _ = godotenv.Load() // .env is optional; env vars can also be set externally @@ -27,12 +28,13 @@ func main() { if err != nil { log.Fatalf("Failed to connect to database: %v", err) } - defer database.Close() if err := firebase.Init(cfg.FirebaseProjectID); err != nil { log.Fatalf("Failed to initialize Firebase: %v", err) } + defer database.Close() + addressModel := models.NewAddressModel(pool) alertRuleModel := models.NewAlertRuleModel(pool) alertEventModel := models.NewAlertEventModel(pool) @@ -51,23 +53,34 @@ func main() { mux.HandleFunc("GET "+b+"/status", handlers.SystemStatus) // Authenticated routes — addresses - mux.Handle("POST "+b+"/addresses", middleware.Authenticate(http.HandlerFunc(addressHandler.Create))) - mux.Handle("GET "+b+"/addresses", middleware.Authenticate(http.HandlerFunc(addressHandler.List))) - mux.Handle("DELETE "+b+"/addresses/{addressId}", middleware.Authenticate(http.HandlerFunc(addressHandler.Remove))) + mux.Handle("POST "+b+"/addresses", + middleware.Authenticate(http.HandlerFunc(addressHandler.Create))) + mux.Handle("GET "+b+"/addresses", + middleware.Authenticate(http.HandlerFunc(addressHandler.List))) + mux.Handle("DELETE "+b+"/addresses/{addressId}", + middleware.Authenticate(http.HandlerFunc(addressHandler.Remove))) // Authenticated routes — alert rules - mux.Handle("POST "+b+"/addresses/{addressId}/alerts", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.Create))) - mux.Handle("GET "+b+"/addresses/{addressId}/alerts", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.ListByAddress))) - mux.Handle("PATCH "+b+"/alerts/{alertId}", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.UpdateStatus))) - mux.Handle("DELETE "+b+"/alerts/{alertId}", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.Remove))) + mux.Handle("POST "+b+"/addresses/{addressId}/alerts", + middleware.Authenticate(http.HandlerFunc(alertRuleHandler.Create))) + mux.Handle("GET "+b+"/addresses/{addressId}/alerts", + middleware.Authenticate(http.HandlerFunc(alertRuleHandler.ListByAddress))) + mux.Handle("PATCH "+b+"/alerts/{alertId}", + middleware.Authenticate(http.HandlerFunc(alertRuleHandler.UpdateStatus))) + mux.Handle("DELETE "+b+"/alerts/{alertId}", + middleware.Authenticate(http.HandlerFunc(alertRuleHandler.Remove))) // Authenticated routes — alert events - mux.Handle("GET "+b+"/alert-events", middleware.Authenticate(http.HandlerFunc(alertEventHandler.List))) + mux.Handle("GET "+b+"/alert-events", + middleware.Authenticate(http.HandlerFunc(alertEventHandler.List))) // Authenticated routes — notification config - mux.Handle("GET "+b+"/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.GetConfig))) - mux.Handle("PUT "+b+"/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.UpdateConfig))) - mux.Handle("DELETE "+b+"/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.DeleteConfig))) + mux.Handle("GET "+b+"/notification-config", + middleware.Authenticate(http.HandlerFunc(notifConfigHandler.GetConfig))) + mux.Handle("PUT "+b+"/notification-config", + middleware.Authenticate(http.HandlerFunc(notifConfigHandler.UpdateConfig))) + mux.Handle("DELETE "+b+"/notification-config", + middleware.Authenticate(http.HandlerFunc(notifConfigHandler.DeleteConfig))) handler := corsMiddleware(mux) @@ -76,9 +89,12 @@ func main() { log.Printf("API base path: %s", cfg.APIBasePath) log.Printf("Environment: %s", cfg.NodeEnv) - if err := http.ListenAndServe(addr, handler); err != nil { + server := &http.Server{ + Addr: addr, + Handler: handler, + } + if err := server.ListenAndServe(); err != nil { log.Fatalf("Server failed: %v", err) - os.Exit(1) } } @@ -90,6 +106,7 @@ func corsMiddleware(next http.Handler) http.Handler { if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) + return } diff --git a/backend-go/cmd/poller/main.go b/backend-go/cmd/poller/main.go index 9c941b8..daffb3f 100644 --- a/backend-go/cmd/poller/main.go +++ b/backend-go/cmd/poller/main.go @@ -1,3 +1,4 @@ +// Package main is the entry point for the blockchain observer poller. package main import ( @@ -18,6 +19,14 @@ import ( "github.com/kjannette/koin-ping/backend-go/internal/services" ) +const ( + // separatorWidth is the number of characters in log separator lines. + separatorWidth = 60 + // msPerSecond converts milliseconds to seconds. + msPerSecond = 1000 +) + +//nolint:funlen func main() { _ = godotenv.Load() @@ -34,13 +43,14 @@ func main() { if err != nil { log.Fatalf("Failed to connect to database: %v", err) } - defer database.Close() eth, err := ethereum.NewJsonRpcEthereum(cfg.EthRPCURL) if err != nil { log.Fatalf("Failed to create Ethereum observer: %v", err) } + defer database.Close() + addressModel := models.NewAddressModel(pool) alertRuleModel := models.NewAlertRuleModel(pool) alertEventModel := models.NewAlertEventModel(pool) @@ -58,20 +68,20 @@ func main() { go func() { <-sigCh log.Println() - log.Println(strings.Repeat("=", 60)) + log.Println(strings.Repeat("=", separatorWidth)) log.Println("Shutting down poller gracefully...") - log.Println(strings.Repeat("=", 60)) + log.Println(strings.Repeat("=", separatorWidth)) cancel() }() interval := time.Duration(cfg.PollIntervalMS) * time.Millisecond - log.Println(strings.Repeat("=", 60)) + log.Println(strings.Repeat("=", separatorWidth)) log.Println("Koin Ping Observer Poller Starting") - log.Println(strings.Repeat("=", 60)) + log.Println(strings.Repeat("=", separatorWidth)) log.Printf("RPC URL: %s", cfg.EthRPCURL) - log.Printf("Poll Interval: %dms (%ds)", cfg.PollIntervalMS, cfg.PollIntervalMS/1000) - log.Println(strings.Repeat("=", 60)) + log.Printf("Poll Interval: %dms (%ds)", cfg.PollIntervalMS, cfg.PollIntervalMS/msPerSecond) + log.Println(strings.Repeat("=", separatorWidth)) runCycle(ctx, observer, evaluator) @@ -82,6 +92,7 @@ func main() { select { case <-ctx.Done(): log.Println("Poller stopped") + return case <-ticker.C: runCycle(ctx, observer, evaluator) @@ -89,19 +100,25 @@ func main() { } } -func runCycle(ctx context.Context, observer *services.ObserverService, evaluator *services.EvaluatorService) { +func runCycle( + ctx context.Context, + observer *services.ObserverService, + evaluator *services.EvaluatorService, +) { startTime := time.Now() log.Printf("[%s] Starting observation cycle...", time.Now().UTC().Format(time.RFC3339)) observations, err := observer.RunOnce(ctx) if err != nil { log.Printf("[%s] Observation cycle failed: %v", time.Now().UTC().Format(time.RFC3339), err) + return } alertsFired, err := evaluator.Evaluate(ctx, observations) if err != nil { log.Printf("[%s] Evaluation failed: %v", time.Now().UTC().Format(time.RFC3339), err) + return } diff --git a/backend-go/internal/config/config.go b/backend-go/internal/config/config.go index 2ee27f6..119c755 100644 --- a/backend-go/internal/config/config.go +++ b/backend-go/internal/config/config.go @@ -1,3 +1,4 @@ +// Package config loads environment-based configuration. package config import ( @@ -7,6 +8,18 @@ import ( "strings" ) +const ( + // defaultPort is the default HTTP server port. + defaultPort = 3001 + // defaultDBPort is the default PostgreSQL port. + defaultDBPort = 5432 + // defaultPollIntervalMS is the default poller interval in milliseconds. + defaultPollIntervalMS = 60000 + // minPollIntervalMS is the minimum allowed poller interval. + minPollIntervalMS = 1000 +) + +// Config holds application configuration. type Config struct { Port int APIBasePath string @@ -22,29 +35,31 @@ type Config struct { NodeEnv string } +// Load reads configuration from environment variables and returns a Config. func Load() (*Config, error) { cfg := &Config{ - Port: getEnvInt("PORT", 3001), + Port: getEnvInt("PORT", defaultPort), APIBasePath: getEnv("API_BASE_PATH", "/v1"), DatabaseURL: os.Getenv("DATABASE_URL"), DBHost: getEnv("DB_HOST", "localhost"), - DBPort: getEnvInt("DB_PORT", 5432), + DBPort: getEnvInt("DB_PORT", defaultDBPort), DBUser: os.Getenv("DB_USER"), DBPassword: os.Getenv("DB_PASSWORD"), DBName: os.Getenv("DB_NAME"), FirebaseProjectID: os.Getenv("FIREBASE_PROJECT_ID"), EthRPCURL: os.Getenv("ETH_RPC_URL"), - PollIntervalMS: getEnvInt("POLL_INTERVAL_MS", 60000), + PollIntervalMS: getEnvInt("POLL_INTERVAL_MS", defaultPollIntervalMS), NodeEnv: getEnv("NODE_ENV", "development"), } - if cfg.PollIntervalMS < 1000 { - return nil, fmt.Errorf("POLL_INTERVAL_MS must be >= 1000, got %d", cfg.PollIntervalMS) + if cfg.PollIntervalMS < minPollIntervalMS { + return nil, fmt.Errorf("POLL_INTERVAL_MS must be >= 1000, got %d", cfg.PollIntervalMS) //nolint:err113 } return cfg, nil } +// DSN returns the PostgreSQL data source name for the configured database. func (c *Config) DSN() string { if c.DatabaseURL != "" { // pgx defaults to sslmode=prefer, which fails against local Postgres. @@ -54,10 +69,13 @@ func (c *Config) DSN() string { if strings.Contains(c.DatabaseURL, "?") { sep = "&" } + return c.DatabaseURL + sep + "sslmode=disable" } + return c.DatabaseURL } + return fmt.Sprintf( "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName, @@ -68,6 +86,7 @@ func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } + return fallback } @@ -77,5 +96,6 @@ func getEnvInt(key string, fallback int) int { return n } } + return fallback } diff --git a/backend-go/internal/database/postgres.go b/backend-go/internal/database/postgres.go index 7ba1cef..d9832a2 100644 --- a/backend-go/internal/database/postgres.go +++ b/backend-go/internal/database/postgres.go @@ -1,3 +1,4 @@ +// Package database manages PostgreSQL connection pools. package database import ( @@ -9,20 +10,34 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -var pool *pgxpool.Pool +const ( + // maxConnIdleSeconds is the maximum idle time for a connection. + maxConnIdleSeconds = 30 + // maxConnLifetimeMinutes is the maximum lifetime for a connection. + maxConnLifetimeMinutes = 5 + // connectTimeoutSeconds is the timeout for initial connection. + connectTimeoutSeconds = 10 + // maxConns is the maximum number of connections in the pool. + maxConns = 20 + // minConns is the minimum number of connections in the pool. + minConns = 2 +) +var pool *pgxpool.Pool //nolint:gochecknoglobals + +// Connect establishes a PostgreSQL connection pool using the given DSN. func Connect(dsn string) (*pgxpool.Pool, error) { cfg, err := pgxpool.ParseConfig(dsn) if err != nil { return nil, fmt.Errorf("parse database config: %w", err) } - cfg.MaxConns = 20 - cfg.MinConns = 2 - cfg.MaxConnIdleTime = 30 * time.Second - cfg.MaxConnLifetime = 5 * time.Minute + cfg.MaxConns = maxConns + cfg.MinConns = minConns + cfg.MaxConnIdleTime = maxConnIdleSeconds * time.Second + cfg.MaxConnLifetime = maxConnLifetimeMinutes * time.Minute - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), connectTimeoutSeconds*time.Second) defer cancel() p, err := pgxpool.NewWithConfig(ctx, cfg) @@ -32,18 +47,22 @@ func Connect(dsn string) (*pgxpool.Pool, error) { if err := p.Ping(ctx); err != nil { p.Close() + return nil, fmt.Errorf("ping database: %w", err) } log.Println("Connected to PostgreSQL database") pool = p + return p, nil } +// Pool returns the global connection pool. func Pool() *pgxpool.Pool { return pool } +// Close closes the global connection pool. func Close() { if pool != nil { pool.Close() diff --git a/backend-go/internal/domain/types.go b/backend-go/internal/domain/types.go index 8efd13e..f98816e 100644 --- a/backend-go/internal/domain/types.go +++ b/backend-go/internal/domain/types.go @@ -1,17 +1,21 @@ +// Package domain defines core domain types shared across the application. package domain import "time" +// Address represents a tracked Ethereum address. type Address struct { ID int `json:"id"` - UserID string `json:"user_id"` + UserID string `json:"user_id"` //nolint:tagliatelle Address string `json:"address"` Label *string `json:"label"` - CreatedAt time.Time `json:"created_at"` + CreatedAt time.Time `json:"created_at"` //nolint:tagliatelle } +// AlertType identifies the kind of alert rule. type AlertType string +// Alert type constants define the supported alert triggers. const ( AlertIncomingTx AlertType = "incoming_tx" AlertOutgoingTx AlertType = "outgoing_tx" @@ -19,6 +23,7 @@ const ( AlertBalanceBelow AlertType = "balance_below" ) +// ValidAlertTypes lists all alert types accepted by the API. var ValidAlertTypes = []AlertType{ AlertIncomingTx, AlertOutgoingTx, @@ -26,90 +31,104 @@ var ValidAlertTypes = []AlertType{ AlertBalanceBelow, } +// ThresholdRequiredTypes lists alert types that require a threshold value. var ThresholdRequiredTypes = []AlertType{ AlertLargeTransfer, AlertBalanceBelow, } +// IsValidAlertType returns true if the given string matches a known AlertType. func IsValidAlertType(t string) bool { for _, v := range ValidAlertTypes { if string(v) == t { return true } } + return false } +// IsThresholdRequired returns true if the given AlertType requires a threshold. func IsThresholdRequired(t AlertType) bool { for _, v := range ThresholdRequiredTypes { if v == t { return true } } + return false } +// AlertRule represents a user-defined alert rule for an address. type AlertRule struct { ID int `json:"id"` - AddressID int `json:"address_id"` + AddressID int `json:"address_id"` //nolint:tagliatelle Type AlertType `json:"type"` Threshold *float64 `json:"threshold"` Enabled bool `json:"enabled"` - CreatedAt time.Time `json:"created_at"` + CreatedAt time.Time `json:"created_at"` //nolint:tagliatelle } +// AlertEvent represents a fired alert event stored for history. type AlertEvent struct { ID int `json:"id"` - AlertRuleID int `json:"alert_rule_id"` + AlertRuleID int `json:"alert_rule_id"` //nolint:tagliatelle Message string `json:"message"` - AddressLabel *string `json:"address_label"` - TxHash *string `json:"tx_hash"` + AddressLabel *string `json:"address_label"` //nolint:tagliatelle + TxHash *string `json:"tx_hash"` //nolint:tagliatelle Timestamp time.Time `json:"timestamp"` } +// AddressCheckpoint tracks the last block checked for an address. type AddressCheckpoint struct { - AddressID int `json:"address_id"` - LastCheckedBlock int `json:"last_checked_block"` - LastCheckedAt time.Time `json:"last_checked_at"` + AddressID int `json:"address_id"` //nolint:tagliatelle + LastCheckedBlock int `json:"last_checked_block"` //nolint:tagliatelle + LastCheckedAt time.Time `json:"last_checked_at"` //nolint:tagliatelle } +// CheckpointDetail combines checkpoint and address info for reporting. type CheckpointDetail struct { - AddressID int `json:"address_id"` + AddressID int `json:"address_id"` //nolint:tagliatelle Address string `json:"address"` Label *string `json:"label"` - LastCheckedBlock int `json:"last_checked_block"` - LastCheckedAt time.Time `json:"last_checked_at"` + LastCheckedBlock int `json:"last_checked_block"` //nolint:tagliatelle + LastCheckedAt time.Time `json:"last_checked_at"` //nolint:tagliatelle } +// NotificationConfig holds a user's notification preferences. type NotificationConfig struct { - UserID string `json:"user_id"` - DiscordWebhookURL *string `json:"discord_webhook_url"` - TelegramChatID *string `json:"telegram_chat_id"` - TelegramBotToken *string `json:"telegram_bot_token,omitempty"` + UserID string `json:"user_id"` //nolint:tagliatelle + DiscordWebhookURL *string `json:"discord_webhook_url"` //nolint:tagliatelle + TelegramChatID *string `json:"telegram_chat_id"` //nolint:tagliatelle + TelegramBotToken *string `json:"telegram_bot_token,omitempty"` //nolint:tagliatelle Email *string `json:"email"` - NotificationEnabled bool `json:"notification_enabled"` - CreatedAt *time.Time `json:"created_at,omitempty"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` + NotificationEnabled bool `json:"notification_enabled"` //nolint:tagliatelle + CreatedAt *time.Time `json:"created_at,omitempty"` //nolint:tagliatelle + UpdatedAt *time.Time `json:"updated_at,omitempty"` //nolint:tagliatelle } +// NormalizedTx is a blockchain transaction normalized for internal use. type NormalizedTx struct { Hash string `json:"hash"` From string `json:"from"` To *string `json:"to"` Value string `json:"value"` // Wei as string for precision - BlockNumber int `json:"block_number"` - BlockTimestamp int64 `json:"block_timestamp"` + BlockNumber int `json:"block_number"` //nolint:tagliatelle + BlockTimestamp int64 `json:"block_timestamp"` //nolint:tagliatelle } +// Direction indicates whether a transaction is incoming or outgoing. type Direction string +// Direction constants indicate the flow of a transaction relative to a watched address. const ( DirectionIncoming Direction = "incoming" DirectionOutgoing Direction = "outgoing" ) +// ObservedTx is a NormalizedTx enriched with address and direction context. type ObservedTx struct { NormalizedTx - AddressID int `json:"address_id"` + AddressID int `json:"address_id"` //nolint:tagliatelle Direction Direction `json:"direction"` } diff --git a/backend-go/internal/firebase/admin.go b/backend-go/internal/firebase/admin.go index 4903ba8..8997769 100644 --- a/backend-go/internal/firebase/admin.go +++ b/backend-go/internal/firebase/admin.go @@ -1,3 +1,4 @@ +// Package firebase provides Firebase authentication integration. package firebase import ( @@ -10,12 +11,13 @@ import ( "google.golang.org/api/option" ) -var ( - authClient *auth.Client - once sync.Once - initErr error +var ( //nolint:gochecknoglobals + authClient *auth.Client //nolint:gochecknoglobals + once sync.Once //nolint:gochecknoglobals + errInit error //nolint:gochecknoglobals ) +// Init initializes the Firebase app and auth client using the given project ID. func Init(projectID string) error { once.Do(func() { ctx := context.Background() @@ -30,20 +32,23 @@ func Init(projectID string) error { app, err = fb.NewApp(ctx, nil) } if err != nil { - initErr = fmt.Errorf("initialize firebase app: %w", err) + errInit = fmt.Errorf("initialize firebase app: %w", err) + return } authClient, err = app.Auth(ctx) if err != nil { - initErr = fmt.Errorf("initialize firebase auth: %w", err) + errInit = fmt.Errorf("initialize firebase auth: %w", err) + return } }) - return initErr + return errInit } +// Auth returns the initialized Firebase auth client. func Auth() *auth.Client { return authClient } diff --git a/backend-go/internal/handlers/address.go b/backend-go/internal/handlers/address.go index 07c8b91..f42946c 100644 --- a/backend-go/internal/handlers/address.go +++ b/backend-go/internal/handlers/address.go @@ -1,3 +1,4 @@ +// Package handlers implements HTTP request handlers for the API. package handlers import ( @@ -14,14 +15,17 @@ import ( var ethAddressRe = regexp.MustCompile(`^0x[a-fA-F0-9]{40}$`) +// AddressHandler handles HTTP requests for address management. type AddressHandler struct { addresses *models.AddressModel } +// NewAddressHandler creates a new AddressHandler. func NewAddressHandler(addresses *models.AddressModel) *AddressHandler { return &AddressHandler{addresses: addresses} } +// Create handles POST requests to add a new tracked address. func (h *AddressHandler) Create(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) @@ -32,16 +36,19 @@ func (h *AddressHandler) Create(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&body); err != nil { log.Printf("Failed to decode address request body: %v", err) writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request body") + return } if body.Address == "" { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Address is required") + return } if !ethAddressRe.MatchString(body.Address) { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid Ethereum address format") + return } @@ -51,10 +58,12 @@ func (h *AddressHandler) Create(w http.ResponseWriter, r *http.Request) { if err != nil { if strings.Contains(err.Error(), "23505") || strings.Contains(err.Error(), "unique") { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "You are already tracking this address") + return } log.Printf("Error creating address: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to create address") + return } @@ -62,6 +71,7 @@ func (h *AddressHandler) Create(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, addr) } +// List handles GET requests to list all tracked addresses for the current user. func (h *AddressHandler) List(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) log.Printf("User %s listing addresses", userID) @@ -70,6 +80,7 @@ func (h *AddressHandler) List(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Error listing addresses: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to list addresses") + return } @@ -81,11 +92,13 @@ func (h *AddressHandler) List(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, addresses) } +// Remove handles DELETE requests to remove a tracked address. func (h *AddressHandler) Remove(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) addressID, ok := parseIntParam(r.PathValue("addressId")) if !ok { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid address ID") + return } @@ -95,12 +108,14 @@ func (h *AddressHandler) Remove(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Error deleting address: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to delete address") + return } if !deleted { log.Printf("Address %d not found or not owned by user", addressID) writeError(w, http.StatusNotFound, "NOT_FOUND", "Address not found") + return } diff --git a/backend-go/internal/handlers/alert_event.go b/backend-go/internal/handlers/alert_event.go index 598e923..27aef80 100644 --- a/backend-go/internal/handlers/alert_event.go +++ b/backend-go/internal/handlers/alert_event.go @@ -11,14 +11,17 @@ import ( "github.com/kjannette/koin-ping/backend-go/internal/models" ) +// AlertEventHandler handles HTTP requests for alert event history. type AlertEventHandler struct { alertEvents *models.AlertEventModel } +// NewAlertEventHandler creates a new AlertEventHandler. func NewAlertEventHandler(alertEvents *models.AlertEventModel) *AlertEventHandler { return &AlertEventHandler{alertEvents: alertEvents} } +// List handles GET requests to list recent alert events for the current user. func (h *AlertEventHandler) List(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) @@ -34,6 +37,7 @@ func (h *AlertEventHandler) List(w http.ResponseWriter, r *http.Request) { if limit < 1 || limit > 100 { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Limit must be between 1 and 100") + return } @@ -41,6 +45,7 @@ func (h *AlertEventHandler) List(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Error listing alert events: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to list alert events") + return } @@ -67,15 +72,15 @@ func mockEvents(limit int) []domain.AlertEvent { Timestamp: time.Now().Add(-2 * time.Hour), }, { - ID: 2, - AlertRuleID: 2, + ID: 2, //nolint:mnd + AlertRuleID: 2, //nolint:mnd Message: "Balance dropped below threshold: Current balance 8.2 ETH", AddressLabel: &label1, Timestamp: time.Now().Add(-5 * time.Hour), }, { - ID: 3, - AlertRuleID: 3, + ID: 3, //nolint:mnd + AlertRuleID: 3, //nolint:mnd Message: "Outgoing transaction detected: 2.0 ETH sent", AddressLabel: &label2, Timestamp: time.Now().Add(-24 * time.Hour), @@ -85,5 +90,6 @@ func mockEvents(limit int) []domain.AlertEvent { if limit < len(mocks) { return mocks[:limit] } + return mocks } diff --git a/backend-go/internal/handlers/alert_rule.go b/backend-go/internal/handlers/alert_rule.go index 1c77880..c6ebb22 100644 --- a/backend-go/internal/handlers/alert_rule.go +++ b/backend-go/internal/handlers/alert_rule.go @@ -2,6 +2,7 @@ package handlers import ( "encoding/json" + "errors" "fmt" "log" "net/http" @@ -13,20 +14,27 @@ import ( "github.com/kjannette/koin-ping/backend-go/internal/models" ) +// errThresholdFormat is returned when the threshold JSON cannot be decoded. +var errThresholdFormat = errors.New("unsupported threshold format") + +// AlertRuleHandler handles HTTP requests for alert rule management. type AlertRuleHandler struct { alertRules *models.AlertRuleModel addresses *models.AddressModel } +// NewAlertRuleHandler creates a new AlertRuleHandler. func NewAlertRuleHandler(alertRules *models.AlertRuleModel, addresses *models.AddressModel) *AlertRuleHandler { return &AlertRuleHandler{alertRules: alertRules, addresses: addresses} } +// Create handles POST requests to create a new alert rule for an address. func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) addressID, ok := parseIntParam(r.PathValue("addressId")) if !ok { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid address ID") + return } @@ -37,6 +45,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&body); err != nil { log.Printf("Failed to decode alert request body: %v", err) writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request body") + return } @@ -44,6 +53,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Failed to parse threshold: %v", err) writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "threshold must be a valid number") + return } @@ -51,6 +61,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { if body.Type == "" { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Alert type is required") + return } @@ -60,7 +71,8 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { types[i] = string(t) } writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", - fmt.Sprintf("Invalid alert type. Must be one of: %s", strings.Join(types, ", "))) + "Invalid alert type. Must be one of: "+strings.Join(types, ", ")) + return } @@ -69,6 +81,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { if threshold == nil || *threshold <= 0 { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", fmt.Sprintf("Alert type '%s' requires a positive threshold value", body.Type)) + return } } @@ -77,11 +90,13 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Error finding address: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to create alert rule") + return } if addr == nil { log.Printf("Address %d not found or not owned by user", addressID) writeError(w, http.StatusNotFound, "NOT_FOUND", "Address not found") + return } @@ -89,6 +104,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Error creating alert rule: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to create alert rule") + return } @@ -98,12 +114,12 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { func parseThreshold(raw json.RawMessage) (*float64, error) { if len(raw) == 0 { - return nil, nil + return nil, nil //nolint:nilnil } // Check null before number -- json.Unmarshal treats null as valid for float64 (sets to 0). if string(raw) == "null" { - return nil, nil + return nil, nil //nolint:nilnil } var asNumber float64 @@ -115,23 +131,26 @@ func parseThreshold(raw json.RawMessage) (*float64, error) { if err := json.Unmarshal(raw, &asString); err == nil { asString = strings.TrimSpace(asString) if asString == "" { - return nil, nil + return nil, nil //nolint:nilnil } parsed, parseErr := strconv.ParseFloat(asString, 64) if parseErr != nil { return nil, parseErr } + return &parsed, nil } - return nil, fmt.Errorf("unsupported threshold format") + return nil, errThresholdFormat } +// ListByAddress handles GET requests to list alert rules for an address. func (h *AlertRuleHandler) ListByAddress(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) addressID, ok := parseIntParam(r.PathValue("addressId")) if !ok { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid address ID") + return } @@ -141,11 +160,13 @@ func (h *AlertRuleHandler) ListByAddress(w http.ResponseWriter, r *http.Request) if err != nil { log.Printf("Error finding address: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to list alerts") + return } if addr == nil { log.Printf("Address %d not found or not owned by user", addressID) writeError(w, http.StatusNotFound, "NOT_FOUND", "Address not found") + return } @@ -153,6 +174,7 @@ func (h *AlertRuleHandler) ListByAddress(w http.ResponseWriter, r *http.Request) if err != nil { log.Printf("Error listing alerts: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to list alerts") + return } @@ -164,11 +186,13 @@ func (h *AlertRuleHandler) ListByAddress(w http.ResponseWriter, r *http.Request) writeJSON(w, http.StatusOK, alerts) } +// UpdateStatus handles PATCH requests to enable or disable an alert rule. func (h *AlertRuleHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) alertID, ok := parseIntParam(r.PathValue("alertId")) if !ok { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid alert ID") + return } @@ -178,6 +202,7 @@ func (h *AlertRuleHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) if err := json.NewDecoder(r.Body).Decode(&body); err != nil { log.Printf("Failed to decode update request body: %v", err) writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request body") + return } @@ -185,6 +210,7 @@ func (h *AlertRuleHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) if body.Enabled == nil { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "enabled must be a boolean value") + return } @@ -192,11 +218,13 @@ func (h *AlertRuleHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) if err != nil { log.Printf("Error finding alert: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to update alert") + return } if alert == nil { log.Printf("Alert %d not found or not owned by user", alertID) writeError(w, http.StatusNotFound, "NOT_FOUND", "Alert rule not found") + return } @@ -204,6 +232,7 @@ func (h *AlertRuleHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) if err != nil { log.Printf("Error updating alert: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to update alert") + return } @@ -211,11 +240,13 @@ func (h *AlertRuleHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) writeJSON(w, http.StatusOK, updated) } +// Remove handles DELETE requests to remove an alert rule. func (h *AlertRuleHandler) Remove(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) alertID, ok := parseIntParam(r.PathValue("alertId")) if !ok { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid alert ID") + return } @@ -225,17 +256,20 @@ func (h *AlertRuleHandler) Remove(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Error finding alert: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to delete alert") + return } if alert == nil { log.Printf("Alert %d not found or not owned by user", alertID) writeError(w, http.StatusNotFound, "NOT_FOUND", "Alert rule not found") + return } if _, err := h.alertRules.Remove(r.Context(), alertID); err != nil { log.Printf("Error deleting alert: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to delete alert") + return } diff --git a/backend-go/internal/handlers/alert_rule_test.go b/backend-go/internal/handlers/alert_rule_test.go index b9a2d50..ea7ac88 100644 --- a/backend-go/internal/handlers/alert_rule_test.go +++ b/backend-go/internal/handlers/alert_rule_test.go @@ -1,3 +1,4 @@ +//nolint:testpackage // parseThreshold is unexported; internal test package required package handlers import ( @@ -6,8 +7,13 @@ import ( "testing" ) +//nolint:gocognit func TestParseThreshold(t *testing.T) { + t.Parallel() + t.Run("nil/empty raw message returns nil", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(nil) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -18,6 +24,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("empty slice returns nil", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage{}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -28,6 +36,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("JSON null returns nil", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage("null")) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -38,6 +48,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("number 10 returns 10.0", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage("10")) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -51,6 +63,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("number 0.5 returns 0.5", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage("0.5")) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -61,6 +75,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("string '10' returns 10.0", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage(`"10"`)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -71,6 +87,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("string '0.001' returns 0.001", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage(`"0.001"`)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -81,6 +99,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("string with spaces ' 10 ' returns 10.0", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage(`" 10 "`)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -91,6 +111,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("empty string returns nil", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage(`""`)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -101,6 +123,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("whitespace-only string returns nil", func(t *testing.T) { + t.Parallel() + val, err := parseThreshold(json.RawMessage(`" "`)) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -111,6 +135,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("invalid string returns error", func(t *testing.T) { + t.Parallel() + _, err := parseThreshold(json.RawMessage(`"abc"`)) if err == nil { t.Fatal("expected error for non-numeric string") @@ -118,6 +144,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("boolean returns error", func(t *testing.T) { + t.Parallel() + _, err := parseThreshold(json.RawMessage("true")) if err == nil { t.Fatal("expected error for boolean") @@ -125,6 +153,8 @@ func TestParseThreshold(t *testing.T) { }) t.Run("array returns error", func(t *testing.T) { + t.Parallel() + _, err := parseThreshold(json.RawMessage("[1,2]")) if err == nil { t.Fatal("expected error for array") @@ -132,7 +162,10 @@ func TestParseThreshold(t *testing.T) { }) } +//nolint:funlen func TestDecodeAlertBody(t *testing.T) { + t.Parallel() + // Verifies that the struct used in Create handler can decode all // payload shapes the frontend might send. @@ -183,12 +216,15 @@ func TestDecodeAlertBody(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var body alertBody err := json.Unmarshal([]byte(tt.payload), &body) if tt.wantErr { if err == nil { t.Fatal("expected decode error") } + return } if err != nil { @@ -199,6 +235,8 @@ func TestDecodeAlertBody(t *testing.T) { } func TestOldStructFailsWithStringThreshold(t *testing.T) { + t.Parallel() + // Documents the original bug: *float64 cannot decode a string threshold. type oldAlertBody struct { Type string `json:"type"` diff --git a/frontend/prettierrc b/frontend/prettierrc new file mode 100644 index 0000000..b00b440 --- /dev/null +++ b/frontend/prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "proseWrap": "always" +} \ No newline at end of file From 7357315810f5964380ae8c8d3df535a1f76c050c Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:42:57 -0500 Subject: [PATCH 14/14] Cleanup --- backend-go/cmd/api/main.go | 14 ++++++-- backend-go/internal/config/config.go | 9 +---- backend-go/internal/domain/types.go | 10 ++++-- backend-go/internal/handlers/alert_rule.go | 2 +- backend-go/internal/middleware/auth.go | 1 + backend-go/internal/models/address.go | 4 ++- backend-go/internal/models/alert_rule.go | 8 +++-- backend-go/internal/models/checkpoint.go | 4 ++- .../internal/models/notification_config.go | 4 ++- backend-go/internal/notifications/discord.go | 34 +++++++++++++++---- backend-go/internal/services/evaluator.go | 6 +++- 11 files changed, 69 insertions(+), 27 deletions(-) diff --git a/backend-go/cmd/api/main.go b/backend-go/cmd/api/main.go index d9a553b..7610e23 100644 --- a/backend-go/cmd/api/main.go +++ b/backend-go/cmd/api/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "time" "github.com/joho/godotenv" "github.com/kjannette/koin-ping/backend-go/internal/config" @@ -15,6 +16,13 @@ import ( "github.com/kjannette/koin-ping/backend-go/internal/models" ) +const ( + // serverReadTimeoutSeconds is the maximum duration to read a request. + serverReadTimeoutSeconds = 5 + // serverWriteTimeoutSeconds is the maximum duration to write a response. + serverWriteTimeoutSeconds = 10 +) + //nolint:funlen func main() { _ = godotenv.Load() // .env is optional; env vars can also be set externally @@ -90,8 +98,10 @@ func main() { log.Printf("Environment: %s", cfg.NodeEnv) server := &http.Server{ - Addr: addr, - Handler: handler, + Addr: addr, + Handler: handler, + ReadTimeout: serverReadTimeoutSeconds * time.Second, + WriteTimeout: serverWriteTimeoutSeconds * time.Second, } if err := server.ListenAndServe(); err != nil { log.Fatalf("Server failed: %v", err) diff --git a/backend-go/internal/config/config.go b/backend-go/internal/config/config.go index 119c755..8097158 100644 --- a/backend-go/internal/config/config.go +++ b/backend-go/internal/config/config.go @@ -1,4 +1,4 @@ -// Package config loads environment-based configuration. +//loads environment-based configuration. package config import ( @@ -9,17 +9,12 @@ import ( ) const ( - // defaultPort is the default HTTP server port. defaultPort = 3001 - // defaultDBPort is the default PostgreSQL port. defaultDBPort = 5432 - // defaultPollIntervalMS is the default poller interval in milliseconds. defaultPollIntervalMS = 60000 - // minPollIntervalMS is the minimum allowed poller interval. minPollIntervalMS = 1000 ) -// Config holds application configuration. type Config struct { Port int APIBasePath string @@ -59,10 +54,8 @@ func Load() (*Config, error) { return cfg, nil } -// DSN returns the PostgreSQL data source name for the configured database. func (c *Config) DSN() string { if c.DatabaseURL != "" { - // pgx defaults to sslmode=prefer, which fails against local Postgres. // Append sslmode=disable if not already specified. if !strings.Contains(c.DatabaseURL, "sslmode=") { sep := "?" diff --git a/backend-go/internal/domain/types.go b/backend-go/internal/domain/types.go index f98816e..8b10330 100644 --- a/backend-go/internal/domain/types.go +++ b/backend-go/internal/domain/types.go @@ -15,6 +15,9 @@ type Address struct { // AlertType identifies the kind of alert rule. type AlertType string +// String implements fmt.Stringer. +func (a AlertType) String() string { return string(a) } + // Alert type constants define the supported alert triggers. const ( AlertIncomingTx AlertType = "incoming_tx" @@ -24,7 +27,7 @@ const ( ) // ValidAlertTypes lists all alert types accepted by the API. -var ValidAlertTypes = []AlertType{ +var ValidAlertTypes = []AlertType{ //nolint:gochecknoglobals AlertIncomingTx, AlertOutgoingTx, AlertLargeTransfer, @@ -32,7 +35,7 @@ var ValidAlertTypes = []AlertType{ } // ThresholdRequiredTypes lists alert types that require a threshold value. -var ThresholdRequiredTypes = []AlertType{ +var ThresholdRequiredTypes = []AlertType{ //nolint:gochecknoglobals AlertLargeTransfer, AlertBalanceBelow, } @@ -120,6 +123,9 @@ type NormalizedTx struct { // Direction indicates whether a transaction is incoming or outgoing. type Direction string +// String implements fmt.Stringer. +func (d Direction) String() string { return string(d) } + // Direction constants indicate the flow of a transaction relative to a watched address. const ( DirectionIncoming Direction = "incoming" diff --git a/backend-go/internal/handlers/alert_rule.go b/backend-go/internal/handlers/alert_rule.go index c6ebb22..eb2888b 100644 --- a/backend-go/internal/handlers/alert_rule.go +++ b/backend-go/internal/handlers/alert_rule.go @@ -68,7 +68,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { if !domain.IsValidAlertType(body.Type) { types := make([]string, len(domain.ValidAlertTypes)) for i, t := range domain.ValidAlertTypes { - types[i] = string(t) + types[i] = t.String() } writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid alert type. Must be one of: "+strings.Join(types, ", ")) diff --git a/backend-go/internal/middleware/auth.go b/backend-go/internal/middleware/auth.go index 2bce577..c06784c 100644 --- a/backend-go/internal/middleware/auth.go +++ b/backend-go/internal/middleware/auth.go @@ -1,3 +1,4 @@ +// Package middleware provides HTTP middleware for authentication and context injection. package middleware import ( diff --git a/backend-go/internal/models/address.go b/backend-go/internal/models/address.go index ef4125d..fd1c8f9 100644 --- a/backend-go/internal/models/address.go +++ b/backend-go/internal/models/address.go @@ -2,7 +2,9 @@ package models import ( "context" + "errors" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/koin-ping/backend-go/internal/domain" ) @@ -97,7 +99,7 @@ func (m *AddressModel) FindByID(ctx context.Context, id int, userID *string) (*d } if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err diff --git a/backend-go/internal/models/alert_rule.go b/backend-go/internal/models/alert_rule.go index ea45e45..809009b 100644 --- a/backend-go/internal/models/alert_rule.go +++ b/backend-go/internal/models/alert_rule.go @@ -2,7 +2,9 @@ package models import ( "context" + "errors" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/koin-ping/backend-go/internal/domain" ) @@ -21,7 +23,7 @@ func (m *AlertRuleModel) Create(ctx context.Context, addressID int, alertType do `INSERT INTO alert_rules (address_id, type, threshold, enabled) VALUES ($1, $2, $3, TRUE) RETURNING id, address_id, type, threshold, enabled, created_at`, - addressID, string(alertType), threshold, + addressID, alertType.String(), threshold, ).Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt) if err != nil { return nil, err @@ -75,7 +77,7 @@ func (m *AlertRuleModel) FindByID(ctx context.Context, id int, userID *string) ( } if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err @@ -93,7 +95,7 @@ func (m *AlertRuleModel) UpdateEnabled(ctx context.Context, id int, enabled bool id, enabled, ).Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt) if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err diff --git a/backend-go/internal/models/checkpoint.go b/backend-go/internal/models/checkpoint.go index 94008b9..c6c60d6 100644 --- a/backend-go/internal/models/checkpoint.go +++ b/backend-go/internal/models/checkpoint.go @@ -2,7 +2,9 @@ package models import ( "context" + "errors" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/koin-ping/backend-go/internal/domain" ) @@ -23,7 +25,7 @@ func (m *CheckpointModel) GetLastCheckedBlock(ctx context.Context, addressID int addressID, ).Scan(&block) if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return 0, false, nil } return 0, false, err diff --git a/backend-go/internal/models/notification_config.go b/backend-go/internal/models/notification_config.go index 7e6b175..4a43a63 100644 --- a/backend-go/internal/models/notification_config.go +++ b/backend-go/internal/models/notification_config.go @@ -2,7 +2,9 @@ package models import ( "context" + "errors" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/koin-ping/backend-go/internal/domain" ) @@ -26,7 +28,7 @@ func (m *NotificationConfigModel) GetConfig(ctx context.Context, userID string) ).Scan(&c.UserID, &c.DiscordWebhookURL, &c.TelegramChatID, &c.TelegramBotToken, &c.Email, &c.NotificationEnabled, &c.CreatedAt, &c.UpdatedAt) if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err diff --git a/backend-go/internal/notifications/discord.go b/backend-go/internal/notifications/discord.go index 128a8a3..b1bb788 100644 --- a/backend-go/internal/notifications/discord.go +++ b/backend-go/internal/notifications/discord.go @@ -9,6 +9,22 @@ import ( "time" ) +const ( + // discordHTTPTimeoutSeconds is the timeout for Discord webhook requests. + discordHTTPTimeoutSeconds = 10 + + // Alert color codes for Discord embeds. + colorGreen = 0x00ff00 + colorOrange = 0xff9900 + colorRed = 0xff0000 + colorBlue = 0x0099ff +) + +// discordHTTPClient is a shared HTTP client with a timeout for Discord requests. +var discordHTTPClient = &http.Client{ //nolint:gochecknoglobals + Timeout: discordHTTPTimeoutSeconds * time.Second, +} + type AlertMetadata struct { TxHash string AddressLabel string @@ -73,7 +89,9 @@ func SendDiscordNotification(webhookURL, message string, meta AlertMetadata) (bo return false, fmt.Errorf("marshal discord payload: %w", err) } - resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body)) + resp, err := discordHTTPClient.Post( + webhookURL, "application/json", bytes.NewReader(body), + ) if err != nil { log.Printf("Failed to send Discord notification: %v", err) return false, err @@ -98,7 +116,9 @@ func TestDiscordWebhook(webhookURL string) (bool, error) { return false, err } - resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body)) + resp, err := discordHTTPClient.Post( + webhookURL, "application/json", bytes.NewReader(body), + ) if err != nil { log.Printf("Discord webhook test failed: %v", err) return false, err @@ -111,14 +131,14 @@ func TestDiscordWebhook(webhookURL string) (bool, error) { func colorForAlertType(alertType string) int { switch alertType { case "incoming_tx": - return 0x00ff00 // Green + return colorGreen case "outgoing_tx": - return 0xff9900 // Orange + return colorOrange case "large_transfer": - return 0xff0000 // Red + return colorRed case "balance_below": - return 0xff0000 // Red + return colorRed default: - return 0x0099ff // Blue + return colorBlue } } diff --git a/backend-go/internal/services/evaluator.go b/backend-go/internal/services/evaluator.go index 9dfc9b2..0e30d35 100644 --- a/backend-go/internal/services/evaluator.go +++ b/backend-go/internal/services/evaluator.go @@ -170,7 +170,11 @@ func (s *EvaluatorService) fireAlert(ctx context.Context, rule domain.AlertRule, // Send Discord notification (non-fatal on failure) if addr != nil { - go s.sendNotification(ctx, addr.UserID, message, obs, addressLabel, rule, addr.Address) + go func() { + s.sendNotification( + ctx, addr.UserID, message, obs, addressLabel, rule, addr.Address, + ) + }() } return nil